import { test } from '@affine-test/kit/playwright'; import { importFile } from '@affine-test/kit/utils/attachment'; import { pasteContent } from '@affine-test/kit/utils/clipboard'; import { clickEdgelessModeButton, clickPageModeButton, clickView, getCodeBlockIds, getParagraphIds, locateEditorContainer, toViewCoord, } from '@affine-test/kit/utils/editor'; import { copyByKeyboard, cutByKeyboard, pasteByKeyboard, pressEnter, } from '@affine-test/kit/utils/keyboard'; import { openHomePage } from '@affine-test/kit/utils/load-page'; import { addCodeBlock, clickNewPageButton, type, waitForEditorLoad, } from '@affine-test/kit/utils/page-logic'; import { setSelection } from '@affine-test/kit/utils/selection'; import type { CodeBlockComponent } from '@blocksuite/affine-block-code'; import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph'; import type { PageRootBlockComponent } from '@blocksuite/affine-block-root'; import type { BlockComponent } from '@blocksuite/std'; import { expect, type Page } from '@playwright/test'; const paragraphLocator = 'affine-note affine-paragraph'; const codeBlockLocator = 'affine-note affine-code'; // Helper function to create paragraph blocks with text async function createParagraphBlocks(page: Page, texts: string[]) { for (const text of texts) { await pressEnter(page); await type(page, text); } } // Helper function to verify block text content async function verifyBlockContent( page: Page, selector: string, index: number, expectedText: string ) { expect( await page .locator(selector) .nth(index) .evaluate((block: T, expected: string) => { const model = block.model; // Check if model has text property if (!('text' in model)) return false; const text = model.text; // Check if text exists and has toString method return text && text.toString() === expected; }, expectedText) ).toBeTruthy(); } // Helper functions using the generic verifyBlockContent async function verifyParagraphContent( page: Page, index: number, expectedText: string ) { await verifyBlockContent( page, paragraphLocator, index, expectedText ); } async function verifyCodeBlockContent( page: Page, index: number, expectedText: string ) { await verifyBlockContent( page, codeBlockLocator, index, expectedText ); } test.beforeEach(async ({ page }) => { await openHomePage(page); await clickNewPageButton(page, 'Clipboard Test'); await waitForEditorLoad(page); }); test.describe('paste in multiple blocks text selection', () => { test('paste plain text', async ({ page }) => { const texts = ['hello world', 'hello world', 'hello world']; await createParagraphBlocks(page, texts); const { blockIds: paragraphIds } = await getParagraphIds(page); /** * select text cross the 3 blocks: * hello |world * hello world * hello| world */ await setSelection(page, paragraphIds[0], 6, paragraphIds[2], 5); await pasteContent(page, { 'text/plain': 'test' }); /** * after paste: * hello test world */ await verifyParagraphContent(page, 0, 'hello test world'); }); test('paste snapshot', async ({ page }) => { // Create initial test blocks await createParagraphBlocks(page, ['test', 'test']); // Create target blocks await createParagraphBlocks(page, [ 'hello world', 'hello world', 'hello world', ]); /** * before paste: * test * test * hello world * hello world * hello world */ const { blockIds: paragraphIds } = await getParagraphIds(page); /** * select the first 2 blocks: * |test * test| * hello world * hello world * hello world */ await setSelection(page, paragraphIds[0], 0, paragraphIds[1], 4); // copy the first 2 blocks await copyByKeyboard(page); /** * select the last 3 blocks: * test * test * hello |world * hello world * hello| world */ await setSelection(page, paragraphIds[2], 6, paragraphIds[4], 5); // paste the first 2 blocks await pasteByKeyboard(page); await page.waitForTimeout(100); /** * after paste: * test * test * hello test * test world */ await verifyParagraphContent(page, 2, 'hello test'); await verifyParagraphContent(page, 3, 'test world'); }); }); test.describe('surface-ref block', () => { async function setupSurfaceRefBlock(page: Page) { await clickEdgelessModeButton(page); const container = locateEditorContainer(page); await container.click(); // add a shape await page.keyboard.press('s'); await container.click({ position: { x: 100, y: 300 } }); await page.waitForTimeout(50); // add a frame await page.keyboard.press('f'); await page.waitForTimeout(50); // click on the frame title to trigger the change frame button toolbar const frameTitle = page.locator('affine-frame-title'); await frameTitle.click(); await page.waitForTimeout(50); const toolbar = page.locator('affine-toolbar-widget editor-toolbar'); const insertIntoPageButton = toolbar.getByLabel('Insert into Page'); await insertIntoPageButton.click(); await clickPageModeButton(page); await waitForEditorLoad(page); await container.click(); return { container }; } test('paste surface-ref block to another doc as embed-linked-doc block', async ({ page, }) => { await setupSurfaceRefBlock(page); // copy surface-ref block const surfaceRefBlock = page.locator('affine-surface-ref'); await surfaceRefBlock.click(); await page.waitForSelector('affine-surface-ref .focused'); await copyByKeyboard(page); // paste to another doc await clickNewPageButton(page, 'page2'); await pressEnter(page); // paste the surface-ref block await pasteByKeyboard(page); await page.waitForTimeout(50); const embedLinkedDocBlock = page.locator('affine-embed-linked-doc-block'); await expect(embedLinkedDocBlock).toBeVisible(); const embedLinkedDocBlockTitle = embedLinkedDocBlock.locator( '.affine-embed-linked-doc-content-title-text' ); await expect(embedLinkedDocBlockTitle).toHaveText('Clipboard Test'); }); test('cut and paste surface-ref block to same doc should remain surface-ref block', async ({ page, }) => { const { container } = await setupSurfaceRefBlock(page); // cut surface-ref block const surfaceRefBlock = page.locator('affine-surface-ref'); await surfaceRefBlock.click(); await page.waitForSelector('affine-surface-ref .focused'); await cutByKeyboard(page); // focus on the editor await container.click(); // paste the surface-ref block await pasteByKeyboard(page); await page.waitForTimeout(50); await expect(surfaceRefBlock).toHaveCount(1); await expect(surfaceRefBlock).toBeVisible(); }); }); test.describe('paste to code block', () => { test('should replace the selected text when pasting plain text', async ({ page, }) => { // press enter to focus on the first block of the editor await pressEnter(page); await addCodeBlock(page); await type(page, 'hello world hello'); const { blockIds: codeBlockIds } = await getCodeBlockIds(page); await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11); await pasteContent(page, { 'text/plain': 'test' }); await verifyCodeBlockContent(page, 0, 'hello test hello'); }); test('should replace the selected text when pasting single snapshot', async ({ page, }) => { // Create initial test blocks // add a paragraph block await createParagraphBlocks(page, ['test']); // add a code block await pressEnter(page); await addCodeBlock(page); await type(page, 'hello world hello'); // select the paragraph content const { blockIds: paragraphIds } = await getParagraphIds(page); await setSelection(page, paragraphIds[0], 0, paragraphIds[0], 4); // copy the paragraph content await copyByKeyboard(page); // select 'world' in the code block const { blockIds: codeBlockIds } = await getCodeBlockIds(page); await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11); // paste to the code block await pasteByKeyboard(page); await page.waitForTimeout(100); await verifyCodeBlockContent(page, 0, 'hello test hello'); }); test('should replace the selected text when pasting multiple snapshots', async ({ page, }) => { // Create initial test blocks // add three paragraph blocks await createParagraphBlocks(page, ['test', 'test', 'test']); // add a code block await pressEnter(page); await addCodeBlock(page); await type(page, 'hello world hello'); // select all paragraph content const { blockIds: paragraphIds } = await getParagraphIds(page); await setSelection(page, paragraphIds[0], 0, paragraphIds[2], 4); // copy the paragraph content await copyByKeyboard(page); // select 'world' in the code block const { blockIds: codeBlockIds } = await getCodeBlockIds(page); await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11); // paste to the code block await pasteByKeyboard(page); await page.waitForTimeout(100); await verifyCodeBlockContent(page, 0, 'hello test\ntest\ntest hello'); }); test('should preserve indentation when pasting code with spaces into code block', async ({ page, }) => { await pressEnter(page); await addCodeBlock(page); // Sample code with proper indentation for text/plain const plainTextCode = [ 'const fibonacci = (n: number): number => {', ' if (n <= 1) return n;', ' return fibonacci(n - 1) + fibonacci(n - 2);', '}', 'function generateSequence(length: number) {', ' const sequence = [];', ' for (let i = 0; i < length; i++) {', ' sequence.push(fibonacci(i));', ' }', ' return sequence;', '}', ].join('\n'); const htmlCode = '
const fibonacci = (n: number): number => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
function generateSequence(length: number) {
const sequence = [];
for (let i = 0; i < length; i++) {
sequence.push(fibonacci(i));
}
return sequence;
}
'; await pasteContent(page, { 'text/plain': plainTextCode, 'text/html': htmlCode, }); await page.waitForTimeout(100); // Verify the pasted code maintains indentation await verifyCodeBlockContent(page, 0, plainTextCode); }); test('html tag should be treated as plain text when pasting', async ({ page, }) => { await pressEnter(page); await addCodeBlock(page); const textWithHtmlTags = '
const fibonacci = (n: number): number => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
function generateSequence(length: number) {
const sequence = [];
for (let i = 0; i < length; i++) {
sequence.push(fibonacci(i));
}
return sequence;
}
'; await pasteContent(page, { 'text/plain': textWithHtmlTags }); await page.waitForTimeout(100); // Verify the pasted code maintains indentation await verifyCodeBlockContent(page, 0, textWithHtmlTags); }); test('should not wrap line in brackets when pasting code', async ({ page, }) => { await pressEnter(page); await addCodeBlock(page); const plainTextCode = [ ' model: anthropic("claude-3-7-sonnet-20250219"),', ' prompt: How many people will live in the world in 2040?', ' providerOptions: {', ' anthropic: {', ' thinking: { type: enabled, budgetTokens: 12000 },', ' } satisfies AnthropicProviderOptions,', ' },', ].join('\n'); await pasteContent(page, { 'text/plain': plainTextCode }); await page.waitForTimeout(100); // Verify the pasted code maintains indentation await verifyCodeBlockContent(page, 0, plainTextCode); }); test('should paste markdown text as plain text', async ({ page }) => { await pressEnter(page); await addCodeBlock(page); const markdownText = [ '# Heading 1', '', '## Heading 2 with **bold** and *italic*', '', '### Lists:', '- Item 1', ' - Nested item with `inline code`', ' - Another nested item', '- Item 2 with [link](https://example.com)', '', '```typescript', 'const code = "block";', 'console.log(code);', '```', '', '> This is a blockquote with **bold** text', '> Multiple lines in blockquote', '', '| Table | Header |', '|-------|--------|', '| Cell 1 | Cell 2 |', '$This is a inline latex$', ].join('\n'); await pasteContent(page, { 'text/plain': markdownText }); await page.waitForTimeout(100); // Verify the pasted code maintains indentation await verifyCodeBlockContent(page, 0, markdownText); }); }); test.describe('paste in readonly mode', () => { test('should not paste content when document is in readonly mode', async ({ page, }) => { await createParagraphBlocks(page, ['This is a test paragraph']); const { blockIds } = await getParagraphIds(page); const initialParagraphCount = blockIds.length; await page.evaluate(() => { const pageRoot = document.querySelector( 'affine-page-root' ) as PageRootBlockComponent; pageRoot.store.readonly = true; }); await setSelection(page, blockIds[0], 0, blockIds[0], 4); await pasteContent(page, { 'text/plain': ' - Added text that should not appear', }); await verifyParagraphContent(page, 0, 'This is a test paragraph'); await pressEnter(page); await pasteContent(page, { 'text/plain': 'This should not be pasted' }); const { blockIds: afterParagraphIds } = await getParagraphIds(page); expect(afterParagraphIds.length).toBe(initialParagraphCount); await setSelection(page, blockIds[0], 0, blockIds[0], 4); await pasteByKeyboard(page); await verifyParagraphContent(page, 0, 'This is a test paragraph'); }); }); test('should copy single image from edgeless and paste to page', async ({ page, }) => { await clickEdgelessModeButton(page); const button = page.locator('edgeless-mindmap-tool-button'); await button.click(); const menu = page.locator('edgeless-mindmap-menu'); const mediaItem = menu.locator('.media-item'); await mediaItem.click(); await importFile(page, 'large-image.png', async () => { await toViewCoord(page, [100, 250]); await clickView(page, [100, 250]); }); const image = page.locator('affine-edgeless-image').first(); await image.click(); await copyByKeyboard(page); await clickPageModeButton(page); await waitForEditorLoad(page); const container = locateEditorContainer(page); await container.click(); await pasteByKeyboard(page); await expect(page.locator('affine-page-image')).toBeVisible(); });