import { expect } from '@playwright/test'; import { activeEmbed, activeNoteInEdgeless, addNoteByClick, click, copyByKeyboard, dragBetweenCoords, dragBetweenIndices, enterPlaygroundRoom, fillLine, focusRichText, focusTitle, getCursorBlockIdAndHeight, getEditorHostLocator, getIndexCoordinate, getInlineSelectionIndex, getInlineSelectionText, getPageSnapshot, getRichTextBoundingBox, getSelectedText, getSelectedTextByInlineEditor, initEmptyEdgelessState, initEmptyParagraphState, initImageState, initThreeLists, initThreeParagraphs, pasteByKeyboard, pressArrowDown, pressArrowLeft, pressArrowRight, pressArrowUp, pressBackspace, pressEnter, pressEscape, pressForwardDelete, pressShiftEnter, pressShiftTab, pressTab, redoByKeyboard, resetHistory, scrollToTop, selectAllByKeyboard, setInlineRangeInInlineEditor, setSelection, SHORT_KEY, switchEditorMode, type, undoByKeyboard, waitNextFrame, } from '../utils/actions/index.js'; import { assertBlockCount, assertBlockSelections, assertClipItems, assertDivider, assertNativeSelectionRangeCount, assertRichTextInlineRange, assertRichTexts, assertTextSelection, assertTitle, } from '../utils/asserts.js'; import { test } from '../utils/playwright.js'; test('native range delete', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [0, 0], [2, 3]); await pressBackspace(page); await assertBlockCount(page, 'paragraph', 1); await assertRichTexts(page, ['']); await waitNextFrame(page); await undoByKeyboard(page); await assertRichTexts(page, ['123', '456', '789']); await redoByKeyboard(page); await assertRichTexts(page, ['']); }); test('native range delete with indent', async ({ page }, testInfo) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '123'); await pressEnter(page); await type(page, '456'); await pressEnter(page); await type(page, '789'); await pressEnter(page); await type(page, 'abc'); await pressEnter(page); await type(page, 'def'); await pressEnter(page); await type(page, 'ghi'); await resetHistory(page); await focusRichText(page, 1); await pressTab(page); await focusRichText(page, 2); await pressTab(page, 2); await focusRichText(page, 4); await pressTab(page); await focusRichText(page, 5); await pressTab(page, 2); // 123 // 456 // 789 // abc // def // ghi expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_init.json` ); await dragBetweenIndices(page, [0, 2], [4, 1]); // 12|3 // 456 // 789 // abc // d|ef // ghi await pressBackspace(page); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_after_backspace.json` ); await waitNextFrame(page); await undoByKeyboard(page); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_after_undo.json` ); await redoByKeyboard(page); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_after_redo.json` ); }); test('native range delete by forwardDelete', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const box123 = await getRichTextBoundingBox(page, '2'); const inside123 = { x: box123.left + 1, y: box123.top + 1 }; const box789 = await getRichTextBoundingBox(page, '4'); const inside789 = { x: box789.right - 1, y: box789.bottom - 1 }; // from top to bottom await dragBetweenCoords(page, inside123, inside789, { steps: 50 }); await pressForwardDelete(page); await assertBlockCount(page, 'paragraph', 1); await assertRichTexts(page, ['']); await waitNextFrame(page); await undoByKeyboard(page); await assertRichTexts(page, ['123', '456', '789']); await redoByKeyboard(page); await assertRichTexts(page, ['']); }); test('native range input', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const box123 = await getRichTextBoundingBox(page, '2'); const inside123 = { x: box123.left + 1, y: box123.top + 1 }; const box789 = await getRichTextBoundingBox(page, '4'); const inside789 = { x: box789.right - 1, y: box789.bottom - 1 }; // from top to bottom await dragBetweenCoords(page, inside123, inside789, { steps: 50 }); await pressForwardDelete(page); await page.keyboard.press('a'); await assertBlockCount(page, 'paragraph', 1); await assertRichTexts(page, ['a']); }); test('native range selection backwards', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const box123 = await getRichTextBoundingBox(page, '2'); const above123 = { x: box123.left + 1, y: box123.top + 1 }; const box789 = await getRichTextBoundingBox(page, '4'); const bottomRight789 = { x: box789.right - 1, y: box789.bottom - 1 }; // from bottom to top await dragBetweenCoords(page, bottomRight789, above123, { steps: 10 }); await pressBackspace(page); await assertBlockCount(page, 'paragraph', 1); await assertRichTexts(page, ['']); await waitNextFrame(page); await undoByKeyboard(page); // FIXME // await assertRichTexts(page, ['123', '456', '789']); await redoByKeyboard(page); await assertRichTexts(page, ['']); }); test('native range selection backwards by forwardDelete', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const box123 = await getRichTextBoundingBox(page, '2'); const above123 = { x: box123.left, y: box123.top - 2 }; const box789 = await getRichTextBoundingBox(page, '4'); const bottomRight789 = { x: box789.right - 1, y: box789.bottom - 1 }; // from bottom to top await dragBetweenCoords(page, bottomRight789, above123, { steps: 10 }); await pressForwardDelete(page); await assertBlockCount(page, 'paragraph', 1); await assertRichTexts(page, ['']); await waitNextFrame(page); await undoByKeyboard(page); await assertRichTexts(page, ['123', '456', '789']); await redoByKeyboard(page); await assertRichTexts(page, ['']); }); test('cursor move up and down', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, 'arrow down test 1'); await pressEnter(page); await type(page, 'arrow down test 2'); await pressArrowUp(page); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('arrow down test 1'); await pressArrowDown(page); const textTwo = await getInlineSelectionText(page); expect(textTwo).toBe('arrow down test 2'); }); test('cursor move to up and down with children block', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, 'arrow down test 1'); await pressEnter(page); await type(page, 'arrow down test 2'); await page.keyboard.press('Tab'); for (let i = 0; i <= 17; i++) { await page.keyboard.press('ArrowRight'); } await pressEnter(page); await type(page, 'arrow down test 3'); await pressShiftTab(page); for (let i = 0; i < 2; i++) { await page.keyboard.press('ArrowRight'); } await page.keyboard.press('ArrowUp'); const indexOne = await getInlineSelectionIndex(page); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('arrow down test 2'); expect(indexOne).toBe(13); for (let i = 0; i < 3; i++) { await page.keyboard.press('ArrowLeft'); } await page.keyboard.press('ArrowUp'); const indexTwo = await getInlineSelectionIndex(page); const textTwo = await getInlineSelectionText(page); expect(textTwo).toBe('arrow down test 1'); expect(indexTwo).toBeGreaterThanOrEqual(12); expect(indexTwo).toBeLessThanOrEqual(17); await page.keyboard.press('ArrowDown'); const textThree = await getInlineSelectionText(page); expect(textThree).toBe('arrow down test 2'); }); test('cursor move left and right', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, 'arrow down test 1'); await pressEnter(page); await type(page, 'arrow down test 2'); const index1 = await getInlineSelectionIndex(page); expect(index1).toBe(17); await pressArrowLeft(page, 17); const index2 = await getInlineSelectionIndex(page); expect(index2).toBe(0); await pressArrowLeft(page); const index3 = await getInlineSelectionIndex(page); expect(index3).toBe(17); }); test('cursor move up at edge of the second line', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await pressEnter(page); const [id, height] = await getCursorBlockIdAndHeight(page); if (id && height) { await fillLine(page, true); await pressArrowLeft(page); const [currentId] = await getCursorBlockIdAndHeight(page); expect(currentId).toBe(id); } }); test('cursor move down at edge of the last line', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await pressEnter(page); const [id] = await getCursorBlockIdAndHeight(page); await page.keyboard.press('ArrowUp'); const [, height] = await getCursorBlockIdAndHeight(page); if (id && height) { await fillLine(page, true); await pressArrowLeft(page); await pressArrowDown(page); const [currentId] = await getCursorBlockIdAndHeight(page); expect(currentId).toBe(id); } }); test('cursor move up and down through note', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await addNoteByClick(page); await focusRichText(page, 0); let currentId: string | null; const [id] = await getCursorBlockIdAndHeight(page); await pressArrowDown(page); currentId = (await getCursorBlockIdAndHeight(page))[0]; expect(id).not.toBe(currentId); await pressArrowUp(page); currentId = (await getCursorBlockIdAndHeight(page))[0]; expect(id).toBe(currentId); }); test('double click choose words', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, 'hello block suite'); await assertRichTexts(page, ['hello block suite']); const hello = await getRichTextBoundingBox(page, '2'); const helloPosition = { x: hello.x + 2, y: hello.y + 8 }; await page.mouse.dblclick(helloPosition.x, helloPosition.y); const text = await page.evaluate(() => { let text = ''; const selection = window.getSelection(); if (selection) { text = selection.toString(); } return text; }); expect(text).toBe('hello'); }); test('select all text with dragging and delete', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [0, 0], [2, 3], { x: 1, y: 1 }, undefined, { steps: 20, }); await pressBackspace(page); await type(page, 'abc'); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('abc'); }); test('select all text with dragging and delete by forwardDelete', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [0, 0], [2, 3], undefined, undefined, { steps: 20, }); await pressForwardDelete(page); await type(page, 'abc'); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('abc'); }); test('select all text with keyboard delete', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await focusRichText(page); await selectAllByKeyboard(page); await pressBackspace(page); const text1 = await getInlineSelectionText(page); expect(text1).toBe(''); await type(page, 'abc'); const text2 = await getInlineSelectionText(page); expect(text2).toBe('abc'); await selectAllByKeyboard(page); await selectAllByKeyboard(page); await pressBackspace(page); await assertRichTexts(page, ['456', '789']); }); test('select text leaving a few words in the last line and delete', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [0, 0], [2, 1], undefined, undefined, { steps: 20, }); await page.keyboard.press('Backspace'); await waitNextFrame(page); await type(page, 'abc'); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('abc89'); }); test('select text leaving a few words in the last line and delete by forwardDelete', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [0, 0], [2, 1], undefined, undefined, { steps: 20, }); await pressForwardDelete(page); await waitNextFrame(page); await type(page, 'abc'); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('abc89'); }); test('select text in the same line with dragging leftward and move outside the affine-note', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const noteLeft = await page.evaluate(() => { const note = document.querySelector('affine-note'); if (!note) { throw new Error(); } return note.getBoundingClientRect().left; }); // `456` const blockRect = await page.evaluate(() => { const block = document.querySelector('[data-block-id="3"]'); if (!block) { throw new Error(); } return block.getBoundingClientRect(); }); await dragBetweenIndices( page, [1, 3], [1, 0], { x: 0, y: 0 }, { x: 0, y: 0 }, { steps: 20, async beforeMouseUp() { await page.mouse.move( noteLeft - 1, blockRect.top + blockRect.height / 2 ); }, } ); await pressBackspace(page); await type(page, 'abc'); await assertRichTexts(page, ['123', 'abc', '789']); }); test('select text in the same line with dragging leftward and move outside the affine-note by forwardDelete', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const noteLeft = await page.evaluate(() => { const note = document.querySelector('affine-note'); if (!note) { throw new Error(); } return note.getBoundingClientRect().left; }); // `456` const blockRect = await page.evaluate(() => { const block = document.querySelector('[data-block-id="3"]'); if (!block) { throw new Error(); } return block.getBoundingClientRect(); }); await dragBetweenIndices( page, [1, 3], [1, 0], { x: 0, y: 0 }, { x: 0, y: 0 }, { steps: 20, async beforeMouseUp() { await page.mouse.move( noteLeft - 1, blockRect.top + blockRect.height / 2 ); }, } ); await pressForwardDelete(page); await type(page, 'abc'); await assertRichTexts(page, ['123', 'abc', '789']); }); test('select text in the same line with dragging rightward and move outside the affine-note', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const noteRight = await page.evaluate(() => { const note = document.querySelector('affine-note'); if (!note) { throw new Error(); } return note.getBoundingClientRect().right; }); // `456` const blockRect = await page.evaluate(() => { const block = document.querySelector('[data-block-id="3"]'); if (!block) { throw new Error(); } return block.getBoundingClientRect(); }); await dragBetweenIndices( page, [1, 0], [1, 3], { x: 0, y: 0 }, { x: 0, y: 0 }, { steps: 20, async beforeMouseUp() { await page.mouse.move( noteRight + 1, blockRect.top + blockRect.height / 2 ); }, } ); await pressBackspace(page); await type(page, 'abc'); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('abc'); }); test('select text in the same line with dragging rightward and move outside the affine-note by forwardDelete', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const noteRight = await page.evaluate(() => { const note = document.querySelector('affine-note'); if (!note) { throw new Error(); } return note.getBoundingClientRect().right; }); // `456` const blockRect = await page.evaluate(() => { const block = document.querySelector('[data-block-id="3"]'); if (!block) { throw new Error(); } return block.getBoundingClientRect(); }); await dragBetweenIndices( page, [1, 0], [1, 3], { x: 0, y: 0 }, { x: 0, y: 0 }, { steps: 20, async beforeMouseUp() { await page.mouse.move( noteRight + 1, blockRect.top + blockRect.height / 2 ); }, } ); await pressForwardDelete(page); await type(page, 'abc'); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('abc'); }); test('select text in the same line with dragging rightward and press enter create block', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); // blur the editor await page.mouse.click(20, 20); const box123 = await getRichTextBoundingBox(page, '2'); const above123 = { x: box123.left + 100, y: box123.top }; const box789 = await getRichTextBoundingBox(page, '4'); const below789 = { x: box789.right + 30, y: box789.bottom + 50 }; await dragBetweenCoords(page, below789, above123, { steps: 50 }); await page.waitForTimeout(300); await pressEnter(page); await pressEnter(page); await type(page, 'abc'); await assertRichTexts(page, ['123', '456', '789', 'abc']); }); test('drag to select tagged text, and copy', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await page.keyboard.insertText('123456789'); await assertRichTexts(page, ['123456789']); await dragBetweenIndices(page, [0, 1], [0, 3], undefined, undefined, { steps: 20, }); await page.keyboard.press(`${SHORT_KEY}+B`); await dragBetweenIndices(page, [0, 0], [0, 5], undefined, undefined, { steps: 20, }); await page.keyboard.press(`${SHORT_KEY}+C`); const textOne = await getSelectedTextByInlineEditor(page); expect(textOne).toBe('12345'); }); test('drag to select tagged text, and input character', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await page.keyboard.insertText('123456789'); await assertRichTexts(page, ['123456789']); await dragBetweenIndices(page, [0, 1], [0, 3], undefined, undefined, { steps: 20, }); await page.keyboard.press(`${SHORT_KEY}+B`); await dragBetweenIndices(page, [0, 0], [0, 5], undefined, undefined, { steps: 20, }); await type(page, '1'); const textOne = await getInlineSelectionText(page); expect(textOne).toBe('16789'); }); test('Change title when first content is divider', async ({ page }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/toeverything/blocksuite/issues/1004', }); await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '--- '); await assertDivider(page, 1); await focusTitle(page); await type(page, 'title'); await assertTitle(page, 'title'); }); test('ArrowUp and ArrowDown to select divider and copy', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '--- '); await assertDivider(page, 1); await pressEscape(page); await pressArrowUp(page); await copyByKeyboard(page); await pressArrowDown(page); await pressEnter(page); await pasteByKeyboard(page); await assertDivider(page, 2); }); test('Delete the blank line between two dividers', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '--- '); await assertDivider(page, 1); await waitNextFrame(page); await pressEnter(page); await type(page, '--- '); await assertDivider(page, 2); await assertRichTexts(page, ['', '']); await pressArrowUp(page); await assertBlockSelections(page, ['5']); await pressArrowUp(page); await assertBlockSelections(page, []); await assertRichTextInlineRange(page, 0, 0); await pressBackspace(page); await assertRichTexts(page, ['']); await assertBlockSelections(page, ['3']); await assertDivider(page, 2); }); test('Delete the second divider between two dividers by forwardDelete', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '--- '); await assertDivider(page, 1); await pressEnter(page); await type(page, '--- '); await assertDivider(page, 2); await pressEscape(page); await pressArrowUp(page); await pressForwardDelete(page); await assertDivider(page, 1); await assertRichTexts(page, ['', '']); }); test('should delete line with content after divider not lose content', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '--- '); await type(page, '123'); await assertDivider(page, 1); // Jump to line start await page.keyboard.press(`${SHORT_KEY}+ArrowLeft`, { delay: 50 }); await waitNextFrame(page); await pressBackspace(page, 2); await assertDivider(page, 0); await assertRichTexts(page, ['123']); }); test('should forwardDelete divider works properly', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '123'); await pressEnter(page); await type(page, '--- '); await assertDivider(page, 1); // Jump to first line start await pressEscape(page); await pressArrowUp(page); await page.keyboard.press(`${SHORT_KEY}+ArrowRight`, { delay: 50 }); await pressForwardDelete(page); await assertDivider(page, 0); await assertRichTexts(page, ['123', '']); }); test('the cursor should move to closest editor block when clicking outside container', async ({ page, }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/toeverything/blocksuite/pull/570', }); // This test only works in playwright or touch device! test.fail(); await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const text2 = page.locator('[data-block-id="3"] .inline-editor'); const rect = await text2.boundingBox(); if (!rect) { throw new Error('rect is not found'); } // The behavior of mouse click is similar to touch in mobile device // await page.mouse.click(rect.x - 50, rect.y + 5); await page.mouse.move(rect.x - 50, rect.y + 5); await page.mouse.down(); await page.mouse.up(); await pressArrowLeft(page, 4); await pressBackspace(page); await waitNextFrame(page); await assertRichTexts(page, ['123456', '789']); await undoByKeyboard(page); await waitNextFrame(page); // await page.mouse.click(rect.x + rect.width + 50, rect.y + 5); await page.mouse.move(rect.x + rect.width + 50, rect.y + 5); await page.mouse.down(); await page.mouse.up(); await waitNextFrame(page); await pressArrowLeft(page); await pressBackspace(page); await waitNextFrame(page); await assertRichTexts(page, ['123', '46', '789']); }); test('should not crash when mouse over the left side of the list block prefix', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeLists(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [1, 2], [1, 0]); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '45'); // `456` const prefixIconRect = await page.evaluate(() => { const block = document.querySelector('[data-block-id="4"]'); if (!block) { throw new Error(); } const prefixIcon = block.querySelector('.affine-list-block__prefix '); if (!prefixIcon) { throw new Error(); } return prefixIcon.getBoundingClientRect(); }); await dragBetweenIndices( page, [1, 2], [1, 0], { x: 0, y: 0 }, { x: 0, y: 0 }, { beforeMouseUp: async () => { await page.mouse.move(prefixIconRect.left - 1, prefixIconRect.top); }, } ); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '45'); }); test('should set the last block to end the range after when leaving the affine-note', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [0, 2], [2, 1]); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '34567'); // blur await page.mouse.click(0, 0); await dragBetweenIndices( page, [0, 2], [2, 1], { x: 0, y: 0 }, { x: 0, y: 30 } // drag below the bottom of the last block ); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '3456789'); }); test('should set the first block to start the range before when leaving the affine-note-block-container', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [2, 1], [0, 2]); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '34567'); // blur await page.mouse.click(0, 0); await dragBetweenIndices( page, [2, 1], [0, 2], { x: 0, y: 0 }, { x: 0, y: -30 } // drag above the top of the first block ); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '1234567'); }); test('should select texts on cross-note dragging', async ({ page }) => { await enterPlaygroundRoom(page); const { rootId } = await initEmptyParagraphState(page); await initThreeParagraphs(page); await initEmptyParagraphState(page, rootId); // focus last block in first note await setInlineRangeInInlineEditor( page, { index: 3, length: 0, }, 3 ); // goto next note await pressArrowDown(page); await waitNextFrame(page); await type(page, 'ABC'); await assertRichTexts(page, ['123', '456', '789', 'ABC']); // blur await page.mouse.click(0, 0); await dragBetweenIndices( page, [0, 2], [3, 1], { x: 0, y: 0 }, { x: 0, y: 30 } // drag below the bottom of the last block ); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '3456789ABC'); }); test('should select full text of the first block when leaving the affine-note-block-container in edgeless mode', async ({ page, }) => { await enterPlaygroundRoom(page); const ids = await initEmptyEdgelessState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await switchEditorMode(page); await activeNoteInEdgeless(page, ids.noteId); await dragBetweenIndices(page, [2, 1], [0, 2], undefined, undefined, { click: true, }); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '34567'); const containerRect = await page.evaluate(() => { const container = document.querySelector('.affine-note-block-container'); if (!container) { throw new Error(); } return container.getBoundingClientRect(); }); await dragBetweenIndices( page, [2, 1], [0, 2], { x: 0, y: 0 }, { x: 0, y: 0 }, // drag above the top of the first block { beforeMouseUp: async () => { await page.mouse.move(containerRect.left, containerRect.top - 30); }, } ); }); test('should add a new line when clicking the bottom of the last non-text block', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await pressEnter(page); await waitNextFrame(page); // code block await type(page, '```'); await pressEnter(page); const locator = page.locator('affine-code'); await expect(locator).toBeVisible(); await type(page, 'ABC'); await waitNextFrame(page); await assertRichTexts(page, ['123', '456', '789', 'ABC']); }); test('should select texts on dragging around the page', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); const coord = await getIndexCoordinate(page, [1, 2]); // blur await page.mouse.click(0, 0); await page.mouse.move(coord.x, coord.y); await page.mouse.down(); // 123 // 45|6 // 789| await page.mouse.move(coord.x + 26, coord.y + 90, { steps: 20 }); await page.mouse.up(); await page.keyboard.press('Backspace'); await waitNextFrame(page); await assertRichTexts(page, ['123', '45']); await waitNextFrame(page); await undoByKeyboard(page); // blur await page.mouse.click(0, 0); await page.mouse.move(coord.x, coord.y); await page.mouse.down(); await page.mouse.move(coord.x + 26, coord.y + 90, { steps: 20 }); await page.mouse.up(); await page.keyboard.press('Backspace'); await waitNextFrame(page); await assertRichTexts(page, ['123', '45']); }); test('indent native multi-selection block', async ({ page }, testInfo) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await pressEnter(page); await type(page, '012'); await assertRichTexts(page, ['123', '456', '789', '012']); const from = { blockId: '3', index: 1, length: 2, }; const to = { blockId: '5', index: 0, length: 1, }; await setSelection(page, 3, 1, 5, 1); await assertTextSelection(page, from, to); await waitNextFrame(page); await pressTab(page); // should restore selection await assertTextSelection(page, from, to); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_after_tab.json` ); await setSelection(page, 3, 1, 5, 1); await assertTextSelection(page, from, to); await waitNextFrame(page); await pressShiftTab(page); // should restore selection await assertTextSelection(page, from, to); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_after_shift_tab.json` ); }); test('should clear native selection before block selection', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices( page, [1, 3], [1, 0], { x: 0, y: 0 }, { x: 0, y: 0 }, { steps: 20 } ); const text0 = await getInlineSelectionText(page); // `123` const first = await page.evaluate(() => { const first = document.querySelector('[data-block-id="2"]'); if (!first) { throw new Error(); } return first.getBoundingClientRect(); }); await dragBetweenCoords( page, { x: first.right + 10, y: first.top + 1, }, { x: first.right - 10, y: first.top + 2, } ); await waitNextFrame(page); const textCount = await page.evaluate(() => { return window.getSelection()?.rangeCount || 0; }); expect(text0).toBe('456'); expect(textCount).toBe(0); const rects = page.locator('affine-block-selection').locator('visible=true'); await expect(rects).toHaveCount(1); }); // ↑ test('should keep native range selection when scrolling backward with the scroll wheel', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); for (let i = 0; i < 10; i++) { await pressEnter(page); } await type(page, '987'); await pressEnter(page); await type(page, '654'); await pressEnter(page); await type(page, '321'); const data = Array.from({ length: 9 }, () => ''); data.unshift('123', '456', '789'); data.push('987', '654', '321'); await assertRichTexts(page, data); const blockHeight = await page.evaluate(() => { const viewport = document.querySelector('.affine-page-viewport'); if (!viewport) { throw new Error(); } const distance = viewport.scrollHeight - viewport.clientHeight; viewport.scrollTo(0, distance); const container = viewport.querySelector( 'affine-note .affine-block-children-container' ); if (!container) { throw new Error(); } const first = container.firstElementChild; if (!first) { throw new Error(); } const second = first.nextElementSibling; if (!second) { throw new Error(); } return ( second.getBoundingClientRect().top - first.getBoundingClientRect().top ); }); await page.waitForTimeout(250); await page.mouse.move(0, 0); await dragBetweenIndices( page, [14, 3], [14, 0], { x: 0, y: 0 }, { x: 0, y: 0 }, { // dont release mouse beforeMouseUp: async () => { await page.mouse.wheel(0, -blockHeight * 4); await page.waitForTimeout(250); }, } ); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '987654321'); }); // ↓ test('should keep native range selection when scrolling forward with the scroll wheel', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); for (let i = 0; i < 10; i++) { await pressEnter(page); } await type(page, '987'); await pressEnter(page); await type(page, '654'); await pressEnter(page); await type(page, '321'); const data = Array.from({ length: 9 }, () => ''); data.unshift('123', '456', '789'); data.push('987', '654', '321'); await assertRichTexts(page, data); const blockHeight = await page.evaluate(() => { const viewport = document.querySelector('.affine-page-viewport'); if (!viewport) { throw new Error(); } const container = viewport.querySelector( 'affine-note .affine-block-children-container' ); if (!container) { throw new Error(); } const first = container.firstElementChild; if (!first) { throw new Error(); } const second = first.nextElementSibling; if (!second) { throw new Error(); } return ( second.getBoundingClientRect().top - first.getBoundingClientRect().top ); }); await page.waitForTimeout(250); await page.evaluate(() => { document.querySelector('.affine-page-viewport')?.scrollTo(0, 0); }); await page.mouse.move(0, 0); await dragBetweenIndices( page, [0, 0], [0, 3], { x: 0, y: 0 }, { x: 0, y: 0 }, { // dont release mouse beforeMouseUp: async () => { await page.mouse.wheel(0, blockHeight * 3); await page.waitForTimeout(250); }, } ); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '123456789'); }); test('should not show option menu of image on native selection', async ({ page, }) => { await enterPlaygroundRoom(page); await initImageState(page); await activeEmbed(page); await expect(page.locator('.affine-image-toolbar-container')).toHaveCount(1); await pressEscape(page); await pressEnter(page); await type(page, '123'); await page.mouse.click(0, 0); await dragBetweenIndices( page, [0, 1], [0, 0], { x: 0, y: 0 }, { x: -40, y: 0 } ); await waitNextFrame(page); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '123'); await page.mouse.click(0, 0); await dragBetweenIndices( page, [0, 1], [0, 0], { x: 0, y: 0 }, { x: -40, y: -100 } ); await waitNextFrame(page); await copyByKeyboard(page); assertClipItems(page, 'text/plain', '123'); await expect(page.locator('.affine-image-toolbar-container')).toHaveCount(0); }); test('should select with shift-click', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await focusRichText(page); await page.click('[data-block-id="4"] [data-v-text]', { modifiers: ['Shift'], }); expect(await getSelectedText(page)).toContain('4567'); }); test('should collapse to end when press arrow-right on multi-line selection', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [0, 0], [1, 2]); expect(await getSelectedText(page)).toBe('12345'); await pressArrowRight(page); await pressBackspace(page); await assertRichTexts(page, ['123', '46', '789']); }); test('should collapse to start when press arrow-left on multi-line selection', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await dragBetweenIndices(page, [0, 1], [1, 2]); expect(await getSelectedText(page)).toBe('2345'); await pressArrowLeft(page); await pressBackspace(page); await assertRichTexts(page, ['23', '456', '789']); }); test('should select when clicking on blank area in edgeless mode', async ({ page, }) => { await enterPlaygroundRoom(page); const ids = await initEmptyEdgelessState(page); await initThreeParagraphs(page); await assertRichTexts(page, ['123', '456', '789']); await switchEditorMode(page); await activeNoteInEdgeless(page, ids.noteId); const r1 = await page.locator('[data-block-id="3"]').boundingBox(); const r2 = await page.locator('[data-block-id="4"]').boundingBox(); const r3 = await page.locator('[data-block-id="5"]').boundingBox(); if (!r1 || !r2 || !r3) { throw new Error(); } await click(page, { x: r3.x + 40, y: r3.y + 5 }); await waitNextFrame(page); expect(await getInlineSelectionText(page)).toBe('789'); }); test('press ArrowLeft in the start of first paragraph should not focus on title', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page, 0); await type(page, '123'); await pressArrowLeft(page, 5); await type(page, 'title'); await assertTitle(page, ''); }); test('should not scroll page when mouse is click down', async ({ page }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/toeverything/blocksuite/issues/5034', }); await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); for (let i = 0; i < 10; i++) { await pressEnter(page); } for (let i = 0; i < 20; i++) { await type(page, String(i)); await pressShiftEnter(page); } await assertRichTexts(page, [ ...' '.repeat(9).split(' '), // 10 empty paragraph '0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n', ]); await scrollToTop(page); await focusRichText(page, 0); const editorHost = getEditorHostLocator(page); const longText = editorHost.locator('rich-text').nth(10); const rect = await longText.boundingBox(); if (!rect) throw new Error(); await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2); await assertRichTextInlineRange(page, 0, 0); await page.mouse.down(); await assertRichTextInlineRange(page, 10, 22); // simulate user click down and wait for 500ms await waitNextFrame(page, 500); await page.mouse.up(); await assertRichTextInlineRange(page, 10, 22); }); test('scroll vertically when inputting long text in a block', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); for (let i = 0; i < 40; i++) { await type(page, String(i)); await pressShiftEnter(page); } const viewportScrollTop = await page.evaluate(() => { const viewport = document.querySelector('.affine-page-viewport'); if (!viewport) { throw new Error('viewport not found'); } return viewport.scrollTop; }); expect(viewportScrollTop).toBeGreaterThan(100); }); test('scroll vertically when adding multiple blocks', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); for (let i = 0; i < 40; i++) { await type(page, String(i)); await pressEnter(page); } const viewportScrollTop = await page.evaluate(() => { const viewport = document.querySelector('.affine-page-viewport'); if (!viewport) { throw new Error('viewport not found'); } return viewport.scrollTop; }); expect(viewportScrollTop).toBeGreaterThan(400); }); test('click to select divided', async ({ page }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/toeverything/blocksuite/issues/4547', }); await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '--- '); await assertDivider(page, 1); await page.click('affine-divider'); const selectedBlocks = page .locator('affine-block-selection') .locator('visible=true'); await expect(selectedBlocks).toHaveCount(1); await pressForwardDelete(page); await assertDivider(page, 0); }); test('auto-scroll when creating a new paragraph-block by pressing enter', async ({ page, }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/toeverything/blocksuite/issues/4547', }); await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); const getScrollTop = async () => { return page.evaluate(() => { const viewport = document.querySelector('.affine-page-viewport'); if (!viewport) { throw new Error(); } return viewport.scrollTop; }); }; await pressEnter(page, 30); const oldScrollTop = await getScrollTop(); await pressEnter(page, 30); const newScrollTop = await getScrollTop(); expect(newScrollTop).toBeGreaterThan(oldScrollTop); }); test('Use arrow up and down to select two types of block', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await type(page, '123'); await pressEnter(page); await type(page, '--- --- '); await type(page, '123'); await pressEnter(page); await type(page, '--- 123'); // 123 // --- // --- // 123 // --- // 123 await assertDivider(page, 3); await assertRichTexts(page, ['123', '123', '123']); // from bottom to top await assertNativeSelectionRangeCount(page, 1); await assertRichTextInlineRange(page, 2, 3); await pressArrowUp(page); await assertNativeSelectionRangeCount(page, 0); await assertBlockSelections(page, ['7']); await pressArrowUp(page); await assertNativeSelectionRangeCount(page, 1); await assertRichTextInlineRange(page, 1, 3); await pressArrowUp(page); await assertNativeSelectionRangeCount(page, 0); await assertBlockSelections(page, ['5']); await pressArrowUp(page); await assertNativeSelectionRangeCount(page, 0); await assertBlockSelections(page, ['4']); await pressArrowUp(page); await assertNativeSelectionRangeCount(page, 1); await assertRichTextInlineRange(page, 0, 3); // from top to bottom await pressArrowDown(page); await assertNativeSelectionRangeCount(page, 0); await assertBlockSelections(page, ['4']); await pressArrowDown(page); await assertNativeSelectionRangeCount(page, 0); await assertBlockSelections(page, ['5']); await pressArrowDown(page); await assertNativeSelectionRangeCount(page, 1); await assertRichTextInlineRange(page, 1, 0); await pressArrowDown(page); await assertNativeSelectionRangeCount(page, 0); await assertBlockSelections(page, ['7']); await pressArrowDown(page); await assertNativeSelectionRangeCount(page, 1); await assertRichTextInlineRange(page, 2, 0); }); test.describe('should scroll text to view when drag to select at top or bottom edge', () => { test('from top to bottom', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); for (let i = 0; i < 50; i++) { await type(page, `${i}`); await pressEnter(page); } const startCoord = await getIndexCoordinate(page, [49, 2]); const endCoord = await getIndexCoordinate(page, [0, 0]); // simulate actual drag to select from bottom to top await page.mouse.move(startCoord.x, startCoord.y); await page.mouse.down(); await page.mouse.move(endCoord.x, 0); // move to top edge await page.waitForTimeout(5000); await page.mouse.up(); const firstParagraph = page.locator('[data-block-id="2"]'); await expect(firstParagraph).toBeInViewport(); }); // playwright doesn't auto scroll when drag selection to bottom edge test.skip('from bottom to top', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); for (let i = 0; i < 50; i++) { await type(page, `${i}`); await pressEnter(page); } const firstParagraph = page.locator('[data-block-id="2"]'); await firstParagraph.scrollIntoViewIfNeeded(); const startCoord = await getIndexCoordinate(page, [0, 0]); const endCoord = await getIndexCoordinate(page, [49, 2]); const viewportHeight = await page.evaluate( () => document.documentElement.clientHeight ); // simulate actual drag to select from top to bottom await page.mouse.move(startCoord.x, startCoord.y); await page.mouse.down(); await page.mouse.move(endCoord.x, viewportHeight - 10); // move to bottom edge await page.waitForTimeout(5000); await page.mouse.up(); const lastParagraph = page.locator('[data-block-id="51"]'); await expect(lastParagraph).toBeInViewport(); }); }); test('abnormal cursor jumping', async ({ page }) => { // https://github.com/toeverything/blocksuite/pull/8552 await enterPlaygroundRoom(page); await initImageState(page); await pressEnter(page); await page.locator('affine-image block-zero-width .block-zero-width').click(); await pressArrowUp(page); await pressTab(page); await pressArrowDown(page); await pressTab(page); await pressEnter(page, 12); const image = page.locator('affine-image'); const rect = await image.boundingBox(); // make sure the image is out of view expect(rect?.y).toBeLessThan(0); await setSelection(page, 4, 0, 4, 0); await type(page, 'aaaaaaaaaaaaaa'); await page.locator('[data-block-id="4"]').dblclick({ position: { x: 50, y: 5, }, }); const newRect = await image.boundingBox(); expect(rect).toEqual(newRect); }); test('unexpected scroll when clicking padding area', async ({ page }) => { // https://github.com/toeverything/blocksuite/pull/8678 await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await focusRichText(page); await pressEnter(page, 30); await pressArrowUp(page, 5); await type(page, '1. aaa\nbbb'); await pressTab(page); const list = page.locator('[data-block-id="34"]'); const listRect = await list.boundingBox(); if (!listRect) { throw new Error('listRect is not found'); } await page.mouse.click(listRect.x - 30, listRect.y + 5); const newListRect = await list.boundingBox(); // not scroll expect(listRect).toEqual(newListRect); await pressArrowUp(page, 4); await type(page, '/tableview\n'); const database = page.locator('affine-database'); const databaseRect = await database.boundingBox(); if (!databaseRect) { throw new Error('databaseRect is not found'); } await page.mouse.click( databaseRect.x + databaseRect.width + 10, databaseRect.y + 100 ); const newDatabaseRect = await database.boundingBox(); // not scroll expect(databaseRect).toEqual(newDatabaseRect); });