Files
AFFiNE-Mirror/blocksuite/tests-legacy/selection/native.spec.ts
2024-12-20 11:08:21 +00:00

1789 lines
49 KiB
TypeScript

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,
assertExists,
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, y: box123.top - 2 };
const box789 = await getRichTextBoundingBox(page, '4');
const bottomRight789 = { x: box789.right, y: box789.bottom };
// 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, y: box789.bottom };
// 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], undefined, 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']);
await type(page, 'abc');
await selectAllByKeyboard(page);
await selectAllByKeyboard(page);
await selectAllByKeyboard(page);
await pressBackspace(page);
await assertRichTexts(page, ['']);
});
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();
assertExists(rect);
// 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();
assertExists(listRect);
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, '/table\n');
const database = page.locator('affine-database');
const databaseRect = await database.boundingBox();
assertExists(databaseRect);
await page.mouse.click(
databaseRect.x + databaseRect.width + 10,
databaseRect.y + 10
);
const newDatabaseRect = await database.boundingBox();
// not scroll
expect(databaseRect).toEqual(newDatabaseRect);
});