diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c6d34e549e316..54835030a3bd3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -112,6 +112,36 @@ jobs: yarn set version $(node -e "console.log(require('./package.json').packageManager.split('@')[1])") git diff --exit-code + e2e-legacy-blocksuite-test: + name: Legacy Blocksuite E2E Test + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: ./.github/actions/setup-node + with: + playwright-install: true + electron-install: false + full-cache: true + + - name: Run playground build + run: yarn workspace @blocksuite/playground build + + - name: Run playwright tests + run: yarn workspace @blocksuite/legacy-e2e test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }} + + - name: Upload test results + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: test-results-e2e-legacy-bs-${{ matrix.shard }} + path: ./test-results + if-no-files-found: ignore + e2e-test: name: E2E Test runs-on: ubuntu-latest @@ -185,6 +215,7 @@ jobs: uses: ./.github/actions/setup-node with: electron-install: true + playwright-install: true full-cache: true - name: Download affine.linux-x64-gnu.node @@ -767,6 +798,7 @@ jobs: - lint - check-yarn-binary - e2e-test + - e2e-legacy-blocksuite-test - e2e-mobile-test - unit-test - build-native diff --git a/.prettierignore b/.prettierignore index 6aa0948e1fdde..489bc0a065375 100644 --- a/.prettierignore +++ b/.prettierignore @@ -25,4 +25,6 @@ packages/frontend/templates/onboarding packages/backend/native/index.d.ts packages/frontend/native/index.d.ts packages/frontend/native/index.js -compose.yaml \ No newline at end of file +compose.yaml + +blocksuite/tests-legacy/snapshots diff --git a/blocksuite/affine/all/vitest.config.ts b/blocksuite/affine/all/vitest.config.ts index 0be7ff0fecb3c..700f600c649af 100644 --- a/blocksuite/affine/all/vitest.config.ts +++ b/blocksuite/affine/all/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../../scripts/vitest-global.ts', + globalSetup: '../../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/block-embed/vitest.config.ts b/blocksuite/affine/block-embed/vitest.config.ts index b86624acc9b42..c0330b2ab1951 100644 --- a/blocksuite/affine/block-embed/vitest.config.ts +++ b/blocksuite/affine/block-embed/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../../scripts/vitest-global.ts', + globalSetup: '../../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/block-list/vitest.config.ts b/blocksuite/affine/block-list/vitest.config.ts index b86624acc9b42..c0330b2ab1951 100644 --- a/blocksuite/affine/block-list/vitest.config.ts +++ b/blocksuite/affine/block-list/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../../scripts/vitest-global.ts', + globalSetup: '../../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/block-paragraph/vitest.config.ts b/blocksuite/affine/block-paragraph/vitest.config.ts index fb99961c008a2..b39a4f9ea873d 100644 --- a/blocksuite/affine/block-paragraph/vitest.config.ts +++ b/blocksuite/affine/block-paragraph/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../../scripts/vitest-global.ts', + globalSetup: '../../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/block-surface/vitest.config.ts b/blocksuite/affine/block-surface/vitest.config.ts index 3bb7c2cc2d0eb..a45195590e4f1 100644 --- a/blocksuite/affine/block-surface/vitest.config.ts +++ b/blocksuite/affine/block-surface/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../../scripts/vitest-global.ts', + globalSetup: '../../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/components/vitest.config.ts b/blocksuite/affine/components/vitest.config.ts index e2eab294b3642..3243b60ffb700 100644 --- a/blocksuite/affine/components/vitest.config.ts +++ b/blocksuite/affine/components/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../../scripts/vitest-global.ts', + globalSetup: '../../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/data-view/vitest.config.ts b/blocksuite/affine/data-view/vitest.config.ts index 1e76565bf5f7c..9ae9d1cc50554 100644 --- a/blocksuite/affine/data-view/vitest.config.ts +++ b/blocksuite/affine/data-view/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../scripts/vitest-global.ts', + globalSetup: '../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/model/vitest.config.ts b/blocksuite/affine/model/vitest.config.ts index a1bcb95c66e1c..9faa866c54031 100644 --- a/blocksuite/affine/model/vitest.config.ts +++ b/blocksuite/affine/model/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../scripts/vitest-global.ts', + globalSetup: '../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/shared/vitest.config.ts b/blocksuite/affine/shared/vitest.config.ts index a4b8d7fdcc7c0..9c1c45d368616 100644 --- a/blocksuite/affine/shared/vitest.config.ts +++ b/blocksuite/affine/shared/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../../scripts/vitest-global.ts', + globalSetup: '../../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/affine/widget-scroll-anchoring/vitest.config.ts b/blocksuite/affine/widget-scroll-anchoring/vitest.config.ts index 287861e3498de..04dcaa6f15a1d 100644 --- a/blocksuite/affine/widget-scroll-anchoring/vitest.config.ts +++ b/blocksuite/affine/widget-scroll-anchoring/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../../scripts/vitest-global.ts', + globalSetup: '../../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/blocks/vitest.config.ts b/blocksuite/blocks/vitest.config.ts index 235c0dbde91f2..8567a9f37a5fa 100644 --- a/blocksuite/blocks/vitest.config.ts +++ b/blocksuite/blocks/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ target: 'es2018', }, test: { - globalSetup: '../../scripts/vitest-global.ts', + globalSetup: '../../scripts/vitest-global.js', include: ['src/__tests__/**/*.unit.spec.ts'], testTimeout: 1000, coverage: { diff --git a/blocksuite/framework/block-std/package.json b/blocksuite/framework/block-std/package.json index b8d8dcb064866..c1795ea3b0595 100644 --- a/blocksuite/framework/block-std/package.json +++ b/blocksuite/framework/block-std/package.json @@ -20,6 +20,7 @@ "@lit/context": "^1.1.2", "@preact/signals-core": "^1.8.0", "@types/hast": "^3.0.4", + "dompurify": "^3.1.6", "fractional-indexing": "^3.2.0", "lib0": "^0.2.97", "lit": "^3.2.0", diff --git a/blocksuite/playground/package.json b/blocksuite/playground/package.json index 6120359211115..47c397ffedc0c 100644 --- a/blocksuite/playground/package.json +++ b/blocksuite/playground/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "vite --host", "dev:hmr": "WC_HMR=1 vite", - "build": "tsc && nx vite:build", + "build": "vite build", "preview": "vite preview" }, "dependencies": { @@ -40,6 +40,8 @@ "@types/micromatch": "^4.0.9", "graphql": "^16.9.0", "magic-string": "^0.30.11", + "vite": "^6.0.3", + "vite-plugin-istanbul": "^6.0.2", "vite-plugin-wasm": "^3.3.0", "vite-plugin-web-components-hmr": "^0.1.3" } diff --git a/blocksuite/presets/src/__tests__/main/snapshot.spec.ts b/blocksuite/presets/src/__tests__/main/snapshot.spec.ts index c5d802f23bd94..f286c8c0e8b99 100644 --- a/blocksuite/presets/src/__tests__/main/snapshot.spec.ts +++ b/blocksuite/presets/src/__tests__/main/snapshot.spec.ts @@ -89,11 +89,13 @@ beforeEach(async () => { const xywhPattern = /\[(\s*-?\d+(\.\d+)?\s*,){3}(\s*-?\d+(\.\d+)?\s*)\]/; -test('snapshot 1 importing', async () => { +// FIXME: snapshot tests +test.skip('snapshot 1 importing', async () => { await snapshotTest('https://test.affineassets.com/test-snapshot-1.zip', 25); }); -test('snapshot 2 importing', async () => { +// FIXME: snapshot tests +test.skip('snapshot 2 importing', async () => { await snapshotTest( 'https://test.affineassets.com/test-snapshot-2%20(onboarding).zip', 174 diff --git a/blocksuite/tests-legacy/attachment.spec.ts b/blocksuite/tests-legacy/attachment.spec.ts new file mode 100644 index 0000000000000..9de5a4509a16f --- /dev/null +++ b/blocksuite/tests-legacy/attachment.spec.ts @@ -0,0 +1,769 @@ +import { sleep } from '@blocksuite/global/utils'; +import { expect, type Page } from '@playwright/test'; +import { switchEditorMode } from 'utils/actions/edgeless.js'; + +import { dragBlockToPoint, popImageMoreMenu } from './utils/actions/drag.js'; +import { + pressArrowDown, + pressArrowUp, + pressBackspace, + pressEnter, + pressEscape, + pressShiftTab, + pressTab, + redoByKeyboard, + SHORT_KEY, + type, + undoByKeyboard, +} from './utils/actions/keyboard.js'; +import { + captureHistory, + enterPlaygroundRoom, + focusRichText, + initEmptyEdgelessState, + initEmptyParagraphState, + resetHistory, + waitNextFrame, +} from './utils/actions/misc.js'; +import { + assertBlockChildrenIds, + assertBlockCount, + assertBlockFlavour, + assertBlockSelections, + assertKeyboardWorkInInput, + assertParentBlockFlavour, + assertRichImage, + assertRichTextInlineRange, + assertStoreMatchJSX, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +const FILE_NAME = 'test-card-1.png'; +const FILE_PATH = `../playground/public/${FILE_NAME}`; +const FILE_ID = 'ejImogf-Tb7AuKY-v94uz1zuOJbClqK-tWBxVr_ksGA='; +const FILE_SIZE = 45801; + +function getAttachment(page: Page) { + const attachment = page.locator('affine-attachment'); + const loading = attachment.locator('.affine-attachment-card.loading'); + const toolbar = page.locator('.affine-attachment-toolbar'); + const switchViewButton = toolbar.getByRole('button', { name: 'Switch view' }); + const renameBtn = toolbar.getByRole('button', { name: 'Rename' }); + const renameInput = page.locator('.affine-attachment-rename-container input'); + + const insertAttachment = async () => { + await page.evaluate(() => { + // Force fallback to input[type=file] in tests + // See https://github.com/microsoft/playwright/issues/8850 + window.showOpenFilePicker = undefined; + }); + + const slashMenu = page.locator(`.slash-menu`); + await waitNextFrame(page); + await type(page, '/'); + await resetHistory(page); + await expect(slashMenu).toBeVisible(); + await type(page, 'file', 100); + await expect(slashMenu).toBeVisible(); + + const fileChooser = page.waitForEvent('filechooser'); + await pressEnter(page); + await sleep(100); + await (await fileChooser).setFiles(FILE_PATH); + + // Try to break the undo redo test + await captureHistory(page); + + await expect(attachment).toBeVisible(); + }; + + const getName = () => + attachment.locator('.affine-attachment-content-title-text').innerText(); + + return { + // locators + attachment, + toolbar, + switchViewButton, + renameBtn, + renameInput, + + // actions + insertAttachment, + /** + * Wait for the attachment upload to finish + */ + waitLoading: () => loading.waitFor({ state: 'hidden' }), + getName, + getSize: () => + attachment.locator('.affine-attachment-content-info').innerText(), + + turnToEmbed: async () => { + await expect(switchViewButton).toBeVisible(); + await switchViewButton.click(); + await page.getByRole('button', { name: 'Embed view' }).click(); + await assertRichImage(page, 1); + }, + rename: async (newName: string) => { + await attachment.hover(); + await expect(toolbar).toBeVisible(); + await renameBtn.click(); + await page.keyboard.press(`${SHORT_KEY}+a`, { delay: 50 }); + await pressBackspace(page); + await type(page, newName); + await pressEnter(page); + expect(await getName()).toContain(newName); + }, + + // external + turnImageToCard: async () => { + const { turnIntoCardButton } = await popImageMoreMenu(page); + await turnIntoCardButton.click(); + await expect(attachment).toBeVisible(); + }, + }; +} + +test('can insert attachment from slash menu', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyParagraphState(page); + + const { insertAttachment, waitLoading, getName, getSize } = + getAttachment(page); + + await focusRichText(page); + await insertAttachment(); + + // Wait for the attachment to be uploaded + await waitLoading(); + + expect(await getName()).toBe(FILE_NAME); + expect(await getSize()).toBe('45.8 kB'); + + await assertStoreMatchJSX( + page, + ` + + +`, + noteId + ); +}); + +test('should undo/redo works for attachment', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyParagraphState(page); + + const { insertAttachment, waitLoading } = getAttachment(page); + + await focusRichText(page); + await insertAttachment(); + + // Wait for the attachment to be uploaded + await waitLoading(); + + await assertStoreMatchJSX( + page, + ` + +`, + noteId + ); + + await undoByKeyboard(page); + await waitNextFrame(page); + // The loading/error state should not be restored after undo + await assertStoreMatchJSX( + page, + ` + + +`, + noteId + ); + + await redoByKeyboard(page); + await waitNextFrame(page); + await assertStoreMatchJSX( + page, + ` + + +`, + noteId + ); +}); + +test('should rename attachment works', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/4534', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const { + attachment, + renameBtn, + renameInput, + insertAttachment, + waitLoading, + getName, + rename, + } = getAttachment(page); + + await focusRichText(page); + await insertAttachment(); + // Wait for the attachment to be uploaded + await waitLoading(); + + expect(await getName()).toBe(FILE_NAME); + + await attachment.hover(); + await expect(renameBtn).toBeVisible(); + await renameBtn.click(); + await assertKeyboardWorkInInput(page, renameInput); + await pressEscape(page); + await expect(renameInput).not.toBeVisible(); + + await rename('new-name'); + expect(await getName()).toBe('new-name.png'); + await rename(''); + expect(await getName()).toBe('.png'); + await rename('abc'); + expect(await getName()).toBe('abc'); +}); + +test('should turn attachment to image works', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyParagraphState(page); + const { insertAttachment, waitLoading, turnToEmbed, turnImageToCard } = + getAttachment(page); + + await focusRichText(page); + await insertAttachment(); + // Wait for the attachment to be uploaded + await waitLoading(); + + await turnToEmbed(); + + await assertStoreMatchJSX( + page, + ` + + +`, + noteId + ); + await turnImageToCard(); + await assertStoreMatchJSX( + page, + ` + + +`, + noteId + ); +}); + +test('should attachment can be deleted', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyParagraphState(page); + const { attachment, insertAttachment, waitLoading } = getAttachment(page); + + await focusRichText(page); + await insertAttachment(); + // Wait for the attachment to be uploaded + await waitLoading(); + + await attachment.click(); + await pressBackspace(page); + await assertStoreMatchJSX( + page, + ` + + +`, + noteId + ); +}); + +test.fixme(`support dragging attachment block directly`, async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyParagraphState(page); + + const { insertAttachment, waitLoading, getName, getSize } = + getAttachment(page); + + await focusRichText(page); + await insertAttachment(); + + // Wait for the attachment to be uploaded + await waitLoading(); + + expect(await getName()).toBe(FILE_NAME); + expect(await getSize()).toBe('45.8 kB'); + + await assertStoreMatchJSX( + page, + ` + +`, + noteId + ); + + const attachmentBlock = page.locator('affine-attachment'); + const rect = await attachmentBlock.boundingBox(); + if (!rect) { + throw new Error('image not found'); + } + + // add new paragraph blocks + await page.mouse.click(rect.x + 20, rect.y + rect.height + 20); + await focusRichText(page); + await type(page, '111'); + await page.waitForTimeout(200); + await pressEnter(page); + + await type(page, '222'); + await page.waitForTimeout(200); + await pressEnter(page); + + await type(page, '333'); + await page.waitForTimeout(200); + + await page.waitForTimeout(200); + await assertStoreMatchJSX( + page, + /*xml*/ ` + + + + + + +` + ); + + // drag bookmark block + await page.mouse.move(rect.x + 20, rect.y + 20); + await page.mouse.down(); + await page.mouse.move(rect.x + 40, rect.y + rect.height + 80, { steps: 20 }); + await page.mouse.up(); + + const rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(1); + + await assertStoreMatchJSX( + page, + /*xml*/ ` + + + + + + +` + ); +}); + +test('press backspace after bookmark block can select bookmark block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const { insertAttachment, waitLoading } = getAttachment(page); + + await focusRichText(page); + await pressEnter(page); + await pressArrowUp(page); + await insertAttachment(); + // Wait for the attachment to be uploaded + await waitLoading(); + + await focusRichText(page); + await assertBlockCount(page, 'paragraph', 1); + await assertRichTextInlineRange(page, 0, 0); + await pressBackspace(page); + await assertBlockSelections(page, ['4']); + await assertBlockCount(page, 'paragraph', 0); +}); + +test('cancel file picker with input element resolves', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + const { attachment } = getAttachment(page); + + await focusRichText(page); + await pressEnter(page); + await pressArrowUp(page); + + await page.evaluate(() => { + // Force fallback to input[type=file] + window.showOpenFilePicker = undefined; + }); + + const slashMenu = page.locator(`.slash-menu`); + await waitNextFrame(page); + await type(page, '/file', 100); + await expect(slashMenu).toBeVisible(); + + const fileChooser = page.waitForEvent('filechooser'); + await pressEnter(page); + const inputFile = page.locator("input[type='file']"); + await expect(inputFile).toHaveCount(1); + + // This does not trigger `cancel` event and, + // therefore, the test isn't representative. + // Waiting for https://github.com/microsoft/playwright/issues/27524 + await (await fileChooser).setFiles([]); + + await expect(attachment).toHaveCount(0); + await expect(inputFile).toHaveCount(0); +}); + +test('indent attachment block to paragraph', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const { insertAttachment, waitLoading } = getAttachment(page); + + await focusRichText(page); + await pressEnter(page); + await insertAttachment(); + // Wait for the attachment to be uploaded + await waitLoading(); + + await assertBlockChildrenIds(page, '1', ['2', '4']); + await assertBlockFlavour(page, '1', 'affine:note'); + await assertBlockFlavour(page, '2', 'affine:paragraph'); + await assertBlockFlavour(page, '4', 'affine:attachment'); + + await focusRichText(page); + await pressArrowDown(page); + await assertBlockSelections(page, ['4']); + await pressTab(page); + await assertBlockChildrenIds(page, '1', ['2']); + await assertBlockChildrenIds(page, '2', ['4']); + + await pressShiftTab(page); + await assertBlockChildrenIds(page, '1', ['2', '4']); +}); + +test('indent attachment block to list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const { insertAttachment, waitLoading } = getAttachment(page); + + await focusRichText(page); + await type(page, '- a'); + await pressEnter(page); + await insertAttachment(); + // Wait for the attachment to be uploaded + await waitLoading(); + + await assertBlockChildrenIds(page, '1', ['3', '5']); + await assertBlockFlavour(page, '1', 'affine:note'); + await assertBlockFlavour(page, '3', 'affine:list'); + await assertBlockFlavour(page, '5', 'affine:attachment'); + + await focusRichText(page); + await pressArrowDown(page); + await assertBlockSelections(page, ['5']); + await pressTab(page); + await assertBlockChildrenIds(page, '1', ['3']); + await assertBlockChildrenIds(page, '3', ['5']); + + await pressShiftTab(page); + await assertBlockChildrenIds(page, '1', ['3', '5']); +}); + +test('attachment can be dragged from note to surface top level block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + const { insertAttachment, waitLoading } = getAttachment(page); + + await focusRichText(page); + await insertAttachment(); + + // Wait for the attachment to be uploaded + await waitLoading(); + + await switchEditorMode(page); + await page.mouse.dblclick(450, 450); + + await dragBlockToPoint(page, '4', { x: 200, y: 200 }); + + await waitNextFrame(page); + await assertParentBlockFlavour(page, '4', 'affine:surface'); +}); diff --git a/blocksuite/tests-legacy/basic.spec.ts b/blocksuite/tests-legacy/basic.spec.ts new file mode 100644 index 0000000000000..eb089a68cf17f --- /dev/null +++ b/blocksuite/tests-legacy/basic.spec.ts @@ -0,0 +1,590 @@ +import type { DeltaInsert } from '@inline/types.js'; +import { expect } from '@playwright/test'; + +import { + addNoteByClick, + captureHistory, + click, + disconnectByClick, + enterPlaygroundRoom, + focusRichText, + focusTitle, + getCurrentEditorTheme, + getCurrentHTMLTheme, + getPageSnapshot, + initEmptyEdgelessState, + initEmptyParagraphState, + pressArrowLeft, + pressArrowRight, + pressBackspace, + pressEnter, + pressForwardDelete, + pressForwardDeleteWord, + pressShiftEnter, + redoByClick, + redoByKeyboard, + setSelection, + switchEditorMode, + toggleDarkMode, + type, + undoByClick, + undoByKeyboard, + waitDefaultPageLoaded, + waitNextFrame, +} from './utils/actions/index.js'; +import { + assertBlockChildrenIds, + assertEmpty, + assertRichTextInlineDeltas, + assertRichTexts, + assertText, + assertTitle, +} from './utils/asserts.js'; +import { scoped, test } from './utils/playwright.js'; +import { getFormatBar } from './utils/query.js'; + +const BASIC_DEFAULT_SNAPSHOT = 'basic test default'; + +test(scoped`basic input`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + + await test.expect(page).toHaveTitle(/BlockSuite/); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${BASIC_DEFAULT_SNAPSHOT}.json` + ); + await assertText(page, 'hello'); +}); + +test(scoped`basic init with external text`, async ({ page }) => { + await enterPlaygroundRoom(page); + + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text('hello'), + }); + const note = doc.addBlock('affine:note', {}, rootId); + + const text = new doc.Text('world'); + doc.addBlock('affine:paragraph', { text }, note); + + const delta = [ + { insert: 'foo ' }, + { insert: 'bar', attributes: { bold: true } }, + ]; + doc.addBlock( + 'affine:paragraph', + { + text: new doc.Text(delta as DeltaInsert[]), + }, + note + ); + }); + + await assertTitle(page, 'hello'); + await assertRichTexts(page, ['world', 'foo bar']); + await focusRichText(page); +}); + +test(scoped`basic multi user state`, async ({ context, page: pageA }) => { + const room = await enterPlaygroundRoom(pageA); + await initEmptyParagraphState(pageA); + await waitNextFrame(pageA); + await waitDefaultPageLoaded(pageA); + await focusTitle(pageA); + await type(pageA, 'hello'); + + const pageB = await context.newPage(); + await enterPlaygroundRoom(pageB, { + flags: {}, + room, + noInit: true, + }); + await waitDefaultPageLoaded(pageB); + await focusTitle(pageB); + await assertTitle(pageB, 'hello'); + + await type(pageB, ' world'); + await assertTitle(pageA, 'hello world'); +}); + +test( + scoped`A open and edit, then joins B`, + async ({ context, page: pageA }) => { + const room = await enterPlaygroundRoom(pageA); + await initEmptyParagraphState(pageA); + await waitNextFrame(pageA); + await focusRichText(pageA); + await type(pageA, 'hello'); + + const pageB = await context.newPage(); + await enterPlaygroundRoom(pageB, { + flags: {}, + room, + noInit: true, + }); + + // wait until pageB content updated + await assertText(pageB, 'hello'); + await Promise.all([ + assertText(pageA, 'hello'), + expect(await getPageSnapshot(pageA, true)).toMatchSnapshot( + `${BASIC_DEFAULT_SNAPSHOT}.json` + ), + expect(await getPageSnapshot(pageB, true)).toMatchSnapshot( + `${BASIC_DEFAULT_SNAPSHOT}.json` + ), + assertBlockChildrenIds(pageA, '0', ['1']), + assertBlockChildrenIds(pageB, '0', ['1']), + ]); + } +); + +test(scoped`A first open, B first edit`, async ({ context, page: pageA }) => { + const room = await enterPlaygroundRoom(pageA); + await initEmptyParagraphState(pageA); + await waitNextFrame(pageA); + await focusRichText(pageA); + + const pageB = await context.newPage(); + await enterPlaygroundRoom(pageB, { + room, + noInit: true, + }); + await pageB.waitForTimeout(500); + await focusRichText(pageB); + + await waitNextFrame(pageA); + await waitNextFrame(pageB); + await type(pageB, 'hello'); + await pageA.waitForTimeout(500); + + // wait until pageA content updated + await assertText(pageA, 'hello'); + await assertText(pageB, 'hello'); + await Promise.all([ + expect(await getPageSnapshot(pageA, true)).toMatchSnapshot( + `${BASIC_DEFAULT_SNAPSHOT}.json` + ), + expect(await getPageSnapshot(pageB, true)).toMatchSnapshot( + `${BASIC_DEFAULT_SNAPSHOT}.json` + ), + ]); +}); + +test( + scoped`does not sync when disconnected`, + async ({ browser, page: pageA }) => { + test.fail(); + + const room = await enterPlaygroundRoom(pageA); + const pageB = await browser.newPage(); + await enterPlaygroundRoom(pageB, { flags: {}, room }); + + await disconnectByClick(pageA); + await disconnectByClick(pageB); + + // click together, both init with default id should lead to conflicts + await initEmptyParagraphState(pageA); + await initEmptyParagraphState(pageB); + + await waitNextFrame(pageA); + await focusRichText(pageA); + await waitNextFrame(pageB); + await focusRichText(pageB); + await waitNextFrame(pageA); + + await type(pageA, ''); + await waitNextFrame(pageB); + await type(pageB, ''); + await waitNextFrame(pageA); + await type(pageA, 'hello'); + await waitNextFrame(pageB); + + await assertText(pageB, 'hello'); + await assertText(pageA, 'hello'); // actually '\n' + } +); + +test(scoped`basic paired undo/redo`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + + await assertText(page, 'hello'); + await undoByClick(page); + await assertEmpty(page); + await redoByClick(page); + await assertText(page, 'hello'); + + await undoByClick(page); + await assertEmpty(page); + await redoByClick(page); + await assertText(page, 'hello'); +}); + +test(scoped`undo/redo with keyboard`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + + await assertText(page, 'hello'); + await undoByKeyboard(page); + await assertEmpty(page); + await redoByClick(page); + await assertText(page, 'hello'); +}); + +test(scoped`undo after adding block twice`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + + await undoByKeyboard(page); + await assertRichTexts(page, ['hello']); + await redoByKeyboard(page); + await assertRichTexts(page, ['hello', 'world']); +}); + +test(scoped`undo/redo twice after adding block twice`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + await assertRichTexts(page, ['hello', 'world']); + + await undoByKeyboard(page); + await assertRichTexts(page, ['hello']); + + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await redoByClick(page); + await assertRichTexts(page, ['hello']); + + await redoByKeyboard(page); + await assertRichTexts(page, ['hello', 'world']); +}); + +test(scoped`should undo/redo works on title`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await waitNextFrame(page); + await focusTitle(page); + await type(page, 'title'); + await focusRichText(page); + await type(page, 'hello world'); + + await assertTitle(page, 'title'); + await assertRichTexts(page, ['hello world']); + + await captureHistory(page); + await pressBackspace(page, 5); + await captureHistory(page); + await focusTitle(page); + await type(page, ' something'); + + await assertTitle(page, 'title something'); + await assertRichTexts(page, ['hello ']); + + await focusRichText(page); + await undoByKeyboard(page); + await assertTitle(page, 'title'); + await assertRichTexts(page, ['hello ']); + await undoByKeyboard(page); + await assertTitle(page, 'title'); + await assertRichTexts(page, ['hello world']); + + await redoByKeyboard(page); + await assertTitle(page, 'title'); + await assertRichTexts(page, ['hello ']); + await redoByKeyboard(page); + await assertTitle(page, 'title something'); + await assertRichTexts(page, ['hello ']); +}); + +test(scoped`undo multi notes`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await addNoteByClick(page); + await assertRichTexts(page, ['', '']); + + await undoByClick(page); + await assertRichTexts(page, ['']); + + await redoByClick(page); + await assertRichTexts(page, ['', '']); +}); + +test(scoped`change theme`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const currentTheme = await getCurrentHTMLTheme(page); + await toggleDarkMode(page); + const expectNextTheme = currentTheme === 'light' ? 'dark' : 'light'; + const nextHTMLTheme = await getCurrentHTMLTheme(page); + expect(nextHTMLTheme).toBe(expectNextTheme); + + const nextEditorTheme = await getCurrentEditorTheme(page); + expect(nextEditorTheme).toBe(expectNextTheme); +}); + +test( + scoped`should be able to delete an emoji completely by pressing backspace once`, + async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2138', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '๐ŸŒท๐Ÿ™…โ€โ™‚๏ธ๐Ÿณ๏ธโ€๐ŸŒˆ'); + await pressBackspace(page); + await pressBackspace(page); + await pressBackspace(page); + await assertText(page, ''); + } +); + +test(scoped`delete emoji in the middle of the text`, async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2138', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '1๐ŸŒท1๐Ÿ™…โ€โ™‚๏ธ1๐Ÿณ๏ธโ€๐ŸŒˆ1๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ1'); + await pressArrowLeft(page, 1); + await pressBackspace(page); + await pressArrowLeft(page, 1); + await pressBackspace(page); + await pressArrowLeft(page, 1); + await pressBackspace(page); + await pressArrowLeft(page, 1); + await pressBackspace(page); + await assertText(page, '11111'); +}); + +test(scoped`delete emoji forward`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '1๐ŸŒท1๐Ÿ™…โ€โ™‚๏ธ1๐Ÿณ๏ธโ€๐ŸŒˆ1๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ1'); + await pressArrowLeft(page, 8); + await pressForwardDelete(page); + await pressArrowRight(page, 1); + await pressForwardDelete(page); + await pressArrowRight(page, 1); + await pressForwardDelete(page); + await pressArrowRight(page, 1); + await pressForwardDelete(page); + await assertText(page, '11111'); +}); + +test( + scoped`ZERO_WIDTH_SPACE should be counted by one cursor position`, + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await pressShiftEnter(page); + await type(page, 'asdfg'); + await pressEnter(page); + await undoByKeyboard(page); + await page.waitForTimeout(300); + await pressBackspace(page); + await assertRichTexts(page, ['\nasdf']); + } +); + +test('when no note block, click editing area auto add a new note block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await page.locator('affine-edgeless-note').click({ force: true }); + await pressBackspace(page); + await switchEditorMode(page); + const edgelessNote = await page.evaluate(() => { + return document.querySelector('affine-edgeless-note'); + }); + expect(edgelessNote).toBeNull(); + await click(page, { x: 200, y: 280 }); + + const pageNote = await page.evaluate(() => { + return document.querySelector('affine-note'); + }); + expect(pageNote).not.toBeNull(); +}); + +test(scoped`automatic identify url text`, async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'abc https://google.com '); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test('ctrl+delete to delete one word forward', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa bbb ccc'); + await pressArrowLeft(page, 8); + await pressForwardDeleteWord(page); + await assertText(page, 'aaa ccc'); +}); + +test('extended inline format', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaabbbaaa'); + + const { boldBtn, italicBtn, underlineBtn, strikeBtn, codeBtn } = + getFormatBar(page); + await setSelection(page, 0, 3, 0, 6); + await boldBtn.click(); + await italicBtn.click(); + await underlineBtn.click(); + await strikeBtn.click(); + await codeBtn.click(); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aaa', + }, + { + insert: 'bbb', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'aaa', + }, + ]); + + // aaa|bbbccc + await setSelection(page, 2, 3, 2, 3); + await captureHistory(page); + await type(page, 'c'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aaac', + }, + { + insert: 'bbb', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'aaa', + }, + ]); + await undoByKeyboard(page); + + // aaab|bbccc + await setSelection(page, 2, 4, 2, 4); + await type(page, 'c'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aaa', + }, + { + insert: 'bcbb', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'aaa', + }, + ]); + await undoByKeyboard(page); + + // aaab|b|bccc + await setSelection(page, 2, 4, 2, 5); + await type(page, 'c'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aaa', + }, + { + insert: 'bcb', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'aaa', + }, + ]); + await undoByKeyboard(page); + + // aaabbb|ccc + await setSelection(page, 2, 6, 2, 6); + await type(page, 'c'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aaa', + }, + { + insert: 'bbb', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'c', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + }, + }, + { + insert: 'aaa', + }, + ]); +}); diff --git a/blocksuite/tests-legacy/bookmark.spec.ts b/blocksuite/tests-legacy/bookmark.spec.ts new file mode 100644 index 0000000000000..f7f7909872557 --- /dev/null +++ b/blocksuite/tests-legacy/bookmark.spec.ts @@ -0,0 +1,461 @@ +import './utils/declare-test-window.js'; + +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { BlockSnapshot } from '@store/index.js'; +import { ignoreSnapshotId } from 'utils/ignore.js'; +import { getEmbedCardToolbar } from 'utils/query.js'; + +import { + activeNoteInEdgeless, + copyByKeyboard, + dragBlockToPoint, + enterPlaygroundRoom, + expectConsoleMessage, + focusRichText, + getPageSnapshot, + initEmptyEdgelessState, + initEmptyParagraphState, + pasteByKeyboard, + pressArrowDown, + pressArrowRight, + pressArrowUp, + pressBackspace, + pressEnter, + pressShiftTab, + pressTab, + selectAllByKeyboard, + setInlineRangeInSelectedRichText, + SHORT_KEY, + switchEditorMode, + type, + waitForInlineEditorStateUpdated, + waitNextFrame, +} from './utils/actions/index.js'; +import { + assertAlmostEqual, + assertBlockChildrenIds, + assertBlockCount, + assertBlockFlavour, + assertBlockSelections, + assertExists, + assertParentBlockFlavour, + assertRichTextInlineRange, +} from './utils/asserts.js'; +import { scoped, test } from './utils/playwright.js'; + +const LOCAL_HOST_URL = 'http://localhost'; + +const YOUTUBE_URL = 'https://www.youtube.com/watch?v=fakeid'; + +const FIGMA_URL = 'https://www.figma.com/design/JuXs6uOAICwf4I4tps0xKZ123'; + +test.beforeEach(async ({ page }) => { + await page.route( + 'https://affine-worker.toeverything.workers.dev/api/worker/link-preview', + async route => { + await route.fulfill({ + json: {}, + }); + } + ); +}); + +const createBookmarkBlockBySlashMenu = async ( + page: Page, + url = LOCAL_HOST_URL +) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await page.waitForTimeout(100); + await type(page, '/link', 100); + await pressEnter(page); + await page.waitForTimeout(100); + await type(page, url); + await pressEnter(page); +}; + +test(scoped`create bookmark by slash menu`, async ({ page }, testInfo) => { + await createBookmarkBlockBySlashMenu(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test(scoped`covert bookmark block to link text`, async ({ page }, testInfo) => { + await createBookmarkBlockBySlashMenu(page); + const bookmark = page.locator('affine-bookmark'); + await bookmark.click(); + await page.waitForTimeout(100); + await page.getByRole('button', { name: 'Switch view' }).click(); + await page.getByRole('button', { name: 'Inline view' }).click(); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test( + scoped`copy url to create bookmark in page mode`, + async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, LOCAL_HOST_URL); + await setInlineRangeInSelectedRichText(page, 0, LOCAL_HOST_URL.length); + await copyByKeyboard(page); + await focusRichText(page); + await type(page, '/link'); + await pressEnter(page); + await page.keyboard.press(`${SHORT_KEY}+v`); + await pressEnter(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); + } +); + +test( + scoped`copy url to create bookmark in edgeless mode`, + async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + const ids = await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, LOCAL_HOST_URL); + + await switchEditorMode(page); + + await activeNoteInEdgeless(page, ids.noteId); + await waitForInlineEditorStateUpdated(page); + await selectAllByKeyboard(page); + await copyByKeyboard(page); + await pressArrowRight(page); + await waitNextFrame(page); + await type(page, '/link', 100); + await pressEnter(page); + await page.waitForTimeout(100); + await waitNextFrame(page); + await page.keyboard.press(`${SHORT_KEY}+v`); + await pressEnter(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); + } +); + +test.fixme( + scoped`support dragging bookmark block directly`, + async ({ page }, testInfo) => { + await createBookmarkBlockBySlashMenu(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + const bookmark = page.locator('affine-bookmark'); + const rect = await bookmark.boundingBox(); + if (!rect) { + throw new Error('image not found'); + } + + // add new paragraph blocks + await page.mouse.click(rect.x + 20, rect.y + rect.height + 20); + await focusRichText(page); + await type(page, '111'); + await page.waitForTimeout(200); + await pressEnter(page); + + await type(page, '222'); + await page.waitForTimeout(200); + await pressEnter(page); + + await type(page, '333'); + await page.waitForTimeout(200); + + await page.waitForTimeout(200); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after_add_paragraph.json` + ); + + // drag bookmark block + await page.mouse.move(rect.x + 20, rect.y + 20); + await page.mouse.down(); + await page.waitForTimeout(200); + + await page.mouse.move(rect.x + 40, rect.y + rect.height + 80, { + steps: 5, + }); + await page.waitForTimeout(200); + + await page.mouse.up(); + await page.waitForTimeout(200); + + const rects = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(rects).toHaveCount(1); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after_drag.json` + ); + } +); + +test('press backspace after bookmark block can select bookmark block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await pressEnter(page); + await pressArrowUp(page); + await type(page, '/link'); + await pressEnter(page); + await page.waitForTimeout(100); + await type(page, LOCAL_HOST_URL); + await pressEnter(page); + + await focusRichText(page); + await assertBlockCount(page, 'paragraph', 1); + await assertRichTextInlineRange(page, 0, 0); + await pressBackspace(page); + await assertBlockSelections(page, ['4']); + await assertBlockCount(page, 'paragraph', 0); +}); + +test.describe('embed card toolbar', () => { + async function showEmbedCardToolbar(page: Page) { + await createBookmarkBlockBySlashMenu(page); + const bookmark = page.locator('affine-bookmark'); + await bookmark.click(); + await page.waitForTimeout(100); + const { embedCardToolbar } = getEmbedCardToolbar(page); + await expect(embedCardToolbar).toBeVisible(); + } + + test('show toolbar when bookmark selected', async ({ page }) => { + await showEmbedCardToolbar(page); + }); + + test('copy bookmark url by copy button', async ({ page }, testInfo) => { + await showEmbedCardToolbar(page); + const { copyButton } = getEmbedCardToolbar(page); + await copyButton.click(); + await page.mouse.click(600, 600); + await waitNextFrame(page); + + await pasteByKeyboard(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); + }); + + test('change card style', async ({ page }) => { + await showEmbedCardToolbar(page); + const bookmark = page.locator('affine-bookmark'); + const { openCardStyleMenu } = getEmbedCardToolbar(page); + await openCardStyleMenu(); + const { cardStyleHorizontalButton, cardStyleListButton } = + getEmbedCardToolbar(page); + await cardStyleListButton.click(); + await waitNextFrame(page); + const listStyleBookmarkBox = await bookmark.boundingBox(); + assertExists(listStyleBookmarkBox); + assertAlmostEqual(listStyleBookmarkBox.width, 752, 2); + assertAlmostEqual(listStyleBookmarkBox.height, 46, 2); + + await openCardStyleMenu(); + await cardStyleHorizontalButton.click(); + await waitNextFrame(page); + const horizontalStyleBookmarkBox = await bookmark.boundingBox(); + assertExists(horizontalStyleBookmarkBox); + assertAlmostEqual(horizontalStyleBookmarkBox.width, 752, 2); + assertAlmostEqual(horizontalStyleBookmarkBox.height, 116, 2); + }); +}); + +test('indent bookmark block to paragraph', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await pressEnter(page); + await type(page, '/link', 100); + await pressEnter(page); + await type(page, LOCAL_HOST_URL); + await pressEnter(page); + + await assertBlockChildrenIds(page, '1', ['2', '4']); + await assertBlockFlavour(page, '1', 'affine:note'); + await assertBlockFlavour(page, '2', 'affine:paragraph'); + await assertBlockFlavour(page, '4', 'affine:bookmark'); + + await focusRichText(page); + await pressArrowDown(page); + await assertBlockSelections(page, ['4']); + await pressTab(page); + await assertBlockChildrenIds(page, '1', ['2']); + await assertBlockChildrenIds(page, '2', ['4']); + + await pressShiftTab(page); + await assertBlockChildrenIds(page, '1', ['2', '4']); +}); + +test('indent bookmark block to list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '- a'); + await pressEnter(page); + await type(page, '/link', 100); + await pressEnter(page); + await type(page, LOCAL_HOST_URL); + await pressEnter(page); + + await assertBlockChildrenIds(page, '1', ['3', '5']); + await assertBlockFlavour(page, '1', 'affine:note'); + await assertBlockFlavour(page, '3', 'affine:list'); + await assertBlockFlavour(page, '5', 'affine:bookmark'); + + await focusRichText(page); + await pressArrowDown(page); + await assertBlockSelections(page, ['5']); + await pressTab(page); + await assertBlockChildrenIds(page, '1', ['3']); + await assertBlockChildrenIds(page, '3', ['5']); + + await pressShiftTab(page); + await assertBlockChildrenIds(page, '1', ['3', '5']); +}); + +test('bookmark can be dragged from note to surface top level block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await page.waitForTimeout(100); + await type(page, '/link', 100); + await pressEnter(page); + await page.waitForTimeout(100); + await type(page, LOCAL_HOST_URL); + await pressEnter(page); + + await switchEditorMode(page); + await page.mouse.dblclick(450, 450); + + await dragBlockToPoint(page, '4', { x: 200, y: 200 }); + + await waitNextFrame(page); + await assertParentBlockFlavour(page, '4', 'affine:surface'); +}); + +test.describe('embed youtube card', () => { + test(scoped`create youtube card by slash menu`, async ({ page }) => { + expectConsoleMessage(page, /Unrecognized feature/, 'warning'); + expectConsoleMessage(page, /Failed to load resource/); + await createBookmarkBlockBySlashMenu(page, YOUTUBE_URL); + const snapshot = (await getPageSnapshot(page)) as BlockSnapshot; + expect(ignoreSnapshotId(snapshot)).toMatchSnapshot('embed-youtube.json'); + }); + + test(scoped`change youtube card style`, async ({ page }) => { + expectConsoleMessage(page, /Unrecognized feature/, 'warning'); + expectConsoleMessage(page, /Failed to load resource/); + + await createBookmarkBlockBySlashMenu(page, YOUTUBE_URL); + const youtube = page.locator('affine-embed-youtube-block'); + await youtube.click(); + await page.waitForTimeout(100); + + // change to card view + const embedToolbar = page.locator('affine-embed-card-toolbar'); + await expect(embedToolbar).toBeVisible(); + const embedView = page.locator('editor-menu-button', { + hasText: 'embed view', + }); + await expect(embedView).toBeVisible(); + await embedView.click(); + const cardView = page.locator('editor-menu-action', { + hasText: 'card view', + }); + await expect(cardView).toBeVisible(); + await cardView.click(); + const snapshot = (await getPageSnapshot(page)) as BlockSnapshot; + expect(ignoreSnapshotId(snapshot)).toMatchSnapshot( + 'horizontal-youtube.json' + ); + + // change to embed view + const bookmark = page.locator('affine-bookmark'); + await bookmark.click(); + await page.waitForTimeout(100); + const cardView2 = page.locator('editor-icon-button', { + hasText: 'card view', + }); + await expect(cardView2).toBeVisible(); + await cardView2.click(); + const embedView2 = page.locator('editor-menu-action', { + hasText: 'embed view', + }); + await expect(embedView2).toBeVisible(); + await embedView2.click(); + const snapshot2 = (await getPageSnapshot(page)) as BlockSnapshot; + expect(ignoreSnapshotId(snapshot2)).toMatchSnapshot('embed-youtube.json'); + }); +}); + +test.describe('embed figma card', () => { + test(scoped`create figma card by slash menu`, async ({ page }) => { + expectConsoleMessage(page, /Failed to load resource/); + expectConsoleMessage(page, /Refused to frame/); + await createBookmarkBlockBySlashMenu(page, FIGMA_URL); + const snapshot = (await getPageSnapshot(page)) as BlockSnapshot; + expect(ignoreSnapshotId(snapshot)).toMatchSnapshot('embed-figma.json'); + }); + + test(scoped`change figma card style`, async ({ page }) => { + expectConsoleMessage(page, /Failed to load resource/); + expectConsoleMessage(page, /Refused to frame/); + expectConsoleMessage(page, /Running frontend commit/, 'log'); + await createBookmarkBlockBySlashMenu(page, FIGMA_URL); + const youtube = page.locator('affine-embed-figma-block'); + await youtube.click(); + await page.waitForTimeout(100); + + // change to card view + const embedToolbar = page.locator('affine-embed-card-toolbar'); + await expect(embedToolbar).toBeVisible(); + const embedView = page.locator('editor-menu-button', { + hasText: 'embed view', + }); + await expect(embedView).toBeVisible(); + await embedView.click(); + const cardView = page.locator('editor-menu-action', { + hasText: 'card view', + }); + await expect(cardView).toBeVisible(); + await cardView.click(); + const snapshot = (await getPageSnapshot(page)) as BlockSnapshot; + expect(ignoreSnapshotId(snapshot)).toMatchSnapshot('horizontal-figma.json'); + + // change to embed view + const bookmark = page.locator('affine-bookmark'); + await bookmark.click(); + await page.waitForTimeout(100); + const cardView2 = page.locator('editor-icon-button', { + hasText: 'card view', + }); + await expect(cardView2).toBeVisible(); + await cardView2.click(); + const embedView2 = page.locator('editor-menu-action', { + hasText: 'embed view', + }); + await expect(embedView2).toBeVisible(); + await embedView2.click(); + const snapshot2 = (await getPageSnapshot(page)) as BlockSnapshot; + expect(ignoreSnapshotId(snapshot2)).toMatchSnapshot('embed-figma.json'); + }); +}); diff --git a/blocksuite/tests-legacy/clipboard/clipboard.spec.ts b/blocksuite/tests-legacy/clipboard/clipboard.spec.ts new file mode 100644 index 0000000000000..f7519838c8d18 --- /dev/null +++ b/blocksuite/tests-legacy/clipboard/clipboard.spec.ts @@ -0,0 +1,409 @@ +import '../utils/declare-test-window.js'; + +import { expect } from '@playwright/test'; + +import { + captureHistory, + copyByKeyboard, + dragBetweenCoords, + dragOverTitle, + enterPlaygroundRoom, + focusRichText, + focusTitle, + getClipboardHTML, + getClipboardSnapshot, + getClipboardText, + getCurrentEditorDocId, + getEditorLocator, + getPageSnapshot, + initEmptyParagraphState, + mockParseDocUrlService, + pasteByKeyboard, + pasteContent, + pressEnter, + pressShiftTab, + pressTab, + resetHistory, + setInlineRangeInSelectedRichText, + setSelection, + SHORT_KEY, + type, + undoByClick, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertBlockTypes, + assertClipItems, + assertExists, + assertRichTexts, + assertText, + assertTitle, +} from '../utils/asserts.js'; +import { scoped, test } from '../utils/playwright.js'; + +test(scoped`clipboard copy paste`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'test'); + await setInlineRangeInSelectedRichText(page, 0, 3); + await waitNextFrame(page); + await copyByKeyboard(page); + await focusRichText(page); + await page.keyboard.press(`${SHORT_KEY}+v`); + await assertText(page, 'testtes'); +}); + +test(scoped`clipboard copy paste title`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + + await type(page, 'test'); + await dragOverTitle(page); + await waitNextFrame(page); + await copyByKeyboard(page); + await focusTitle(page); + await page.keyboard.press(`${SHORT_KEY}+v`); + await assertTitle(page, 'testtest'); +}); + +test(scoped`clipboard paste html`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + // set up clipboard data using html + const clipData = { + 'text/html': `aaabbbcccddd`, + }; + await waitNextFrame(page); + await page.evaluate( + ({ clipData }) => { + const dT = new DataTransfer(); + const e = new ClipboardEvent('paste', { clipboardData: dT }); + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + e.clipboardData?.setData('text/html', clipData['text/html']); + document.dispatchEvent(e); + }, + { clipData } + ); + await assertText(page, 'aaabbbcccddd'); +}); + +test(scoped`split block when paste`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await resetHistory(page); + + const clipData = { + 'text/plain': `# text +# h1 +`, + }; + await type(page, 'abc'); + await captureHistory(page); + + await setInlineRangeInSelectedRichText(page, 1, 1); + await pasteContent(page, clipData); + await waitNextFrame(page); + + await assertRichTexts(page, ['atext', 'h1c']); + + await undoByClick(page); + await assertRichTexts(page, ['abc']); + + await type(page, 'aa'); + await pressEnter(page); + await type(page, 'bb'); + const topLeft123 = await getEditorLocator(page) + .locator('[data-block-id="2"] .inline-editor') + .boundingBox(); + const bottomRight789 = await getEditorLocator(page) + .locator('[data-block-id="4"] .inline-editor') + .boundingBox(); + assertExists(topLeft123); + assertExists(bottomRight789); + await dragBetweenCoords(page, topLeft123, bottomRight789); + + // FIXME see https://github.com/toeverything/blocksuite/pull/878 + // await pasteContent(page, clipData); + // await assertRichTexts(page, ['aaa', 'bbc', 'text', 'h1']); +}); + +test(scoped`copy clipItems format`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await captureHistory(page); + + const clipData = ` +- aa + - bb + - cc + - dd +`; + + await pasteContent(page, { 'text/plain': clipData }); + await page.waitForTimeout(100); + await setSelection(page, 4, 1, 5, 1); + assertClipItems(page, 'text/plain', 'bc'); + assertClipItems(page, 'text/html', ''); + await undoByClick(page); + await assertRichTexts(page, ['']); +}); + +test(scoped`copy partially selected text`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '123 456 789'); + + // select 456 + await setInlineRangeInSelectedRichText(page, 4, 3); + await copyByKeyboard(page); + assertClipItems(page, 'text/plain', '456'); + + // move to line end + await setInlineRangeInSelectedRichText(page, 11, 0); + await pressEnter(page); + await pasteByKeyboard(page); + await waitNextFrame(page); + + await assertRichTexts(page, ['123 456 789', '456']); +}); + +test(scoped`copy & paste outside editor`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await page.evaluate(() => { + const input = document.createElement('input'); + input.setAttribute('id', 'input-test'); + input.value = '123'; + document.body.querySelector('#app')?.append(input); + }); + await page.focus('#input-test'); + await page.dblclick('#input-test'); + await copyByKeyboard(page); + await focusRichText(page); + await pasteByKeyboard(page); + await waitNextFrame(page); + await assertRichTexts(page, ['123']); +}); + +test('should keep first line format when pasted into a new line', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const clipData = ` +- [ ] aaa +`; + + await pasteContent(page, { 'text/plain': clipData }); + await waitNextFrame(page); + await assertRichTexts(page, ['aaa']); + await assertBlockTypes(page, ['todo']); +}); + +test(scoped`auto identify url`, async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + // set up clipboard data using html + const clipData = { + 'text/plain': `test https://www.google.com`, + }; + await waitNextFrame(page); + await page.evaluate( + ({ clipData }) => { + const dT = new DataTransfer(); + const e = new ClipboardEvent('paste', { clipboardData: dT }); + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + e.clipboardData?.setData('text/plain', clipData['text/plain']); + document.dispatchEvent(e); + }, + { clipData } + ); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test(scoped`pasting internal url`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'test page'); + + await focusRichText(page); + const docId = await getCurrentEditorDocId(page); + await mockParseDocUrlService(page, { + 'http://workspace/doc-id': docId, + }); + await pasteContent(page, { + 'text/plain': 'http://workspace/doc-id', + }); + await expect(page.locator('affine-reference')).toContainText('test page'); +}); + +test(scoped`pasting internal url with params`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'test page'); + + await focusRichText(page); + const docId = await getCurrentEditorDocId(page); + await mockParseDocUrlService(page, { + 'http://workspace/doc-id?mode=page&blockIds=rL2_GXbtLU2SsJVfCSmh_': docId, + }); + await pasteContent(page, { + 'text/plain': + 'http://workspace/doc-id?mode=page&blockIds=rL2_GXbtLU2SsJVfCSmh_', + }); + await expect(page.locator('affine-reference')).toContainText('test page'); +}); + +test( + scoped`pasting an external URL from clipboard to automatically creating a link from selection`, + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'test page'); + + await focusRichText(page); + await type(page, 'title alias'); + await setSelection(page, 1, 6, 1, 11); + + await pasteContent(page, { + 'text/plain': 'https://affine.pro/', + }); + await expect(page.locator('affine-link')).toContainText('alias'); + } +); + +test( + scoped`pasting an internal URL from clipboard to automatically creating a link from selection`, + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'test page'); + + await focusRichText(page); + await type(page, 'title alias'); + await setSelection(page, 1, 6, 1, 11); + + const docId = await getCurrentEditorDocId(page); + await mockParseDocUrlService(page, { + 'http://workspace/doc-id': docId, + }); + await pasteContent(page, { + 'text/plain': 'http://workspace/doc-id', + }); + await expect(page.locator('affine-reference')).toContainText('alias'); + } +); + +test(scoped`paste parent block`, async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/3153', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'This is parent'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Tab'); + await type(page, 'This is child 1'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Tab'); + await type(page, 'This is child 2'); + await setInlineRangeInSelectedRichText(page, 0, 3); + await copyByKeyboard(page); + await focusRichText(page, 2); + await page.keyboard.press(`${SHORT_KEY}+v`); + await assertRichTexts(page, [ + 'This is parent', + 'This is child 1', + 'This is child 2Thi', + ]); +}); + +test(scoped`clipboard copy multi selection`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'abc'); + await pressEnter(page); + await type(page, 'def'); + await setSelection(page, 2, 1, 3, 1); + await waitNextFrame(page); + await copyByKeyboard(page); + await waitNextFrame(page); + await focusRichText(page, 1); + await pasteByKeyboard(page); + await waitNextFrame(page); + await type(page, 'cursor'); + await waitNextFrame(page); + await assertRichTexts(page, ['abc', 'defbc', 'dcursor']); +}); + +test(scoped`clipboard copy nested items`, async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'abc'); + await pressEnter(page); + await pressTab(page); + await type(page, 'def'); + await pressEnter(page); + await pressTab(page); + await type(page, 'ghi'); + await pressEnter(page); + await pressShiftTab(page); + await pressShiftTab(page); + await type(page, 'jkl'); + await setSelection(page, 2, 1, 3, 1); + await waitNextFrame(page); + await copyByKeyboard(page); + + const text = await getClipboardText(page); + const html = await getClipboardHTML(page); + const snapshot = await getClipboardSnapshot(page); + expect(text).toMatchSnapshot(`${testInfo.title}-clipboard.md`); + expect(JSON.stringify(snapshot.snapshot.content, null, 2)).toMatchSnapshot( + `${testInfo.title}-clipboard.json` + ); + expect(html).toMatchSnapshot(`${testInfo.title}-clipboard.html`); + + await setSelection(page, 4, 1, 5, 1); + await waitNextFrame(page); + await copyByKeyboard(page); + + const text2 = await getClipboardText(page); + const html2 = await getClipboardHTML(page); + const snapshot2 = await getClipboardSnapshot(page); + expect(text2).toMatchSnapshot(`${testInfo.title}-clipboard2.md`); + expect(JSON.stringify(snapshot2.snapshot.content, null, 2)).toMatchSnapshot( + `${testInfo.title}-clipboard2.json` + ); + expect(html2).toMatchSnapshot(`${testInfo.title}-clipboard2.html`); +}); diff --git a/blocksuite/tests-legacy/clipboard/image.spec.ts b/blocksuite/tests-legacy/clipboard/image.spec.ts new file mode 100644 index 0000000000000..05ff21d442ac2 --- /dev/null +++ b/blocksuite/tests-legacy/clipboard/image.spec.ts @@ -0,0 +1,58 @@ +import { + enterPlaygroundRoom, + focusRichText, + initEmptyParagraphState, + pasteContent, + pressArrowDown, + pressArrowUp, + pressEscape, + waitEmbedLoaded, +} from '../utils/actions/index.js'; +import { assertRichImage, assertText } from '../utils/asserts.js'; +import { scoped, test } from '../utils/playwright.js'; + +test( + scoped`clipboard paste end with image, the cursor should be controlled by up/down keys`, + async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/3639', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + // set up clipboard data using html + const clipData = { + 'text/html': `

Lorem Ipsum placeholder text.

+
+ `, + }; + await page.evaluate( + ({ clipData }) => { + const dT = new DataTransfer(); + const e = new ClipboardEvent('paste', { clipboardData: dT }); + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + e.clipboardData?.setData('text/html', clipData['text/html']); + document.dispatchEvent(e); + }, + { clipData } + ); + const str = 'Lorem Ipsum placeholder text.'; + await waitEmbedLoaded(page); + await assertRichImage(page, 1); + await pressEscape(page); + await pressArrowUp(page, 1); + await pasteContent(page, clipData); + await assertRichImage(page, 2); + await assertText(page, str + str); + await pressArrowDown(page, 1); + await pressEscape(page); + await pasteContent(page, clipData); + await assertRichImage(page, 3); + await assertText(page, 'Lorem Ipsum placeholder text.', 1); + } +); diff --git a/blocksuite/tests-legacy/clipboard/list.spec.ts b/blocksuite/tests-legacy/clipboard/list.spec.ts new file mode 100644 index 0000000000000..ab8ee5c56cf8a --- /dev/null +++ b/blocksuite/tests-legacy/clipboard/list.spec.ts @@ -0,0 +1,714 @@ +import { expect } from '@playwright/test'; + +import { initDatabaseColumn } from '../database/actions.js'; +import { + activeNoteInEdgeless, + changeEdgelessNoteBackground, + copyByKeyboard, + createShapeElement, + cutByKeyboard, + dragBetweenCoords, + enterPlaygroundRoom, + focusRichText, + getAllNoteIds, + getClipboardHTML, + getClipboardSnapshot, + getClipboardText, + getEdgelessSelectedRectModel, + getInlineSelectionIndex, + getInlineSelectionText, + getPageSnapshot, + getRichTextBoundingBox, + initDatabaseDynamicRowWithData, + initEmptyDatabaseWithParagraphState, + initEmptyEdgelessState, + initEmptyParagraphState, + initThreeParagraphs, + pasteByKeyboard, + pasteContent, + pressArrowLeft, + pressArrowRight, + pressEnter, + pressEscape, + pressShiftTab, + pressSpace, + pressTab, + selectAllByKeyboard, + selectNoteInEdgeless, + setInlineRangeInSelectedRichText, + SHORT_KEY, + switchEditorMode, + toViewCoord, + triggerComponentToolbarAction, + type, + undoByKeyboard, + waitForInlineEditorStateUpdated, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertBlockTypes, + assertEdgelessNoteBackground, + assertEdgelessSelectedModelRect, + assertExists, + assertRichTextModelType, + assertRichTexts, + assertStoreMatchJSX, + assertText, +} from '../utils/asserts.js'; +import { scoped, test } from '../utils/playwright.js'; + +test('paste a non-nested list to a non-nested list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const clipData = { + 'text/plain': ` +- a +`, + }; + await type(page, '-'); + await pressSpace(page); + await type(page, '123'); + await page.keyboard.press('Control+ArrowLeft'); + + // paste on start + await waitNextFrame(page); + await pasteContent(page, clipData); + await pressArrowLeft(page); + await assertRichTexts(page, ['a123']); + + // paste in middle + await pressArrowRight(page, 2); + await pasteContent(page, clipData); + await pressArrowRight(page); + await assertRichTexts(page, ['a1a23']); + + // paste on end + await pressArrowRight(page); + await pasteContent(page, clipData); + await waitNextFrame(page); + await assertRichTexts(page, ['a1a23a']); + + await assertBlockTypes(page, ['bulleted']); +}); + +test('copy a nested list by clicking button, the clipboard data should be complete', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const clipData = { + 'text/plain': ` +- aaa + - bbb + - ccc +`, + }; + await pasteContent(page, clipData); + + const rootListBound = await page.locator('affine-list').first().boundingBox(); + assertExists(rootListBound); + + // use drag element to test. + await dragBetweenCoords( + page, + { x: rootListBound.x + 1, y: rootListBound.y - 1 }, + { x: rootListBound.x + 1, y: rootListBound.y + rootListBound.height - 1 } + ); + await copyByKeyboard(page); + + const text = await getClipboardText(page); + const html = await getClipboardHTML(page); + const snapshot = await getClipboardSnapshot(page); + expect(text).toMatchSnapshot(`${testInfo.title}-clipboard.md`); + expect(JSON.stringify(snapshot.snapshot.content, null, 2)).toMatchSnapshot( + `${testInfo.title}-clipboard.json` + ); + expect(html).toMatchSnapshot(`${testInfo.title}-clipboard.html`); +}); + +test('paste a nested list to a nested list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const clipData = { + 'text/plain': ` +- aaa + - bbb + - ccc +`, + }; + await pasteContent(page, clipData); + await focusRichText(page, 1); + + // paste on start + await page.keyboard.press('Control+ArrowLeft'); + + /** + * - aaa + * - |bbb + * - ccc + */ + await pasteContent(page, clipData); + /** + * - aaa + * - aaa + * - bbb + * - ccc|bbb + * -ccc + */ + + await assertRichTexts(page, ['aaa', 'aaa', 'bbb', 'cccbbb', 'ccc']); + expect(await getInlineSelectionText(page)).toEqual('cccbbb'); + expect(await getInlineSelectionIndex(page)).toEqual(3); + + // paste in middle + await undoByKeyboard(page); + await pressArrowRight(page); + + /** + * - aaa + * - b|bb + * - ccc + */ + await pasteContent(page, clipData); + /** + * - aaa + * - baaa + * - bbb + * - ccc|bb + * - ccc + */ + + await assertRichTexts(page, ['aaa', 'baaa', 'bbb', 'cccbb', 'ccc']); + expect(await getInlineSelectionText(page)).toEqual('cccbb'); + expect(await getInlineSelectionIndex(page)).toEqual(3); + + // paste on end + await undoByKeyboard(page); + await page.keyboard.press('Control+ArrowRight'); + + /** + * - aaa + * - bbb| + * - ccc + */ + await pasteContent(page, clipData); + /** + * - aaa + * - bbbaaa + * - bbb + * - ccc| + * - ccc + */ + + await assertRichTexts(page, ['aaa', 'bbbaaa', 'bbb', 'ccc', 'ccc']); + expect(await getInlineSelectionText(page)).toEqual('ccc'); + expect(await getInlineSelectionIndex(page)).toEqual(3); +}); + +test('paste nested lists to a nested list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const clipData = { + 'text/plain': ` +- aaa + - bbb + - ccc +`, + }; + await pasteContent(page, clipData); + await focusRichText(page, 1); + + const clipData2 = { + 'text/plain': ` +- 111 + - 222 +- 111 + - 222 +`, + }; + + // paste on start + await page.keyboard.press('Control+ArrowLeft'); + + /** + * - aaa + * - |bbb + * - ccc + */ + await pasteContent(page, clipData2); + /** + * - aaa + * - 111 + * - 222 + * - 111 + * - 222|bbb + * - ccc + */ + + await assertRichTexts(page, ['aaa', '111', '222', '111', '222bbb', 'ccc']); + expect(await getInlineSelectionText(page)).toEqual('222bbb'); + expect(await getInlineSelectionIndex(page)).toEqual(3); + + // paste in middle + await undoByKeyboard(page); + await pressArrowRight(page); + + /** + * - aaa + * - b|bb + * - ccc + */ + await pasteContent(page, clipData2); + /** + * - aaa + * - b111 + * - 222 + * - 111 + * - 222|bb + * - ccc + */ + + await assertRichTexts(page, ['aaa', 'b111', '222', '111', '222bb', 'ccc']); + expect(await getInlineSelectionText(page)).toEqual('222bb'); + expect(await getInlineSelectionIndex(page)).toEqual(3); + + // paste on end + await undoByKeyboard(page); + await page.keyboard.press('Control+ArrowRight'); + + /** + * - aaa + * - bbb| + * - ccc + */ + await pasteContent(page, clipData2); + /** + * - aaa + * - bbb111 + * - 222 + * - 111 + * - 222| + * - ccc + */ + + await assertRichTexts(page, ['aaa', 'bbb111', '222', '111', '222', 'ccc']); + expect(await getInlineSelectionText(page)).toEqual('222'); + expect(await getInlineSelectionIndex(page)).toEqual(3); +}); + +test('paste non-nested lists to a nested list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const clipData = { + 'text/plain': ` +- aaa + - bbb +`, + }; + await pasteContent(page, clipData); + await focusRichText(page, 0); + + const clipData2 = { + 'text/plain': ` +- 123 +- 456 +`, + }; + + // paste on start + await page.keyboard.press('Control+ArrowLeft'); + + /** + * - |aaa + * - bbb + */ + await pasteContent(page, clipData2); + /** + * - 123 + * - 456|aaa + * - bbb + */ + + await assertRichTexts(page, ['123', '456aaa', 'bbb']); + expect(await getInlineSelectionText(page)).toEqual('456aaa'); + expect(await getInlineSelectionIndex(page)).toEqual(3); +}); + +test(scoped`cut should work for multi-block selection`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'a'); + await pressEnter(page); + await type(page, 'b'); + await pressEnter(page); + await type(page, 'c'); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await cutByKeyboard(page); + await page.locator('.affine-page-viewport').click(); + await waitNextFrame(page); + await assertText(page, ''); +}); + +test( + scoped`pasting into empty list should not convert the list into paragraph`, + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'test'); + await setInlineRangeInSelectedRichText(page, 0, 4); + await copyByKeyboard(page); + await type(page, '- '); + await page.keyboard.press(`${SHORT_KEY}+v`); + await assertRichTexts(page, ['test']); + await assertRichTextModelType(page, 'bulleted'); + } +); + +test('cut will delete all content, and copy will reappear content', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '-'); + await pressSpace(page); + await type(page, '1'); + await pressEnter(page); + await pressTab(page); + await type(page, '2'); + await pressEnter(page); + await type(page, '3'); + await pressEnter(page); + await pressShiftTab(page); + await type(page, '4'); + + const box123 = await getRichTextBoundingBox(page, '1'); + const inside123 = { x: box123.left + 1, y: box123.top + 1 }; + + const box789 = await getRichTextBoundingBox(page, '6'); + const inside789 = { x: box789.right - 1, y: box789.bottom - 1 }; + // from top to bottom + await dragBetweenCoords(page, inside123, inside789); + + await cutByKeyboard(page); + await waitNextFrame(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after-cut.json` + ); + await waitNextFrame(page); + await focusRichText(page); + + await pasteByKeyboard(page); + await waitNextFrame(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after-paste.json` + ); +}); + +test(scoped`should copy and paste of database work`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseWithParagraphState(page); + + // init database columns and rows + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, 'abc', true); + await pressEscape(page); + await focusRichText(page, 1); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await copyByKeyboard(page); + await pressEnter(page); + await pasteByKeyboard(page); + await page.waitForTimeout(100); + + await assertStoreMatchJSX( + page, + /*xml*/ ` + + + + + + + + + + +` + ); + + await undoByKeyboard(page); + await assertStoreMatchJSX( + page, + /*xml*/ ` + + + + + + + +` + ); +}); + +test(`copy canvas element and text note in edgeless mode`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await initThreeParagraphs(page); + await createShapeElement(page, [0, 0], [100, 100]); + await selectAllByKeyboard(page); + const bound = await getEdgelessSelectedRectModel(page); + await copyByKeyboard(page); + const coord = await toViewCoord(page, [ + bound[0] + bound[2] / 2, + bound[1] + bound[3] / 2 + 200, + ]); + await page.mouse.move(coord[0], coord[1]); + await page.waitForTimeout(300); + await pasteByKeyboard(page, false); + bound[1] = bound[1] + 200; + await assertEdgelessSelectedModelRect(page, bound); +}); + +test(scoped`copy when text note active in edgeless`, async ({ page }) => { + await enterPlaygroundRoom(page); + const ids = await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, '1234'); + + await switchEditorMode(page); + + await activeNoteInEdgeless(page, ids.noteId); + await waitForInlineEditorStateUpdated(page); + await setInlineRangeInSelectedRichText(page, 0, 4); + await copyByKeyboard(page); + await pressArrowRight(page); + await type(page, '555'); + await pasteByKeyboard(page, false); + await assertText(page, '12345551234'); +}); + +test(scoped`paste note block with background`, async ({ page }) => { + await enterPlaygroundRoom(page); + const ids = await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, '1234'); + + await switchEditorMode(page); + await selectNoteInEdgeless(page, ids.noteId); + + await triggerComponentToolbarAction(page, 'changeNoteColor'); + const color = '--affine-note-background-grey'; + await changeEdgelessNoteBackground(page, color); + await assertEdgelessNoteBackground(page, ids.noteId, color); + + await copyByKeyboard(page); + + await page.mouse.move(0, 0); + await pasteByKeyboard(page, false); + const noteIds = await getAllNoteIds(page); + for (const noteId of noteIds) { + await assertEdgelessNoteBackground(page, noteId, color); + } +}); + +test(scoped`copy and paste to selection block selection`, async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2265', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '1234'); + + await selectAllByKeyboard(page); + await copyByKeyboard(page); + await pressArrowRight(page); + await pasteByKeyboard(page, false); + await waitNextFrame(page); + await assertRichTexts(page, ['12341234']); +}); + +test( + scoped`should keep paragraph block's type when pasting at the start of empty paragraph block except type text`, + async ({ page }, testInfo) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2336', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await focusRichText(page); + await type(page, '>'); + await page.keyboard.press('Space', { delay: 50 }); + + await page.evaluate(() => { + const input = document.createElement('input'); + input.setAttribute('id', 'input-test'); + input.value = '123'; + document.body.querySelector('#app')?.append(input); + }); + await page.focus('#input-test'); + await page.dblclick('#input-test'); + await copyByKeyboard(page); + await focusRichText(page); + await pasteByKeyboard(page); + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after-paste-1.json` + ); + + await pressEnter(page); + await waitNextFrame(page); + await pressEnter(page); + await waitNextFrame(page); + await pasteByKeyboard(page, false); + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after-paste-2.json` + ); + } +); + +test(scoped`paste from FeiShu list format`, async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2438', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + // set up clipboard data using html + const clipData = { + 'text/html': `
  • aaaa
  • `, + }; + await waitNextFrame(page); + await page.evaluate( + ({ clipData }) => { + const dT = new DataTransfer(); + const e = new ClipboardEvent('paste', { clipboardData: dT }); + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + e.clipboardData?.setData('text/html', clipData['text/html']); + document.dispatchEvent(e); + }, + { clipData } + ); + await assertText(page, 'aaaa'); + await assertBlockTypes(page, ['bulleted']); +}); + +test(scoped`paste in list format`, async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2281', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '- test'); + await focusRichText(page); + + const clipData = { + 'text/html': ``, + }; + await waitNextFrame(page); + await page.evaluate( + ({ clipData }) => { + const dT = new DataTransfer(); + const e = new ClipboardEvent('paste', { clipboardData: dT }); + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + e.clipboardData?.setData('text/html', clipData['text/html']); + document.dispatchEvent(e); + }, + { clipData } + ); + await assertRichTexts(page, ['test111', '222']); +}); diff --git a/blocksuite/tests-legacy/clipboard/markdown.spec.ts b/blocksuite/tests-legacy/clipboard/markdown.spec.ts new file mode 100644 index 0000000000000..1484fdb6ae5e6 --- /dev/null +++ b/blocksuite/tests-legacy/clipboard/markdown.spec.ts @@ -0,0 +1,170 @@ +import { + enterPlaygroundRoom, + focusRichText, + initEmptyParagraphState, + pasteContent, + resetHistory, + undoByClick, + waitEmbedLoaded, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertBlockTypes, + assertRichImage, + assertRichTexts, + assertTextFormats, +} from '../utils/asserts.js'; +import { scoped, test } from '../utils/playwright.js'; + +test(scoped`markdown format parse`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await resetHistory(page); + + let clipData = { + 'text/plain': `# h1 + +## h2 + +### h3 + +#### h4 + +##### h5 + +###### h6 + +- [ ] todo + +- [ ] todo + +- [x] todo + +* bulleted + +- bulleted + +1. numbered + +> quote +`, + }; + await waitNextFrame(page); + await pasteContent(page, clipData); + await page.waitForTimeout(200); + await assertBlockTypes(page, [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'todo', + 'todo', + 'todo', + 'bulleted', + 'bulleted', + 'numbered', + 'quote', + ]); + await assertRichTexts(page, [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'todo', + 'todo', + 'todo', + 'bulleted', + 'bulleted', + 'numbered', + 'quote', + ]); + await undoByClick(page); + await assertRichTexts(page, ['']); + await focusRichText(page); + + clipData = { + 'text/plain': `# ***bolditalic*** +# **bold** + +*italic* + +~~strikethrough~~ + +[link](linktest) + +\`code\` +`, + }; + await waitNextFrame(page); + await pasteContent(page, clipData); + await page.waitForTimeout(200); + await assertTextFormats(page, [ + { bold: true, italic: true }, + { bold: true }, + { italic: true }, + { strike: true }, + { link: 'linktest' }, + { code: true }, + ]); + await undoByClick(page); + await assertRichTexts(page, ['']); +}); + +test(scoped`import markdown`, async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await resetHistory(page); + const clipData = `# text +# h1 +`; + await pasteContent(page, { 'text/plain': clipData }); + await page.waitForTimeout(100); + await assertRichTexts(page, ['text', 'h1']); + await undoByClick(page); + await assertRichTexts(page, ['']); +}); + +test( + scoped`clipboard paste HTML containing markdown syntax code and image `, + async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2855', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + // set up clipboard data using html + const clipData = { + 'text/html': `

    ็ฌฆๅˆ Markdown ๆ ผๅผ็š„ URL ๆ”พๅˆฐ็ฌ”่ฎฐไธญ๏ผŒๆญคๆ—ถ้œ€่ฆ็š„ๆ ผๅผๅฆ‚ไธ‹๏ผš

    +
    md [ไปปๅŠก็ฎก็†่ฟ™ไปถไบ‹ - ๅฐ‘ๆ•ฐๆดพ](https://sspai.com/post/61092)
    +

    ๏ผˆๅฐ†ไธ€ๆฎตๆ–‡ๅญ—ๅŒ…่ฃนๅœจ[[]]ไธญ๏ผ‰ๆญคๆ—ถ้œ€่ฆ็š„ๆ ผๅผๅฆ‚ไธ‹๏ผš

    +
    +

    ไธŠๅ›พไธญ๏ผŒๅฝ“ๆˆ‘ไปฌๅค„ๅœจ Obsidian ็š„ใ€Œ้ข„่งˆๆจกๅผใ€ๆ—ถ๏ผŒ็‚นๅ‡ป่ฟ™ไธชใ€ŒๅŒๅ‘้“พๆŽฅใ€

    + `, + }; + await page.evaluate( + ({ clipData }) => { + const dT = new DataTransfer(); + const e = new ClipboardEvent('paste', { clipboardData: dT }); + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + e.clipboardData?.setData('text/html', clipData['text/html']); + document.dispatchEvent(e); + }, + { clipData } + ); + await waitEmbedLoaded(page); + // await page.waitForTimeout(500); + await assertRichImage(page, 1); + } +); diff --git a/blocksuite/tests-legacy/code/copy-paste.spec.ts b/blocksuite/tests-legacy/code/copy-paste.spec.ts new file mode 100644 index 0000000000000..9e7a9cd3d46d9 --- /dev/null +++ b/blocksuite/tests-legacy/code/copy-paste.spec.ts @@ -0,0 +1,149 @@ +import { expect } from '@playwright/test'; + +import { + copyByKeyboard, + pasteByKeyboard, + pressArrowLeft, + pressEnter, + pressEnterWithShortkey, + selectAllByKeyboard, + type, +} from '../utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + focusRichText, + getInlineSelectionText, + getPageSnapshot, + initEmptyCodeBlockState, + setSelection, +} from '../utils/actions/misc.js'; +import { assertRichTextInlineRange } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; +import { getCodeBlock } from './utils.js'; + +test('keyboard selection and copy paste', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'use'); + await page.keyboard.down('Shift'); + await pressArrowLeft(page, 'use'.length); + await page.keyboard.up('Shift'); + await copyByKeyboard(page); + await pressArrowLeft(page, 1); + await pasteByKeyboard(page); + + const content = await getInlineSelectionText(page); + expect(content).toBe('useuse'); + + await assertRichTextInlineRange(page, 0, 3, 0); +}); + +test('paste with more than one continuous breakline should remain in code block, ', async ({ + page, +}) => { + await page.setContent(`
    use super::*; +use fern::{ + colors::{Color, ColoredLevelConfig}, + Dispatch, +}; +

    +#[inline]
    `); + await page.focus('div'); + await selectAllByKeyboard(page); + await copyByKeyboard(page); + + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + await pasteByKeyboard(page); + + const locator = page.locator('affine-paragraph'); + await expect(locator).toBeHidden(); +}); + +test('drag copy paste', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'use'); + + await setSelection(page, 2, 0, 2, 3); + await copyByKeyboard(page); + await pressArrowLeft(page); + await pasteByKeyboard(page); + + const content = await getInlineSelectionText(page); + expect(content).toBe('useuse'); + + await assertRichTextInlineRange(page, 0, 3, 0); +}); + +test.skip('use keyboard copy inside code block copy', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'use'); + await page.keyboard.down('Shift'); + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < 'use'.length; i++) { + await page.keyboard.press('ArrowLeft'); + } + await page.keyboard.up('Shift'); + await copyByKeyboard(page); + await page.keyboard.press('ArrowRight'); + await pressEnter(page); + await pressEnter(page); + await pasteByKeyboard(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_pasted.json` + ); +}); + +test('code block has content, click code block copy menu, copy whole code block', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page, { language: 'javascript' }); + await focusRichText(page); + await page.keyboard.type('use'); + await pressEnterWithShortkey(page); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + + await expect(codeBlockController.copyButton).toBeVisible(); + await codeBlockController.copyButton.click(); + + await focusRichText(page, 1); + await pasteByKeyboard(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_pasted.json` + ); +}); + +test('code block is empty, click code block copy menu, copy the empty code block', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page, { language: 'javascript' }); + await focusRichText(page); + + await pressEnterWithShortkey(page); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + await expect(codeBlockController.copyButton).toBeVisible(); + await codeBlockController.copyButton.click(); + + await focusRichText(page, 1); + await pasteByKeyboard(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_pasted.json` + ); +}); diff --git a/blocksuite/tests-legacy/code/crud.spec.ts b/blocksuite/tests-legacy/code/crud.spec.ts new file mode 100644 index 0000000000000..fc8cc5a5a04a9 --- /dev/null +++ b/blocksuite/tests-legacy/code/crud.spec.ts @@ -0,0 +1,668 @@ +import { expect } from '@playwright/test'; +import { dragBetweenIndices } from 'utils/actions/drag.js'; +import { getFormatBar } from 'utils/query.js'; + +import { updateBlockType } from '../utils/actions/block.js'; +import { + createCodeBlock, + pressArrowLeft, + pressArrowUp, + pressBackspace, + pressEnter, + pressEscape, + pressShiftTab, + pressTab, + redoByKeyboard, + type, + undoByKeyboard, +} from '../utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + focusRichText, + focusRichTextEnd, + getPageSnapshot, + initEmptyCodeBlockState, + initEmptyParagraphState, + setSelection, + waitNextFrame, +} from '../utils/actions/misc.js'; +import { + assertBlockCount, + assertRichTexts, + assertStoreMatchJSX, + assertTitle, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; +import { getCodeBlock } from './utils.js'; + +test('use debug menu can create code block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await updateBlockType(page, 'affine:code'); + + const locator = page.locator('affine-code'); + await expect(locator).toBeVisible(); +}); + +test('use markdown syntax can create code block', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + await pressEnter(page); + await type(page, 'bbb'); + await pressTab(page); + await pressEnter(page); + await type(page, 'ccc'); + await pressTab(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await setSelection(page, 2, 0, 2, 0); + // |aaa + // bbb + // ccc + + await type(page, '``` '); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_markdown_syntax.json` + ); +}); + +test('use markdown syntax with trailing characters can create code block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '```JavaScript'); + await type(page, ' '); + + const locator = page.locator('affine-code'); + await expect(locator).toBeVisible(); +}); + +test('support ```[lang] to add code block with language', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/1314', + }); + + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '```ts'); + await type(page, ' '); + + const codeBlockController = getCodeBlock(page); + const codeLocator = codeBlockController.codeBlock; + await expect(codeLocator).toBeVisible(); + + const codeRect = await codeLocator.boundingBox(); + if (!codeRect) { + throw new Error('Failed to get bounding box of code block.'); + } + const position = { + x: codeRect.x + codeRect.width / 2, + y: codeRect.y + codeRect.height / 2, + }; + await page.mouse.move(position.x, position.y); + + const languageButton = codeBlockController.languageButton; + await expect(languageButton).toBeVisible(); + await expect(languageButton).toHaveText('TypeScript'); +}); + +test('use more than three backticks can not create code block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '`````'); + await type(page, ' '); + + const codeBlockLocator = page.locator('affine-code'); + await expect(codeBlockLocator).toBeHidden(); + const inlineCodelocator = page.getByText('```'); + await expect(inlineCodelocator).toBeVisible(); + expect(await inlineCodelocator.count()).toEqual(1); +}); + +test('use shortcut can create code block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await createCodeBlock(page); + + const locator = page.locator('affine-code'); + await expect(locator).toBeVisible(); +}); + +test('change code language can work', async ({ page }) => { + await enterPlaygroundRoom(page); + const { codeBlockId } = await initEmptyCodeBlockState(page); + await focusRichText(page); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + await codeBlockController.clickLanguageButton(); + const locator = codeBlockController.langList; + await expect(locator).toBeVisible(); + + await type(page, 'rust'); + await page.click( + '.affine-filterable-list > .items-container > icon-button:nth-child(1)' + ); + await expect(locator).toBeHidden(); + + await codeBlockController.codeBlock.hover(); + await expect(codeBlockController.languageButton).toHaveText('Rust'); + + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); + await undoByKeyboard(page); + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); + + // Can switch to another language + await codeBlockController.clickLanguageButton(); + await type(page, 'ty'); + await pressEnter(page); + await expect(locator).toBeHidden(); + await expect(codeBlockController.languageButton).toHaveText('TypeScript'); +}); + +test('duplicate code block', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page, { language: 'javascript' }); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + + // change language + await codeBlockController.clickLanguageButton(); + const langLocator = codeBlockController.langList; + await expect(langLocator).toBeVisible(); + await type(page, 'rust'); + await page.click( + '.affine-filterable-list > .items-container > icon-button:nth-child(1)' + ); + + // add text + await focusRichTextEnd(page); + await type(page, 'let a: u8 = 7'); + await pressEscape(page); + await waitNextFrame(page, 100); + + // add a caption + await codeBlockController.codeBlock.hover(); + await codeBlockController.captionButton.click(); + await type(page, 'BlockSuite'); + await pressEnter(page); + await pressBackspace(page); // remove paragraph + await waitNextFrame(page, 100); + + // turn on wrap + await codeBlockController.codeBlock.hover(); + await (await codeBlockController.openMore()).wrapButton.click(); + + // duplicate + await codeBlockController.codeBlock.hover(); + await (await codeBlockController.openMore()).duplicateButton.click(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test('delete code block in more menu', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page, { language: 'javascript' }); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + const moreMenu = await codeBlockController.openMore(); + + await expect(moreMenu.menu).toBeVisible(); + await moreMenu.deleteButton.click(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test('undo and redo works in code block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'const a = 10;'); + await assertRichTexts(page, ['const a = 10;']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await redoByKeyboard(page); + await assertRichTexts(page, ['const a = 10;']); +}); + +test('toggle code block wrap can work', async ({ page }) => { + await enterPlaygroundRoom(page); + const { codeBlockId } = await initEmptyCodeBlockState(page); + + const codeBlockController = getCodeBlock(page); + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); + + await codeBlockController.codeBlock.hover(); + await (await codeBlockController.openMore()).wrapButton.click(); + + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); + + await codeBlockController.codeBlock.hover(); + await (await codeBlockController.openMore()).cancelWrapButton.click(); + + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); +}); + +test('add caption works', async ({ page }) => { + await enterPlaygroundRoom(page); + const { codeBlockId } = await initEmptyCodeBlockState(page); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + await codeBlockController.captionButton.click(); + await type(page, 'BlockSuite'); + await pressEnter(page); + await waitNextFrame(page, 100); + + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); +}); + +test('undo code block wrap can work', async ({ page }) => { + await enterPlaygroundRoom(page); + const { codeBlockId } = await initEmptyCodeBlockState(page); + await focusRichText(page); + + const codeBlockController = getCodeBlock(page); + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); + + await codeBlockController.codeBlock.hover(); + await (await codeBlockController.openMore()).wrapButton.click(); + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); + + await focusRichText(page); + await undoByKeyboard(page); + await assertStoreMatchJSX( + page, + /*xml*/ ` +`, + codeBlockId + ); +}); + +test('code block toolbar widget can appear and disappear during mousemove', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + const position = await page.locator('affine-code').boundingBox(); + if (!position) throw new Error('Failed to get affine code position'); + await page.mouse.move(position.x, position.y); + + const locator = page.locator('.code-toolbar-container'); + const toolbarPosition = await locator.boundingBox(); + if (!toolbarPosition) throw new Error('Failed to get option position'); + await page.mouse.move(toolbarPosition.x, toolbarPosition.y); + await expect(locator).toBeVisible(); + await page.mouse.move(position.x - 10, position.y - 10); + await expect(locator).toBeHidden(); +}); + +test('should tab works in code block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'const a = 10;'); + await assertRichTexts(page, ['const a = 10;']); + await page.keyboard.press('Tab', { delay: 50 }); + await assertRichTexts(page, [' const a = 10;']); + await page.keyboard.press(`Shift+Tab`, { delay: 50 }); + await assertRichTexts(page, ['const a = 10;']); + + await page.keyboard.press('Enter', { delay: 50 }); + await type(page, 'const b = "NothingToSay'); + await page.keyboard.press('ArrowUp', { delay: 50 }); + await page.keyboard.press('Enter', { delay: 50 }); + await page.keyboard.press('Tab', { delay: 50 }); + await assertRichTexts(page, ['const a = 10;\n \nconst b = "NothingToSay"']); +}); + +test('should open more menu and close on selecting', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + await expect(codeBlockController.codeToolbar).toBeVisible(); + const moreMenu = await codeBlockController.openMore(); + + await expect(moreMenu.menu).toBeVisible(); + await moreMenu.wrapButton.click(); + await expect(moreMenu.menu).toBeHidden(); +}); + +test('should code block lang input supports alias', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + const codeBlockController = getCodeBlock(page); + const codeBlock = codeBlockController.codeBlock; + await codeBlock.hover(); + await codeBlockController.clickLanguageButton(); + await expect(codeBlockController.langList).toBeVisible(); + await type(page, 'ๆ–‡่จ€'); + await pressEnter(page); + await expect(codeBlockController.languageButton).toHaveText('Wenyan'); +}); + +test('multi-line indent', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'aaa'); + await pressEnter(page); + + await type(page, 'bbb'); + await pressEnter(page); + + await type(page, 'ccc'); + + await page.keyboard.down('Shift'); + await pressArrowUp(page, 2); + await page.keyboard.up('Shift'); + + await pressTab(page); + + await assertRichTexts(page, [' aaa\n bbb\n ccc']); + + await pressShiftTab(page); + + await assertRichTexts(page, ['aaa\nbbb\nccc']); + + await pressShiftTab(page); + + await assertRichTexts(page, ['aaa\nbbb\nccc']); +}); + +test('should bracket complete works in code block', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/1800', + }); + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'const a = "'); + await assertRichTexts(page, ['const a = ""']); + + await type(page, 'str'); + await assertRichTexts(page, ['const a = "str"']); + await type(page, '('); + await assertRichTexts(page, ['const a = "str()"']); + await type(page, ']'); + await assertRichTexts(page, ['const a = "str(])"']); +}); + +test('auto scroll horizontally when typing', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '``` '); + + for (let i = 0; i < 100; i++) { + await type(page, String(i)); + } + + const richTextScrollLeft1 = await page.evaluate(() => { + const richText = document.querySelector('affine-code rich-text'); + if (!richText) { + throw new Error('Failed to get rich text'); + } + + return richText.scrollLeft; + }); + expect(richTextScrollLeft1).toBeGreaterThan(200); + + await pressArrowLeft(page, 5); + await type(page, 'aa'); + + const richTextScrollLeft2 = await page.evaluate(() => { + const richText = document.querySelector('affine-code rich-text'); + if (!richText) { + throw new Error('Failed to get rich text'); + } + + return richText.scrollLeft; + }); + + expect(richTextScrollLeft2).toEqual(richTextScrollLeft1); +}); + +test('code hotkey should not effect in global', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await pressEnter(page); + await type(page, '``` '); + + await assertTitle(page, ''); + await assertBlockCount(page, 'paragraph', 1); + await assertBlockCount(page, 'code', 1); + + await pressArrowUp(page); + await pressBackspace(page); + await type(page, 'aaa'); + + await assertTitle(page, 'aaa'); + await assertBlockCount(page, 'paragraph', 0); + await assertBlockCount(page, 'code', 1); +}); + +test('language selection list should not close when hovering out of code block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page, { language: 'javascript' }); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + + await codeBlockController.clickLanguageButton(); + const langLocator = codeBlockController.langList; + await expect(langLocator).toBeVisible(); + + const bBox = await codeBlockController.codeBlock.boundingBox(); + if (!bBox) throw new Error('Expected bounding box'); + + const { x, y, width, height } = bBox; + + // hovering inside the code block should keep the list open + await page.mouse.move(x + width / 2, y + height / 2); + await expect(langLocator).toBeVisible(); + + // hovering out should not close the list + await page.mouse.move(x - 10, y - 10); + await waitNextFrame(page); + await expect(langLocator).toBeVisible(); +}); + +test('language selection list should not change when hovering over its elements', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + + const codeBlockController = getCodeBlock(page); + await codeBlockController.codeBlock.hover(); + await codeBlockController.clickLanguageButton(); + await waitNextFrame(page, 100); + + const langListLocator = codeBlockController.langList; + const langItemsLocator = langListLocator.locator('icon-button'); + + // checking first 4 language list items + for (let i = 0; i < 3; i++) { + const item = langItemsLocator.nth(i); // current item in language list + const nextItem = langItemsLocator.nth(i + 1); // next item in language list + + await item.hover(); + + const initialItemText = await item.textContent(); + const initialNextItemText = await nextItem.textContent(); + + await nextItem.hover(); + + const currentItemText = await item.textContent(); + const currentNextItemText = await nextItem.textContent(); + + // text content should remain unchanged after next item receives focus + expect(initialItemText).toBe(currentItemText); + expect(initialNextItemText).toBe(currentNextItemText); + } +}); + +test('format text in code block', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '```ts '); + await waitNextFrame(page, 100); + await type(page, 'const aaa = 1000;'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + const line = page.locator('affine-code rich-text v-line > div'); + expect(await line.innerText()).toBe('const aaa = 1000;'); + + const { boldBtn, linkBtn } = getFormatBar(page); + + await dragBetweenIndices(page, [0, 1], [0, 2]); + await boldBtn.click(); + expect(await line.innerText()).toBe('const aaa = 1000;'); + await dragBetweenIndices(page, [0, 4], [0, 7]); + await boldBtn.click(); + expect(await line.innerText()).toBe('const aaa = 1000;'); + await dragBetweenIndices(page, [0, 8], [0, 16]); + await boldBtn.click(); + expect(await line.innerText()).toBe('const aaa = 1000;'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_format.json` + ); + + await dragBetweenIndices(page, [0, 4], [0, 10]); + await linkBtn.click(); + await type(page, 'https://www.baidu.com'); + await pressEnter(page); + + expect(await line.innerText()).toBe('const aaa = 1000;'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_link.json` + ); +}); diff --git a/blocksuite/tests-legacy/code/readonly.spec.ts b/blocksuite/tests-legacy/code/readonly.spec.ts new file mode 100644 index 0000000000000..aada3d552f6e4 --- /dev/null +++ b/blocksuite/tests-legacy/code/readonly.spec.ts @@ -0,0 +1,59 @@ +import { expect } from '@playwright/test'; + +import { switchReadonly } from '../utils/actions/click.js'; +import { + pressBackspace, + pressEnter, + pressTab, + type, +} from '../utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + focusRichText, + focusRichTextEnd, + initEmptyCodeBlockState, +} from '../utils/actions/misc.js'; +import { assertRichTexts } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; +import { getCodeBlock } from './utils.js'; + +test('should code block widget be disabled in read only mode', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichTextEnd(page); + + await page.waitForTimeout(300); + await switchReadonly(page); + + const codeBlockController = getCodeBlock(page); + const codeBlock = codeBlockController.codeBlock; + await codeBlock.hover(); + await codeBlockController.clickLanguageButton(); + await expect(codeBlockController.langList).toBeHidden(); + + await codeBlock.hover(); + await expect(codeBlockController.codeToolbar).toBeVisible(); + await expect(codeBlockController.moreButton).toHaveAttribute('disabled'); + + await expect(codeBlockController.copyButton).toBeVisible(); + await expect(codeBlockController.moreMenu).toBeHidden(); +}); + +test('should not be able to modify code block in readonly mode', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'const a = 10;'); + await assertRichTexts(page, ['const a = 10;']); + + await switchReadonly(page); + await pressBackspace(page, 3); + await pressTab(page, 3); + await pressEnter(page, 2); + await assertRichTexts(page, ['const a = 10;']); +}); diff --git a/blocksuite/tests-legacy/code/selections.spec.ts b/blocksuite/tests-legacy/code/selections.spec.ts new file mode 100644 index 0000000000000..3a6960a17de04 --- /dev/null +++ b/blocksuite/tests-legacy/code/selections.spec.ts @@ -0,0 +1,195 @@ +import { expect } from '@playwright/test'; + +import { dragBetweenCoords } from '../utils/actions/drag.js'; +import { + pressArrowLeft, + pressBackspace, + pressEnter, + pressEnterWithShortkey, + redoByKeyboard, + type, + undoByKeyboard, +} from '../utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + focusRichText, + getInlineSelectionIndex, + getInlineSelectionText, + initEmptyCodeBlockState, +} from '../utils/actions/misc.js'; +import { + assertBlockCount, + assertBlockSelections, + assertRichTextInlineRange, + assertRichTexts, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; +import { getCodeBlock } from './utils.js'; + +test('click outside should close language list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + const codeBlock = getCodeBlock(page); + await codeBlock.clickLanguageButton(); + const locator = codeBlock.langList; + await expect(locator).toBeVisible(); + + const rect = await page.locator('affine-filterable-list').boundingBox(); + if (!rect) throw new Error('Failed to get bounding box of code block.'); + await page.mouse.click(rect.x - 10, rect.y - 10); + + await expect(locator).toBeHidden(); +}); + +test('split code by enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'hello'); + + // he|llo + await pressArrowLeft(page, 3); + + await pressEnter(page); + await assertRichTexts(page, ['he\nllo']); + + await undoByKeyboard(page); + await assertRichTexts(page, ['hello']); + + await redoByKeyboard(page); + await assertRichTexts(page, ['he\nllo']); +}); + +test('split code with selection by enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'hello'); + + // select 'll' + await pressArrowLeft(page, 1); + await page.keyboard.down('Shift'); + await pressArrowLeft(page, 2); + await page.keyboard.up('Shift'); + + await pressEnter(page); + await assertRichTexts(page, ['he\no']); + + await undoByKeyboard(page); + await assertRichTexts(page, ['hello']); + + await redoByKeyboard(page); + await assertRichTexts(page, ['he\no']); +}); + +test('drag select code block can delete it', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + const codeBlock = page.locator('affine-code'); + const bbox = await codeBlock.boundingBox(); + if (!bbox) { + throw new Error("Failed to get code block's bounding box"); + } + const position = { + startX: bbox.x - 10, + startY: bbox.y - 10, + endX: bbox.x + bbox.width, + endY: bbox.y + bbox.height / 2, + }; + await dragBetweenCoords( + page, + { x: position.startX, y: position.startY }, + { x: position.endX, y: position.endY }, + { steps: 20 } + ); + await page.waitForTimeout(10); + await page.keyboard.press('Backspace'); + const locator = page.locator('affine-code'); + await expect(locator).toBeHidden(); +}); + +test('press short key and enter at end of code block can jump out', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await pressEnterWithShortkey(page); + + const locator = page.locator('affine-paragraph'); + await expect(locator).toBeVisible(); +}); + +test('press short key and enter at end of code block with content can jump out', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + await type(page, 'const a = 10;'); + await pressEnterWithShortkey(page); + + const locator = page.locator('affine-paragraph'); + await expect(locator).toBeVisible(); +}); + +test('press backspace inside should select code block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + const codeBlock = page.locator('affine-code'); + const selectedRects = page + .locator('affine-block-selection') + .locator('visible=true'); + await page.keyboard.press('Backspace'); + await expect(selectedRects).toHaveCount(1); + await expect(codeBlock).toBeVisible(); + await page.keyboard.press('Backspace'); + await expect(selectedRects).toHaveCount(0); + await expect(codeBlock).toBeHidden(); +}); + +test('press backspace after code block can select code block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + const code = 'const a = 1;'; + await type(page, code); + + await assertRichTextInlineRange(page, 0, 12); + await pressEnterWithShortkey(page); + await assertRichTextInlineRange(page, 1, 0); + await assertBlockCount(page, 'paragraph', 1); + await pressBackspace(page); + await assertBlockSelections(page, ['2']); + await assertBlockCount(page, 'paragraph', 0); +}); + +test('press ArrowUp after code block can enter code block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + const code = 'const a = 1;'; + await type(page, code); + + await pressEnterWithShortkey(page); + await page.keyboard.press('ArrowUp'); + + const index = await getInlineSelectionIndex(page); + expect(index).toBe(0); + + const text = await getInlineSelectionText(page); + expect(text).toBe(code); +}); diff --git a/blocksuite/tests-legacy/code/utils.ts b/blocksuite/tests-legacy/code/utils.ts new file mode 100644 index 0000000000000..c9783c9c3c729 --- /dev/null +++ b/blocksuite/tests-legacy/code/utils.ts @@ -0,0 +1,61 @@ +import type { Page } from '@playwright/test'; + +/** + * @example + * ```ts + * const codeBlockController = getCodeBlock(page); + * const codeBlock = codeBlockController.codeBlock; + * ``` + */ +export function getCodeBlock(page: Page) { + const codeBlock = page.locator('affine-code'); + const languageButton = page.getByTestId('lang-button'); + + const clickLanguageButton = async () => { + await codeBlock.hover(); + await languageButton.click({ delay: 50 }); + }; + + const langList = page.locator('affine-filterable-list'); + const langFilterInput = langList.locator('#filter-input'); + + const codeToolbar = page.locator('affine-code-toolbar'); + + const copyButton = codeToolbar.getByRole('button', { name: 'Copy code' }); + const captionButton = codeToolbar.getByRole('button', { name: 'Caption' }); + const moreButton = codeToolbar.getByRole('button', { name: 'More' }); + + const menu = page.locator('.more-popup-menu'); + + const openMore = async () => { + await moreButton.click(); + + const wrapButton = menu.getByRole('button', { name: 'Wrap' }); + const cancelWrapButton = menu.getByRole('button', { name: 'Cancel wrap' }); + const duplicateButton = menu.getByRole('button', { name: 'Duplicate' }); + const deleteButton = menu.getByRole('button', { name: 'Delete' }); + + return { + menu, + wrapButton, + cancelWrapButton, + duplicateButton, + deleteButton, + }; + }; + + return { + codeBlock, + codeToolbar, + captionButton, + languageButton, + langList, + copyButton, + moreButton, + langFilterInput, + moreMenu: menu, + + openMore, + clickLanguageButton, + }; +} diff --git a/blocksuite/tests-legacy/database/actions.ts b/blocksuite/tests-legacy/database/actions.ts new file mode 100644 index 0000000000000..e78627ad5e268 --- /dev/null +++ b/blocksuite/tests-legacy/database/actions.ts @@ -0,0 +1,588 @@ +import type { + RichTextCell, + RichTextCellEditing, +} from '@blocks/database-block/properties/rich-text/cell-renderer.js'; +import { press } from '@inline/__tests__/utils.js'; +import { ZERO_WIDTH_SPACE } from '@inline/consts.js'; +import { expect, type Locator, type Page } from '@playwright/test'; + +import { + pressEnter, + selectAllByKeyboard, + type, +} from '../utils/actions/keyboard.js'; +import { + getBoundingBox, + getBoundingClientRect, + getEditorLocator, + waitNextFrame, +} from '../utils/actions/misc.js'; + +export async function initDatabaseColumn(page: Page, title = '') { + const editor = getEditorLocator(page); + await editor.locator('affine-data-view-table-group').first().hover(); + const columnAddBtn = editor.locator('.header-add-column-button'); + await columnAddBtn.click(); + await waitNextFrame(page, 200); + + if (title) { + await selectAllByKeyboard(page); + await type(page, title); + await waitNextFrame(page); + await pressEnter(page); + } else { + await pressEnter(page); + } +} + +export const renameColumn = async (page: Page, name: string) => { + const column = page.locator('affine-database-header-column', { + hasText: name, + }); + await column.click(); +}; + +export async function performColumnAction( + page: Page, + name: string, + action: string +) { + await renameColumn(page, name); + + const actionMenu = page.locator(`.affine-menu-button`, { hasText: action }); + await actionMenu.click(); +} + +export async function switchColumnType( + page: Page, + columnType: string, + columnIndex = 1 +) { + const { typeIcon } = await getDatabaseHeaderColumn(page, columnIndex); + await typeIcon.click(); + + await clickColumnType(page, columnType); +} + +export function clickColumnType(page: Page, columnType: string) { + const typeMenu = page.locator(`.affine-menu-button`, { + hasText: new RegExp(`${columnType}`), + }); + return typeMenu.click(); +} + +export function getDatabaseBodyRows(page: Page) { + const rowContainer = page.locator('.affine-database-block-rows'); + return rowContainer.locator('.database-row'); +} + +export function getDatabaseBodyRow(page: Page, rowIndex = 0) { + const rows = getDatabaseBodyRows(page); + return rows.nth(rowIndex); +} + +export function getDatabaseTableContainer(page: Page) { + const container = page.locator('.affine-database-table-container'); + return container; +} + +export async function assertDatabaseTitleColumnText( + page: Page, + title: string, + index = 0 +) { + const text = await page.evaluate(index => { + const rowContainer = document.querySelector('.affine-database-block-rows'); + const row = rowContainer?.querySelector( + `.database-row:nth-child(${index + 1})` + ); + const titleColumnCell = row?.querySelector('.database-cell:nth-child(1)'); + const titleSpan = titleColumnCell?.querySelector( + '.data-view-header-area-rich-text' + ) as HTMLElement; + if (!titleSpan) throw new Error('Cannot find database title column editor'); + return titleSpan.innerText; + }, index); + + if (title === '') { + expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_SPACE}])$`)); + } else { + expect(text).toBe(title); + } +} + +export function getDatabaseBodyCell( + page: Page, + { + rowIndex, + columnIndex, + }: { + rowIndex: number; + columnIndex: number; + } +) { + const row = getDatabaseBodyRow(page, rowIndex); + const cell = row.locator('.database-cell').nth(columnIndex); + return cell; +} + +export function getDatabaseBodyCellContent( + page: Page, + { + rowIndex, + columnIndex, + cellClass, + }: { + rowIndex: number; + columnIndex: number; + cellClass: string; + } +) { + const cell = getDatabaseBodyCell(page, { rowIndex, columnIndex }); + const cellContent = cell.locator(`.${cellClass}`); + return cellContent; +} + +export function getFirstColumnCell(page: Page, cellClass: string) { + const cellContent = getDatabaseBodyCellContent(page, { + rowIndex: 0, + columnIndex: 1, + cellClass, + }); + return cellContent; +} + +export async function clickSelectOption(page: Page, index = 0) { + await page.locator('.select-option-icon').nth(index).click(); +} + +export async function performSelectColumnTagAction( + page: Page, + name: string, + index = 0 +) { + await clickSelectOption(page, index); + await page + .locator('.affine-menu-button', { hasText: new RegExp(name) }) + .click(); +} + +export async function assertSelectedStyle( + page: Page, + key: keyof CSSStyleDeclaration, + value: string +) { + const style = await getElementStyle(page, '.select-selected', key); + expect(style).toBe(value); +} + +export async function clickDatabaseOutside(page: Page) { + const docTitle = page.locator('.doc-title-container'); + await docTitle.click(); +} + +export async function assertColumnWidth(locator: Locator, width: number) { + const box = await getBoundingBox(locator); + expect(box.width).toBe(width + 1); + return box; +} + +export async function assertDatabaseCellRichTexts( + page: Page, + { + rowIndex = 0, + columnIndex = 1, + text, + }: { + rowIndex?: number; + columnIndex?: number; + text: string; + } +) { + const cellContainer = page.locator( + `affine-database-cell-container[data-row-index='${rowIndex}'][data-column-index='${columnIndex}']` + ); + + const cellEditing = cellContainer.locator( + 'affine-database-rich-text-cell-editing' + ); + const cell = cellContainer.locator('affine-database-rich-text-cell'); + + const richText = (await cellEditing.count()) === 0 ? cell : cellEditing; + const actualTexts = await richText.evaluate(ele => { + return (ele as RichTextCellEditing).inlineEditor?.yTextString; + }); + expect(actualTexts).toEqual(text); +} + +export async function assertDatabaseCellNumber( + page: Page, + { + rowIndex = 0, + columnIndex = 1, + text, + }: { + rowIndex?: number; + columnIndex?: number; + text: string; + } +) { + const actualText = await page + .locator('.affine-database-block-rows') + .locator('.database-row') + .nth(rowIndex) + .locator('.database-cell') + .nth(columnIndex) + .locator('.number') + .textContent(); + expect(actualText?.trim()).toEqual(text); +} + +export async function assertDatabaseCellLink( + page: Page, + { + rowIndex = 0, + columnIndex = 1, + text, + }: { + rowIndex?: number; + columnIndex?: number; + text: string; + } +) { + const actualTexts = await page.evaluate( + ({ rowIndex, columnIndex }) => { + const rows = document.querySelector('.affine-database-block-rows'); + const row = rows?.querySelector( + `.database-row:nth-child(${rowIndex + 1})` + ); + const cell = row?.querySelector( + `.database-cell:nth-child(${columnIndex + 1})` + ); + const richText = + cell?.querySelector('affine-database-link-cell') ?? + cell?.querySelector( + 'affine-database-link-cell-editing' + ); + if (!richText) throw new Error('Missing database rich text cell'); + return richText.inlineEditor.yText.toString(); + }, + { rowIndex, columnIndex } + ); + expect(actualTexts).toEqual(text); +} + +export async function assertDatabaseTitleText(page: Page, text: string) { + const dbTitle = page.locator('[data-block-is-database-title="true"]'); + expect(await dbTitle.inputValue()).toEqual(text); +} + +export async function waitSearchTransitionEnd(page: Page) { + await waitNextFrame(page, 400); +} + +export async function assertDatabaseSearching( + page: Page, + isSearching: boolean +) { + const searchExpand = page.locator('.search-container-expand'); + const count = await searchExpand.count(); + expect(count).toBe(isSearching ? 1 : 0); +} + +export async function focusDatabaseSearch(page: Page) { + await (await getDatabaseMouse(page)).mouseOver(); + + const searchExpand = page.locator('.search-container-expand'); + const count = await searchExpand.count(); + if (count === 1) { + const input = page.locator('.affine-database-search-input'); + await input.click(); + } else { + const searchIcon = page.locator('.affine-database-search-input-icon'); + await searchIcon.click(); + await waitSearchTransitionEnd(page); + } +} + +export async function blurDatabaseSearch(page: Page) { + const dbTitle = page.locator('[data-block-is-database-title="true"]'); + await dbTitle.click(); +} + +export async function focusDatabaseHeader(page: Page, columnIndex = 0) { + const column = page.locator('.affine-database-column').nth(columnIndex); + const box = await getBoundingBox(column); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await waitNextFrame(page); + return column; +} + +export async function getDatabaseMouse(page: Page) { + const databaseRect = await getBoundingClientRect( + page, + '.affine-database-table' + ); + return { + mouseOver: async () => { + await page.mouse.move(databaseRect.x, databaseRect.y); + }, + mouseLeave: async () => { + await page.mouse.move(databaseRect.x - 1, databaseRect.y - 1); + }, + }; +} + +export async function getDatabaseHeaderColumn(page: Page, index = 0) { + const column = page.locator('.affine-database-column').nth(index); + const box = await getBoundingBox(column); + const textElement = column.locator('.affine-database-column-text-input'); + const text = await textElement.innerText(); + const typeIcon = column.locator('.affine-database-column-type-icon'); + + return { + column, + box, + text, + textElement, + typeIcon, + }; +} + +export async function assertRowsSelection( + page: Page, + rowIndexes: [start: number, end: number] +) { + const rows = page.locator('data-view-table-row'); + const startIndex = rowIndexes[0]; + const endIndex = rowIndexes[1]; + for (let i = startIndex; i <= endIndex; i++) { + const row = rows.nth(i); + await row.locator('.row-select-checkbox .selected').isVisible(); + } +} + +export async function assertCellsSelection( + page: Page, + cellIndexes: { + start: [rowIndex: number, columnIndex: number]; + end?: [rowIndex: number, columnIndex: number]; + } +) { + const { start, end } = cellIndexes; + + if (!end) { + // single cell + const focus = page.locator('.database-focus'); + const focusBox = await getBoundingBox(focus); + + const [rowIndex, columnIndex] = start; + const cell = getDatabaseBodyCell(page, { rowIndex, columnIndex }); + const cellBox = await getBoundingBox(cell); + expect(focusBox).toEqual({ + x: cellBox.x, + y: cellBox.y - 1, + height: cellBox.height + 2, + width: cellBox.width + 1, + }); + } else { + // multi cells + const selection = page.locator('.database-selection'); + const selectionBox = await getBoundingBox(selection); + + const [startRowIndex, startColumnIndex] = start; + const [endRowIndex, endColumnIndex] = end; + + const rowIndexStart = Math.min(startRowIndex, endRowIndex); + const rowIndexEnd = Math.max(startRowIndex, endRowIndex); + const columnIndexStart = Math.min(startColumnIndex, endColumnIndex); + const columnIndexEnd = Math.max(startColumnIndex, endColumnIndex); + + let height = 0; + let width = 0; + let x = 0; + let y = 0; + for (let i = rowIndexStart; i <= rowIndexEnd; i++) { + const cell = getDatabaseBodyCell(page, { + rowIndex: i, + columnIndex: columnIndexStart, + }); + const box = await getBoundingBox(cell); + height += box.height + 1; + if (i === rowIndexStart) { + y = box.y; + } + } + + for (let j = columnIndexStart; j <= columnIndexEnd; j++) { + const cell = getDatabaseBodyCell(page, { + rowIndex: rowIndexStart, + columnIndex: j, + }); + const box = await getBoundingBox(cell); + width += box.width; + if (j === columnIndexStart) { + x = box.x; + } + } + + expect(selectionBox).toEqual({ + x, + y, + height, + width: width + 1, + }); + } +} + +export async function getElementStyle( + page: Page, + selector: string, + key: keyof CSSStyleDeclaration +) { + const style = await page.evaluate( + ({ key, selector }) => { + const el = document.querySelector(selector); + if (!el) throw new Error(`Missing ${selector} tag`); + // @ts-ignore + return el.style[key]; + }, + { + key, + selector, + } + ); + + return style; +} + +export async function focusKanbanCardHeader(page: Page, index = 0) { + const cardHeader = page.locator('data-view-header-area-text').nth(index); + await cardHeader.click(); +} + +export async function clickKanbanCardHeader(page: Page, index = 0) { + const cardHeader = page.locator('data-view-header-area-text').nth(index); + await cardHeader.click(); + await cardHeader.click(); +} + +export async function assertKanbanCardHeaderText( + page: Page, + text: string, + index = 0 +) { + const cardHeader = page.locator('data-view-header-area-text').nth(index); + + await expect(cardHeader).toHaveText(text); +} + +export async function assertKanbanCellSelected( + page: Page, + { + groupIndex, + cardIndex, + cellIndex, + }: { + groupIndex: number; + cardIndex: number; + cellIndex: number; + } +) { + const border = await page.evaluate( + ({ groupIndex, cardIndex, cellIndex }) => { + const group = document.querySelector( + `affine-data-view-kanban-group:nth-child(${groupIndex + 1})` + ); + const card = group?.querySelector( + `affine-data-view-kanban-card:nth-child(${cardIndex + 1})` + ); + const cells = Array.from( + card?.querySelectorAll(`affine-data-view-kanban-cell`) ?? + [] + ); + const cell = cells[cellIndex]; + if (!cell) throw new Error(`Missing cell tag`); + return cell.style.border; + }, + { + groupIndex, + cardIndex, + cellIndex, + } + ); + + expect(border).toEqual('1px solid var(--affine-primary-color)'); +} + +export async function assertKanbanCardSelected( + page: Page, + { + groupIndex, + cardIndex, + }: { + groupIndex: number; + cardIndex: number; + } +) { + const border = await page.evaluate( + ({ groupIndex, cardIndex }) => { + const group = document.querySelector( + `affine-data-view-kanban-group:nth-child(${groupIndex + 1})` + ); + const card = group?.querySelector( + `affine-data-view-kanban-card:nth-child(${cardIndex + 1})` + ); + if (!card) throw new Error(`Missing card tag`); + return card.style.border; + }, + { + groupIndex, + cardIndex, + } + ); + + expect(border).toEqual('1px solid var(--affine-primary-color)'); +} + +export function getKanbanCard( + page: Page, + { + groupIndex, + cardIndex, + }: { + groupIndex: number; + cardIndex: number; + } +) { + const group = page.locator('affine-data-view-kanban-group').nth(groupIndex); + const card = group.locator('affine-data-view-kanban-card').nth(cardIndex); + return card; +} +export const moveToCenterOf = async (page: Page, locator: Locator) => { + const box = (await locator.boundingBox())!; + expect(box).toBeDefined(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); +}; +export const changeColumnType = async ( + page: Page, + column: number, + name: string +) => { + await waitNextFrame(page); + await page.locator('affine-database-header-column').nth(column).click(); + await waitNextFrame(page, 200); + await pressKey(page, 'Escape'); + await pressKey(page, 'ArrowDown'); + await pressKey(page, 'Enter'); + await type(page, name); + await pressKey(page, 'ArrowDown'); + await pressKey(page, 'Enter'); +}; +export const pressKey = async (page: Page, key: string, count: number = 1) => { + for (let i = 0; i < count; i++) { + await waitNextFrame(page); + await press(page, key); + } + await waitNextFrame(page); +}; diff --git a/blocksuite/tests-legacy/database/clipboard.spec.ts b/blocksuite/tests-legacy/database/clipboard.spec.ts new file mode 100644 index 0000000000000..d8dd54906086b --- /dev/null +++ b/blocksuite/tests-legacy/database/clipboard.spec.ts @@ -0,0 +1,176 @@ +import { expect } from '@playwright/test'; + +import { dragBetweenCoords } from '../utils/actions/drag.js'; +import { + copyByKeyboard, + pasteByKeyboard, + pressArrowDown, + pressArrowUp, + pressEnter, + pressEscape, + type, +} from '../utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + focusRichText, + getBoundingBox, + initDatabaseDynamicRowWithData, + initDatabaseRowWithData, + initEmptyDatabaseState, + initEmptyDatabaseWithParagraphState, + waitNextFrame, +} from '../utils/actions/misc.js'; +import { assertRichTexts } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; +import { + assertDatabaseTitleColumnText, + getDatabaseBodyCell, + getElementStyle, + initDatabaseColumn, + switchColumnType, +} from './actions.js'; + +test.describe('copy&paste when editing', () => { + test.skip('should support copy&paste of the title column', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseWithParagraphState(page); + + await initDatabaseColumn(page); + await initDatabaseRowWithData(page, 'abc123'); + await pressEscape(page); + + await pressEnter(page); + await page.keyboard.down('Shift'); + for (let i = 0; i < 4; i++) { + await page.keyboard.press('ArrowLeft'); + } + await page.keyboard.up('Shift'); + await copyByKeyboard(page); + + const bgValue = await getElementStyle(page, '.database-focus', 'boxShadow'); + expect(bgValue).not.toBe('unset'); + + await focusRichText(page, 1); + await pasteByKeyboard(page); + await assertRichTexts(page, ['Database 1', 'c123']); + }); +}); + +test.describe('copy&paste when selecting', () => { + test.skip('should support copy&paste of a single cell', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseRowWithData(page, 'abc123'); + await initDatabaseRowWithData(page, ''); + await pressEscape(page); + await waitNextFrame(page, 100); + await pressArrowUp(page); + + await copyByKeyboard(page); + await pressArrowDown(page); + await pasteByKeyboard(page, false); + await waitNextFrame(page); + await assertDatabaseTitleColumnText(page, 'abc123', 1); + }); + + test.skip('should support copy&paste of multi cells', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseRowWithData(page, 'text1'); + await initDatabaseDynamicRowWithData(page, '123', false); + await pressEscape(page); + await initDatabaseRowWithData(page, 'text2'); + await initDatabaseDynamicRowWithData(page, 'a', false); + await pressEscape(page); + + await initDatabaseRowWithData(page, ''); + await initDatabaseRowWithData(page, ''); + + const startCell = getDatabaseBodyCell(page, { + rowIndex: 0, + columnIndex: 0, + }); + const startCellBox = await getBoundingBox(startCell); + const endCell = getDatabaseBodyCell(page, { rowIndex: 1, columnIndex: 1 }); + const endCellBox = await getBoundingBox(endCell); + const startX = startCellBox.x + startCellBox.width / 2; + const startY = startCellBox.y + startCellBox.height / 2; + const endX = endCellBox.x + endCellBox.width / 2; + const endY = endCellBox.y + endCellBox.height / 2; + await dragBetweenCoords( + page, + { x: startX, y: startY }, + { x: endX, y: endY }, + { + steps: 50, + } + ); + await copyByKeyboard(page); + + await pressArrowDown(page); + await pressArrowDown(page); + await waitNextFrame(page); + await pasteByKeyboard(page, false); + + await assertDatabaseTitleColumnText(page, 'text1', 2); + await assertDatabaseTitleColumnText(page, 'text2', 3); + const selectCell21 = getDatabaseBodyCell(page, { + rowIndex: 2, + columnIndex: 1, + }); + const selectCell31 = getDatabaseBodyCell(page, { + rowIndex: 3, + columnIndex: 1, + }); + expect(await selectCell21.innerText()).toBe('123'); + expect(await selectCell31.innerText()).toBe('a'); + }); + + test.skip('should support copy&paste of a single row', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseRowWithData(page, 'text1'); + await pressEscape(page); + await waitNextFrame(page, 100); + await initDatabaseDynamicRowWithData(page, 'abc', false); + await pressEscape(page); + await waitNextFrame(page, 100); + await initDatabaseColumn(page); + await switchColumnType(page, 'Number', 2); + const numberCell = getDatabaseBodyCell(page, { + rowIndex: 0, + columnIndex: 2, + }); + await numberCell.click(); + await waitNextFrame(page); + await type(page, '123'); + await pressEscape(page); + await pressEscape(page); + await copyByKeyboard(page); + + await initDatabaseRowWithData(page, ''); + await pressEscape(page); + await pasteByKeyboard(page, false); + await waitNextFrame(page); + + await assertDatabaseTitleColumnText(page, 'text1', 1); + const selectCell = getDatabaseBodyCell(page, { + rowIndex: 1, + columnIndex: 1, + }); + expect(await selectCell.innerText()).toBe('abc'); + const selectNumberCell = getDatabaseBodyCell(page, { + rowIndex: 1, + columnIndex: 2, + }); + expect(await selectNumberCell.innerText()).toBe('123'); + }); +}); diff --git a/blocksuite/tests-legacy/database/column.spec.ts b/blocksuite/tests-legacy/database/column.spec.ts new file mode 100644 index 0000000000000..199a5d7f31e29 --- /dev/null +++ b/blocksuite/tests-legacy/database/column.spec.ts @@ -0,0 +1,599 @@ +import { expect } from '@playwright/test'; + +import { + assertDatabaseColumnOrder, + dragBetweenCoords, + enterPlaygroundRoom, + getBoundingBox, + initDatabaseDynamicRowWithData, + initEmptyDatabaseState, + pressArrowRight, + pressArrowUp, + pressArrowUpWithShiftKey, + pressBackspace, + pressEnter, + pressEscape, + redoByClick, + selectAllByKeyboard, + type, + undoByClick, + waitNextFrame, +} from '../utils/actions/index.js'; +import { test } from '../utils/playwright.js'; +import { + assertDatabaseCellNumber, + assertDatabaseCellRichTexts, + assertSelectedStyle, + changeColumnType, + clickDatabaseOutside, + clickSelectOption, + getDatabaseHeaderColumn, + getFirstColumnCell, + initDatabaseColumn, + performColumnAction, + performSelectColumnTagAction, + switchColumnType, +} from './actions.js'; + +test.describe('column operations', () => { + test('should support rename column', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, 'abc'); + + const { textElement } = await getDatabaseHeaderColumn(page, 1); + expect(await textElement.innerText()).toBe('abc'); + await textElement.click(); + await waitNextFrame(page, 200); + await pressArrowRight(page); + await type(page, '123'); + await pressEnter(page); + expect(await textElement.innerText()).toBe('abc123'); + + await undoByClick(page); + expect(await textElement.innerText()).toBe('abc'); + await redoByClick(page); + expect(await textElement.innerText()).toBe('abc123'); + }); + + test('should support add new column', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await pressEscape(page); + const { text: title1 } = await getDatabaseHeaderColumn(page, 1); + expect(title1).toBe('Column 1'); + + const selected = getFirstColumnCell(page, 'select-selected'); + expect(await selected.innerText()).toBe('123'); + + await initDatabaseColumn(page, 'abc'); + const { text: title2 } = await getDatabaseHeaderColumn(page, 2); + expect(title2).toBe('abc'); + + await initDatabaseColumn(page); + const { text: title3 } = await getDatabaseHeaderColumn(page, 3); + expect(title3).toBe('Column 2'); + }); + + test('should support right insert column', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, '1'); + + await performColumnAction(page, '1', 'Insert right'); + await selectAllByKeyboard(page); + await type(page, '2'); + await pressEnter(page); + const columns = page.locator('.affine-database-column'); + expect(await columns.count()).toBe(3); + + await assertDatabaseColumnOrder(page, ['1', '2']); + }); + + test('should support left insert column', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, '1'); + + await performColumnAction(page, '1', 'Insert left'); + await selectAllByKeyboard(page); + await type(page, '2'); + await pressEnter(page); + const columns = page.locator('.affine-database-column'); + expect(await columns.count()).toBe(3); + + await assertDatabaseColumnOrder(page, ['2', '1']); + }); + + test('should support delete column', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, '1'); + + const columns = page.locator('.affine-database-column'); + expect(await columns.count()).toBe(2); + + await performColumnAction(page, '1', 'Delete'); + expect(await columns.count()).toBe(1); + }); + + test('should support duplicate column', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, '1'); + await initDatabaseDynamicRowWithData(page, '123', true); + await pressEscape(page); + await performColumnAction(page, '1', 'duplicate'); + await pressEscape(page); + const cells = page.locator('affine-database-multi-select-cell'); + expect(await cells.count()).toBe(2); + + const secondCell = cells.nth(1); + const selected = secondCell.locator('.select-selected'); + expect(await selected.innerText()).toBe('123'); + }); + + test('should support move column right', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, '1'); + await initDatabaseDynamicRowWithData(page, '123', true); + await pressEscape(page); + await initDatabaseColumn(page, '2'); + await initDatabaseDynamicRowWithData(page, 'abc', false, 1); + await pressEscape(page); + await assertDatabaseColumnOrder(page, ['1', '2']); + await waitNextFrame(page, 350); + await performColumnAction(page, '1', 'Move right'); + await assertDatabaseColumnOrder(page, ['2', '1']); + + await undoByClick(page); + const { column } = await getDatabaseHeaderColumn(page, 2); + await column.click(); + const moveLeft = page.locator('.action', { hasText: 'Move right' }); + expect(await moveLeft.count()).toBe(0); + }); + + test('should support move column left', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, '1'); + await initDatabaseDynamicRowWithData(page, '123', true); + await pressEscape(page); + await initDatabaseColumn(page, '2'); + await initDatabaseDynamicRowWithData(page, 'abc', false, 1); + await pressEscape(page); + await assertDatabaseColumnOrder(page, ['1', '2']); + + const { column } = await getDatabaseHeaderColumn(page, 0); + await column.click(); + const moveLeft = page.locator('.action', { hasText: 'Move left' }); + expect(await moveLeft.count()).toBe(0); + await waitNextFrame(page, 200); + await pressEscape(page); + await pressEscape(page); + + await performColumnAction(page, '2', 'Move left'); + await assertDatabaseColumnOrder(page, ['2', '1']); + }); +}); + +test.describe('switch column type', () => { + test('switch to number', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123abc', true); + await pressEscape(page); + await changeColumnType(page, 1, 'Number'); + + const cell = getFirstColumnCell(page, 'number'); + await assertDatabaseCellNumber(page, { + text: '', + }); + await pressEnter(page); + await type(page, '123abc'); + await pressEscape(page); + expect((await cell.textContent())?.trim()).toBe('123'); + }); + + test('switch to rich-text', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123abc', true); + await pressEscape(page); + await switchColumnType(page, 'Text'); + + // For now, rich-text will only be initialized on click + // Therefore, for the time being, here is to detect whether there is '.affine-database-rich-text' + const cell = getFirstColumnCell(page, 'affine-database-rich-text'); + expect(await cell.count()).toBe(1); + await pressEnter(page); + await type(page, '123'); + await pressEscape(page); + await pressEnter(page); + await type(page, 'abc'); + await pressEscape(page); + await assertDatabaseCellRichTexts(page, { text: '123abc123abc' }); + }); + + test('switch between multi-select and select', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await type(page, 'abc'); + await pressEnter(page); + await pressEscape(page); + const cell = getFirstColumnCell(page, 'select-selected'); + expect(await cell.count()).toBe(2); + + await switchColumnType(page, 'Select', 1); + expect(await cell.count()).toBe(1); + expect(await cell.innerText()).toBe('123'); + + await pressEnter(page); + await type(page, 'def'); + await pressEnter(page); + expect(await cell.innerText()).toBe('def'); + + await switchColumnType(page, 'Multi-select'); + await pressEnter(page); + await type(page, '666'); + await pressEnter(page); + await pressEscape(page); + expect(await cell.count()).toBe(2); + expect(await cell.nth(0).innerText()).toBe('def'); + expect(await cell.nth(1).innerText()).toBe('666'); + + await switchColumnType(page, 'Select'); + expect(await cell.count()).toBe(1); + expect(await cell.innerText()).toBe('def'); + + await pressEnter(page); + await type(page, '888'); + await pressEnter(page); + expect(await cell.innerText()).toBe('888'); + }); + + test('switch between number and rich-text', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Number'); + + await initDatabaseDynamicRowWithData(page, '123abc', true); + await assertDatabaseCellNumber(page, { + text: '123', + }); + + await switchColumnType(page, 'Text'); + await pressEnter(page); + await type(page, 'abc'); + await pressEscape(page); + await assertDatabaseCellRichTexts(page, { text: '123abc' }); + + await switchColumnType(page, 'Number'); + await assertDatabaseCellNumber(page, { + text: '123', + }); + }); + + test('switch number to select', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Number'); + + await initDatabaseDynamicRowWithData(page, '123', true); + const cell = getFirstColumnCell(page, 'number'); + expect((await cell.textContent())?.trim()).toBe('123'); + + await switchColumnType(page, 'Select'); + await initDatabaseDynamicRowWithData(page, 'abc'); + const selectCell = getFirstColumnCell(page, 'select-selected'); + expect(await selectCell.innerText()).toBe('abc'); + + await switchColumnType(page, 'Number'); + await assertDatabaseCellNumber(page, { + text: '', + }); + }); + + test('switch to checkbox', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + await changeColumnType(page, 1, 'Checkbox'); + + const checkbox = getFirstColumnCell(page, 'checkbox'); + await expect(checkbox).not.toHaveClass('checked'); + + await waitNextFrame(page, 500); + await checkbox.click(); + await expect(checkbox).toHaveClass(/checked/); + + await undoByClick(page); + await expect(checkbox).not.toHaveClass('checked'); + }); + + test('checkbox to text', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + await changeColumnType(page, 1, 'Checkbox'); + + let checkbox = getFirstColumnCell(page, 'checkbox'); + await expect(checkbox).not.toHaveClass('checked'); + + // checked + await checkbox.click(); + await changeColumnType(page, 1, 'Text'); + await clickDatabaseOutside(page); + await waitNextFrame(page, 100); + await assertDatabaseCellRichTexts(page, { text: 'Yes' }); + await clickDatabaseOutside(page); + await waitNextFrame(page, 100); + await changeColumnType(page, 1, 'Checkbox'); + checkbox = getFirstColumnCell(page, 'checkbox'); + await expect(checkbox).toHaveClass(/checked/); + + // not checked + await checkbox.click(); + await changeColumnType(page, 1, 'Text'); + await clickDatabaseOutside(page); + await waitNextFrame(page, 100); + await assertDatabaseCellRichTexts(page, { text: 'No' }); + await clickDatabaseOutside(page); + await waitNextFrame(page, 100); + await changeColumnType(page, 1, 'Checkbox'); + checkbox = getFirstColumnCell(page, 'checkbox'); + await expect(checkbox).not.toHaveClass('checked'); + }); + + test('switch to progress', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + await switchColumnType(page, 'Progress'); + + const progress = getFirstColumnCell(page, 'progress'); + expect(await progress.textContent()).toBe('0'); + await waitNextFrame(page, 500); + const progressBg = page.locator('.affine-database-progress-bg'); + const { + x: progressBgX, + y: progressBgY, + width: progressBgWidth, + } = await getBoundingBox(progressBg); + await page.mouse.move(progressBgX, progressBgY); + await page.mouse.click(progressBgX, progressBgY); + const dragHandle = page.locator('.affine-database-progress-drag-handle'); + const { + x: dragX, + y: dragY, + width, + height, + } = await getBoundingBox(dragHandle); + const dragCenterX = dragX + width / 2; + const dragCenterY = dragY + height / 2; + await page.mouse.move(dragCenterX, dragCenterY); + + const endX = dragCenterX + progressBgWidth; + await dragBetweenCoords( + page, + { x: dragCenterX, y: dragCenterY }, + { x: endX, y: dragCenterY } + ); + expect(await progress.textContent()).toBe('100'); + await pressEscape(page); + await undoByClick(page); + expect(await progress.textContent()).toBe('0'); + }); + + test('switch to link', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + + await switchColumnType(page, 'Link'); + + const linkText = 'http://example.com'; + const cell = getFirstColumnCell(page, 'affine-database-link'); + await pressEnter(page); + await type(page, linkText); + await pressEscape(page); + const link = cell.locator('affine-database-link-node > a'); + const linkContent = link.locator('.link-node-text'); + await expect(link).toHaveAttribute('href', linkText); + expect(await linkContent.textContent()).toBe(linkText); + + // not link text + await cell.hover(); + const linkEdit = getFirstColumnCell(page, 'affine-database-link-icon'); + await linkEdit.click(); + await selectAllByKeyboard(page); + await type(page, 'abc'); + await pressEnter(page); + await expect(link).toBeHidden(); + }); +}); + +test.describe('select column tag action', () => { + test('should support select tag renaming', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await type(page, 'abc'); + await pressEnter(page); + await clickSelectOption(page); + await waitNextFrame(page); + await pressArrowRight(page); + await type(page, '4567abc00'); + await pressEnter(page); + const options = page.locator('.select-options-container .tag-text'); + expect(await options.nth(0).innerText()).toBe('abc4567abc00'); + expect(await options.nth(1).innerText()).toBe('123'); + }); + + test('should select tag renaming support shortcut key', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await clickSelectOption(page); + await waitNextFrame(page); + await pressArrowRight(page); + await type(page, '456'); + // esc + await pressEscape(page); + await pressEscape(page); + const options = page.locator('.select-options-container .tag-text'); + const option1 = options.nth(0); + expect(await option1.innerText()).toBe('123456'); + }); + + test('should support select tag deletion', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await performSelectColumnTagAction(page, 'Delete'); + const options = page.locator('.select-option-name'); + expect(await options.count()).toBe(0); + }); + + test('should support modifying select tag color', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await performSelectColumnTagAction(page, 'Red'); + await pressEscape(page); + await assertSelectedStyle( + page, + 'backgroundColor', + 'var(--affine-v2-chip-label-red)' + ); + }); +}); + +test.describe('drag-to-fill', () => { + test('should show when cell in focus and hide on blur', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '', true); + + await pressEscape(page); + + const dragToFillHandle = page.locator('.drag-to-fill'); + + await expect(dragToFillHandle).toBeVisible(); + + await pressEscape(page); + + await expect(dragToFillHandle).toBeHidden(); + }); + + test('should not show in multi (row or column) selection', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + + const dragToFillHandle = page.locator('.drag-to-fill'); + + await expect(dragToFillHandle).toBeVisible(); + + await pressArrowUpWithShiftKey(page); + + await expect(dragToFillHandle).toBeHidden(); + await pressArrowUp(page); + + await expect(dragToFillHandle).toBeVisible(); + }); + + test('should fill columns with data', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + + await initDatabaseDynamicRowWithData(page, 'thing', true); + await pressEscape(page); + + await initDatabaseDynamicRowWithData(page, '', true); + await pressBackspace(page); + await type(page, 'aaa'); + await pressEnter(page); + await pressEnter(page); + + await pressEscape(page); + await pressArrowUp(page); + + const cells = page.locator('affine-database-multi-select-cell'); + + expect(await cells.nth(0).innerText()).toBe('thing'); + expect(await cells.nth(1).innerText()).toBe('aaa'); + + const dragToFillHandle = page.locator('.drag-to-fill'); + + await expect(dragToFillHandle).toBeVisible(); + + const bbox = await getBoundingBox(dragToFillHandle); + + if (!bbox) throw new Error('Expected a bounding box'); + + await dragBetweenCoords( + page, + { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 }, + { x: bbox.x, y: bbox.y + 200 } + ); + + expect(await cells.nth(0).innerText()).toBe('thing'); + expect(await cells.nth(1).innerText()).toBe('thing'); + }); +}); diff --git a/blocksuite/tests-legacy/database/database.spec.ts b/blocksuite/tests-legacy/database/database.spec.ts new file mode 100644 index 0000000000000..3404738cae437 --- /dev/null +++ b/blocksuite/tests-legacy/database/database.spec.ts @@ -0,0 +1,670 @@ +import { expect } from '@playwright/test'; + +import { + dragBetweenCoords, + enterPlaygroundRoom, + focusDatabaseTitle, + getBoundingBox, + initDatabaseDynamicRowWithData, + initDatabaseRowWithData, + initEmptyDatabaseState, + pressArrowLeft, + pressArrowRight, + pressBackspace, + pressEnter, + pressEscape, + pressShiftEnter, + redoByKeyboard, + selectAllByKeyboard, + setInlineRangeInInlineEditor, + switchReadonly, + type, + undoByClick, + undoByKeyboard, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertBlockProps, + assertInlineEditorDeltas, + assertRowCount, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; +import { getFormatBar } from '../utils/query.js'; +import { + assertColumnWidth, + assertDatabaseCellRichTexts, + assertDatabaseSearching, + assertDatabaseTitleText, + blurDatabaseSearch, + clickColumnType, + clickDatabaseOutside, + focusDatabaseHeader, + focusDatabaseSearch, + getDatabaseBodyCell, + getDatabaseHeaderColumn, + getFirstColumnCell, + initDatabaseColumn, + switchColumnType, +} from './actions.js'; + +test('edit database block title and create new rows', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + const locator = page.locator('affine-database'); + await expect(locator).toBeVisible(); + const dbTitle = 'Database 1'; + await assertBlockProps(page, '2', { + title: dbTitle, + }); + await focusDatabaseTitle(page); + await selectAllByKeyboard(page); + await pressBackspace(page); + + const expected = 'hello'; + await type(page, expected); + await assertBlockProps(page, '2', { + title: 'hello', + }); + await undoByClick(page); + await assertBlockProps(page, '2', { + title: 'Database 1', + }); + await initDatabaseRowWithData(page, ''); + await initDatabaseRowWithData(page, ''); + await assertRowCount(page, 2); + await waitNextFrame(page, 100); + await pressEscape(page); + await undoByClick(page); + await undoByClick(page); + await assertRowCount(page, 0); +}); + +test('edit column title', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, '1'); + + // first added column + const { column } = await getDatabaseHeaderColumn(page, 1); + expect(await column.innerText()).toBe('1'); + + await undoByClick(page); + expect(await column.innerText()).toBe('Column 1'); +}); + +test('should modify the value when the input loses focus', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Number'); + await initDatabaseDynamicRowWithData(page, '1', true); + + await clickDatabaseOutside(page); + const cell = getFirstColumnCell(page, 'number'); + const text = await cell.textContent(); + expect(text?.trim()).toBe('1'); +}); + +test('should rich-text column support soft enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Text'); + await initDatabaseDynamicRowWithData(page, '123', true); + + const cell = getFirstColumnCell(page, 'affine-database-rich-text'); + await cell.click(); + await pressArrowLeft(page); + await pressEnter(page); + await assertDatabaseCellRichTexts(page, { text: '123' }); + + await cell.click(); + await pressArrowRight(page); + await pressArrowLeft(page); + await pressShiftEnter(page); + await pressEnter(page); + await assertDatabaseCellRichTexts(page, { text: '12\n3' }); +}); + +test('should the multi-select mode work correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '1', true); + await pressEscape(page); + await initDatabaseDynamicRowWithData(page, '2'); + await pressEscape(page); + const cell = getFirstColumnCell(page, 'select-selected'); + expect(await cell.count()).toBe(2); + expect(await cell.nth(0).innerText()).toBe('1'); + expect(await cell.nth(1).innerText()).toBe('2'); +}); + +test('should database search work', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseRowWithData(page, 'text1'); + await initDatabaseDynamicRowWithData(page, '123', false); + await pressEscape(page); + await initDatabaseRowWithData(page, 'text2'); + await initDatabaseDynamicRowWithData(page, 'a', false); + await pressEscape(page); + await initDatabaseRowWithData(page, 'text3'); + await initDatabaseDynamicRowWithData(page, '26', false); + await pressEscape(page); + // search for '2' + await focusDatabaseSearch(page); + await type(page, '2'); + const rows = page.locator('.affine-database-block-row'); + expect(await rows.count()).toBe(3); + + // search for '23' + await type(page, '3'); + expect(await rows.count()).toBe(1); + + const cell = page.locator('.select-selected'); + expect(await cell.innerText()).toBe('123'); + + // clear search input + const closeIcon = page.locator('.close-icon'); + await closeIcon.click(); + expect(await rows.count()).toBe(3); +}); + +test('should database search input displayed correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await focusDatabaseSearch(page); + await blurDatabaseSearch(page); + await assertDatabaseSearching(page, false); + + await focusDatabaseSearch(page); + await type(page, '2'); + await blurDatabaseSearch(page); + await assertDatabaseSearching(page, true); + + await focusDatabaseSearch(page); + await pressBackspace(page); + await blurDatabaseSearch(page); + await assertDatabaseSearching(page, false); + + await focusDatabaseSearch(page); + await type(page, '2'); + const closeIcon = page.locator('.close-icon'); + await closeIcon.click(); + await blurDatabaseSearch(page); + await assertDatabaseSearching(page, false); + + await focusDatabaseSearch(page); + await type(page, '2'); + await pressEscape(page); + await blurDatabaseSearch(page); + await assertDatabaseSearching(page, false); +}); + +test('should database title and rich-text support undo/redo', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Text'); + await initDatabaseDynamicRowWithData(page, '123', true); + await undoByKeyboard(page); + await assertDatabaseCellRichTexts(page, { text: '' }); + await pressEscape(page); + await redoByKeyboard(page); + await assertDatabaseCellRichTexts(page, { text: '123' }); + + await focusDatabaseTitle(page); + await type(page, 'abc'); + await assertDatabaseTitleText(page, 'Database 1abc'); + await undoByKeyboard(page); + await assertDatabaseTitleText(page, 'Database 1'); + await redoByKeyboard(page); + await assertDatabaseTitleText(page, 'Database 1abc'); +}); + +test('should support drag to change column width', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + const headerColumns = page.locator('.affine-database-column'); + const titleColumn = headerColumns.nth(0); + const normalColumn = headerColumns.nth(1); + + const dragDistance = 100; + const titleColumnWidth = 260; + const normalColumnWidth = 180; + + await assertColumnWidth(titleColumn, titleColumnWidth - 1); + const box = await assertColumnWidth(normalColumn, normalColumnWidth - 1); + + await dragBetweenCoords( + page, + { x: box.x, y: box.y }, + { x: box.x + dragDistance, y: box.y }, + { + steps: 50, + beforeMouseUp: async () => { + await waitNextFrame(page); + }, + } + ); + + await assertColumnWidth(titleColumn, titleColumnWidth + dragDistance); + await assertColumnWidth(normalColumn, normalColumnWidth - 1); + + await undoByClick(page); + await assertColumnWidth(titleColumn, titleColumnWidth - 1); + await assertColumnWidth(normalColumn, normalColumnWidth - 1); +}); + +test('should display the add column button on the right side of database correctly', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + const normalColumn = page.locator('.affine-database-column').nth(1); + + const addColumnBtn = page.locator('.header-add-column-button'); + + const box = await getBoundingBox(normalColumn); + await dragBetweenCoords( + page, + { x: box.x, y: box.y }, + { x: box.x + 400, y: box.y }, + { + steps: 50, + beforeMouseUp: async () => { + await waitNextFrame(page); + }, + } + ); + await focusDatabaseHeader(page); + await expect(addColumnBtn).toBeVisible(); +}); + +test('should support drag and drop to move columns', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page, 'column1'); + await initDatabaseColumn(page, 'column2'); + await initDatabaseColumn(page, 'column3'); + + const column1 = await focusDatabaseHeader(page, 1); + const moveIcon = column1.locator('.affine-database-column-move'); + const moveIconBox = await getBoundingBox(moveIcon); + const x = moveIconBox.x + moveIconBox.width / 2; + const y = moveIconBox.y + moveIconBox.height / 2; + + await dragBetweenCoords( + page, + { x, y }, + { x: x + 100, y }, + { + steps: 50, + beforeMouseUp: async () => { + await waitNextFrame(page); + const indicator = page.locator('.vertical-indicator').first(); + await expect(indicator).toBeVisible(); + + const { box } = await getDatabaseHeaderColumn(page, 2); + const indicatorBox = await getBoundingBox(indicator); + expect(box.x + box.width - indicatorBox.x < 10).toBe(true); + }, + } + ); + + const { text } = await getDatabaseHeaderColumn(page, 2); + expect(text).toBe('column1'); +}); + +test('should title column support quick renaming', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, 'a', true); + await pressEscape(page); + await focusDatabaseHeader(page, 1); + const { textElement } = await getDatabaseHeaderColumn(page, 1); + await textElement.click(); + await waitNextFrame(page); + await selectAllByKeyboard(page); + await type(page, '123'); + await pressEnter(page); + expect(await textElement.innerText()).toBe('123'); + + await undoByClick(page); + expect(await textElement.innerText()).toBe('Column 1'); + await textElement.click(); + await waitNextFrame(page); + await selectAllByKeyboard(page); + await type(page, '123'); + await pressEnter(page); + expect(await textElement.innerText()).toBe('123'); +}); + +test('should title column support quick changing of column type', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, 'a', true); + await pressEscape(page); + await initDatabaseDynamicRowWithData(page, 'b'); + await pressEscape(page); + await focusDatabaseHeader(page, 1); + const { typeIcon } = await getDatabaseHeaderColumn(page, 1); + await typeIcon.click(); + await waitNextFrame(page); + await clickColumnType(page, 'Select'); + const cell = getFirstColumnCell(page, 'select-selected'); + expect(await cell.count()).toBe(1); +}); + +test('database format-bar in header and text column', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Text'); + await initDatabaseDynamicRowWithData(page, 'column', true); + await pressArrowLeft(page); + await pressEnter(page); + await type(page, 'header'); + // Title | Column1 + // ---------------- + // header | column + + const formatBar = getFormatBar(page); + await setInlineRangeInInlineEditor(page, { index: 1, length: 4 }, 1); + expect(await formatBar.formatBar.isVisible()).toBe(true); + // Title | Column1 + // ---------------- + // h|eade|r | column + + await assertInlineEditorDeltas( + page, + [ + { + insert: 'header', + }, + ], + 1 + ); + await formatBar.boldBtn.click(); + await assertInlineEditorDeltas( + page, + [ + { + insert: 'h', + }, + { + insert: 'eade', + attributes: { + bold: true, + }, + }, + { + insert: 'r', + }, + ], + 1 + ); + + await pressEscape(page); + await pressArrowRight(page); + await pressEnter(page); + await setInlineRangeInInlineEditor(page, { index: 2, length: 2 }, 2); + expect(await formatBar.formatBar.isVisible()).toBe(true); + // Title | Column1 + // ---------------- + // header | co|lu|mn + + await assertInlineEditorDeltas( + page, + [ + { + insert: 'column', + }, + ], + 2 + ); + await formatBar.boldBtn.click(); + await assertInlineEditorDeltas( + page, + [ + { + insert: 'co', + }, + { + insert: 'lu', + attributes: { + bold: true, + }, + }, + { + insert: 'mn', + }, + ], + 2 + ); +}); + +test.describe('readonly mode', () => { + test('database title should not be edited in readonly mode', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + const locator = page.locator('affine-database'); + await expect(locator).toBeVisible(); + + const dbTitle = 'Database 1'; + await assertBlockProps(page, '2', { + title: dbTitle, + }); + + await focusDatabaseTitle(page); + await selectAllByKeyboard(page); + await pressBackspace(page); + + await type(page, 'hello'); + await assertBlockProps(page, '2', { + title: 'hello', + }); + + await switchReadonly(page); + + await type(page, ' world'); + await assertBlockProps(page, '2', { + title: 'hello', + }); + + await pressBackspace(page, 'hello world'.length); + await assertBlockProps(page, '2', { + title: 'hello', + }); + }); + + test('should rich-text not be edited in readonly mode', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Text'); + await initDatabaseDynamicRowWithData(page, '', true); + + const cell = getFirstColumnCell(page, 'affine-database-rich-text'); + await cell.click(); + await type(page, '123'); + await assertDatabaseCellRichTexts(page, { text: '123' }); + + await switchReadonly(page); + await pressBackspace(page); + await type(page, '789'); + await assertDatabaseCellRichTexts(page, { text: '123' }); + }); + + test('should hide edit widget after switch to readonly mode', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Text'); + await initDatabaseDynamicRowWithData(page, '', true); + + const database = page.locator('affine-database'); + await expect(database).toBeVisible(); + + const databaseMenu = database.locator('.database-ops'); + await expect(databaseMenu).toBeVisible(); + + const addViewButton = database.getByTestId('database-add-view-button'); + await expect(addViewButton).toBeVisible(); + + const titleHeader = page.locator('affine-database-header-column').filter({ + hasText: 'Title', + }); + await titleHeader.hover(); + const columnDragBar = titleHeader.locator('.control-r'); + await expect(columnDragBar).toBeVisible(); + + const filter = database.locator('data-view-header-tools-filter'); + const search = database.locator('data-view-header-tools-search'); + const options = database.locator('data-view-header-tools-view-options'); + const headerAddRow = database.locator('data-view-header-tools-add-row'); + + await database.hover(); + await expect(filter).toBeVisible(); + await expect(search).toBeVisible(); + await expect(options).toBeVisible(); + await expect(headerAddRow).toBeVisible(); + + const row = database.locator('data-view-table-row'); + const rowOptions = row.locator('.row-op'); + const rowDragBar = row.locator('.data-view-table-view-drag-handler>div'); + await row.hover(); + await expect(rowOptions).toHaveCount(2); + await expect(rowOptions.nth(0)).toBeVisible(); + await expect(rowOptions.nth(1)).toBeVisible(); + await expect(rowDragBar).toBeVisible(); + + const addRow = database.locator('.data-view-table-group-add-row'); + await expect(addRow).toBeVisible(); + + // Readonly Mode + { + await switchReadonly(page); + await expect(databaseMenu).toBeHidden(); + await expect(addViewButton).toBeHidden(); + + await titleHeader.hover(); + await expect(columnDragBar).toBeHidden(); + + await database.hover(); + await expect(filter).toBeHidden(); + await expect(search).toBeVisible(); // Note the search should not be hidden + await expect(options).toBeHidden(); + await expect(headerAddRow).toBeHidden(); + + await row.hover(); + await expect(rowOptions.nth(0)).toBeHidden(); + await expect(rowOptions.nth(1)).toBeHidden(); + await expect(rowDragBar).toBeHidden(); + + await expect(addRow).toBeHidden(); + } + }); + + test('should hide focus border after switch to readonly mode', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Text'); + await initDatabaseDynamicRowWithData(page, '', true); + + const database = page.locator('affine-database'); + await expect(database).toBeVisible(); + + const cell = getFirstColumnCell(page, 'affine-database-rich-text'); + await cell.click(); + + const focusBorder = database.locator( + 'data-view-table-selection .database-focus' + ); + await expect(focusBorder).toBeVisible(); + + await switchReadonly(page); + await expect(focusBorder).toBeHidden(); + }); + + test('should hide selection after switch to readonly mode', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await switchColumnType(page, 'Text'); + await initDatabaseDynamicRowWithData(page, '', true); + + const database = page.locator('affine-database'); + await expect(database).toBeVisible(); + + const startCell = getDatabaseBodyCell(page, { + rowIndex: 0, + columnIndex: 0, + }); + const endCell = getDatabaseBodyCell(page, { + rowIndex: 0, + columnIndex: 1, + }); + + const startBox = await getBoundingBox(startCell); + const endBox = await getBoundingBox(endCell); + + const startX = startBox.x + startBox.width / 2; + const startY = startBox.y + startBox.height / 2; + const endX = endBox.x + endBox.width / 2; + const endY = endBox.y + endBox.height / 2; + + await dragBetweenCoords( + page, + { x: startX, y: startY }, + { x: endX, y: endY } + ); + + const selection = database.locator( + 'data-view-table-selection .database-selection' + ); + + await expect(selection).toBeVisible(); + + await switchReadonly(page); + await expect(selection).toBeHidden(); + }); +}); diff --git a/blocksuite/tests-legacy/database/selection.spec.ts b/blocksuite/tests-legacy/database/selection.spec.ts new file mode 100644 index 0000000000000..e0911a1b7c683 --- /dev/null +++ b/blocksuite/tests-legacy/database/selection.spec.ts @@ -0,0 +1,567 @@ +import { expect } from '@playwright/test'; + +import { dragBetweenCoords } from '../utils/actions/drag.js'; +import { shiftClick } from '../utils/actions/edgeless.js'; +import { + pressArrowDown, + pressArrowDownWithShiftKey, + pressArrowLeft, + pressArrowRight, + pressArrowUp, + pressArrowUpWithShiftKey, + pressBackspace, + pressEnter, + pressEscape, + type, +} from '../utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + getBoundingBox, + initDatabaseDynamicRowWithData, + initDatabaseRowWithData, + initEmptyDatabaseState, + initKanbanViewState, + waitNextFrame, +} from '../utils/actions/misc.js'; +import { test } from '../utils/playwright.js'; +import { + assertCellsSelection, + assertDatabaseTitleColumnText, + assertKanbanCardHeaderText, + assertKanbanCardSelected, + assertKanbanCellSelected, + assertRowsSelection, + clickKanbanCardHeader, + focusKanbanCardHeader, + getDatabaseBodyCell, + getKanbanCard, + initDatabaseColumn, + switchColumnType, +} from './actions.js'; + +test.describe('focus', () => { + test('should support move focus by arrow key', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await pressEscape(page); + await waitNextFrame(page, 100); + await pressEscape(page); + await assertRowsSelection(page, [0, 0]); + }); + + test('should support multi row selection', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + await switchColumnType(page, 'Number'); + await initDatabaseDynamicRowWithData(page, '123', true); + + const selectColumn = getDatabaseBodyCell(page, { + rowIndex: 1, + columnIndex: 1, + }); + + const endBox = await getBoundingBox(selectColumn); + const endX = endBox.x + endBox.width / 2; + + await dragBetweenCoords( + page, + { x: endX, y: endBox.y }, + { x: endX, y: endBox.y + endBox.height } + ); + await pressEscape(page); + await assertRowsSelection(page, [0, 1]); + }); + + test('should support row selection with dynamic height', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123123', true); + await type(page, '456456'); + await pressEnter(page); + await type(page, 'abcabc'); + await pressEnter(page); + await type(page, 'defdef'); + await pressEnter(page); + await pressEscape(page); + + await pressEscape(page); + await assertRowsSelection(page, [0, 0]); + }); +}); + +test.describe('row-level selection', () => { + test('should support title selection', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseRowWithData(page, 'title'); + await pressEscape(page); + await waitNextFrame(page, 100); + await assertCellsSelection(page, { + start: [0, 0], + }); + + await pressEscape(page); + await assertRowsSelection(page, [0, 0]); + }); + + test('should support pressing esc to trigger row selection', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await pressEscape(page); + await waitNextFrame(page, 100); + await pressEscape(page); + await assertRowsSelection(page, [0, 0]); + }); + + test('should support multi row selection', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + await switchColumnType(page, 'Number'); + await initDatabaseDynamicRowWithData(page, '123', true); + + const selectColumn = getDatabaseBodyCell(page, { + rowIndex: 1, + columnIndex: 1, + }); + + const endBox = await getBoundingBox(selectColumn); + const endX = endBox.x + endBox.width / 2; + + await dragBetweenCoords( + page, + { x: endX, y: endBox.y }, + { x: endX, y: endBox.y + endBox.height } + ); + await pressEscape(page); + await assertRowsSelection(page, [0, 1]); + }); + + test('should support row selection with dynamic height', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '123123', true); + await type(page, '456456'); + await pressEnter(page); + await type(page, 'abcabc'); + await pressEnter(page); + await type(page, 'defdef'); + await pressEnter(page); + await pressEscape(page); + + await pressEscape(page); + await assertRowsSelection(page, [0, 0]); + }); + + test('move row selection with (up | down)', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + + // add two rows + await initDatabaseDynamicRowWithData(page, '123123', true); + await pressEscape(page); + + await initDatabaseDynamicRowWithData(page, '123123', true); + await pressEscape(page); + + await pressEscape(page); // switch to row selection + + await assertRowsSelection(page, [1, 1]); + + await pressArrowUp(page); + await assertRowsSelection(page, [0, 0]); + + // should not allow under selection + await pressArrowUp(page); + await assertRowsSelection(page, [0, 0]); + + await pressArrowDown(page); + await assertRowsSelection(page, [1, 1]); + + // should not allow over selection + await pressArrowDown(page); + await assertRowsSelection(page, [1, 1]); + }); + + test('increment decrement row selection with shift+(up | down)', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + + // add two rows + await initDatabaseDynamicRowWithData(page, '123123', true); + await pressEscape(page); + + await initDatabaseDynamicRowWithData(page, '123123', true); + await pressEscape(page); + + await pressEscape(page); // switch to row selection + + await pressArrowUpWithShiftKey(page); + await assertRowsSelection(page, [0, 1]); + + await pressArrowDownWithShiftKey(page); + await assertRowsSelection(page, [1, 1]); // should decrement back + + await pressArrowUp(page); // go to first row + + await pressArrowDownWithShiftKey(page); + await assertRowsSelection(page, [0, 1]); + + await pressArrowUpWithShiftKey(page); + await assertRowsSelection(page, [0, 0]); + }); +}); + +test.describe('cell-level selection', () => { + test('should support multi cell selection', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseDynamicRowWithData(page, '', true); + await pressEscape(page); + await switchColumnType(page, 'Number'); + await initDatabaseDynamicRowWithData(page, '123', true); + + const startCell = getDatabaseBodyCell(page, { + rowIndex: 0, + columnIndex: 0, + }); + const endCell = getDatabaseBodyCell(page, { + rowIndex: 1, + columnIndex: 1, + }); + + const startBox = await getBoundingBox(startCell); + const endBox = await getBoundingBox(endCell); + + const startX = startBox.x + startBox.width / 2; + const startY = startBox.y + startBox.height / 2; + const endX = endBox.x + endBox.width / 2; + const endY = endBox.y + endBox.height / 2; + + await dragBetweenCoords( + page, + { x: startX, y: startY }, + { x: endX, y: endY } + ); + + await assertCellsSelection(page, { + start: [0, 0], + end: [1, 1], + }); + }); + + test("should support backspace key to delete cell's content", async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + await initDatabaseColumn(page); + await initDatabaseRowWithData(page, 'row1'); + await initDatabaseDynamicRowWithData(page, 'abc', false); + await pressEscape(page); + await initDatabaseRowWithData(page, 'row2'); + await initDatabaseDynamicRowWithData(page, '123', false); + await pressEscape(page); + + const startCell = getDatabaseBodyCell(page, { + rowIndex: 0, + columnIndex: 0, + }); + const endCell = getDatabaseBodyCell(page, { + rowIndex: 1, + columnIndex: 1, + }); + + const startBox = await getBoundingBox(startCell); + const endBox = await getBoundingBox(endCell); + + const startX = startBox.x + startBox.width / 2; + const startY = startBox.y + startBox.height / 2; + const endX = endBox.x + endBox.width / 2; + const endY = endBox.y + endBox.height / 2; + + await dragBetweenCoords( + page, + { x: startX, y: startY }, + { x: endX, y: endY } + ); + + await pressBackspace(page); + await assertDatabaseTitleColumnText(page, '', 0); + await assertDatabaseTitleColumnText(page, '', 1); + const selectCell1 = getDatabaseBodyCell(page, { + rowIndex: 0, + columnIndex: 1, + }); + expect(await selectCell1.innerText()).toBe(''); + const selectCell2 = getDatabaseBodyCell(page, { + rowIndex: 1, + columnIndex: 1, + }); + expect(await selectCell2.innerText()).toBe(''); + }); +}); + +test.describe('kanban view selection', () => { + test("should support move cell's focus by arrow key(up&down) within a card", async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initKanbanViewState(page, { + rows: ['row1'], + columns: [ + { + type: 'number', + value: [1], + }, + { + type: 'rich-text', + value: ['text'], + }, + ], + }); + + await focusKanbanCardHeader(page); + await assertKanbanCellSelected(page, { + // group by `number` column, the first(groupIndex: 0) group is `Ungroups` + groupIndex: 1, + cardIndex: 0, + cellIndex: 0, + }); + + await pressArrowDown(page, 3); + await assertKanbanCellSelected(page, { + groupIndex: 1, + cardIndex: 0, + cellIndex: 0, + }); + + await pressArrowUp(page); + await assertKanbanCellSelected(page, { + groupIndex: 1, + cardIndex: 0, + cellIndex: 2, + }); + }); + + test("should support move cell's focus by arrow key(up&down) within multi cards", async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initKanbanViewState(page, { + rows: ['row1', 'row2'], + columns: [ + { + type: 'number', + value: [1, 2], + }, + { + type: 'rich-text', + value: ['text'], + }, + ], + }); + + await focusKanbanCardHeader(page); + await pressArrowUp(page); + await assertKanbanCellSelected(page, { + groupIndex: 1, + cardIndex: 1, + cellIndex: 2, + }); + + await pressArrowDown(page); + await assertKanbanCellSelected(page, { + groupIndex: 1, + cardIndex: 0, + cellIndex: 0, + }); + }); + + test("should support move cell's focus by arrow key(left&right)", async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initKanbanViewState(page, { + rows: ['row1', 'row2', 'row3'], + columns: [ + { + type: 'number', + value: [undefined, 1, 10], + }, + ], + }); + + await focusKanbanCardHeader(page); + + await pressArrowRight(page, 3); + await assertKanbanCellSelected(page, { + groupIndex: 0, + cardIndex: 0, + cellIndex: 0, + }); + + await pressArrowLeft(page); + await assertKanbanCellSelected(page, { + groupIndex: 2, + cardIndex: 0, + cellIndex: 0, + }); + }); + + test("should support move card's focus by arrow key(up&down)", async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initKanbanViewState(page, { + rows: ['row1', 'row2', 'row3'], + columns: [ + { + type: 'number', + value: [undefined, undefined, undefined], + }, + ], + }); + + await focusKanbanCardHeader(page); + await pressEscape(page); + await pressEscape(page); + await assertKanbanCardSelected(page, { + groupIndex: 0, + cardIndex: 0, + }); + + await pressArrowDown(page, 3); + await assertKanbanCardSelected(page, { + groupIndex: 0, + cardIndex: 0, + }); + + await pressArrowUp(page); + await assertKanbanCardSelected(page, { + groupIndex: 0, + cardIndex: 2, + }); + }); + + test("should support move card's focus by arrow key(left&right)", async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initKanbanViewState(page, { + rows: ['row1', 'row2', 'row3'], + columns: [ + { + type: 'number', + value: [undefined, 1, 10], + }, + ], + }); + + await focusKanbanCardHeader(page); + await pressEscape(page); + await pressEscape(page); + + await pressArrowRight(page, 3); + await assertKanbanCardSelected(page, { + groupIndex: 0, + cardIndex: 0, + }); + + await pressArrowLeft(page); + await assertKanbanCardSelected(page, { + groupIndex: 2, + cardIndex: 0, + }); + }); + + test('should support multi card selection', async ({ page }) => { + await enterPlaygroundRoom(page); + await initKanbanViewState(page, { + rows: ['row1', 'row2'], + columns: [ + { + type: 'number', + value: [undefined, 1], + }, + ], + }); + + await focusKanbanCardHeader(page); + await pressEscape(page); + await pressEscape(page); + + const card = getKanbanCard(page, { + groupIndex: 1, + cardIndex: 0, + }); + const box = await getBoundingBox(card); + await shiftClick(page, { + x: box.x + box.width / 2, + y: box.y + box.height / 2, + }); + + await assertKanbanCardSelected(page, { + groupIndex: 0, + cardIndex: 0, + }); + await assertKanbanCardSelected(page, { + groupIndex: 1, + cardIndex: 0, + }); + }); + + test("should support move cursor in card's title by arrow key(left&right)", async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initKanbanViewState(page, { + rows: ['row1'], + columns: [ + { + type: 'rich-text', + value: ['text'], + }, + ], + }); + + await clickKanbanCardHeader(page); + await type(page, 'abc'); + await pressArrowLeft(page, 2); + await pressArrowRight(page); + await pressBackspace(page); + await pressEscape(page); + + await assertKanbanCardHeaderText(page, 'row1ac'); + }); +}); diff --git a/blocksuite/tests-legacy/database/sort.spec.ts b/blocksuite/tests-legacy/database/sort.spec.ts new file mode 100644 index 0000000000000..8c3eea01eef64 --- /dev/null +++ b/blocksuite/tests-legacy/database/sort.spec.ts @@ -0,0 +1,112 @@ +import { expect, type Locator } from '@playwright/test'; + +import { + enterPlaygroundRoom, + initDatabaseDynamicRowWithData, + initEmptyDatabaseState, + waitNextFrame, +} from '../utils/actions/index.js'; +import { test } from '../utils/playwright.js'; +import { initDatabaseColumn, switchColumnType } from './actions.js'; + +test('database sort with multiple rules', async ({ page }) => { + // Initialize database + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + + // Add test columns: Name (text) and Age (number) + await initDatabaseColumn(page, 'Name'); + await switchColumnType(page, 'Text', 1); + await initDatabaseColumn(page, 'Age'); + await switchColumnType(page, 'Number', 2); + + // Add test data + const testData = [ + { name: 'Alice', age: '25' }, + { name: 'Bob', age: '30' }, + { name: 'Alice', age: '20' }, + { name: 'Charlie', age: '25' }, + ]; + + for (const data of testData) { + await initDatabaseDynamicRowWithData(page, data.name, true, 0); + await initDatabaseDynamicRowWithData(page, data.age, false, 1); + } + + // Open sort menu + const sortButton = page.locator('data-view-header-tools-sort'); + await sortButton.click(); + + // Add first sort rule: Name ascending + await page.locator('affine-menu').getByText('Name').click(); + await waitNextFrame(page); + + // Add second sort rule: Age ascending + await page.getByText('Add sort').click(); + await page.locator('affine-menu').getByText('Age').click(); + await waitNextFrame(page); + + // Get all rows after sorting + const rows = await page.locator('affine-database-row').all(); + const getCellText = async (row: Locator, index: number) => { + const cell = row.locator('.cell').nth(index); + return cell.innerText(); + }; + + // Verify sorting results + // Should be sorted by Name first, then by Age + const expectedOrder = [ + { name: 'Alice', age: '20' }, + { name: 'Alice', age: '25' }, + { name: 'Bob', age: '30' }, + { name: 'Charlie', age: '25' }, + ]; + + for (let i = 0; i < rows.length; i++) { + const name = await getCellText(rows[i], 1); + const age = await getCellText(rows[i], 2); + expect(name).toBe(expectedOrder[i].name); + expect(age).toBe(expectedOrder[i].age); + } + + // Change sort order of Name to descending + await page.locator('.sort-item').first().getByText('Ascending').click(); + await page.getByText('Descending').click(); + await waitNextFrame(page); + + // Verify new sorting results + const expectedOrderDesc = [ + { name: 'Charlie', age: '25' }, + { name: 'Bob', age: '30' }, + { name: 'Alice', age: '20' }, + { name: 'Alice', age: '25' }, + ]; + + const rowsAfterDesc = await page.locator('affine-database-row').all(); + for (let i = 0; i < rowsAfterDesc.length; i++) { + const name = await getCellText(rowsAfterDesc[i], 1); + const age = await getCellText(rowsAfterDesc[i], 2); + expect(name).toBe(expectedOrderDesc[i].name); + expect(age).toBe(expectedOrderDesc[i].age); + } + + // Remove first sort rule + await page.locator('.sort-item').first().getByRole('img').last().click(); + await waitNextFrame(page); + + // Verify sorting now only by Age + const expectedOrderAgeOnly = [ + { name: 'Alice', age: '20' }, + { name: 'Alice', age: '25' }, + { name: 'Charlie', age: '25' }, + { name: 'Bob', age: '30' }, + ]; + + const rowsAfterRemove = await page.locator('affine-database-row').all(); + for (let i = 0; i < rowsAfterRemove.length; i++) { + const name = await getCellText(rowsAfterRemove[i], 1); + const age = await getCellText(rowsAfterRemove[i], 2); + expect(name).toBe(expectedOrderAgeOnly[i].name); + expect(age).toBe(expectedOrderAgeOnly[i].age); + } +}); diff --git a/blocksuite/tests-legacy/database/statistics.spec.ts b/blocksuite/tests-legacy/database/statistics.spec.ts new file mode 100644 index 0000000000000..536db36ca74f3 --- /dev/null +++ b/blocksuite/tests-legacy/database/statistics.spec.ts @@ -0,0 +1,103 @@ +import { press } from '@inline/__tests__/utils.js'; +import { expect, type Page } from '@playwright/test'; + +import { type } from '../utils/actions/index.js'; +import { + enterPlaygroundRoom, + getAddRow, + initEmptyDatabaseState, + waitNextFrame, +} from '../utils/actions/misc.js'; +import { test } from '../utils/playwright.js'; +import { changeColumnType, moveToCenterOf, pressKey } from './actions.js'; + +const addRow = async (page: Page, count: number = 1) => { + await waitNextFrame(page); + const addRow = getAddRow(page); + for (let i = 0; i < count; i++) { + await addRow.click(); + } + await press(page, 'Escape'); + await waitNextFrame(page); +}; +const insertRightColumn = async (page: Page, index = 0) => { + await waitNextFrame(page); + await page.locator('affine-database-header-column').nth(index).click(); + await waitNextFrame(page, 200); + await pressKey(page, 'Escape'); + const menu = page.locator('.affine-menu-button', { + hasText: new RegExp('Insert Right'), + }); + await menu.click(); + await waitNextFrame(page, 200); + await pressKey(page, 'Enter'); +}; +const menuSelect = async (page: Page, selectors: string[]) => { + await waitNextFrame(page); + for (const name of selectors) { + const menu = page.locator('.affine-menu-button', { + hasText: new RegExp(name), + }); + await menu.click(); + } +}; +test.describe('title', () => { + test('empty count', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + await addRow(page, 3); + const statCell = page.locator('affine-database-column-stats-cell').nth(0); + await moveToCenterOf(page, statCell); + await statCell.click(); + await menuSelect(page, ['Count', 'Count Empty']); + const value = statCell.locator('.value'); + expect((await value.textContent())?.trim()).toBe('3'); + await page.locator('affine-database-cell-container').nth(0).click(); + await pressKey(page, 'Enter'); + await type(page, 'asd'); + await pressKey(page, 'Escape'); + expect((await value.textContent())?.trim()).toBe('2'); + }); +}); + +test.describe('rich-text', () => { + test('empty count', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + await addRow(page, 3); + await insertRightColumn(page); + await changeColumnType(page, 1, 'text'); + const statCell = page.locator('affine-database-column-stats-cell').nth(1); + await moveToCenterOf(page, statCell); + await statCell.click(); + await menuSelect(page, ['Count', 'Count Empty']); + const value = statCell.locator('.value'); + expect((await value.textContent())?.trim()).toBe('3'); + await page.locator('affine-database-cell-container').nth(1).click(); + await pressKey(page, 'Enter'); + await type(page, 'asd'); + await pressKey(page, 'Escape'); + expect((await value.textContent())?.trim()).toBe('2'); + }); +}); + +test.describe('select', () => { + test('empty count', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + await addRow(page, 3); + await insertRightColumn(page); + await changeColumnType(page, 1, 'select'); + const statCell = page.locator('affine-database-column-stats-cell').nth(1); + await moveToCenterOf(page, statCell); + await statCell.click(); + await menuSelect(page, ['Count', 'Count Empty']); + const value = statCell.locator('.value'); + expect((await value.textContent())?.trim()).toBe('3'); + await page.locator('affine-database-cell-container').nth(1).click(); + await pressKey(page, 'Enter'); + await type(page, 'select'); + await pressKey(page, 'Enter'); + expect((await value.textContent())?.trim()).toBe('2'); + }); +}); diff --git a/blocksuite/tests-legacy/database/title.spec.ts b/blocksuite/tests-legacy/database/title.spec.ts new file mode 100644 index 0000000000000..5c5e68440490d --- /dev/null +++ b/blocksuite/tests-legacy/database/title.spec.ts @@ -0,0 +1,19 @@ +import { press } from '@inline/__tests__/utils.js'; +import { expect } from '@playwright/test'; + +import { + enterPlaygroundRoom, + initDatabaseDynamicRowWithData, + initEmptyDatabaseState, +} from '../utils/actions/misc.js'; +import { test } from '../utils/playwright.js'; + +test.describe('title', () => { + test('should able to link doc by press @', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyDatabaseState(page); + await initDatabaseDynamicRowWithData(page, '123', true); + await press(page, '@'); + await expect(page.locator('.linked-doc-popover')).toBeVisible(); + }); +}); diff --git a/blocksuite/tests-legacy/drag.spec.ts b/blocksuite/tests-legacy/drag.spec.ts new file mode 100644 index 0000000000000..1c9146371e310 --- /dev/null +++ b/blocksuite/tests-legacy/drag.spec.ts @@ -0,0 +1,768 @@ +import { BLOCK_CHILDREN_CONTAINER_PADDING_LEFT } from '@blocks/_common/consts.js'; +import { expect } from '@playwright/test'; + +import { + dragBetweenCoords, + dragBetweenIndices, + dragHandleFromBlockToBlockBottomById, + enterPlaygroundRoom, + focusRichText, + initEmptyParagraphState, + initThreeLists, + initThreeParagraphs, + pressEnter, + pressShiftTab, + pressTab, + type, +} from './utils/actions/index.js'; +import { + getBoundingClientRect, + getEditorHostLocator, + getPageSnapshot, + initParagraphsByCount, +} from './utils/actions/misc.js'; +import { assertRichTexts } from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +test('only have one drag handle in screen', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + const topLeft = await page.evaluate(() => { + const paragraph = document.querySelector('[data-block-id="2"]'); + const box = paragraph?.getBoundingClientRect(); + if (!box) { + throw new Error(); + } + return { x: box.left, y: box.top + 2 }; + }, []); + + const bottomRight = await page.evaluate(() => { + const paragraph = document.querySelector('[data-block-id="4"]'); + const box = paragraph?.getBoundingClientRect(); + if (!box) { + throw new Error(); + } + return { x: box.right, y: box.bottom - 2 }; + }, []); + + await page.mouse.move(topLeft.x, topLeft.y); + const length1 = await page.evaluate(() => { + const handles = document.querySelectorAll('affine-drag-handle-widget'); + return handles.length; + }, []); + expect(length1).toBe(1); + await page.mouse.move(bottomRight.x, bottomRight.y); + const length2 = await page.evaluate(() => { + const handles = document.querySelectorAll('affine-drag-handle-widget'); + return handles.length; + }, []); + expect(length2).toBe(1); +}); + +test('move drag handle in paragraphs', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + await dragHandleFromBlockToBlockBottomById(page, '2', '4'); + await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + await assertRichTexts(page, ['456', '789', '123']); +}); + +test('move drag handle in list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeLists(page); + await assertRichTexts(page, ['123', '456', '789']); + await dragHandleFromBlockToBlockBottomById(page, '5', '3', false); + await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + await assertRichTexts(page, ['789', '123', '456']); +}); + +test('move drag handle in nested block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '-'); + await page.keyboard.press('Space', { delay: 50 }); + await type(page, '1'); + await pressEnter(page); + await type(page, '2'); + + await pressEnter(page); + await pressTab(page); + await type(page, '21'); + await pressEnter(page); + await type(page, '22'); + await pressEnter(page); + await type(page, '23'); + await pressEnter(page); + await pressShiftTab(page); + + await type(page, '3'); + + await assertRichTexts(page, ['1', '2', '21', '22', '23', '3']); + + await dragHandleFromBlockToBlockBottomById(page, '5', '7'); + await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + await assertRichTexts(page, ['1', '2', '22', '23', '21', '3']); + + // FIXME(DND) + // await dragHandleFromBlockToBlockBottomById(page, '3', '8'); + // await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + // await assertRichTexts(page, ['2', '22', '23', '21', '3', '1']); +}); + +test('move drag handle into another block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '-'); + await page.keyboard.press('Space', { delay: 50 }); + await type(page, '1'); + await pressEnter(page); + await type(page, '2'); + + await pressEnter(page); + await pressTab(page); + await type(page, '21'); + await pressEnter(page); + await type(page, '22'); + await pressEnter(page); + await type(page, '23'); + await pressEnter(page); + await pressShiftTab(page); + + await type(page, '3'); + + await assertRichTexts(page, ['1', '2', '21', '22', '23', '3']); + + await dragHandleFromBlockToBlockBottomById( + page, + '5', + '7', + true, + 2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT + ); + await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + await assertRichTexts(page, ['1', '2', '22', '23', '21', '3']); + // FIXME(DND) + // await assertBlockChildrenIds(page, '7', ['5']); + + // await dragHandleFromBlockToBlockBottomById( + // page, + // '3', + // '8', + // true, + // 2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT + // ); + // await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + // await assertRichTexts(page, ['2', '22', '23', '21', '3', '1']); + // await assertBlockChildrenIds(page, '8', ['3']); +}); + +test('move to the last block of each level in multi-level nesting', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '-'); + await page.keyboard.press('Space', { delay: 50 }); + await type(page, 'A'); + await pressEnter(page); + await type(page, 'B'); + await pressEnter(page); + await type(page, 'C'); + await pressEnter(page); + await pressTab(page); + await type(page, 'D'); + await pressEnter(page); + await type(page, 'E'); + await pressEnter(page); + await pressTab(page); + await type(page, 'F'); + await pressEnter(page); + await type(page, 'G'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await dragHandleFromBlockToBlockBottomById(page, '3', '9'); + await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_drag_3_9.json` + ); + + // FIXME(DND) + // await dragHandleFromBlockToBlockBottomById( + // page, + // '4', + // '3', + // true, + // -(1 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT) + // ); + // await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + // + // expect(await getPageSnapshot(page, true)).toMatchSnapshot( + // `${testInfo.title}_drag_4_3.json` + // ); + // + // await assertRichTexts(page, ['C', 'D', 'E', 'F', 'G', 'A', 'B']); + // await dragHandleFromBlockToBlockBottomById( + // page, + // '3', + // '4', + // true, + // -(2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT) + // ); + // await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + // + // expect(await getPageSnapshot(page, true)).toMatchSnapshot( + // `${testInfo.title}_drag_3_4.json` + // ); + // + // await assertRichTexts(page, ['C', 'D', 'E', 'F', 'G', 'B', 'A']); +}); + +test('should sync selected-blocks to session-manager when clicking drag handle', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await focusRichText(page, 1); + const rect = await getBoundingClientRect(page, '[data-block-id="1"]'); + if (!rect) { + throw new Error(); + } + await page.mouse.move(rect.x + 10, rect.y + 10, { steps: 2 }); + + const handle = page.locator('.affine-drag-handle-container'); + await handle.click(); + + await page.keyboard.press('Backspace'); + await assertRichTexts(page, ['', '456', '789']); +}); + +test.fixme( + 'should be able to drag & drop multiple blocks', + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await dragBetweenIndices( + page, + [0, 0], + [1, 3], + { x: -60, y: 0 }, + { x: 80, y: 0 }, + { + steps: 50, + } + ); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(2); + + await dragHandleFromBlockToBlockBottomById(page, '2', '4', true); + await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + + await assertRichTexts(page, ['789', '123', '456']); + + // Selection is still 2 after drop + await expect(blockSelections).toHaveCount(2); + } +); + +test.fixme( + 'should be able to drag & drop multiple blocks to nested block', + async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '-'); + await page.keyboard.press('Space', { delay: 50 }); + await type(page, 'A'); + await pressEnter(page); + await type(page, 'B'); + await pressEnter(page); + await type(page, 'C'); + await pressEnter(page); + await pressTab(page); + await type(page, 'D'); + await pressEnter(page); + await type(page, 'E'); + await pressEnter(page); + await pressTab(page); + await type(page, 'F'); + await pressEnter(page); + await type(page, 'G'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await dragBetweenIndices( + page, + [0, 0], + [1, 1], + { x: -80, y: 0 }, + { x: 80, y: 0 }, + { + steps: 50, + } + ); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(2); + + await dragHandleFromBlockToBlockBottomById(page, '3', '8'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); + } +); + +test('should blur rich-text first on starting block selection', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await expect(page.locator('*:focus')).toHaveCount(1); + + await dragHandleFromBlockToBlockBottomById(page, '2', '4'); + await expect(page.locator('.affine-drag-indicator')).toBeHidden(); + await assertRichTexts(page, ['456', '789', '123']); + + await expect(page.locator('*:focus')).toHaveCount(0); +}); + +test('hide drag handle when mouse is hovering over the title', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + const rect = await getBoundingClientRect( + page, + '.affine-note-block-container' + ); + const dragHandle = page.locator('.affine-drag-handle-container'); + // When there is a gap between paragraph blocks, it is the correct behavior for the drag handle to appear + // when the mouse is over the gap. Therefore, we use rect.y - 20 to make the Y offset greater than the gap between the + // paragraph blocks. + await page.mouse.move(rect.x, rect.y - 20, { steps: 2 }); + await expect(dragHandle).toBeHidden(); + + await page.mouse.move(rect.x, rect.y, { steps: 2 }); + expect(await dragHandle.isVisible()).toBe(true); + await expect(dragHandle).toBeVisible(); +}); + +test.fixme('should create preview when dragging', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + const dragPreview = page.locator('affine-drag-preview'); + + await dragBetweenIndices( + page, + [0, 0], + [1, 3], + { x: -60, y: 0 }, + { x: 80, y: 0 }, + { + steps: 50, + } + ); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(2); + + await dragHandleFromBlockToBlockBottomById( + page, + '2', + '4', + true, + undefined, + async () => { + await expect(dragPreview).toBeVisible(); + await expect(dragPreview.locator('[data-block-id]')).toHaveCount(4); + } + ); +}); + +test.fixme( + 'should drag and drop blocks under block-level selection', + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await dragBetweenIndices( + page, + [0, 0], + [1, 3], + { x: -60, y: 0 }, + { x: 80, y: 0 }, + { + steps: 50, + } + ); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(2); + + const editorHost = getEditorHostLocator(page); + const editors = editorHost.locator('rich-text'); + const editorRect0 = await editors.nth(0).boundingBox(); + const editorRect2 = await editors.nth(2).boundingBox(); + if (!editorRect0 || !editorRect2) { + throw new Error(); + } + + await dragBetweenCoords( + page, + { + x: editorRect0.x - 10, + y: editorRect0.y + editorRect0.height / 2, + }, + { + x: editorRect2.x + 10, + y: editorRect2.y + editorRect2.height / 2 + 10, + }, + { + steps: 50, + } + ); + + await assertRichTexts(page, ['789', '123', '456']); + await expect(blockSelections).toHaveCount(2); + } +); + +test('should trigger click event on editor container when clicking on blocks under block-level selection', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await dragBetweenIndices( + page, + [0, 0], + [1, 3], + { x: -60, y: 0 }, + { x: 80, y: 0 }, + { + steps: 50, + } + ); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(2); + await expect(page.locator('*:focus')).toHaveCount(0); + + const editorHost = getEditorHostLocator(page); + const editors = editorHost.locator('rich-text'); + const editorRect0 = await editors.nth(0).boundingBox(); + if (!editorRect0) { + throw new Error(); + } + + await page.mouse.move( + editorRect0.x + 10, + editorRect0.y + editorRect0.height / 2 + ); + await page.mouse.down(); + await page.mouse.up(); + await expect(blockSelections).toHaveCount(0); + await expect(page.locator('*:focus')).toHaveCount(1); +}); + +test('should get to selected block when dragging unselected block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '123'); + await pressEnter(page); + await type(page, '456'); + await assertRichTexts(page, ['123', '456']); + + const editorHost = getEditorHostLocator(page); + const editors = editorHost.locator('rich-text'); + const editorRect0 = await editors.nth(0).boundingBox(); + const editorRect1 = await editors.nth(1).boundingBox(); + + if (!editorRect0 || !editorRect1) { + throw new Error(); + } + + await page.mouse.move(editorRect1.x - 5, editorRect0.y); + await page.mouse.down(); + await page.mouse.up(); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(1); + + await page.mouse.move(editorRect1.x - 5, editorRect0.y); + await page.mouse.down(); + await page.mouse.move( + editorRect1.x - 5, + editorRect1.y + editorRect1.height / 2 + 1, + { + steps: 10, + } + ); + await page.mouse.up(); + + await expect(blockSelections).toHaveCount(1); + + // FIXME(DND) + // await assertRichTexts(page, ['456', '123']); +}); + +test.fixme( + 'should clear the currently selected block when clicked again', + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '123'); + await pressEnter(page); + await type(page, '456'); + await assertRichTexts(page, ['123', '456']); + + const editorHost = getEditorHostLocator(page); + const editors = editorHost.locator('rich-text'); + const editorRect0 = await editors.nth(0).boundingBox(); + const editorRect1 = await editors.nth(1).boundingBox(); + + if (!editorRect0 || !editorRect1) { + throw new Error(); + } + + await page.mouse.move( + editorRect1.x + 5, + editorRect1.y + editorRect1.height / 2 + ); + + await page.mouse.move( + editorRect1.x - 10, + editorRect1.y + editorRect1.height / 2 + ); + await page.mouse.down(); + await page.mouse.up(); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(1); + + let selectedBlockRect = await blockSelections.nth(0).boundingBox(); + + if (!selectedBlockRect) { + throw new Error(); + } + + expect(editorRect1).toEqual(selectedBlockRect); + + await page.mouse.move( + editorRect0.x - 10, + editorRect0.y + editorRect0.height / 2 + ); + await page.mouse.down(); + await page.mouse.up(); + + await expect(blockSelections).toHaveCount(1); + + selectedBlockRect = await blockSelections.nth(0).boundingBox(); + + if (!selectedBlockRect) { + throw new Error(); + } + + expect(editorRect0).toEqual(selectedBlockRect); + } +); + +test.fixme( + 'should support moving blocks from multiple notes', + async ({ page }) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + doc.addBlock('affine:surface', {}, rootId); + + ['123', '456', '789', '987', '654', '321'].forEach(text => { + const noteId = doc.addBlock('affine:note', {}, rootId); + doc.addBlock( + 'affine:paragraph', + { + text: new doc.Text(text), + }, + noteId + ); + }); + + doc.resetHistory(); + }); + + await dragBetweenIndices( + page, + [1, 0], + [2, 3], + { x: -60, y: 0 }, + { x: 80, y: 0 }, + { + steps: 50, + } + ); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(2); + + const editorHost = getEditorHostLocator(page); + const editors = editorHost.locator('rich-text'); + const editorRect1 = await editors.nth(1).boundingBox(); + const editorRect3 = await editors.nth(3).boundingBox(); + if (!editorRect1 || !editorRect3) { + throw new Error(); + } + + await dragBetweenCoords( + page, + { + x: editorRect1.x - 10, + y: editorRect1.y + editorRect1.height / 2, + }, + { + x: editorRect3.x + 10, + y: editorRect3.y + editorRect3.height / 2 + 10, + }, + { + steps: 50, + } + ); + + await assertRichTexts(page, ['123', '987', '456', '789', '654', '321']); + await expect(blockSelections).toHaveCount(2); + + await dragBetweenIndices( + page, + [5, 0], + [4, 3], + { x: -60, y: 0 }, + { x: 80, y: 0 }, + { + steps: 50, + } + ); + + const editorRect0 = await editors.nth(0).boundingBox(); + const editorRect5 = await editors.nth(5).boundingBox(); + if (!editorRect0 || !editorRect5) { + throw new Error(); + } + + await dragBetweenCoords( + page, + { + x: editorRect5.x - 10, + y: editorRect5.y + editorRect5.height / 2, + }, + { + x: editorRect0.x + 10, + y: editorRect0.y + editorRect0.height / 2 - 5, + }, + { + steps: 50, + } + ); + + await assertRichTexts(page, ['654', '321', '123', '987', '456', '789']); + await expect(blockSelections).toHaveCount(2); + } +); + +test('drag handle should show on right block when scroll viewport', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initParagraphsByCount(page, 30); + + await page.mouse.wheel(0, 200); + + const editorHost = getEditorHostLocator(page); + const editors = editorHost.locator('rich-text'); + const blockRect28 = await editors.nth(28).boundingBox(); + if (!blockRect28) { + throw new Error(); + } + + await page.mouse.move(blockRect28.x + 10, blockRect28.y + 10); + const dragHandle = page.locator('.affine-drag-handle-container'); + await expect(dragHandle).toBeVisible(); + + await page.mouse.move( + blockRect28.x - 10, + blockRect28.y + blockRect28.height / 2 + ); + await page.mouse.down(); + await page.mouse.up(); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(1); + + const selectedBlockRect = await blockSelections.nth(0).boundingBox(); + + if (!selectedBlockRect) { + throw new Error(); + } + + expect(blockRect28).toEqual(selectedBlockRect); +}); diff --git a/blocksuite/tests-legacy/edgeless/align.spec.ts b/blocksuite/tests-legacy/edgeless/align.spec.ts new file mode 100644 index 0000000000000..45b9a9936d80b --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/align.spec.ts @@ -0,0 +1,435 @@ +import { expect } from '@playwright/test'; + +import { + addBasicBrushElement, + createConnectorElement, + createFrameElement, + createNote, + createShapeElement, + setEdgelessTool, + Shape, + toViewCoord, + triggerComponentToolbarAction, +} from '../utils/actions/edgeless.js'; +import { + clickView, + edgelessCommonSetup as commonSetup, + selectAllByKeyboard, + type, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertEdgelessSelectedModelRect, + getSelectedRect, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('auto arrange align', () => { + test('arrange shapes', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + await createShapeElement(page, [100, -100], [300, 100], Shape.Ellipse); + await createShapeElement(page, [200, 300], [300, 400], Shape.Square); + await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle); + await createShapeElement( + page, + [0, 200], + [100, 300], + Shape['Rounded rectangle'] + ); + + await page.mouse.click(0, 0); + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, -100, 500, 500]); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 560, 320]); + }); + + test('arrange rotated shapes', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Ellipse); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + + const point = await toViewCoord(page, [100, 100]); + await page.mouse.click(point[0] + 50, point[1] + 50); + await page.mouse.move(point[0] - 5, point[1] - 5); + await page.mouse.down(); + await page.mouse.move(point[0] - 5, point[1] + 45); + await page.mouse.up(); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 220, 220]); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 261, 141]); + }); + + test('arrange connected shapes', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 100], [200, 200], Shape.Ellipse); + await createConnectorElement(page, [50, 100], [150, 100]); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 200, 200]); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, -21, 220, 141.4]); + }); + + test('arrange connector', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createConnectorElement(page, [200, 200], [300, 200]); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 300, 200]); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 220, 100]); + }); + + test('arrange edgeless text', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + + const point = await toViewCoord(page, [200, -100]); + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'a'); + await page.mouse.click(0, 0); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, -125, 225, 225]); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 170, 100]); + }); + + test('arrange note', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [200, 200], 'Hello World'); + await page.mouse.click(0, 0); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 668, 252]); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 618, 100]); + }); + + test('arrange group', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [200, 300], [300, 400], Shape.Square); + await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 500, 400]); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 420, 300]); + }); + + test('arrange frame', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [200, 300], [300, 400], Shape.Square); + await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle); + await selectAllByKeyboard(page); + await createFrameElement(page, [150, 50], [550, 450]); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + + await page.mouse.click(0, 0); + await page.mouse.move(75, 395); + await page.mouse.down(); + await page.mouse.move(900, 900); + await page.mouse.up(); + await assertEdgelessSelectedModelRect(page, [0, 0, 550, 450]); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 520, 400]); + }); + + // TODO mindmap size different on CI + test('arrange mindmap', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + await page.keyboard.press('m'); + await clickView(page, [500, 200]); + + await selectAllByKeyboard(page); + const box1 = await getSelectedRect(page); + expect(box1.width).toBeGreaterThan(700); + expect(box1.height).toBeGreaterThan(300); + + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + const box2 = await getSelectedRect(page); + expect(box2.width).toBeLessThan(550); + expect(box2.height).toBeLessThan(210); + }); + + test('arrange shape, note, connector, brush and edgeless text', async ({ + page, + }) => { + await commonSetup(page); + // shape + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [150, 150], [300, 300], Shape.Ellipse); + //note + await createNote(page, [200, 100], 'Hello World'); + // connector + await createConnectorElement(page, [200, -200], [400, -100]); + // brush + const start = { x: 400, y: 400 }; + const end = { x: 480, y: 480 }; + await addBasicBrushElement(page, start, end); + // edgeless text + const point = await toViewCoord(page, [-100, -100]); + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'edgeless text'); + + await page.mouse.click(0, 0); + await selectAllByKeyboard(page); + + await assertEdgelessSelectedModelRect(page, [-125, -200, 793, 500]); + // arrange + await triggerComponentToolbarAction(page, 'autoArrange'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [-125, -125, 668, 270]); + }); +}); + +test.describe('auto resize align', () => { + test('resize and arrange shapes', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + await createShapeElement(page, [100, -100], [300, 100], Shape.Ellipse); + await createShapeElement(page, [200, 300], [300, 400], Shape.Square); + await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle); + await createShapeElement( + page, + [0, 200], + [100, 300], + Shape['Rounded rectangle'] + ); + + await page.mouse.click(0, 0); + await selectAllByKeyboard(page); + + await assertEdgelessSelectedModelRect(page, [0, -100, 500, 500]); + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 860, 420]); + }); + + test('resize and arrange rotated shapes', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Ellipse); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + + const point = await toViewCoord(page, [100, 100]); + await page.mouse.click(point[0] + 50, point[1] + 50); + await page.mouse.move(point[0] - 5, point[1] - 5); + await page.mouse.down(); + await page.mouse.move(point[0] - 5, point[1] + 45); + await page.mouse.up(); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 220, 220]); + + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 420, 200]); + }); + + test('resize and arrange connected shapes', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 100], [200, 200], Shape.Ellipse); + await createConnectorElement(page, [50, 100], [150, 100]); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 200, 200]); + + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, -16, 420, 232]); + }); + + test('resize and arrange connector', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createConnectorElement(page, [200, 200], [300, 200]); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 300, 200]); + + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 320, 200]); + }); + + test('resize and arrange edgeless text', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + + const point = await toViewCoord(page, [200, -100]); + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'a'); + await page.mouse.click(0, 0); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, -125, 225, 225]); + + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 604.6, 200]); + }); + + test('resize and arrange note', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [200, 200], 'Hello World'); + await page.mouse.click(0, 0); + + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 668, 252]); + + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 1302.5, 200]); + }); + + test('resize and arrange group', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [200, 300], [300, 400], Shape.Square); + await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 500, 400]); + + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 420, 200]); + }); + + test('resize and arrange frame', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [200, 300], [300, 400], Shape.Square); + await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle); + await selectAllByKeyboard(page); + await createFrameElement(page, [150, 50], [550, 450]); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + + await page.mouse.click(0, 0); + await page.mouse.move(75, 395); + await page.mouse.down(); + await page.mouse.move(900, 900); + await page.mouse.up(); + await assertEdgelessSelectedModelRect(page, [0, 0, 550, 450]); + + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 420, 200]); + }); + + // TODO mindmap size different on CI + test('resize and arrange mindmap', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + await page.keyboard.press('m'); + await clickView(page, [500, 200]); + + await selectAllByKeyboard(page); + const box1 = await getSelectedRect(page); + expect(box1.width).toBeGreaterThan(700); + expect(box1.height).toBeGreaterThan(300); + + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + const box2 = await getSelectedRect(page); + expect(box2.width).toBeLessThan(650); + expect(box2.height).toBeLessThan(210); + }); + + test('resize and arrange shape, note, connector, brush and text', async ({ + page, + }) => { + await commonSetup(page); + // shape + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [150, 150], [300, 300], Shape.Ellipse); + //note + await createNote(page, [200, 100], 'Hello World'); + // connector + await createConnectorElement(page, [200, -200], [400, -100]); + // brush + const start = { x: 400, y: 400 }; + const end = { x: 480, y: 480 }; + await addBasicBrushElement(page, start, end); + // edgeless text + const point = await toViewCoord(page, [-100, -100]); + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'edgeless text'); + + await page.mouse.click(0, 0); + await selectAllByKeyboard(page); + + await assertEdgelessSelectedModelRect(page, [-125, -200, 793, 500]); + // arrange + await triggerComponentToolbarAction(page, 'autoResize'); + await waitNextFrame(page, 200); + await assertEdgelessSelectedModelRect(page, [0, 0, 1421.5, 420]); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/auto-complete.spec.ts b/blocksuite/tests-legacy/edgeless/auto-complete.spec.ts new file mode 100644 index 0000000000000..253cc60478a3f --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/auto-complete.spec.ts @@ -0,0 +1,244 @@ +import { DEFAULT_NOTE_BACKGROUND_COLOR } from '@blocksuite/affine-model'; +import { expect, type Page } from '@playwright/test'; + +import { clickView, moveView } from '../utils/actions/click.js'; +import { dragBetweenCoords } from '../utils/actions/drag.js'; +import { + addNote, + changeEdgelessNoteBackground, + changeShapeFillColor, + changeShapeStrokeColor, + createShapeElement, + deleteAll, + dragBetweenViewCoords, + edgelessCommonSetup, + getEdgelessSelectedRectModel, + Shape, + switchEditorMode, + toViewCoord, + triggerComponentToolbarAction, +} from '../utils/actions/edgeless.js'; +import { + enterPlaygroundRoom, + initEmptyEdgelessState, + waitForInlineEditorStateUpdated, + waitNextFrame, +} from '../utils/actions/misc.js'; +import { + assertConnectorStrokeColor, + assertEdgelessCanvasText, + assertEdgelessNoteBackground, + assertExists, + assertRichTexts, + assertSelectedBound, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +function getAutoCompletePanelButton(page: Page, type: string) { + return page + .locator('.auto-complete-panel-container') + .locator('edgeless-tool-icon-button') + .filter({ hasText: `${type}` }); +} + +test.describe('auto-complete', () => { + test.describe('click on auto-complete button', () => { + test('click on right auto-complete button', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await assertSelectedBound(page, [0, 0, 100, 100]); + await clickView(page, [120, 50]); + await assertSelectedBound(page, [200, 0, 100, 100]); + }); + test('click on bottom auto-complete button', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await assertSelectedBound(page, [0, 0, 100, 100]); + await clickView(page, [50, 120]); + await assertSelectedBound(page, [0, 200, 100, 100]); + }); + test('click on left auto-complete button', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await assertSelectedBound(page, [0, 0, 100, 100]); + await clickView(page, [-20, 50]); + await assertSelectedBound(page, [-200, 0, 100, 100]); + }); + test('click on top auto-complete button', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await assertSelectedBound(page, [0, 0, 100, 100]); + await clickView(page, [50, -20]); + await assertSelectedBound(page, [0, -200, 100, 100]); + }); + + test('click on note auto-complete button', async ({ page }) => { + await edgelessCommonSetup(page); + await addNote(page, 'note', 100, 100); + await page.mouse.click(600, 50); + await page.mouse.click(300, 50); + await page.mouse.click(150, 120); + const rect = await getEdgelessSelectedRectModel(page); + await moveView(page, [rect[0] + rect[2] + 30, rect[1] + rect[3] / 2]); + await clickView(page, [rect[0] + rect[2] + 30, rect[1] + rect[3] / 2]); + const newRect = await getEdgelessSelectedRectModel(page); + expect(rect[0]).not.toEqual(newRect[0]); + expect(rect[1]).toEqual(newRect[1]); + expect(rect[2]).toEqual(newRect[2]); + expect(rect[3]).toEqual(newRect[3]); + }); + }); + + test.describe('drag on auto-complete button', () => { + test('drag on right auto-complete button to add shape', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await assertSelectedBound(page, [0, 0, 100, 100]); + await dragBetweenViewCoords(page, [120, 50], [200, 0]); + + const ellipseButton = getAutoCompletePanelButton(page, 'ellipse'); + await expect(ellipseButton).toBeVisible(); + await ellipseButton.click(); + + await assertSelectedBound(page, [200, -50, 100, 100]); + }); + + test('drag on right auto-complete button to add canvas text', async ({ + page, + }) => { + await enterPlaygroundRoom(page, { + flags: { + enable_edgeless_text: false, + }, + }); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await deleteAll(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await assertSelectedBound(page, [0, 0, 100, 100]); + await dragBetweenViewCoords(page, [120, 50], [200, 0]); + + const canvasTextButton = getAutoCompletePanelButton(page, 'text'); + await expect(canvasTextButton).toBeVisible(); + await canvasTextButton.click(); + + await waitForInlineEditorStateUpdated(page); + await waitNextFrame(page); + await page.keyboard.type('hello'); + await assertEdgelessCanvasText(page, 'hello'); + }); + + test('drag on right auto-complete button to add note', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await assertSelectedBound(page, [0, 0, 100, 100]); + await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); + const lineColor = '--affine-palette-line-red'; + await changeShapeStrokeColor(page, lineColor); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + const color = '--affine-palette-shape-green'; + await changeShapeFillColor(page, color); + await dragBetweenViewCoords(page, [120, 50], [200, 0]); + + const noteButton = getAutoCompletePanelButton(page, 'note'); + await expect(noteButton).toBeVisible(); + await noteButton.click(); + await waitNextFrame(page); + + const edgelessNote = page.locator('affine-edgeless-note'); + + expect(await edgelessNote.count()).toBe(1); + const [x, y] = await toViewCoord(page, [240, 20]); + await page.mouse.click(x, y); + await page.keyboard.type('hello'); + await waitNextFrame(page); + await assertRichTexts(page, ['hello']); + + const noteId = await page.evaluate(() => { + const note = document.body.querySelector('affine-edgeless-note'); + return note?.getAttribute('data-block-id'); + }); + assertExists(noteId); + await assertEdgelessNoteBackground( + page, + noteId, + DEFAULT_NOTE_BACKGROUND_COLOR + ); + + const rect = await edgelessNote.boundingBox(); + assertExists(rect); + + // blur note block + await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height * 3); + await waitNextFrame(page); + + // select connector + await dragBetweenViewCoords(page, [140, 50], [160, 0]); + await waitNextFrame(page); + await assertConnectorStrokeColor(page, lineColor); + + // select note block + await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height / 2); + await waitNextFrame(page); + + await triggerComponentToolbarAction(page, 'changeNoteColor'); + const noteColor = '--affine-note-background-red'; + await changeEdgelessNoteBackground(page, noteColor); + + // move to arrow icon + await page.mouse.move( + rect.x + rect.width + 20, + rect.y + rect.height / 2, + { steps: 5 } + ); + await waitNextFrame(page); + + // drag arrow + await dragBetweenCoords( + page, + { + x: rect.x + rect.width + 20, + y: rect.y + rect.height / 2, + }, + { + x: rect.x + rect.width + 20 + 50, + y: rect.y + rect.height / 2 + 50, + } + ); + + // `Add a same object` button has the same type. + const noteButton2 = getAutoCompletePanelButton(page, 'note').nth(0); + await expect(noteButton2).toBeVisible(); + await noteButton2.click(); + await waitNextFrame(page); + + const noteId2 = await page.evaluate(() => { + const note = document.body.querySelectorAll('affine-edgeless-note')[1]; + return note?.getAttribute('data-block-id'); + }); + assertExists(noteId2); + await assertEdgelessNoteBackground(page, noteId, noteColor); + + expect(await edgelessNote.count()).toBe(2); + }); + + test('drag on right auto-complete button to add frame', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await assertSelectedBound(page, [0, 0, 100, 100]); + await dragBetweenViewCoords(page, [120, 50], [200, 0]); + + expect(await page.locator('.affine-frame-container').count()).toBe(0); + + const frameButton = getAutoCompletePanelButton(page, 'frame'); + await expect(frameButton).toBeVisible(); + await frameButton.click(); + + expect(await page.locator('.affine-frame-container').count()).toBe(1); + }); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/auto-connect.spec.ts b/blocksuite/tests-legacy/edgeless/auto-connect.spec.ts new file mode 100644 index 0000000000000..23187c196b858 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/auto-connect.spec.ts @@ -0,0 +1,180 @@ +import { NoteDisplayMode } from '@blocksuite/affine-model'; +import { assertExists } from '@blocksuite/global/utils'; +import { expect, type Page } from '@playwright/test'; + +import { + addNote, + changeNoteDisplayModeWithId, + dragBetweenViewCoords, + edgelessCommonSetup, + getNoteBoundBoxInEdgeless, + getSelectedBound, + selectNoteInEdgeless, + zoomResetByKeyboard, +} from '../utils/actions/edgeless.js'; +import { assertSelectedBound } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('auto-connect', () => { + async function init(page: Page) { + await edgelessCommonSetup(page); + } + test('navigator', async ({ page }) => { + await init(page); + const id1 = await addNote(page, 'page1', 200, 300); + const id2 = await addNote(page, 'page2', 300, 500); + const id3 = await addNote(page, 'page3', 400, 700); + + await page.mouse.click(200, 50); + // Notes added in edgeless mode only visible in edgeless mode + // To use index label navigator, we need to change display mode to PageAndEdgeless + await changeNoteDisplayModeWithId( + page, + id1, + NoteDisplayMode.DocAndEdgeless + ); + await changeNoteDisplayModeWithId( + page, + id2, + NoteDisplayMode.DocAndEdgeless + ); + await changeNoteDisplayModeWithId( + page, + id3, + NoteDisplayMode.DocAndEdgeless + ); + + await selectNoteInEdgeless(page, id1); + const bound = await getSelectedBound(page, 0); + await page.locator('.page-visible-index-label').nth(0).click(); + await assertSelectedBound(page, bound); + + await page.locator('.edgeless-auto-connect-next-button').click(); + bound[0] += 100; + bound[1] += 200; + await assertSelectedBound(page, bound); + + await page.locator('.edgeless-auto-connect-next-button').click(); + bound[0] += 100; + bound[1] += 200; + await assertSelectedBound(page, bound); + }); + + test('should display index label when select note', async ({ page }) => { + await init(page); + const id1 = await addNote(page, 'page1', 200, 300); + const id2 = await addNote(page, 'page2', 300, 500); + + await page.mouse.click(200, 50); + + await changeNoteDisplayModeWithId( + page, + id1, + NoteDisplayMode.DocAndEdgeless + ); + + await selectNoteInEdgeless(page, id2); + const edgelessOnlyIndexLabel = page.locator('.edgeless-only-index-label'); + await expect(edgelessOnlyIndexLabel).toBeVisible(); + await expect(edgelessOnlyIndexLabel).toHaveCount(1); + + await selectNoteInEdgeless(page, id1); + const pageVisibleIndexLabel = page.locator('.page-visible-index-label'); + await expect(pageVisibleIndexLabel).toBeVisible(); + await expect(pageVisibleIndexLabel).toHaveCount(1); + }); + + test('should hide index label when dragging note', async ({ page }) => { + await init(page); + const id1 = await addNote(page, 'page1', 200, 300); + + await page.mouse.click(200, 50); + + await changeNoteDisplayModeWithId( + page, + id1, + NoteDisplayMode.DocAndEdgeless + ); + + const pageVisibleIndexLabel = page.locator('.page-visible-index-label'); + await expect(pageVisibleIndexLabel).toBeVisible(); + await expect(pageVisibleIndexLabel).toHaveCount(1); + + const bound = await getNoteBoundBoxInEdgeless(page, id1); + await page.mouse.move( + bound.x + bound.width / 2, + bound.y + bound.height / 2 + ); + await page.mouse.down(); + await page.mouse.move( + bound.x + bound.width * 2, + bound.y + bound.height * 2 + ); + + await expect(pageVisibleIndexLabel).not.toBeVisible(); + + await page.mouse.up(); + await expect(pageVisibleIndexLabel).toBeVisible(); + }); + + test('should update index label position after dragging', async ({ + page, + }) => { + await init(page); + await zoomResetByKeyboard(page); + + const id1 = await addNote(page, 'page1', 200, 300); + const id2 = await addNote(page, 'page2', 300, 500); + + await page.mouse.click(200, 50); + + await changeNoteDisplayModeWithId( + page, + id1, + NoteDisplayMode.DocAndEdgeless + ); + + await selectNoteInEdgeless(page, id2); + const edgelessOnlyIndexLabel = page.locator('.edgeless-only-index-label'); + await expect(edgelessOnlyIndexLabel).toBeVisible(); + + // check initial index label position + const noteBound = await getNoteBoundBoxInEdgeless(page, id2); + const edgelessOnlyIndexLabelBound = + await edgelessOnlyIndexLabel.boundingBox(); + assertExists(edgelessOnlyIndexLabelBound); + const border = 1; + const offset = 16; + expect(edgelessOnlyIndexLabelBound.x).toBeCloseTo( + noteBound.x + + noteBound.width / 2 - + edgelessOnlyIndexLabelBound.width / 2 + + border + ); + expect(edgelessOnlyIndexLabelBound.y).toBeCloseTo( + noteBound.y + noteBound.height + offset + ); + + // move note + await dragBetweenViewCoords( + page, + [noteBound.x + noteBound.width / 2, noteBound.y + noteBound.height / 2], + [noteBound.x + noteBound.width, noteBound.y + noteBound.height] + ); + + // check new index label position + const newNoteBound = await getNoteBoundBoxInEdgeless(page, id2); + const newEdgelessOnlyIndexLabelBound = + await edgelessOnlyIndexLabel.boundingBox(); + assertExists(newEdgelessOnlyIndexLabelBound); + expect(newEdgelessOnlyIndexLabelBound.x).toBeCloseTo( + newNoteBound.x + + newNoteBound.width / 2 - + newEdgelessOnlyIndexLabelBound.width / 2 + + border + ); + expect(newEdgelessOnlyIndexLabelBound.y).toBeCloseTo( + newNoteBound.y + newNoteBound.height + offset + ); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/basic.spec.ts b/blocksuite/tests-legacy/edgeless/basic.spec.ts new file mode 100644 index 0000000000000..392a6cb059a9b --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/basic.spec.ts @@ -0,0 +1,358 @@ +import { + DEFAULT_NOTE_HEIGHT, + DEFAULT_NOTE_WIDTH, +} from '@blocksuite/affine-model'; +import { assertExists } from '@blocksuite/global/utils'; +import { expect } from '@playwright/test'; + +import { + createShapeElement, + decreaseZoomLevel, + deleteAll, + edgelessCommonSetup, + increaseZoomLevel, + locatorEdgelessComponentToolButton, + multiTouchDown, + multiTouchMove, + multiTouchUp, + optionMouseDrag, + Shape, + shiftClickView, + switchEditorMode, + toggleEditorReadonly, + ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH, + zoomByMouseWheel, + zoomResetByKeyboard, +} from '../utils/actions/edgeless.js'; +import { + addBasicBrushElement, + addBasicRectShapeElement, + captureHistory, + clickView, + enterPlaygroundRoom, + focusRichText, + initEmptyEdgelessState, + redoByClick, + type, + undoByClick, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertEdgelessNonSelectedRect, + assertEdgelessSelectedModelRect, + assertEdgelessSelectedRect, + assertNoteXYWH, + assertRichTextInlineRange, + assertRichTexts, + assertSelectedBound, + assertZoomLevel, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +const CENTER_X = 450; +const CENTER_Y = 450; + +test('switch to edgeless mode', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + await assertRichTextInlineRange(page, 0, 5, 0); + + await switchEditorMode(page); + const locator = page.locator('affine-edgeless-root gfx-viewport'); + await expect(locator).toHaveCount(1); + await assertRichTexts(page, ['hello']); + await waitNextFrame(page); + + // FIXME: got very flaky result on cursor keeping + // await assertNativeSelectionRangeCount(page, 1); +}); + +test('can zoom viewport', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); + + await page.mouse.click(CENTER_X, CENTER_Y); + const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]; + await assertEdgelessSelectedModelRect(page, original); + await assertZoomLevel(page, 100); + + await decreaseZoomLevel(page); + await assertZoomLevel(page, 75); + await decreaseZoomLevel(page); + await assertZoomLevel(page, 50); + + const zoomed = [0, 0, original[2] * 0.5, original[3] * 0.5]; + await assertEdgelessSelectedModelRect(page, zoomed); + + await increaseZoomLevel(page); + await assertZoomLevel(page, 75); + await increaseZoomLevel(page); + await assertZoomLevel(page, 100); + await assertEdgelessSelectedModelRect(page, original); +}); + +test('zoom by mouse', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertZoomLevel(page, 100); + + await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); + + await page.mouse.click(CENTER_X, CENTER_Y); + const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]; + await assertEdgelessSelectedModelRect(page, original); + + await zoomByMouseWheel(page, 0, 125); + await assertZoomLevel(page, 90); + + const zoomed = [0, 0, original[2] * 0.9, original[3] * 0.9]; + await assertEdgelessSelectedModelRect(page, zoomed); +}); + +test('zoom by pinch', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertZoomLevel(page, 100); + + await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); + + await page.mouse.click(CENTER_X, CENTER_Y); + const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]; + await assertEdgelessSelectedModelRect(page, original); + + const from = [ + { x: CENTER_X - 100, y: CENTER_Y }, + { x: CENTER_X + 100, y: CENTER_Y }, + ]; + const to = [ + { x: CENTER_X - 50, y: CENTER_Y - 35 }, + { x: CENTER_X + 50, y: CENTER_Y + 35 }, + ]; + await multiTouchDown(page, from); + await multiTouchMove(page, from, to); + await multiTouchUp(page, to); + + await assertZoomLevel(page, 50); + const zoomed = [0, 0, 0.5 * DEFAULT_NOTE_WIDTH, 46]; + await assertEdgelessSelectedModelRect(page, zoomed); +}); + +test('zoom by pinch when edgeless is readonly', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertZoomLevel(page, 100); + + await toggleEditorReadonly(page); + + const from = [ + { x: CENTER_X - 100, y: CENTER_Y }, + { x: CENTER_X + 100, y: CENTER_Y }, + ]; + const to = [ + { x: CENTER_X - 50, y: CENTER_Y - 35 }, + { x: CENTER_X + 50, y: CENTER_Y + 35 }, + ]; + await multiTouchDown(page, from); + await multiTouchMove(page, from, to); + await multiTouchUp(page, to); + + await toggleEditorReadonly(page); + await assertZoomLevel(page, 50); +}); + +test('move by pan', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertZoomLevel(page, 100); + + await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); + + await page.mouse.click(CENTER_X, CENTER_Y); + const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]; + await assertEdgelessSelectedModelRect(page, original); + + const from = [ + { x: CENTER_X - 100, y: CENTER_Y }, + { x: CENTER_X + 100, y: CENTER_Y }, + ]; + const to = [ + { x: CENTER_X - 50, y: CENTER_Y + 50 }, + { x: CENTER_X + 150, y: CENTER_Y + 50 }, + ]; + + await multiTouchDown(page, from); + await multiTouchMove(page, from, to); + await multiTouchUp(page, to); + + const moved = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]; + await assertEdgelessSelectedModelRect(page, moved); +}); + +test('option/alt mouse drag duplicate a new element', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await deleteAll(page); + + const start = [0, 0]; + const end = [100, 100]; + await createShapeElement(page, start, end, Shape.Square); + await optionMouseDrag(page, [50, 50], [150, 50]); + await assertSelectedBound(page, [100, 0, 100, 100]); + + await captureHistory(page); + await undoByClick(page); + await assertSelectedBound(page, [0, 0, 100, 100]); + + await redoByClick(page); + await assertSelectedBound(page, [100, 0, 100, 100]); +}); + +test('should cancel select when the selected point is outside the current selected element', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + const firstStart = { x: 100, y: 100 }; + const firstEnd = { x: 200, y: 200 }; + await addBasicRectShapeElement(page, firstStart, firstEnd); + + const secondStart = { x: 300, y: 300 }; + const secondEnd = { x: 400, y: 400 }; + await addBasicRectShapeElement(page, secondStart, secondEnd); + + // select the first rect + await page.mouse.click(110, 150); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + // click outside the selected rect + await page.mouse.click(200, 200); + await assertEdgelessNonSelectedRect(page); +}); + +test('the tooltip of more button should be hidden when the action menu is shown', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicBrushElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y + 5); + await assertEdgelessSelectedRect(page, [98, 98, 104, 104]); + + const moreButton = locatorEdgelessComponentToolButton(page, 'more'); + await expect(moreButton).toBeVisible(); + + const moreButtonBox = await moreButton.boundingBox(); + const tooltip = page.locator('.affine-tooltip'); + + assertExists(moreButtonBox); + + // need to wait for previous tooltip to be hidden + await page.waitForTimeout(100); + await page.mouse.move(moreButtonBox.x + 10, moreButtonBox.y + 10); + await expect(tooltip).toBeVisible(); + + await page.mouse.click(moreButtonBox.x + 10, moreButtonBox.y + 10); + await expect(tooltip).toBeHidden(); + + await page.mouse.click(moreButtonBox.x + 10, moreButtonBox.y + 10); + await expect(tooltip).toBeVisible(); +}); + +test('shift click multi select and de-select', async ({ page }) => { + await edgelessCommonSetup(page); + const start = [0, 0]; + const end = [100, 100]; + await createShapeElement(page, start, end, Shape.Square); + start[0] = 100; + end[0] = 200; + await createShapeElement(page, start, end, Shape.Square); + + await clickView(page, [50, 0]); + await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]); + + await shiftClickView(page, [150, 50]); + await assertEdgelessSelectedModelRect(page, [0, 0, 200, 100]); + + // we will try to write text on a shape element when we dbclick it + + await waitNextFrame(page, 500); + await shiftClickView(page, [150, 50]); + await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]); +}); + +test('Before and after switching to Edgeless, the previous zoom ratio and position when Edgeless was opened should be remembered', async ({ + page, +}) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2479', + }); + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertZoomLevel(page, 100); + await increaseZoomLevel(page); + await assertZoomLevel(page, 125); + await switchEditorMode(page); + await switchEditorMode(page); + await assertZoomLevel(page, 125); +}); + +test('should close zoom bar when click blank area', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const screenWidth = page.viewportSize()?.width; + assertExists(screenWidth); + if (screenWidth > ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH) { + await page.setViewportSize({ + width: 1000, + height: 1000, + }); + } + + await zoomResetByKeyboard(page); + await assertZoomLevel(page, 100); + await increaseZoomLevel(page); + await assertZoomLevel(page, 125); + + const verticalZoomBar = '.edgeless-zoom-toolbar-container.vertical'; + const zoomBar = page.locator(verticalZoomBar); + await expect(zoomBar).toBeVisible(); + + // Click Blank Area + await page.mouse.click(10, 100); + await expect(zoomBar).toBeHidden(); +}); diff --git a/blocksuite/tests-legacy/edgeless/brush.spec.ts b/blocksuite/tests-legacy/edgeless/brush.spec.ts new file mode 100644 index 0000000000000..893dc2a1ee5c0 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/brush.spec.ts @@ -0,0 +1,193 @@ +import { expect } from '@playwright/test'; + +import { + assertEdgelessTool, + deleteAll, + pickColorAtPoints, + selectBrushColor, + selectBrushSize, + setEdgelessTool, + switchEditorMode, + updateExistedBrushElementSize, + zoomResetByKeyboard, +} from '../utils/actions/edgeless.js'; +import { + addBasicBrushElement, + click, + dragBetweenCoords, + enterPlaygroundRoom, + initEmptyEdgelessState, + resizeElementByHandle, +} from '../utils/actions/index.js'; +import { + assertEdgelessColorSameWithHexColor, + assertEdgelessSelectedRect, + assertSameColor, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('change editor mode when brush color palette opening', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await setEdgelessTool(page, 'brush'); + + const brushMenu = page.locator('edgeless-brush-menu'); + await expect(brushMenu).toBeVisible(); + + await switchEditorMode(page); + await expect(brushMenu).toBeHidden(); +}); + +test('add brush element', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicBrushElement(page, start, end, false); + + await assertEdgelessTool(page, 'brush'); +}); + +test('resize brush element', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicBrushElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y + 5); + await assertEdgelessSelectedRect(page, [98, 98, 104, 104]); + + await page.mouse.click(start.x + 5, start.y + 5); + const delta = { x: 20, y: 40 }; + await resizeElementByHandle(page, delta, 'top-left', 10); + + await page.mouse.click(start.x + 25, start.y + 45); + await assertEdgelessSelectedRect(page, [118, 138, 84, 64]); +}); + +test('add brush element with color', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await setEdgelessTool(page, 'brush'); + const color = '--affine-palette-line-blue'; + await selectBrushColor(page, color); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await dragBetweenCoords(page, start, end, { steps: 100 }); + + const [pickedColor] = await pickColorAtPoints(page, [[110, 110]]); + + await assertEdgelessColorSameWithHexColor(page, color, pickedColor); +}); + +test('keep same color when mouse mode switched back to brush', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await deleteAll(page); + + await setEdgelessTool(page, 'brush'); + const color = '--affine-palette-line-blue'; + await selectBrushColor(page, color); + const start = { x: 200, y: 200 }; + const end = { x: 300, y: 300 }; + await dragBetweenCoords(page, start, end, { steps: 100 }); + + await setEdgelessTool(page, 'default'); + await click(page, { x: 50, y: 50 }); + + await setEdgelessTool(page, 'brush'); + const origin = { x: 100, y: 100 }; + await dragBetweenCoords(page, origin, start, { steps: 100 }); + const [pickedColor] = await pickColorAtPoints(page, [[110, 110]]); + await assertEdgelessColorSameWithHexColor(page, color, pickedColor); +}); + +test('add brush element with different size', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await setEdgelessTool(page, 'brush'); + await selectBrushSize(page, 'ten'); + const color = '--affine-palette-line-blue'; + await selectBrushColor(page, color); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 100 }; + await dragBetweenCoords(page, start, end, { steps: 100 }); + + const [topEdge, bottomEdge, nearTopEdge, nearBottomEdge] = + await pickColorAtPoints(page, [ + // Select two points on the top and bottom border of the line, + // their color should be the same as the specified color + [110, 95], + [110, 104], + // Select two points close to the upper and lower boundaries of the line, + // their color should be different from the specified color + [110, 94], + [110, 105], + ]); + + await assertEdgelessColorSameWithHexColor(page, color, topEdge); + await assertEdgelessColorSameWithHexColor(page, color, bottomEdge); + assertSameColor(nearTopEdge, '#4f90ff'); + assertSameColor(nearBottomEdge, '#4f90ff'); +}); + +test('change brush element size by component-toolbar', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicBrushElement(page, start, end); + + // wait for menu hide animation + await page.waitForTimeout(500); + + // change to line width 12 + await page.mouse.click(110, 110); + await updateExistedBrushElementSize(page, 6); + await assertEdgelessSelectedRect(page, [94, 94, 112, 112]); + + // change to line width 10 + await page.mouse.click(110, 110); + await updateExistedBrushElementSize(page, 5); + await assertEdgelessSelectedRect(page, [95, 95, 110, 110]); + + // change to line width 8 + await page.mouse.click(110, 110); + await updateExistedBrushElementSize(page, 4); + await assertEdgelessSelectedRect(page, [96, 96, 108, 108]); + + // change to line width 6 + await page.mouse.click(110, 110); + await updateExistedBrushElementSize(page, 3); + await assertEdgelessSelectedRect(page, [97, 97, 106, 106]); + + // change to line width 4 + await page.mouse.click(110, 110); + await updateExistedBrushElementSize(page, 2); + await assertEdgelessSelectedRect(page, [98, 98, 104, 104]); + + // change to line width 2 + await page.mouse.click(110, 110); + await updateExistedBrushElementSize(page, 1); + await assertEdgelessSelectedRect(page, [99, 99, 102, 102]); +}); diff --git a/blocksuite/tests-legacy/edgeless/clipboard.spec.ts b/blocksuite/tests-legacy/edgeless/clipboard.spec.ts new file mode 100644 index 0000000000000..52a6a7f3db0d3 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/clipboard.spec.ts @@ -0,0 +1,235 @@ +import { expect } from '@playwright/test'; + +import { + createNote, + createShapeElement, + decreaseZoomLevel, + deleteAll, + getAllSortedIds, + Shape, + switchEditorMode, + toViewCoord, + triggerComponentToolbarAction, +} from '../utils/actions/edgeless.js'; +import { + copyByKeyboard, + cutByKeyboard, + edgelessCommonSetup as commonSetup, + enterPlaygroundRoom, + expectConsoleMessage, + focusTitle, + getCurrentEditorDocId, + initEmptyEdgelessState, + mockParseDocUrlService, + pasteByKeyboard, + pasteContent, + selectAllByKeyboard, + type, + waitNextFrame, +} from '../utils/actions/index.js'; +import { assertRichImage } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('mime', () => { + test('should paste svg in text/plain mime', async ({ page }) => { + expectConsoleMessage(page, 'Error: Image sourceId is missing!', 'warning'); + await commonSetup(page); + const content = { + 'text/plain': ` + + + + `, + }; + + await pasteContent(page, content); + + // wait for paste + await page.waitForTimeout(200); + await assertRichImage(page, 1); + }); + + test('should not paste bad svg', async ({ page }) => { + expectConsoleMessage(page, 'BlockSuiteError: val does not exist', 'error'); + expectConsoleMessage(page, 'Error: Image sourceId is missing!', 'warning'); + + await commonSetup(page); + const contents = [ + { + 'text/plain': ` + + + `, + }, + + { + 'text/plain': ` + + + + `, + }, + ]; + for (const content of contents) { + await pasteContent(page, content); + } + + await assertRichImage(page, 0); + }); +}); + +test.describe('frame clipboard', () => { + test('copy and paste frame with shape elements inside', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [100, -100]); + await page.mouse.click(10, 50); + + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addFrame'); + const originIds = await getAllSortedIds(page); + expect(originIds.length).toBe(3); + + await copyByKeyboard(page); + const move = await toViewCoord(page, [250, 250]); + await page.mouse.move(move[0], move[1]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, true); + await waitNextFrame(page, 500); + const sortedIds = await getAllSortedIds(page); + expect(sortedIds.length).toBe(6); + }); + + test('copy and paste frame with group elements inside', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [100, -100]); + await page.mouse.click(10, 50); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await triggerComponentToolbarAction(page, 'createFrameOnMoreOption'); + const originIds = await getAllSortedIds(page); + expect(originIds.length).toBe(5); + + await selectAllByKeyboard(page); + await copyByKeyboard(page); + const move = await toViewCoord(page, [250, 250]); + await page.mouse.move(move[0], move[1]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, true); + await waitNextFrame(page, 500); + const sortedIds = await getAllSortedIds(page); + expect(sortedIds.length).toBe(10); + }); + + test('copy and paste frame with frame inside', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [100, -100]); + await page.mouse.click(10, 50); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addFrame'); + + await decreaseZoomLevel(page); + await createShapeElement(page, [700, 0], [800, 100], Shape.Square); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addFrame'); + + const originIds = await getAllSortedIds(page); + expect(originIds.length).toBe(5); + + await copyByKeyboard(page); + const move = await toViewCoord(page, [250, 250]); + await page.mouse.move(move[0], move[1]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, true); + await waitNextFrame(page, 500); + const sortedIds = await getAllSortedIds(page); + expect(sortedIds.length).toBe(10); + }); + + test('cut frame with shape elements inside', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [100, -100]); + await page.mouse.click(10, 50); + + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addFrame'); + const originIds = await getAllSortedIds(page); + expect(originIds.length).toBe(3); + + await cutByKeyboard(page); + const move = await toViewCoord(page, [250, 250]); + await page.mouse.move(move[0], move[1]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, true); + await waitNextFrame(page, 500); + const sortedIds = await getAllSortedIds(page); + expect(sortedIds.length).toBe(3); + }); +}); + +test.describe('pasting URLs', () => { + test('pasting github pr url', async ({ page }) => { + await commonSetup(page); + await waitNextFrame(page); + await pasteContent(page, { + 'text/plain': 'https://github.com/toeverything/blocksuite/pull/7217', + }); + + await expect( + page.locator('affine-embed-edgeless-github-block') + ).toBeVisible(); + }); + + test('pasting internal link', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await waitNextFrame(page); + await focusTitle(page); + const docId = await getCurrentEditorDocId(page); + + await type(page, 'doc title'); + + await switchEditorMode(page); + await deleteAll(page); + + await mockParseDocUrlService(page, { + 'http://workspace/doc-id': docId, + }); + + await pasteContent(page, { + 'text/plain': 'http://workspace/doc-id', + }); + + await expect( + page.locator('affine-embed-edgeless-linked-doc-block') + ).toBeVisible(); + + await expect( + page.locator('.affine-embed-linked-doc-content-title') + ).toHaveText('doc title'); + }); + + test('pasting external link', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await waitNextFrame(page); + await focusTitle(page); + + await type(page, 'doc title'); + + await switchEditorMode(page); + await deleteAll(page); + await waitNextFrame(page); + + await pasteContent(page, { + 'text/plain': 'https://affine.pro', + }); + + await expect(page.locator('bookmark-card')).toBeVisible(); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/color-picker.spec.ts b/blocksuite/tests-legacy/edgeless/color-picker.spec.ts new file mode 100644 index 0000000000000..8e137be135c2f --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/color-picker.spec.ts @@ -0,0 +1,368 @@ +import { parseStringToRgba } from '@blocks/root-block/edgeless/components/color-picker/utils.js'; +import { expect, type Locator, type Page } from '@playwright/test'; +import { dragBetweenCoords } from 'utils/actions/drag.js'; +import { + addBasicShapeElement, + Shape, + switchEditorMode, + triggerComponentToolbarAction, +} from 'utils/actions/edgeless.js'; +import { + enterPlaygroundRoom, + initEmptyEdgelessState, +} from 'utils/actions/misc.js'; + +import { test } from '../utils/playwright.js'; + +async function setupWithColorPickerFunction(page: Page) { + await enterPlaygroundRoom(page, { flags: { enable_color_picker: true } }); + await initEmptyEdgelessState(page); + await switchEditorMode(page); +} + +function getColorPickerButtonWithClass(page: Page, classes: string) { + return page.locator(`edgeless-color-picker-button.${classes}`); +} + +function getCurrentColorUnitButton(locator: Locator) { + return locator.locator('edgeless-color-button').locator('.color-unit'); +} + +function getCurrentColor(locator: Locator) { + return locator.evaluate(ele => + getComputedStyle(ele).getPropertyValue('background-color') + ); +} + +function getCustomButton(locator: Locator) { + return locator.locator('edgeless-color-custom-button'); +} + +function getColorPickerPanel(locator: Locator) { + return locator.locator('edgeless-color-picker'); +} + +function getPaletteControl(locator: Locator) { + return locator.locator('.color-palette'); +} + +function getHueControl(locator: Locator) { + return locator.locator('.color-slider-wrapper.hue .color-slider'); +} + +function getAlphaControl(locator: Locator) { + return locator.locator('.color-slider-wrapper.alpha .color-slider'); +} + +function getHexInput(locator: Locator) { + return locator.locator('label.color input'); +} + +function getAlphaInput(locator: Locator) { + return locator.locator('label.alpha input'); +} + +// Basic functions +test.describe('basic functions', () => { + test('custom color button should be displayed', async ({ page }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + await expect(fillColorButton).toBeVisible(); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const customButton = getCustomButton(fillColorButton); + await expect(customButton).toBeVisible(); + }); + + test('should open color-picker panel when clicking on custom color button', async ({ + page, + }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const customButton = getCustomButton(fillColorButton); + + await customButton.click(); + + const colorPickerPanel = getColorPickerPanel(fillColorButton); + + await expect(colorPickerPanel).toBeVisible(); + }); + + test('should close color-picker panel when clicking on outside', async ({ + page, + }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const currentColorUnit = getCurrentColorUnitButton(fillColorButton); + + const value = await getCurrentColor(currentColorUnit); + await expect(currentColorUnit).toHaveCSS('background-color', value); + + const customButton = getCustomButton(fillColorButton); + + await customButton.click(); + + const colorPickerPanel = getColorPickerPanel(fillColorButton); + + await expect(colorPickerPanel).toBeVisible(); + + await colorPickerPanel.click({ position: { x: 0, y: 0 } }); + await expect(colorPickerPanel).toBeVisible(); + + await page.mouse.click(0, 0); + + await expect(colorPickerPanel).toBeHidden(); + }); + + test('should return to the palette panel when re-clicking the color button', async ({ + page, + }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const customButton = getCustomButton(fillColorButton); + const colorPickerPanel = getColorPickerPanel(fillColorButton); + + await customButton.click(); + + await expect(colorPickerPanel).toBeVisible(); + + await page.mouse.click(0, 0); + + await expect(colorPickerPanel).toBeHidden(); + + await dragBetweenCoords(page, { x: 125, y: 75 }, { x: 175, y: 225 }); + + await fillColorButton.click(); + + await expect(customButton).toBeVisible(); + await expect(colorPickerPanel).toBeHidden(); + }); + + test('should pick a color when clicking on the palette canvas', async ({ + page, + }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const customButton = getCustomButton(fillColorButton); + const colorPickerPanel = getColorPickerPanel(fillColorButton); + + await customButton.click(); + + const paletteControl = getPaletteControl(colorPickerPanel); + const hexInput = getHexInput(colorPickerPanel); + + const value = await hexInput.inputValue(); + + await paletteControl.click(); + + const newValue = await hexInput.inputValue(); + + expect(value).not.toEqual(newValue); + }); + + test('should pick a color when clicking on the hue control', async ({ + page, + }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const customButton = getCustomButton(fillColorButton); + const colorPickerPanel = getColorPickerPanel(fillColorButton); + + await customButton.click(); + + const hueControl = getHueControl(colorPickerPanel); + const hexInput = getHexInput(colorPickerPanel); + + const value = await hexInput.inputValue(); + + await hueControl.click(); + + const newValue = await hexInput.inputValue(); + + expect(value).not.toEqual(newValue); + }); + + test('should update color when changing the hex input', async ({ page }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const customButton = getCustomButton(fillColorButton); + const colorPickerPanel = getColorPickerPanel(fillColorButton); + + await customButton.click(); + + const hexInput = getHexInput(colorPickerPanel); + + await hexInput.fill('fff'); + await page.keyboard.press('Enter'); + await expect(hexInput).toHaveValue('ffffff'); + + await hexInput.fill('000000'); + await page.keyboard.press('Enter'); + await expect(hexInput).toHaveValue('000000'); + + await hexInput.fill('fff$'); + await page.keyboard.press('Enter'); + await expect(hexInput).toHaveValue('ffffff'); + + await hexInput.fill('#f0f'); + await page.keyboard.press('Enter'); + await expect(hexInput).toHaveValue('ff00ff'); + }); + + test('should adjust alpha when clicking on the alpha control', async ({ + page, + }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const customButton = getCustomButton(fillColorButton); + const colorPickerPanel = getColorPickerPanel(fillColorButton); + + await customButton.click(); + + const alphaControl = getAlphaControl(colorPickerPanel); + const alphaInput = getAlphaInput(colorPickerPanel); + + const value = await alphaInput.inputValue(); + + await alphaControl.click(); + + const newValue = await alphaInput.inputValue(); + + expect(value).not.toEqual(newValue); + }); + + test('should adjust alpha when changing the alpha input', async ({ + page, + }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const customButton = getCustomButton(fillColorButton); + const colorPickerPanel = getColorPickerPanel(fillColorButton); + + await customButton.click(); + + const alphaInput = getAlphaInput(colorPickerPanel); + + await alphaInput.fill('101'); + await expect(alphaInput).toHaveValue('100'); + + await alphaInput.fill('-1'); + await expect(alphaInput).toHaveValue('1'); + + await alphaInput.pressSequentially('--1'); + await expect(alphaInput).toHaveValue('1'); + + await alphaInput.pressSequentially('++1'); + await expect(alphaInput).toHaveValue('1'); + + await alphaInput.pressSequentially('-+1'); + await expect(alphaInput).toHaveValue('1'); + + await alphaInput.pressSequentially('+-1'); + await expect(alphaInput).toHaveValue('1'); + + await alphaInput.fill('23'); + await expect(alphaInput).toHaveValue('23'); + }); + + test('the computed style should be parsed correctly', async ({ page }) => { + await setupWithColorPickerFunction(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicShapeElement(page, start0, end0, Shape.Square); + + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + + const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color'); + const currentColorUnit = getCurrentColorUnitButton(fillColorButton); + + const value = await getCurrentColor(currentColorUnit); + let rgba = parseStringToRgba(value); + + expect(rgba.a).toEqual(1); + + rgba = parseStringToRgba('rgb(25.5,0,0)'); + expect(rgba.r).toBeCloseTo(0.1); + + rgba = parseStringToRgba('rgba(233,233,233, .5)'); + expect(rgba.a).toEqual(0.5); + + rgba = parseStringToRgba('transparent'); + expect(rgba).toEqual({ r: 1, g: 1, b: 1, a: 0 }); + + rgba = parseStringToRgba('--blocksuite-transparent'); + expect(rgba).toEqual({ r: 1, g: 1, b: 1, a: 0 }); + + rgba = parseStringToRgba('--affine-palette-transparent'); + expect(rgba).toEqual({ r: 1, g: 1, b: 1, a: 0 }); + + rgba = parseStringToRgba('#ff0'); + expect(rgba).toEqual({ r: 1, g: 1, b: 0, a: 1 }); + + rgba = parseStringToRgba('#ff09'); + expect(rgba).toEqual({ r: 1, g: 1, b: 0, a: 0.6 }); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/connector/clipboard.spec.ts b/blocksuite/tests-legacy/edgeless/connector/clipboard.spec.ts new file mode 100644 index 0000000000000..eb30a6b13397b --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/connector/clipboard.spec.ts @@ -0,0 +1,142 @@ +import { expect } from '@playwright/test'; + +import { + copyByKeyboard, + createConnectorElement, + createNote, + createShapeElement, + edgelessCommonSetup as commonSetup, + getAllSortedIds, + getTypeById, + pasteByKeyboard, + selectAllByKeyboard, + Shape, + toViewCoord, + triggerComponentToolbarAction, + waitNextFrame, +} from '../../utils/actions/index.js'; +import { assertConnectorPath } from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test.describe('connector clipboard', () => { + test('copy and paste connector whose both sides connect nothing', async ({ + page, + }) => { + await commonSetup(page); + await createConnectorElement(page, [0, 0], [200, 100]); + await waitNextFrame(page); + await copyByKeyboard(page); + const move = await toViewCoord(page, [100, -50]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, false); + await waitNextFrame(page); + await assertConnectorPath( + page, + [ + [0, -100], + [100, -100], + [100, 0], + [200, 0], + ], + 1 + ); + }); + + test('copy and paste connector whose both sides connect elements', async ({ + page, + }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [60, 50], [240, 50]); + + await selectAllByKeyboard(page); + await copyByKeyboard(page); + const move = await toViewCoord(page, [150, -50]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, false); + await waitNextFrame(page); + await assertConnectorPath( + page, + [ + [100, -50], + [200, -50], + ], + 1 + ); + }); + + test('copy and paste connector whose both sides connect elements, but only paste connector', async ({ + page, + }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [70, 50], [230, 50]); + + await copyByKeyboard(page); + const move = await toViewCoord(page, [150, -50]); + await page.mouse.move(move[0], move[1]); + await pasteByKeyboard(page, false); + await waitNextFrame(page); + await assertConnectorPath( + page, + [ + [100, -50], + [200, -50], + ], + 1 + ); + }); + + test('copy and paste connector whose one side connects elements', async ({ + page, + }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createConnectorElement(page, [55, 50], [200, 50]); + + await selectAllByKeyboard(page); + await copyByKeyboard(page); + const move = await toViewCoord(page, [100, -50]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, false); + await assertConnectorPath( + page, + [ + [100, -50], + [200, -50], + ], + 1 + ); + }); + + test('original relative index should keep same when copy and paste group with note and shape', async ({ + page, + }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [100, 50]); + await page.mouse.click(10, 50); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + await copyByKeyboard(page); + const move = await toViewCoord(page, [250, 250]); + await page.mouse.move(move[0], move[1]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, true); + await waitNextFrame(page, 500); + + const sortedIds = await getAllSortedIds(page); + expect(sortedIds.length).toBe(6); + expect(await getTypeById(page, sortedIds[0])).toBe( + await getTypeById(page, sortedIds[3]) + ); + expect(await getTypeById(page, sortedIds[1])).toBe( + await getTypeById(page, sortedIds[4]) + ); + expect(await getTypeById(page, sortedIds[2])).toBe( + await getTypeById(page, sortedIds[5]) + ); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/connector/connector.spec.ts b/blocksuite/tests-legacy/edgeless/connector/connector.spec.ts new file mode 100644 index 0000000000000..049daee8fbcb1 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/connector/connector.spec.ts @@ -0,0 +1,320 @@ +import { expect } from '@playwright/test'; + +import { + addBasicConnectorElement, + changeConnectorStrokeColor, + changeConnectorStrokeStyle, + changeConnectorStrokeWidth, + createConnectorElement, + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup as commonSetup, + pickColorAtPoints, + rotateElementByHandle, + Shape, + toModelCoord, + toViewCoord, + triggerComponentToolbarAction, +} from '../../utils/actions/edgeless.js'; +import { pressBackspace, waitNextFrame } from '../../utils/actions/index.js'; +import { + assertConnectorPath, + assertEdgelessNonSelectedRect, + assertEdgelessSelectedRect, + assertExists, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test('path #1, the upper line is parallel with the lower line of antoher, and anchor from top to bottom of another', async ({ + page, +}) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, -100], [300, 0], Shape.Square); + await createConnectorElement(page, [50, 0], [250, 0]); + + await waitNextFrame(page); + await assertConnectorPath(page, [ + [50, 0], + [50, -20], + [150, -20], + [150, 20], + [250, 20], + [250, 0], + ]); +}); + +test('path #2, the top-right point is overlapped with the bottom-left point of another, and anchor from top to bottom of another', async ({ + page, +}) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, -100], [200, 0], Shape.Square); + await createConnectorElement(page, [50, 0], [150, 0]); + + await assertConnectorPath(page, [ + [50, 0], + [50, -120], + [220, -120], + [220, 20], + [150, 20], + [150, 0], + ]); +}); + +test('path #3, the two shape are parallel in x axis, the anchor from the right to right', async ({ + page, +}) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [100, 50], [300, 50]); + await assertConnectorPath(page, [ + [100, 50], + [150, 50], + [150, 120], + [320, 120], + [320, 50], + [300, 50], + ]); +}); + +test('when element is removed, connector should be deleted too', async ({ + page, +}) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createConnectorElement(page, [100, 50], [200, 0]); + + //select + await dragBetweenViewCoords(page, [10, -10], [20, 20]); + await pressBackspace(page); + await dragBetweenViewCoords(page, [100, 50], [0, 50]); + await assertEdgelessNonSelectedRect(page); +}); + +test('connector connects triangle shape', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Triangle); + await createConnectorElement(page, [75, 50], [100, 50]); + + await assertConnectorPath(page, [ + [75, 50], + [100, 50], + ]); +}); + +test('connector connects diamond shape', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond); + await createConnectorElement(page, [100, 50], [200, 50]); + + await assertConnectorPath(page, [ + [100, 50], + [200, 50], + ]); +}); + +test('connector connects rotated Square shape', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createConnectorElement(page, [50, 0], [50, -100]); + await dragBetweenViewCoords(page, [-10, 50], [60, 60]); + await rotateElementByHandle(page, 30, 'top-left'); + await assertConnectorPath(page, [ + [75, 6.7], + [75, -46.65], + [50, -46.65], + [50, -100], + ]); + await rotateElementByHandle(page, 30, 'top-left'); + await assertConnectorPath(page, [ + [93.3, 25], + [138.3, 25], + [138.3, -38.3], + [50, -38.3], + [50, -100], + ]); +}); + +test('change connector line width', async ({ page }) => { + await commonSetup(page); + + const start = { x: 100, y: 200 }; + const end = { x: 300, y: 300 }; + await addBasicConnectorElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y); + await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor'); + await changeConnectorStrokeColor(page, '--affine-palette-line-teal'); + + await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); + await changeConnectorStrokeWidth(page, 5); + + await waitNextFrame(page); + + await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); + + const pickedColor = await pickColorAtPoints(page, [ + [start.x + 5, start.y], + [start.x + 10, start.y], + ]); + expect(pickedColor[0]).toBe(pickedColor[1]); +}); + +test('change connector stroke style', async ({ page }) => { + await commonSetup(page); + + const start = { x: 100, y: 200 }; + const end = { x: 300, y: 300 }; + await addBasicConnectorElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y); + await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor'); + await changeConnectorStrokeColor(page, '--affine-palette-line-teal'); + + await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); + await changeConnectorStrokeStyle(page, 'dash'); + await waitNextFrame(page); + + await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles'); + + const pickedColor = await pickColorAtPoints(page, [[start.x + 20, start.y]]); + expect(pickedColor[0]).toBe('#000000'); +}); + +test.describe('quick connect', () => { + test('should create a connector when clicking on button', async ({ + page, + }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + const [x, y] = await toViewCoord(page, [50, 50]); + await page.mouse.click(x, y); + + const quickConnectBtn = page.getByRole('button', { + name: 'Draw connector', + }); + + await expect(quickConnectBtn).toBeVisible(); + await quickConnectBtn.click(); + await expect(quickConnectBtn).toBeHidden(); + + await assertConnectorPath(page, [ + [100, 50], + [x, y], + ]); + }); + + test('should be uncreated if the target is not found after clicking', async ({ + page, + }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + const [x, y] = await toViewCoord(page, [50, 50]); + await page.mouse.click(x, y); + + const quickConnectBtn = page.getByRole('button', { + name: 'Draw connector', + }); + + const bounds = await quickConnectBtn.boundingBox(); + assertExists(bounds); + + await quickConnectBtn.click(); + + await page.mouse.click(bounds.x, bounds.y); + await assertEdgelessSelectedRect(page, [x - 50, y - 50, 100, 100]); + }); + + test('should be uncreated if the target is not found after pressing ESC', async ({ + page, + }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + + // select shape + const [x, y] = await toViewCoord(page, [50, 50]); + await page.mouse.click(x, y); + + // click button + await triggerComponentToolbarAction(page, 'quickConnect'); + + await page.keyboard.press('Escape'); + + await assertEdgelessNonSelectedRect(page); + }); + + test('should be connected if the target is found', async ({ page }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + + // select shape + const [x, y] = await toViewCoord(page, [50, 50]); + await page.mouse.click(x, y); + + // click button + await triggerComponentToolbarAction(page, 'quickConnect'); + + // click target + const [tx, ty] = await toViewCoord(page, [200, 50]); + await page.mouse.click(tx, ty); + + await assertConnectorPath(page, [ + [100, 50], + [200, 50], + ]); + }); + + test('should follow the mouse to automatically select the starting point', async ({ + page, + }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + const shapeBounds = await toViewCoord(page, [0, 0]); + + // select shape + const [x, y] = await toViewCoord(page, [50, 50]); + await page.mouse.click(x, y); + + // click button + const quickConnectBtn = page.getByRole('button', { + name: 'Draw connector', + }); + const bounds = await quickConnectBtn.boundingBox(); + assertExists(bounds); + await quickConnectBtn.click(); + + // at right + let point: [number, number] = [bounds.x, bounds.y]; + let endpoint = await toModelCoord(page, point); + await assertConnectorPath(page, [[100, 50], endpoint]); + + // at top + point = [shapeBounds[0] + 50, shapeBounds[1] - 50]; + endpoint = await toModelCoord(page, point); + await page.mouse.move(...point); + await waitNextFrame(page); + await assertConnectorPath(page, [[50, 0], endpoint]); + + // at left + point = [shapeBounds[0] - 50, shapeBounds[1] + 50]; + endpoint = await toModelCoord(page, point); + await page.mouse.move(...point); + await assertConnectorPath(page, [[0, 50], endpoint]); + + // at bottom + point = [shapeBounds[0] + 50, shapeBounds[1] + 100 + 50]; + endpoint = await toModelCoord(page, point); + await page.mouse.move(...point); + await assertConnectorPath(page, [[50, 100], endpoint]); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/connector/elbow.spec.ts b/blocksuite/tests-legacy/edgeless/connector/elbow.spec.ts new file mode 100644 index 0000000000000..0cde357a324a8 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/connector/elbow.spec.ts @@ -0,0 +1,206 @@ +import { + assertEdgelessConnectorToolMode, + ConnectorMode, + createConnectorElement, + createShapeElement, + deleteAllConnectors, + dragBetweenViewCoords, + edgelessCommonSetup as commonSetup, + redoByClick, + setEdgelessTool, + Shape, + undoByClick, +} from '../../utils/actions/index.js'; +import { assertConnectorPath } from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test('elbow connector without node and width greater than height', async ({ + page, +}) => { + await commonSetup(page); + await setEdgelessTool(page, 'connector'); + await assertEdgelessConnectorToolMode(page, ConnectorMode.Curve); + await dragBetweenViewCoords(page, [0, 0], [200, 100]); + await assertConnectorPath(page, [ + [0, 0], + [100, 0], + [100, 100], + [200, 100], + ]); +}); + +test('elbow connector without node and width less than height', async ({ + page, +}) => { + await commonSetup(page); + await createConnectorElement(page, [0, 0], [100, 200]); + await assertConnectorPath(page, [ + [0, 0], + [0, 100], + [100, 100], + [100, 200], + ]); +}); + +test('elbow connector one side attached element another side free', async ({ + page, +}) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createConnectorElement(page, [51, 50], [200, 0]); + + await assertConnectorPath(page, [ + [100, 50], + [150, 50], + [150, 0], + [200, 0], + ]); + + await deleteAllConnectors(page); + await createConnectorElement(page, [50, 50], [125, 0]); + + await assertConnectorPath(page, [ + [50, 0], + [125, 50], + [125, 0], + ]); +}); + +test('elbow connector both side attatched element', async ({ page }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [50, 50], [249, 50]); + + await assertConnectorPath(page, [ + [50, 0], + [200, 50], + ]); + + // Could drag directly + // because the default shape type change to general style with filled color + await dragBetweenViewCoords(page, [250, 50], [250, 0]); + await assertConnectorPath(page, [ + [50, 0], + [150, 50], + [150, 0], + [200, 0], + ]); + + await dragBetweenViewCoords(page, [250, 0], [150, -50]); + await assertConnectorPath(page, [ + [50, 0], + [50, -50], + [100, -50], + ]); + + await dragBetweenViewCoords(page, [150, -50], [150, -150]); + await assertConnectorPath(page, [ + [50, 0], + [50, -50], + [150, -50], + [150, -100], + ]); + + await undoByClick(page); + await assertConnectorPath(page, [ + [50, 0], + [50, -50], + [100, -50], + ]); + await undoByClick(page); + await assertConnectorPath(page, [ + [50, 0], + [150, 50], + [150, 0], + [200, 0], + ]); + await undoByClick(page); + await assertConnectorPath(page, [ + [50, 0], + [200, 50], + ]); + await redoByClick(page); + await assertConnectorPath(page, [ + [50, 0], + [150, 50], + [150, 0], + [200, 0], + ]); + await redoByClick(page); + await assertConnectorPath(page, [ + [50, 0], + [50, -50], + [100, -50], + ]); + await redoByClick(page); + await assertConnectorPath(page, [ + [50, 0], + [50, -50], + [150, -50], + [150, -100], + ]); +}); + +test('elbow connector both side attached element with one attach element and other is fixed', async ({ + page, +}) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [50, 0], [250, 50]); + + await assertConnectorPath(page, [ + [50, 0], + [50, -20], + [150, -20], + [150, 50], + [200, 50], + ]); + + // select + await dragBetweenViewCoords(page, [255, -10], [255, 55]); + await dragBetweenViewCoords(page, [250, 50], [250, 0]); + + await assertConnectorPath(page, [ + [50, 0], + [50, -20], + [150, -20], + [150, 0], + [200, 0], + ]); + + await dragBetweenViewCoords(page, [250, 0], [250, -20]); + await assertConnectorPath(page, [ + [50, 0], + [50, -20], + [200, -20], + ]); + + await dragBetweenViewCoords(page, [250, -20], [150, -150]); + await assertConnectorPath(page, [ + [50, 0], + [50, -50], + [150, -50], + [150, -100], + ]); +}); + +test('elbow connector both side attached element with all fixed', async ({ + page, +}) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [50, 0], [300, 50]); + await assertConnectorPath(page, [ + [50, 0], + [50, -20], + [320, -20], + [320, 50], + [300, 50], + ]); +}); diff --git a/blocksuite/tests-legacy/edgeless/connector/group.spec.ts b/blocksuite/tests-legacy/edgeless/connector/group.spec.ts new file mode 100644 index 0000000000000..28ab2b931bc84 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/connector/group.spec.ts @@ -0,0 +1,107 @@ +import type { Page } from '@playwright/test'; + +import { + clickView, + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup as commonSetup, + moveView, + selectAllByKeyboard, + Shape, + triggerComponentToolbarAction, + waitNextFrame, +} from '../../utils/actions/index.js'; +import { assertConnectorPath } from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test.describe('groups connections', () => { + async function groupsSetup(page: Page) { + await commonSetup(page); + + // group 1 + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + // group 2 + await createShapeElement(page, [500, 0], [600, 100], Shape.Square); + await createShapeElement(page, [600, 100], [700, 200], Shape.Square); + await dragBetweenViewCoords(page, [550, -50], [650, 250]); + await triggerComponentToolbarAction(page, 'addGroup'); + + await waitNextFrame(page); + } + + test('should connect to other groups', async ({ page }) => { + await groupsSetup(page); + + // click button + await triggerComponentToolbarAction(page, 'quickConnect'); + + // move to group 1 + await moveView(page, [200, 50]); + await waitNextFrame(page); + + await assertConnectorPath(page, [ + [500, 100], + [200, 50], + ]); + }); + + test('should connect to elements within other groups', async ({ page }) => { + await groupsSetup(page); + + // click button + await triggerComponentToolbarAction(page, 'quickConnect'); + + // move to group 1 + await moveView(page, [200, 100]); + await waitNextFrame(page); + + await assertConnectorPath(page, [ + [500, 100], + [200, 100], + ]); + + // move to elements within group 1 + await moveView(page, [190, 150]); + await waitNextFrame(page); + + await assertConnectorPath(page, [ + [500, 100], + [200, 150], + ]); + }); + + test('elements within groups should connect to other groups', async ({ + page, + }) => { + await groupsSetup(page); + + // click elements within group 1 + await clickView(page, [40, 40]); + await clickView(page, [60, 60]); + + // click button + await triggerComponentToolbarAction(page, 'quickConnect'); + + // move to elements within group 2 + await moveView(page, [610, 50]); + await waitNextFrame(page); + + await assertConnectorPath(page, [ + [100, 50], + [600, 50], + ]); + + // move to group 2 + await moveView(page, [600, 100]); + await waitNextFrame(page); + + await assertConnectorPath(page, [ + [100, 50], + [600, 100], + ]); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/connector/label.spec.ts b/blocksuite/tests-legacy/edgeless/connector/label.spec.ts new file mode 100644 index 0000000000000..dd6a33f31b800 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/connector/label.spec.ts @@ -0,0 +1,334 @@ +import { assertExists } from '@blocksuite/global/utils'; +import { expect, type Page } from '@playwright/test'; + +import { + addBasicConnectorElement, + createConnectorElement, + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup as commonSetup, + locatorComponentToolbar, + setEdgelessTool, + Shape, + SHORT_KEY, + toViewCoord, + triggerComponentToolbarAction, + type, + waitNextFrame, +} from '../../utils/actions/index.js'; +import { + assertConnectorPath, + assertEdgelessCanvasText, + assertPointAlmostEqual, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test.describe('connector label with straight shape', () => { + async function getEditorCenter(page: Page) { + const bounds = await page + .locator('edgeless-connector-label-editor rich-text') + .boundingBox(); + assertExists(bounds); + const cx = bounds.x + bounds.width / 2; + const cy = bounds.y + bounds.height / 2; + return [cx, cy]; + } + + function calcOffsetDistance(s: number[], e: number[], p: number[]) { + const p1 = Math.hypot(s[1] - p[1], s[0] - p[0]); + const f1 = Math.hypot(s[1] - e[1], s[0] - e[0]); + return p1 / f1; + } + + test('should insert in the middle of the path when clicking on the button', async ({ + page, + }) => { + await commonSetup(page); + const start = { x: 100, y: 200 }; + const end = { x: 300, y: 300 }; + await addBasicConnectorElement(page, start, end); + await page.mouse.click(105, 200); + + await triggerComponentToolbarAction(page, 'addText'); + await type(page, ' a '); + await assertEdgelessCanvasText(page, ' a '); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + await page.mouse.click(105, 200); + + const addTextBtn = locatorComponentToolbar(page).getByRole('button', { + name: 'Add text', + }); + await expect(addTextBtn).toBeHidden(); + + await page.mouse.dblclick(200, 250); + await assertEdgelessCanvasText(page, 'a'); + + await page.keyboard.press('Backspace'); + await assertEdgelessCanvasText(page, ''); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + await page.mouse.click(200, 250); + + await expect(addTextBtn).toBeVisible(); + }); + + test('should insert at the place when double clicking on the path', async ({ + page, + }) => { + await commonSetup(page); + await setEdgelessTool(page, 'connector'); + + await page.mouse.move(0, 0); + + const menu = page.locator('edgeless-connector-menu'); + await expect(menu).toBeVisible(); + + const straightBtn = menu.locator('edgeless-tool-icon-button', { + hasText: 'Straight', + }); + await expect(straightBtn).toBeVisible(); + await straightBtn.click(); + + const start = { x: 250, y: 250 }; + const end = { x: 500, y: 250 }; + await addBasicConnectorElement(page, start, end); + + await page.mouse.dblclick(300, 250); + await type(page, 'a'); + await assertEdgelessCanvasText(page, 'a'); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.dblclick(300, 250); + await waitNextFrame(page); + + await page.keyboard.press('ArrowRight'); + await type(page, 'b'); + await assertEdgelessCanvasText(page, 'ab'); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.dblclick(300, 250); + await waitNextFrame(page); + + await type(page, 'c'); + await assertEdgelessCanvasText(page, 'c'); + await waitNextFrame(page); + + const [cx, cy] = await getEditorCenter(page); + assertPointAlmostEqual([cx, cy], [300, 250]); + expect((cx - 250) / (500 - 250)).toBeCloseTo(50 / 250); + }); + + test('should move alone the path', async ({ page }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [100, 50], [200, 50]); + + await dragBetweenViewCoords(page, [140, 40], [160, 60]); + await triggerComponentToolbarAction(page, 'changeConnectorShape'); + const straightBtn = locatorComponentToolbar(page).getByRole('button', { + name: 'Straight', + }); + await straightBtn.click(); + + await assertConnectorPath(page, [ + [100, 50], + [200, 50], + ]); + + const [x, y] = await toViewCoord(page, [150, 50]); + await page.mouse.dblclick(x, y); + await type(page, 'label'); + await assertEdgelessCanvasText(page, 'label'); + await waitNextFrame(page); + + let [cx, cy] = await getEditorCenter(page); + assertPointAlmostEqual([cx, cy], [x, y]); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await dragBetweenViewCoords(page, [150, 50], [130, 30]); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.dblclick(x - 20, y); + await waitNextFrame(page); + + [cx, cy] = await getEditorCenter(page); + assertPointAlmostEqual([cx, cy], [x - 20, y]); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await dragBetweenViewCoords(page, [130, 50], [170, 70]); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.dblclick(x + 20, y); + await waitNextFrame(page); + + [cx, cy] = await getEditorCenter(page); + assertPointAlmostEqual([cx, cy], [x + 20, y]); + }); + + test('should only move within constraints', async ({ page }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [100, 50], [200, 50]); + + await assertConnectorPath(page, [ + [100, 50], + [200, 50], + ]); + + const [x, y] = await toViewCoord(page, [150, 50]); + await page.mouse.dblclick(x, y); + await type(page, 'label'); + await assertEdgelessCanvasText(page, 'label'); + await waitNextFrame(page); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await dragBetweenViewCoords(page, [150, 50], [300, 110]); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.dblclick(x + 55, y); + await waitNextFrame(page); + + let [cx, cy] = await getEditorCenter(page); + assertPointAlmostEqual([cx, cy], [x + 50, y]); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await dragBetweenViewCoords(page, [200, 50], [0, 50]); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.dblclick(x - 55, y); + await waitNextFrame(page); + + [cx, cy] = await getEditorCenter(page); + assertPointAlmostEqual([cx, cy], [x - 50, y]); + }); + + test('should automatically adjust position via offset distance', async ({ + page, + }) => { + await commonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createConnectorElement(page, [100, 50], [200, 50]); + + await dragBetweenViewCoords(page, [140, 40], [160, 60]); + await triggerComponentToolbarAction(page, 'changeConnectorShape'); + const straightBtn = locatorComponentToolbar(page).getByRole('button', { + name: 'Straight', + }); + await straightBtn.click(); + + const point = [170, 50]; + const offsetDistance = calcOffsetDistance([100, 50], [200, 50], point); + let [x, y] = await toViewCoord(page, point); + await page.mouse.dblclick(x, y); + await type(page, 'label'); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.dblclick(x, y); + await waitNextFrame(page); + + let [cx, cy] = await getEditorCenter(page); + assertPointAlmostEqual([cx, cy], [x, y]); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.click(50, 50); + await waitNextFrame(page); + await dragBetweenViewCoords(page, [50, 50], [-50, 50]); + await waitNextFrame(page); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + + await page.mouse.click(250, 50); + await waitNextFrame(page); + await dragBetweenViewCoords(page, [250, 50], [350, 50]); + await waitNextFrame(page); + + const start = [0, 50]; + const end = [300, 50]; + const mx = start[0] + offsetDistance * (end[0] - start[0]); + const my = start[1] + offsetDistance * (end[1] - start[1]); + [x, y] = await toViewCoord(page, [mx, my]); + + await page.mouse.dblclick(x, y); + await waitNextFrame(page); + + [cx, cy] = await getEditorCenter(page); + assertPointAlmostEqual([cx, cy], [x, y]); + }); + + test('should enter the label editing state when pressing `Enter`', async ({ + page, + }) => { + await commonSetup(page); + const start = { x: 100, y: 200 }; + const end = { x: 300, y: 300 }; + await addBasicConnectorElement(page, start, end); + await page.mouse.click(105, 200); + + await page.keyboard.press('Enter'); + await type(page, ' a '); + await assertEdgelessCanvasText(page, ' a '); + }); + + test('should exit the label editing state when pressing `Mod-Enter` or `Escape`', async ({ + page, + }) => { + await commonSetup(page); + const start = { x: 100, y: 200 }; + const end = { x: 300, y: 300 }; + await addBasicConnectorElement(page, start, end); + await page.mouse.click(105, 200); + + await page.keyboard.press('Enter'); + await waitNextFrame(page); + await type(page, ' a '); + await assertEdgelessCanvasText(page, ' a '); + + await page.keyboard.press(`${SHORT_KEY}+Enter`); + + await page.keyboard.press('Enter'); + await waitNextFrame(page); + await type(page, 'b'); + await assertEdgelessCanvasText(page, 'b'); + + await page.keyboard.press('Escape'); + + await page.keyboard.press('Enter'); + await waitNextFrame(page); + await type(page, 'c'); + await assertEdgelessCanvasText(page, 'c'); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts b/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts new file mode 100644 index 0000000000000..85f4aa164f019 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts @@ -0,0 +1,596 @@ +import type { EdgelessTextBlockComponent } from '@blocks/edgeless-text-block/edgeless-text-block.js'; +import { Bound } from '@blocksuite/global/utils'; +import { expect, type Page } from '@playwright/test'; + +import { + autoFit, + captureHistory, + cutByKeyboard, + dragBetweenIndices, + enterPlaygroundRoom, + getEdgelessSelectedRect, + getPageSnapshot, + initEmptyEdgelessState, + pasteByKeyboard, + pressArrowLeft, + pressArrowRight, + pressArrowUp, + pressBackspace, + pressEnter, + pressEscape, + selectAllByKeyboard, + setEdgelessTool, + switchEditorMode, + toViewCoord, + type, + undoByKeyboard, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertBlockChildrenIds, + assertBlockFlavour, + assertBlockTextContent, + assertRichTextInlineDeltas, + assertRichTextInlineRange, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; +import { getFormatBar } from '../utils/query.js'; + +async function assertEdgelessTextModelRect( + page: Page, + id: string, + bound: Bound +) { + const realXYWH = await page.evaluate(id => { + const block = window.host.view.getBlock(id) as EdgelessTextBlockComponent; + return block?.model.xywh; + }, id); + const realBound = Bound.deserialize(realXYWH); + expect(realBound.x).toBeCloseTo(bound.x, 0); + expect(realBound.y).toBeCloseTo(bound.y, 0); + expect(realBound.w).toBeCloseTo(bound.w, 0); + expect(realBound.h).toBeCloseTo(bound.h, 0); +} + +test.describe('edgeless text block', () => { + test.beforeEach(async ({ page }) => { + await enterPlaygroundRoom(page, { + flags: { + enable_edgeless_text: true, + }, + }); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + }); + + test('add text block in default mode', async ({ page }) => { + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + await waitNextFrame(page); + + // https://github.com/toeverything/blocksuite/pull/8574 + await pressBackspace(page); + + await type(page, 'aaa'); + await pressEnter(page); + await type(page, 'bbb'); + await pressEnter(page); + await type(page, 'ccc'); + + await assertBlockFlavour(page, 4, 'affine:edgeless-text'); + await assertBlockFlavour(page, 5, 'affine:paragraph'); + await assertBlockFlavour(page, 6, 'affine:paragraph'); + await assertBlockFlavour(page, 7, 'affine:paragraph'); + await assertBlockChildrenIds(page, '4', ['5', '6', '7']); + await assertBlockTextContent(page, 5, 'aaa'); + await assertBlockTextContent(page, 6, 'bbb'); + await assertBlockTextContent(page, 7, 'ccc'); + + await dragBetweenIndices(page, [1, 1], [3, 2]); + await captureHistory(page); + await pressBackspace(page); + await assertBlockChildrenIds(page, '4', ['5']); + await assertBlockTextContent(page, 5, 'ac'); + + await undoByKeyboard(page); + await assertBlockChildrenIds(page, '4', ['5', '6', '7']); + await assertBlockTextContent(page, 5, 'aaa'); + await assertBlockTextContent(page, 6, 'bbb'); + await assertBlockTextContent(page, 7, 'ccc'); + + const { boldBtn } = getFormatBar(page); + await boldBtn.click(); + await assertRichTextInlineDeltas( + page, + [ + { + insert: 'a', + }, + { + insert: 'aa', + attributes: { + bold: true, + }, + }, + ], + 1 + ); + await assertRichTextInlineDeltas( + page, + [ + { + insert: 'bbb', + attributes: { + bold: true, + }, + }, + ], + 2 + ); + await assertRichTextInlineDeltas( + page, + [ + { + insert: 'cc', + attributes: { + bold: true, + }, + }, + { + insert: 'c', + }, + ], + 3 + ); + + await pressArrowRight(page); + await assertRichTextInlineRange(page, 3, 2); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 2, 2); + }); + + test('edgeless text width auto-adjusting', async ({ page }) => { + await setEdgelessTool(page, 'default'); + const point = await toViewCoord(page, [0, 0]); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await waitNextFrame(page); + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 50, 26)); + + await type(page, 'aaaaaaaaaa'); + await waitNextFrame(page, 1000); + // just width changed + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 83, 26)); + + await type(page, '\nbbb'); + // width not changed, height changed + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 83, 50)); + await type(page, '\ncccccccccccccccc'); + + // width and height changed + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 131, 74)); + + // blur, max width set to true + await page.mouse.click(point[0] - 50, point[1], { + delay: 100, + }); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await type(page, 'dddddddddddddddddddd'); + // width not changed, height changed + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 131, 98)); + }); + + test('edgeless text width fixed when drag moving', async ({ page }) => { + // https://github.com/toeverything/blocksuite/pull/7486 + + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'aaaaaa bbbb '); + await pressEscape(page); + await waitNextFrame(page); + await page.mouse.click(130, 140); + await page.mouse.down(); + await page.mouse.move(800, 800, { + steps: 15, + }); + + const rect = await page.evaluate(() => { + const container = document.querySelector( + '.edgeless-text-block-container' + )!; + return container.getBoundingClientRect(); + }); + const model = await page.evaluate(() => { + const block = window.host.view.getBlock( + '4' + ) as EdgelessTextBlockComponent; + return block.model; + }); + const bound = Bound.deserialize(model.xywh); + expect(rect.width).toBeCloseTo(bound.w); + expect(rect.height).toBeCloseTo(bound.h); + }); + + test('When creating edgeless text, if the input is empty, it will be automatically deleted', async ({ + page, + }) => { + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + await waitNextFrame(page); + let block = page.locator('affine-edgeless-text[data-block-id="4"]'); + expect(await block.isVisible()).toBe(true); + await page.mouse.click(0, 0); + expect(await block.isVisible()).toBe(false); + + block = page.locator('affine-edgeless-text[data-block-id="6"]'); + expect(await block.isVisible()).not.toBe(true); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + expect(await block.isVisible()).toBe(true); + await type(page, '\na'); + expect(await block.isVisible()).toBe(true); + await page.mouse.click(0, 0); + expect(await block.isVisible()).not.toBe(false); + }); + + test('edgeless text should maintain selection when deleting across multiple lines', async ({ + page, + }) => { + // https://github.com/toeverything/blocksuite/pull/7443 + + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'aaaa\nbbbb'); + await assertBlockTextContent(page, 5, 'aaaa'); + await assertBlockTextContent(page, 6, 'bbbb'); + + await pressArrowLeft(page); + await page.keyboard.down('Shift'); + await pressArrowLeft(page, 3); + await pressArrowUp(page); + await pressArrowRight(page); + await page.keyboard.up('Shift'); + await pressBackspace(page); + await assertBlockTextContent(page, 5, 'ab'); + await type(page, 'sss\n'); + await assertBlockTextContent(page, 5, 'asss'); + await assertBlockTextContent(page, 7, 'b'); + }); + + test('edgeless text should not blur after pressing backspace', async ({ + page, + }) => { + // https://github.com/toeverything/blocksuite/pull/7555 + + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'a'); + await assertBlockTextContent(page, 5, 'a'); + await pressBackspace(page); + await type(page, 'b'); + await assertBlockTextContent(page, 5, 'b'); + }); + + // FIXME(@flrande): This test fails randomly on CI + test.fixme('edgeless text max width', async ({ page }) => { + await setEdgelessTool(page, 'default'); + const point = await toViewCoord(page, [0, 0]); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await waitNextFrame(page); + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 50, 56)); + + await type(page, 'aaaaaa'); + await waitNextFrame(page); + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 71, 56)); + await type(page, 'bbb'); + await waitNextFrame(page, 200); + // height not changed + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 98, 56)); + + // blur + await page.mouse.click(0, 0); + // select text element + await page.mouse.click(point[0] + 10, point[1] + 10); + + let selectedRect = await getEdgelessSelectedRect(page); + + // move cursor to the right edge and drag it to resize the width of text + + // from left to right + await page.mouse.move( + selectedRect.x + selectedRect.width, + selectedRect.y + selectedRect.height / 2 + ); + await page.mouse.down(); + await page.mouse.move( + selectedRect.x + selectedRect.width + 30, + selectedRect.y + selectedRect.height / 2, + { + steps: 10, + } + ); + await page.mouse.up(); + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 128, 56)); + selectedRect = await getEdgelessSelectedRect(page); + let textRect = await page + .locator('affine-edgeless-text[data-block-id="4"]') + .boundingBox(); + expect(selectedRect).not.toBeNull(); + expect(selectedRect.width).toBeCloseTo(textRect!.width); + expect(selectedRect.height).toBeCloseTo(textRect!.height); + expect(selectedRect.x).toBeCloseTo(textRect!.x); + expect(selectedRect.y).toBeCloseTo(textRect!.y); + + // from right to left + await page.mouse.move( + selectedRect.x + selectedRect.width, + selectedRect.y + selectedRect.height / 2 + ); + await page.mouse.down(); + await page.mouse.move( + selectedRect.x + selectedRect.width - 45, + selectedRect.y + selectedRect.height / 2, + { + steps: 10, + } + ); + await page.mouse.up(); + // height changed + await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 83, 80)); + selectedRect = await getEdgelessSelectedRect(page); + textRect = await page + .locator('affine-edgeless-text[data-block-id="4"]') + .boundingBox(); + expect(selectedRect).not.toBeNull(); + expect(selectedRect.width).toBeCloseTo(textRect!.width); + expect(selectedRect.height).toBeCloseTo(textRect!.height); + expect(selectedRect.x).toBeCloseTo(textRect!.x); + expect(selectedRect.y).toBeCloseTo(textRect!.y); + }); + + test('min width limit for embed block', async ({ page }, testInfo) => { + await setEdgelessTool(page, 'default'); + const point = await toViewCoord(page, [0, 0]); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await type(page, '@'); + await pressEnter(page); + await waitNextFrame(page, 200); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_add_linked_doc.json` + ); + + await page.locator('affine-reference').hover(); + await page.getByLabel('Switch view').click(); + await page.getByTestId('link-to-card').click(); + await autoFit(page); + + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_link_to_card.json` + ); + + // blur + await page.mouse.click(0, 0); + // select text element + await page.mouse.click(point[0] + 10, point[1] + 10); + await waitNextFrame(page, 200); + const selectedRect0 = await getEdgelessSelectedRect(page); + + // from right to left + await page.mouse.move( + selectedRect0.x + selectedRect0.width, + selectedRect0.y + selectedRect0.height / 2 + ); + await page.mouse.down(); + await page.mouse.move( + selectedRect0.x, + selectedRect0.y + selectedRect0.height / 2, + { + steps: 10, + } + ); + await page.mouse.up(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_link_to_card_min_width.json` + ); + + const selectedRect1 = await getEdgelessSelectedRect(page); + // from left to right + await page.mouse.move( + selectedRect1.x + selectedRect1.width, + selectedRect1.y + selectedRect1.height / 2 + ); + await page.mouse.down(); + await page.mouse.move( + selectedRect0.x + selectedRect0.width + 45, + selectedRect1.y + selectedRect1.height / 2, + { + steps: 10, + } + ); + await page.mouse.up(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_drag.json` + ); + }); + + test('cut edgeless text', async ({ page }) => { + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'aaaa\nbbbb\ncccc'); + + const edgelessText = page.locator('affine-edgeless-text'); + const paragraph = page.locator('affine-edgeless-text affine-paragraph'); + + expect(await edgelessText.count()).toBe(1); + expect(await paragraph.count()).toBe(3); + + await page.mouse.click(50, 50, { + delay: 100, + }); + await waitNextFrame(page); + await page.mouse.click(130, 140, { + delay: 100, + }); + await cutByKeyboard(page); + expect(await edgelessText.count()).toBe(0); + expect(await paragraph.count()).toBe(0); + + await pasteByKeyboard(page); + expect(await edgelessText.count()).toBe(1); + expect(await paragraph.count()).toBe(3); + }); + + test('latex in edgeless text', async ({ page }) => { + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + await waitNextFrame(page); + await type(page, '$$bbb$$ '); + await assertRichTextInlineDeltas( + page, + [ + { + insert: ' ', + attributes: { + latex: 'bbb', + }, + }, + ], + 1 + ); + + await page.locator('affine-latex-node').click(); + await waitNextFrame(page); + await type(page, 'ccc'); + await assertRichTextInlineDeltas( + page, + [ + { + insert: ' ', + attributes: { + latex: 'bbbccc', + }, + }, + ], + 1 + ); + + await page.locator('.latex-editor-hint').click(); + await type(page, 'sss'); + await assertRichTextInlineDeltas( + page, + [ + { + insert: ' ', + attributes: { + latex: 'bbbccc', + }, + }, + ], + 1 + ); + await page.locator('latex-editor-unit').click(); + await selectAllByKeyboard(page); + await type(page, 'sss'); + await assertRichTextInlineDeltas( + page, + [ + { + insert: ' ', + attributes: { + latex: 'sss', + }, + }, + ], + 1 + ); + }); +}); + +test('press backspace at the start of first line when edgeless text exist', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page, { + flags: { + enable_edgeless_text: true, + }, + }); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + doc.addBlock('affine:surface', {}, rootId); + doc.addBlock('affine:note', {}, rootId); + + // do not add paragraph block + + doc.resetHistory(); + }); + await switchEditorMode(page); + + await setEdgelessTool(page, 'default'); + const point = await toViewCoord(page, [0, 0]); + await page.mouse.dblclick(point[0], point[1], { + delay: 100, + }); + await waitNextFrame(page); + await type(page, 'aaa'); + + await waitNextFrame(page); + await switchEditorMode(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_note_empty.json` + ); + + await page.locator('.affine-page-root-block-container').click(); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_note_not_empty.json` + ); + + await type(page, 'bbb'); + await pressArrowLeft(page, 3); + await pressBackspace(page); + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); diff --git a/blocksuite/tests-legacy/edgeless/element-toolbar.spec.ts b/blocksuite/tests-legacy/edgeless/element-toolbar.spec.ts new file mode 100644 index 0000000000000..777e7cd6a6053 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/element-toolbar.spec.ts @@ -0,0 +1,88 @@ +import { expect } from '@playwright/test'; + +import { + addBasicRectShapeElement, + locatorComponentToolbar, + resizeElementByHandle, + selectNoteInEdgeless, + switchEditorMode, + zoomResetByKeyboard, +} from '../utils/actions/edgeless.js'; +import { + enterPlaygroundRoom, + initEmptyEdgelessState, +} from '../utils/actions/misc.js'; +import { test } from '../utils/playwright.js'; + +test('toolbar should appear when select note', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await selectNoteInEdgeless(page, noteId); + + const toolbar = locatorComponentToolbar(page); + await expect(toolbar).toBeVisible(); +}); + +test('tooltip should be hidden after clicking on button', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await selectNoteInEdgeless(page, noteId); + + const toolbar = locatorComponentToolbar(page); + const modeBtn = toolbar.getByRole('button', { name: 'Mode' }); + + await modeBtn.hover(); + await expect(page.locator('.blocksuite-portal')).toBeVisible(); + + await modeBtn.click(); + await expect(page.locator('.blocksuite-portal')).toBeHidden(); + await expect(page.locator('note-display-mode-panel')).toBeVisible(); + + await modeBtn.click(); + await expect(page.locator('.blocksuite-portal')).toBeVisible(); + await expect(page.locator('note-display-mode-panel')).toBeHidden(); + + await modeBtn.click(); + await expect(page.locator('.blocksuite-portal')).toBeHidden(); + await expect(page.locator('note-display-mode-panel')).toBeVisible(); + + const colorBtn = toolbar.getByRole('button', { + name: 'Background', + }); + + await colorBtn.hover(); + await expect(page.locator('.blocksuite-portal')).toBeVisible(); + + await colorBtn.click(); + await expect(page.locator('.blocksuite-portal')).toBeHidden(); + await expect(page.locator('note-display-mode-panel')).toBeHidden(); + await expect(page.locator('edgeless-color-panel')).toBeVisible(); +}); + +test('should be hidden when resizing element', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await addBasicRectShapeElement(page, { x: 210, y: 110 }, { x: 310, y: 210 }); + await page.mouse.click(220, 120); + + const toolbar = locatorComponentToolbar(page); + await expect(toolbar).toBeVisible(); + + await resizeElementByHandle(page, { x: 400, y: 300 }, 'top-left', 30); + + await page.mouse.move(450, 300); + await expect(toolbar).toBeEmpty(); + + await page.mouse.move(320, 220); + await expect(toolbar).toBeEmpty(); + + await page.mouse.up(); + await expect(toolbar).toBeVisible(); +}); diff --git a/blocksuite/tests-legacy/edgeless/eraser.spec.ts b/blocksuite/tests-legacy/edgeless/eraser.spec.ts new file mode 100644 index 0000000000000..cba83e8f8a23f --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/eraser.spec.ts @@ -0,0 +1,49 @@ +import { click } from '../utils/actions/click.js'; +import { dragBetweenCoords } from '../utils/actions/drag.js'; +import { + addBasicRectShapeElement, + deleteAll, + getNoteBoundBoxInEdgeless, + setEdgelessTool, + switchEditorMode, +} from '../utils/actions/edgeless.js'; +import { + enterPlaygroundRoom, + initEmptyEdgelessState, +} from '../utils/actions/misc.js'; +import { + assertBlockCount, + assertEdgelessNonSelectedRect, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('erase shape', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await deleteAll(page); + + await addBasicRectShapeElement(page, { x: 0, y: 0 }, { x: 100, y: 100 }); + await setEdgelessTool(page, 'eraser'); + + await dragBetweenCoords(page, { x: 50, y: 150 }, { x: 50, y: 50 }); + await click(page, { x: 50, y: 50 }); + await assertEdgelessNonSelectedRect(page); +}); + +test('erase note', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await assertBlockCount(page, 'edgeless-note', 1); + + await setEdgelessTool(page, 'eraser'); + const box = await getNoteBoundBoxInEdgeless(page, noteId); + await dragBetweenCoords( + page, + { x: 0, y: 0 }, + { x: box.x + 10, y: box.y + 10 } + ); + await assertBlockCount(page, 'edgeless-note', 0); +}); diff --git a/blocksuite/tests-legacy/edgeless/frame/clipboard.spec.ts b/blocksuite/tests-legacy/edgeless/frame/clipboard.spec.ts new file mode 100644 index 0000000000000..929668ae0a37c --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/frame/clipboard.spec.ts @@ -0,0 +1,157 @@ +import type { Page } from '@playwright/test'; +import { clickView, moveView } from 'utils/actions/click.js'; +import { + autoFit, + createFrame as _createFrame, + createShapeElement, + deleteAll, + dragBetweenViewCoords, + edgelessCommonSetup, + getAllSortedIds, + getFirstContainerId, + getIds, + Shape, + shiftClickView, + triggerComponentToolbarAction, + zoomResetByKeyboard, +} from 'utils/actions/edgeless.js'; +import { + copyByKeyboard, + pasteByKeyboard, + pressBackspace, + pressEscape, +} from 'utils/actions/keyboard.js'; +import { assertContainerOfElements } from 'utils/asserts.js'; + +import { test } from '../../utils/playwright.js'; + +const createFrame = async ( + page: Page, + coord1: [number, number], + coord2: [number, number] +) => { + await _createFrame(page, coord1, coord2); + await autoFit(page); +}; + +test.beforeEach(async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); +}); + +test.describe('frame copy and paste', () => { + test('copy of frame should keep relationship of child elements', async ({ + page, + }) => { + await createFrame(page, [50, 50], [450, 450]); + await createShapeElement(page, [200, 200], [300, 300], Shape.Square); + + const frameTitle = page.locator('affine-frame-title'); + + await pressEscape(page); + await frameTitle.click(); + await copyByKeyboard(page); + await deleteAll(page); + await moveView(page, [500, 500]); // center copy + await pasteByKeyboard(page); + + const frameId = await getFirstContainerId(page); + const shapeId = (await getAllSortedIds(page)).filter(id => id !== frameId); + await assertContainerOfElements(page, shapeId, frameId); + }); + + test('copy of frame by alt/option dragging should keep relationship of child elements', async ({ + page, + }) => { + await createFrame(page, [50, 50], [450, 450]); + await createShapeElement(page, [200, 200], [300, 300], Shape.Square); + await createShapeElement(page, [250, 250], [350, 350], Shape.Square); + await createShapeElement(page, [300, 300], [400, 400], Shape.Square); + await pressEscape(page); + + const frameTitles = page.locator('affine-frame-title'); + + await shiftClickView(page, [260, 260]); + await shiftClickView(page, [310, 310]); + await triggerComponentToolbarAction(page, 'addGroup'); + await pressEscape(page); + + await frameTitles.nth(0).click(); + await page.keyboard.down('Alt'); + await dragBetweenViewCoords(page, [60, 60], [460, 460]); + await page.keyboard.up('Alt'); + await pressEscape(page); + + await frameTitles.nth(0).click({ modifiers: ['Shift'] }); + await shiftClickView(page, [250, 250]); + await shiftClickView(page, [350, 350]); + await pressBackspace(page); // remove original elements + + const frameId = await getFirstContainerId(page); + const groupId = await getFirstContainerId(page, [frameId]); + const shapeIds = (await getIds(page)).filter( + id => ![frameId, groupId].includes(id) + ); + + await assertContainerOfElements(page, [groupId], frameId); + await assertContainerOfElements(page, [shapeIds[0]], frameId); + await assertContainerOfElements(page, [shapeIds[1]], groupId); + await assertContainerOfElements(page, [shapeIds[2]], groupId); + }); + + test('duplicate element in frame', async ({ page }) => { + await createFrame(page, [50, 50], [450, 450]); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + await pressEscape(page); + + const frameTitles = page.locator('affine-frame-title'); + + await frameTitles.nth(0).click(); + await page.locator('edgeless-more-button').click(); + await page.locator('editor-menu-action', { hasText: 'Duplicate' }).click(); + await pressEscape(page); + + await frameTitles.nth(0).click(); + await shiftClickView(page, [150, 150]); + await pressBackspace(page); // remove original elements + + const frameId = await getFirstContainerId(page); + const shapeIds = (await getIds(page)).filter(id => id !== frameId); + await assertContainerOfElements(page, shapeIds, frameId); + }); + + test('copy of element by alt/option dragging in frame should belong to frame', async ({ + page, + }) => { + await createFrame(page, [50, 50], [450, 450]); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + await pressEscape(page); + + await clickView(page, [150, 150]); + await page.keyboard.down('Alt'); + await dragBetweenViewCoords(page, [150, 150], [250, 250]); + await page.keyboard.up('Alt'); + + const frameId = await getFirstContainerId(page); + const shapeIds = (await getIds(page)).filter(id => id !== frameId); + await assertContainerOfElements(page, shapeIds, frameId); + }); + + test('copy of element by alt/option dragging out of frame should not belong to frame', async ({ + page, + }) => { + await createFrame(page, [50, 50], [450, 450]); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + await pressEscape(page); + + await clickView(page, [150, 150]); + await page.keyboard.down('Alt'); + await dragBetweenViewCoords(page, [150, 150], [550, 550]); + await page.keyboard.up('Alt'); + + const frameId = await getFirstContainerId(page); + const shapeIds = (await getIds(page)).filter(id => id !== frameId); + await assertContainerOfElements(page, [shapeIds[0]], frameId); + await assertContainerOfElements(page, [shapeIds[1]], null); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/frame/frame-mindmap.spec.ts b/blocksuite/tests-legacy/edgeless/frame/frame-mindmap.spec.ts new file mode 100644 index 0000000000000..9c726685e6aa5 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/frame/frame-mindmap.spec.ts @@ -0,0 +1,224 @@ +import type { Page } from '@playwright/test'; +import { clickView } from 'utils/actions/click.js'; +import { + createFrame, + dragBetweenViewCoords as _dragBetweenViewCoords, + edgelessCommonSetup, + getFirstContainerId, + getSelectedBound, + toViewCoord, + triggerComponentToolbarAction, + zoomResetByKeyboard, +} from 'utils/actions/edgeless.js'; +import { pressEscape } from 'utils/actions/keyboard.js'; +import { waitNextFrame } from 'utils/actions/misc.js'; +import { assertContainerOfElements } from 'utils/asserts.js'; + +import { test } from '../../utils/playwright.js'; + +const dragBetweenViewCoords = async ( + page: Page, + start: number[], + end: number[] +) => { + // dragging slowly may drop frame if mindmap is existed, so for test we drag quickly + await _dragBetweenViewCoords(page, start, end, { steps: 2 }); + await waitNextFrame(page); +}; + +test.beforeEach(async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); +}); + +test('drag root node of mindmap into frame partially, then drag root node of mindmap out.', async ({ + page, +}) => { + await createFrame(page, [50, 50], [550, 550]); + await pressEscape(page); + const frameId = await getFirstContainerId(page); + + await triggerComponentToolbarAction(page, 'addMindmap'); + const mindmapId = await getFirstContainerId(page, [frameId]); + + // drag in + { + const mindmapBound = await getSelectedBound(page); + await clickView(page, [ + mindmapBound[0] + 10, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]); + await dragBetweenViewCoords( + page, + [mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]], + [100, 100] + ); + } + + await assertContainerOfElements(page, [mindmapId], frameId); + + // drag out + { + const mindmapBound = await getSelectedBound(page); + await clickView(page, [ + mindmapBound[0] + 10, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]); + await dragBetweenViewCoords( + page, + [mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]], + [-100, -100] + ); + } + + await assertContainerOfElements(page, [mindmapId], null); +}); + +test('drag root node of mindmap into frame fully, then drag root node of mindmap out.', async ({ + page, +}) => { + await createFrame(page, [50, 50], [550, 550]); + const frameId = await getFirstContainerId(page); + await pressEscape(page); + + await triggerComponentToolbarAction(page, 'addMindmap'); + const mindmapId = await getFirstContainerId(page, [frameId]); + + // drag in + { + const mindmapBound = await getSelectedBound(page); + + await clickView(page, [ + mindmapBound[0] + 10, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]); + await dragBetweenViewCoords( + page, + [mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]], + [100, 200] + ); + } + + await assertContainerOfElements(page, [mindmapId], frameId); + + // drag out + { + const mindmapBound = await getSelectedBound(page); + await clickView(page, [ + mindmapBound[0] + 10, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]); + await dragBetweenViewCoords( + page, + [mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]], + [-100, -100] + ); + } + + await assertContainerOfElements(page, [mindmapId], null); +}); + +test('drag whole mindmap into frame, then drag root node of mindmap out.', async ({ + page, +}) => { + await createFrame(page, [50, 50], [550, 550]); + const frameId = await getFirstContainerId(page); + await pressEscape(page); + + await triggerComponentToolbarAction(page, 'addMindmap'); + const mindmapId = await getFirstContainerId(page, [frameId]); + + // drag in + { + const mindmapBound = await getSelectedBound(page); + const rootNodePos = [ + mindmapBound[0] + 10, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]; + await dragBetweenViewCoords(page, rootNodePos, [ + rootNodePos[0] - 20, + rootNodePos[1] + 200, + ]); + } + + await assertContainerOfElements(page, [mindmapId], frameId); + + // drag out + { + const mindmapBound = await getSelectedBound(page); + const rootNodePos = [ + mindmapBound[0] + 10, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]; + + await dragBetweenViewCoords(page, rootNodePos, [-100, -100]); + } + + await assertContainerOfElements(page, [mindmapId], null); +}); + +test('add mindmap into frame, then drag root node of mindmap out.', async ({ + page, +}) => { + await createFrame(page, [50, 50], [550, 550]); + const frameId = await getFirstContainerId(page); + await pressEscape(page); + + const button = page.locator('edgeless-mindmap-tool-button'); + await button.click(); + await toViewCoord(page, [100, 200]); + await clickView(page, [100, 200]); + const mindmapId = await getFirstContainerId(page, [frameId]); + + await assertContainerOfElements(page, [mindmapId], frameId); + + // drag out + { + const mindmapBound = await getSelectedBound(page); + pressEscape(page); + await clickView(page, [ + mindmapBound[0] + 10, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]); + await dragBetweenViewCoords( + page, + [mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]], + [-20, -20] + ); + } + + await assertContainerOfElements(page, [mindmapId], null); +}); + +test('add mindmap out of frame and add new node in frame then drag frame', async ({ + page, +}) => { + await createFrame(page, [500, 50], [1000, 550]); + await pressEscape(page); + + const button = page.locator('edgeless-mindmap-tool-button'); + await button.click(); + await toViewCoord(page, [20, 200]); + await clickView(page, [20, 200]); + await waitNextFrame(page, 100); + const mindmapId = await getFirstContainerId(page); + + // add new node + { + const mindmapBound = await getSelectedBound(page); + await pressEscape(page); + await waitNextFrame(page, 500); + await clickView(page, [ + mindmapBound[2] - 50, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]); + await waitNextFrame(page, 500); + await clickView(page, [ + mindmapBound[2] + 10, + mindmapBound[1] + 0.5 * mindmapBound[3], + ]); + await pressEscape(page, 2); + } + + await assertContainerOfElements(page, [mindmapId], null); +}); diff --git a/blocksuite/tests-legacy/edgeless/frame/frame-title.spec.ts b/blocksuite/tests-legacy/edgeless/frame/frame-title.spec.ts new file mode 100644 index 0000000000000..35b6f2e5823b8 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/frame/frame-title.spec.ts @@ -0,0 +1,158 @@ +import { expect, type Page } from '@playwright/test'; +import { + addNote, + autoFit, + createFrame as _createFrame, + dragBetweenViewCoords, + edgelessCommonSetup, + getFrameTitle, + zoomOutByKeyboard, + zoomResetByKeyboard, +} from 'utils/actions/edgeless.js'; +import { + pressBackspace, + pressEnter, + pressEscape, + type, +} from 'utils/actions/keyboard.js'; +import { waitNextFrame } from 'utils/actions/misc.js'; + +import { test } from '../../utils/playwright.js'; + +const createFrame = async ( + page: Page, + coord1: [number, number], + coord2: [number, number] +) => { + const frame = await _createFrame(page, coord1, coord2); + await autoFit(page); + return frame; +}; + +test.beforeEach(async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); +}); + +const enterFrameTitleEditor = async (page: Page) => { + const frameTitle = page.locator('affine-frame-title'); + await frameTitle.dblclick(); + + const frameTitleEditor = page.locator('edgeless-frame-title-editor'); + await frameTitleEditor.waitFor({ + state: 'attached', + }); + return frameTitleEditor; +}; + +test.describe('frame title rendering', () => { + test('frame title should be displayed', async ({ page }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + + const frameTitle = getFrameTitle(page, frame); + await expect(frameTitle).toBeVisible(); + await expect(frameTitle).toHaveText('Frame 1'); + }); + + test('frame title should be rendered on the top', async ({ page }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + + const frameTitle = getFrameTitle(page, frame); + await expect(frameTitle).toBeVisible(); + + const frameTitleBounding = await frameTitle.boundingBox(); + expect(frameTitleBounding).not.toBeNull(); + if (!frameTitleBounding) return; + + const frameTitleCenter = [ + frameTitleBounding.x + frameTitleBounding.width / 2, + frameTitleBounding.y + frameTitleBounding.height / 2, + ]; + + await addNote(page, '', frameTitleCenter[0], frameTitleCenter[1]); + await pressEscape(page, 3); + await waitNextFrame(page, 500); + + try { + // if the frame title is rendered on the top, it should be clickable + await frameTitle.click(); + } catch { + expect(true, 'frame title should be rendered on the top').toBeFalsy(); + } + }); + + test('should not display frame title component when title is empty', async ({ + page, + }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + await enterFrameTitleEditor(page); + + await pressBackspace(page); + await pressEnter(page); + const frameTitle = getFrameTitle(page, frame); + await expect(frameTitle).toBeHidden(); + }); +}); + +test.describe('frame title editing', () => { + test('edit frame title by db-click title', async ({ page }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + const frameTitle = getFrameTitle(page, frame); + + await enterFrameTitleEditor(page); + + await type(page, 'ABC'); + await pressEnter(page); + await expect(frameTitle).toHaveText('ABC'); + }); + + test('frame title can be edited repeatedly', async ({ page }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + const frameTitle = getFrameTitle(page, frame); + + await enterFrameTitleEditor(page); + await type(page, 'ABC'); + await pressEnter(page); + + await enterFrameTitleEditor(page); + await type(page, 'DEF'); + await pressEnter(page); + await expect(frameTitle).toHaveText('DEF'); + }); + + test('edit frame after zoom', async ({ page }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + const frameTitle = getFrameTitle(page, frame); + + await zoomOutByKeyboard(page); + await enterFrameTitleEditor(page); + await type(page, 'ABC'); + await pressEnter(page); + await expect(frameTitle).toHaveText('ABC'); + }); + + test('edit frame title after drag', async ({ page }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + const frameTitle = getFrameTitle(page, frame); + await dragBetweenViewCoords(page, [50 + 10, 50 + 10], [50 + 20, 50 + 20]); + + await enterFrameTitleEditor(page); + await type(page, 'ABC'); + await pressEnter(page); + await expect(frameTitle).toHaveText('ABC'); + }); + + test('blur unmount frame editor', async ({ page }) => { + await createFrame(page, [50, 50], [150, 150]); + const frameTitleEditor = await enterFrameTitleEditor(page); + await page.mouse.click(10, 10); + await expect(frameTitleEditor).toHaveCount(0); + }); + + test('enter unmount frame editor', async ({ page }) => { + await createFrame(page, [50, 50], [150, 150]); + const frameTitleEditor = await enterFrameTitleEditor(page); + await pressEnter(page); + await expect(frameTitleEditor).toHaveCount(0); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/frame/frame.spec.ts b/blocksuite/tests-legacy/edgeless/frame/frame.spec.ts new file mode 100644 index 0000000000000..088a15bb9b79f --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/frame/frame.spec.ts @@ -0,0 +1,414 @@ +import { + DEFAULT_NOTE_HEIGHT, + DEFAULT_NOTE_WIDTH, +} from '@blocksuite/affine-model'; +import { Bound } from '@blocksuite/global/utils'; +import { expect, type Page } from '@playwright/test'; + +import { clickView } from '../../utils/actions/click.js'; +import { + addNote, + autoFit, + createFrame as _createFrame, + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup, + getFirstContainerId, + getIds, + getSelectedBound, + getSelectedIds, + pickColorAtPoints, + setEdgelessTool, + Shape, + shiftClickView, + toViewCoord, + triggerComponentToolbarAction, + zoomResetByKeyboard, +} from '../../utils/actions/edgeless.js'; +import { + pressBackspace, + pressEscape, + SHORT_KEY, +} from '../../utils/actions/keyboard.js'; +import { + assertCanvasElementsCount, + assertContainerChildCount, + assertEdgelessElementBound, + assertSelectedBound, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +const createFrame = async ( + page: Page, + coord1: [number, number], + coord2: [number, number] +) => { + const frameId = await _createFrame(page, coord1, coord2); + await autoFit(page); + await pressEscape(page); + return frameId; +}; + +test.beforeEach(async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); +}); + +test.describe('add a frame', () => { + const createThreeShapesAndSelectTowShape = async (page: Page) => { + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 0], [200, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + + await clickView(page, [50, 50]); + await shiftClickView(page, [150, 50]); + }; + + test('multi select and add frame by shortcut F', async ({ page }) => { + await createThreeShapesAndSelectTowShape(page); + await page.keyboard.press('f'); + + await expect(page.locator('affine-frame')).toHaveCount(1); + await assertSelectedBound(page, [-40, -40, 280, 180]); + + const frameId = await getFirstContainerId(page); + await assertContainerChildCount(page, frameId, 2); + }); + + test('multi select and add frame by component toolbar', async ({ page }) => { + await createThreeShapesAndSelectTowShape(page); + await triggerComponentToolbarAction(page, 'addFrame'); + + await expect(page.locator('affine-frame')).toHaveCount(1); + await assertSelectedBound(page, [-40, -40, 280, 180]); + + const frameId = await getFirstContainerId(page); + await assertContainerChildCount(page, frameId, 2); + }); + + test('multi select and add frame by more option create frame', async ({ + page, + }) => { + await createThreeShapesAndSelectTowShape(page); + await triggerComponentToolbarAction(page, 'createFrameOnMoreOption'); + + await expect(page.locator('affine-frame')).toHaveCount(1); + await assertSelectedBound(page, [-40, -40, 280, 180]); + + const frameId = await getFirstContainerId(page); + await assertContainerChildCount(page, frameId, 2); + }); + + test('multi select add frame by edgeless toolbar', async ({ page }) => { + await createThreeShapesAndSelectTowShape(page); + await autoFit(page); + await setEdgelessTool(page, 'frame'); + const frameMenu = page.locator('edgeless-frame-menu'); + await expect(frameMenu).toBeVisible(); + const button = page.locator('.frame-add-button[data-name="1:1"]'); + await button.click(); + await assertSelectedBound(page, [-450, -550, 1200, 1200]); + + // the third should be inner frame because + const frameId = await getFirstContainerId(page); + await assertContainerChildCount(page, frameId, 3); + }); + + test('add frame by dragging with shortcut F', async ({ page }) => { + await createThreeShapesAndSelectTowShape(page); + await pressEscape(page); // unselect + + await page.keyboard.press('f'); + await dragBetweenViewCoords(page, [-10, -10], [210, 110]); + + await expect(page.locator('affine-frame')).toHaveCount(1); + await assertSelectedBound(page, [-10, -10, 220, 120]); + + const frameId = await getFirstContainerId(page); + await assertContainerChildCount(page, frameId, 2); + }); + + test('add inner frame', async ({ page }) => { + await createFrame(page, [50, 50], [450, 450]); + await createShapeElement(page, [200, 200], [300, 300], Shape.Square); + await pressEscape(page); + + await shiftClickView(page, [250, 250]); + await page.keyboard.press('f'); + const innerFrameBound = await getSelectedBound(page); + expect( + new Bound(50, 50, 400, 400).contains(Bound.fromXYWH(innerFrameBound)) + ).toBeTruthy(); + }); +}); + +test.describe('add element to frame and then move frame', () => { + test.describe('add single element', () => { + test('element should be moved since it is created in frame', async ({ + page, + }) => { + const frameId = await createFrame(page, [50, 50], [550, 550]); + const shapeId = await createShapeElement( + page, + [100, 100], + [200, 200], + Shape.Square + ); + + const noteCoord = await toViewCoord(page, [200, 200]); + const noteId = await addNote(page, '', noteCoord[0], noteCoord[1]); + + const frameTitle = page.locator('affine-frame-title'); + + await pressEscape(page); + + await frameTitle.click(); + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeId, [150, 150, 100, 100]); + await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]); + await assertEdgelessElementBound(page, noteId, [ + 220, + 210, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + }); + + test('element should be not moved since it is created not in frame', async ({ + page, + }) => { + const frameId = await createFrame(page, [50, 50], [550, 550]); + const shapeId = await createShapeElement( + page, + [600, 600], + [500, 500], + Shape.Square + ); + await pressEscape(page); + + const frameTitle = page.locator('affine-frame-title'); + + await frameTitle.click(); + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeId, [500, 500, 100, 100]); + await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]); + }); + }); + + test.describe('add group', () => { + // Group + // |<150px>| + // โ”Œโ”€โ”€โ”€โ”€โ” โ”€ + // โ”‚ โ”Œโ”€โ”ผโ”€โ”€โ” 150 px + // โ””โ”€โ”€โ”ผโ”€โ”˜ โ”‚ | + // โ””โ”€โ”€โ”€โ”€โ”˜ โ”€ + + test('group should be moved since it is fully contained in frame', async ({ + page, + }) => { + const [frameId, ...shapeIds] = [ + await createFrame(page, [50, 50], [550, 550]), + await createShapeElement(page, [100, 100], [200, 200], Shape.Square), + await createShapeElement(page, [150, 150], [250, 250], Shape.Square), + ]; + await pressEscape(page); + + const frameTitle = page.locator('affine-frame-title'); + + await shiftClickView(page, [110, 110]); + await shiftClickView(page, [160, 160]); + await page.keyboard.press(`${SHORT_KEY}+g`); + const groupId = (await getSelectedIds(page))[0]; + await pressEscape(page); + + await frameTitle.click(); + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeIds[0], [150, 150, 100, 100]); + await assertEdgelessElementBound(page, shapeIds[1], [200, 200, 100, 100]); + await assertEdgelessElementBound(page, groupId, [150, 150, 150, 150]); + await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]); + }); + + test('group should be moved since its center is in frame', async ({ + page, + }) => { + const [frameId, ...shapeIds] = [ + await createFrame(page, [50, 50], [550, 550]), + await createShapeElement(page, [450, 450], [550, 550], Shape.Square), + await createShapeElement(page, [500, 500], [600, 600], Shape.Square), + ]; + await pressEscape(page); + + const frameTitle = page.locator('affine-frame-title'); + + await shiftClickView(page, [460, 460]); + await shiftClickView(page, [510, 510]); + await page.keyboard.press(`${SHORT_KEY}+g`); + const groupId = (await getSelectedIds(page))[0]; + await pressEscape(page); + + await frameTitle.click(); + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeIds[0], [500, 500, 100, 100]); + await assertEdgelessElementBound(page, shapeIds[1], [550, 550, 100, 100]); + await assertEdgelessElementBound(page, groupId, [500, 500, 150, 150]); + await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]); + }); + }); + + test.describe('add inner frame', () => { + test('the inner frame and its children should be moved since it is fully contained in frame', async ({ + page, + }) => { + const [frameId, innerId, shapeId] = [ + await createFrame(page, [50, 50], [550, 550]), + await createFrame(page, [100, 100], [300, 300]), + await createShapeElement(page, [150, 150], [250, 250], Shape.Square), + ]; + await pressEscape(page); + + const frameTitles = page.locator('affine-frame-title'); + + await frameTitles.nth(0).click(); + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeId, [200, 200, 100, 100]); + await assertEdgelessElementBound(page, innerId, [150, 150, 200, 200]); + await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]); + }); + + test('the inner frame and its children should be moved since its center is in frame', async ({ + page, + }) => { + const [frameId, innerId, shapeId] = [ + await createFrame(page, [50, 50], [550, 550]), + await createFrame(page, [400, 400], [600, 600]), + await createShapeElement(page, [550, 550], [600, 600], Shape.Square), + ]; + await pressEscape(page); + + const frameTitles = page.locator('affine-frame-title'); + + await frameTitles.nth(0).click(); + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeId, [600, 600, 50, 50]); + await assertEdgelessElementBound(page, innerId, [450, 450, 200, 200]); + await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]); + }); + + test('the inner frame and its children should also be moved even though its center is not in frame', async ({ + page, + }) => { + const [frameId, innerId, shapeId] = [ + await createFrame(page, [50, 50], [550, 550]), + await createFrame(page, [500, 500], [600, 600]), + await createShapeElement(page, [550, 550], [600, 600], Shape.Square), + ]; + + const frameTitles = page.locator('affine-frame-title'); + + await frameTitles.nth(0).click(); + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeId, [600, 600, 50, 50]); + await assertEdgelessElementBound(page, innerId, [550, 550, 100, 100]); + await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]); + }); + }); +}); + +test.describe('resize frame then move ', () => { + test('resize frame to warp shape', async ({ page }) => { + const [frameId, shapeId] = [ + await createFrame(page, [50, 50], [150, 150]), + await createShapeElement(page, [200, 200], [300, 300], Shape.Square), + ]; + await pressEscape(page); + + const frameTitle = page.locator('affine-frame-title'); + + await frameTitle.click(); + await dragBetweenViewCoords(page, [150, 150], [450, 450]); + + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeId, [250, 250, 100, 100]); + await assertEdgelessElementBound(page, frameId, [100, 100, 400, 400]); + }); + + test('resize frame to unwrap shape', async ({ page }) => { + const [frameId, shapeId] = [ + await createFrame(page, [50, 50], [450, 450]), + await createShapeElement(page, [200, 200], [300, 300], Shape.Square), + ]; + await pressEscape(page); + + const frameTitle = page.locator('affine-frame-title'); + + await frameTitle.click(); + await dragBetweenViewCoords(page, [450, 450], [150, 150]); + + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + await assertEdgelessElementBound(page, shapeId, [200, 200, 100, 100]); + await assertEdgelessElementBound(page, frameId, [100, 100, 100, 100]); + }); +}); + +test('delete frame should also delete its children', async ({ page }) => { + await createFrame(page, [50, 50], [450, 450]); + await createShapeElement(page, [200, 200], [300, 300], Shape.Square); + await pressEscape(page); + + const frameTitle = page.locator('affine-frame-title'); + + await frameTitle.click(); + await pressBackspace(page); + await expect(page.locator('affine-frame')).toHaveCount(0); + + await assertCanvasElementsCount(page, 0); +}); + +test('delete frame by click ungroup should not delete its children', async ({ + page, +}) => { + await createFrame(page, [50, 50], [450, 450]); + const shapeId = await createShapeElement( + page, + [200, 200], + [300, 300], + Shape.Square + ); + await pressEscape(page); + + const frameTitle = page.locator('affine-frame-title'); + await frameTitle.click(); + const elementToolbar = page.locator('edgeless-element-toolbar-widget'); + const ungroupButton = elementToolbar.getByLabel('Ungroup'); + await ungroupButton.click(); + + await assertCanvasElementsCount(page, 1); + expect(await getIds(page)).toEqual([shapeId]); +}); + +test('outline should keep updated during a new frame created by frame-tool dragging', async ({ + page, +}) => { + await page.keyboard.press('f'); + + const start = await toViewCoord(page, [0, 0]); + const end = await toViewCoord(page, [100, 100]); + await page.mouse.move(start[0], start[1]); + await page.mouse.down(); + await page.mouse.move(end[0], end[1], { steps: 10 }); + await page.waitForTimeout(50); + + expect( + await pickColorAtPoints(page, [start, [end[0] - 1, end[1] - 1]]) + ).toEqual(['#1e96eb', '#1e96eb']); +}); diff --git a/blocksuite/tests-legacy/edgeless/frame/layer.spec.ts b/blocksuite/tests-legacy/edgeless/frame/layer.spec.ts new file mode 100644 index 0000000000000..025af14ae2d65 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/frame/layer.spec.ts @@ -0,0 +1,64 @@ +import { expect } from '@playwright/test'; +import { + createFrame, + createNote, + createShapeElement, + edgelessCommonSetup, + getAllSortedIds, + getEdgelessSelectedRectModel, + Shape, + zoomResetByKeyboard, +} from 'utils/actions/edgeless.js'; +import { pressEscape, selectAllByKeyboard } from 'utils/actions/keyboard.js'; + +import { test } from '../../utils/playwright.js'; + +test.beforeEach(async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); +}); + +test.describe('layer logic of frame block', () => { + test('a new frame should be on the bottom layer', async ({ page }) => { + const shapeId = await createShapeElement( + page, + [100, 100], + [200, 200], + Shape.Square + ); + const noteId = await createNote(page, [200, 200]); + await pressEscape(page, 3); + + await selectAllByKeyboard(page); + const [x, y, w, h] = await getEdgelessSelectedRectModel(page); + await pressEscape(page); + const frameAId = await createFrame( + page, + [x - 10, y - 10], + [x + w + 10, y + h + 10] + ); + + let sortedIds = await getAllSortedIds(page); + expect( + sortedIds[0], + 'a new frame created by frame-tool should be on the bottom layer' + ).toBe(frameAId); + expect(sortedIds[1]).toBe(shapeId); + expect(sortedIds[2]).toBe(noteId); + + await selectAllByKeyboard(page); + await page.keyboard.press('f'); + + sortedIds = await getAllSortedIds(page); + const frameBId = sortedIds.find( + id => ![frameAId, noteId, shapeId].includes(id) + ); + expect( + sortedIds[0], + 'a new frame created by short-cut should also be on the bottom layer' + ).toBe(frameBId); + expect(sortedIds[1]).toBe(frameAId); + expect(sortedIds[2]).toBe(shapeId); + expect(sortedIds[3]).toBe(noteId); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/frame/selection.spec.ts b/blocksuite/tests-legacy/edgeless/frame/selection.spec.ts new file mode 100644 index 0000000000000..626d0acbd7ec6 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/frame/selection.spec.ts @@ -0,0 +1,158 @@ +import { expect, type Page } from '@playwright/test'; +import { click, clickView, dblclickView } from 'utils/actions/click.js'; +import { + addNote, + autoFit, + createFrame as _createFrame, + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup, + getFrameTitle, + getSelectedBoundCount, + getSelectedIds, + Shape, + toViewCoord, + zoomResetByKeyboard, +} from 'utils/actions/edgeless.js'; +import { + pressBackspace, + pressEnter, + pressEscape, + selectAllByKeyboard, + type, +} from 'utils/actions/keyboard.js'; +import { waitNextFrame } from 'utils/actions/misc.js'; +import { + assertEdgelessCanvasText, + assertRichTexts, + assertSelectedBound, +} from 'utils/asserts.js'; + +import { test } from '../../utils/playwright.js'; + +const createFrame = async ( + page: Page, + coord1: [number, number], + coord2: [number, number] +) => { + const frame = await _createFrame(page, coord1, coord2); + await autoFit(page); + return frame; +}; + +test.beforeEach(async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); +}); + +test.describe('frame selection', () => { + test('frame can not be selected by click blank area of frame if it has title', async ({ + page, + }) => { + await createFrame(page, [50, 50], [150, 150]); + await pressEscape(page); + expect(await getSelectedBoundCount(page)).toBe(0); + + await clickView(page, [100, 100]); + expect(await getSelectedBoundCount(page)).toBe(0); + }); + + test('frame can selected by click blank area of frame if it has not title', async ({ + page, + }) => { + await createFrame(page, [50, 50], [150, 150]); + await pressEscape(page); + expect(await getSelectedBoundCount(page)).toBe(0); + + await page.locator('affine-frame-title').dblclick(); + await pressBackspace(page); + await pressEnter(page); + + await clickView(page, [100, 100]); + expect(await getSelectedBoundCount(page)).toBe(1); + }); + + test('frame can be selected by click frame title', async ({ page }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + await pressEscape(page); + expect(await getSelectedBoundCount(page)).toBe(0); + + const frameTitle = getFrameTitle(page, frame); + await frameTitle.click(); + + expect(await getSelectedBoundCount(page)).toBe(1); + await assertSelectedBound(page, [50, 50, 100, 100]); + }); + + test('frame can be selected by click frame title when a note overlap on it', async ({ + page, + }) => { + const frame = await createFrame(page, [50, 50], [150, 150]); + await pressEscape(page); + + const frameTitle = getFrameTitle(page, frame); + const frameTitleBox = await frameTitle.boundingBox(); + expect(frameTitleBox).not.toBeNull(); + if (frameTitleBox === null) return; + + const frameTitleCenter = { + x: frameTitleBox.x + frameTitleBox.width / 2, + y: frameTitleBox.y + frameTitleBox.height / 2, + }; + + await addNote(page, '', frameTitleCenter.x - 10, frameTitleCenter.y); + await pressEscape(page, 3); + await waitNextFrame(page, 500); + expect(await getSelectedBoundCount(page)).toBe(0); + + await click(page, frameTitleCenter); + expect(await getSelectedBoundCount(page)).toBe(1); + const selectedIds = await getSelectedIds(page); + expect(selectedIds.length).toBe(1); + expect(selectedIds[0]).toBe(frame); + }); + + test('shape inside frame can be selected and edited', async ({ page }) => { + await createFrame(page, [50, 50], [150, 150]); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + await pressEscape(page); + + await clickView(page, [150, 150]); + expect(await getSelectedBoundCount(page)).toBe(1); + await assertSelectedBound(page, [100, 100, 100, 100]); + + await dblclickView(page, [150, 150]); + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); + }); + + test('dom inside frame can be selected and edited', async ({ page }) => { + await createFrame(page, [50, 50], [150, 150]); + const noteCoord = await toViewCoord(page, [100, 100]); + await addNote(page, '', noteCoord[0], noteCoord[1]); + await page.mouse.click(noteCoord[0] - 80, noteCoord[1]); + + await dblclickView(page, [150, 150]); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + }); + + test('element in frame should not be selected when frame is selected by drag or Cmd/Ctrl + A', async ({ + page, + }) => { + await createFrame(page, [50, 50], [200, 200]); + await createShapeElement(page, [100, 100], [150, 150], Shape.Square); + await pressEscape(page); + + await dragBetweenViewCoords(page, [0, 0], [250, 250]); + expect(await getSelectedBoundCount(page)).toBe(1); + await assertSelectedBound(page, [50, 50, 150, 150]); + + await pressEscape(page); + expect(await getSelectedBoundCount(page)).toBe(0); + + await selectAllByKeyboard(page); + expect(await getSelectedBoundCount(page)).toBe(1); + await assertSelectedBound(page, [50, 50, 150, 150]); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/group/clipboard.spec.ts b/blocksuite/tests-legacy/edgeless/group/clipboard.spec.ts new file mode 100644 index 0000000000000..0f8a4c1f10349 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/group/clipboard.spec.ts @@ -0,0 +1,152 @@ +import { expect } from '@playwright/test'; + +import { + copyByKeyboard, + createConnectorElement, + createNote, + createShapeElement, + decreaseZoomLevel, + edgelessCommonSetup as commonSetup, + edgelessCommonSetup, + getAllSortedIds, + getFirstContainerId, + pasteByKeyboard, + selectAllByKeyboard, + Shape, + toViewCoord, + triggerComponentToolbarAction, + waitNextFrame, +} from '../../utils/actions/index.js'; +import { + assertContainerChildCount, + assertContainerIds, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test.describe('clipboard', () => { + test('copy and paste group', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 0], [200, 100], Shape.Square); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + const originGroupId = await getFirstContainerId(page); + + await copyByKeyboard(page); + await waitNextFrame(page, 100); + const move = await toViewCoord(page, [100, -50]); + await page.mouse.click(move[0], move[1]); + await waitNextFrame(page, 1000); + await pasteByKeyboard(page, false); + const copyedGroupId = await getFirstContainerId(page, [originGroupId]); + + await assertContainerIds(page, { + [originGroupId]: 2, + [copyedGroupId]: 2, + null: 2, + }); + await assertContainerChildCount(page, originGroupId, 2); + await assertContainerChildCount(page, copyedGroupId, 2); + }); + + test('copy and paste group with connector', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 0], [200, 100], Shape.Square); + await createConnectorElement(page, [100, 50], [200, 50]); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + const originGroupId = await getFirstContainerId(page); + + await copyByKeyboard(page); + await waitNextFrame(page, 100); + const move = await toViewCoord(page, [100, -50]); + await page.mouse.click(move[0], move[1]); + await waitNextFrame(page, 1000); + await pasteByKeyboard(page, false); + const copyedGroupId = await getFirstContainerId(page, [originGroupId]); + + await assertContainerIds(page, { + [originGroupId]: 3, + [copyedGroupId]: 3, + null: 2, + }); + await assertContainerChildCount(page, originGroupId, 3); + await assertContainerChildCount(page, copyedGroupId, 3); + }); +}); + +test.describe('group clipboard', () => { + test('copy and paste group with shape and note inside', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [100, -100]); + await page.mouse.click(10, 50); + + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + const originIds = await getAllSortedIds(page); + expect(originIds.length).toBe(3); + + await copyByKeyboard(page); + const move = await toViewCoord(page, [250, 250]); + await page.mouse.move(move[0], move[1]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, true); + await waitNextFrame(page, 500); + const sortedIds = await getAllSortedIds(page); + expect(sortedIds.length).toBe(6); + }); + + test('copy and paste group with group inside', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + await createNote(page, [100, -100]); + await page.mouse.click(10, 50); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'createGroupOnMoreOption'); + + const originIds = await getAllSortedIds(page); + expect(originIds.length).toBe(5); + + await copyByKeyboard(page); + const move = await toViewCoord(page, [250, 250]); + await page.mouse.move(move[0], move[1]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, true); + await waitNextFrame(page, 500); + const sortedIds = await getAllSortedIds(page); + expect(sortedIds.length).toBe(10); + }); + + test('copy and paste group with frame inside', async ({ page }) => { + await commonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createNote(page, [100, -100]); + await page.mouse.click(10, 50); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addFrame'); + + await decreaseZoomLevel(page); + await createShapeElement(page, [700, 0], [800, 100], Shape.Square); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + const originIds = await getAllSortedIds(page); + expect(originIds.length).toBe(5); + + await copyByKeyboard(page); + const move = await toViewCoord(page, [250, 250]); + await page.mouse.move(move[0], move[1]); + await page.mouse.click(move[0], move[1]); + await pasteByKeyboard(page, true); + await waitNextFrame(page, 500); + const sortedIds = await getAllSortedIds(page); + expect(sortedIds.length).toBe(10); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/group/group-and-ungroup.spec.ts b/blocksuite/tests-legacy/edgeless/group/group-and-ungroup.spec.ts new file mode 100644 index 0000000000000..cd18fe9229fa9 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/group/group-and-ungroup.spec.ts @@ -0,0 +1,123 @@ +import type { Page } from '@playwright/test'; + +import { + captureHistory, + clickView, + createShapeElement, + edgelessCommonSetup, + getFirstContainerId, + getIds, + redoByKeyboard, + selectAllByKeyboard, + Shape, + shiftClickView, + toIdCountMap, + triggerComponentToolbarAction, + undoByKeyboard, +} from '../../utils/actions/index.js'; +import { + assertContainerChildCount, + assertContainerChildIds, + assertContainerIds, + assertSelectedBound, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +let initShapes: string[] = []; +async function init(page: Page) { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 0], [200, 100], Shape.Square); + initShapes = await getIds(page); +} + +test.describe('group and ungroup in group', () => { + let outterGroupId: string; + let newAddedShape: string; + + test.beforeEach(async ({ page }) => { + await init(page); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + newAddedShape = (await getIds(page)).filter( + id => !initShapes.includes(id) + )[0]; + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + outterGroupId = await getFirstContainerId(page); + }); + + test('group in group', async ({ page }) => { + await clickView(page, [50, 50]); + await shiftClickView(page, [150, 50]); + await captureHistory(page); + await triggerComponentToolbarAction(page, 'addGroup'); + const groupId = await getFirstContainerId(page, [outterGroupId]); + await assertSelectedBound(page, [0, 0, 200, 100]); + await assertContainerIds(page, { + [groupId]: 2, + [outterGroupId]: 2, + null: 1, + }); + await assertContainerChildCount(page, groupId, 2); + await assertContainerChildCount(page, outterGroupId, 2); + + // undo the creation + await undoByKeyboard(page); + await assertContainerIds(page, { + [outterGroupId]: 3, + null: 1, + }); + await assertContainerChildCount(page, outterGroupId, 3); + + // redo the creation + await redoByKeyboard(page); + await assertContainerIds(page, { + [groupId]: 2, + [outterGroupId]: 2, + null: 1, + }); + await assertContainerChildCount(page, groupId, 2); + await assertContainerChildCount(page, outterGroupId, 2); + }); + + test('ungroup in group', async ({ page }) => { + await clickView(page, [50, 50]); + await shiftClickView(page, [150, 50]); + await triggerComponentToolbarAction(page, 'addGroup'); + await captureHistory(page); + const groupId = await getFirstContainerId(page, [outterGroupId]); + await triggerComponentToolbarAction(page, 'ungroup'); + await assertContainerIds(page, { [outterGroupId]: 3, null: 1 }); + await assertContainerChildIds( + page, + toIdCountMap(await getIds(page, true)), + outterGroupId + ); + + // undo, group should in group again + await undoByKeyboard(page); + await assertContainerIds(page, { + [outterGroupId]: 2, + [groupId]: 2, + null: 1, + }); + await assertContainerChildIds(page, toIdCountMap(initShapes), groupId); + await assertContainerChildIds( + page, + { + [groupId]: 1, + [newAddedShape]: 1, + }, + outterGroupId + ); + + // redo, group should be ungroup again + await redoByKeyboard(page); + await assertContainerIds(page, { [outterGroupId]: 3, null: 1 }); + await assertContainerChildIds( + page, + toIdCountMap(await getIds(page, true)), + outterGroupId + ); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/group/group.spec.ts b/blocksuite/tests-legacy/edgeless/group/group.spec.ts new file mode 100644 index 0000000000000..94e4477191daf --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/group/group.spec.ts @@ -0,0 +1,260 @@ +import { expect, type Page } from '@playwright/test'; + +import { clickView } from '../../utils/actions/click.js'; +import { + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup, + getFirstContainerId, + Shape, + shiftClickView, + triggerComponentToolbarAction, +} from '../../utils/actions/edgeless.js'; +import { + pressBackspace, + redoByKeyboard, + selectAllByKeyboard, + SHORT_KEY, + undoByKeyboard, +} from '../../utils/actions/keyboard.js'; +import { captureHistory } from '../../utils/actions/misc.js'; +import { + assertCanvasElementsCount, + assertContainerChildCount, + assertContainerIds, + assertEdgelessNonSelectedRect, + assertSelectedBound, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +export const GROUP_ROOT_ID = 'GROUP_ROOT'; + +test.describe('group', () => { + async function init(page: Page) { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 0], [200, 100], Shape.Square); + } + + test.describe('group create', () => { + test.beforeEach(async ({ page }) => { + await init(page); + }); + + test('create group button not show when single select', async ({ + page, + }) => { + await clickView(page, [50, 50]); + await expect( + page.locator('edgeless-element-toolbar-widget') + ).toBeVisible(); + await expect(page.locator('edgeless-add-group-button')).not.toBeVisible(); + }); + + test('create button show up when multi select', async ({ page }) => { + await selectAllByKeyboard(page); + await expect(page.locator('edgeless-add-group-button')).toBeVisible(); + }); + + test('create group by component toolbar', async ({ page }) => { + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + await assertSelectedBound(page, [0, 0, 200, 100]); + }); + + test('create group by shortcut mod + G', async ({ page }) => { + await selectAllByKeyboard(page); + await page.keyboard.press(`${SHORT_KEY}+g`); + await assertSelectedBound(page, [0, 0, 200, 100]); + }); + + test('create group and undo, redo', async ({ page }) => { + await selectAllByKeyboard(page); + await captureHistory(page); + await page.keyboard.press(`${SHORT_KEY}+g`); + await assertSelectedBound(page, [0, 0, 200, 100]); + await undoByKeyboard(page); + await assertSelectedBound(page, [0, 0, 100, 100]); + await redoByKeyboard(page); + await assertSelectedBound(page, [0, 0, 200, 100]); + }); + }); + + test.describe('ungroup', () => { + test.beforeEach(async ({ page }) => { + await init(page); + }); + + test('ungroup by component toolbar', async ({ page }) => { + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + await assertSelectedBound(page, [0, 0, 200, 100]); + await triggerComponentToolbarAction(page, 'ungroup'); + await assertEdgelessNonSelectedRect(page); + }); + + test('ungroup by shortcut mod + shift + G', async ({ page }) => { + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + await assertSelectedBound(page, [0, 0, 200, 100]); + await page.keyboard.press(`${SHORT_KEY}+Shift+g`); + await assertEdgelessNonSelectedRect(page); + }); + + test('ungroup and undo, redo', async ({ page }) => { + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + await assertSelectedBound(page, [0, 0, 200, 100]); + await captureHistory(page); + await page.keyboard.press(`${SHORT_KEY}+Shift+g`); + await assertEdgelessNonSelectedRect(page); + await undoByKeyboard(page); + await assertSelectedBound(page, [0, 0, 200, 100]); + await redoByKeyboard(page); + await assertEdgelessNonSelectedRect(page); + }); + }); + + test.describe('drag group', () => { + test.beforeEach(async ({ page }) => { + await init(page); + }); + + test('drag group to move', async ({ page }) => { + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + await dragBetweenViewCoords(page, [100, 50], [110, 50]); + await assertSelectedBound(page, [10, 0, 200, 100]); + }); + }); + + test.describe('select', () => { + test.beforeEach(async ({ page }) => { + await init(page); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + }); + + test('select group by click', async ({ page }) => { + await clickView(page, [300, -100]); + await assertEdgelessNonSelectedRect(page); + await clickView(page, [50, 50]); + await assertSelectedBound(page, [0, 0, 200, 100]); + }); + + test('select sub-element by first select group', async ({ page }) => { + await clickView(page, [50, 50]); + await assertSelectedBound(page, [0, 0, 100, 100]); + }); + + test('select element when enter gorup', async ({ page }) => { + await clickView(page, [50, 50]); + await assertSelectedBound(page, [0, 0, 100, 100]); + await clickView(page, [150, 50]); + await assertSelectedBound(page, [100, 0, 100, 100]); + }); + }); + + test.describe('delete', () => { + test.beforeEach(async ({ page }) => { + await init(page); + }); + + test('delete root group', async ({ page }) => { + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + const groupId = await getFirstContainerId(page); + await captureHistory(page); + await pressBackspace(page); + await assertCanvasElementsCount(page, 0); + + // undo the delete + await undoByKeyboard(page); + await assertCanvasElementsCount(page, 3); + await assertContainerIds(page, { + [groupId]: 2, + null: 1, + }); + await assertContainerChildCount(page, groupId, 2); + + // redo the delete + await redoByKeyboard(page); + await assertCanvasElementsCount(page, 0); + }); + + test('delete sub-element in group', async ({ page }) => { + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + const groupId = await getFirstContainerId(page); + await captureHistory(page); + await clickView(page, [50, 50]); + await pressBackspace(page); + + await assertCanvasElementsCount(page, 2); + await assertContainerIds(page, { + [groupId]: 1, + null: 1, + }); + await assertContainerChildCount(page, groupId, 1); + + // undo the delete + await undoByKeyboard(page); + await assertCanvasElementsCount(page, 3); + await assertContainerIds(page, { + [groupId]: 2, + null: 1, + }); + await assertContainerChildCount(page, groupId, 2); + + // redo the delete + await redoByKeyboard(page); + await assertCanvasElementsCount(page, 2); + await assertContainerIds(page, { + [groupId]: 1, + null: 1, + }); + await assertContainerChildCount(page, groupId, 1); + }); + + test('delete group in group', async ({ page }) => { + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + const firstGroup = await getFirstContainerId(page); + await clickView(page, [50, 50]); + await shiftClickView(page, [150, 50]); + await triggerComponentToolbarAction(page, 'addGroup'); + const secondGroup = await getFirstContainerId(page, [firstGroup]); + await captureHistory(page); + + // delete group in group + await pressBackspace(page); + await assertCanvasElementsCount(page, 2); + await assertContainerIds(page, { + [firstGroup]: 1, + null: 1, + }); + await assertContainerChildCount(page, firstGroup, 1); + + // undo the delete + await undoByKeyboard(page); + await assertCanvasElementsCount(page, 5); + await assertContainerIds(page, { + [firstGroup]: 2, + [secondGroup]: 2, + null: 1, + }); + await assertContainerChildCount(page, firstGroup, 2); + await assertContainerChildCount(page, secondGroup, 2); + + // redo the delete + await redoByKeyboard(page); + await assertCanvasElementsCount(page, 2); + await assertContainerIds(page, { + [firstGroup]: 1, + null: 1, + }); + await assertContainerChildCount(page, firstGroup, 1); + }); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/group/release.spec.ts b/blocksuite/tests-legacy/edgeless/group/release.spec.ts new file mode 100644 index 0000000000000..7de0e84c8eb7a --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/group/release.spec.ts @@ -0,0 +1,116 @@ +import type { Page } from '@playwright/test'; + +import { + captureHistory, + clickView, + createShapeElement, + edgelessCommonSetup, + getFirstContainerId, + redoByKeyboard, + selectAllByKeyboard, + Shape, + shiftClickView, + triggerComponentToolbarAction, + undoByKeyboard, +} from '../../utils/actions/index.js'; +import { + assertContainerChildCount, + assertContainerIds, + assertSelectedBound, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +async function init(page: Page) { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 0], [200, 100], Shape.Square); +} + +test.describe('release from group', () => { + let outterGroupId: string; + + test.beforeEach(async ({ page }) => { + await init(page); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + outterGroupId = await getFirstContainerId(page); + }); + + test('release element from group', async ({ page }) => { + await clickView(page, [50, 50]); + await captureHistory(page); + await triggerComponentToolbarAction(page, 'releaseFromGroup'); + await assertContainerIds(page, { + [outterGroupId]: 2, + null: 2, + }); + await assertContainerChildCount(page, outterGroupId, 2); + await assertSelectedBound(page, [0, 0, 100, 100]); + + // undo the release + await undoByKeyboard(page); + await assertContainerIds(page, { + [outterGroupId]: 3, + null: 1, + }); + await assertContainerChildCount(page, outterGroupId, 3); + await assertSelectedBound(page, [0, 0, 100, 100]); + + // redo the release + await redoByKeyboard(page); + await assertContainerIds(page, { + [outterGroupId]: 2, + null: 2, + }); + await assertContainerChildCount(page, outterGroupId, 2); + await assertSelectedBound(page, [0, 0, 100, 100]); + }); + + test('release group from group', async ({ page }) => { + await clickView(page, [50, 50]); + await shiftClickView(page, [150, 50]); + await triggerComponentToolbarAction(page, 'addGroup'); + await captureHistory(page); + const groupId = await getFirstContainerId(page, [outterGroupId]); + + await assertContainerIds(page, { + [groupId]: 2, + [outterGroupId]: 2, + null: 1, + }); + await assertContainerChildCount(page, groupId, 2); + await assertContainerChildCount(page, outterGroupId, 2); + + // release group from group + await triggerComponentToolbarAction(page, 'releaseFromGroup'); + await assertContainerIds(page, { + [groupId]: 2, + [outterGroupId]: 1, + null: 2, + }); + await assertContainerChildCount(page, outterGroupId, 1); + await assertContainerChildCount(page, groupId, 2); + + // undo the release + await undoByKeyboard(page); + await assertContainerIds(page, { + [groupId]: 2, + [outterGroupId]: 2, + null: 1, + }); + await assertContainerChildCount(page, groupId, 2); + await assertContainerChildCount(page, outterGroupId, 2); + + // redo the release + await redoByKeyboard(page); + await assertContainerIds(page, { + [groupId]: 2, + [outterGroupId]: 1, + null: 2, + }); + await assertContainerChildCount(page, outterGroupId, 1); + await assertContainerChildCount(page, groupId, 2); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/group/title.spec.ts b/blocksuite/tests-legacy/edgeless/group/title.spec.ts new file mode 100644 index 0000000000000..cac0d334ce9ac --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/group/title.spec.ts @@ -0,0 +1,72 @@ +import { expect, type Page } from '@playwright/test'; + +import { + createShapeElement, + dblclickView, + edgelessCommonSetup, + getSelectedBound, + pressEnter, + selectAllByKeyboard, + Shape, + triggerComponentToolbarAction, + type, +} from '../../utils/actions/index.js'; +import { assertEdgelessCanvasText } from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +async function init(page: Page) { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 0], [200, 100], Shape.Square); +} + +test.describe('group title', () => { + test.beforeEach(async ({ page }) => { + await init(page); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + }); + + test('edit group title by component toolbar', async ({ page }) => { + expect(await page.locator('edgeless-group-title-editor').count()).toBe(0); + + await triggerComponentToolbarAction(page, 'renameGroup'); + await page.locator('edgeless-group-title-editor').waitFor({ + state: 'attached', + }); + }); + + test('edit group title by dbclick', async ({ page }) => { + expect(await page.locator('edgeless-group-title-editor').count()).toBe(0); + + const bound = await getSelectedBound(page); + await dblclickView(page, [bound[0] + 10, bound[1] - 10]); + await page.locator('edgeless-group-title-editor').waitFor({ + state: 'attached', + }); + await type(page, 'ABC'); + await assertEdgelessCanvasText(page, 'ABC'); + }); + + test('blur unmount group editor', async ({ page }) => { + const bound = await getSelectedBound(page); + await dblclickView(page, [bound[0] + 10, bound[1] - 10]); + + await page.locator('edgeless-group-title-editor').waitFor({ + state: 'attached', + }); + await page.mouse.click(10, 10); + expect(await page.locator('edgeless-group-title-editor').count()).toBe(0); + }); + + test('enter unmount group editor', async ({ page }) => { + const bound = await getSelectedBound(page); + await dblclickView(page, [bound[0] + 10, bound[1] - 10]); + + await page.locator('edgeless-group-title-editor').waitFor({ + state: 'attached', + }); + await pressEnter(page); + expect(await page.locator('edgeless-group-title-editor').count()).toBe(0); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/lasso.spec.ts b/blocksuite/tests-legacy/edgeless/lasso.spec.ts new file mode 100644 index 0000000000000..d0a74967357d4 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/lasso.spec.ts @@ -0,0 +1,262 @@ +import { sleep } from '@blocksuite/global/utils'; +import { expect } from '@playwright/test'; + +import { + addBasicRectShapeElement, + assertEdgelessTool, + edgelessCommonSetup as commonSetup, + setEdgelessTool, +} from '../utils/actions/edgeless.js'; +import { + dragBetweenCoords, + selectAllByKeyboard, +} from '../utils/actions/index.js'; +import { + assertEdgelessNonSelectedRect, + assertEdgelessSelectedRect, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.skip('lasso tool should deselect when dragging in an empty area', async ({ + page, +}) => { + await commonSetup(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicRectShapeElement(page, start, end); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + await setEdgelessTool(page, 'lasso'); + await assertEdgelessTool(page, 'lasso'); + + await dragBetweenCoords(page, { x: 10, y: 10 }, { x: 15, y: 15 }); + + await assertEdgelessNonSelectedRect(page); +}); + +test.skip('freehand lasso basic test', async ({ page }) => { + await commonSetup(page); + + await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 }); + + await page.mouse.click(10, 10); // deselect + + await setEdgelessTool(page, 'lasso'); + await assertEdgelessTool(page, 'lasso'); + + await assertEdgelessNonSelectedRect(page); + + // simulate a basic lasso selection to select both the rects + const points: [number, number][] = [ + [500, 100], + [500, 500], + [90, 500], + ]; + await page.mouse.move(90, 90); + await page.mouse.down(); + for (const point of points) await page.mouse.move(...point); + await page.mouse.up(); + + await assertEdgelessSelectedRect(page, [100, 100, 200, 200]); +}); + +test.skip('freehand lasso add to selection', async ({ page }) => { + await commonSetup(page); + + await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 }); + + await page.mouse.click(10, 10); // deselect + + await setEdgelessTool(page, 'lasso'); + await assertEdgelessTool(page, 'lasso'); + await assertEdgelessNonSelectedRect(page); + + // some random selection covering the rectangle + let points: [number, number][] = [ + [250, 90], + [250, 300], + [10, 300], + ]; + await page.mouse.move(90, 90); + await page.mouse.down(); + for (const point of points) await page.mouse.move(...point); + await page.mouse.up(); + + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + points = [ + [400, 250], + [400, 450], + [250, 450], + ]; + + await page.keyboard.down('Shift'); // addition selection + await page.mouse.move(250, 250); + await page.mouse.down(); + for (const point of points) await page.mouse.move(...point); + await page.mouse.up(); + + await assertEdgelessSelectedRect(page, [100, 100, 200, 200]); +}); + +test.skip('freehand lasso subtract from selection', async ({ page }) => { + await commonSetup(page); + + await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 }); + await setEdgelessTool(page, 'default'); + + await selectAllByKeyboard(page); + + await setEdgelessTool(page, 'lasso'); + + const points: [number, number][] = [ + [410, 290], + [410, 410], + [290, 410], + ]; + + await page.keyboard.down('Alt'); + + await page.mouse.move(290, 290); + await page.mouse.down(); + for (const point of points) await page.mouse.move(...point); + await page.mouse.up(); + + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); // only the first rectangle should be selected +}); + +test.skip('polygonal lasso basic test', async ({ page }) => { + await commonSetup(page); + await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 }); + await page.mouse.click(10, 10); // deselect + + await assertEdgelessNonSelectedRect(page); + + await setEdgelessTool(page, 'lasso'); + await setEdgelessTool(page, 'lasso'); // switch to polygonal lasso + await sleep(100); + + const points: [number, number][] = [ + [90, 90], + [500, 90], + [500, 500], + [90, 500], + [90, 90], + ]; + + for (const point of points) { + await page.mouse.click(...point); + } + + await assertEdgelessSelectedRect(page, [100, 100, 200, 200]); +}); + +test.skip('polygonal lasso add to selection by holding Shift Key', async ({ + page, +}) => { + await commonSetup(page); + + await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 }); + + await page.mouse.click(10, 10); // deselect + await assertEdgelessNonSelectedRect(page); + + await setEdgelessTool(page, 'lasso'); + await setEdgelessTool(page, 'lasso'); + await sleep(100); + + let points: [number, number][] = [ + [90, 90], + [150, 90], + [150, 150], + [90, 150], + [90, 90], + ]; + + // select the first rectangle + for (const point of points) await page.mouse.click(...point); + + points = [ + [290, 290], + [350, 290], + [350, 350], + [290, 350], + [290, 290], + ]; + + await page.keyboard.down('Shift'); // add to selection + // selects the second rectangle + for (const point of points) await page.mouse.click(...point); + + // by the end both of the rects should be selected + await assertEdgelessSelectedRect(page, [100, 100, 200, 200]); +}); + +test.skip('polygonal lasso subtract from selection by holding Alt', async ({ + page, +}) => { + await commonSetup(page); + + await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 }); + + await selectAllByKeyboard(page); + + const points: [number, number][] = [ + [290, 290], + [350, 290], + [350, 350], + [290, 350], + [290, 290], + ]; + + // switch to polygonal lasso tool + await setEdgelessTool(page, 'lasso'); + await setEdgelessTool(page, 'lasso'); + await sleep(100); + + await page.keyboard.down('Alt'); // subtract from selection + for (const point of points) await page.mouse.click(...point); + + // By the end the second rectangle must be deselected leaving the first rect selection + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); +}); + +test.skip('polygonal lasso should complete selection when clicking the last point', async ({ + page, +}) => { + await commonSetup(page); + + // switch to polygonal lasso + await setEdgelessTool(page, 'lasso'); + await setEdgelessTool(page, 'lasso'); + await sleep(100); + + const lassoPoints: [number, number][] = [ + [100, 100], + [200, 200], + [250, 150], + [100, 100], + ]; + + for (const point of lassoPoints) await page.mouse.click(...point); + + const isSelecting = await page.evaluate(() => { + const edgeless = document.querySelector('affine-edgeless-root'); + if (!edgeless) throw new Error('Missing edgless root block'); + + const curController = edgeless.gfx.tool.currentTool$.peek(); + if (curController?.toolName !== 'lasso') + throw new Error('expected lasso tool controller'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (curController as any)['_isSelecting']; + }); + + expect(isSelecting).toBe(false); +}); diff --git a/blocksuite/tests-legacy/edgeless/linked-doc.spec.ts b/blocksuite/tests-legacy/edgeless/linked-doc.spec.ts new file mode 100644 index 0000000000000..5215ff760bae3 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/linked-doc.spec.ts @@ -0,0 +1,354 @@ +import { assertNotExists } from '@blocksuite/global/utils'; +import { expect } from '@playwright/test'; + +import { + activeNoteInEdgeless, + createConnectorElement, + createNote, + createShapeElement, + edgelessCommonSetup, + getConnectorPath, + locatorComponentToolbarMoreButton, + selectNoteInEdgeless, + Shape, + triggerComponentToolbarAction, +} from '../utils/actions/edgeless.js'; +import { + addBasicBrushElement, + pressEnter, + selectAllByKeyboard, + type, + waitNextFrame, +} from '../utils/actions/index.js'; +import { assertConnectorPath, assertExists } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('note to linked doc', () => { + test('select a note and turn it into a linked doc', async ({ page }) => { + await edgelessCommonSetup(page); + const noteId = await createNote(page, [100, 0], ''); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 200); + await type(page, 'Hello'); + await pressEnter(page); + await type(page, 'World'); + + await page.mouse.click(10, 50); + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc'); + + await waitNextFrame(page, 200); + const embedSyncedBlock = page.locator('affine-embed-synced-doc-block'); + assertExists(embedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + const noteBlock = page.locator('affine-edgeless-note'); + assertExists(noteBlock); + const noteContent = await noteBlock.innerText(); + expect(noteContent).toBe('Hello\nWorld'); + }); + + test('turn note into a linked doc, connector keeps', async ({ page }) => { + await edgelessCommonSetup(page); + const noteId = await createNote(page, [100, 0]); + await createShapeElement(page, [100, 100], [100, 100], Shape.Square); + await createConnectorElement(page, [100, 150], [100, 10]); + const connectorPath = await getConnectorPath(page); + + await page.mouse.click(10, 50); + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc'); + + await waitNextFrame(page, 200); + const embedSyncedBlock = page.locator('affine-embed-synced-doc-block'); + assertExists(embedSyncedBlock); + + await assertConnectorPath(page, [connectorPath[0], connectorPath[1]], 0); + }); + + // TODO FIX ME + test.skip('embed-synced-doc card can not turn into linked doc', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const noteId = await createNote(page, [100, 0]); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 200); + await type(page, 'Hello World'); + + await page.mouse.click(10, 50); + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc'); + + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + const turnButton = page.locator('.turn-into-linked-doc'); + assertNotExists(turnButton); + }); + + // TODO FIX ME + test.skip('embed-linked-doc card can not turn into linked doc', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const noteId = await createNote(page, [100, 0]); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 200); + await type(page, 'Hello World'); + + await page.mouse.click(10, 50); + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc'); + + await triggerComponentToolbarAction(page, 'toCardView'); + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + const turnButton = page.locator('.turn-into-linked-doc'); + assertNotExists(turnButton); + }); +}); + +test.describe('single edgeless element to linked doc', () => { + test('select a shape, turn into a linked doc', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [100, 100], Shape.Square); + + await triggerComponentToolbarAction(page, 'createLinkedDoc'); + await waitNextFrame(page, 200); + const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block'); + assertExists(linkedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + + const shapes = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + return container!.service + .getElementsByType('shape') + .map(s => ({ type: s.type, xywh: s.xywh })); + }); + expect(shapes.length).toBe(1); + expect(shapes[0]).toEqual({ type: 'shape', xywh: '[100,100,100,100]' }); + }); + + test('select a connector, turn into a linked doc', async ({ page }) => { + await edgelessCommonSetup(page); + await createConnectorElement(page, [100, 150], [100, 10]); + const connectorPath = await getConnectorPath(page); + + await triggerComponentToolbarAction(page, 'createLinkedDoc'); + await waitNextFrame(page, 200); + const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block'); + assertExists(linkedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + await assertConnectorPath(page, [connectorPath[0], connectorPath[1]], 0); + }); + + test('select a brush, turn into a linked doc', async ({ page }) => { + await edgelessCommonSetup(page); + const start = { x: 400, y: 400 }; + const end = { x: 500, y: 500 }; + await addBasicBrushElement(page, start, end); + await page.mouse.click(start.x + 5, start.y + 5); + + await triggerComponentToolbarAction(page, 'createLinkedDoc'); + await waitNextFrame(page, 200); + const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block'); + assertExists(linkedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + const brushes = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + return container!.service + .getElementsByType('brush') + .map(s => ({ type: s.type, xywh: s.xywh })); + }); + expect(brushes.length).toBe(1); + }); + + test('select a group, turn into a linked doc', async ({ page }) => { + await edgelessCommonSetup(page); + await createNote(page, [100, 0]); + await createShapeElement(page, [100, 100], [100, 100], Shape.Square); + await createConnectorElement(page, [100, 150], [100, 10]); + const start = { x: 400, y: 400 }; + const end = { x: 500, y: 500 }; + await addBasicBrushElement(page, start, end); + + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + await triggerComponentToolbarAction(page, 'createLinkedDoc'); + await waitNextFrame(page, 200); + const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block'); + assertExists(linkedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + const groups = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + return container!.service.getElementsByType('group').map(s => ({ + type: s.type, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + children: s.childElements.map((c: any) => c.type || c.flavour), + })); + }); + expect(groups.length).toBe(1); + expect(groups[0].children).toContain('affine:note'); + expect(groups[0].children).toContain('shape'); + expect(groups[0].children).toContain('connector'); + expect(groups[0].children).toContain('brush'); + }); + + test('select a frame, turn into a linked doc', async ({ page }) => { + await edgelessCommonSetup(page); + await createNote(page, [100, 0]); + await createShapeElement(page, [100, 100], [100, 100], Shape.Square); + await createConnectorElement(page, [100, 150], [100, 10]); + + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + const start = { x: 400, y: 400 }; + const end = { x: 500, y: 500 }; + await addBasicBrushElement(page, start, end); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addFrame'); + + await triggerComponentToolbarAction(page, 'createLinkedDoc'); + await waitNextFrame(page, 200); + const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block'); + assertExists(linkedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + const nodes = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + const elements = container!.service.elements.map(s => s.type); + const blocks = container!.service.blocks.map(b => b.flavour); + + blocks.sort(); + elements.sort(); + + return { blocks, elements }; + }); + + expect(nodes).toEqual({ + blocks: ['affine:note', 'affine:frame'].sort(), + elements: ['group', 'shape', 'connector', 'brush'].sort(), + }); + }); +}); + +test.describe('multiple edgeless elements to linked doc', () => { + test('multi-select note, frame, shape, connector, brush and group, turn it into a linked doc', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createNote(page, [100, 0], 'Hello World'); + await page.mouse.click(10, 50); + + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addGroup'); + + await createShapeElement(page, [200, 200], [300, 300], Shape.Square); + await createConnectorElement(page, [250, 300], [100, 70]); + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'addFrame'); + + const start = { x: 400, y: 400 }; + const end = { x: 500, y: 500 }; + await addBasicBrushElement(page, start, end); + + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'createLinkedDoc'); + await waitNextFrame(page, 200); + const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block'); + assertExists(linkedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + const nodes = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + const elements = container!.service.elements.map(s => s.type); + const blocks = container!.service.blocks.map(b => b.flavour); + + blocks.sort(); + elements.sort(); + + return { blocks, elements }; + }); + expect(nodes).toEqual({ + blocks: ['affine:frame', 'affine:note'].sort(), + elements: ['shape', 'shape', 'group', 'connector', 'brush'].sort(), + }); + }); + + test('multi-select with embed doc card inside, turn it into a linked doc', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const noteId = await createNote(page, [100, 0], 'Hello World'); + await page.mouse.click(10, 50); + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc'); + + await createShapeElement(page, [100, 100], [100, 100], Shape.Square); + await createConnectorElement(page, [100, 150], [100, 10]); + + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'createLinkedDoc'); + await waitNextFrame(page, 200); + const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block'); + assertExists(linkedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + const nodes = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + const elements = container!.service.elements.map(s => s.type); + const blocks = container!.service.blocks.map(b => b.flavour); + return { blocks, elements }; + }); + + expect(nodes.blocks).toHaveLength(1); + expect(nodes.blocks).toContain('affine:embed-synced-doc'); + + expect(nodes.elements).toHaveLength(2); + expect(nodes.elements).toContain('shape'); + expect(nodes.elements).toContain('connector'); + }); + + test('multi-select with mindmap, turn it into a linked doc', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await triggerComponentToolbarAction(page, 'addMindmap'); + + await selectAllByKeyboard(page); + await triggerComponentToolbarAction(page, 'createLinkedDoc'); + await waitNextFrame(page, 200); + const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block'); + assertExists(linkedSyncedBlock); + + await triggerComponentToolbarAction(page, 'openLinkedDoc'); + await waitNextFrame(page, 200); + const nodes = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + const elements = container!.service.elements.map(s => s.type); + const blocks = container!.service.blocks.map(b => b.flavour); + return { blocks, elements }; + }); + + expect(nodes.blocks).toHaveLength(0); + + expect(nodes.elements).toHaveLength(5); + expect(nodes.elements).toContain('mindmap'); + expect(nodes.elements.filter(el => el === 'shape')).toHaveLength(4); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/lock.spec.ts b/blocksuite/tests-legacy/edgeless/lock.spec.ts new file mode 100644 index 0000000000000..b5e5863aa74f8 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/lock.spec.ts @@ -0,0 +1,559 @@ +import { expect, type Page } from '@playwright/test'; +import { clickView, dblclickView, moveView } from 'utils/actions/click.js'; +import { + createBrushElement, + createConnectorElement, + createEdgelessText, + createFrame, + createMindmap, + createNote as _createNote, + createShapeElement, + deleteAll, + dragBetweenViewCoords, + edgelessCommonSetup, + getContainerChildIds, + getSelectedBound, + getSelectedIds, + getTypeById, + setEdgelessTool, +} from 'utils/actions/edgeless.js'; +import { + copyByKeyboard, + pasteByKeyboard, + pressArrowDown, + pressBackspace, + pressEscape, + pressForwardDelete, + pressTab, + selectAllByKeyboard, + SHORT_KEY, + type, + undoByKeyboard, +} from 'utils/actions/keyboard.js'; +import { waitNextFrame } from 'utils/actions/misc.js'; +import { + assertCanvasElementsCount, + assertEdgelessElementBound, + assertEdgelessSelectedModelRect, + assertRichTexts, +} from 'utils/asserts.js'; + +import { test } from '../utils/playwright.js'; + +test.describe('lock', () => { + const getButtons = (page: Page) => { + const elementToolbar = page.locator('edgeless-element-toolbar-widget'); + return { + lock: elementToolbar.locator('edgeless-lock-button[data-locked="false"]'), + unlock: elementToolbar.locator( + 'edgeless-lock-button[data-locked="true"]' + ), + }; + }; + + async function createNote(page: Page, coord1: number[], content?: string) { + await _createNote(page, coord1, content); + await pressEscape(page, 3); + } + + test('edgeless element can be locked and unlocked', async ({ page }) => { + await edgelessCommonSetup(page); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const wrapTest = async any>( + elementCreateFn: F, + ...args: Parameters + ) => { + await elementCreateFn(...args); + await waitNextFrame(page); + await pressEscape(page); + await selectAllByKeyboard(page); + + const ids = await getSelectedIds(page); + expect(ids).toHaveLength(1); + const type = await getTypeById(page, ids[0]); + const message = `element(${type}) should be able to be (un)locked`; + + const { lock, unlock } = getButtons(page); + + await expect(lock, message).toBeVisible(); + await expect(unlock, message).toBeHidden(); + + await lock.click(); + await expect(lock, message).toBeHidden(); + await expect(unlock, message).toBeVisible(); + + await unlock.click(); + await expect(lock, message).toBeVisible(); + await expect(unlock, message).toBeHidden(); + await deleteAll(page); + await waitNextFrame(page); + }; + + await wrapTest(createBrushElement, page, [100, 100], [150, 150]); + await wrapTest(createConnectorElement, page, [100, 100], [150, 150]); + await wrapTest(createShapeElement, page, [100, 100], [150, 150]); + await wrapTest(createEdgelessText, page, [100, 100]); + await wrapTest(createMindmap, page, [100, 100]); + await wrapTest(createFrame, page, [100, 100], [150, 150]); + await wrapTest(createNote, page, [100, 100]); + + await wrapTest(async () => { + await createShapeElement(page, [100, 100], [150, 150]); + await createShapeElement(page, [150, 150], [200, 200]); + await selectAllByKeyboard(page); + await page.keyboard.press(`${SHORT_KEY}+g`); + }); + }); + + test('locked element should be selectable by clicking or short-cut', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [150, 150]); + await selectAllByKeyboard(page); + + await getButtons(page).lock.click(); + expect(await getSelectedIds(page)).toHaveLength(1); + await pressEscape(page); + expect(await getSelectedIds(page)).toHaveLength(0); + await selectAllByKeyboard(page); + expect(await getSelectedIds(page)).toHaveLength(1); + + await pressEscape(page); + await clickView(page, [125, 125]); + expect(await getSelectedIds(page)).toHaveLength(1); + }); + + test('locked element should not be selectable by dragging default tool or lasso tool. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [150, 150]); + await selectAllByKeyboard(page); + + const { lock, unlock } = getButtons(page); + + await lock.click(); + await pressEscape(page); + await dragBetweenViewCoords(page, [90, 90], [160, 160]); + expect(await getSelectedIds(page)).toHaveLength(0); + + await clickView(page, [125, 125]); + await unlock.click(); + await dragBetweenViewCoords(page, [90, 90], [160, 160]); + expect(await getSelectedIds(page)).toHaveLength(1); + }); + + test('descendant of locked element should not be selectable. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const shapeId = await createShapeElement(page, [100, 100], [150, 150]); + await createShapeElement(page, [150, 150], [200, 200]); + await selectAllByKeyboard(page); + await page.keyboard.press(`${SHORT_KEY}+g`); + const groupId = (await getSelectedIds(page))[0]; + + const { lock, unlock } = getButtons(page); + + await lock.click(); + await pressEscape(page); + await clickView(page, [125, 125]); + expect(await getSelectedIds(page)).toEqual([groupId]); + await clickView(page, [125, 125]); + expect(await getSelectedIds(page)).toEqual([groupId]); + + await unlock.click(); + await clickView(page, [125, 125]); + expect(await getSelectedIds(page)).toEqual([shapeId]); + await pressEscape(page); + + const frameId = await createFrame(page, [50, 50], [250, 250]); + await selectAllByKeyboard(page); + await lock.click(); + await pressEscape(page); + await clickView(page, [125, 125]); + expect(await getSelectedIds(page)).toEqual([frameId]); + await unlock.click(); + await clickView(page, [125, 125]); + expect(await getSelectedIds(page)).toEqual([shapeId]); + }); + + test('the selected rect of locked element should contain descendant. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + // frame + await createFrame(page, [0, 0], [100, 100]); + await createShapeElement(page, [100, 100], [150, 150]); + await dragBetweenViewCoords(page, [125, 125], [95, 95]); // add shape to frame, and partial area out of frame + await selectAllByKeyboard(page); + + await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]); // only frame outline + + const { lock, unlock } = getButtons(page); + + await lock.click(); + await assertEdgelessSelectedModelRect(page, [0, 0, 120, 120]); // frame outline and shape + await pressEscape(page); + await clickView(page, [100, 100]); + await assertEdgelessSelectedModelRect(page, [0, 0, 120, 120]); + + await unlock.click(); + await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]); + await pressEscape(page); + await clickView(page, [100, 100]); + await assertEdgelessSelectedModelRect(page, [70, 70, 50, 50]); + + await deleteAll(page); + + // mindmap + await createMindmap(page, [100, 100]); + const bound = await getSelectedBound(page); + const rootNodePos: [number, number] = [ + bound[0] + 10, + bound[1] + 0.5 * bound[3], + ]; + + await clickView(page, rootNodePos); + const rootNodeBound = await getSelectedBound(page); + + await lock.click(); + await assertEdgelessSelectedModelRect(page, bound); + await clickView(page, rootNodePos); + await assertEdgelessSelectedModelRect(page, bound); + + await unlock.click(); + await assertEdgelessSelectedModelRect(page, bound); + await clickView(page, rootNodePos); + await assertEdgelessSelectedModelRect(page, rootNodeBound); + }); + + test('locked element should be copyable, and the copy is unlocked', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [150, 150]); + await selectAllByKeyboard(page); + + await getButtons(page).lock.click(); + await pressEscape(page); + await clickView(page, [125, 125]); + await copyByKeyboard(page); + await moveView(page, [200, 200]); + await pasteByKeyboard(page); + await clickView(page, [200, 200]); + await expect(getButtons(page).lock).toBeVisible(); + }); + + test('locked element and descendant should not be draggable and moved by arrow key. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const frame = await createFrame(page, [50, 50], [250, 250]); + const shape1 = await createShapeElement(page, [100, 100], [150, 150]); + const shape2 = await createShapeElement(page, [150, 150], [200, 200]); + await selectAllByKeyboard(page); + + await getButtons(page).lock.click(); + await pressEscape(page); + + await dragBetweenViewCoords(page, [100, 100], [150, 150]); + await assertEdgelessElementBound(page, frame, [50, 50, 200, 200]); + await assertEdgelessElementBound(page, shape1, [100, 100, 50, 50]); + await assertEdgelessElementBound(page, shape2, [150, 150, 50, 50]); + + await pressArrowDown(page, 3); + await assertEdgelessElementBound(page, frame, [50, 50, 200, 200]); + await assertEdgelessElementBound(page, shape1, [100, 100, 50, 50]); + await assertEdgelessElementBound(page, shape2, [150, 150, 50, 50]); + + await getButtons(page).unlock.click(); + await dragBetweenViewCoords(page, [100, 100], [150, 150]); + await assertEdgelessElementBound(page, frame, [100, 100, 200, 200]); + await assertEdgelessElementBound(page, shape1, [150, 150, 50, 50]); + await assertEdgelessElementBound(page, shape2, [200, 200, 50, 50]); + + await pressArrowDown(page, 3); + await assertEdgelessElementBound(page, frame, [100, 103, 200, 200]); + await assertEdgelessElementBound(page, shape1, [150, 153, 50, 50]); + await assertEdgelessElementBound(page, shape2, [200, 203, 50, 50]); + }); + + test('locked element should be moved if parent is moved', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const frame = await createFrame(page, [50, 50], [250, 250]); + const shape = await createShapeElement(page, [100, 100], [150, 150]); + await clickView(page, [125, 125]); + await getButtons(page).lock.click(); + + await selectAllByKeyboard(page); + await dragBetweenViewCoords(page, [100, 100], [150, 150]); + + assertEdgelessElementBound(page, frame, [100, 100, 200, 200]); + assertEdgelessElementBound(page, shape, [150, 150, 50, 50]); + }); + + test('locked element should not be scalable and rotatable. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [150, 150]); + await selectAllByKeyboard(page); + + const rect = page.locator('edgeless-selected-rect'); + const { lock, unlock } = getButtons(page); + await expect(rect.locator('.resize')).toHaveCount(8); + await expect(rect.locator('.rotate')).toHaveCount(4); + + await lock.click(); + await expect(rect.locator('.resize')).toHaveCount(0); + await expect(rect.locator('.rotate')).toHaveCount(0); + + await unlock.click(); + await expect(rect.locator('.resize')).toHaveCount(8); + await expect(rect.locator('.rotate')).toHaveCount(4); + }); + + test('locked element should not be editable. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const { lock, unlock } = getButtons(page); + + // Shape + { + await createShapeElement(page, [100, 100], [150, 150]); + await selectAllByKeyboard(page); + await lock.click(); + await dblclickView(page, [125, 125]); + await expect(page.locator('edgeless-shape-text-editor')).toHaveCount(0); + await unlock.click(); + await dblclickView(page, [125, 125]); + await expect(page.locator('edgeless-shape-text-editor')).toHaveCount(1); + await deleteAll(page); + } + + // Connector + { + await createConnectorElement(page, [100, 100], [150, 150]); + await selectAllByKeyboard(page); + await lock.click(); + await dblclickView(page, [125, 125]); + await expect(page.locator('edgeless-connector-label-editor')).toHaveCount( + 0 + ); + await unlock.click(); + await dblclickView(page, [125, 125]); + await expect(page.locator('edgeless-connector-label-editor')).toHaveCount( + 1 + ); + await deleteAll(page); + } + + // Mindmap + { + await createMindmap(page, [100, 100]); + const bound = await getSelectedBound(page); + const rootPos: [number, number] = [ + bound[0] + 10, + bound[1] + 0.5 * bound[3], + ]; + await lock.click(); + await dblclickView(page, rootPos); + await expect(page.locator('edgeless-shape-text-editor')).toHaveCount(0); + await unlock.click(); + await dblclickView(page, rootPos); + await expect(page.locator('edgeless-shape-text-editor')).toHaveCount(1); + await deleteAll(page); + } + + // Edgeless Text + { + await createEdgelessText(page, [100, 100], 'text'); + await selectAllByKeyboard(page); + await lock.click(); + const text = page.locator('affine-edgeless-text'); + await text.dblclick(); + await type(page, '111'); + await expect(text).toHaveText('text'); + await unlock.click(); + await text.dblclick(); + await type(page, '111'); + await expect(text).toHaveText('111'); + await deleteAll(page); + } + + // Note + { + await createNote(page, [100, 100], 'note'); + await selectAllByKeyboard(page); + await lock.click(); + const note = page.locator('affine-edgeless-note'); + await note.dblclick(); + await page.keyboard.press('End'); + await type(page, '111'); + await assertRichTexts(page, ['note']); + await unlock.click(); + await note.dblclick(); + await page.keyboard.press('End'); + await type(page, '111'); + await assertRichTexts(page, ['note111']); + await pressEscape(page, 3); + await deleteAll(page); + } + }); + + test('locked element should not be deletable. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [150, 150]); + await selectAllByKeyboard(page); + const { lock, unlock } = getButtons(page); + + await lock.click(); + await clickView(page, [125, 125]); + await pressBackspace(page); + await assertCanvasElementsCount(page, 1); + await page.keyboard.press('Delete'); + await assertCanvasElementsCount(page, 1); + await pressForwardDelete(page); + await assertCanvasElementsCount(page, 1); + await setEdgelessTool(page, 'eraser'); + await dragBetweenViewCoords(page, [90, 90], [160, 160], { steps: 2 }); + await assertCanvasElementsCount(page, 1); + await setEdgelessTool(page, 'default'); + + await selectAllByKeyboard(page); + await unlock.click(); + await page.evaluate(() => { + window.doc.captureSync(); + }); + await pressBackspace(page); + await assertCanvasElementsCount(page, 0); + await undoByKeyboard(page); + await assertCanvasElementsCount(page, 1); + await page.keyboard.press('Delete'); + await assertCanvasElementsCount(page, 0); + await undoByKeyboard(page); + await assertCanvasElementsCount(page, 1); + await pressForwardDelete(page); + await assertCanvasElementsCount(page, 0); + await undoByKeyboard(page); + await assertCanvasElementsCount(page, 1); + await setEdgelessTool(page, 'eraser'); + await dragBetweenViewCoords(page, [90, 90], [160, 160], { steps: 2 }); + await assertCanvasElementsCount(page, 0); + }); + + test('locked frame should not add new child element. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const frame = await createFrame(page, [50, 50], [250, 250]); + await selectAllByKeyboard(page); + const frameTitle = page.locator('affine-frame-title'); + const { lock, unlock } = getButtons(page); + + await lock.click(); + const shape = await createShapeElement(page, [100, 100], [150, 150]); + + expect(await getContainerChildIds(page, frame)).toHaveLength(0); + + await frameTitle.click(); + await unlock.click(); + expect(await getContainerChildIds(page, frame)).toHaveLength(0); + await clickView(page, [125, 125]); + await dragBetweenViewCoords(page, [125, 125], [130, 130]); // move shape into frame + expect(await getContainerChildIds(page, frame)).toEqual([shape]); + }); + + test('locked mindmap can not create new node by pressing Tab. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createMindmap(page, [100, 100]); + await selectAllByKeyboard(page); + const bound = await getSelectedBound(page); + const rootPos: [number, number] = [ + bound[0] + 10, + bound[1] + 0.5 * bound[3], + ]; + const nodeEditor = page.locator('edgeless-shape-text-editor'); + const { lock, unlock } = getButtons(page); + await lock.click(); + await clickView(page, rootPos); + await pressTab(page); + await expect(nodeEditor).toHaveCount(0); + await unlock.click(); + await clickView(page, rootPos); + await pressTab(page); + await expect(nodeEditor).toHaveCount(1); + await expect(nodeEditor).toHaveText('New node'); + }); + + test('endpoint of locked connector should not be changeable. unlocking will recover', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createConnectorElement(page, [100, 100], [150, 150]); + const handles = page.locator('edgeless-connector-handle'); + await expect(handles).toHaveCount(1); + const { lock, unlock } = getButtons(page); + await lock.click(); + await expect(handles).toHaveCount(0); + await unlock.click(); + await expect(handles).toHaveCount(1); + }); + + test('locking multiple elements will create locked group. unlocking a group will release elements', async ({ + page, + }) => { + await edgelessCommonSetup(page); + const shape1 = await createShapeElement(page, [100, 100], [150, 150]); + const shape2 = await createShapeElement(page, [150, 150], [200, 200]); + await selectAllByKeyboard(page); + const { lock, unlock } = getButtons(page); + await lock.click(); + const group = (await getSelectedIds(page))[0]; + expect(group).not.toBeUndefined(); + expect(await getTypeById(page, group)).toBe('group'); + + await unlock.click(); + expect(await getSelectedIds(page)).toEqual([shape1, shape2]); + }); + + test('locking a group should not create a new group', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [150, 150]); + await createShapeElement(page, [150, 150], [200, 200]); + await selectAllByKeyboard(page); + await page.keyboard.press(`${SHORT_KEY}+g`); + const group = (await getSelectedIds(page))[0]; + await getButtons(page).lock.click(); + expect(await getSelectedIds(page)).toEqual([group]); + }); + + test('unlocking an element should not unlock its locked descendant', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createFrame(page, [50, 50], [250, 250]); + await createShapeElement(page, [150, 150], [200, 200]); + + const { lock, unlock } = getButtons(page); + + await clickView(page, [175, 175]); + await lock.click(); + await page.locator('affine-frame-title').click(); + await lock.click(); + await unlock.click(); + await clickView(page, [175, 175]); + await expect(lock).toBeHidden(); + await expect(unlock).toBeVisible(); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/mindmap.spec.ts b/blocksuite/tests-legacy/edgeless/mindmap.spec.ts new file mode 100644 index 0000000000000..7b1a10c4a41a0 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/mindmap.spec.ts @@ -0,0 +1,126 @@ +import type { MindmapElementModel } from '@blocksuite/affine-model'; +import { expect } from '@playwright/test'; +import { clickView } from 'utils/actions/click.js'; +import { dragBetweenCoords } from 'utils/actions/drag.js'; +import { + addBasicRectShapeElement, + autoFit, + edgelessCommonSetup, + getSelectedBound, + getSelectedBoundCount, + zoomResetByKeyboard, +} from 'utils/actions/edgeless.js'; +import { + pressBackspace, + selectAllByKeyboard, + undoByKeyboard, +} from 'utils/actions/keyboard.js'; +import { waitNextFrame } from 'utils/actions/misc.js'; +import { + assertEdgelessSelectedRect, + assertSelectedBound, +} from 'utils/asserts.js'; + +import { test } from '../utils/playwright.js'; + +test('elements should be selectable after open mindmap menu', async ({ + page, +}) => { + await edgelessCommonSetup(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicRectShapeElement(page, start, end); + + await page.locator('.basket-wrapper').click({ position: { x: 0, y: 0 } }); + await expect(page.locator('edgeless-mindmap-menu')).toBeVisible(); + + await page.mouse.click(start.x + 5, start.y + 5); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); +}); + +test('undo deletion of mindmap should restore the deleted element', async ({ + page, +}) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); + + await page.keyboard.press('m'); + await clickView(page, [0, 0]); + await autoFit(page); + + await selectAllByKeyboard(page); + const mindmapBound = await getSelectedBound(page); + + await pressBackspace(page); + + await selectAllByKeyboard(page); + expect(await getSelectedBoundCount(page)).toBe(0); + + await undoByKeyboard(page); + + await selectAllByKeyboard(page); + await assertSelectedBound(page, mindmapBound); +}); + +test('drag mind map node to reorder the node', async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); + + await page.keyboard.press('m'); + await clickView(page, [0, 0]); + await autoFit(page); + + const { mindmapId, nodeId, nodeRect } = await page.evaluate(() => { + const edgelessBlock = document.querySelector('affine-edgeless-root'); + if (!edgelessBlock) { + throw new Error('edgeless block not found'); + } + const mindmap = edgelessBlock.gfx.gfxElements.filter( + el => 'type' in el && el.type === 'mindmap' + )[0] as MindmapElementModel; + const node = mindmap.tree.children[0].element; + const rect = edgelessBlock.gfx.viewport.toViewBound(node.elementBound); + + edgelessBlock.gfx.selection.set({ elements: [node.id] }); + + return { + mindmapId: mindmap.id, + nodeId: node.id, + nodeRect: { + x: rect.x, + y: rect.y, + w: rect.w, + h: rect.h, + }, + }; + }); + + await waitNextFrame(page, 100); + + await dragBetweenCoords( + page, + { x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 }, + { x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 + 120 }, + { + steps: 50, + } + ); + + const secondNodeId = await page.evaluate( + ({ mindmapId }) => { + const edgelessBlock = document.querySelector('affine-edgeless-root'); + if (!edgelessBlock) { + throw new Error('edgeless block not found'); + } + const mindmap = edgelessBlock.gfx.getElementById( + mindmapId + ) as MindmapElementModel; + + return mindmap.tree.children[1].id; + }, + { mindmapId, nodeId } + ); + + expect(secondNodeId).toEqual(nodeId); +}); diff --git a/blocksuite/tests-legacy/edgeless/note/drag-handle.spec.ts b/blocksuite/tests-legacy/edgeless/note/drag-handle.spec.ts new file mode 100644 index 0000000000000..b83955c303632 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/note/drag-handle.spec.ts @@ -0,0 +1,155 @@ +import { expect } from '@playwright/test'; + +import { + addNote, + dragHandleFromBlockToBlockBottomById, + enterPlaygroundRoom, + focusRichText, + initEmptyEdgelessState, + initThreeParagraphs, + setEdgelessTool, + switchEditorMode, + type, + waitNextFrame, +} from '../../utils/actions/index.js'; +import { assertRectExist, assertRichTexts } from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +const CENTER_X = 450; +const CENTER_Y = 450; + +test('drag handle should be shown when a note is activated in default mode or hidden in other modes', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + await switchEditorMode(page); + const noteBox = await page.locator('affine-edgeless-note').boundingBox(); + if (!noteBox) { + throw new Error('Missing edgeless affine-note'); + } + + const [x, y] = [noteBox.x + 26, noteBox.y + noteBox.height / 2]; + + await page.mouse.move(x, y); + await expect(page.locator('.affine-drag-handle-container')).toBeHidden(); + await page.mouse.dblclick(x, y); + await waitNextFrame(page); + await page.mouse.move(x, y); + + await expect(page.locator('.affine-drag-handle-container')).toBeVisible(); + + await page.mouse.move(0, 0); + await setEdgelessTool(page, 'shape'); + await page.mouse.move(x, y); + await expect(page.locator('.affine-drag-handle-container')).toBeHidden(); + + await page.mouse.move(0, 0); + await setEdgelessTool(page, 'default'); + await page.mouse.move(x, y); + await expect(page.locator('.affine-drag-handle-container')).toBeVisible(); +}); + +test('drag handle can drag note into another note', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + await switchEditorMode(page); + const noteRect = await page + .locator(`[data-block-id="${noteId}"]`) + .boundingBox(); + assertRectExist(noteRect); + + const secondNoteId = await addNote(page, 'hello world', 100, 100); + await waitNextFrame(page); + const secondNoteRect = await page + .locator(`[data-block-id="${secondNoteId}"]`) + .boundingBox(); + assertRectExist(secondNoteRect); + + { + const [x, y] = [ + noteRect.x + noteRect.width / 2, + noteRect.y + noteRect.height / 2, + ]; + await page.mouse.click(noteRect.x, noteRect.y + noteRect.height + 100); + await page.mouse.move(x, y); + await page.mouse.click(x, y); + + const handlerRect = await page + .locator('.affine-drag-handle-container') + .boundingBox(); + assertRectExist(handlerRect); + + await page.mouse.move( + handlerRect.x + handlerRect.width / 2, + handlerRect.y + handlerRect.height / 2 + ); + await page.mouse.down(); + + const [targetX, targetY] = [ + secondNoteRect.x + 10, + secondNoteRect.y + secondNoteRect.height / 2, + ]; + await page.mouse.move(targetX, targetY); + await page.mouse.up(); + + await waitNextFrame(page); + } +}); + +test('drag handle should work inside one note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + + await switchEditorMode(page); + + await page.mouse.dblclick(CENTER_X, CENTER_Y); + await dragHandleFromBlockToBlockBottomById(page, '3', '5'); + await waitNextFrame(page); + await expect(page.locator('affine-drag-handle-container')).toBeHidden(); + await assertRichTexts(page, ['456', '789', '123']); +}); + +test.fixme( + 'drag handle should work across multiple notes', + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await switchEditorMode(page); + + await setEdgelessTool(page, 'note'); + + await page.mouse.click(200, 200); + await focusRichText(page, 3); + await waitNextFrame(page); + + // block id 7 + await type(page, '000'); + + await page.mouse.dblclick(CENTER_X, CENTER_Y - 20); + await dragHandleFromBlockToBlockBottomById(page, '3', '7'); + await expect(page.locator('.affine-drag-handle-container')).toBeHidden(); + await waitNextFrame(page); + await assertRichTexts(page, ['456', '789', '000', '123']); + + // await page.mouse.dblclick(305, 305); + await dragHandleFromBlockToBlockBottomById(page, '3', '4'); + await waitNextFrame(page); + await expect(page.locator('.affine-drag-handle-container')).toBeHidden(); + await assertRichTexts(page, ['456', '123', '789', '000']); + + await expect(page.locator('selected > *')).toHaveCount(0); + } +); diff --git a/blocksuite/tests-legacy/edgeless/note/mode.spec.ts b/blocksuite/tests-legacy/edgeless/note/mode.spec.ts new file mode 100644 index 0000000000000..b3d0cec10a740 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/note/mode.spec.ts @@ -0,0 +1,80 @@ +import { NoteDisplayMode } from '@blocksuite/affine-model'; + +import { + addNote, + changeNoteDisplayModeWithId, + enterPlaygroundRoom, + initEmptyEdgelessState, + switchEditorMode, + zoomResetByKeyboard, +} from '../../utils/actions/index.js'; +import { assertBlockCount } from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test('Note added on doc mode should display on both modes by default', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + // there should be 1 note in doc page + await assertBlockCount(page, 'note', 1); + + await switchEditorMode(page); + // there should be 1 note in edgeless page as well + await assertBlockCount(page, 'edgeless-note', 1); +}); + +test('Note added on edgeless mode should display on edgeless only by default', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await addNote(page, 'note2', 100, 100); + + // assert add note success, there should be 2 notes in edgeless page + await assertBlockCount(page, 'edgeless-note', 2); + + await switchEditorMode(page); + // switch to doc mode, the note added on edgeless mode should not render on doc mode + // there should be only 1 note in doc page + await assertBlockCount(page, 'note', 1); +}); + +test('Note can be changed to display on doc and edgeless mode', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + const noteId = await addNote(page, 'note2', 100, 200); + await page.mouse.click(200, 150); + // assert add note success, there should be 2 notes in edgeless page + await assertBlockCount(page, 'edgeless-note', 2); + + // switch to doc mode + await switchEditorMode(page); + // there should be 1 notes in doc page + await assertBlockCount(page, 'note', 1); + + // switch back to edgeless mode + await switchEditorMode(page); + // change note display mode to doc only + await changeNoteDisplayModeWithId( + page, + noteId, + NoteDisplayMode.DocAndEdgeless + ); + // there should still be 2 notes in edgeless page + await assertBlockCount(page, 'edgeless-note', 2); + + // switch to doc mode + await switchEditorMode(page); + // change successfully, there should be 2 notes in doc page + await assertBlockCount(page, 'note', 2); +}); diff --git a/blocksuite/tests-legacy/edgeless/note/note.spec.ts b/blocksuite/tests-legacy/edgeless/note/note.spec.ts new file mode 100644 index 0000000000000..fb5e422e93230 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/note/note.spec.ts @@ -0,0 +1,531 @@ +import { + DEFAULT_NOTE_HEIGHT, + DEFAULT_NOTE_WIDTH, + NoteDisplayMode, +} from '@blocksuite/affine-model'; +import { expect } from '@playwright/test'; + +import { + activeNoteInEdgeless, + addNote, + assertEdgelessTool, + changeEdgelessNoteBackground, + changeNoteDisplayMode, + locatorComponentToolbar, + locatorEdgelessZoomToolButton, + selectNoteInEdgeless, + setEdgelessTool, + switchEditorMode, + triggerComponentToolbarAction, + zoomOutByKeyboard, + zoomResetByKeyboard, +} from '../../utils/actions/edgeless.js'; +import { + click, + clickBlockById, + dragBetweenCoords, + dragBetweenIndices, + enterPlaygroundRoom, + focusRichText, + focusRichTextEnd, + initEmptyEdgelessState, + initThreeParagraphs, + pressArrowDown, + pressArrowUp, + pressBackspace, + pressEnter, + pressTab, + type, + undoByKeyboard, + waitForInlineEditorStateUpdated, + waitNextFrame, +} from '../../utils/actions/index.js'; +import { + assertBlockChildrenIds, + assertBlockCount, + assertEdgelessNonSelectedRect, + assertEdgelessNoteBackground, + assertEdgelessSelectedRect, + assertExists, + assertNoteSequence, + assertNoteXYWH, + assertRichTextInlineRange, + assertRichTexts, + assertTextSelection, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +const CENTER_X = 450; +const CENTER_Y = 450; + +test('can drag selected non-active note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); + + // selected, non-active + await page.mouse.click(CENTER_X, CENTER_Y); + await dragBetweenCoords( + page, + { x: CENTER_X, y: CENTER_Y }, + { x: CENTER_X, y: CENTER_Y + 100 } + ); + await assertNoteXYWH(page, [0, 100, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); + + await undoByKeyboard(page); + await waitNextFrame(page); + await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); +}); + +test('add Note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await addNote(page, 'hello', 300, 300); + + await assertEdgelessTool(page, 'default'); + await assertRichTexts(page, ['', 'hello']); + await page.mouse.click(300, 200); + await page.mouse.click(350, 320); + await assertEdgelessSelectedRect(page, [ + 270, + 260, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); +}); + +test('add empty Note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await setEdgelessTool(page, 'note'); + // add note at 300,300 + await page.mouse.click(300, 300); + await waitForInlineEditorStateUpdated(page); + // should wait for inline editor update and resizeObserver callback + await waitNextFrame(page); + + // assert add note success + await assertBlockCount(page, 'edgeless-note', 2); + + // click out of note + await page.mouse.click(250, 200); + + // assert empty note is note removed + await page.mouse.move(320, 320); + await assertBlockCount(page, 'edgeless-note', 2); +}); + +test('always keep at least 1 note block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await setEdgelessTool(page, 'default'); + + // clicking in default mode will try to remove empty note block + await page.mouse.click(0, 0); + + const notes = await page.locator('affine-edgeless-note').all(); + expect(notes.length).toEqual(1); +}); + +test('edgeless arrow up/down', async ({ page }) => { + await enterPlaygroundRoom(page); + const { paragraphId, noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 400); + + await type(page, 'aaaaa'); + await pressEnter(page); + await type(page, 'aaaaa'); + await pressEnter(page); + await type(page, 'aaa'); + + await waitForInlineEditorStateUpdated(page); + // 0 for page, 1 for surface, 2 for note, 3 for paragraph + expect(paragraphId).toBe('3'); + await clickBlockById(page, paragraphId); + await assertRichTextInlineRange(page, 0, 5, 0); + + await pressArrowDown(page); + await waitNextFrame(page); + await assertRichTextInlineRange(page, 1, 5, 0); + + await pressArrowUp(page); + await waitNextFrame(page); + await assertRichTextInlineRange(page, 0, 5, 0); + + await pressArrowUp(page); + await waitNextFrame(page); + await assertRichTextInlineRange(page, 0, 0, 0); +}); + +test('dragging un-selected note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + await switchEditorMode(page); + + const noteBox = await page.locator('affine-edgeless-note').boundingBox(); + if (!noteBox) { + throw new Error('Missing edgeless affine-note'); + } + await page.mouse.click(noteBox.x + 5, noteBox.y + 5); + await assertEdgelessSelectedRect(page, [ + noteBox.x, + noteBox.y, + noteBox.width, + noteBox.height, + ]); + + await dragBetweenCoords( + page, + { x: noteBox.x + 10, y: noteBox.y + 15 }, + { x: noteBox.x + 10, y: noteBox.y + 35 }, + { steps: 10 } + ); + + await assertEdgelessSelectedRect(page, [ + noteBox.x, + noteBox.y + 20, + noteBox.width, + noteBox.height, + ]); +}); + +test('format quick bar should show up when double-clicking on text', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + await switchEditorMode(page); + + await page.mouse.dblclick(CENTER_X, CENTER_Y); + await waitNextFrame(page); + + await page + .locator('rich-text') + .nth(1) + .dblclick({ + position: { x: 10, y: 10 }, + delay: 20, + }); + await page.waitForTimeout(200); + const formatBar = page.locator('.affine-format-bar-widget'); + await expect(formatBar).toBeVisible(); +}); + +test('when editing text in edgeless, should hide component toolbar', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + await switchEditorMode(page); + + await selectNoteInEdgeless(page, noteId); + + const toolbar = locatorComponentToolbar(page); + await expect(toolbar).toBeVisible(); + + await page.mouse.click(0, 0); + await activeNoteInEdgeless(page, noteId); + await expect(toolbar).toBeHidden(); +}); + +test('duplicate note should work correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await switchEditorMode(page); + + await selectNoteInEdgeless(page, noteId); + + await triggerComponentToolbarAction(page, 'duplicate'); + await waitNextFrame(page, 200); // wait viewport fit animation + const moreActionsContainer = page.locator('.more-actions-container'); + await expect(moreActionsContainer).toBeHidden(); + + const noteLocator = page.locator('affine-edgeless-note'); + await expect(noteLocator).toHaveCount(2); + const [firstNote, secondNote] = await noteLocator.all(); + + // content should be same + expect(await firstNote.innerText()).toEqual(await secondNote.innerText()); + + // size should be same + const firstNoteBox = await firstNote.boundingBox(); + const secondNoteBox = await secondNote.boundingBox(); + expect(firstNoteBox!.width).toBeCloseTo(secondNoteBox!.width); + expect(firstNoteBox!.height).toBeCloseTo(secondNoteBox!.height); +}); + +test('double click toolbar zoom button, should not add text', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const zoomOutButton = await locatorEdgelessZoomToolButton( + page, + 'zoomOut', + false + ); + await zoomOutButton.dblclick(); + await assertEdgelessNonSelectedRect(page); +}); + +test('change note color', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + await switchEditorMode(page); + + await assertEdgelessNoteBackground( + page, + noteId, + '--affine-note-background-white' + ); + + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'changeNoteColor'); + const color = '--affine-note-background-green'; + await changeEdgelessNoteBackground(page, color); + await assertEdgelessNoteBackground(page, noteId, color); +}); + +test('cursor for active and inactive state', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await pressEnter(page); + await assertRichTexts(page, ['hello', '', '']); + + await switchEditorMode(page); + + await assertTextSelection(page); + await page.mouse.click(CENTER_X, CENTER_Y); + await waitNextFrame(page); + await assertTextSelection(page); + await page.mouse.dblclick(CENTER_X, CENTER_Y); + await waitNextFrame(page); + await assertTextSelection(page, { + blockId: '3', + index: 5, + length: 0, + }); +}); + +test('when no visible note block, clicking in page mode will auto add a new note block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await assertBlockCount(page, 'edgeless-note', 1); + // select note + await selectNoteInEdgeless(page, '2'); + await assertNoteSequence(page, '1'); + await assertBlockCount(page, 'edgeless-note', 1); + // hide note + await triggerComponentToolbarAction(page, 'changeNoteDisplayMode'); + await waitNextFrame(page); + await changeNoteDisplayMode(page, NoteDisplayMode.EdgelessOnly); + + await switchEditorMode(page); + let note = await page.evaluate(() => { + return document.querySelector('affine-note'); + }); + expect(note).toBeNull(); + await click(page, { x: 200, y: 280 }); + + note = await page.evaluate(() => { + return document.querySelector('affine-note'); + }); + expect(note).not.toBeNull(); +}); + +test.fixme( + 'Click at empty note should add a paragraph block', + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, '123'); + await assertRichTexts(page, ['123']); + + await switchEditorMode(page); + + // Drag paragraph out of note block + const paragraphBlock = await page + .locator(`[data-block-id="3"]`) + .boundingBox(); + assertExists(paragraphBlock); + await page.mouse.dblclick(paragraphBlock.x, paragraphBlock.y); + await waitNextFrame(page); + await page.mouse.move( + paragraphBlock.x + paragraphBlock.width / 2, + paragraphBlock.y + paragraphBlock.height / 2 + ); + await waitNextFrame(page); + const handle = await page + .locator('.affine-drag-handle-container') + .boundingBox(); + assertExists(handle); + await page.mouse.move( + handle.x + handle.width / 2, + handle.y + handle.height / 2, + { steps: 10 } + ); + await page.mouse.down(); + await page.mouse.move(100, 200, { steps: 30 }); + await page.mouse.up(); + + // There should be two note blocks and one paragraph block + await assertRichTexts(page, ['123']); + await assertBlockCount(page, 'edgeless-note', 2); + await assertBlockCount(page, 'paragraph', 1); + + // Click at empty note block to add a paragraph block + const emptyNote = await page.locator(`[data-block-id="2"]`).boundingBox(); + assertExists(emptyNote); + await page.mouse.click( + emptyNote.x + emptyNote.width / 2, + emptyNote.y + emptyNote.height / 2 + ); + await waitNextFrame(page, 300); + await type(page, '456'); + await waitNextFrame(page, 400); + + await page.mouse.click(100, 100); + await waitNextFrame(page, 400); + await assertBlockCount(page, 'paragraph', 2); + } +); + +test('Should focus at closest text block when note collapse', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + // Make sure there is no rich text content + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertRichTexts(page, ['']); + + // Select the note + await zoomOutByKeyboard(page); + const notePortalBox = await page + .locator('affine-edgeless-note') + .boundingBox(); + assertExists(notePortalBox); + await page.mouse.click(notePortalBox.x + 10, notePortalBox.y + 10); + await waitNextFrame(page, 200); + const selectedRect = page + .locator('edgeless-selected-rect') + .locator('.affine-edgeless-selected-rect'); + await expect(selectedRect).toBeVisible(); + + // Collapse the note + const selectedBox = await selectedRect.boundingBox(); + assertExists(selectedBox); + await page.mouse.move( + selectedBox.x + selectedBox.width / 2, + selectedBox.y + selectedBox.height + ); + await page.mouse.down(); + await page.mouse.move( + selectedBox.x + selectedBox.width / 2, + selectedBox.y + selectedBox.height + 200, + { steps: 10 } + ); + await page.mouse.up(); + await expect(selectedRect).toBeVisible(); + + // Click at the bottom of note to focus at the closest text block + await page.mouse.click( + selectedBox.x + selectedBox.width / 2, + selectedBox.y + selectedBox.height - 20 + ); + await waitNextFrame(page, 200); + + // Should be enter edit mode and there are no selected rect + await expect(selectedRect).toBeHidden(); + + // Focus at the closest text block and make sure can type + await type(page, 'hello'); + await waitNextFrame(page, 200); + await assertRichTexts(page, ['hello']); +}); + +test('delete first block in edgeless note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); + await page.mouse.dblclick(CENTER_X, CENTER_Y); + + // first block without children, nothing should happen + await assertRichTexts(page, ['']); + await assertBlockChildrenIds(page, '3', []); + await pressBackspace(page); + + await type(page, 'aaa'); + await pressEnter(page); + await type(page, 'bbb'); + await pressTab(page); + await assertRichTexts(page, ['aaa', 'bbb']); + await assertBlockChildrenIds(page, '3', ['4']); + + // first block with children, need to bring children to parent + await focusRichTextEnd(page); + await pressBackspace(page, 3); + await assertRichTexts(page, ['', 'bbb']); + await pressBackspace(page); + await assertRichTexts(page, ['bbb']); + await assertBlockChildrenIds(page, '4', []); +}); + +test('select text cross blocks in edgeless note', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 400); + + await type(page, 'aaa'); + await pressEnter(page); + await type(page, 'bbb'); + await pressEnter(page); + await type(page, 'ccc'); + await assertRichTexts(page, ['aaa', 'bbb', 'ccc']); + + await dragBetweenIndices(page, [0, 1], [2, 2]); + await pressBackspace(page); + await assertRichTexts(page, ['ac']); +}); diff --git a/blocksuite/tests-legacy/edgeless/note/resize.spec.ts b/blocksuite/tests-legacy/edgeless/note/resize.spec.ts new file mode 100644 index 0000000000000..2226e6a1f1dd0 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/note/resize.spec.ts @@ -0,0 +1,254 @@ +import { NOTE_MIN_HEIGHT, NOTE_MIN_WIDTH } from '@blocksuite/affine-model'; +import { expect } from '@playwright/test'; + +import { + activeNoteInEdgeless, + dragBetweenCoords, + enterPlaygroundRoom, + getNoteRect, + initEmptyEdgelessState, + redoByClick, + selectNoteInEdgeless, + setEdgelessTool, + switchEditorMode, + triggerComponentToolbarAction, + type, + undoByClick, + waitForInlineEditorStateUpdated, + waitNextFrame, + zoomResetByKeyboard, +} from '../../utils/actions/index.js'; +import { + assertBlockCount, + assertEdgelessSelectedRect, + assertNoteRectEqual, + assertRectEqual, + assertRichTexts, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test('resize note in edgeless mode', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 400); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + // unselect note + await page.mouse.click(50, 50); + + expect(noteId).toBe('2'); // 0 for page, 1 for surface + await selectNoteInEdgeless(page, noteId); + + const initRect = await getNoteRect(page, noteId); + const leftHandle = page.locator('.handle[aria-label="left"] .resize'); + const box = await leftHandle.boundingBox(); + if (box === null) throw new Error(); + + await dragBetweenCoords( + page, + { x: box.x + 5, y: box.y + 5 }, + { x: box.x - 95, y: box.y + 5 } + ); + const draggedRect = await getNoteRect(page, noteId); + assertRectEqual(draggedRect, { + x: initRect.x - 100, + y: initRect.y, + w: initRect.w + 100, + h: initRect.h, + }); + + await switchEditorMode(page); + await switchEditorMode(page); + const newRect = await getNoteRect(page, noteId); + assertRectEqual(newRect, draggedRect); +}); + +test('resize note then collapse note', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 400); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + // unselect note + await page.mouse.click(50, 50); + + expect(noteId).toBe('2'); // 0 for page, 1 for surface + await selectNoteInEdgeless(page, noteId); + + const initRect = await getNoteRect(page, noteId); + const leftHandle = page.locator('.handle[aria-label="left"] .resize'); + let box = await leftHandle.boundingBox(); + if (box === null) throw new Error(); + + await dragBetweenCoords( + page, + { x: box.x + 50, y: box.y + box.height }, + { x: box.x + 50, y: box.y + box.height + 100 } + ); + let noteRect = await getNoteRect(page, noteId); + await expect(page.locator('.edgeless-note-collapse-button')).toBeVisible(); + assertRectEqual(noteRect, { + x: initRect.x, + y: initRect.y, + w: initRect.w, + h: initRect.h + 100, + }); + + await page.locator('.edgeless-note-collapse-button')!.click(); + let domRect = await page.locator('affine-edgeless-note').boundingBox(); + expect(domRect!.height).toBeCloseTo(NOTE_MIN_HEIGHT); + + await page.locator('.edgeless-note-collapse-button')!.click(); + domRect = await page.locator('affine-edgeless-note').boundingBox(); + expect(domRect!.height).toBeCloseTo(initRect.h + 100); + + await selectNoteInEdgeless(page, noteId); + box = await leftHandle.boundingBox(); + if (box === null) throw new Error(); + await dragBetweenCoords( + page, + { x: box.x + 50, y: box.y + box.height }, + { x: box.x + 50, y: box.y + box.height - 150 } + ); + noteRect = await getNoteRect(page, noteId); + await expect( + page.locator('.edgeless-note-collapse-button') + ).not.toBeVisible(); + assertRectEqual(noteRect, { + x: initRect.x, + y: initRect.y, + w: initRect.w, + h: NOTE_MIN_HEIGHT, + }); + + await switchEditorMode(page); + await switchEditorMode(page); + const newRect = await getNoteRect(page, noteId); + assertRectEqual(newRect, noteRect); +}); + +test('resize note then auto size and custom size', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 400); + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + // unselect note + await page.mouse.click(50, 50); + await selectNoteInEdgeless(page, noteId); + + const initRect = await getNoteRect(page, noteId); + const bottomRightResize = page.locator( + '.handle[aria-label="bottom-right"] .resize' + ); + const box = await bottomRightResize.boundingBox(); + if (box === null) throw new Error(); + + await dragBetweenCoords( + page, + { x: box.x + 5, y: box.y + 5 }, + { x: box.x + 5, y: box.y + 105 } + ); + + const draggedRect = await getNoteRect(page, noteId); + assertRectEqual(draggedRect, { + x: initRect.x, + y: initRect.y, + w: initRect.w, + h: initRect.h + 100, + }); + + await triggerComponentToolbarAction(page, 'autoSize'); + await waitNextFrame(page, 200); + const autoSizeRect = await getNoteRect(page, noteId); + assertRectEqual(autoSizeRect, initRect); + + await triggerComponentToolbarAction(page, 'autoSize'); + await waitNextFrame(page, 200); + await assertNoteRectEqual(page, noteId, draggedRect); + + await undoByClick(page); + await page.mouse.click(50, 50); + await waitNextFrame(page, 200); + await assertNoteRectEqual(page, noteId, initRect); + + await redoByClick(page); + await waitNextFrame(page, 200); + await assertNoteRectEqual(page, noteId, draggedRect); +}); + +test('drag to add customized size note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await setEdgelessTool(page, 'note'); + // add note at 300,300 + await page.mouse.move(300, 300); + await page.mouse.down(); + await page.mouse.move(900, 600, { steps: 10 }); + await page.mouse.up(); + // should wait for inline editor update and resizeObserver callback + await waitForInlineEditorStateUpdated(page); + + // assert add note success + await assertBlockCount(page, 'edgeless-note', 2); + + // click out of note + await page.mouse.click(250, 200); + // click on note to select it + await page.mouse.click(600, 500); + // assert selected note + // note add on edgeless mode will have a offsetX of 30 and offsetY of 40 + await assertEdgelessSelectedRect(page, [270, 260, 600, 300]); +}); + +test('drag to add customized size note: should clamp to min width and min height', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await setEdgelessTool(page, 'note'); + + // add note at 300,300 + await page.mouse.move(300, 300); + await page.mouse.down(); + await page.mouse.move(400, 360, { steps: 10 }); + await page.mouse.up(); + await waitNextFrame(page); + + await waitNextFrame(page); + + // should wait for inline editor update and resizeObserver callback + await waitForInlineEditorStateUpdated(page); + // assert add note success + await assertBlockCount(page, 'edgeless-note', 2); + + // click out of note + await page.mouse.click(250, 200); + // click on note to select it + await page.mouse.click(320, 300); + // assert selected note + // note add on edgeless mode will have a offsetX of 30 and offsetY of 40 + await assertEdgelessSelectedRect(page, [ + 270, + 260, + NOTE_MIN_WIDTH, + NOTE_MIN_HEIGHT, + ]); +}); diff --git a/blocksuite/tests-legacy/edgeless/note/scale.spec.ts b/blocksuite/tests-legacy/edgeless/note/scale.spec.ts new file mode 100644 index 0000000000000..97c65157f9909 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/note/scale.spec.ts @@ -0,0 +1,146 @@ +import { expect, type Page } from '@playwright/test'; +import { + addNote, + locatorScalePanelButton, + selectNoteInEdgeless, + switchEditorMode, + triggerComponentToolbarAction, + zoomResetByKeyboard, +} from 'utils/actions/edgeless.js'; +import { + copyByKeyboard, + pasteByKeyboard, + selectAllByKeyboard, +} from 'utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + initEmptyEdgelessState, + waitNextFrame, +} from 'utils/actions/misc.js'; +import { assertRectExist } from 'utils/asserts.js'; +import { test } from 'utils/playwright.js'; + +async function setupAndAddNote(page: Page) { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + const noteId = await addNote(page, 'hello world', 100, 200); + await page.mouse.click(0, 0); + return noteId; +} + +async function openScalePanel(page: Page, noteId: string) { + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'changeNoteScale'); + await waitNextFrame(page); + const scalePanel = page.locator('edgeless-scale-panel'); + await expect(scalePanel).toBeVisible(); + return scalePanel; +} + +async function checkNoteScale( + page: Page, + noteId: string, + expectedScale: number, + expectedType: 'equal' | 'greater' | 'less' = 'equal' +) { + const edgelessNote = page.locator( + `affine-edgeless-note[data-block-id="${noteId}"]` + ); + const noteContainer = edgelessNote.locator('.edgeless-note-container'); + const style = await noteContainer.getAttribute('style'); + + if (!style) { + throw new Error('Style attribute not found'); + } + + const scaleMatch = style.match(/transform:\s*scale\(([\d.]+)\)/); + if (!scaleMatch) { + throw new Error('Scale transform not found in style'); + } + + const actualScale = parseFloat(scaleMatch[1]); + + switch (expectedType) { + case 'equal': + expect(actualScale).toBeCloseTo(expectedScale, 2); + break; + case 'greater': + expect(actualScale).toBeGreaterThan(expectedScale); + break; + case 'less': + expect(actualScale).toBeLessThan(expectedScale); + } +} + +test.describe('note scale', () => { + test('Note scale can be changed by scale panel button', async ({ page }) => { + const noteId = await setupAndAddNote(page); + await openScalePanel(page, noteId); + + const scale150 = locatorScalePanelButton(page, 50); + await scale150.click(); + + await checkNoteScale(page, noteId, 0.5); + }); + + test('Note scale can be changed by scale panel input', async ({ page }) => { + const noteId = await setupAndAddNote(page); + const scalePanel = await openScalePanel(page, noteId); + + const scaleInput = scalePanel.locator('.scale-input'); + await scaleInput.click(); + await page.keyboard.type('50'); + await page.keyboard.press('Enter'); + + await checkNoteScale(page, noteId, 0.5); + }); + + test('Note scale input support copy paste', async ({ page }) => { + const noteId = await setupAndAddNote(page); + const scalePanel = await openScalePanel(page, noteId); + + const scaleInput = scalePanel.locator('.scale-input'); + await scaleInput.click(); + await page.keyboard.type('50'); + await selectAllByKeyboard(page); + await copyByKeyboard(page); + await page.mouse.click(0, 0); + + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'changeNoteScale'); + await waitNextFrame(page); + + await scaleInput.click(); + await pasteByKeyboard(page); + await page.keyboard.press('Enter'); + + await checkNoteScale(page, noteId, 0.5); + }); + + test('Note scale can be changed by shift drag', async ({ page }) => { + const noteId = await setupAndAddNote(page); + await selectNoteInEdgeless(page, noteId); + + const edgelessNote = page.locator( + `affine-edgeless-note[data-block-id="${noteId}"]` + ); + const noteRect = await edgelessNote.boundingBox(); + assertRectExist(noteRect); + await page.mouse.move( + noteRect.x + noteRect.width, + noteRect.y + noteRect.height + ); + await page.keyboard.down('Shift'); + await page.mouse.down(); + await page.mouse.move( + noteRect.x + noteRect.width * 2, + noteRect.y + noteRect.height * 2 + ); + await page.mouse.up(); + + // expect style scale to be greater than 1 + await checkNoteScale(page, noteId, 1, 'greater'); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/note/slicer.spec.ts b/blocksuite/tests-legacy/edgeless/note/slicer.spec.ts new file mode 100644 index 0000000000000..4b40031d9c8d5 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/note/slicer.spec.ts @@ -0,0 +1,156 @@ +import { expect } from '@playwright/test'; + +import { + enterPlaygroundRoom, + initEmptyEdgelessState, + initSixParagraphs, + initThreeParagraphs, + selectNoteInEdgeless, + switchEditorMode, + triggerComponentToolbarAction, +} from '../../utils/actions/index.js'; +import { assertRectExist, assertRichTexts } from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test.describe('note slicer', () => { + test('could enable and disenable note slicer', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await initSixParagraphs(page); + + await switchEditorMode(page); + await selectNoteInEdgeless(page, noteId); + // note slicer button should not be visible when note slicer setting is disenabled + await expect(page.locator('.note-slicer-button')).toBeHidden(); + await expect(page.locator('.note-slicer-dividing-line')).toHaveCount(0); + + await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting'); + // note slicer button should be visible when note slicer setting is enabled + await expect(page.locator('.note-slicer-button')).toBeVisible(); + await expect(page.locator('.note-slicer-dividing-line')).toHaveCount(5); + }); + + test('note slicer will add new note', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await initSixParagraphs(page); + + await switchEditorMode(page); + await expect(page.locator('affine-edgeless-note')).toHaveCount(1); + + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting'); + await expect(page.locator('.note-slicer-button')).toBeVisible(); + + await page.locator('.note-slicer-button').click(); + + await expect(page.locator('affine-edgeless-note')).toHaveCount(2); + }); + + test('note slicer button should appears at right position', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await switchEditorMode(page); + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting'); + + const blocks = await page + .locator(`[data-block-id="${noteId}"] [data-block-id]`) + .all(); + expect(blocks.length).toBe(3); + + const firstBlockRect = await blocks[0].boundingBox(); + assertRectExist(firstBlockRect); + const secondBlockRect = await blocks[1].boundingBox(); + assertRectExist(secondBlockRect); + await page.mouse.move( + secondBlockRect.x + 1, + secondBlockRect.y + secondBlockRect.height / 2 + ); + + let slicerButtonRect = await page + .locator('.note-slicer-button') + .boundingBox(); + assertRectExist(slicerButtonRect); + + let buttonRectMiddle = slicerButtonRect.y + slicerButtonRect.height / 2; + + expect(buttonRectMiddle).toBeGreaterThan( + firstBlockRect.y + firstBlockRect.height + ); + expect(buttonRectMiddle).toBeGreaterThan(secondBlockRect.y); + + const thirdBlockRect = await blocks[2].boundingBox(); + assertRectExist(thirdBlockRect); + await page.mouse.move( + thirdBlockRect.x + 1, + thirdBlockRect.y + thirdBlockRect.height / 2 + ); + + slicerButtonRect = await page.locator('.note-slicer-button').boundingBox(); + assertRectExist(slicerButtonRect); + + buttonRectMiddle = slicerButtonRect.y + slicerButtonRect.height / 2; + expect(buttonRectMiddle).toBeGreaterThan( + secondBlockRect.y + secondBlockRect.height + ); + expect(buttonRectMiddle).toBeLessThan(thirdBlockRect.y); + }); + + test('note slicer button should appears at right position when editor is not located at left top corner', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await switchEditorMode(page); + await selectNoteInEdgeless(page, noteId); + + await page.evaluate(() => { + const el = document.createElement('div'); + const app = document.querySelector('#app') as HTMLElement; + + el.style.height = '100px'; + el.style.background = 'red'; + + app!.style.paddingLeft = '80px'; + + document.body.insertBefore(el, app); + }); + + const blocks = await page + .locator(`[data-block-id="${noteId}"] [data-block-id]`) + .all(); + expect(blocks.length).toBe(3); + + const firstBlockRect = await blocks[0].boundingBox(); + assertRectExist(firstBlockRect); + const secondBlockRect = await blocks[1].boundingBox(); + assertRectExist(secondBlockRect); + + await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting'); + await page.mouse.move( + secondBlockRect.x + 1, + secondBlockRect.y + secondBlockRect.height / 2 + ); + + const slicerButtonRect = await page + .locator('.note-slicer-button') + .boundingBox(); + assertRectExist(slicerButtonRect); + + const buttonRectMiddle = slicerButtonRect.y + slicerButtonRect.height / 2; + + expect(buttonRectMiddle).toBeGreaterThan( + firstBlockRect.y + firstBlockRect.height + ); + expect(buttonRectMiddle).toBeGreaterThan(secondBlockRect.y); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/note/undo-redo.spec.ts b/blocksuite/tests-legacy/edgeless/note/undo-redo.spec.ts new file mode 100644 index 0000000000000..48c5e5129a260 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/note/undo-redo.spec.ts @@ -0,0 +1,140 @@ +import { expect } from '@playwright/test'; + +import { + activeNoteInEdgeless, + click, + copyByKeyboard, + countBlock, + dragBetweenCoords, + enterPlaygroundRoom, + fillLine, + focusRichText, + getNoteRect, + initEmptyEdgelessState, + initSixParagraphs, + pasteByKeyboard, + redoByClick, + redoByKeyboard, + selectNoteInEdgeless, + switchEditorMode, + triggerComponentToolbarAction, + type, + undoByClick, + undoByKeyboard, + waitNextFrame, + zoomResetByKeyboard, +} from '../../utils/actions/index.js'; +import { assertRectEqual } from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test('undo/redo should work correctly after clipping', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await initSixParagraphs(page); + + await switchEditorMode(page); + await expect(page.locator('affine-edgeless-note')).toHaveCount(1); + + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting'); + + const button = page.locator('.note-slicer-button'); + await button.click(); + await expect(page.locator('affine-edgeless-note')).toHaveCount(2); + + await undoByKeyboard(page); + await waitNextFrame(page); + await expect(page.locator('affine-edgeless-note')).toHaveCount(1); + await redoByKeyboard(page); + await waitNextFrame(page); + await expect(page.locator('affine-edgeless-note')).toHaveCount(2); +}); + +test('undo/redo should work correctly after resizing', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await activeNoteInEdgeless(page, noteId); + await waitNextFrame(page, 400); + // current implementation may be a little inefficient + await fillLine(page, true); + await page.mouse.click(0, 0); + await waitNextFrame(page, 400); + await selectNoteInEdgeless(page, noteId); + + const initRect = await getNoteRect(page, noteId); + const rightHandle = page.locator('.handle[aria-label="right"] .resize'); + const box = await rightHandle.boundingBox(); + if (box === null) throw new Error(); + + await dragBetweenCoords( + page, + { x: box.x + 5, y: box.y + 5 }, + { x: box.x + 105, y: box.y + 5 } + ); + const draggedRect = await getNoteRect(page, noteId); + assertRectEqual(draggedRect, { + x: initRect.x, + y: initRect.y, + w: initRect.w + 100, + h: draggedRect.h, // not assert `h` here + }); + expect(draggedRect.h).toBe(initRect.h); + + await undoByKeyboard(page); + await waitNextFrame(page); + const undoRect = await getNoteRect(page, noteId); + assertRectEqual(undoRect, initRect); + + await redoByKeyboard(page); + await waitNextFrame(page); + const redoRect = await getNoteRect(page, noteId); + assertRectEqual(redoRect, draggedRect); +}); + +test('continuous undo and redo (note block add operation) should work', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await switchEditorMode(page); + await click(page, { x: 260, y: 450 }); + await copyByKeyboard(page); + + let count = await countBlock(page, 'affine-edgeless-note'); + expect(count).toBe(1); + + await page.mouse.move(100, 100); + await pasteByKeyboard(page, false); + await waitNextFrame(page, 1000); + + await page.mouse.move(200, 200); + await pasteByKeyboard(page, false); + await waitNextFrame(page, 1000); + + await page.mouse.move(300, 300); + await pasteByKeyboard(page, false); + await waitNextFrame(page, 1000); + + count = await countBlock(page, 'affine-edgeless-note'); + expect(count).toBe(4); + + await undoByClick(page); + count = await countBlock(page, 'affine-edgeless-note'); + expect(count).toBe(3); + + await undoByClick(page); + count = await countBlock(page, 'affine-edgeless-note'); + expect(count).toBe(2); + + await redoByClick(page); + count = await countBlock(page, 'affine-edgeless-note'); + expect(count).toBe(3); + + await redoByClick(page); + count = await countBlock(page, 'affine-edgeless-note'); + expect(count).toBe(4); +}); diff --git a/blocksuite/tests-legacy/edgeless/pan.spec.ts b/blocksuite/tests-legacy/edgeless/pan.spec.ts new file mode 100644 index 0000000000000..1b8cf2b465de2 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/pan.spec.ts @@ -0,0 +1,263 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +import { + activeNoteInEdgeless, + addNote, + assertEdgelessTool, + locatorEdgelessToolButton, + multiTouchDown, + multiTouchMove, + multiTouchUp, + setEdgelessTool, + switchEditorMode, +} from '../utils/actions/edgeless.js'; +import { + addBasicRectShapeElement, + dragBetweenCoords, + enterPlaygroundRoom, + initEmptyEdgelessState, + toggleEditorReadonly, + type, + waitForInlineEditorStateUpdated, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertEdgelessSelectedRect, + assertNotHasClass, + assertRichTexts, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('pan tool basic', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicRectShapeElement(page, start, end); + + await setEdgelessTool(page, 'pan'); + await dragBetweenCoords( + page, + { + x: start.x + 5, + y: start.y + 5, + }, + { + x: start.x + 25, + y: start.y + 25, + } + ); + await setEdgelessTool(page, 'default'); + + await page.mouse.click(start.x + 25, start.y + 25); + await assertEdgelessSelectedRect(page, [120, 120, 100, 100]); +}); + +test('pan tool shortcut', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicRectShapeElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y + 5); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + await page.keyboard.down('Space'); + await assertEdgelessTool(page, 'pan'); + + await dragBetweenCoords( + page, + { + x: start.x + 5, + y: start.y + 5, + }, + { + x: start.x + 25, + y: start.y + 25, + } + ); + + await page.keyboard.up('Space'); + await assertEdgelessSelectedRect(page, [120, 120, 100, 100]); +}); + +// FIXME(@doouding): Failed on CI +test.skip('pan tool with middle button', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicRectShapeElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y + 5); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + await dragBetweenCoords( + page, + { + x: 400, + y: 400, + }, + { + x: 420, + y: 420, + }, + { + button: 'middle', + } + ); + + await assertEdgelessTool(page, 'default'); + await assertEdgelessSelectedRect(page, [120, 120, 100, 100]); +}); + +test('pan tool shortcut should revert to the previous tool on keyup', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await page.mouse.click(100, 100); + + await setEdgelessTool(page, 'brush'); + { + await page.keyboard.down('Space'); + await assertEdgelessTool(page, 'pan'); + + await page.keyboard.up('Space'); + await assertEdgelessTool(page, 'brush'); + } +}); + +test('pan tool shortcut does not affect other tools while using the tool', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + // Test if while drawing shortcut does not switch to pan tool + await setEdgelessTool(page, 'brush'); + await dragBetweenCoords( + page, + { x: 100, y: 110 }, + { x: 200, y: 300 }, + { + click: true, + beforeMouseUp: async () => { + await page.keyboard.down('Space'); + await assertEdgelessTool(page, 'brush'); + }, + } + ); + + await setEdgelessTool(page, 'eraser'); + await dragBetweenCoords( + page, + { x: 100, y: 110 }, + { x: 200, y: 300 }, + { + click: true, + beforeMouseUp: async () => { + await page.keyboard.down('Space'); + await assertEdgelessTool(page, 'eraser'); + }, + } + ); + // Maybe add other tools too +}); + +test('pan tool shortcut when user is editing', async ({ page }) => { + await enterPlaygroundRoom(page); + const ids = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await setEdgelessTool(page, 'default'); + + await activeNoteInEdgeless(page, ids.noteId); + await waitForInlineEditorStateUpdated(page); + + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + await page.keyboard.down('Space'); + const defaultButton = await locatorEdgelessToolButton(page, 'pan', false); + await assertNotHasClass(defaultButton, 'pan'); + await waitNextFrame(page); +}); + +test.describe('pan tool in readonly mode', () => { + async function setupReadonlyEdgeless(page: Page) { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const noteId = await addNote(page, 'hello world', 100, 200); + await page.mouse.click(50, 100); + + const edgelessNote = page.locator( + `affine-edgeless-note[data-block-id="${noteId}"]` + ); + const originalBoundingBox = await edgelessNote.boundingBox(); + expect(originalBoundingBox).not.toBeNull(); + const { x: originalX, y: originalY } = originalBoundingBox!; + + // Toggle readonly mode + await toggleEditorReadonly(page); + await page.waitForTimeout(100); + + return { edgelessNote, originalX, originalY }; + } + + async function assertPanned( + edgelessNote: Locator, + originalX: number, + originalY: number + ) { + const newBoundingBox = await edgelessNote.boundingBox(); + expect(newBoundingBox).not.toBeNull(); + const { x: newX, y: newY } = newBoundingBox!; + + expect(newX).toBeGreaterThan(originalX); + expect(newY).toBeGreaterThan(originalY); + } + + test('can be used by keyboard', async ({ page }) => { + const { edgelessNote, originalX, originalY } = + await setupReadonlyEdgeless(page); + + await page.keyboard.down('Space'); + await assertEdgelessTool(page, 'pan'); + + // Pan the viewport + await dragBetweenCoords(page, { x: 300, y: 300 }, { x: 400, y: 400 }); + + await assertPanned(edgelessNote, originalX, originalY); + }); + + test('can be used by multi-touch', async ({ page }) => { + const { edgelessNote, originalX, originalY } = + await setupReadonlyEdgeless(page); + + // Pan the viewport using multi-touch + const from = [ + { x: 300, y: 300 }, + { x: 400, y: 300 }, + ]; + const to = [ + { x: 350, y: 350 }, + { x: 450, y: 350 }, + ]; + await multiTouchDown(page, from); + await multiTouchMove(page, from, to); + await multiTouchUp(page, to); + + await assertPanned(edgelessNote, originalX, originalY); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/paste-block.spec.ts b/blocksuite/tests-legacy/edgeless/paste-block.spec.ts new file mode 100644 index 0000000000000..280ba6954032d --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/paste-block.spec.ts @@ -0,0 +1,127 @@ +import { expect, type Page } from '@playwright/test'; +import { + click, + copyByKeyboard, + enterPlaygroundRoom, + focusRichText, + getAllEdgelessNoteIds, + getAllEdgelessTextIds, + getNoteBoundBoxInEdgeless, + initEmptyEdgelessState, + pasteByKeyboard, + pasteTestImage, + pressEnter, + pressEnterWithShortkey, + pressEscape, + selectAllByKeyboard, + setEdgelessTool, + switchEditorMode, + type, +} from 'utils/actions/index.js'; + +import { test } from '../utils/playwright.js'; + +test.describe('pasting blocks', () => { + const initContent = async (page: Page) => { + // Text + await type(page, 'hello'); + await pressEnter(page); + // Image + await pasteTestImage(page); + await pressEnter(page); + // Text + await type(page, 'world'); + await pressEnter(page); + // code + await type(page, '``` '); + await type(page, 'code'); + await pressEnterWithShortkey(page); + }; + test('pasting a note block', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await focusRichText(page); + await initContent(page); + await switchEditorMode(page); + const box = await getNoteBoundBoxInEdgeless(page, noteId); + await click(page, { + x: box.x + 10, + y: box.y + 10, + }); + await copyByKeyboard(page); + await pasteByKeyboard(page); + // not equal to noteId + const noteIds = await getAllEdgelessNoteIds(page); + expect(noteIds.length).toBe(2); + expect(noteIds[0]).toBe(noteId); + const newNoteId = noteIds[1]; + const newNote = page.locator( + `affine-edgeless-note[data-block-id="${newNoteId}"]` + ); + await expect(newNote).toBeVisible(); + const blocks = newNote.locator('[data-block-id]'); + await expect(blocks.nth(0)).toContainText('hello'); + await expect(blocks.nth(1).locator('.resizable-img')).toBeVisible(); + await expect(blocks.nth(2)).toContainText('world'); + await expect(blocks.nth(3)).toContainText('code'); + }); + test('pasting a edgeless block', async ({ page }) => { + await enterPlaygroundRoom(page, { + flags: { + enable_edgeless_text: true, + }, + }); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140, { + delay: 100, + }); + await initContent(page); + await pressEscape(page, 3); + await page.mouse.click(130, 140); + await copyByKeyboard(page); + await pasteByKeyboard(page); + const textIds = await getAllEdgelessTextIds(page); + expect(textIds.length).toBe(2); + const newTextId = textIds[1]; + const newText = page.locator( + `affine-edgeless-text[data-block-id="${newTextId}"]` + ); + await expect(newText).toBeVisible(); + const blocks = newText.locator('[data-block-id]'); + await expect(blocks.nth(0)).toContainText('hello'); + await expect(blocks.nth(1).locator('.resizable-img')).toBeVisible(); + await expect(blocks.nth(2)).toContainText('world'); + await expect(blocks.nth(3)).toContainText('code'); + }); + + test('pasting a note block from doc mode', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello world'); + + await selectAllByKeyboard(page); + await copyByKeyboard(page); + + await switchEditorMode(page); + await click(page, { + x: 100, + y: 100, + }); + await pasteByKeyboard(page); + + // not equal to noteId + const noteIds = await getAllEdgelessNoteIds(page); + expect(noteIds.length).toBe(2); + + const newNoteId = noteIds[1]; + const newNote = page.locator( + `affine-edgeless-note[data-block-id="${newNoteId}"]` + ); + await expect(newNote).toBeVisible(); + const blocks = newNote.locator('[data-block-id]'); + await expect(blocks.nth(0)).toContainText('hello world'); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/presentation.spec.ts b/blocksuite/tests-legacy/edgeless/presentation.spec.ts new file mode 100644 index 0000000000000..0531f724b4bf0 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/presentation.spec.ts @@ -0,0 +1,249 @@ +import { expect } from '@playwright/test'; +import { + assertEdgelessTool, + createFrame, + createNote, + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup, + enterPresentationMode, + locatorPresentationToolbarButton, + setEdgelessTool, + Shape, + toggleFramePanel, +} from 'utils/actions/edgeless.js'; +import { + copyByKeyboard, + pasteByKeyboard, + pressEscape, + selectAllBlocksByKeyboard, +} from 'utils/actions/keyboard.js'; +import { waitNextFrame } from 'utils/actions/misc.js'; + +import { test } from '../utils/playwright.js'; + +test.describe('presentation', () => { + test('should render note when enter presentation mode', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + await createNote(page, [300, 100], 'hello'); + + // Frame shape + await setEdgelessTool(page, 'frame'); + await dragBetweenViewCoords(page, [80, 80], [220, 220]); + await waitNextFrame(page, 100); + + // Frame note + await setEdgelessTool(page, 'frame'); + await dragBetweenViewCoords(page, [240, 0], [800, 200]); + + expect(await page.locator('affine-frame').count()).toBe(2); + + await enterPresentationMode(page); + await waitNextFrame(page, 100); + + const nextButton = locatorPresentationToolbarButton(page, 'next'); + await nextButton.click(); + const edgelessNote = page.locator('affine-edgeless-note'); + await expect(edgelessNote).toBeVisible(); + + const prevButton = locatorPresentationToolbarButton(page, 'previous'); + await prevButton.click(); + await expect(edgelessNote).toBeHidden(); + + await waitNextFrame(page, 300); + await nextButton.click(); + await expect(edgelessNote).toBeVisible(); + }); + + test('should exit presentation mode when press escape', async ({ page }) => { + await edgelessCommonSetup(page); + await createNote(page, [300, 100], 'hello'); + + // Frame note + await setEdgelessTool(page, 'frame'); + await dragBetweenViewCoords(page, [240, 0], [800, 200]); + + expect(await page.locator('affine-frame').count()).toBe(1); + + await enterPresentationMode(page); + await waitNextFrame(page, 300); + + await assertEdgelessTool(page, 'frameNavigator'); + const navigatorBlackBackground = page.locator( + '.edgeless-navigator-black-background' + ); + await expect(navigatorBlackBackground).toBeVisible(); + + await pressEscape(page); + await waitNextFrame(page, 100); + + await assertEdgelessTool(page, 'default'); + await expect(navigatorBlackBackground).toBeHidden(); + }); + + test('should be able to adjust order of presentation in toolbar', async ({ + page, + }) => { + await edgelessCommonSetup(page); + + await createFrame(page, [100, 100], [100, 200]); + await createFrame(page, [200, 100], [300, 200]); + await createFrame(page, [300, 100], [400, 200]); + await createFrame(page, [400, 100], [500, 200]); + + await enterPresentationMode(page); + + await page.locator('.edgeless-frame-order-button').click(); + const frameItems = page.locator( + 'edgeless-frame-order-menu .item.draggable' + ); + const dragIndicators = page.locator( + 'edgeless-frame-order-menu .drag-indicator' + ); + + await expect(frameItems).toHaveCount(4); + await expect(frameItems.nth(0)).toHaveText('Frame 1'); + await expect(frameItems.nth(1)).toHaveText('Frame 2'); + await expect(frameItems.nth(2)).toHaveText('Frame 3'); + await expect(frameItems.nth(3)).toHaveText('Frame 4'); + + // 1 2 3 4 + await frameItems.nth(2).dragTo(dragIndicators.nth(0)); + // 3 1 2 4 + await frameItems.nth(3).dragTo(dragIndicators.nth(2)); + // 3 1 4 2 + await frameItems.nth(1).dragTo(dragIndicators.nth(3)); + // 3 4 1 2 + + await expect(frameItems).toHaveCount(4); + await expect(frameItems.nth(0)).toHaveText('Frame 3'); + await expect(frameItems.nth(1)).toHaveText('Frame 4'); + await expect(frameItems.nth(2)).toHaveText('Frame 1'); + await expect(frameItems.nth(3)).toHaveText('Frame 2'); + + const currentFrame = page.locator('.edgeless-frame-navigator-title'); + const nextButton = locatorPresentationToolbarButton(page, 'next'); + + await expect(currentFrame).toHaveText('Frame 3'); + await nextButton.click(); + await expect(currentFrame).toHaveText('Frame 4'); + await nextButton.click(); + await expect(currentFrame).toHaveText('Frame 1'); + await nextButton.click(); + await expect(currentFrame).toHaveText('Frame 2'); + }); + + test('should be able to adjust order of presentation in frame panel', async ({ + page, + }) => { + await edgelessCommonSetup(page); + + await createFrame(page, [100, 100], [100, 200]); + await createFrame(page, [200, 100], [300, 200]); + await createFrame(page, [300, 100], [400, 200]); + await createFrame(page, [400, 100], [500, 200]); + + // await enterPresentationMode(page); + + await toggleFramePanel(page); + + // await page.locator('.edgeless-frame-order-button').click(); + const frameCards = page.locator('affine-frame-card .frame-card-body'); + const frameTitles = page.locator('affine-frame-card-title .card-title'); + + await expect(frameTitles).toHaveCount(4); + await expect(frameTitles.nth(0)).toHaveText('Frame 1'); + await expect(frameTitles.nth(1)).toHaveText('Frame 2'); + await expect(frameTitles.nth(2)).toHaveText('Frame 3'); + await expect(frameTitles.nth(3)).toHaveText('Frame 4'); + + const drag = async (from: number, to: number) => { + const startBBox = await frameCards.nth(from).boundingBox(); + expect(startBBox).not.toBeNull(); + if (startBBox === null) return; + + const endBBox = await frameTitles.nth(to).boundingBox(); + expect(endBBox).not.toBeNull(); + if (endBBox === null) return; + + await page.mouse.move( + startBBox.x + startBBox.width / 2, + startBBox.y + startBBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(endBBox.x + endBBox.width / 2, endBBox.y, { + steps: 2, + }); + await page.mouse.up(); + }; + + // 1 2 3 4 + await drag(2, 0); + // 3 1 2 4 + await drag(3, 2); + // 3 1 4 2 + await drag(1, 3); + // 3 4 1 2 + + await expect(frameTitles).toHaveCount(4); + await expect(frameTitles.nth(0)).toHaveText('Frame 3'); + await expect(frameTitles.nth(1)).toHaveText('Frame 4'); + await expect(frameTitles.nth(2)).toHaveText('Frame 1'); + await expect(frameTitles.nth(3)).toHaveText('Frame 2'); + + await enterPresentationMode(page); + await page.locator('.edgeless-frame-order-button').click(); + const frameItems = page.locator( + 'edgeless-frame-order-menu .item.draggable' + ); + + await expect(frameItems).toHaveCount(4); + await expect(frameItems.nth(0)).toHaveText('Frame 3'); + await expect(frameItems.nth(1)).toHaveText('Frame 4'); + await expect(frameItems.nth(2)).toHaveText('Frame 1'); + await expect(frameItems.nth(3)).toHaveText('Frame 2'); + + const currentFrame = page.locator('.edgeless-frame-navigator-title'); + const nextButton = locatorPresentationToolbarButton(page, 'next'); + + await expect(currentFrame).toHaveText('Frame 3'); + await nextButton.click(); + await expect(currentFrame).toHaveText('Frame 4'); + await nextButton.click(); + await expect(currentFrame).toHaveText('Frame 1'); + await nextButton.click(); + await expect(currentFrame).toHaveText('Frame 2'); + }); + + test('duplicate frames should keep the presentation orders', async ({ + page, + }) => { + await edgelessCommonSetup(page); + + await createFrame(page, [100, 100], [100, 200]); + await createFrame(page, [200, 100], [300, 200]); + await createFrame(page, [300, 100], [400, 200]); + await createFrame(page, [400, 100], [500, 200]); + + await selectAllBlocksByKeyboard(page); + await copyByKeyboard(page); + await pasteByKeyboard(page); + + await enterPresentationMode(page); + await page.locator('.edgeless-frame-order-button').click(); + const frameItems = page.locator( + 'edgeless-frame-order-menu .item.draggable' + ); + + await expect(frameItems).toHaveCount(8); + await expect(frameItems.nth(0)).toHaveText('Frame 1'); + await expect(frameItems.nth(1)).toHaveText('Frame 2'); + await expect(frameItems.nth(2)).toHaveText('Frame 3'); + await expect(frameItems.nth(3)).toHaveText('Frame 4'); + await expect(frameItems.nth(4)).toHaveText('Frame 1'); + await expect(frameItems.nth(5)).toHaveText('Frame 2'); + await expect(frameItems.nth(6)).toHaveText('Frame 3'); + await expect(frameItems.nth(7)).toHaveText('Frame 4'); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/reordering.spec.ts b/blocksuite/tests-legacy/edgeless/reordering.spec.ts new file mode 100644 index 0000000000000..999f80cf54f9a --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/reordering.spec.ts @@ -0,0 +1,448 @@ +import { + DEFAULT_NOTE_HEIGHT, + DEFAULT_NOTE_WIDTH, +} from '@blocksuite/affine-model'; +import { expect, type Page } from '@playwright/test'; + +import { + createShapeElement, + edgelessCommonSetup, + getFirstContainerId, + getSelectedBound, + getSortedIds, + initThreeOverlapFilledShapes, + initThreeOverlapNotes, + Shape, + shiftClickView, + switchEditorMode, + triggerComponentToolbarAction, + zoomResetByKeyboard, +} from '../utils/actions/edgeless.js'; +import { + captureHistory, + clickView, + enterPlaygroundRoom, + initEmptyEdgelessState, + redoByKeyboard, + undoByKeyboard, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertEdgelessSelectedRect, + assertSelectedBound, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('reordering', () => { + test.describe('group index', () => { + let sortedIds: string[]; + + async function init(page: Page) { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [100, 0], [200, 100], Shape.Square); + await createShapeElement(page, [200, 0], [300, 100], Shape.Square); + await createShapeElement(page, [300, 0], [400, 100], Shape.Square); + sortedIds = await getSortedIds(page); + } + + test('group', async ({ page }) => { + await init(page); + await clickView(page, [50, 50]); + await shiftClickView(page, [150, 50]); + await triggerComponentToolbarAction(page, 'addGroup'); + const groupId = await getFirstContainerId(page); + const currentSortedIds = await getSortedIds(page); + + expect(currentSortedIds).toEqual([ + ...sortedIds.slice(2), + groupId, + ...sortedIds.slice(0, 2), + ]); + }); + + test('release from group', async ({ page }) => { + await init(page); + await clickView(page, [50, 50]); + await shiftClickView(page, [150, 50]); + await triggerComponentToolbarAction(page, 'addGroup'); + const groupId = await getFirstContainerId(page); + await clickView(page, [50, 50]); + await triggerComponentToolbarAction(page, 'releaseFromGroup'); + const currentSortedIds = await getSortedIds(page); + const releasedShapeId = sortedIds[0]; + + expect(currentSortedIds).toEqual([ + ...sortedIds.slice(2), + groupId, + sortedIds[1], + releasedShapeId, + ]); + }); + + test('ungroup', async ({ page }) => { + await init(page); + await clickView(page, [50, 50]); + await shiftClickView(page, [150, 50]); + await triggerComponentToolbarAction(page, 'addGroup'); + await triggerComponentToolbarAction(page, 'ungroup'); + const currentSortedIds = await getSortedIds(page); + const ungroupedIds = [sortedIds[0], sortedIds[1]]; + + expect(currentSortedIds).toEqual([ + ...sortedIds.filter(id => !ungroupedIds.includes(id)), + ...ungroupedIds, + ]); + }); + }); + + test.describe('reordering shapes', () => { + async function init(page: Page) { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await initThreeOverlapFilledShapes(page); + await page.mouse.click(0, 0); + } + + test('bring to front', async ({ page }) => { + await init(page); + + // should be rect2 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [160, 160, 100, 100]); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be rect1 + await page.mouse.click(150, 150); + await assertEdgelessSelectedRect(page, [130, 130, 100, 100]); + + // should be rect0 + await page.mouse.click(110, 130); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + // bring rect0 to front + await triggerComponentToolbarAction(page, 'bringToFront'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be rect0 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + }); + + test('bring forward', async ({ page }) => { + await init(page); + + // should be rect0 + await page.mouse.click(120, 120); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + // bring rect0 forward + await triggerComponentToolbarAction(page, 'bringForward'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be rect0 + await page.mouse.click(150, 150); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + }); + + test('send backward', async ({ page }) => { + await init(page); + + // should be rect2 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [160, 160, 100, 100]); + + // bring rect2 backward + await triggerComponentToolbarAction(page, 'sendBackward'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be rect1 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [130, 130, 100, 100]); + }); + + test('send to back', async ({ page }) => { + await init(page); + + // should be rect2 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [160, 160, 100, 100]); + + // bring rect2 to back + await triggerComponentToolbarAction(page, 'sendToBack'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be rect1 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [130, 130, 100, 100]); + + // send rect1 to back + await triggerComponentToolbarAction(page, 'sendToBack'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be rect0 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + }); + + test('undo and redo', async ({ page }) => { + await init(page); + + // should be rect2 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [160, 160, 100, 100]); + + // send rect2 to back + await triggerComponentToolbarAction(page, 'sendToBack'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be rect1 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [130, 130, 100, 100]); + + // undo + await undoByKeyboard(page); + + // clear selection + await page.mouse.click(50, 50); + + // should be rect2 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [160, 160, 100, 100]); + + // redo + await redoByKeyboard(page); + + // clear selection + await page.mouse.click(50, 50); + + // should be rect2 + await page.mouse.click(180, 180); + await assertEdgelessSelectedRect(page, [130, 130, 100, 100]); + }); + }); + + test.describe('reordering notes', () => { + async function init(page: Page) { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await initThreeOverlapNotes(page); + await waitNextFrame(page); + await page.mouse.click(0, 0); + } + + test('bring to front', async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); + await initThreeOverlapNotes(page, 130, 190); + await waitNextFrame(page); + // click outside to clear selection + await page.mouse.click(50, 100); + // should be note2 + await page.mouse.click(180, 200); + const bound = await getSelectedBound(page); + + await assertSelectedBound(page, bound); + + await clickView(page, [bound[0] - 15, bound[1] + 10]); + bound[0] -= 30; + await assertSelectedBound(page, bound); + + await clickView(page, [bound[0] - 15, bound[1] + 10]); + bound[0] -= 30; + await assertSelectedBound(page, bound); + + // bring note0 to front + await triggerComponentToolbarAction(page, 'bringToFront'); + // clear + await page.mouse.click(100, 50); + // should be note0 + await clickView(page, [bound[0] + 40, bound[1] + 10]); + await assertSelectedBound(page, bound); + }); + + test('bring forward', async ({ page }) => { + await init(page); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be note0 + await page.mouse.click(120, 140); + await assertEdgelessSelectedRect(page, [ + 100, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + + // bring note0 forward + await triggerComponentToolbarAction(page, 'bringForward'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be rect0 + await page.mouse.click(150, 140); + await assertEdgelessSelectedRect(page, [ + 100, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + }); + + test('send backward', async ({ page }) => { + await init(page); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be note2 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 160, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + + // bring note2 backward + await triggerComponentToolbarAction(page, 'sendBackward'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be note1 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 130, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + }); + + test('send to back', async ({ page }) => { + await init(page); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be note2 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 160, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + + // bring note2 to back + await triggerComponentToolbarAction(page, 'sendToBack'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be note1 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 130, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + + // send note1 to back + await triggerComponentToolbarAction(page, 'sendToBack'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be note0 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 100, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + }); + + test('undo and redo', async ({ page }) => { + await init(page); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be note2 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 160, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + + await captureHistory(page); + + // bring note2 to back + await triggerComponentToolbarAction(page, 'sendToBack'); + + // click outside to clear selection + await page.mouse.click(50, 50); + + // should be note1 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 130, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + + // undo + await undoByKeyboard(page); + // clear selection + await page.mouse.click(50, 50); + // should be note2 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 160, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + + // redo + await redoByKeyboard(page); + // clear selection + await page.mouse.click(50, 50); + // should be note1 + await page.mouse.click(180, 140); + await assertEdgelessSelectedRect(page, [ + 130, + 100, + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + ]); + }); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/resizing.spec.ts b/blocksuite/tests-legacy/edgeless/resizing.spec.ts new file mode 100644 index 0000000000000..46fd70e5d33f9 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/resizing.spec.ts @@ -0,0 +1,199 @@ +import { + switchEditorMode, + zoomResetByKeyboard, +} from '../utils/actions/edgeless.js'; +import { + addBasicBrushElement, + addBasicRectShapeElement, + dragBetweenCoords, + enterPlaygroundRoom, + initEmptyEdgelessState, + resizeElementByHandle, +} from '../utils/actions/index.js'; +import { + assertEdgelessSelectedReactCursor, + assertEdgelessSelectedRect, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('resizing shapes and aspect ratio will be maintained', () => { + test('positive adjustment', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await page.mouse.click(110, 110); + await assertEdgelessSelectedRect(page, [98, 98, 104, 104]); + + await addBasicRectShapeElement( + page, + { x: 210, y: 110 }, + { x: 310, y: 210 } + ); + await page.mouse.click(220, 120); + await assertEdgelessSelectedRect(page, [210, 110, 100, 100]); + + await dragBetweenCoords(page, { x: 120, y: 90 }, { x: 220, y: 130 }); + await assertEdgelessSelectedRect(page, [98, 98, 212, 112]); + + await resizeElementByHandle(page, { x: 50, y: 50 }); + await assertEdgelessSelectedRect(page, [148, 124.19, 162, 85.81]); + + await page.mouse.move(160, 160); + await assertEdgelessSelectedRect(page, [148, 124.19, 162, 85.81]); + + await page.mouse.move(260, 160); + await assertEdgelessSelectedRect(page, [148, 124.19, 162, 85.81]); + }); + + test('negative adjustment', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await page.mouse.click(110, 110); + await assertEdgelessSelectedRect(page, [98, 98, 104, 104]); + + await addBasicRectShapeElement( + page, + { x: 210, y: 110 }, + { x: 310, y: 210 } + ); + await page.mouse.click(220, 120); + await assertEdgelessSelectedRect(page, [210, 110, 100, 100]); + + await dragBetweenCoords(page, { x: 120, y: 90 }, { x: 220, y: 130 }); + await assertEdgelessSelectedRect(page, [98, 98, 212, 112]); + + await resizeElementByHandle(page, { x: 400, y: 300 }, 'top-left', 30); + await assertEdgelessSelectedRect(page, [310, 210, 356, 188]); + + await page.mouse.move(450, 300); + await assertEdgelessSelectedRect(page, [310, 210, 356, 188]); + + await page.mouse.move(320, 220); + await assertEdgelessSelectedRect(page, [310, 210, 356, 188]); + }); +}); + +test.describe('cursor style', () => { + test('editor is aligned at the start of viewport', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await addBasicRectShapeElement( + page, + { x: 200, y: 200 }, + { x: 300, y: 300 } + ); + await page.mouse.click(250, 250); + await assertEdgelessSelectedRect(page, [200, 200, 100, 100]); + + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top', + cursor: 'ns-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'right', + cursor: 'ew-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom', + cursor: 'ns-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'left', + cursor: 'ew-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top-left', + cursor: 'nwse-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top-right', + cursor: 'nesw-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom-left', + cursor: 'nesw-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom-right', + cursor: 'nwse-resize', + }); + }); + + test('editor is not aligned at the start of viewport', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await page.addStyleTag({ + content: 'body { padding: 100px 150px; }', + }); + + await addBasicRectShapeElement( + page, + { x: 200, y: 200 }, + { x: 300, y: 300 } + ); + await page.mouse.click(250, 250); + await assertEdgelessSelectedRect(page, [200, 200, 100, 100]); + + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top', + cursor: 'ns-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'right', + cursor: 'ew-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom', + cursor: 'ns-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'left', + cursor: 'ew-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top-left', + cursor: 'nwse-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top-right', + cursor: 'nesw-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom-left', + cursor: 'nesw-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom-right', + cursor: 'nwse-resize', + }); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/rotation.spec.ts b/blocksuite/tests-legacy/edgeless/rotation.spec.ts new file mode 100644 index 0000000000000..49bab473fc52b --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/rotation.spec.ts @@ -0,0 +1,227 @@ +import { + addBasicRectShapeElement, + dragBetweenCoords, + enterPlaygroundRoom, + initEmptyEdgelessState, + resizeElementByHandle, + rotateElementByHandle, + switchEditorMode, +} from '../utils/actions/index.js'; +import { + assertEdgelessSelectedReactCursor, + assertEdgelessSelectedRect, + assertEdgelessSelectedRectRotation, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('rotation', () => { + test('angle adjustment by four corners', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + + await rotateElementByHandle(page, 45, 'top-left'); + await assertEdgelessSelectedRectRotation(page, 45); + + await rotateElementByHandle(page, 45, 'top-right'); + await assertEdgelessSelectedRectRotation(page, 90); + + await rotateElementByHandle(page, 45, 'bottom-right'); + await assertEdgelessSelectedRectRotation(page, 135); + + await rotateElementByHandle(page, 45, 'bottom-left'); + await assertEdgelessSelectedRectRotation(page, 180); + }); + + test('angle snap', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + + await page.keyboard.down('Shift'); + + await rotateElementByHandle(page, 5); + await assertEdgelessSelectedRectRotation(page, 0); + + await rotateElementByHandle(page, 10); + await assertEdgelessSelectedRectRotation(page, 15); + + await rotateElementByHandle(page, 10); + await assertEdgelessSelectedRectRotation(page, 30); + + await rotateElementByHandle(page, 10); + await assertEdgelessSelectedRectRotation(page, 45); + + await rotateElementByHandle(page, 5); + await assertEdgelessSelectedRectRotation(page, 45); + + await page.keyboard.up('Shift'); + }); + + test('single shape', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + await rotateElementByHandle(page, 45, 'top-right'); + await assertEdgelessSelectedRectRotation(page, 45); + }); + + test('multiple shapes', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + await addBasicRectShapeElement( + page, + { x: 200, y: 100 }, + { x: 300, y: 200 } + ); + + await dragBetweenCoords(page, { x: 90, y: 90 }, { x: 310, y: 110 }); + await assertEdgelessSelectedRect(page, [100, 100, 200, 100]); + + await rotateElementByHandle(page, 90, 'bottom-right'); + await assertEdgelessSelectedRectRotation(page, 0); + await assertEdgelessSelectedRect(page, [150, 50, 100, 200]); + }); + + test('combination with resizing', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + await rotateElementByHandle(page, 90, 'bottom-left'); + await assertEdgelessSelectedRectRotation(page, 90); + + await resizeElementByHandle(page, { x: 10, y: -10 }, 'bottom-right'); + await assertEdgelessSelectedRect(page, [110, 100, 90, 90]); + + await rotateElementByHandle(page, -90, 'bottom-right'); + await assertEdgelessSelectedRectRotation(page, 0); + + await resizeElementByHandle(page, { x: 10, y: 10 }, 'bottom-right'); + await assertEdgelessSelectedRect(page, [110, 100, 100, 100]); + }); + + test('combination with resizing for multiple shapes', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + await addBasicRectShapeElement( + page, + { x: 200, y: 100 }, + { x: 300, y: 200 } + ); + + await dragBetweenCoords(page, { x: 90, y: 90 }, { x: 310, y: 110 }); + await assertEdgelessSelectedRect(page, [100, 100, 200, 100]); + + await rotateElementByHandle(page, 90, 'bottom-left'); + await assertEdgelessSelectedRectRotation(page, 0); + await assertEdgelessSelectedRect(page, [150, 50, 100, 200]); + + await resizeElementByHandle(page, { x: -10, y: -20 }, 'bottom-right'); + await assertEdgelessSelectedRect(page, [150, 50, 90, 180]); + + await rotateElementByHandle(page, -90, 'bottom-right'); + await assertEdgelessSelectedRectRotation(page, 0); + await assertEdgelessSelectedRect(page, [105, 95, 180, 90]); + + await resizeElementByHandle(page, { x: 20, y: 10 }, 'bottom-right'); + await assertEdgelessSelectedRect(page, [105, 95, 200, 100]); + }); +}); + +test.describe('cursor style', () => { + test('update resize cursor direction after rotating', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + + await rotateElementByHandle(page, 45, 'top-left'); + await assertEdgelessSelectedRectRotation(page, 45); + + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top', + cursor: 'nesw-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'right', + cursor: 'nwse-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom', + cursor: 'nesw-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'left', + cursor: 'nwse-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top-right', + cursor: 'ew-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'top-left', + cursor: 'ns-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom-right', + cursor: 'ns-resize', + }); + await assertEdgelessSelectedReactCursor(page, { + mode: 'resize', + handle: 'bottom-left', + cursor: 'ew-resize', + }); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/selection/connector.spec.ts b/blocksuite/tests-legacy/edgeless/selection/connector.spec.ts new file mode 100644 index 0000000000000..f0f7a351e0572 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/selection/connector.spec.ts @@ -0,0 +1,94 @@ +import { expect } from '@playwright/test'; + +import * as actions from '../../utils/actions/edgeless.js'; +import { + addBasicConnectorElement, + createConnectorElement, + createShapeElement, + dragBetweenCoords, + enterPlaygroundRoom, + initEmptyEdgelessState, + Shape, + switchEditorMode, + toModelCoord, + waitNextFrame, +} from '../../utils/actions/index.js'; +import { test } from '../../utils/playwright.js'; + +test.describe('select multiple connectors', () => { + test('should show single selection rect', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + await addBasicConnectorElement( + page, + { x: 100, y: 200 }, + { x: 300, y: 200 } + ); + await addBasicConnectorElement( + page, + { x: 100, y: 230 }, + { x: 300, y: 230 } + ); + await addBasicConnectorElement( + page, + { x: 100, y: 260 }, + { x: 300, y: 260 } + ); + + await dragBetweenCoords(page, { x: 50, y: 50 }, { x: 400, y: 290 }); + await waitNextFrame(page); + + expect( + await page + .locator('.affine-edgeless-selected-rect') + .locator('.element-handle') + .count() + ).toBe(0); + }); + + test('should disable resize when a connector is already connected', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + const start = await toModelCoord(page, [100, 0]); + const end = await toModelCoord(page, [200, 100]); + await createShapeElement(page, start, end, Shape.Diamond); + const c1 = await toModelCoord(page, [200, 50]); + const c2 = await toModelCoord(page, [450, 50]); + await createConnectorElement(page, c1, c2); + + await addBasicConnectorElement( + page, + { x: 250, y: 200 }, + { x: 450, y: 200 } + ); + await addBasicConnectorElement( + page, + { x: 250, y: 230 }, + { x: 450, y: 230 } + ); + await addBasicConnectorElement( + page, + { x: 250, y: 260 }, + { x: 450, y: 260 } + ); + + await dragBetweenCoords(page, { x: 500, y: 20 }, { x: 400, y: 290 }); + await waitNextFrame(page); + + const selectedRectLocalor = page.locator('.affine-edgeless-selected-rect'); + expect(await selectedRectLocalor.locator('.element-handle').count()).toBe( + 0 + ); + expect( + await selectedRectLocalor.locator('.handle').locator('.resize').count() + ).toBe(0); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/selection/keyboard.spec.ts b/blocksuite/tests-legacy/edgeless/selection/keyboard.spec.ts new file mode 100644 index 0000000000000..a7690fa1f419b --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/selection/keyboard.spec.ts @@ -0,0 +1,265 @@ +import { NoteDisplayMode } from '@blocksuite/affine-model'; +import { expect } from '@playwright/test'; + +import * as actions from '../../utils/actions/edgeless.js'; +import { + addNote, + changeNoteDisplayModeWithId, + setEdgelessTool, + zoomResetByKeyboard, +} from '../../utils/actions/edgeless.js'; +import { + addBasicBrushElement, + addBasicRectShapeElement, + dragBetweenCoords, + enterPlaygroundRoom, + initEmptyEdgelessState, + selectAllByKeyboard, + switchEditorMode, +} from '../../utils/actions/index.js'; +import { + assertEdgelessDraggingArea, + assertEdgelessNonSelectedRect, + assertEdgelessSelectedElementHandleCount, + assertEdgelessSelectedRect, + assertVisibleBlockCount, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test.describe('translation should constrain to cur axis when dragged with shift key', () => { + test('constrain-x', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + + await page.mouse.move(110, 110); + await page.mouse.down(); + + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + await page.keyboard.down('Shift'); + await page.mouse.move(110, 200); // constrain to y + await page.mouse.move(300, 200); // constrain to x + await assertEdgelessSelectedRect(page, [290, 100, 100, 100]); // y should remain same as constrained to x + }); + + test('constrain-y', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + + await page.mouse.move(110, 110); + await page.mouse.down(); + + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + await page.keyboard.down('Shift'); + await page.mouse.move(200, 110); // constrain to x + await page.mouse.move(200, 300); // constrain to y + await assertEdgelessSelectedRect(page, [100, 290, 100, 100]); // x should remain same as constrained to y + }); +}); + +test('select multiple shapes and press "Escape" to cancel selection', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await page.mouse.click(110, 110); + await assertEdgelessSelectedRect(page, [98, 98, 104, 104]); + + await addBasicRectShapeElement(page, { x: 210, y: 110 }, { x: 310, y: 210 }); + await page.mouse.click(220, 120); + await assertEdgelessSelectedRect(page, [210, 110, 100, 100]); + + // Select both shapes + await dragBetweenCoords(page, { x: 90, y: 90 }, { x: 320, y: 220 }); + + // assert all shapes are selected + await assertEdgelessSelectedRect(page, [98, 98, 212, 112]); + + // Press "Escape" to cancel the selection + await page.keyboard.press('Escape'); + + await assertEdgelessNonSelectedRect(page); +}); + +test('should move selection drag area when holding spaceBar', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + await setEdgelessTool(page, 'default'); + + // Click to start the initial dragging area + await page.mouse.click(100, 100); + + const initialX = 100, + initialY = 100; + const finalX = 300, + finalY = 300; + + await dragBetweenCoords( + page, + { x: initialX, y: initialY }, + { x: finalX, y: finalY }, + { + beforeMouseUp: async () => { + await page.keyboard.down('Space'); + + const dx = 100, + dy = 100; + await page.mouse.move(finalX + dx, finalY + dy); + await assertEdgelessDraggingArea(page, [ + initialX + dx, + initialY + dy, + // width and height should be same + finalX - initialX, + finalY - initialY, + ]); + + await page.keyboard.up('Space'); + }, + } + ); +}); + +test('selection drag-area start should be same when space is pressed again', async ({ + page, +}) => { + //? This test is to check whether there is any flicker or jump when using the space again in the same selection + + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + // Make the selection out side the rect and move the selection to the rect + await dragBetweenCoords( + page, + // Make the selection not selecting the rect + { x: 100, y: 100 }, + { x: 200, y: 200 }, + { + beforeMouseUp: async () => { + await page.keyboard.down('Space'); + // Move the selection over to the rect + await page.mouse.move(300, 300); + + let draggingArea = page.locator('.affine-edgeless-dragging-area'); + const firstBound = await draggingArea.boundingBox(); + + await page.keyboard.up('Space'); + + await page.mouse.move(400, 400); + await page.keyboard.down('Space'); + + await page.mouse.move(410, 410); + await page.mouse.move(400, 400); + + draggingArea = page.locator('.affine-edgeless-dragging-area'); + const newBound = await draggingArea.boundingBox(); + + expect(firstBound).not.toBe(null); + expect(newBound).not.toBe(null); + + const { x: fx, y: fy } = firstBound!; + const { x: nx, y: ny } = newBound!; + + expect([fx, fy]).toStrictEqual([nx, ny]); + }, + } + ); +}); + +test('should be able to update selection dragging area after releasing space', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + await setEdgelessTool(page, 'default'); + + // Click to start the initial dragging area + await page.mouse.click(100, 100); + + const initialX = 100, + initialY = 100; + const finalX = 300, + finalY = 300; + + await dragBetweenCoords( + page, + { x: initialX, y: initialY }, + { x: finalX, y: finalY }, + { + beforeMouseUp: async () => { + await page.keyboard.down('Space'); + + const dx = 100, + dy = 100; + + // Move the mouse to simulate dragging with spaceBar held + await page.mouse.move(finalX + dx, finalY + dy); + + await page.keyboard.up('Space'); + // scale after moving + const dSx = 100; + const dSy = 100; + + await page.mouse.move(finalX + dx + dSx, finalY + dy + dSy); + + await assertEdgelessDraggingArea(page, [ + initialX + dx, + initialY + dy, + // In the second scale it should scale by dS(.) + finalX - initialX + dSx, + finalY - initialY + dSy, + ]); + }, + } + ); +}); + +test('cmd+a should not select doc only note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + const note2 = await addNote(page, 'note2', 100, 200); + await addNote(page, 'note3', 200, 300); + await page.mouse.click(200, 500); + // assert add note success, there should be 2 notes in edgeless page + await assertVisibleBlockCount(page, 'edgeless-note', 3); + + // change note display mode to doc only + await changeNoteDisplayModeWithId(page, note2, NoteDisplayMode.DocOnly); + // there should still be 2 notes in edgeless page + await assertVisibleBlockCount(page, 'edgeless-note', 2); + + // cmd+a should not select doc only note + await selectAllByKeyboard(page); + // there should be only 2 notes in selection + await assertEdgelessSelectedElementHandleCount(page, 2); +}); diff --git a/blocksuite/tests-legacy/edgeless/selection/selection.spec.ts b/blocksuite/tests-legacy/edgeless/selection/selection.spec.ts new file mode 100644 index 0000000000000..754838e5f458e --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/selection/selection.spec.ts @@ -0,0 +1,466 @@ +import { expect } from '@playwright/test'; + +import * as actions from '../../utils/actions/edgeless.js'; +import { + getNoteBoundBoxInEdgeless, + setEdgelessTool, + switchEditorMode, +} from '../../utils/actions/edgeless.js'; +import { + addBasicBrushElement, + addBasicRectShapeElement, + click, + clickInCenter, + dragBetweenCoords, + enterPlaygroundRoom, + getBoundingRect, + initEmptyEdgelessState, + initThreeParagraphs, + pressEnter, + waitNextFrame, +} from '../../utils/actions/index.js'; +import { + assertBlockCount, + assertEdgelessRemoteSelectedModelRect, + assertEdgelessRemoteSelectedRect, + assertEdgelessSelectedModelRect, + assertEdgelessSelectedRect, + assertSelectionInNote, +} from '../../utils/asserts.js'; +import { test } from '../../utils/playwright.js'; + +test('should update rect of selection when resizing viewport', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await actions.switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await dragBetweenCoords(page, { x: 120, y: 90 }, { x: 220, y: 130 }); + + const selectedRectClass = '.affine-edgeless-selected-rect'; + + await actions.zoomResetByKeyboard(page); + + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + await actions.decreaseZoomLevel(page); + await waitNextFrame(page); + await actions.decreaseZoomLevel(page); + await waitNextFrame(page); + const selectedRectInZoom = await getBoundingRect(page, selectedRectClass); + await assertEdgelessSelectedRect(page, [ + selectedRectInZoom.x, + selectedRectInZoom.y, + 50, + 50, + ]); + + await actions.switchEditorEmbedMode(page); + await waitNextFrame(page); + const selectedRectInEmbed = await getBoundingRect(page, selectedRectClass); + await assertEdgelessSelectedRect(page, [ + selectedRectInEmbed.x, + selectedRectInEmbed.y, + 50, + 50, + ]); + + await actions.switchEditorEmbedMode(page); + await actions.increaseZoomLevel(page); + await waitNextFrame(page); + await actions.increaseZoomLevel(page); + await waitNextFrame(page); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); +}); + +test('should update react of remote selection when resizing viewport', async ({ + context, + page: pageA, +}) => { + const room = await enterPlaygroundRoom(pageA); + await initEmptyEdgelessState(pageA); + await actions.switchEditorMode(pageA); + await actions.zoomResetByKeyboard(pageA); + + const pageB = await context.newPage(); + await enterPlaygroundRoom(pageB, { + room, + noInit: true, + }); + await actions.switchEditorMode(pageB); + await actions.zoomResetByKeyboard(pageB); + + await actions.createShapeElement( + pageA, + [0, 0], + [100, 100], + actions.Shape.Square + ); + const point = await actions.toViewCoord(pageA, [50, 50]); + await click(pageA, { x: point[0], y: point[1] }); + await click(pageB, { x: point[0], y: point[1] }); + + await assertEdgelessSelectedModelRect(pageB, [0, 0, 100, 100]); + await assertEdgelessRemoteSelectedModelRect(pageB, [0, 0, 100, 100]); + + // to 50% + await actions.decreaseZoomLevel(pageB); + await waitNextFrame(pageB); + await actions.decreaseZoomLevel(pageB); + await waitNextFrame(pageB); + + const selectedRectInZoom = await getBoundingRect( + pageB, + '.affine-edgeless-selected-rect' + ); + await assertEdgelessRemoteSelectedRect(pageB, [ + selectedRectInZoom.x, + selectedRectInZoom.y, + 50, + 50, + ]); +}); + +test('select multiple shapes and translate', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await page.mouse.click(110, 110); + await assertEdgelessSelectedRect(page, [98, 98, 104, 104]); + + await addBasicRectShapeElement(page, { x: 210, y: 110 }, { x: 310, y: 210 }); + await page.mouse.click(220, 120); + await assertEdgelessSelectedRect(page, [210, 110, 100, 100]); + + await dragBetweenCoords(page, { x: 120, y: 90 }, { x: 220, y: 130 }); + await assertEdgelessSelectedRect(page, [98, 98, 212, 112]); + + await dragBetweenCoords(page, { x: 120, y: 120 }, { x: 150, y: 150 }); + await assertEdgelessSelectedRect(page, [125, 128, 212, 112]); + + await page.mouse.click(160, 160); + await assertEdgelessSelectedRect(page, [125, 128, 104, 104]); + + await page.mouse.click(250, 150); + await assertEdgelessSelectedRect(page, [237, 140, 100, 100]); +}); + +test('selection box of shape element sync on fast dragging', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + await setEdgelessTool(page, 'shape'); + await dragBetweenCoords(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await setEdgelessTool(page, 'default'); + await dragBetweenCoords( + page, + { x: 110, y: 110 }, + { x: 660, y: 460 }, + { click: true } + ); + + await assertEdgelessSelectedRect(page, [650, 446, 100, 100]); +}); + +test('when the selection is always a note, it should remain in an active state', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + const ids = await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + const bound = await getNoteBoundBoxInEdgeless(page, ids.noteId); + + await setEdgelessTool(page, 'note'); + const newNoteX = bound.x; + const newNoteY = bound.y + bound.height + 100; + // add text + await page.mouse.click(newNoteX, newNoteY); + await waitNextFrame(page); + await page.keyboard.type('hello'); + await pressEnter(page); + // should wait for inline editor update and resizeObserver callback + await waitNextFrame(page); + // assert add text success + await assertBlockCount(page, 'edgeless-note', 2); + + await clickInCenter(page, bound); + await clickInCenter(page, bound); + await waitNextFrame(page); + await assertSelectionInNote(page, ids.noteId, 'affine-edgeless-note'); +}); + +test('should auto panning when selection rectangle reaches viewport edges', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + await addBasicRectShapeElement(page, { x: 200, y: 100 }, { x: 300, y: 200 }); + await page.mouse.click(210, 110); + await assertEdgelessSelectedRect(page, [200, 100, 100, 100]); + + const selectedRectClass = '.affine-edgeless-selected-rect'; + + // Panning to the left + await setEdgelessTool(page, 'pan'); + await dragBetweenCoords( + page, + { + x: 600, + y: 200, + }, + { + x: 200, + y: 200, + } + ); + await setEdgelessTool(page, 'default'); + await page.mouse.click(210, 110); + let selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(300); + await expect(selectedRect).toBeHidden(); + // Click to start selection and hold the mouse to trigger auto panning to the left + await page.mouse.move(210, 110); + await page.mouse.down(); + await page.mouse.move(0, 210, { steps: 20 }); + await page.waitForTimeout(500); + await page.mouse.up(); + + // Expect to select the shape element + selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(300); + await expect(selectedRect).toBeVisible(); + + // Panning to the top + await page.mouse.click(400, 600); + await setEdgelessTool(page, 'pan'); + await dragBetweenCoords( + page, + { + x: 400, + y: 600, + }, + { + x: 400, + y: 100, + } + ); + await setEdgelessTool(page, 'default'); + await page.mouse.click(600, 100); + selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(300); + await expect(selectedRect).toBeHidden(); + // Click to start selection and hold the mouse to trigger auto panning to the top + await page.mouse.move(600, 100); + await page.mouse.down(); + await page.mouse.move(400, 0, { steps: 20 }); + await page.waitForTimeout(500); + await page.mouse.up(); + + // Expect to select the empty note + selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(300); + await expect(selectedRect).toBeVisible(); + + // Panning to the right + await page.mouse.click(100, 600); + await setEdgelessTool(page, 'pan'); + await dragBetweenCoords( + page, + { + x: 20, + y: 600, + }, + { + x: 1000, + y: 600, + } + ); + await setEdgelessTool(page, 'default'); + await page.mouse.click(800, 600); + selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(100); + await expect(selectedRect).toBeHidden(); + // Click to start selection and hold the mouse to trigger auto panning to the right + await dragBetweenCoords( + page, + { + x: 800, + y: 600, + }, + { + x: 1000, + y: 200, + }, + { + beforeMouseUp: async () => { + await page.waitForTimeout(600); + }, + } + ); + + // Expect to select the empty note + selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(300); + await expect(selectedRect).toBeVisible(); + + // Panning to the bottom + await page.mouse.click(400, 100); + await setEdgelessTool(page, 'pan'); + await dragBetweenCoords( + page, + { + x: 400, + y: 100, + }, + { + x: 400, + y: 850, + }, + { + click: true, + } + ); + await setEdgelessTool(page, 'default'); + await waitNextFrame(page, 500); + await page.mouse.click(400, 400); + selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(100); + await expect(selectedRect).toBeHidden(); + + // Click to start selection and hold the mouse to trigger auto panning to the right + await dragBetweenCoords( + page, + { + x: 800, + y: 300, + }, + { + x: 820, + y: 1150, + }, + { + click: true, + beforeMouseUp: async () => { + await page.waitForTimeout(500); + }, + } + ); + + // Expect to select the empty note + selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(300); + await expect(selectedRect).toBeVisible(); +}); + +test('should also update dragging area when viewport changes', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + // Panning to the top + await page.mouse.click(400, 600); + await setEdgelessTool(page, 'pan'); + await dragBetweenCoords( + page, + { + x: 400, + y: 600, + }, + { + x: 400, + y: 100, + } + ); + await setEdgelessTool(page, 'default'); + await page.mouse.click(200, 300); + + const selectedRectClass = '.affine-edgeless-selected-rect'; + let selectedRect = page.locator(selectedRectClass); + await expect(selectedRect).toBeHidden(); + // set up initial dragging area + await page.mouse.move(200, 300); + await page.mouse.down(); + await page.mouse.move(600, 200, { steps: 20 }); + await page.waitForTimeout(300); + + // wheel the viewport to the top + await page.mouse.wheel(0, -300); + await page.waitForTimeout(300); + await page.mouse.up(); + + // Expect to select the empty note + selectedRect = page.locator(selectedRectClass); + await page.waitForTimeout(300); + await expect(selectedRect).toBeVisible(); + await page.waitForTimeout(300); +}); + +test('should select shapes while moving selection', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await actions.zoomResetByKeyboard(page); + + await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + + // Make the selection out side the rect and move the selection to the rect + await dragBetweenCoords( + page, + // Make the selection not selecting the rect + { x: 70, y: 70 }, + { x: 90, y: 90 }, + { + beforeMouseUp: async () => { + await page.keyboard.down('Space'); + // Move the selection over to the rect + await page.mouse.move(120, 120); + await page.keyboard.up('Space'); + }, + } + ); + + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + await addBasicBrushElement(page, { x: 210, y: 100 }, { x: 310, y: 300 }); + await page.mouse.click(211, 101); + + // Make a wide selection and move it to select both of the shapes + await dragBetweenCoords( + page, + // Make the selection above the spaces + { x: 70, y: 70 }, + { x: 400, y: 90 }, + { + beforeMouseUp: async () => { + await page.keyboard.down('Space'); + // Move the selection over both of the shapes + await page.mouse.move(400, 120); + await page.keyboard.up('Space'); + }, + } + ); + + await assertEdgelessSelectedRect(page, [100, 98, 212, 204]); +}); diff --git a/blocksuite/tests-legacy/edgeless/shape.spec.ts b/blocksuite/tests-legacy/edgeless/shape.spec.ts new file mode 100644 index 0000000000000..3af1f2bd5ba80 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/shape.spec.ts @@ -0,0 +1,727 @@ +import { expect, type Page } from '@playwright/test'; + +import { + assertEdgelessTool, + changeShapeFillColor, + changeShapeFillColorToTransparent, + changeShapeStrokeColor, + changeShapeStrokeStyle, + changeShapeStrokeWidth, + changeShapeStyle, + clickComponentToolbarMoreMenuButton, + getEdgelessSelectedRect, + locatorComponentToolbar, + locatorEdgelessToolButton, + locatorShapeStrokeStyleButton, + openComponentToolbarMoreMenu, + pickColorAtPoints, + resizeElementByHandle, + setEdgelessTool, + switchEditorMode, + triggerComponentToolbarAction, + zoomResetByKeyboard, +} from '../utils/actions/edgeless.js'; +import { + addBasicBrushElement, + addBasicRectShapeElement, + copyByKeyboard, + dragBetweenCoords, + enterPlaygroundRoom, + focusRichText, + initEmptyEdgelessState, + pasteByKeyboard, + pressEscape, + type, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertEdgelessCanvasText, + assertEdgelessColorSameWithHexColor, + assertEdgelessNonSelectedRect, + assertEdgelessSelectedRect, + assertExists, + assertRichTexts, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('add shape', () => { + test('without holding shift key', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicRectShapeElement(page, start0, end0); + + await assertEdgelessTool(page, 'default'); + await assertEdgelessSelectedRect(page, [100, 100, 50, 100]); + + const start1 = { x: 100, y: 100 }; + const end1 = { x: 200, y: 150 }; + await addBasicRectShapeElement(page, start1, end1); + + await assertEdgelessTool(page, 'default'); + await assertEdgelessSelectedRect(page, [100, 100, 100, 50]); + }); + + test('with holding shift key', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await page.keyboard.down('Shift'); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 150, y: 200 }; + await addBasicRectShapeElement(page, start0, end0); + + await page.keyboard.up('Shift'); + + await assertEdgelessTool(page, 'default'); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + await page.keyboard.down('Shift'); + + const start1 = { x: 100, y: 100 }; + const end1 = { x: 200, y: 150 }; + await addBasicRectShapeElement(page, start1, end1); + + await assertEdgelessTool(page, 'default'); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + }); + test('with holding space bar', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 200, y: 200 }; + await setEdgelessTool(page, 'shape'); + await dragBetweenCoords(page, start0, end0, { + steps: 50, + beforeMouseUp: async () => { + // move the shape + await page.keyboard.down('Space'); + await page.mouse.move(300, 300); + await page.keyboard.up('Space'); + + await page.mouse.move(500, 600); + }, + }); + + await assertEdgelessSelectedRect(page, [200, 200, 300, 400]); + }); + + test('with holding space bar + shift', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start0 = { x: 100, y: 100 }; + const end0 = { x: 200, y: 200 }; + await setEdgelessTool(page, 'shape'); + await page.keyboard.down('Shift'); + await dragBetweenCoords(page, start0, end0, { + steps: 50, + beforeMouseUp: async () => { + // move the shape + await page.keyboard.down('Space'); + await page.mouse.move(300, 300); + await page.keyboard.up('Space'); + + await page.mouse.move(500, 600); + }, + }); + + await assertEdgelessSelectedRect(page, [200, 200, 400, 400]); + }); +}); + +test('delete shape by component-toolbar', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicBrushElement(page, start, end); + + await page.mouse.click(110, 110); + await openComponentToolbarMoreMenu(page); + await clickComponentToolbarMoreMenuButton(page, 'delete'); + await assertEdgelessNonSelectedRect(page); +}); + +//FIXME: need a way to test hand-drawn-like style +test.skip('change shape fill color', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const rect = { + start: { x: 100, y: 100 }, + end: { x: 200, y: 200 }, + }; + await addBasicRectShapeElement(page, rect.start, rect.end); + + await page.mouse.click(rect.start.x + 5, rect.start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + const color = '--affine-palette-shape-teal'; + await changeShapeFillColor(page, color); + await page.waitForTimeout(50); + const [picked] = await pickColorAtPoints(page, [ + [rect.start.x + 20, rect.start.y + 20], + ]); + + await assertEdgelessColorSameWithHexColor(page, color, picked); +}); + +test('change shape stroke color', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const rect = { + start: { x: 100, y: 100 }, + end: { x: 200, y: 200 }, + }; + await addBasicRectShapeElement(page, rect.start, rect.end); + + await page.mouse.click(rect.start.x + 5, rect.start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); + const color = '--affine-palette-line-teal'; + await changeShapeStrokeColor(page, color); + await page.waitForTimeout(50); + const [picked] = await pickColorAtPoints(page, [ + [rect.start.x + 1, rect.start.y + 1], + ]); + + await assertEdgelessColorSameWithHexColor(page, color, picked); +}); + +test('the tooltip of shape tool button should be hidden when the shape menu is shown', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const shapeTool = await locatorEdgelessToolButton(page, 'shape'); + const shapeToolBox = await shapeTool.boundingBox(); + const tooltip = page.locator('.affine-tooltip'); + + assertExists(shapeToolBox); + + await page.mouse.move(shapeToolBox.x + 2, shapeToolBox.y + 2); + await expect(tooltip).toBeVisible(); + + await page.mouse.click(shapeToolBox.x + 2, shapeToolBox.y + 2); + await expect(tooltip).toBeHidden(); + + await page.mouse.click(shapeToolBox.x + 2, shapeToolBox.y + 2); + await expect(tooltip).toBeVisible(); +}); + +test('delete shape block by keyboard', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await setEdgelessTool(page, 'shape'); + await dragBetweenCoords(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + + await setEdgelessTool(page, 'default'); + const startPoint = await page.evaluate(() => { + const hitbox = document.querySelector('[data-block-id="3"]'); + if (!hitbox) { + throw new Error('hitbox is null'); + } + const rect = hitbox.getBoundingClientRect(); + if (rect == null) { + throw new Error('rect is null'); + } + return { + x: rect.x, + y: rect.y, + }; + }); + await page.mouse.click(startPoint.x + 2, startPoint.y + 2); + await waitNextFrame(page); + await page.keyboard.press('Backspace'); + const exist = await page.evaluate(() => { + return document.querySelector('[data-block-id="3"]') != null; + }); + expect(exist).toBe(false); +}); + +test('edgeless toolbar shape menu shows up and close normally', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const toolbarLocator = page.locator('.edgeless-toolbar-container'); + await expect(toolbarLocator).toBeVisible(); + + const shapeTool = await locatorEdgelessToolButton(page, 'shape'); + const shapeToolBox = await shapeTool.boundingBox(); + + assertExists(shapeToolBox); + + await page.mouse.click(shapeToolBox.x + 2, shapeToolBox.y + 2); + + const shapeMenu = page.locator('edgeless-shape-menu'); + await expect(shapeMenu).toBeVisible(); + await page.waitForTimeout(500); + + await page.mouse.click(shapeToolBox.x + 2, shapeToolBox.y + 2); + await page.waitForTimeout(500); + await expect(shapeMenu).toBeHidden(); +}); + +test('hovering on shape should not have effect on underlying block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + await switchEditorMode(page); + + const block = page.locator('affine-edgeless-note'); + const blockBox = await block.boundingBox(); + if (blockBox === null) throw new Error('Unexpected box value: box is null'); + + const { x, y } = blockBox; + + await setEdgelessTool(page, 'shape'); + await dragBetweenCoords(page, { x, y }, { x: x + 100, y: y + 100 }); + await setEdgelessTool(page, 'default'); + + await page.mouse.click(x + 10, y + 10); + await assertEdgelessSelectedRect(page, [x, y, 100, 100]); +}); + +test('shape element should not move when the selected state is inactive', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await setEdgelessTool(page, 'shape'); + await dragBetweenCoords(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + await setEdgelessTool(page, 'default'); + await dragBetweenCoords( + page, + { x: 50, y: 50 }, + { x: 110, y: 110 }, + { steps: 2 } + ); + + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); +}); + +test('change shape stroke width', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 150 }; + const end = { x: 200, y: 250 }; + await addBasicRectShapeElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); + await changeShapeStrokeColor(page, '--affine-palette-line-teal'); + + await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); + await changeShapeStrokeWidth(page); + await page.mouse.click(start.x + 5, start.y + 5); + await assertEdgelessSelectedRect(page, [100, 150, 100, 100]); + + await waitNextFrame(page); + + await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); +}); + +test('change shape stroke style', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 150 }; + const end = { x: 200, y: 250 }; + await addBasicRectShapeElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); + await changeShapeStrokeColor(page, '--affine-palette-line-teal'); + + await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); + await changeShapeStrokeStyle(page, 'dash'); + await waitNextFrame(page); + + await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles'); + const activeButton = locatorShapeStrokeStyleButton(page, 'dash'); + const className = await activeButton.evaluate(ele => ele.className); + expect(className.includes(' active')).toBeTruthy(); + + const pickedColor = await pickColorAtPoints(page, [[start.x + 20, start.y]]); + expect(pickedColor[0]).toBe('#000000'); +}); + +test('click to add shape', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await setEdgelessTool(page, 'shape'); + await waitNextFrame(page, 500); + + await page.mouse.move(400, 400); + await page.mouse.move(200, 200); + await page.mouse.click(200, 200, { button: 'left', delay: 300 }); + + await assertEdgelessTool(page, 'default'); + await assertEdgelessSelectedRect(page, [200, 200, 100, 100]); +}); + +test('dbclick to add text in shape', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await setEdgelessTool(page, 'shape'); + await waitNextFrame(page, 500); + + await page.mouse.click(200, 150); + await waitNextFrame(page); + await page.mouse.dblclick(250, 200); + await waitNextFrame(page); + + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); + await assertEdgelessTool(page, 'default'); + + // test select, copy, paste + const select = async () => { + await page.mouse.move(245, 205); + await page.mouse.down(); + + await page.mouse.move(245, 205); + await page.mouse.down(); + await page.mouse.move(262, 205, { + steps: 10, + }); + await page.mouse.up(); + }; + await select(); + // h|ell|o + await waitNextFrame(page); + await copyByKeyboard(page); + await waitNextFrame(page); + + // FIXME(@Flrande): this is a workaround, we should keep selection + await select(); + + await waitNextFrame(page); + await type(page, 'ddd', 50); + await waitNextFrame(page); + await assertEdgelessCanvasText(page, 'hdddo'); + + await pasteByKeyboard(page); + await assertEdgelessCanvasText(page, 'hdddello'); +}); + +test('should show selected rect after exiting editing by pressing Escape', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await setEdgelessTool(page, 'shape'); + await waitNextFrame(page, 500); + + await dragBetweenCoords(page, { x: 100, y: 100 }, { x: 200, y: 200 }); + + await waitNextFrame(page); + await page.mouse.dblclick(150, 150); + await waitNextFrame(page); + + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); + + await pressEscape(page); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); +}); + +test('auto wrap text in shape', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await setEdgelessTool(page, 'shape'); + await waitNextFrame(page, 500); + + await page.mouse.click(200, 150); + await waitNextFrame(page); + await page.mouse.dblclick(250, 200); + await waitNextFrame(page); + + await type(page, 'aaaa\nbbbb\n'); + await assertEdgelessCanvasText(page, 'aaaa\nbbbb\n'); + await assertEdgelessTool(page, 'default'); + + // blur to finish typing + await page.mouse.click(150, 150); + // select shape + await page.mouse.click(200, 150); + // the height of shape should be increased because of \n + let selectedRect = await getEdgelessSelectedRect(page); + let lastWidth = selectedRect.width; + let lastHeight = selectedRect.height; + + await page.mouse.dblclick(250, 200); + await waitNextFrame(page); + // type long text + await type(page, '\ncccccccc'); + await assertEdgelessCanvasText(page, 'aaaa\nbbbb\ncccccccc'); + + // blur to finish typing + await page.mouse.click(150, 150); + // select shape + await page.mouse.click(200, 150); + // the height of shape should be increased because of long text + // cccccccc -- wrap --> cccccc\ncc + selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).toBe(lastWidth); + expect(selectedRect.height).toBeGreaterThan(lastHeight); + lastWidth = selectedRect.width; + lastHeight = selectedRect.height; + + // try to decrease height + await resizeElementByHandle(page, { x: 0, y: -50 }, 'bottom-right'); + // you can't decrease height because of min height to fit text + selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).toBe(lastWidth); + expect(selectedRect.height).toBeGreaterThanOrEqual(lastHeight); + lastWidth = selectedRect.width; + lastHeight = selectedRect.height; + + // increase width to make text not wrap + await resizeElementByHandle(page, { x: 50, y: -10 }, 'bottom-right'); + // the height of shape should be decreased because of long text not wrap + selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).toBeGreaterThan(lastWidth); + expect(selectedRect.height).toBeLessThan(lastHeight); + + // try to decrease width + await resizeElementByHandle(page, { x: -140, y: 0 }, 'bottom-right'); + // you can't decrease width after text can't wrap (each line just has 1 char) + await assertEdgelessSelectedRect(page, [200, 150, 52, 404]); +}); + +test('change shape style', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 150 }; + const end = { x: 200, y: 250 }; + await addBasicRectShapeElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeStyle'); + await changeShapeStyle(page, 'general'); + await waitNextFrame(page); + + await page.mouse.click(start.x + 5, start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeStrokeColor'); + const color = '--affine-palette-line-teal'; + await changeShapeStrokeColor(page, color); + await page.waitForTimeout(50); + const [picked] = await pickColorAtPoints(page, [[start.x + 1, start.y + 1]]); + + await assertEdgelessColorSameWithHexColor(page, color, picked); +}); + +test('shape adds text by button', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await setEdgelessTool(page, 'shape'); + await waitNextFrame(page, 500); + + await page.mouse.click(200, 150); + await waitNextFrame(page); + + await triggerComponentToolbarAction(page, 'addText'); + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); +}); + +test('should reset shape text when text is empty', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + await setEdgelessTool(page, 'shape'); + await waitNextFrame(page, 500); + + await page.mouse.click(200, 150); + await waitNextFrame(page); + + await triggerComponentToolbarAction(page, 'addText'); + await type(page, ' a '); + await assertEdgelessCanvasText(page, ' a '); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + await page.mouse.click(200, 150); + + const addTextBtn = locatorComponentToolbar(page).getByRole('button', { + name: 'Add text', + }); + await expect(addTextBtn).toBeHidden(); + + await page.mouse.dblclick(250, 200); + await assertEdgelessCanvasText(page, 'a'); + + await page.keyboard.press('Backspace'); + await assertEdgelessCanvasText(page, ''); + + await page.mouse.click(0, 0); + await waitNextFrame(page); + await page.mouse.click(200, 150); + + await expect(addTextBtn).toBeVisible(); +}); + +test.describe('shape hit test', () => { + async function addTransparentRect( + page: Page, + start: { x: number; y: number }, + end: { x: number; y: number } + ) { + const rect = { + start, + end, + }; + await addBasicRectShapeElement(page, rect.start, rect.end); + + await page.mouse.click(rect.start.x + 5, rect.start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + await changeShapeFillColorToTransparent(page); + await page.waitForTimeout(50); + } + + test.beforeEach(async ({ page }) => { + await enterPlaygroundRoom(page, { + flags: { + enable_edgeless_text: false, + }, + }); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + }); + + const rect = { + start: { x: 100, y: 100 }, + end: { x: 200, y: 200 }, + }; + + test('can select hollow shape by clicking center area', async ({ page }) => { + await addTransparentRect(page, rect.start, rect.end); + await page.mouse.click(rect.start.x - 20, rect.start.y - 20); + await assertEdgelessNonSelectedRect(page); + + await page.mouse.click(rect.start.x + 50, rect.start.y + 50); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + }); + + test('double click can add text in shape hollow area', async ({ page }) => { + await addTransparentRect(page, rect.start, rect.end); + await page.mouse.click(rect.start.x - 20, rect.start.y - 20); + await assertEdgelessNonSelectedRect(page); + + await assertEdgelessTool(page, 'default'); + await page.mouse.dblclick(rect.start.x + 20, rect.start.y + 20); + await waitNextFrame(page); + + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); + }); + + // FIXME(@flrande): This is broken by recent changes + // In Playwright, we can't add text in shape hollow area + test.fixme( + 'using text tool to add text in shape hollow area', + async ({ page }) => { + await addTransparentRect(page, rect.start, rect.end); + await page.mouse.click(rect.start.x - 20, rect.start.y - 20); + await assertEdgelessNonSelectedRect(page); + + await assertEdgelessTool(page, 'default'); + await setEdgelessTool(page, 'text'); + await page.mouse.click(rect.start.x + 50, rect.start.y + 50); + await waitNextFrame(page); + + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); + } + ); + + test('should enter edit mode when double-clicking a text area in a shape with a transparent background', async ({ + page, + }) => { + await addTransparentRect(page, rect.start, rect.end); + await page.mouse.click(rect.start.x - 20, rect.start.y - 20); + await assertEdgelessNonSelectedRect(page); + + await assertEdgelessTool(page, 'default'); + await page.mouse.dblclick(rect.start.x + 50, rect.start.y + 50); + await waitNextFrame(page); + await type(page, 'hello'); + + await pressEscape(page); + await waitNextFrame(page); + + const textAlignBtn = locatorComponentToolbar(page).getByRole('button', { + name: 'Alignment', + }); + await textAlignBtn.click(); + + await page + .locator('edgeless-align-panel') + .getByRole('button', { name: 'Left' }) + .click(); + + // creates an edgeless-text + await page.mouse.dblclick(rect.start.x + 80, rect.start.y + 20); + await waitNextFrame(page); + await page.locator('edgeless-text-editor').isVisible(); + + await pressEscape(page); + await waitNextFrame(page); + + // enters edit mode + await page.mouse.dblclick(rect.start.x + 20, rect.start.y + 50); + await page.locator('edgeless-shape-text-editor').isVisible(); + await type(page, ' world'); + await assertEdgelessCanvasText(page, 'hello world'); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/shortcut.spec.ts b/blocksuite/tests-legacy/edgeless/shortcut.spec.ts new file mode 100644 index 0000000000000..f77e4c9513b02 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/shortcut.spec.ts @@ -0,0 +1,324 @@ +import { expect } from '@playwright/test'; + +import { + addBasicRectShapeElement, + assertEdgelessShapeType, + createShapeElement, + edgelessCommonSetup, + getEdgelessSelectedRect, + getZoomLevel, + locatorEdgelessToolButton, + setEdgelessTool, + type ShapeName, + switchEditorMode, + zoomFitByKeyboard, + zoomInByKeyboard, + zoomOutByKeyboard, + zoomResetByKeyboard, +} from '../utils/actions/edgeless.js'; +import { + clickView, + enterPlaygroundRoom, + focusRichText, + initEmptyEdgelessState, + pressBackspace, + pressEscape, + pressForwardDelete, + selectAllByKeyboard, + selectNoteInEdgeless, + type, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertBlockCount, + assertEdgelessNonSelectedRect, + assertEdgelessSelectedModelRect, + assertEdgelessSelectedRect, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('shortcut', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await page.mouse.click(100, 100); + + // text is removed temporarily + // await page.keyboard.press('t'); + // const textButton = await locatorEdgelessToolButton(page, 'text'); + // await expect(textButton).toHaveAttribute('active', ''); + + await page.keyboard.press('s'); + const shapeButton = await locatorEdgelessToolButton(page, 'shape'); + await expect(shapeButton).toHaveAttribute('active', ''); + + await page.keyboard.press('p'); + const penButton = await locatorEdgelessToolButton(page, 'brush'); + await expect(penButton).toHaveAttribute('active', ''); + + await page.keyboard.press('h'); + const panButton = await locatorEdgelessToolButton(page, 'pan'); + await expect(panButton).toHaveAttribute('active', ''); + + await page.keyboard.press('c'); + const connectorButton = await locatorEdgelessToolButton(page, 'connector'); + await expect(connectorButton).toHaveAttribute('active', ''); + + // await page.keyboard.press('l'); + // const lassoButton = await locatorEdgelessToolButton(page, 'lasso'); + // await expect(lassoButton).toHaveAttribute('active', ''); +}); + +test.skip('toggle lasso tool modes', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await page.mouse.click(100, 100); + + const lassoButton = await locatorEdgelessToolButton(page, 'lasso', false); + + const isLassoMode = async (type: 'freehand' | 'polygonal') => { + const classes = (await lassoButton.getAttribute('class'))?.split(' ') ?? []; + return classes.includes(type); + }; + + await page.keyboard.press('Shift+l'); + expect(await isLassoMode('freehand')).toBe(true); + + await page.keyboard.press('Shift+l'); + expect(await isLassoMode('polygonal')).toBe(true); + + await page.keyboard.press('Shift+l'); + expect(await isLassoMode('freehand')).toBe(true); +}); + +test('toggle shapes shortcut', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await page.mouse.click(100, 100); + await setEdgelessTool(page, 'shape'); + + const shapesInOrder = [ + 'ellipse', + 'diamond', + 'triangle', + 'roundedRect', + 'rect', + 'ellipse', + 'diamond', + 'triangle', + 'roundedRect', + ] as ShapeName[]; + for (const shape of shapesInOrder) { + await page.keyboard.press('Shift+s'); + await assertEdgelessShapeType(page, shape); + } +}); + +test('should not switch shapes in editing', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await setEdgelessTool(page, 'shape'); + await waitNextFrame(page); + await assertEdgelessShapeType(page, 'rect'); + + await page.mouse.click(200, 150); + await waitNextFrame(page); + await page.mouse.dblclick(250, 200); + await waitNextFrame(page); + + await type(page, 'hello'); + await page.keyboard.press('Shift+s'); + await page.keyboard.press('Escape'); + await waitNextFrame(page); + await setEdgelessTool(page, 'shape'); + await assertEdgelessShapeType(page, 'rect'); + + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(250, 200); + await waitNextFrame(page); + await page.keyboard.press('Shift+S'); + await page.keyboard.press('Escape'); + await waitNextFrame(page); + await setEdgelessTool(page, 'shape'); + await assertEdgelessShapeType(page, 'rect'); +}); + +test('pressing the ESC key will return to the default state', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + const start = { x: 100, y: 100 }; + const end = { x: 200, y: 200 }; + await addBasicRectShapeElement(page, start, end); + + await page.mouse.click(start.x + 5, start.y + 5); + await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); + + await pressEscape(page); + await assertEdgelessNonSelectedRect(page); +}); + +test.describe('zooming', () => { + test('zoom fit to screen', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + + const start = { x: 0, y: 0 }; + const end = { x: 900, y: 200 }; + await addBasicRectShapeElement(page, start, end); + await zoomFitByKeyboard(page); + + const zoom = await getZoomLevel(page); + expect(zoom).not.toBe(100); + }); + test('zoom out', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await clickView(page, [0, 0]); + await zoomResetByKeyboard(page); + + await zoomOutByKeyboard(page); + + let zoom = await getZoomLevel(page); + expect(zoom).toBe(75); + + await zoomOutByKeyboard(page); + + zoom = await getZoomLevel(page); + expect(zoom).toBe(50); + }); + test('zoom reset', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await clickView(page, [0, 0]); + await zoomResetByKeyboard(page); + let zoom = await getZoomLevel(page); + expect(zoom).toBe(100); + + await zoomOutByKeyboard(page); + + zoom = await getZoomLevel(page); + expect(zoom).toBe(75); + + await zoomResetByKeyboard(page); + + zoom = await getZoomLevel(page); + expect(zoom).toBe(100); + }); + test('zoom in', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await clickView(page, [0, 0]); + await zoomResetByKeyboard(page); + + await zoomInByKeyboard(page); + + let zoom = await getZoomLevel(page); + expect(zoom).toBe(125); + + await zoomInByKeyboard(page); + + zoom = await getZoomLevel(page); + expect(zoom).toBe(150); + }); +}); + +test('cmd + A should select all elements by default', async ({ page }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [0, 0], [100, 100]); + await createShapeElement(page, [100, 0], [200, 100]); + await selectAllByKeyboard(page); + await assertEdgelessSelectedModelRect(page, [0, 0, 200, 100]); +}); + +test('cmd + A should not fire inside active note', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await switchEditorMode(page); + + await selectNoteInEdgeless(page, noteId); + // second click become active + await selectNoteInEdgeless(page, noteId); + await selectAllByKeyboard(page); + + // should not have selected rect + let error = null; + try { + await getEdgelessSelectedRect(page); + } catch (e) { + error = e; + } + expect(error).not.toBeNull(); +}); + +test.describe('delete', () => { + test('do not delete element when active', async ({ page }) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await focusRichText(page); + await type(page, 'hello'); + await switchEditorMode(page); + await selectNoteInEdgeless(page, noteId); + const box1 = await getEdgelessSelectedRect(page); + await page.mouse.click(box1.x + 10, box1.y + 10); + await pressBackspace(page); + await assertBlockCount(page, 'edgeless-note', 1); + await pressForwardDelete(page); + await assertBlockCount(page, 'edgeless-note', 1); + }); +}); + +test.describe('Arrow Keys should move selection', () => { + test('with shift increment by 10px', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + + await page.keyboard.down('Shift'); + + for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowLeft'); + for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowDown'); + + await assertEdgelessSelectedRect(page, [0, 200, 100, 100]); + }); + + test('without shift increment by 1px', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + + await addBasicRectShapeElement( + page, + { x: 100, y: 100 }, + { x: 200, y: 200 } + ); + + for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowRight'); + for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowUp'); + + await assertEdgelessSelectedRect(page, [110, 90, 100, 100]); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/snap.spec.ts b/blocksuite/tests-legacy/edgeless/snap.spec.ts new file mode 100644 index 0000000000000..666e54f76ec76 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/snap.spec.ts @@ -0,0 +1,45 @@ +import { undoByClick } from '../utils/actions/click.js'; +import { + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup, + Shape, +} from '../utils/actions/edgeless.js'; +import { waitNextFrame } from '../utils/actions/misc.js'; +import { assertSelectedBound } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.describe('snap', () => { + test('snap', async ({ page }) => { + await edgelessCommonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [300, 0], [300 + 100, 100], Shape.Square); + + await assertSelectedBound(page, [300, 0, 100, 100]); + + await dragBetweenViewCoords(page, [300 + 5, 50], [300 + 5, 50 + 5]); + await assertSelectedBound(page, [300, 5, 100, 100]); + + await undoByClick(page); + await dragBetweenViewCoords(page, [300 + 5, 50], [300 + 5, 50 + 3]); + await assertSelectedBound(page, [300, 0, 100, 100]); + }); + + test('snapDistribute', async ({ page }) => { + await edgelessCommonSetup(page); + + await createShapeElement(page, [0, 0], [100, 100], Shape.Square); + await createShapeElement(page, [300, 0], [300 + 100, 100], Shape.Square); + await createShapeElement(page, [144, 0], [144 + 100, 100], Shape.Square); + + await assertSelectedBound(page, [144, 0, 100, 100]); + await dragBetweenViewCoords( + page, + [144 + 100 - 9, 100 - 9], + [144 + 100 - 9 + 3, 100 - 9] + ); + await assertSelectedBound(page, [150, 0, 100, 100]); + await waitNextFrame(page); + }); +}); diff --git a/blocksuite/tests-legacy/edgeless/text.spec.ts b/blocksuite/tests-legacy/edgeless/text.spec.ts new file mode 100644 index 0000000000000..a0197659aded5 --- /dev/null +++ b/blocksuite/tests-legacy/edgeless/text.spec.ts @@ -0,0 +1,316 @@ +import { expect, type Page } from '@playwright/test'; +import { getLinkedDocPopover } from 'utils/actions/linked-doc.js'; + +import { + assertEdgelessTool, + enterPlaygroundRoom, + getEdgelessSelectedRect, + initEmptyEdgelessState, + pressArrowLeft, + pressEnter, + setEdgelessTool, + SHORT_KEY, + switchEditorMode, + type, + waitForInlineEditorStateUpdated, + waitNextFrame, + zoomResetByKeyboard, +} from '../utils/actions/index.js'; +import { assertEdgelessCanvasText } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +async function assertTextFont(page: Page, font: string) { + const fontButton = page.getByRole('button', { + name: /^Font$/, + }); + const fontPanel = page.locator('edgeless-font-family-panel'); + const isFontPanelShow = await fontPanel.isVisible(); + if (!isFontPanelShow) { + if (!(await fontButton.isVisible())) + throw new Error('edgeless change text toolbar is not visible'); + + await fontButton.click(); + } + + const button = fontPanel.locator(`[data-font="${font}"]`); + await expect(button.locator('.active-mode-color[active]')).toBeVisible(); +} + +test.describe('edgeless canvas text', () => { + test.beforeEach(async ({ page }) => { + await enterPlaygroundRoom(page, { + flags: { + enable_edgeless_text: false, + }, + }); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + }); + + test('add text element in default mode', async ({ page }) => { + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140); + await waitForInlineEditorStateUpdated(page); + await waitNextFrame(page); + + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); + await assertEdgelessTool(page, 'default'); + + await page.mouse.click(120, 140); + + expect(await page.locator('edgeless-text-editor').count()).toBe(0); + + await page.mouse.dblclick(145, 155); + await waitNextFrame(page); + await page.locator('edgeless-text-editor').waitFor({ + state: 'attached', + }); + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hhelloello'); + + await pressArrowLeft(page, 5); + await type(page, 'ddd\n'); + await assertEdgelessCanvasText(page, 'hddd\nhelloello'); + }); + + test('should not trigger linked doc popover in canvas text', async ({ + page, + }) => { + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140); + await waitForInlineEditorStateUpdated(page); + await waitNextFrame(page); + + await type(page, '@'); + const { linkedDocPopover } = getLinkedDocPopover(page); + await expect(linkedDocPopover).not.toBeVisible(); + await pressEnter(page); + await assertEdgelessCanvasText(page, '@\n'); + }); + + // it's also a little flaky + test('add text element in text mode', async ({ page }) => { + await page.mouse.dblclick(130, 140); + await waitNextFrame(page); + + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); + await assertEdgelessTool(page, 'default'); + + await page.mouse.click(120, 140); + + expect(await page.locator('edgeless-text-editor').count()).toBe(0); + + await page.mouse.dblclick(145, 145); + + await page.locator('edgeless-text-editor').waitFor({ + state: 'attached', + }); + await type(page, 'hello'); + await page.waitForTimeout(100); + await assertEdgelessCanvasText(page, 'hhelloello'); + + await page.mouse.click(145, 155); + await type(page, 'ddd\n'); + await assertEdgelessCanvasText(page, 'hddd\nhelloello'); + }); + + test('copy and paste', async ({ page }) => { + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140); + await waitNextFrame(page); + + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hello'); + await assertEdgelessTool(page, 'default'); + + await page.mouse.move(145, 155); + await page.mouse.down(); + await page.mouse.move(170, 155, { + steps: 10, + }); + await page.mouse.up(); + // h|ell|o + await waitNextFrame(page, 200); + await page.keyboard.press(`${SHORT_KEY}+c`); + + await waitNextFrame(page, 200); + await type(page, 'ddd', 100); + await waitNextFrame(page, 200); + await assertEdgelessCanvasText(page, 'hdddo'); + + await page.keyboard.press(`${SHORT_KEY}+v`); + await assertEdgelessCanvasText(page, 'hdddello'); + }); + + test('normalize text element rect after change its font', async ({ + page, + }) => { + await page.mouse.dblclick(200, 200); + await waitNextFrame(page); + + await type(page, 'aaa\nbbbbbbbb\n\ncc'); + await assertEdgelessCanvasText(page, 'aaa\nbbbbbbbb\n\ncc'); + await assertEdgelessTool(page, 'default'); + await page.mouse.click(10, 100); + + await page.mouse.click(220, 210); + await waitNextFrame(page); + let { width: lastWidth, height: lastHeight } = + await getEdgelessSelectedRect(page); + const fontButton = page.getByRole('button', { name: /^Font$/ }); + await fontButton.click(); + + // Default is Inter + await assertTextFont(page, 'Inter'); + const kalamTextFont = page.getByText('Kalam'); + await kalamTextFont.click(); + await waitNextFrame(page); + let selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).not.toEqual(lastWidth); + expect(selectedRect.height).not.toEqual(lastHeight); + + lastWidth = selectedRect.width; + lastHeight = selectedRect.height; + await fontButton.click(); + await assertTextFont(page, 'Kalam'); + const InterTextFont = page.getByText('Inter'); + await InterTextFont.click(); + await waitNextFrame(page); + selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).not.toEqual(lastWidth); + expect(selectedRect.height).not.toEqual(lastHeight); + }); + + test('auto wrap text by dragging left and right edge', async ({ page }) => { + await zoomResetByKeyboard(page); + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140); + await waitForInlineEditorStateUpdated(page); + await waitNextFrame(page); + + await type(page, 'hellohello'); + await assertEdgelessCanvasText(page, 'hellohello'); + await assertEdgelessTool(page, 'default'); + + // quit edit mode + await page.mouse.click(120, 140); + + // select text element + await page.mouse.click(150, 140); + await waitNextFrame(page); + + // should exit selected rect and record last width and height, then compare them + let selectedRect = await getEdgelessSelectedRect(page); + let lastWidth = selectedRect.width; + let lastHeight = selectedRect.height; + + // move cursor to the right edge and drag it to resize the width of text element + await page.mouse.move(130 + lastWidth, 160); + await page.mouse.down(); + await page.mouse.move(130 + lastWidth / 2, 160, { + steps: 10, + }); + await page.mouse.up(); + + // the text should be wrapped, so check the width and height of text element + selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).toBeLessThan(lastWidth); + expect(selectedRect.height).toBeGreaterThan(lastHeight); + + await page.mouse.dblclick(140, 160); + await waitForInlineEditorStateUpdated(page); + await waitNextFrame(page); + await assertEdgelessCanvasText(page, 'hellohello'); + + // quit edit mode + await page.mouse.click(120, 140); + + // select text element + await page.mouse.click(150, 140); + await waitNextFrame(page); + + // check selected rect and record the last width and height + selectedRect = await getEdgelessSelectedRect(page); + lastWidth = selectedRect.width; + lastHeight = selectedRect.height; + // move cursor to the left edge and drag it to resize the width of text element + await page.mouse.move(130, 160); + await page.mouse.down(); + await page.mouse.move(60, 160, { + steps: 10, + }); + await page.mouse.up(); + + // the text should be unwrapped, check the width and height of text element + selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).toBeGreaterThan(lastWidth); + expect(selectedRect.height).toBeLessThan(lastHeight); + + await page.mouse.dblclick(100, 160); + await waitForInlineEditorStateUpdated(page); + await waitNextFrame(page); + await assertEdgelessCanvasText(page, 'hellohello'); + }); + + test('text element should have maxWidth after adjusting width by dragging left or right edge', async ({ + page, + }) => { + await zoomResetByKeyboard(page); + await setEdgelessTool(page, 'default'); + await page.mouse.dblclick(130, 140); + await waitForInlineEditorStateUpdated(page); + await waitNextFrame(page); + + await type(page, 'hellohello'); + await assertEdgelessCanvasText(page, 'hellohello'); + await assertEdgelessTool(page, 'default'); + + // quit edit mode + await page.mouse.click(120, 140); + + // select text element + await page.mouse.click(150, 140); + await waitNextFrame(page); + + let selectedRect = await getEdgelessSelectedRect(page); + let lastWidth = selectedRect.width; + let lastHeight = selectedRect.height; + + // move cursor to the right edge and drag it to resize the width of text element + await page.mouse.move(130 + lastWidth, 160); + await page.mouse.down(); + await page.mouse.move(130 + lastWidth / 2, 160, { + steps: 10, + }); + await page.mouse.up(); + + // the text should be wrapped, so check the width and height of text element + selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).toBeLessThan(lastWidth); + expect(selectedRect.height).toBeGreaterThan(lastHeight); + lastWidth = selectedRect.width; + lastHeight = selectedRect.height; + + // enter edit mode + await waitNextFrame(page); + await page.mouse.dblclick(140, 180); + await waitForInlineEditorStateUpdated(page); + await waitNextFrame(page); + await type(page, 'hello'); + await assertEdgelessCanvasText(page, 'hellohellohello'); + + // quit edit mode + await page.mouse.click(120, 140); + + // select text element + await page.mouse.click(150, 140); + await waitNextFrame(page); + + // after input, the width of the text element should be the same as before, but the height should be changed + selectedRect = await getEdgelessSelectedRect(page); + expect(selectedRect.width).toBeCloseTo(Math.round(lastWidth)); + expect(selectedRect.height).toBeGreaterThan(lastHeight); + }); +}); diff --git a/blocksuite/tests-legacy/embed-synced-doc.spec.ts b/blocksuite/tests-legacy/embed-synced-doc.spec.ts new file mode 100644 index 0000000000000..9fd6b8f1a674e --- /dev/null +++ b/blocksuite/tests-legacy/embed-synced-doc.spec.ts @@ -0,0 +1,268 @@ +import type { DatabaseBlockModel } from '@blocksuite/affine-model'; +import { assertExists } from '@blocksuite/global/utils'; +import { expect, type Page } from '@playwright/test'; +import { switchEditorMode } from 'utils/actions/edgeless.js'; +import { getLinkedDocPopover } from 'utils/actions/linked-doc.js'; +import { + enterPlaygroundRoom, + focusRichText, + initEmptyEdgelessState, + initEmptyParagraphState, + waitNextFrame, +} from 'utils/actions/misc.js'; + +import { test } from './utils/playwright.js'; + +test.describe('Embed synced doc', () => { + test.beforeEach(async ({ page }) => { + await enterPlaygroundRoom(page); + }); + + async function createAndConvertToEmbedSyncedDoc(page: Page) { + const { createLinkedDoc } = getLinkedDocPopover(page); + const linkedDoc = await createLinkedDoc('page1'); + const lickedDocBox = await linkedDoc.boundingBox(); + assertExists(lickedDocBox); + await page.mouse.move( + lickedDocBox.x + lickedDocBox.width / 2, + lickedDocBox.y + lickedDocBox.height / 2 + ); + + await waitNextFrame(page, 200); + const referencePopup = page.locator('.affine-reference-popover-container'); + await expect(referencePopup).toBeVisible(); + + const switchButton = page.getByRole('button', { name: 'Switch view' }); + await switchButton.click(); + + const embedSyncedDocBtn = page.getByRole('button', { name: 'Embed view' }); + await expect(embedSyncedDocBtn).toBeVisible(); + + await embedSyncedDocBtn.click(); + await waitNextFrame(page, 200); + + const embedSyncedBlock = page.locator('affine-embed-synced-doc-block'); + expect(await embedSyncedBlock.count()).toBe(1); + } + + test('can change linked doc to embed synced doc', async ({ page }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + + await createAndConvertToEmbedSyncedDoc(page); + }); + + test('can change embed synced doc to card view', async ({ page }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + + await createAndConvertToEmbedSyncedDoc(page); + + const syncedDoc = page.locator(`affine-embed-synced-doc-block`); + const syncedDocBox = await syncedDoc.boundingBox(); + assertExists(syncedDocBox); + await page.mouse.click( + syncedDocBox.x + syncedDocBox.width / 2, + syncedDocBox.y + syncedDocBox.height / 2 + ); + + await waitNextFrame(page, 200); + const toolbar = page.locator('.embed-card-toolbar'); + await expect(toolbar).toBeVisible(); + + const switchBtn = toolbar.getByRole('button', { name: 'Switch view' }); + await expect(switchBtn).toBeVisible(); + + await switchBtn.click(); + await waitNextFrame(page, 200); + + const cardBtn = toolbar.getByRole('button', { name: 'Card view' }); + await cardBtn.click(); + await waitNextFrame(page, 200); + + const embedSyncedBlock = page.locator('affine-embed-linked-doc-block'); + expect(await embedSyncedBlock.count()).toBe(1); + }); + + test.fixme( + 'drag embed synced doc to whiteboard should fit in height', + async ({ page }) => { + await initEmptyEdgelessState(page); + await focusRichText(page); + + await createAndConvertToEmbedSyncedDoc(page); + + // Focus on the embed synced doc + const embedSyncedBlock = page.locator('affine-embed-synced-doc-block'); + let embedSyncedBox = await embedSyncedBlock.boundingBox(); + assertExists(embedSyncedBox); + await page.mouse.click( + embedSyncedBox.x + embedSyncedBox.width / 2, + embedSyncedBox.y + embedSyncedBox.height / 2 + ); + + // Switch to edgeless mode + await switchEditorMode(page); + await waitNextFrame(page, 200); + + // Double click on note to enter edit status + const noteBlock = page.locator('affine-edgeless-note'); + const noteBlockBox = await noteBlock.boundingBox(); + assertExists(noteBlockBox); + await page.mouse.dblclick(noteBlockBox.x + 10, noteBlockBox.y + 10); + await waitNextFrame(page, 200); + + // Drag the embed synced doc to whiteboard + embedSyncedBox = await embedSyncedBlock.boundingBox(); + assertExists(embedSyncedBox); + const height = embedSyncedBox.height; + await page.mouse.move(embedSyncedBox.x - 10, embedSyncedBox.y - 100); + await page.mouse.move(embedSyncedBox.x - 10, embedSyncedBox.y + 10); + await waitNextFrame(page); + await page.mouse.down(); + await page.mouse.move(100, 200, { steps: 30 }); + await page.mouse.up(); + + // Check the height of the embed synced doc portal, it should be the same as the embed synced doc in note + const EmbedSyncedDocBlock = page.locator( + 'affine-embed-edgeless-synced-doc-block' + ); + const EmbedSyncedDocBlockBox = await EmbedSyncedDocBlock.boundingBox(); + const border = 1; + assertExists(EmbedSyncedDocBlockBox); + expect(EmbedSyncedDocBlockBox.height).toBeCloseTo(height + 2 * border, 1); + } + ); + + test('nested embed synced doc should be rendered as card when depth >=1', async ({ + page, + }) => { + await page.evaluate(() => { + const { doc, collection } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + + const noteId = doc.addBlock('affine:note', {}, rootId); + doc.addBlock('affine:paragraph', {}, noteId); + + const doc2 = collection.createDoc({ id: 'doc2' }); + doc2.load(); + const rootId2 = doc2.addBlock('affine:page', { + title: new doc.Text('Doc 2'), + }); + + const noteId2 = doc2.addBlock('affine:note', {}, rootId2); + doc2.addBlock( + 'affine:paragraph', + { + text: new doc.Text('Hello from Doc 2'), + }, + noteId2 + ); + + const doc3 = collection.createDoc({ id: 'doc3' }); + doc3.load(); + const rootId3 = doc3.addBlock('affine:page', { + title: new doc.Text('Doc 3'), + }); + + const noteId3 = doc3.addBlock('affine:note', {}, rootId3); + doc3.addBlock( + 'affine:paragraph', + { + text: new doc.Text('Hello from Doc 3'), + }, + noteId3 + ); + + doc2.addBlock( + 'affine:embed-synced-doc', + { + pageId: 'doc3', + }, + noteId2 + ); + doc.addBlock( + 'affine:embed-synced-doc', + { + pageId: 'doc2', + }, + noteId + ); + }); + expect(await page.locator('affine-embed-synced-doc-block').count()).toBe(2); + expect(await page.locator('affine-paragraph').count()).toBe(2); + expect(await page.locator('affine-embed-synced-doc-card').count()).toBe(1); + expect(await page.locator('editor-host').count()).toBe(2); + }); + + test.describe('synced doc should be readonly', () => { + test('synced doc should be readonly', async ({ page }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + await createAndConvertToEmbedSyncedDoc(page); + const locator = page.locator('affine-embed-synced-doc-block'); + await locator.click(); + + const toolbar = page.locator('editor-toolbar'); + const openMenu = toolbar.getByRole('button', { name: 'Open' }); + await openMenu.click(); + + const button = toolbar.getByRole('button', { name: 'Open this doc' }); + await button.click(); + + await page.evaluate(async () => { + const { collection } = window; + const getDocCollection = () => { + for (const [id, doc] of collection.docs.entries()) { + if (id === 'doc:home') { + continue; + } + return doc; + } + return null; + }; + + const doc2Collection = getDocCollection(); + const doc2 = doc2Collection!.getDoc(); + const [noteBlock] = doc2!.getBlocksByFlavour('affine:note'); + const noteId = noteBlock.id; + + const databaseId = doc2.addBlock( + 'affine:database', + { + title: new doc2.Text('Database 1'), + }, + noteId + ); + const model = doc2.getBlockById(databaseId) as DatabaseBlockModel; + await new Promise(resolve => setTimeout(resolve, 100)); + const databaseBlock = document.querySelector('affine-database'); + const databaseService = databaseBlock?.service; + if (databaseService) { + databaseService.databaseViewInitEmpty( + model, + databaseService.viewPresets.tableViewMeta.type + ); + databaseService.applyColumnUpdate(model); + } + }); + + // go back to previous doc + await page.evaluate(() => { + const { collection, editor } = window; + editor.doc = collection.getDoc('doc:home')!; + }); + + const databaseFirstCell = page.locator( + '.affine-database-column-header.database-row' + ); + await databaseFirstCell.click({ force: true }); + const indicatorCount = await page + .locator('affine-drag-indicator') + .count(); + expect(indicatorCount).toBe(1); + }); + }); +}); diff --git a/blocksuite/tests-legacy/fixtures/smile.png b/blocksuite/tests-legacy/fixtures/smile.png new file mode 100644 index 0000000000000..1a51fe7204149 Binary files /dev/null and b/blocksuite/tests-legacy/fixtures/smile.png differ diff --git a/blocksuite/tests-legacy/format-bar.spec.ts b/blocksuite/tests-legacy/format-bar.spec.ts new file mode 100644 index 0000000000000..c9b7dd4fdc921 --- /dev/null +++ b/blocksuite/tests-legacy/format-bar.spec.ts @@ -0,0 +1,1027 @@ +import type { DeltaInsert } from '@inline/types.js'; +import { expect } from '@playwright/test'; + +import { + activeEmbed, + captureHistory, + dragBetweenCoords, + dragBetweenIndices, + enterPlaygroundRoom, + focusRichText, + focusTitle, + getBoundingBox, + getEditorHostLocator, + getPageSnapshot, + getSelectionRect, + initEmptyParagraphState, + initImageState, + initThreeParagraphs, + pasteByKeyboard, + pressArrowDown, + pressArrowUp, + pressEnter, + pressEscape, + pressTab, + scrollToBottom, + scrollToTop, + selectAllBlocksByKeyboard, + selectAllByKeyboard, + setInlineRangeInInlineEditor, + setSelection, + switchReadonly, + type, + undoByKeyboard, + updateBlockType, + waitNextFrame, + withPressKey, +} from './utils/actions/index.js'; +import { + assertAlmostEqual, + assertBlockChildrenIds, + assertExists, + assertLocatorVisible, + assertRichImage, + assertRichTextInlineRange, + assertRichTexts, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; +import { getFormatBar } from './utils/query.js'; + +test('should format quick bar show when select text', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await dragBetweenIndices(page, [0, 0], [2, 3]); + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + + const box = await formatBar.boundingBox(); + if (!box) { + throw new Error("formatBar doesn't exist"); + } + const rect = await getSelectionRect(page); + assertAlmostEqual(box.x - rect.left, -98, 5); + assertAlmostEqual(box.y - rect.bottom, 10, 5); + + // Click the edge of the format quick bar + await page.mouse.click(box.x + 4, box.y + box.height / 2); + // Even not any button is clicked, the format quick bar should't be hidden + await expect(formatBar).toBeVisible(); + + const noteEl = page.locator('affine-note'); + const { x, y } = await getBoundingBox(noteEl); + await page.mouse.click(x + 100, y + 20); + await expect(formatBar).not.toBeVisible(); +}); + +test('should format quick bar show when clicking drag handle', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + const locator = page.locator('affine-paragraph').first(); + await locator.hover(); + const dragHandle = page.locator('.affine-drag-handle-grabber'); + const dragHandleRect = await dragHandle.boundingBox(); + assertExists(dragHandleRect); + await dragHandle.click(); + + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + + const box = await formatBar.boundingBox(); + if (!box) { + throw new Error("formatBar doesn't exist"); + } + assertAlmostEqual(box.x, 251, 5); + assertAlmostEqual(box.y - dragHandleRect.y, -55.5, 5); +}); + +test('should format quick bar show when select text by keyboard', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello world'); + await withPressKey(page, 'Shift', async () => { + let i = 10; + while (i--) { + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + } + }); + + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + + const formatBarBox = await formatBar.boundingBox(); + if (!formatBarBox) { + throw new Error("formatBar doesn't exist"); + } + let selectionRect = await getSelectionRect(page); + assertAlmostEqual(formatBarBox.x - selectionRect.x, -107, 3); + assertAlmostEqual( + formatBarBox.y + formatBarBox.height - selectionRect.top, + -10, + 3 + ); + + await page.keyboard.press('ArrowLeft'); + await expect(formatBar).not.toBeVisible(); + + await withPressKey(page, 'Shift', async () => { + let i = 10; + while (i--) { + await page.keyboard.press('ArrowRight'); + await waitNextFrame(page); + } + }); + + await expect(formatBar).toBeVisible(); + + const rightBox = await formatBar.boundingBox(); + if (!rightBox) { + throw new Error("formatBar doesn't exist"); + } + // The x position of the format quick bar depends on the font size + // so there are slight differences in different environments + selectionRect = await getSelectionRect(page); + assertAlmostEqual(formatBarBox.x - selectionRect.x, -107, 3); + assertAlmostEqual( + formatBarBox.y + formatBarBox.height - selectionRect.top, + -10, + 3 + ); +}); + +test('should format quick bar can only display one at a time', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await dragBetweenIndices(page, [0, 3], [0, 0]); + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + + await dragBetweenIndices(page, [2, 0], [2, 3]); + await expect(formatBar).toHaveCount(1); +}); + +test('should format quick bar hide when type text', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await dragBetweenIndices(page, [0, 0], [2, 3]); + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + await type(page, '1'); + await expect(formatBar).not.toBeVisible(); +}); + +test('should format quick bar be able to format text', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + // drag only the `456` paragraph + await dragBetweenIndices(page, [1, 0], [1, 3]); + + const { boldBtn, italicBtn, underlineBtn, strikeBtn, codeBtn } = + getFormatBar(page); + + await expect(boldBtn).not.toHaveAttribute('active', ''); + await expect(italicBtn).not.toHaveAttribute('active', ''); + await expect(underlineBtn).not.toHaveAttribute('active', ''); + await expect(strikeBtn).not.toHaveAttribute('active', ''); + await expect(codeBtn).not.toHaveAttribute('active', ''); + + await boldBtn.click(); + await italicBtn.click(); + await underlineBtn.click(); + await strikeBtn.click(); + await codeBtn.click(); + + // The button should be active after click + await expect(boldBtn).toHaveAttribute('active', ''); + await expect(italicBtn).toHaveAttribute('active', ''); + await expect(underlineBtn).toHaveAttribute('active', ''); + await expect(strikeBtn).toHaveAttribute('active', ''); + await expect(codeBtn).toHaveAttribute('active', ''); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await boldBtn.click(); + await underlineBtn.click(); + await codeBtn.click(); + + await waitNextFrame(page); + + // The bold button should be inactive after click again + await expect(boldBtn).not.toHaveAttribute('active', ''); + await expect(italicBtn).toHaveAttribute('active', ''); + await expect(underlineBtn).not.toHaveAttribute('active', ''); + await expect(strikeBtn).toHaveAttribute('active', ''); + await expect(codeBtn).not.toHaveAttribute('active', ''); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); + +test('should format quick bar be able to change background color', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + // select `456` paragraph by dragging + await dragBetweenIndices(page, [1, 0], [1, 3]); + + const { highlight } = getFormatBar(page); + + await highlight.highlightBtn.hover(); + await expect(highlight.redForegroundBtn).toBeVisible(); + await expect(highlight.highlightBtn).toHaveAttribute( + 'data-last-used', + 'unset' + ); + await highlight.redForegroundBtn.click(); + await expect(highlight.highlightBtn).toHaveAttribute( + 'data-last-used', + 'var(--affine-text-highlight-foreground-red)' + ); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + // select `123` paragraph by ctrl + a + await focusRichText(page); + await selectAllByKeyboard(page); + // use last used color + await highlight.highlightBtn.click(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_select_all.json` + ); + + await expect(highlight.defaultColorBtn).toBeVisible(); + await highlight.defaultColorBtn.click(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_default_color.json` + ); +}); + +test('should format quick bar be able to format text when select multiple line', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await dragBetweenIndices(page, [0, 0], [2, 3]); + + const { boldBtn } = getFormatBar(page); + await expect(boldBtn).not.toHaveAttribute('active', ''); + await boldBtn.click(); + + // The bold button should be active after click + await expect(boldBtn).toHaveAttribute('active', ''); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await boldBtn.click(); + await expect(boldBtn).not.toHaveAttribute('active', ''); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); + +test('should format quick bar be able to link text', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + // drag only the `456` paragraph + await dragBetweenIndices(page, [1, 0], [1, 3]); + + const { linkBtn } = getFormatBar(page); + await expect(linkBtn).not.toHaveAttribute('active', ''); + await linkBtn.click(); + + const linkPopoverInput = page.locator('.affine-link-popover-input'); + await expect(linkPopoverInput).toBeVisible(); + + await type(page, 'https://www.example.com'); + await pressEnter(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + // FIXME: remove this + await focusRichText(page); + await setSelection(page, 3, 0, 3, 3); + // The link button should be active after click + await expect(linkBtn).toHaveAttribute('active', ''); + await linkBtn.click(); + await waitNextFrame(page); + await expect(linkBtn).not.toHaveAttribute('active', ''); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); + +test('should format quick bar be able to change to heading paragraph type', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + // drag only the `456` paragraph + await dragBetweenIndices(page, [0, 0], [0, 3]); + + const { openParagraphMenu, h1Btn, bulletedBtn } = getFormatBar(page); + await openParagraphMenu(); + + await expect(h1Btn).toBeVisible(); + await h1Btn.click(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await bulletedBtn.click(); + await openParagraphMenu(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_bulleted.json` + ); + + const { textBtn } = getFormatBar(page); + await textBtn.click(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); + await page.waitForTimeout(10); + // The paragraph button should prevent selection after click + await assertRichTextInlineRange(page, 0, 0, 3); +}); + +test('should format quick bar show when double click text', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + const editorHost = getEditorHostLocator(page); + const richText = editorHost.locator('rich-text').nth(0); + await richText.dblclick({ + position: { x: 10, y: 10 }, + }); + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); +}); + +test('should format quick bar not show at readonly mode', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await switchReadonly(page); + + await dragBetweenIndices(page, [0, 0], [2, 3]); + const { formatBar } = getFormatBar(page); + await expect(formatBar).not.toBeVisible(); + + const editorHost = getEditorHostLocator(page); + const richText = editorHost.locator('rich-text').nth(0); + await richText.dblclick({ + position: { x: 10, y: 10 }, + }); + await expect(formatBar).not.toBeVisible(); +}); + +test('should format bar follow scroll', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + for (let i = 0; i < 30; i++) { + await pressEnter(page); + } + + await scrollToTop(page); + + await dragBetweenIndices(page, [0, 0], [2, 3]); + const { formatBar, boldBtn } = getFormatBar(page); + await assertLocatorVisible(page, formatBar); + + await scrollToBottom(page); + + await assertLocatorVisible(page, formatBar, false); + + // should format bar follow scroll after click bold button + await scrollToTop(page); + await assertLocatorVisible(page, formatBar); + await boldBtn.click(); + await scrollToBottom(page); + await assertLocatorVisible(page, formatBar, false); + + // should format bar follow scroll after transform text type + await scrollToTop(page); + await assertLocatorVisible(page, formatBar); + await updateBlockType(page, 'affine:list', 'bulleted'); + await scrollToBottom(page); + await assertLocatorVisible(page, formatBar, false); +}); + +test('should format quick bar position correct at the start of second line', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + const text = new doc.Text('a'.repeat(100)); + const paragraphId = doc.addBlock('affine:paragraph', { text }, note); + return paragraphId; + }); + // await focusRichText(page); + const editorHost = getEditorHostLocator(page); + const locator = editorHost.locator('.inline-editor').nth(0); + const textBox = await locator.boundingBox(); + if (!textBox) { + throw new Error("Can't get bounding box"); + } + // Drag to the start of the second line + await dragBetweenCoords( + page, + { x: textBox.x + textBox.width - 1, y: textBox.y + textBox.height - 1 }, + { x: textBox.x, y: textBox.y + textBox.height - 1 } + ); + + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + await waitNextFrame(page); + + const formatBox = await formatBar.boundingBox(); + if (!formatBox) { + throw new Error("formatBar doesn't exist"); + } + const selectionRect = await getSelectionRect(page); + assertAlmostEqual(formatBox.x - selectionRect.x, -99, 5); + assertAlmostEqual(formatBox.y + formatBox.height - selectionRect.top, 68, 5); +}); + +test('should format quick bar action status updated while undo', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'helloworld'); + await captureHistory(page); + await dragBetweenIndices(page, [0, 1], [0, 6]); + + const { formatBar, boldBtn } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + await expect(boldBtn).toBeVisible(); + + await expect(boldBtn).not.toHaveAttribute('active', ''); + await boldBtn.click(); + await expect(boldBtn).toHaveAttribute('active', ''); + + await undoByKeyboard(page); + await expect(formatBar).toBeVisible(); + await expect(boldBtn).not.toHaveAttribute('active', ''); +}); + +test('should format quick bar work in single block selection', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await dragBetweenIndices( + page, + [1, 0], + [1, 3], + { x: -26 - 24, y: -10 }, + { x: 0, y: 0 } + ); + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(1); + + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + + const formatRect = await formatBar.boundingBox(); + const selectionRect = await blockSelections.boundingBox(); + assertExists(formatRect); + assertExists(selectionRect); + assertAlmostEqual(formatRect.x - selectionRect.x, 147.5, 10); + assertAlmostEqual(formatRect.y - selectionRect.y, 33, 10); + + const boldBtn = formatBar.getByTestId('bold'); + await boldBtn.click(); + const italicBtn = formatBar.getByTestId('italic'); + await italicBtn.click(); + const underlineBtn = formatBar.getByTestId('underline'); + await underlineBtn.click(); + //FIXME: trt to cancel italic + // Cancel italic + // await italicBtn.click(); + + await expect(blockSelections).toHaveCount(1); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); + + const noteEl = page.locator('affine-note'); + const { x, y, width, height } = await getBoundingBox(noteEl); + await page.mouse.click(x + width / 2, y + height / 2); + await expect(formatBar).not.toBeVisible(); +}); + +test('should format quick bar work in multiple block selection', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await dragBetweenIndices( + page, + [2, 3], + [0, 0], + { x: 20, y: 20 }, + { x: 0, y: 0 } + ); + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(3); + + const formatBarController = getFormatBar(page); + await expect(formatBarController.formatBar).toBeVisible(); + + const box = await formatBarController.formatBar.boundingBox(); + if (!box) { + throw new Error("formatBar doesn't exist"); + } + const rect = await blockSelections.first().boundingBox(); + assertExists(rect); + assertAlmostEqual(box.x - rect.x, 147.5, 10); + assertAlmostEqual(box.y - rect.y, 99, 10); + + await formatBarController.boldBtn.click(); + await formatBarController.italicBtn.click(); + await formatBarController.underlineBtn.click(); + // Cancel italic + await formatBarController.italicBtn.click(); + + await expect(blockSelections).toHaveCount(3); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); + + const noteEl = page.locator('affine-note'); + const { x, y, width, height } = await getBoundingBox(noteEl); + await page.mouse.click(x + width / 2, y + height / 2); + await expect(formatBarController.formatBar).not.toBeVisible(); +}); + +test('should format quick bar with block selection works when update block type', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await dragBetweenIndices( + page, + [2, 3], + [0, 0], + { x: 20, y: 20 }, + { x: 0, y: 0 } + ); + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(3); + + const formatBarController = getFormatBar(page); + await expect(formatBarController.formatBar).toBeVisible(); + + await formatBarController.openParagraphMenu(); + await formatBarController.bulletedBtn.click(); + await expect(blockSelections).toHaveCount(3); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await expect(formatBarController.formatBar).toBeVisible(); + await formatBarController.h1Btn.click(); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); + await expect(formatBarController.formatBar).toBeVisible(); + await expect(blockSelections).toHaveCount(3); + + const noteEl = page.locator('affine-note'); + const { x, y, width, height } = await getBoundingBox(noteEl); + await page.mouse.click(x + width / 2, y + height / 2); + await expect(formatBarController.formatBar).not.toBeVisible(); +}); + +test('should format quick bar show after convert to code block', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + const formatBarController = getFormatBar(page); + await dragBetweenIndices( + page, + [2, 3], + [0, 0], + { x: 20, y: 20 }, + { x: 0, y: 0 } + ); + await expect(formatBarController.formatBar).toBeVisible(); + await expect(formatBarController.formatBar).toBeInViewport(); + + await formatBarController.openParagraphMenu(); + await formatBarController.codeBlockBtn.click(); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); +}); + +test('buttons in format quick bar should have correct active styles', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + // `45` + await setInlineRangeInInlineEditor( + page, + { + index: 0, + length: 2, + }, + 2 + ); + const { codeBtn } = getFormatBar(page); + await codeBtn.click(); + await expect(codeBtn).toHaveAttribute('active', ''); + + // `456` + await setInlineRangeInInlineEditor( + page, + { + index: 0, + length: 3, + }, + 2 + ); + await expect(codeBtn).not.toHaveAttribute('active', ''); +}); + +test('should format bar style active correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + const delta = [ + { insert: '1', attributes: { bold: true, italic: true } }, + { insert: '2', attributes: { bold: true, underline: true } }, + { insert: '3', attributes: { bold: true, code: true } }, + ]; + const text = new doc.Text(delta as DeltaInsert[]); + doc.addBlock('affine:paragraph', { text }, note); + }); + + const { boldBtn, codeBtn, underlineBtn } = getFormatBar(page); + await dragBetweenIndices(page, [0, 0], [0, 3]); + await expect(boldBtn).toHaveAttribute('active', ''); + await expect(underlineBtn).not.toHaveAttribute('active', ''); + await expect(codeBtn).not.toHaveAttribute('active', ''); + + await underlineBtn.click(); + await expect(underlineBtn).toHaveAttribute('active', ''); + await expect(boldBtn).toHaveAttribute('active', ''); + await expect(codeBtn).not.toHaveAttribute('active', ''); +}); + +test('should format quick bar show when double click button', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await dragBetweenIndices(page, [0, 0], [2, 3]); + const { formatBar, boldBtn } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + await boldBtn.dblclick({ + delay: 100, + }); + await expect(formatBar).toBeVisible(); +}); + +test('should the database action icon show correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + const databaseAction = page.getByTestId('convert-to-database'); + + await focusRichText(page); + await dragBetweenIndices( + page, + [2, 3], + [0, 0], + { x: 20, y: 20 }, + { x: 0, y: 0 } + ); + await expect(databaseAction).toBeVisible(); + + await focusRichText(page, 2); + await pressEnter(page); + await updateBlockType(page, 'affine:code'); + const codeBlock = page.locator('affine-code'); + const codeBox = await codeBlock.boundingBox(); + if (!codeBox) throw new Error('Missing code block box'); + + await page.keyboard.type('hello world'); + const position = { + startX: codeBox.x, + startY: codeBox.y + codeBox.height / 2, + endX: codeBox.x + codeBox.width, + endY: codeBox.y + codeBox.height / 2, + }; + await page.mouse.click(position.endX + 150, position.endY + 150); + await dragBetweenCoords( + page, + { x: position.startX + 10, y: position.startY - 10 }, + { x: position.endX, y: position.endY }, + { steps: 20 } + ); + await expect(databaseAction).not.toBeVisible(); +}); + +test('should convert to database work', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await dragBetweenIndices( + page, + [2, 3], + [0, 0], + { x: 20, y: 20 }, + { x: 0, y: 0 } + ); + const databaseAction = page.getByTestId('convert-to-database'); + await databaseAction.click(); + const database = page.locator('affine-database'); + await expect(database).toBeVisible(); + const rows = page.locator('.affine-database-block-row'); + expect(await rows.count()).toBe(3); +}); + +test('should show format-quick-bar and select all text of the block when triple clicking on text', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello world'); + + const editorHost = getEditorHostLocator(page); + const locator = editorHost.locator('.inline-editor').nth(0); + const textBox = await locator.boundingBox(); + if (!textBox) { + throw new Error("Can't get bounding box"); + } + + await page.mouse.dblclick(textBox.x + 10, textBox.y + textBox.height / 2); + + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + + await assertRichTextInlineRange(page, 0, 0, 5); + + const noteEl = page.locator('affine-note'); + const { x, y, width, height } = await getBoundingBox(noteEl); + await page.mouse.click(x + width / 2, y + height / 2); + + await expect(formatBar).toBeHidden(); + + await page.mouse.move(textBox.x + 10, textBox.y + textBox.height / 2); + + const options = { + clickCount: 1, + }; + await page.mouse.down(options); + await page.mouse.up(options); + + options.clickCount++; + await page.mouse.down(options); + await page.mouse.up(options); + + options.clickCount++; + await page.mouse.down(options); + await page.mouse.up(options); + + await assertRichTextInlineRange(page, 0, 0, 'hello world'.length); +}); + +test('should update the format quick bar state when there is a change in keyboard selection', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + const delta = [ + { insert: '1', attributes: { bold: true } }, + { insert: '2', attributes: { bold: true } }, + { insert: '3', attributes: { bold: false } }, + ]; + const text = new doc.Text(delta as DeltaInsert[]); + doc.addBlock('affine:paragraph', { text }, note); + }); + await focusTitle(page); + await pressArrowDown(page); + + const formatBar = getFormatBar(page); + await withPressKey(page, 'Shift', async () => { + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await expect(formatBar.boldBtn).toHaveAttribute('active', ''); + await page.keyboard.press('ArrowRight'); + await expect(formatBar.boldBtn).not.toHaveAttribute('active', ''); + }); +}); + +test('format quick bar should not break cursor jumping', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await dragBetweenIndices(page, [1, 3], [1, 2]); + + const { formatBar } = getFormatBar(page); + await expect(formatBar).toBeVisible(); + + await pressArrowUp(page); + await type(page, '0'); + await assertRichTexts(page, ['1203', '456', '789']); + + await dragBetweenIndices(page, [1, 3], [1, 2]); + await pressArrowDown(page); + await type(page, '0'); + await assertRichTexts(page, ['1203', '456', '7809']); +}); + +test('selecting image should not show format bar', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/4535', + }); + await enterPlaygroundRoom(page); + await initImageState(page); + await assertRichImage(page, 1); + await activeEmbed(page); + await waitNextFrame(page); + const { formatBar } = getFormatBar(page); + await expect(formatBar).not.toBeVisible(); +}); + +test('create linked doc from block selection with format bar', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await focusRichText(page, 1); + await pressTab(page); + await assertRichTexts(page, ['123', '456', '789']); + await assertBlockChildrenIds(page, '1', ['2', '4']); + await assertBlockChildrenIds(page, '2', ['3']); + + await selectAllBlocksByKeyboard(page); + await waitNextFrame(page, 200); + + const blockSelections = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(blockSelections).toHaveCount(2); + + const { createLinkedDocBtn } = getFormatBar(page); + expect(await createLinkedDocBtn.isVisible()).toBe(true); + await createLinkedDocBtn.click(); + + const linkedDocBlock = page.locator('affine-embed-linked-doc-block'); + await expect(linkedDocBlock).toHaveCount(1); + + const linkedDocBox = await linkedDocBlock.boundingBox(); + assertExists(linkedDocBox); + await page.mouse.dblclick( + linkedDocBox.x + linkedDocBox.width / 2, + linkedDocBox.y + linkedDocBox.height / 2 + ); + await waitNextFrame(page, 200); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); +}); + +test.describe('more menu button', () => { + test('should be able to perform the copy action', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + // drag only the `456` paragraph + await dragBetweenIndices(page, [1, 0], [1, 3]); + + const { openMoreMenu, copyBtn } = getFormatBar(page); + await openMoreMenu(); + await expect(copyBtn).toBeVisible(); + await assertRichTextInlineRange(page, 1, 0, 3); + await copyBtn.click(); + await assertRichTextInlineRange(page, 1, 0, 3); + + await focusRichText(page, 1); + await pasteByKeyboard(page); + await waitNextFrame(page); + + await assertRichTexts(page, ['123', '456456', '789']); + }); + + test('should be able to perform the duplicate action', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await focusRichText(page, 1); + await pressEscape(page); + + const { openMoreMenu, duplicateBtn } = getFormatBar(page); + await openMoreMenu(); + await expect(duplicateBtn).toBeVisible(); + await duplicateBtn.click(); + + await waitNextFrame(page); + + await assertRichTexts(page, ['123', '456', '456', '789']); + }); + + test('should be able to perform the delete action', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await focusRichText(page, 1); + await pressEscape(page); + + const { openMoreMenu, deleteBtn } = getFormatBar(page); + await openMoreMenu(); + await expect(deleteBtn).toBeVisible(); + await deleteBtn.click(); + + await waitNextFrame(page); + + await assertRichTexts(page, ['123', '789']); + }); +}); diff --git a/blocksuite/tests-legacy/fragments/frame-panel.spec.ts b/blocksuite/tests-legacy/fragments/frame-panel.spec.ts new file mode 100644 index 0000000000000..451e2e8275d49 --- /dev/null +++ b/blocksuite/tests-legacy/fragments/frame-panel.spec.ts @@ -0,0 +1,356 @@ +import { expect, type Locator, type Page } from '@playwright/test'; +import { dragBetweenCoords } from 'utils/actions/drag.js'; +import { + assertEdgelessNonSelectedRect, + assertEdgelessSelectedRect, + assertZoomLevel, +} from 'utils/asserts.js'; + +import { + addBasicShapeElement, + addNote, + createNote, + createShapeElement, + dragBetweenViewCoords, + edgelessCommonSetup, + enterPresentationMode, + getZoomLevel, + setEdgelessTool, + Shape, + switchEditorMode, + toggleFramePanel, +} from '../utils/actions/edgeless.js'; +import { waitNextFrame } from '../utils/actions/index.js'; +import { test } from '../utils/playwright.js'; + +async function dragFrameCard( + page: Page, + fromCard: Locator, + toCard: Locator, + direction: 'up' | 'down' = 'down' +) { + const fromRect = await fromCard.boundingBox(); + const toRect = await toCard.boundingBox(); + // drag to the center of the toCard + const center = { x: toRect!.width / 2, y: toRect!.height / 2 }; + const offset = direction === 'up' ? { x: 0, y: -20 } : { x: 0, y: 20 }; + await page.mouse.move(fromRect!.x + center.x, fromRect!.y + center.y); + await page.mouse.down(); + await page.mouse.move( + toRect!.x + center.x + offset.x, + toRect!.y + center.y + offset.y, + { steps: 10 } + ); + await page.mouse.up(); +} + +test.describe('frame panel', () => { + test('should display empty placeholder when no frames', async ({ page }) => { + await edgelessCommonSetup(page); + await toggleFramePanel(page); + const frameCards = page.locator('affine-frame-card'); + expect(await frameCards.count()).toBe(0); + + const placeholder = page.locator('.no-frame-placeholder'); + expect(await placeholder.isVisible()).toBeTruthy(); + }); + + test('should display frame cards when there are frames', async ({ page }) => { + await edgelessCommonSetup(page); + await toggleFramePanel(page); + + await addBasicShapeElement( + page, + { x: 300, y: 300 }, + { x: 350, y: 350 }, + Shape.Square + ); + + await addNote(page, 'hello', 150, 500); + + await page.mouse.click(0, 0); + + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords(page, { x: 250, y: 250 }, { x: 360, y: 360 }); + + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 }); + + const frames = page.locator('affine-frame'); + expect(await frames.count()).toBe(2); + const frameCards = page.locator('affine-frame-card'); + expect(await frameCards.count()).toBe(2); + }); + + test('should render edgeless note correctly in frame preview', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await toggleFramePanel(page); + + await addNote(page, 'hello', 150, 500); + + await page.mouse.click(0, 0); + + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 }); + await waitNextFrame(page, 100); + + const frames = page.locator('affine-frame'); + expect(await frames.count()).toBe(1); + const frameCards = page.locator('affine-frame-card'); + expect(await frameCards.count()).toBe(1); + const edgelessNote = page.locator('affine-frame-card affine-edgeless-note'); + expect(await edgelessNote.count()).toBe(1); + }); + + test('should update panel when frames change', async ({ page }) => { + await edgelessCommonSetup(page); + await toggleFramePanel(page); + const frameCards = page.locator('affine-frame-card'); + expect(await frameCards.count()).toBe(0); + + await addNote(page, 'hello', 150, 500); + + await page.mouse.click(0, 0); + + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 }); + + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords(page, { x: 50, y: 300 }, { x: 120, y: 400 }); + await waitNextFrame(page); + + const frames = page.locator('affine-frame'); + expect(await frames.count()).toBe(2); + expect(await frameCards.count()).toBe(2); + + await page.mouse.click(50, 300); + await page.keyboard.press('Delete'); + await waitNextFrame(page); + + expect(await frames.count()).toBe(1); + expect(await frameCards.count()).toBe(1); + }); + + test.describe('frame panel behavior after mode switch', () => { + async function setupFrameTest(page: Page) { + await edgelessCommonSetup(page); + await toggleFramePanel(page); + + await addNote(page, 'hello', 150, 500); + await page.mouse.click(0, 0); + await waitNextFrame(page, 100); + + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords( + page, + { x: 100, y: 440 }, + { x: 640, y: 600 }, + { steps: 10 } + ); + await waitNextFrame(page, 100); + + const edgelessNote = page.locator( + 'affine-frame-card affine-edgeless-note' + ); + expect(await edgelessNote.count()).toBe(1); + + return edgelessNote; + } + + test('should render edgeless note correctly after mode switch', async ({ + page, + }) => { + const edgelessNote = await setupFrameTest(page); + + const initialNoteRect = await edgelessNote.boundingBox(); + expect(initialNoteRect).not.toBeNull(); + + const { + width: noteWidth, + height: noteHeight, + x: noteX, + y: noteY, + } = initialNoteRect!; + + const checkNoteRect = async () => { + expect(await edgelessNote.count()).toBe(1); + + const newNoteRect = await edgelessNote.boundingBox(); + expect(newNoteRect).not.toBeNull(); + + expect(newNoteRect!.width).toBe(noteWidth); + expect(newNoteRect!.height).toBe(noteHeight); + expect(newNoteRect!.x).toBe(noteX); + expect(newNoteRect!.y).toBe(noteY); + }; + + await switchEditorMode(page); + await checkNoteRect(); + + await switchEditorMode(page); + await checkNoteRect(); + }); + + test('should update frame preview when note is moved', async ({ page }) => { + const edgelessNote = await setupFrameTest(page); + + const initialNoteRect = await edgelessNote.boundingBox(); + expect(initialNoteRect).not.toBeNull(); + + await switchEditorMode(page); + await switchEditorMode(page); + + async function moveNoteAndCheck( + start: { x: number; y: number }, + end: { x: number; y: number }, + comparison: 'greaterThan' | 'lessThan' + ) { + await page.mouse.move(start.x, start.y); + await page.mouse.down(); + await page.mouse.move(end.x, end.y); + await page.mouse.up(); + await waitNextFrame(page); + + const newNoteRect = await edgelessNote.boundingBox(); + expect(newNoteRect).not.toBeNull(); + + if (comparison === 'greaterThan') { + expect(newNoteRect!.x).toBeGreaterThan(initialNoteRect!.x); + expect(newNoteRect!.y).toBeGreaterThan(initialNoteRect!.y); + } else { + expect(newNoteRect!.x).toBeLessThan(initialNoteRect!.x); + expect(newNoteRect!.y).toBeLessThan(initialNoteRect!.y); + } + } + + // Move the note to the right + await moveNoteAndCheck( + { x: 150, y: 500 }, + { x: 200, y: 550 }, + 'greaterThan' + ); + + // Move the note back to the left + await moveNoteAndCheck( + { x: 200, y: 550 }, + { x: 100, y: 450 }, + 'lessThan' + ); + + // Move the note diagonally + await moveNoteAndCheck( + { x: 100, y: 450 }, + { x: 250, y: 600 }, + 'greaterThan' + ); + }); + }); + + test.describe('select and de-select frame', () => { + async function setupFrameTest(page: Page) { + await edgelessCommonSetup(page); + await toggleFramePanel(page); + + await addNote(page, 'hello', 150, 500); + + await page.mouse.click(0, 0); + + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 640, y: 600 }); + await waitNextFrame(page); + + const frames = page.locator('affine-frame'); + const frameCards = page.locator('affine-frame-card'); + expect(await frames.count()).toBe(1); + expect(await frameCards.count()).toBe(1); + + return { frames, frameCards }; + } + + test('by click on frame card', async ({ page }) => { + const { frameCards } = await setupFrameTest(page); + + // click on the first frame card + await frameCards.nth(0).click(); + await assertEdgelessSelectedRect(page, [100, 440, 540, 160]); + + await frameCards.nth(0).click(); + await assertEdgelessNonSelectedRect(page); + }); + + test('by click on blank area', async ({ page }) => { + const { frameCards } = await setupFrameTest(page); + + // click on the first frame card + await frameCards.nth(0).click(); + await assertEdgelessSelectedRect(page, [100, 440, 540, 160]); + + const framePanel = page.locator('.frame-panel-container'); + const panelRect = await framePanel.boundingBox(); + expect(panelRect).not.toBeNull(); + const { x, y, width, height } = panelRect!; + await page.mouse.click(x + width / 2, y + height / 2); + await assertEdgelessNonSelectedRect(page); + }); + }); + + test('should fit the viewport to the frame when double click frame card', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await toggleFramePanel(page); + + await assertZoomLevel(page, 100); + + await addNote(page, 'hello', 150, 500); + await page.mouse.click(0, 0); + + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 }); + await waitNextFrame(page); + + const frameCards = page.locator('affine-frame-card'); + await frameCards.nth(0).dblclick(); + + const zoomLevel = await getZoomLevel(page); + expect(zoomLevel).toBeGreaterThan(100); + }); + + test('should reorder frames when drag and drop frame card', async ({ + page, + }) => { + await edgelessCommonSetup(page); + await createShapeElement(page, [100, 100], [200, 200], Shape.Square); + await createNote(page, [300, 100], 'hello'); + + // Frame shape + await setEdgelessTool(page, 'frame'); + await dragBetweenViewCoords(page, [80, 80], [220, 220]); + await waitNextFrame(page, 100); + + // Frame note + await setEdgelessTool(page, 'frame'); + await dragBetweenViewCoords(page, [240, 0], [800, 200]); + + expect(await page.locator('affine-frame').count()).toBe(2); + + await toggleFramePanel(page); + + const frameCards = page.locator('affine-frame-card'); + expect(await frameCards.count()).toBe(2); + + // Drag the first frame card to the second + await dragFrameCard(page, frameCards.nth(0), frameCards.nth(1)); + + await enterPresentationMode(page); + await waitNextFrame(page, 100); + + // Check if frame contains note now is the first + const edgelessNote = page.locator( + 'affine-edgeless-root affine-edgeless-note' + ); + await expect(edgelessNote).toBeVisible(); + }); +}); diff --git a/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts b/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts new file mode 100644 index 0000000000000..2c4e9f669aa86 --- /dev/null +++ b/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts @@ -0,0 +1,361 @@ +import { NoteDisplayMode } from '@blocksuite/affine-model'; +import { expect, type Locator, type Page } from '@playwright/test'; +import { + addNote, + changeNoteDisplayModeWithId, + switchEditorMode, + triggerComponentToolbarAction, + zoomResetByKeyboard, +} from 'utils/actions/edgeless.js'; +import { pressBackspace, pressEnter, type } from 'utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + focusRichTextEnd, + focusTitle, + getEditorHostLocator, + initEmptyEdgelessState, + initEmptyParagraphState, + waitNextFrame, +} from 'utils/actions/misc.js'; +import { assertRichTexts } from 'utils/asserts.js'; + +import { test } from '../../utils/playwright.js'; +import { + createHeadingsWithGap, + getVerticalCenterFromLocator, +} from './utils.js'; + +test.describe('toc-panel', () => { + async function toggleTocPanel(page: Page) { + await page.click('sl-button:text("Test Operations")'); + await page.click('sl-menu-item:text("Toggle Outline Panel")'); + await waitNextFrame(page); + const panel = page.locator('affine-outline-panel'); + await expect(panel).toBeVisible(); + + return panel; + } + + function getHeading(panel: Locator, level: number) { + return panel.locator(`affine-outline-panel-body .h${level} > span`); + } + + function getTitle(panel: Locator) { + return panel.locator(`affine-outline-panel-body .title`); + } + + async function toggleNoteSorting(page: Page) { + const enableSortingButton = page.locator( + '.outline-panel-header-container .note-sorting-button' + ); + await enableSortingButton.click(); + } + + async function dragNoteCard(page: Page, fromCard: Locator, toCard: Locator) { + const fromRect = await fromCard.boundingBox(); + const toRect = await toCard.boundingBox(); + + await page.mouse.move(fromRect!.x + 10, fromRect!.y + 10); + await page.mouse.down(); + await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 10 }); + await page.mouse.up(); + } + + test('should display placeholder when no headings', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const panel = await toggleTocPanel(page); + + const noHeadingPlaceholder = panel.locator('.note-placeholder'); + + await focusTitle(page); + await type(page, 'Title'); + await focusRichTextEnd(page); + await type(page, 'Hello World'); + + await expect(noHeadingPlaceholder).toBeVisible(); + }); + + test('should not display empty when there are only empty headings', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const panel = await toggleTocPanel(page); + + await focusTitle(page); + await type(page, 'Title'); + await focusRichTextEnd(page); + + // heading 1 to 6 + for (let i = 1; i <= 6; i++) { + await type(page, `${'#'.repeat(i)} `); + await pressEnter(page); + await expect(getHeading(panel, i)).toBeHidden(); + } + + // Title also should be hidden + await expect(getTitle(panel)).toBeHidden(); + }); + + test('should display title and headings when there are non-empty headings in editor', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const panel = await toggleTocPanel(page); + + await focusRichTextEnd(page); + + // heading 1 to 6 + for (let i = 1; i <= 6; i++) { + await type(page, `${'#'.repeat(i)} `); + await type(page, `Heading ${i}`); + await pressEnter(page); + + const heading = getHeading(panel, i); + await expect(heading).toBeVisible(); + await expect(heading).toContainText(`Heading ${i}`); + } + + const title = getTitle(panel); + await expect(title).toBeHidden(); + await focusTitle(page); + await type(page, 'Title'); + await expect(title).toHaveText('Title'); + + // heading 1 to 6 + for (let i = 1; i <= 6; i++) { + const heading = getHeading(panel, i); + await expect(heading).toBeVisible(); + await expect(heading).toContainText(`Heading ${i}`); + } + }); + + test('should update headings', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const panel = await toggleTocPanel(page); + + await focusRichTextEnd(page); + + const h1 = getHeading(panel, 1); + + await type(page, '# Heading 1'); + await expect(h1).toContainText('Heading 1'); + + await pressBackspace(page, 'Heading 1'.length); + await expect(h1).toBeHidden(); + await type(page, 'Hello World'); + await expect(h1).toContainText('Hello World'); + + const title = getTitle(panel); + + await focusTitle(page); + await type(page, 'Title'); + await expect(title).toContainText('Title'); + + await pressBackspace(page, 2); + await expect(title).toContainText('Tit'); + }); + + test('should add padding to sub-headings', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const panel = await toggleTocPanel(page); + + await focusRichTextEnd(page); + + await type(page, '# Heading 1'); + await pressEnter(page); + + await type(page, '## Heading 2'); + await pressEnter(page); + + const h1 = getHeading(panel, 1); + const h2 = getHeading(panel, 2); + + const h1Rect = await h1.boundingBox(); + const h2Rect = await h2.boundingBox(); + + expect(h1Rect).not.toBeNull(); + expect(h2Rect).not.toBeNull(); + + expect(h1Rect!.x).toBeLessThan(h2Rect!.x); + }); + + test('should highlight heading when scroll to area before viewport center', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const editor = getEditorHostLocator(page); + const panel = await toggleTocPanel(page); + + await focusRichTextEnd(page); + const headings = await createHeadingsWithGap(page); + await editor.locator('.inline-editor').first().scrollIntoViewIfNeeded(); + + const viewportCenter = await getVerticalCenterFromLocator( + page.locator('body') + ); + + const activeHeadingContainer = panel.locator( + 'affine-outline-panel-body .active' + ); + + for (let i = 0; i < headings.length; i++) { + const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]); + await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 50); + await waitNextFrame(page); + await expect(activeHeadingContainer).toContainText(`Heading ${i + 1}`); + } + }); + + test('should scroll to heading and highlight heading when click item in outline panel', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const panel = await toggleTocPanel(page); + + await focusRichTextEnd(page); + const headings = await createHeadingsWithGap(page); + const activeHeadingContainer = panel.locator( + 'affine-outline-panel-body .active' + ); + + const headingsInPanel = Array.from({ length: 6 }, (_, i) => + getHeading(panel, i + 1) + ); + + await headingsInPanel[2].click(); + await expect(headings[2]).toBeVisible(); + await expect(activeHeadingContainer).toContainText('Heading 3'); + }); + + test('should scroll to title when click title in outline panel', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const panel = await toggleTocPanel(page); + + await focusTitle(page); + await type(page, 'Title'); + + await focusRichTextEnd(page); + await createHeadingsWithGap(page); + + const title = page.locator('doc-title'); + const titleInPanel = getTitle(panel); + + await expect(title).not.toBeInViewport(); + await titleInPanel.click(); + await waitNextFrame(page, 50); + await expect(title).toBeVisible(); + }); + + test('should update notes when change note display mode from note toolbar', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + const noteId = await addNote(page, 'hello', 300, 300); + await page.mouse.click(100, 100); + + await toggleTocPanel(page); + await toggleNoteSorting(page); + const docVisibleCard = page.locator( + '.card-container[data-invisible="false"]' + ); + const docInvisibleCard = page.locator( + '.card-container[data-invisible="true"]' + ); + + await expect(docVisibleCard).toHaveCount(1); + await expect(docInvisibleCard).toHaveCount(1); + + await changeNoteDisplayModeWithId( + page, + noteId, + NoteDisplayMode.DocAndEdgeless + ); + + await expect(docVisibleCard).toHaveCount(2); + await expect(docInvisibleCard).toHaveCount(0); + }); + + test('should reorder notes when drag and drop note in outline panel', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + const note1 = await addNote(page, 'hello', 300, 300); + const note2 = await addNote(page, 'world', 300, 500); + await page.mouse.click(100, 100); + + await changeNoteDisplayModeWithId( + page, + note1, + NoteDisplayMode.DocAndEdgeless + ); + await changeNoteDisplayModeWithId( + page, + note2, + NoteDisplayMode.DocAndEdgeless + ); + + await toggleTocPanel(page); + await toggleNoteSorting(page); + const docVisibleCard = page.locator( + '.card-container[data-invisible="false"]' + ); + + await expect(docVisibleCard).toHaveCount(3); + await assertRichTexts(page, ['', 'hello', 'world']); + + const noteCard3 = docVisibleCard.nth(2); + const noteCard1 = docVisibleCard.nth(0); + + await dragNoteCard(page, noteCard3, noteCard1); + + await waitNextFrame(page); + await assertRichTexts(page, ['world', '', 'hello']); + }); + + test('should update notes after slicing note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await zoomResetByKeyboard(page); + const note1 = await addNote(page, 'hello', 100, 300); + await pressEnter(page); + await type(page, 'world'); + await page.mouse.click(100, 100); + + await changeNoteDisplayModeWithId( + page, + note1, + NoteDisplayMode.DocAndEdgeless + ); + + await toggleTocPanel(page); + await toggleNoteSorting(page); + const docVisibleCard = page.locator( + '.card-container[data-invisible="false"]' + ); + + await expect(docVisibleCard).toHaveCount(2); + + await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting'); + await expect(page.locator('.note-slicer-button')).toBeVisible(); + await page.locator('.note-slicer-button').click(); + + await expect(docVisibleCard).toHaveCount(3); + }); +}); diff --git a/blocksuite/tests-legacy/fragments/outline/toc-viewer.spec.ts b/blocksuite/tests-legacy/fragments/outline/toc-viewer.spec.ts new file mode 100644 index 0000000000000..6818613b79821 --- /dev/null +++ b/blocksuite/tests-legacy/fragments/outline/toc-viewer.spec.ts @@ -0,0 +1,208 @@ +import { noop } from '@blocksuite/global/utils'; +import type { OutlineViewer } from '@blocksuite/presets'; +import { expect, type Page } from '@playwright/test'; +import { addNote, switchEditorMode } from 'utils/actions/edgeless.js'; +import { pressEnter, type } from 'utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + focusRichTextEnd, + focusTitle, + getEditorLocator, + initEmptyEdgelessState, + initEmptyParagraphState, + waitNextFrame, +} from 'utils/actions/misc.js'; + +import { test } from '../../utils/playwright.js'; +import { + createHeadingsWithGap, + getVerticalCenterFromLocator, +} from './utils.js'; + +test.describe('toc-viewer', () => { + async function toggleTocViewer(page: Page) { + await page.click('sl-button:text("Test Operations")'); + await page.click('sl-menu-item:text("Enable Outline Viewer")'); + await waitNextFrame(page); + const viewer = page.locator('affine-outline-viewer'); + return viewer; + } + + function getIndicators(page: Page) { + return page.locator('affine-outline-viewer .outline-viewer-indicator'); + } + + test('should display highlight indicators when non-empty headings exists', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await toggleTocViewer(page); + + await focusRichTextEnd(page); + + const indicators = getIndicators(page); + + // heading 1 to 6 + for (let i = 1; i <= 6; i++) { + await type(page, `${'#'.repeat(i)} `); + await type(page, `Heading ${i}`); + await pressEnter(page); + + await expect(indicators.nth(i - 1)).toBeVisible(); + } + }); + + test('should be hidden when only empty headings exists', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await toggleTocViewer(page); + + await focusTitle(page); + await type(page, 'Title'); + await focusRichTextEnd(page); + + const indicators = getIndicators(page); + + // heading 1 to 6 + for (let i = 1; i <= 6; i++) { + await type(page, `${'#'.repeat(i)} `); + await pressEnter(page); + await expect(indicators).toHaveCount(0); + } + }); + + test('should display outline content when hovering over indicators', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await toggleTocViewer(page); + + await focusRichTextEnd(page); + + await type(page, '# Heading 1'); + await pressEnter(page); + + const indicator = getIndicators(page).first(); + await indicator.hover({ force: true }); + + const items = page.locator('.outline-viewer-item'); + await expect(items).toHaveCount(2); + await expect(items.nth(0)).toContainText(['Table of Contents']); + await expect(items.nth(1)).toContainText(['Heading 1']); + }); + + test('should highlight indicator when scrolling', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await toggleTocViewer(page); + await focusRichTextEnd(page); + + const editor = getEditorLocator(page); + const indicators = getIndicators(page); + const headings = await createHeadingsWithGap(page); + await editor.locator('.inline-editor').first().scrollIntoViewIfNeeded(); + + const viewportCenter = await getVerticalCenterFromLocator( + page.locator('body') + ); + for (let i = 0; i < headings.length; i++) { + const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]); + await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 50); + await expect(indicators.nth(i)).toHaveClass(/active/); + } + }); + + test('should highlight indicator when click item in outline panel', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const viewer = await toggleTocViewer(page); + + await focusRichTextEnd(page); + const headings = await createHeadingsWithGap(page); + + const indicators = getIndicators(page); + await indicators.first().hover({ force: true }); + + const headingsInPanel = Array.from({ length: 6 }, (_, i) => + viewer.locator(`.h${i + 1} > span`) + ); + + await headingsInPanel[2].click(); + await expect(headings[2]).toBeVisible(); + await expect(indicators.nth(2)).toHaveClass(/active/); + }); + + test('should hide in edgeless mode', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await toggleTocViewer(page); + + const indicators = getIndicators(page); + + await focusRichTextEnd(page); + await type(page, '# Heading 1'); + await pressEnter(page); + + await expect(indicators).toHaveCount(1); + + await switchEditorMode(page); + + await expect(indicators).toHaveCount(0); + }); + + test('should hide edgeless-only note headings', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + const viewer = await toggleTocViewer(page); + + await focusRichTextEnd(page); + + await type(page, '# Heading 1'); + await pressEnter(page); + + await type(page, '## Heading 2'); + await pressEnter(page); + + await switchEditorMode(page); + + await addNote(page, '# Edgeless', 300, 300); + + await switchEditorMode(page); + + const indicators = getIndicators(page); + await expect(indicators).toHaveCount(2); + + await indicators.first().hover({ force: true }); + + await expect(viewer).toBeVisible(); + const hiddenTitle = viewer.locator('.hidden-title'); + await expect(hiddenTitle).toBeHidden(); + }); + + test('outline panel toggle button', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const viewer = await toggleTocViewer(page); + + await focusRichTextEnd(page); + await createHeadingsWithGap(page); + + const toggleButton = viewer.locator( + '[data-testid="toggle-outline-panel-button"]' + ); + await expect(toggleButton).toHaveCount(0); + await viewer.evaluate((el: OutlineViewer) => { + el.toggleOutlinePanel = () => { + noop(); + }; + }); + + await waitNextFrame(page); + await expect(toggleButton).toHaveCount(1); + await expect(toggleButton).toBeVisible(); + }); +}); diff --git a/blocksuite/tests-legacy/fragments/outline/utils.ts b/blocksuite/tests-legacy/fragments/outline/utils.ts new file mode 100644 index 0000000000000..2e686201646da --- /dev/null +++ b/blocksuite/tests-legacy/fragments/outline/utils.ts @@ -0,0 +1,27 @@ +import { expect, type Locator, type Page } from '@playwright/test'; +import { pressEnter, type } from 'utils/actions/keyboard.js'; +import { getEditorHostLocator } from 'utils/actions/misc.js'; + +export async function getVerticalCenterFromLocator(locator: Locator) { + const rect = await locator.boundingBox(); + return rect!.y + rect!.height / 2; +} + +export async function createHeadingsWithGap(page: Page) { + // heading 1 to 6 + const editor = getEditorHostLocator(page); + + const headings: Locator[] = []; + await pressEnter(page, 10); + for (let i = 1; i <= 6; i++) { + await type(page, `${'#'.repeat(i)} `); + await type(page, `Heading ${i}`); + const heading = editor.locator(`.h${i}`); + await expect(heading).toBeVisible(); + headings.push(heading); + await pressEnter(page, 10); + } + await pressEnter(page, 10); + + return headings; +} diff --git a/blocksuite/tests-legacy/hotkey/bracket.spec.ts b/blocksuite/tests-legacy/hotkey/bracket.spec.ts new file mode 100644 index 0000000000000..1455e02fabe28 --- /dev/null +++ b/blocksuite/tests-legacy/hotkey/bracket.spec.ts @@ -0,0 +1,93 @@ +import { expect } from '@playwright/test'; + +import { + dragBetweenIndices, + enterPlaygroundRoom, + focusRichText, + getPageSnapshot, + initEmptyCodeBlockState, + initEmptyParagraphState, + initThreeParagraphs, + resetHistory, + type, + undoByClick, +} from '../utils/actions/index.js'; +import { assertRichTexts } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('should bracket complete works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '([{'); + // type without selection should not trigger bracket complete + await assertRichTexts(page, ['([{']); + + await dragBetweenIndices(page, [0, 1], [0, 2]); + await type(page, '('); + await assertRichTexts(page, ['(([){']); + + await type(page, ')'); + // Should not trigger bracket complete when type right bracket + await assertRichTexts(page, ['(()){']); +}); + +test('bracket complete should not work when selecting mutiple lines', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + // 1(23 45)6 789 + await dragBetweenIndices(page, [0, 1], [1, 2]); + await type(page, '('); + await assertRichTexts(page, ['1(6', '789']); +}); + +test('should bracket complete with backtick works', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello world'); + + await dragBetweenIndices(page, [0, 2], [0, 5]); + await resetHistory(page); + await type(page, '`'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); + + await undoByClick(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_undo.json` + ); +}); + +test('auto delete bracket right', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + await type(page, '('); + await assertRichTexts(page, ['()']); + await type(page, '('); + await assertRichTexts(page, ['(())']); + await page.keyboard.press('Backspace'); + await assertRichTexts(page, ['()']); + await page.keyboard.press('Backspace'); + await assertRichTexts(page, ['']); +}); + +test('skip redundant right bracket', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + await type(page, '('); + await assertRichTexts(page, ['()']); + await type(page, ')'); + await assertRichTexts(page, ['()']); + await type(page, ')'); + await assertRichTexts(page, ['())']); +}); diff --git a/blocksuite/tests-legacy/hotkey/hotkey.spec.ts b/blocksuite/tests-legacy/hotkey/hotkey.spec.ts new file mode 100644 index 0000000000000..581ca2bb7ea1e --- /dev/null +++ b/blocksuite/tests-legacy/hotkey/hotkey.spec.ts @@ -0,0 +1,474 @@ +import { expect } from '@playwright/test'; + +import { + dragBetweenIndices, + enterPlaygroundRoom, + focusRichText, + getPageSnapshot, + initEmptyParagraphState, + initThreeParagraphs, + inlineCode, + MODIFIER_KEY, + pressArrowDown, + pressArrowLeft, + pressArrowRight, + pressArrowUp, + pressEnter, + pressForwardDelete, + pressShiftTab, + pressTab, + readClipboardText, + redoByClick, + redoByKeyboard, + resetHistory, + setInlineRangeInSelectedRichText, + SHIFT_KEY, + SHORT_KEY, + strikethrough, + type, + undoByClick, + undoByKeyboard, + updateBlockType, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertBlockChildrenIds, + assertRichTextInlineRange, + assertRichTextModelType, + assertRichTexts, + assertTextFormat, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('rich-text hotkey scope on single press', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + await assertRichTexts(page, ['hello', 'world']); + + await dragBetweenIndices(page, [0, 0], [1, 5]); + await page.keyboard.press('Backspace'); + await assertRichTexts(page, ['']); +}); + +test('single line rich-text inline code hotkey', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await dragBetweenIndices(page, [0, 0], [0, 5]); + await inlineCode(page); + await assertTextFormat(page, 0, 5, { code: true }); + + // undo + await undoByKeyboard(page); + await assertTextFormat(page, 0, 5, {}); + // redo + await redoByKeyboard(page); + await waitNextFrame(page); + await assertTextFormat(page, 0, 5, { code: true }); + + // the format should be removed after trigger the hotkey again + await inlineCode(page); + await assertTextFormat(page, 0, 5, {}); +}); + +test('type character jump out code node', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'Hello'); + await setInlineRangeInSelectedRichText(page, 0, 5); + await inlineCode(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_1.json` + ); + await focusRichText(page); + await page.keyboard.press(`${SHORT_KEY}+ArrowRight`); + await type(page, 'block suite'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_2.json` + ); +}); + +test('single line rich-text strikethrough hotkey', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await dragBetweenIndices(page, [0, 0], [0, 5]); + await strikethrough(page); + await assertTextFormat(page, 0, 5, { strike: true }); + + await undoByClick(page); + await assertTextFormat(page, 0, 5, {}); + + await redoByClick(page); + await assertTextFormat(page, 0, 5, { strike: true }); + + await waitNextFrame(page); + // the format should be removed after trigger the hotkey again + await strikethrough(page); + await assertTextFormat(page, 0, 5, {}); +}); + +test('use formatted cursor with hotkey', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + // format italic + await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 }); + await type(page, 'bbb'); + // format bold + await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 }); + await type(page, 'ccc'); + // unformat italic + await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 }); + await type(page, 'ddd'); + // unformat bold + await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 }); + await type(page, 'eee'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + // format bold + await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 }); + await type(page, 'fff'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_bold.json` + ); + + await pressArrowLeft(page); + await pressArrowRight(page); + await type(page, 'ggg'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_bold_ggg.json` + ); + + await setInlineRangeInSelectedRichText(page, 3, 0); + await waitNextFrame(page); + await type(page, 'hhh'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_bold_hhh.json` + ); +}); + +test('use formatted cursor with hotkey at empty line', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + // format bold + await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 }); + await type(page, 'aaa'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_bold.json` + ); +}); + +test('should single line format hotkey work', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await dragBetweenIndices(page, [0, 1], [0, 4]); + + // bold + await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 }); + // italic + await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 }); + // underline + await page.keyboard.press(`${SHORT_KEY}+u`, { delay: 50 }); + // strikethrough + await page.keyboard.press(`${SHORT_KEY}+Shift+s`, { delay: 50 }); + + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + // bold + await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 }); + // italic + await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 }); + // underline + await page.keyboard.press(`${SHORT_KEY}+u`, { delay: 50 }); + // strikethrough + await page.keyboard.press(`${SHORT_KEY}+Shift+s`, { delay: 50 }); + + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); + +test('should hotkey work in paragraph', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page, 0); + await type(page, 'hello'); + + // XXX wait for group to be updated + await page.waitForTimeout(10); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+1`); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+6`); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_press_6.json` + ); + await page.waitForTimeout(50); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+8`); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_press_8.json` + ); + await page.waitForTimeout(50); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+9`); + await waitNextFrame(page, 200); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_press_9.json` + ); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+0`); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_press_0.json` + ); + await page.waitForTimeout(50); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+d`); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_press_d.json` + ); +}); + +test('format list to h1', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page, 0); + await updateBlockType(page, 'affine:list', 'bulleted'); + await type(page, 'aa'); + await focusRichText(page, 0); + await updateBlockType(page, 'affine:paragraph', 'h1'); + await assertRichTextModelType(page, 'h1'); + await undoByClick(page); + await assertRichTextModelType(page, 'bulleted'); + await redoByClick(page); + await assertRichTextModelType(page, 'h1'); +}); + +test('should cut work single line', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await resetHistory(page); + await dragBetweenIndices(page, [0, 1], [0, 4]); + // cut + await page.keyboard.press(`${SHORT_KEY}+x`); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + await undoByKeyboard(page); + const text = await readClipboardText(page); + expect(text).toBe('ell'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_undo.json` + ); +}); + +test('should ctrl+enter create new block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, '123'); + await pressArrowLeft(page, 2); + await pressEnter(page); + await waitNextFrame(page); + await assertRichTexts(page, ['1', '23']); + await page.keyboard.press(`${SHORT_KEY}+Enter`); + await assertRichTexts(page, ['1', '23', '']); +}); + +test('should left/right key navigator works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await focusRichText(page, 0); + await assertRichTextInlineRange(page, 0, 3); + await page.keyboard.press(`${SHORT_KEY}+ArrowLeft`, { delay: 50 }); + await assertRichTextInlineRange(page, 0, 0); + await pressArrowLeft(page); + await assertRichTextInlineRange(page, 0, 0); + await page.keyboard.press(`${SHORT_KEY}+ArrowRight`, { delay: 50 }); + await assertRichTextInlineRange(page, 0, 3); + await pressArrowRight(page); + await assertRichTextInlineRange(page, 1, 0); + await pressArrowLeft(page); + await assertRichTextInlineRange(page, 0, 3); + await pressArrowRight(page, 4); + await assertRichTextInlineRange(page, 1, 3); + await pressArrowRight(page); + await assertRichTextInlineRange(page, 2, 0); + await pressArrowLeft(page); + await assertRichTextInlineRange(page, 1, 3); +}); + +test('should up/down key navigator works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await focusRichText(page, 0); + await assertRichTextInlineRange(page, 0, 3); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 1, 3); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 2, 3); + await page.keyboard.press(`${SHORT_KEY}+ArrowLeft`, { delay: 50 }); + await assertRichTextInlineRange(page, 2, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 1, 0); + await pressArrowRight(page); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 0, 1); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 1, 1); +}); + +test('should support ctrl/cmd+shift+l convert to linked doc', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await dragBetweenIndices( + page, + [2, 3], + [0, 0], + { x: 20, y: 20 }, + { x: 0, y: 0 } + ); + + await waitNextFrame(page); + await page.keyboard.press(`${SHORT_KEY}+${SHIFT_KEY}+l`); + + const linkedDocCard = page.locator('affine-embed-linked-doc-block'); + await expect(linkedDocCard).toBeVisible(); + + const title = page.locator('.affine-embed-linked-doc-content-title-text'); + expect(await title.innerText()).toBe('Untitled'); + + const noteContent = page.locator('.affine-embed-linked-doc-content-note'); + expect(await noteContent.innerText()).toBe('123'); +}); + +test('should forwardDelete works when delete single character', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page, 0); + await type(page, 'hello'); + await pressArrowLeft(page, 5); + await pressForwardDelete(page); + await assertRichTexts(page, ['ello']); +}); + +test.describe('keyboard operation to move block up or down', () => { + test('common paragraph', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + await pressEnter(page); + await type(page, 'foo'); + await pressEnter(page); + await type(page, 'bar'); + await assertRichTexts(page, ['hello', 'world', 'foo', 'bar']); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowUp`); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowUp`); + await assertRichTexts(page, ['hello', 'bar', 'world', 'foo']); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowDown`); + await assertRichTexts(page, ['hello', 'world', 'bar', 'foo']); + }); + + test('with indent', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await pressTab(page); + await waitNextFrame(page); + await type(page, 'world'); + await pressEnter(page); + await pressShiftTab(page); + await waitNextFrame(page); + await type(page, 'foo'); + await assertRichTexts(page, ['hello', 'world', 'foo']); + await assertBlockChildrenIds(page, '2', ['3']); + await pressArrowUp(page, 2); + await waitNextFrame(page); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowDown`); + await waitNextFrame(page); + await assertRichTexts(page, ['foo', 'hello', 'world']); + await assertBlockChildrenIds(page, '1', ['4', '2']); + await assertBlockChildrenIds(page, '2', ['3']); + }); + + test('keep cursor', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + await pressEnter(page); + await type(page, 'foo'); + await assertRichTexts(page, ['hello', 'world', 'foo']); + await assertRichTextInlineRange(page, 2, 3); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowUp`); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowUp`); + await assertRichTextInlineRange(page, 0, 3); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowDown`); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowDown`); + await assertRichTextInlineRange(page, 2, 3); + }); +}); + +test('Enter key should as expected after setting heading by shortkey', async ({ + page, +}, testInfo) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/4987', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+1`); + await pressEnter(page); + await type(page, 'world'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); +}); diff --git a/blocksuite/tests-legacy/hotkey/multiline.spec.ts b/blocksuite/tests-legacy/hotkey/multiline.spec.ts new file mode 100644 index 0000000000000..1a0d1f1b9a5fe --- /dev/null +++ b/blocksuite/tests-legacy/hotkey/multiline.spec.ts @@ -0,0 +1,176 @@ +import { expect } from '@playwright/test'; + +import { + dragBetweenIndices, + enterPlaygroundRoom, + focusRichText, + getPageSnapshot, + initEmptyParagraphState, + initThreeParagraphs, + inlineCode, + pressArrowLeft, + pressArrowUp, + pressEnter, + pressForwardDelete, + pressShiftEnter, + readClipboardText, + redoByClick, + resetHistory, + setInlineRangeInSelectedRichText, + SHORT_KEY, + type, + undoByClick, + undoByKeyboard, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertBlockSelections, + assertRichTextInlineRange, + assertRichTexts, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('should multiple line format hotkey work', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + // 0 1 2 + // 1|23 456 78|9 + await dragBetweenIndices(page, [0, 1], [2, 2]); + + // bold + await page.keyboard.press(`${SHORT_KEY}+b`); + // italic + await page.keyboard.press(`${SHORT_KEY}+i`); + // underline + await page.keyboard.press(`${SHORT_KEY}+u`); + // strikethrough + await page.keyboard.press(`${SHORT_KEY}+Shift+S`); + + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + // bold + await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 }); + // italic + await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 }); + // underline + await page.keyboard.press(`${SHORT_KEY}+u`, { delay: 50 }); + // strikethrough + await page.keyboard.press(`${SHORT_KEY}+Shift+s`, { delay: 50 }); + + await waitNextFrame(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); + +test('multi line rich-text inline code hotkey', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + // 0 1 2 + // 1|23 456 78|9 + await dragBetweenIndices(page, [0, 1], [2, 2]); + await inlineCode(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await undoByClick(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_undo.json` + ); + + await redoByClick(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_redo.json` + ); +}); + +test('should cut work multiple line', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await resetHistory(page); + // 0 1 2 + // 1|23 456 78|9 + await dragBetweenIndices(page, [0, 1], [2, 2]); + // cut + await page.keyboard.press(`${SHORT_KEY}+x`); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + await undoByKeyboard(page); + const text = await readClipboardText(page); + expect(text).toBe(`23 456 78`); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_undo.json` + ); +}); + +test('arrow up and down behavior on multiline text blocks when previous is non-text', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await pressEnter(page); + await pressArrowUp(page); + await type(page, '--- '); + await pressEnter(page); + + await focusRichText(page); + await type(page, '124'); + await pressShiftEnter(page); + await type(page, '1234'); + + await pressArrowUp(page); + await waitNextFrame(page, 100); + await assertRichTextInlineRange(page, 0, 3); + + await pressArrowUp(page); + await assertBlockSelections(page, ['4']); +}); + +test('should forwardDelete works when delete multi characters', async ({ + page, +}) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/3122', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page, 0); + await type(page, 'hello'); + await pressArrowLeft(page, 5); + await setInlineRangeInSelectedRichText(page, 1, 3); + await pressForwardDelete(page); + await assertRichTexts(page, ['ho']); +}); + +test('should drag multiple block and input text works', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2982', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await dragBetweenIndices(page, [0, 1], [2, 1]); + await type(page, 'ab'); + await assertRichTexts(page, ['1ab89']); + await undoByKeyboard(page); + await assertRichTexts(page, ['123', '456', '789']); +}); diff --git a/blocksuite/tests-legacy/hotkey/title.spec.ts b/blocksuite/tests-legacy/hotkey/title.spec.ts new file mode 100644 index 0000000000000..4958a107c4b22 --- /dev/null +++ b/blocksuite/tests-legacy/hotkey/title.spec.ts @@ -0,0 +1,43 @@ +import { + cutByKeyboard, + dragOverTitle, + enterPlaygroundRoom, + focusRichText, + focusTitle, + initEmptyParagraphState, + pasteByKeyboard, + pressEnter, + type, +} from '../utils/actions/index.js'; +import { assertRichTexts, assertTitle } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('should cut in title works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusTitle(page); + await type(page, 'hello'); + await assertTitle(page, 'hello'); + + await dragOverTitle(page); + await cutByKeyboard(page); + await assertTitle(page, ''); + + await focusRichText(page); + await pasteByKeyboard(page); + await assertRichTexts(page, ['hello']); +}); + +test('enter in title should move cursor in new paragraph block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'hello'); + await assertTitle(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + await assertRichTexts(page, ['world', '']); +}); diff --git a/blocksuite/tests-legacy/image/image.spec.ts b/blocksuite/tests-legacy/image/image.spec.ts new file mode 100644 index 0000000000000..de51aaac51aea --- /dev/null +++ b/blocksuite/tests-legacy/image/image.spec.ts @@ -0,0 +1,154 @@ +import '../utils/declare-test-window.js'; + +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import { + activeEmbed, + copyByKeyboard, + dragEmbedResizeByTopLeft, + dragEmbedResizeByTopRight, + enterPlaygroundRoom, + initImageState, + moveToImage, + pasteByKeyboard, + pressArrowLeft, + pressEnter, + redoByClick, + redoByKeyboard, + type, + undoByKeyboard, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertImageOption, + assertImageSize, + assertRichDragButton, + assertRichImage, + assertRichTextInlineRange, + assertRichTexts, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +async function focusCaption(page: Page) { + await page.click( + '.affine-image-toolbar-container .image-toolbar-button.caption' + ); +} + +test('can drag resize image by left menu', async ({ page }) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await assertRichImage(page, 1); + + await activeEmbed(page); + await assertRichDragButton(page); + await assertImageSize(page, { width: 752, height: 564 }); + + await dragEmbedResizeByTopLeft(page); + await waitNextFrame(page); + await assertImageSize(page, { width: 358, height: 268 }); + + await undoByKeyboard(page); + await waitNextFrame(page); + await assertImageSize(page, { width: 752, height: 564 }); + + await redoByKeyboard(page); + await waitNextFrame(page); + await assertImageSize(page, { width: 358, height: 268 }); +}); + +test('can drag resize image by right menu', async ({ page }) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await assertRichImage(page, 1); + + await activeEmbed(page); + await assertRichDragButton(page); + await assertImageSize(page, { width: 752, height: 564 }); + + await dragEmbedResizeByTopRight(page); + await assertImageSize(page, { width: 338, height: 253 }); + + await undoByKeyboard(page); + await assertImageSize(page, { width: 752, height: 564 }); + + await redoByKeyboard(page); + await assertImageSize(page, { width: 338, height: 253 }); +}); + +test('can click and delete image', async ({ page }) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await assertRichImage(page, 1); + + await activeEmbed(page); + await page.keyboard.press('Backspace'); + await assertRichImage(page, 0); + + await undoByKeyboard(page); + await assertRichImage(page, 1); + + await redoByClick(page); + await assertRichImage(page, 0); +}); + +test('can click and copy image', async ({ page }) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await assertRichImage(page, 1); + + await activeEmbed(page); + await copyByKeyboard(page); + await pressEnter(page); + await waitNextFrame(page); + + await pasteByKeyboard(page); + await waitNextFrame(page, 200); + await assertRichImage(page, 2); +}); + +test('enter shortcut on focusing embed block and its caption', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await assertRichImage(page, 1); + + await moveToImage(page); + await assertImageOption(page); + + const caption = page.locator('affine-image block-caption-editor textarea'); + await focusCaption(page); + await type(page, '123'); + + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2495', + }); + + // blur + await page.mouse.click(0, 500); + await caption.click({ position: { x: 0, y: 0 } }); + await type(page, 'abc'); + await expect(caption).toHaveValue('abc123'); +}); + +test('should support the enter key of image caption', async ({ page }) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await assertRichImage(page, 1); + + await moveToImage(page); + await assertImageOption(page); + + const caption = page.locator('affine-image block-caption-editor textarea'); + await focusCaption(page); + await type(page, 'abc123'); + await pressArrowLeft(page, 3); + await pressEnter(page); + await expect(caption).toHaveValue('abc'); + + await assertRichTexts(page, ['123']); + await assertRichTextInlineRange(page, 0, 0, 0); +}); diff --git a/blocksuite/tests-legacy/image/keymap.spec.ts b/blocksuite/tests-legacy/image/keymap.spec.ts new file mode 100644 index 0000000000000..19af869e154f7 --- /dev/null +++ b/blocksuite/tests-legacy/image/keymap.spec.ts @@ -0,0 +1,79 @@ +import { expect } from '@playwright/test'; + +import { + activeEmbed, + enterPlaygroundRoom, + initImageState, + pressArrowDown, + pressArrowUp, + pressBackspace, + pressEnter, + type, +} from '../utils/actions/index.js'; +import { + assertBlockCount, + assertBlockSelections, + assertRichImage, + assertRichTextInlineRange, + assertRichTexts, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test.beforeEach(async ({ page }) => { + await enterPlaygroundRoom(page); + await initImageState(page, true); + await assertRichImage(page, 1); +}); + +test('press enter will create new block when click and select image', async ({ + page, +}) => { + await activeEmbed(page); + await pressEnter(page); + await type(page, 'aa'); + await assertRichTexts(page, ['', 'aa']); +}); + +test('press backspace after image block can select image block', async ({ + page, +}) => { + await activeEmbed(page); + await pressEnter(page); + await assertRichTextInlineRange(page, 1, 0); + await assertBlockCount(page, 'paragraph', 2); + await pressBackspace(page); + await assertBlockSelections(page, ['3']); + await assertBlockCount(page, 'paragraph', 1); +}); + +test('press enter when image is selected should move next paragraph and should placeholder', async ({ + page, +}) => { + await activeEmbed(page); + await pressEnter(page); + + const placeholder = page.locator('.affine-paragraph-placeholder.visible'); + await expect(placeholder).toBeVisible(); +}); + +test('press arrow up when image is selected should move to previous paragraph', async ({ + page, +}) => { + await activeEmbed(page); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 0, 0); + await type(page, 'aa'); + await assertRichTexts(page, ['aa']); +}); + +test('press arrow down when image is selected should move to previous paragraph', async ({ + page, +}) => { + await activeEmbed(page); + await pressEnter(page); + await type(page, 'aa'); + await activeEmbed(page); + await pressArrowDown(page); + await type(page, 'bb'); + await assertRichTexts(page, ['', 'bbaa']); +}); diff --git a/blocksuite/tests-legacy/image/load.spec.ts b/blocksuite/tests-legacy/image/load.spec.ts new file mode 100644 index 0000000000000..b67ad44599a16 --- /dev/null +++ b/blocksuite/tests-legacy/image/load.spec.ts @@ -0,0 +1,168 @@ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import { + enterPlaygroundRoom, + expectConsoleMessage, +} from '../utils/actions/index.js'; +import { test } from '../utils/playwright.js'; + +const mockImageId = '_e2e_test_image_id_'; + +async function initMockImage(page: Page) { + await page.evaluate(() => { + const { doc } = window; + doc.captureSync(); + const rootId = doc.addBlock('affine:page'); + const noteId = doc.addBlock('affine:note', {}, rootId); + doc.addBlock( + 'affine:image', + { + sourceId: '_e2e_test_image_id_', + width: 200, + height: 180, + }, + noteId + ); + doc.captureSync(); + }); +} + +test('image loading but failed', async ({ page }) => { + expectConsoleMessage( + page, + 'Error: Failed to fetch blob _e2e_test_image_id_', + 'warning' + ); + expectConsoleMessage( + page, + 'Failed to load resource: the server responded with a status of 404 (Not Found)' + ); + expectConsoleMessage( + page, + 'Error: Image blob is missing!, retrying', + 'warning' + ); + + const room = await enterPlaygroundRoom(page, { blobSource: ['mock'] }); + const timeout = 2000; + + // block image data request, force wait 100ms for loading test, + // always return 404 + await page.route( + `**/api/collection/${room}/blob/${mockImageId}`, + async route => { + await page.waitForTimeout(timeout); + // broken image + return route.fulfill({ + status: 404, + }); + } + ); + + await initMockImage(page); + + const loadingContent = await page + .locator( + '.affine-image-fallback-card .affine-image-fallback-card-title-text' + ) + .innerText(); + expect(loadingContent).toBe('Loading image...'); + + await page.waitForTimeout(3 * timeout); + + await expect( + page.locator( + '.affine-image-fallback-card .affine-image-fallback-card-title-text' + ) + ).toContainText('Image loading failed.'); +}); + +test('image loading but success', async ({ page }) => { + expectConsoleMessage( + page, + 'Error: Failed to fetch blob _e2e_test_image_id_', + 'warning' + ); + expectConsoleMessage( + page, + 'Failed to load resource: the server responded with a status of 404 (Not Found)' + ); + expectConsoleMessage( + page, + 'Error: Image blob is missing!, retrying', + 'warning' + ); + + const room = await enterPlaygroundRoom(page, { blobSource: ['mock'] }); + const imageBuffer = await readFile( + fileURLToPath(new URL('../fixtures/smile.png', import.meta.url)) + ); + + const timeout = 2000; + let count = 0; + + // block image data request, force wait 100ms for loading test, + // always return 404 + await page.route( + `**/api/collection/${room}/blob/${mockImageId}`, + async route => { + await page.waitForTimeout(timeout); + count++; + if (count === 3) { + return route.fulfill({ + status: 200, + body: imageBuffer, + }); + } + // broken image + return route.fulfill({ + status: 404, + }); + } + ); + + await initMockImage(page); + + const loadingContent = await page + .locator( + '.affine-image-fallback-card .affine-image-fallback-card-title-text' + ) + .innerText(); + expect(loadingContent).toBe('Loading image...'); + + await page.waitForTimeout(3 * timeout); + + const img = page.locator('.affine-image-container img'); + await expect(img).toBeVisible(); + const src = await img.getAttribute('src'); + expect(src).toBeDefined(); +}); + +test('image loaded successfully', async ({ page }) => { + const room = await enterPlaygroundRoom(page, { blobSource: ['mock'] }); + const imageBuffer = await readFile( + fileURLToPath(new URL('../fixtures/smile.png', import.meta.url)) + ); + await page.route( + `**/api/collection/${room}/blob/${mockImageId}`, + async route => { + return route.fulfill({ + status: 200, + body: imageBuffer, + }); + } + ); + + await initMockImage(page); + + await page.waitForTimeout(1000); + + const img = page.locator('.affine-image-container img'); + await expect(img).toBeVisible(); + const src = await img.getAttribute('src'); + expect(src).toBeDefined(); +}); diff --git a/blocksuite/tests-legacy/image/menu.spec.ts b/blocksuite/tests-legacy/image/menu.spec.ts new file mode 100644 index 0000000000000..e62cf9632ccb3 --- /dev/null +++ b/blocksuite/tests-legacy/image/menu.spec.ts @@ -0,0 +1,90 @@ +import { expect } from '@playwright/test'; + +import { + activeEmbed, + dragBetweenCoords, + enterPlaygroundRoom, + initImageState, + insertThreeLevelLists, + pressEnter, + scrollToTop, +} from '../utils/actions/index.js'; +import { assertRichImage } from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +// FIXME(@fundon): This behavior is not meeting the design spec +test.skip('popup menu should follow position of image when scrolling', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await activeEmbed(page); + await pressEnter(page); + await insertThreeLevelLists(page, 0); + await pressEnter(page); + await insertThreeLevelLists(page, 3); + await pressEnter(page); + await insertThreeLevelLists(page, 6); + await pressEnter(page); + await insertThreeLevelLists(page, 9); + await pressEnter(page); + await insertThreeLevelLists(page, 12); + + await scrollToTop(page); + + const rect = await page.locator('.affine-image-container img').boundingBox(); + if (!rect) throw new Error('image not found'); + + await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2); + + await page.waitForTimeout(150); + + const menu = page.locator('.affine-image-toolbar-container'); + + await expect(menu).toBeVisible(); + + await page.evaluate( + ([rect]) => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + // const distance = viewport.scrollHeight - viewport.clientHeight; + viewport.scrollTo(0, (rect.height + rect.y) / 2); + }, + [rect] + ); + + await page.waitForTimeout(150); + const image = page.locator('.affine-image-container img'); + const imageRect = await image.boundingBox(); + const menuRect = await menu.boundingBox(); + if (!imageRect) throw new Error('image not found'); + if (!menuRect) throw new Error('menu not found'); + expect(imageRect.y).toBeCloseTo((rect.y - rect.height) / 2, 172); + expect(menuRect.y).toBeCloseTo(65, -0.325); +}); + +test('select image should not show format bar', async ({ page }) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await assertRichImage(page, 1); + + const image = page.locator('affine-image'); + const rect = await image.boundingBox(); + if (!rect) { + throw new Error('image not found'); + } + await dragBetweenCoords( + page, + { x: rect.x - 20, y: rect.y + 20 }, + { x: rect.x + 20, y: rect.y + 40 } + ); + const rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(1); + const formatQuickBar = page.locator(`.format-quick-bar`); + await expect(formatQuickBar).not.toBeVisible(); + await page.mouse.wheel(0, rect.y + rect.height); + await expect(formatQuickBar).not.toBeVisible(); + await page.mouse.click(0, 0); +}); diff --git a/blocksuite/tests-legacy/inline/inline-editor.spec.ts b/blocksuite/tests-legacy/inline/inline-editor.spec.ts new file mode 100644 index 0000000000000..a1eaa110729d5 --- /dev/null +++ b/blocksuite/tests-legacy/inline/inline-editor.spec.ts @@ -0,0 +1,1013 @@ +import { + assertSelection, + enterInlineEditorPlayground, + focusInlineRichText, + getDeltaFromInlineRichText, + getInlineRangeIndexRect, + getInlineRichTextLine, + press, + setInlineRichTextRange, + type, +} from '@inline/__tests__/utils.js'; +import { ZERO_WIDTH_SPACE } from '@inline/consts.js'; +import type { InlineEditor } from '@inline/index.js'; +import { expect, test } from '@playwright/test'; + +test('basic input', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorB = page.locator('[data-v-root="true"]').nth(1); + + const editorAUndo = page.getByText('undo').nth(0); + const editorARedo = page.getByText('redo').nth(0); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + + await type(page, 'abcd๐Ÿ˜ƒefg๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆhj'); + + expect(await editorA.innerText()).toBe('abcd๐Ÿ˜ƒefg๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆhj'); + expect(await editorB.innerText()).toBe('abcd๐Ÿ˜ƒefg๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆhj'); + + await editorAUndo.click(); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await editorARedo.click(); + + expect(await editorA.innerText()).toBe('abcd๐Ÿ˜ƒefg๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆhj'); + expect(await editorB.innerText()).toBe('abcd๐Ÿ˜ƒefg๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆhj'); + + await focusInlineRichText(page); + await press(page, 'Backspace'); + await press(page, 'Backspace'); + await press(page, 'Backspace'); + + expect(await editorA.innerText()).toBe('abcd๐Ÿ˜ƒefg'); + expect(await editorB.innerText()).toBe('abcd๐Ÿ˜ƒefg'); + + await editorAUndo.click(); + + expect(await editorA.innerText()).toBe('abcd๐Ÿ˜ƒefg๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆhj'); + expect(await editorB.innerText()).toBe('abcd๐Ÿ˜ƒefg๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆhj'); + + await editorARedo.click(); + + expect(await editorA.innerText()).toBe('abcd๐Ÿ˜ƒefg'); + expect(await editorB.innerText()).toBe('abcd๐Ÿ˜ƒefg'); + + await focusInlineRichText(page); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'Delete'); + await press(page, 'Delete'); + + await type(page, '๐Ÿฅฐ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'); + expect(await editorA.innerText()).toBe('abc๐Ÿฅฐ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆefg'); + expect(await editorB.innerText()).toBe('abc๐Ÿฅฐ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆefg'); + + await setInlineRichTextRange(page, { + index: 3, + length: 16, + }); + await page.waitForTimeout(100); + await press(page, 'Delete'); + + expect(await editorA.innerText()).toBe('abc'); + expect(await editorA.innerText()).toBe('abc'); + + await editorAUndo.click(); + + expect(await editorA.innerText()).toBe('abcd๐Ÿ˜ƒefg'); + expect(await editorB.innerText()).toBe('abcd๐Ÿ˜ƒefg'); + + await editorARedo.click(); + + expect(await editorA.innerText()).toBe('abc'); + expect(await editorB.innerText()).toBe('abc'); + + await focusInlineRichText(page); + await page.waitForTimeout(100); + await press(page, 'Enter'); + await press(page, 'Enter'); + await type(page, 'bbb'); + + await page.waitForTimeout(100); + + expect(await editorA.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); + expect(await editorB.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); + + await editorAUndo.click(); + + expect(await editorA.innerText()).toBe('abc'); + expect(await editorB.innerText()).toBe('abc'); + + await editorARedo.click(); + + expect(await editorA.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); + expect(await editorB.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); + + await focusInlineRichText(page); + await page.waitForTimeout(100); + await press(page, 'Backspace'); + await press(page, 'Backspace'); + await press(page, 'Backspace'); + await press(page, 'Backspace'); + await press(page, 'Backspace'); + + expect(await editorA.innerText()).toBe('abc'); + expect(await editorB.innerText()).toBe('abc'); + + await editorAUndo.click(); + + expect(await editorA.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); + expect(await editorB.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); + + await editorARedo.click(); + + expect(await editorA.innerText()).toBe('abc'); + + await focusInlineRichText(page); + await page.waitForTimeout(100); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await type(page, 'bb'); + await press(page, 'ArrowRight'); + await press(page, 'ArrowRight'); + await type(page, 'dd'); + + expect(await editorA.innerText()).toBe('abbbcdd'); + expect(await editorB.innerText()).toBe('abbbcdd'); + + await editorAUndo.click(); + + expect(await editorA.innerText()).toBe('abc'); + + await editorARedo.click(); + + expect(await editorA.innerText()).toBe('abbbcdd'); + expect(await editorB.innerText()).toBe('abbbcdd'); + + await focusInlineRichText(page); + await page.waitForTimeout(100); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'Enter'); + await press(page, 'Enter'); + + expect(await editorA.innerText()).toBe('abbbc\n' + ZERO_WIDTH_SPACE + '\ndd'); + expect(await editorB.innerText()).toBe('abbbc\n' + ZERO_WIDTH_SPACE + '\ndd'); + + await editorAUndo.click(); + + expect(await editorA.innerText()).toBe('abbbcdd'); + expect(await editorB.innerText()).toBe('abbbcdd'); + + await editorARedo.click(); + + expect(await editorA.innerText()).toBe('abbbc\n' + ZERO_WIDTH_SPACE + '\ndd'); + expect(await editorB.innerText()).toBe('abbbc\n' + ZERO_WIDTH_SPACE + '\ndd'); +}); + +test('chinese input', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorB = page.locator('[data-v-root="true"]').nth(1); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + const client = await page.context().newCDPSession(page); + await client.send('Input.imeSetComposition', { + selectionStart: 0, + selectionEnd: 0, + text: 'n', + }); + await client.send('Input.imeSetComposition', { + selectionStart: 0, + selectionEnd: 1, + text: 'ni', + }); + await client.send('Input.insertText', { + text: 'ไฝ ', + }); + expect(await editorA.innerText()).toBe('ไฝ '); + expect(await editorB.innerText()).toBe('ไฝ '); +}); + +test('type many times in one moment', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + await page.waitForTimeout(100); + await Promise.all( + 'aaaaaaaaaaaaaaaaaaaa'.split('').map(s => page.keyboard.type(s)) + ); + const preOffset = await page.evaluate(() => { + return getSelection()?.getRangeAt(0).endOffset; + }); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + const offset = await page.evaluate(() => { + return getSelection()?.getRangeAt(0).endOffset; + }); + expect(preOffset).toBe(offset); +}); + +test('readonly mode', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorB = page.locator('[data-v-root="true"]').nth(1); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + + await type(page, 'abcdefg'); + + expect(await editorA.innerText()).toBe('abcdefg'); + expect(await editorB.innerText()).toBe('abcdefg'); + + await page.evaluate(() => { + const richTextA = document + .querySelector('test-page') + ?.querySelector('test-rich-text'); + + if (!richTextA) { + throw new Error('Cannot find editor'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (richTextA as any).inlineEditor.setReadonly(true); + }); + + await type(page, 'aaaa'); + + expect(await editorA.innerText()).toBe('abcdefg'); + expect(await editorB.innerText()).toBe('abcdefg'); +}); + +test('basic styles', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorB = page.locator('[data-v-root="true"]').nth(1); + + const editorABold = page.getByText('bold').nth(0); + const editorAItalic = page.getByText('italic').nth(0); + const editorAUnderline = page.getByText('underline').nth(0); + const editorAStrike = page.getByText('strike').nth(0); + const editorACode = page.getByText('code').nth(0); + + const editorAUndo = page.getByText('undo').nth(0); + const editorARedo = page.getByText('redo').nth(0); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + + await type(page, 'abcdefg'); + + expect(await editorA.innerText()).toBe('abcdefg'); + expect(await editorB.innerText()).toBe('abcdefg'); + + let delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'abcdefg', + }, + ]); + + await setInlineRichTextRange(page, { index: 2, length: 3 }); + + await editorABold.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + bold: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorAItalic.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + bold: true, + italic: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorAUnderline.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + bold: true, + italic: true, + underline: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorAStrike.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorACode.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorAUndo.click({ + clickCount: 5, + }); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'abcdefg', + }, + ]); + + await editorARedo.click({ + clickCount: 5, + }); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + bold: true, + italic: true, + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorABold.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + italic: true, + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorAItalic.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + underline: true, + strike: true, + code: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorAUnderline.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + strike: true, + code: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorAStrike.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'ab', + }, + { + insert: 'cde', + attributes: { + code: true, + }, + }, + { + insert: 'fg', + }, + ]); + + await editorACode.click(); + await page.waitForTimeout(100); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'abcdefg', + }, + ]); +}); + +test('overlapping styles', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorB = page.locator('[data-v-root="true"]').nth(1); + + const editorABold = page.getByText('bold').nth(0); + const editorAItalic = page.getByText('italic').nth(0); + + const editorAUndo = page.getByText('undo').nth(0); + const editorARedo = page.getByText('redo').nth(0); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + + await type(page, 'abcdefghijk'); + + expect(await editorA.innerText()).toBe('abcdefghijk'); + expect(await editorB.innerText()).toBe('abcdefghijk'); + + let delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'abcdefghijk', + }, + ]); + + await setInlineRichTextRange(page, { index: 1, length: 3 }); + await editorABold.click(); + + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'a', + }, + { + insert: 'bcd', + attributes: { + bold: true, + }, + }, + { + insert: 'efghijk', + }, + ]); + + await setInlineRichTextRange(page, { index: 7, length: 3 }); + await editorABold.click(); + + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'a', + }, + { + insert: 'bcd', + attributes: { + bold: true, + }, + }, + { + insert: 'efg', + }, + { + insert: 'hij', + attributes: { + bold: true, + }, + }, + { + insert: 'k', + }, + ]); + + await setInlineRichTextRange(page, { index: 3, length: 5 }); + await editorAItalic.click(); + + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'a', + }, + { + insert: 'bc', + attributes: { + bold: true, + }, + }, + { + insert: 'd', + attributes: { + bold: true, + italic: true, + }, + }, + { + insert: 'efg', + attributes: { + italic: true, + }, + }, + { + insert: 'h', + attributes: { + bold: true, + italic: true, + }, + }, + { + insert: 'ij', + attributes: { + bold: true, + }, + }, + { + insert: 'k', + }, + ]); + + await editorAUndo.click({ + clickCount: 3, + }); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'abcdefghijk', + }, + ]); + + await editorARedo.click({ + clickCount: 3, + }); + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'a', + }, + { + insert: 'bc', + attributes: { + bold: true, + }, + }, + { + insert: 'd', + attributes: { + bold: true, + italic: true, + }, + }, + { + insert: 'efg', + attributes: { + italic: true, + }, + }, + { + insert: 'h', + attributes: { + bold: true, + italic: true, + }, + }, + { + insert: 'ij', + attributes: { + bold: true, + }, + }, + { + insert: 'k', + }, + ]); +}); + +test('input continuous spaces', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorB = page.locator('[data-v-root="true"]').nth(1); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + + await type(page, 'abc def'); + + expect(await editorA.innerText()).toBe('abc def'); + expect(await editorB.innerText()).toBe('abc def'); + + await focusInlineRichText(page); + await page.waitForTimeout(100); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + + await press(page, 'Enter'); + + expect(await editorA.innerText()).toBe('abc \n' + ' def'); + expect(await editorB.innerText()).toBe('abc \n' + ' def'); +}); + +test('select from the start of line using shift+arrow', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorB = page.locator('[data-v-root="true"]').nth(1); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + + await type(page, 'abc'); + await press(page, 'Enter'); + await type(page, 'def'); + await press(page, 'Enter'); + await type(page, 'ghi'); + + expect(await editorA.innerText()).toBe('abc\ndef\nghi'); + expect(await editorB.innerText()).toBe('abc\ndef\nghi'); + + /** + * abc + * def + * |ghi + */ + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await assertSelection(page, 0, 8); + + /** + * |abc + * def + * |ghi + */ + await page.keyboard.down('Shift'); + await press(page, 'ArrowUp'); + await press(page, 'ArrowUp'); + await assertSelection(page, 0, 0, 8); + + /** + * a|bc + * def + * |ghi + */ + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 1, 7); + await press(page, 'Backspace'); + await page.waitForTimeout(100); + + expect(await editorA.innerText()).toBe('aghi'); + expect(await editorB.innerText()).toBe('aghi'); +}); + +test('getLine', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorB = page.locator('[data-v-root="true"]').nth(1); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + + await type(page, 'abc\ndef\nghi'); + + expect(await editorA.innerText()).toBe('abc\ndef\nghi'); + expect(await editorB.innerText()).toBe('abc\ndef\nghi'); + + const [line1, offset1] = await getInlineRichTextLine(page, 0); + const [line2, offset2] = await getInlineRichTextLine(page, 1); + const [line3, offset3] = await getInlineRichTextLine(page, 4); + const [line4, offset4] = await getInlineRichTextLine(page, 5); + const [line5, offset5] = await getInlineRichTextLine(page, 8); + const [line6, offset6] = await getInlineRichTextLine(page, 11); + + expect(line1).toEqual('abc'); + expect(offset1).toEqual(0); + expect(line2).toEqual('abc'); + expect(offset2).toEqual(1); + expect(line3).toEqual('def'); + expect(offset3).toEqual(0); + expect(line4).toEqual('def'); + expect(offset4).toEqual(1); + expect(line5).toEqual('ghi'); + expect(offset5).toEqual(0); + expect(line6).toEqual('ghi'); + expect(offset6).toEqual(3); +}); + +test('yText should not contain \r', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + await page.waitForTimeout(100); + const message = await page.evaluate(() => { + const richText = document + .querySelector('test-page') + ?.querySelector('test-rich-text'); + + if (!richText) { + throw new Error('Cannot find test-rich-text'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const editor = (richText as any).inlineEditor as InlineEditor; + + try { + editor.insertText({ index: 0, length: 0 }, 'abc\r'); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (e as any).message; + } + }); + + expect(message).toBe( + 'yText must not contain "\\r" because it will break the range synchronization' + ); +}); + +test('embed', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorAEmbed = page.getByText('embed').nth(0); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + + await page.waitForTimeout(100); + + await type(page, 'abcde'); + + expect(await editorA.innerText()).toBe('abcde'); + + await press(page, 'ArrowLeft'); + await page.waitForTimeout(100); + await page.keyboard.down('Shift'); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await press(page, 'ArrowLeft'); + await page.keyboard.up('Shift'); + await page.waitForTimeout(100); + await assertSelection(page, 0, 1, 3); + + await editorAEmbed.click(); + const embedCount = await page.locator('[data-v-embed="true"]').count(); + expect(embedCount).toBe(3); + + // try to update cursor position using arrow keys + await assertSelection(page, 0, 1, 3); + await press(page, 'ArrowLeft'); + await assertSelection(page, 0, 1, 0); + await press(page, 'ArrowLeft'); + await assertSelection(page, 0, 0, 0); + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 1, 0); + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 1, 1); + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 2, 0); + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 2, 1); + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 3, 0); + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 3, 1); + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 4, 0); + await press(page, 'ArrowRight'); + await assertSelection(page, 0, 5, 0); + await press(page, 'ArrowLeft'); + await assertSelection(page, 0, 4, 0); + await press(page, 'ArrowLeft'); + await assertSelection(page, 0, 3, 1); + + // try to update cursor position and select embed element by clicking embed element + let rect = await getInlineRangeIndexRect(page, [0, 1]); + await page.mouse.click(rect.x + 3, rect.y); + await assertSelection(page, 0, 1, 1); + + rect = await getInlineRangeIndexRect(page, [0, 2]); + await page.mouse.click(rect.x + 3, rect.y); + await assertSelection(page, 0, 2, 1); + + rect = await getInlineRangeIndexRect(page, [0, 3]); + await page.mouse.click(rect.x + 3, rect.y); + await assertSelection(page, 0, 3, 1); +}); + +test('delete embed when pressing backspace after embed', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + const editorAEmbed = page.getByText('embed').nth(0); + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + await page.waitForTimeout(100); + await type(page, 'ab'); + expect(await editorA.innerText()).toBe('ab'); + + await page.keyboard.down('Shift'); + await press(page, 'ArrowLeft'); + await page.keyboard.up('Shift'); + await page.waitForTimeout(100); + await assertSelection(page, 0, 1, 1); + await editorAEmbed.click(); + + let delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'a', + }, + { + insert: 'b', + attributes: { + embed: true, + }, + }, + ]); + + const rect = await getInlineRangeIndexRect(page, [0, 2]); + // use click to select right side of the embed instead of use arrow key + await page.mouse.click(rect.x + 3, rect.y); + await assertSelection(page, 0, 2, 0); + await press(page, 'Backspace'); + + delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'a', + }, + ]); +}); + +test('markdown shortcut using keyboard util', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + await page.waitForTimeout(100); + + await type(page, 'aaa**bbb** ccc'); + + const delta = await getDeltaFromInlineRichText(page); + expect(delta).toEqual([ + { + insert: 'aaa', + }, + { + insert: 'bbb', + attributes: { + bold: true, + }, + }, + { + insert: 'ccc', + }, + ]); +}); + +test('triple click to select line', async ({ page }) => { + await enterInlineEditorPlayground(page); + await focusInlineRichText(page); + + const editorA = page.locator('[data-v-root="true"]').nth(0); + + expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); + await page.waitForTimeout(100); + await type(page, 'abc\nabc abc abc\nabc'); + + expect(await editorA.innerText()).toBe('abc\nabc abc abc\nabc'); + + const rect = await getInlineRangeIndexRect(page, [0, 10]); + await page.mouse.click(rect.x, rect.y, { + clickCount: 3, + }); + await assertSelection(page, 0, 4, 11); + + await press(page, 'Backspace'); + expect(await editorA.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nabc'); +}); diff --git a/blocksuite/tests-legacy/latex/block.spec.ts b/blocksuite/tests-legacy/latex/block.spec.ts new file mode 100644 index 0000000000000..c2b782f475576 --- /dev/null +++ b/blocksuite/tests-legacy/latex/block.spec.ts @@ -0,0 +1,62 @@ +import { expect } from '@playwright/test'; + +import { + enterPlaygroundRoom, + focusRichText, + getPageSnapshot, + initEmptyParagraphState, + type, +} from '../utils/actions/index.js'; +import { test } from '../utils/playwright.js'; + +test('add latex block using slash menu', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await type(page, '/eq\naaa'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); + +test('add latex block using markdown shortcut with space', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await type(page, '$$$$ aaa'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); + +test('add latex block using markdown shortcut with enter', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await type(page, '$$$$\naaa'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); diff --git a/blocksuite/tests-legacy/latex/inline.spec.ts b/blocksuite/tests-legacy/latex/inline.spec.ts new file mode 100644 index 0000000000000..5fe461a3f72d5 --- /dev/null +++ b/blocksuite/tests-legacy/latex/inline.spec.ts @@ -0,0 +1,337 @@ +import { ZERO_WIDTH_SPACE } from '@inline/consts.js'; +import { expect } from '@playwright/test'; +import { + assertRichTextInlineDeltas, + assertRichTextInlineRange, +} from 'utils/asserts.js'; + +import { + cutByKeyboard, + pasteByKeyboard, + pressArrowLeft, + pressArrowRight, + pressArrowUp, + pressBackspace, + pressBackspaceWithShortKey, + pressEnter, + pressShiftEnter, + redoByKeyboard, + selectAllByKeyboard, + type, + undoByKeyboard, +} from '../utils/actions/keyboard.js'; +import { + enterPlaygroundRoom, + focusRichText, + initEmptyParagraphState, +} from '../utils/actions/misc.js'; +import { test } from '../utils/playwright.js'; + +test('add inline latex at the start of line', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const latexEditorLine = page.locator('latex-editor-menu v-line div'); + const latexElement = page.locator( + 'affine-paragraph rich-text affine-latex-node' + ); + + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + expect(await latexElement.isVisible()).not.toBeTruthy(); + await type(page, '$$ '); + expect(await latexEditorLine.isVisible()).toBeTruthy(); + expect(await latexElement.isVisible()).toBeTruthy(); + expect(await latexElement.locator('.placeholder').innerText()).toBe( + 'Equation' + ); + await type(page, 'E=mc^2'); + expect(await latexEditorLine.innerText()).toBe('E=mc^2'); + expect(await latexElement.locator('.katex').innerHTML()).toBe( + 'E=mc2E=mc^2' + ); + + await pressEnter(page); + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + expect(await latexElement.locator('.katex').innerHTML()).toBe( + 'E=mc2E=mc^2' + ); +}); + +test('add inline latex in the middle of text', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const latexEditorLine = page.locator('latex-editor-menu v-line div'); + const latexElement = page.locator( + 'affine-paragraph rich-text affine-latex-node' + ); + + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + expect(await latexElement.isVisible()).not.toBeTruthy(); + await type(page, 'aaaa'); + await pressArrowLeft(page, 2); + await type(page, '$$ '); + expect(await latexEditorLine.isVisible()).toBeTruthy(); + expect(await latexElement.isVisible()).toBeTruthy(); + expect(await latexElement.locator('.placeholder').innerText()).toBe( + 'Equation' + ); + await type(page, 'E=mc^2'); + expect(await latexEditorLine.innerText()).toBe('E=mc^2'); + expect(await latexElement.locator('.katex').innerHTML()).toBe( + 'E=mc2E=mc^2' + ); + + await pressEnter(page); + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + expect(await latexElement.locator('.katex').innerHTML()).toBe( + 'E=mc2E=mc^2' + ); +}); + +test('update inline latex by clicking the node', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const latexEditorLine = page.locator('latex-editor-menu v-line div'); + const latexElement = page.locator( + 'affine-paragraph rich-text affine-latex-node' + ); + + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + await type(page, '$$ '); + expect(await latexEditorLine.isVisible()).toBeTruthy(); + await type(page, 'E=mc^2'); + await pressEnter(page); + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + await latexElement.click(); + expect(await latexEditorLine.isVisible()).toBeTruthy(); + await pressBackspace(page, 6); + await type(page, String.raw`\def\arraystretch{1.5}`); + await pressShiftEnter(page); + await type(page, String.raw`\begin{array}{c:c:c}`); + await pressShiftEnter(page); + await type(page, String.raw`a & b & c \\ \\ hline`); + await pressShiftEnter(page); + await type(page, String.raw`d & e & f \\`); + await pressShiftEnter(page); + await type(page, String.raw`\hdashline`); + await pressShiftEnter(page); + await type(page, String.raw`g & h & i`); + await pressShiftEnter(page); + await type(page, String.raw`\end{array}`); + expect(await latexElement.locator('.katex').innerHTML()).toBe( + 'abchlinedefghi\\def\\arraystretch{1.5}\n\\begin{array}{c:c:c}\na & b & c \\\\ \\\\ hline\nd & e & f \\\\\n\\hdashline\ng & h & i\n\\end{array}' + ); + + // click outside to hide the editor + await page.click('affine-editor-container'); + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); +}); + +test('latex editor', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const latexEditorLine = page.locator('latex-editor-menu v-line div'); + const latexElement = page.locator( + 'affine-paragraph rich-text affine-latex-node' + ); + + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + await type(page, '$$ '); + expect(await latexEditorLine.isVisible()).toBeTruthy(); + // test cursor movement works as expected + // https://github.com/toeverything/blocksuite/pull/8368 + await type(page, 'ababababababababababababababababababababababababab'); + expect(await latexEditorLine.innerText()).toBe( + 'ababababababababababababababababababababababababab' + ); + // click outside to hide the editor + expect(await latexEditorLine.isVisible()).toBeTruthy(); + await page.mouse.click(130, 130); + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + await latexElement.click(); + expect(await latexEditorLine.isVisible()).toBeTruthy(); + expect(await latexEditorLine.innerText()).toBe( + 'ababababababababababababababababababababababababab' + ); + + await pressBackspaceWithShortKey(page, 2); + expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE); + await undoByKeyboard(page); + expect(await latexEditorLine.innerText()).toBe( + 'ababababababababababababababababababababababababab' + ); + await redoByKeyboard(page); + expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE); + await undoByKeyboard(page); + expect(await latexEditorLine.innerText()).toBe( + 'ababababababababababababababababababababababababab' + ); + + // undo-redo + await pressArrowLeft(page, 5); + await page.keyboard.down('Shift'); + await pressArrowUp(page); + await pressArrowRight(page); + await page.keyboard.up('Shift'); + /** + * abababababababababab|ababab + * abababababababababa|babab + */ + await cutByKeyboard(page); + expect(await latexEditorLine.innerText()).toBe('ababababababababababbabab'); + /** + * abababababababababab|babab + */ + await pressArrowRight(page, 2); + /** + * ababababababababababba|bab + */ + await pasteByKeyboard(page); + expect(await latexEditorLine.innerText()).toBe( + 'ababababababababababbaabababababababababababababab' + ); + + await selectAllByKeyboard(page); + await pressBackspace(page); + expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE); + + // highlight + await type( + page, + String.raw`a+\left(\vcenter{\hbox{$\frac{\frac a b}c$}}\right)` + ); + expect( + (await latexEditorLine.locator('latex-editor-unit').innerHTML()).replace( + /lit\$\d+\$/g, + 'lit$test$' + ) + ).toBe( + '\x3C!---->\x3C!--?lit$test$-->\x3C!---->\x3C!---->\x3C!--?lit$test$-->a+\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->\\left\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->(\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->\\vcenter\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->{\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->\\hbox\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->{\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->$\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->\\frac{\\frac a b}c\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->$\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->}}\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->\\right\x3C!---->\x3C!---->\x3C!---->\x3C!--?lit$test$-->)\x3C!---->' + ); +}); + +test('add inline latex using slash menu', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const latexEditorLine = page.locator('latex-editor-menu v-line div'); + const latexElement = page.locator( + 'affine-paragraph rich-text affine-latex-node' + ); + + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + expect(await latexElement.isVisible()).not.toBeTruthy(); + await type(page, '/ieq\n'); + expect(await latexEditorLine.isVisible()).toBeTruthy(); + expect(await latexElement.isVisible()).toBeTruthy(); + expect(await latexElement.locator('.placeholder').innerText()).toBe( + 'Equation' + ); + await type(page, 'E=mc^2'); + expect(await latexEditorLine.innerText()).toBe('E=mc^2'); + expect(await latexElement.locator('.katex').innerHTML()).toBe( + 'E=mc2E=mc^2' + ); + + await pressEnter(page); + expect(await latexEditorLine.isVisible()).not.toBeTruthy(); + expect(await latexElement.locator('.katex').innerHTML()).toBe( + 'E=mc2E=mc^2' + ); +}); + +test('add inline latex using markdown shortcut', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + // toggle by space or enter + await type(page, 'aa$$bb$$ cc$$dd$$\n'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: ' ', + attributes: { + latex: 'bb', + }, + }, + { + insert: 'cc', + }, + { + insert: ' ', + attributes: { + latex: 'dd', + }, + }, + ]); + + await pressArrowUp(page); + await pressArrowRight(page, 3); + await pressBackspace(page); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aacc', + }, + { + insert: ' ', + attributes: { + latex: 'dd', + }, + }, + ]); +}); + +test('undo-redo when add inline latex using markdown shortcut', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'aa$$bb$$ '); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: ' ', + attributes: { + latex: 'bb', + }, + }, + ]); + await assertRichTextInlineRange(page, 0, 3, 0); + + await undoByKeyboard(page); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa$$bb$$ ', + }, + ]); + await assertRichTextInlineRange(page, 0, 9, 0); + + await redoByKeyboard(page); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: ' ', + attributes: { + latex: 'bb', + }, + }, + ]); + await assertRichTextInlineRange(page, 0, 3, 0); +}); diff --git a/blocksuite/tests-legacy/link.spec.ts b/blocksuite/tests-legacy/link.spec.ts new file mode 100644 index 0000000000000..568a3eb8aa7c1 --- /dev/null +++ b/blocksuite/tests-legacy/link.spec.ts @@ -0,0 +1,543 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import { + cutByKeyboard, + dragBetweenIndices, + enterPlaygroundRoom, + focusRichText, + focusRichTextEnd, + getPageSnapshot, + initEmptyParagraphState, + pasteByKeyboard, + pressEnter, + pressShiftEnter, + pressTab, + selectAllByKeyboard, + setSelection, + SHORT_KEY, + switchReadonly, + type, + waitNextFrame, +} from './utils/actions/index.js'; +import { + assertKeyboardWorkInInput, + assertStoreMatchJSX, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +const pressCreateLinkShortCut = async (page: Page) => { + await page.keyboard.press(`${SHORT_KEY}+k`); +}; + +test('basic link', async ({ page }, testInfo) => { + const linkText = 'linkText'; + const link = 'http://example.com'; + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, linkText); + + // Create link + await dragBetweenIndices(page, [0, 0], [0, 8]); + await pressCreateLinkShortCut(page); + await page.mouse.move(0, 0); + + const createLinkPopoverLocator = page.locator('.affine-link-popover.create'); + await expect(createLinkPopoverLocator).toBeVisible(); + const linkPopoverInput = page.locator('.affine-link-popover-input'); + await expect(linkPopoverInput).toBeVisible(); + await type(page, link); + await pressEnter(page); + await expect(createLinkPopoverLocator).not.toBeVisible(); + + const linkLocator = page.locator('affine-link a'); + await expect(linkLocator).toHaveAttribute('href', link); + + // clear text selection + await page.keyboard.press('ArrowLeft'); + + const viewLinkPopoverLocator = page.locator('.affine-link-popover.view'); + // Hover link + await expect(viewLinkPopoverLocator).not.toBeVisible(); + await linkLocator.hover(); + // wait for popover delay open + await page.waitForTimeout(200); + await expect(viewLinkPopoverLocator).toBeVisible(); + + // Edit link + const text2 = 'link2'; + const link2 = 'https://github.com'; + const editLinkBtn = viewLinkPopoverLocator.getByTestId('edit'); + await editLinkBtn.click(); + + const editLinkPopoverLocator = page.locator('.affine-link-edit-popover'); + await expect(editLinkPopoverLocator).toBeVisible(); + // workaround to make tab key work as expected + await editLinkPopoverLocator.click({ + position: { x: 5, y: 5 }, + }); + await page.keyboard.press('Tab'); + await type(page, text2); + await page.keyboard.press('Tab'); + await type(page, link2); + await page.keyboard.press('Tab'); + await pressEnter(page); + const link2Locator = page.locator('affine-link a'); + + await expect(link2Locator).toHaveAttribute('href', link2); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); +}); + +test('add link when dragging from empty line', async ({ page }) => { + const linkText = 'linkText\n\n'; + const link = 'http://example.com'; + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, linkText); + + // Create link + await dragBetweenIndices(page, [2, 0], [0, 0], { + x: 1, + y: 2, + }); + await pressCreateLinkShortCut(page); + await page.mouse.move(0, 0); + + const createLinkPopoverLocator = page.locator('.affine-link-popover.create'); + await expect(createLinkPopoverLocator).toBeVisible(); + const linkPopoverInput = page.locator('.affine-link-popover-input'); + await expect(linkPopoverInput).toBeVisible(); + await type(page, link); + await pressEnter(page); + await expect(createLinkPopoverLocator).not.toBeVisible(); + + const linkLocator = page.locator('affine-link a'); + await expect(linkLocator).toHaveAttribute('href', link); +}); + +async function createLinkBlock(page: Page, str: string, link: string) { + const id = await page.evaluate( + ([str, link]) => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text('title'), + }); + const noteId = doc.addBlock('affine:note', {}, rootId); + + const text = new doc.Text([ + { insert: 'Hello' }, + { insert: str, attributes: { link } }, + ]); + const id = doc.addBlock( + 'affine:paragraph', + { type: 'text', text: text }, + noteId + ); + return id; + }, + [str, link] + ); + return id; +} + +test('type character in link should not jump out link node', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + const id = await createLinkBlock(page, 'link text', 'http://example.com'); + await focusRichText(page, 0); + await page.keyboard.press('ArrowLeft'); + await type(page, 'IN_LINK'); + await assertStoreMatchJSX( + page, + ` + + + + + } + prop:type="text" +/>`, + id + ); +}); + +test('type character after link should not extend the link attributes', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + const id = await createLinkBlock(page, 'link text', 'http://example.com'); + await focusRichText(page, 0); + await type(page, 'AFTER_LINK'); + await assertStoreMatchJSX( + page, + ` + + + + + + } + prop:type="text" +/>`, + id + ); +}); + +test('readonly mode should not trigger link popup', async ({ page }) => { + await enterPlaygroundRoom(page); + const linkText = 'linkText'; + await createLinkBlock(page, 'linkText', 'http://example.com'); + await focusRichText(page, 0); + const linkLocator = page.locator(`text="${linkText}"`); + + // Hover link + const linkPopoverLocator = page.locator('.affine-link-popover'); + await linkLocator.hover(); + await expect(linkPopoverLocator).toBeVisible(); + await switchReadonly(page); + + await page.mouse.move(0, 0); + // XXX Wait for readonly delay + await page.waitForTimeout(300); + + await linkLocator.hover(); + await expect(linkPopoverLocator).not.toBeVisible(); + + // --- + // press hotkey should not trigger create link popup + + await dragBetweenIndices(page, [0, 0], [0, 3]); + await pressCreateLinkShortCut(page); + + await expect(linkPopoverLocator).not.toBeVisible(); + const linkPopoverInput = page.locator('.affine-link-popover-input'); + await expect(linkPopoverInput).not.toBeVisible(); +}); + +test('should mock selection not stored', async ({ page }) => { + const linkText = 'linkText'; + const link = 'http://example.com'; + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, linkText); + + // Create link + await dragBetweenIndices(page, [0, 0], [0, 8]); + await pressCreateLinkShortCut(page); + + const mockSelectNode = page.locator('.mock-selection'); + await expect(mockSelectNode).toHaveCount(1); + await expect(mockSelectNode).toBeVisible(); + + // the mock select node should not be stored in the Y doc + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + + await type(page, link); + await pressEnter(page); + + // the mock select node should be removed after link created + await expect(mockSelectNode).not.toBeVisible(); + await expect(mockSelectNode).toHaveCount(0); +}); + +test('should keyboard work in link popover', async ({ page }) => { + await enterPlaygroundRoom(page); + const linkText = 'linkText'; + await createLinkBlock(page, linkText, 'http://example.com'); + + await dragBetweenIndices(page, [0, 0], [0, 8]); + await pressCreateLinkShortCut(page); + const linkPopoverInput = page.locator('.affine-link-popover-input'); + await assertKeyboardWorkInInput(page, linkPopoverInput); + await page.mouse.click(500, 500); + + const linkLocator = page.locator(`text="${linkText}"`); + const linkPopover = page.locator('.affine-link-popover'); + await linkLocator.hover(); + await waitNextFrame(page, 200); + await expect(linkLocator).toBeVisible(); + // Hover link + await linkLocator.hover(); + // wait for popover delay open + await page.waitForTimeout(200); + await expect(linkPopover).toBeVisible(); + const editLinkBtn = linkPopover.getByTestId('edit'); + await editLinkBtn.click(); + + const editLinkPopover = page.locator('.affine-link-edit-popover'); + await expect(editLinkPopover).toBeVisible(); + + const editTextInput = editLinkPopover.locator( + '.affine-edit-area.text .affine-edit-input' + ); + await assertKeyboardWorkInInput(page, editTextInput); + const editLinkInput = editLinkPopover.locator( + '.affine-edit-area.link .affine-edit-input' + ); + await assertKeyboardWorkInInput(page, editLinkInput); +}); + +test('link bar should not be appear when the range is collapsed', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + + await pressCreateLinkShortCut(page); + const linkPopoverLocator = page.locator('.affine-link-popover'); + await expect(linkPopoverLocator).not.toBeVisible(); + + await dragBetweenIndices(page, [0, 0], [0, 3]); + await pressCreateLinkShortCut(page); + await expect(linkPopoverLocator).toBeVisible(); + + await focusRichText(page); // click to cancel the link popover + await focusRichTextEnd(page); + await pressShiftEnter(page); + await waitNextFrame(page); + await type(page, 'bbb'); + await dragBetweenIndices(page, [0, 1], [0, 5]); + await pressCreateLinkShortCut(page); + await expect(linkPopoverLocator).toBeVisible(); + + await focusRichTextEnd(page); + await pressEnter(page); + // create auto line-break in span element + await type(page, 'd'.repeat(67)); + await page.mouse.click(1, 1); + await waitNextFrame(page); + await dragBetweenIndices(page, [1, 1], [1, 66]); + await pressCreateLinkShortCut(page); + await expect(linkPopoverLocator).toBeVisible(); +}); + +test('create link with paste', async ({ page }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + + await dragBetweenIndices(page, [0, 0], [0, 3]); + await pressCreateLinkShortCut(page); + + const createLinkPopoverLocator = page.locator('.affine-link-popover.create'); + const confirmBtn = createLinkPopoverLocator.locator('.affine-confirm-button'); + + await expect(createLinkPopoverLocator).toBeVisible(); + await expect(confirmBtn).toHaveAttribute('disabled'); + + await type(page, 'affine.pro'); + await expect(confirmBtn).not.toHaveAttribute('disabled'); + await selectAllByKeyboard(page); + await cutByKeyboard(page); + + // press enter should not trigger confirm + await pressEnter(page); + await expect(createLinkPopoverLocator).toBeVisible(); + await expect(confirmBtn).toHaveAttribute('disabled'); + + await pasteByKeyboard(page, false); + await expect(confirmBtn).not.toHaveAttribute('disabled'); + await pressEnter(page); + await expect(createLinkPopoverLocator).not.toBeVisible(); + await assertStoreMatchJSX( + page, + ` + + + + } + prop:type="text" +/>`, + paragraphId + ); +}); + +test('convert link to card', async ({ page }, testInfo) => { + const linkText = 'alinkTexta'; + const link = 'http://example.com'; + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + await pressEnter(page); + await type(page, linkText); + + // Create link + await setSelection(page, 3, 1, 3, 9); + await pressCreateLinkShortCut(page); + await waitNextFrame(page); + const linkPopoverLocator = page.locator('.affine-link-popover'); + await expect(linkPopoverLocator).toBeVisible(); + const linkPopoverInput = page.locator('.affine-link-popover-input'); + await expect(linkPopoverInput).toBeVisible(); + await type(page, link); + await pressEnter(page); + await expect(linkPopoverLocator).not.toBeVisible(); + await focusRichText(page, 1); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); + + const linkLocator = page.locator('affine-link a'); + + await linkLocator.hover(); + await waitNextFrame(page); + await expect(linkPopoverLocator).toBeVisible(); + + await page.getByRole('button', { name: 'Switch view' }).click(); + const linkToCardBtn = page.getByTestId('link-to-card'); + const linkToEmbedBtn = page.getByTestId('link-to-embed'); + await expect(linkToCardBtn).toBeVisible(); + await expect(linkToEmbedBtn).not.toBeVisible(); + + await page.mouse.move(0, 0); + await waitNextFrame(page); + await expect(linkPopoverLocator).not.toBeVisible(); + await focusRichText(page, 1); + await pressTab(page); + + await linkLocator.hover(); + await waitNextFrame(page); + await expect(linkPopoverLocator).toBeVisible(); + await page.getByRole('button', { name: 'Switch view' }).click(); + await expect(linkToCardBtn).toBeVisible(); + await expect(linkToEmbedBtn).not.toBeVisible(); +}); + +//TODO: wait for embed block completed +test.skip('convert link to embed', async ({ page }) => { + const linkText = 'alinkTexta'; + const link = 'https://www.youtube.com/watch?v=U6s2pdxebSo'; + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + await pressEnter(page); + await type(page, linkText); + + // Create link + await setSelection(page, 3, 1, 3, 9); + await pressCreateLinkShortCut(page); + await waitNextFrame(page); + const linkPopoverLocator = page.locator('.affine-link-popover'); + await expect(linkPopoverLocator).toBeVisible(); + const linkPopoverInput = page.locator('.affine-link-popover-input'); + await expect(linkPopoverInput).toBeVisible(); + await type(page, link); + await pressEnter(page); + await expect(linkPopoverLocator).not.toBeVisible(); + await focusRichText(page); + + await assertStoreMatchJSX( + page, + ` + + + + + + + + + } + prop:type="text" + /> + +` + ); + + const linkToCardBtn = page.getByTestId('link-to-card'); + const linkToEmbedBtn = page.getByTestId('link-to-embed'); + const linkLocator = page.locator('affine-link a'); + + await linkLocator.hover(); + await waitNextFrame(page); + await expect(linkPopoverLocator).toBeVisible(); + await expect(linkToCardBtn).toBeVisible(); + await expect(linkToEmbedBtn).toBeVisible(); + + await page.mouse.move(0, 0); + await waitNextFrame(page); + await expect(linkPopoverLocator).not.toBeVisible(); + await focusRichText(page, 1); + await pressTab(page); + + await linkLocator.hover(); + await waitNextFrame(page); + await expect(linkPopoverLocator).toBeVisible(); + await expect(linkToCardBtn).not.toBeVisible(); + await expect(linkToEmbedBtn).not.toBeVisible(); +}); diff --git a/blocksuite/tests-legacy/linked-page.spec.ts b/blocksuite/tests-legacy/linked-page.spec.ts new file mode 100644 index 0000000000000..bcfa9ca1e4214 --- /dev/null +++ b/blocksuite/tests-legacy/linked-page.spec.ts @@ -0,0 +1,1220 @@ +import { expect, type Page } from '@playwright/test'; +import { switchEditorMode } from 'utils/actions/edgeless.js'; +import { getLinkedDocPopover } from 'utils/actions/linked-doc.js'; + +import { + addNewPage, + getDebugMenu, + switchToPage, +} from './utils/actions/click.js'; +import { dragBetweenIndices, dragBlockToPoint } from './utils/actions/drag.js'; +import { + copyByKeyboard, + cutByKeyboard, + pasteByKeyboard, + pressArrowLeft, + pressArrowRight, + pressBackspace, + pressEnter, + redoByKeyboard, + selectAllByKeyboard, + SHORT_KEY, + type, + undoByKeyboard, +} from './utils/actions/keyboard.js'; +import { + captureHistory, + enterPlaygroundRoom, + focusRichText, + focusTitle, + getPageSnapshot, + initEmptyEdgelessState, + initEmptyParagraphState, + setInlineRangeInSelectedRichText, + waitNextFrame, +} from './utils/actions/misc.js'; +import { + assertExists, + assertParentBlockFlavour, + assertRichTexts, + assertStoreMatchJSX, + assertTitle, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +async function createAndConvertToEmbedLinkedDoc(page: Page) { + const { createLinkedDoc } = getLinkedDocPopover(page); + const linkedDoc = await createLinkedDoc('page1'); + const lickedDocBox = await linkedDoc.boundingBox(); + assertExists(lickedDocBox); + await page.mouse.move( + lickedDocBox.x + lickedDocBox.width / 2, + lickedDocBox.y + lickedDocBox.height / 2 + ); + + await waitNextFrame(page, 200); + const referencePopup = page.locator('.affine-reference-popover-container'); + await expect(referencePopup).toBeVisible(); + + const switchButton = page.getByRole('button', { name: 'Switch view' }); + await switchButton.click(); + + const embedLinkedDocBtn = page.getByRole('button', { name: 'Card view' }); + await expect(embedLinkedDocBtn).toBeVisible(); + await embedLinkedDocBtn.click(); + await waitNextFrame(page, 200); +} + +test.describe('multiple page', () => { + test('should create and switch page work', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'title0'); + await focusRichText(page); + await type(page, 'page0'); + await assertRichTexts(page, ['page0']); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + const { id } = await addNewPage(page); + await switchToPage(page, id); + await focusTitle(page); + await type(page, 'title1'); + await focusRichText(page); + await type(page, 'page1'); + await assertRichTexts(page, ['page1']); + + await switchToPage(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); + }); +}); + +test.describe('reference node', () => { + test('linked doc popover can show and hide correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '[['); + + // `[[` should be converted to `@` + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + const { linkedDocPopover } = getLinkedDocPopover(page); + await expect(linkedDocPopover).toBeVisible(); + await pressArrowRight(page); + await expect(linkedDocPopover).toBeHidden(); + await type(page, '@'); + await expect(linkedDocPopover).toBeVisible(); + await assertRichTexts(page, ['@@']); + await pressBackspace(page); + await expect(linkedDocPopover).toBeHidden(); + }); + + test('linked doc popover should not show when the current content is @xx and pressing backspace', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '@'); + await page.keyboard.press('Escape'); + await type(page, 'a'); + + const { linkedDocPopover } = getLinkedDocPopover(page); + await expect(linkedDocPopover).toBeHidden(); + + await pressBackspace(page); + await expect(linkedDocPopover).toBeHidden(); + }); + + test('should reference node attributes correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + const { id } = await addNewPage(page); + await focusRichText(page); + await type(page, '[['); + await pressEnter(page); + + await assertStoreMatchJSX( + page, + ` + + + + } + prop:type="text" +/>`, + paragraphId + ); + + await pressBackspace(page); + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + }); + + test('should reference node can be selected', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await addNewPage(page); + await focusRichText(page); + + await type(page, '1'); + await type(page, '[['); + await pressEnter(page); + + await assertRichTexts(page, ['1 ']); + await type(page, '2'); + await assertRichTexts(page, ['1 2']); + await page.keyboard.press('ArrowLeft'); + await type(page, '3'); + await assertRichTexts(page, ['1 32']); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + // select the reference node + await page.keyboard.press('ArrowLeft'); + + // delete the reference node and insert text + await type(page, '4'); + await assertRichTexts(page, ['1432']); + }); + + test('text inserted in the between of reference nodes should not be extend attributes', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + const { id } = await addNewPage(page); + await focusRichText(page); + + await type(page, '1'); + await type(page, '@'); + await pressEnter(page); + await type(page, '@'); + await pressEnter(page); + + await assertRichTexts(page, ['1 ']); + await type(page, '2'); + await assertRichTexts(page, ['1 2']); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + await page.keyboard.press('ArrowLeft'); + await type(page, '3'); + await assertRichTexts(page, ['1 3 2']); + + const snapshot = ` + + + + + + + + } + prop:type="text" +/>`; + await assertStoreMatchJSX(page, snapshot, paragraphId); + }); + + test('text can be inserted as expected when reference node is in the start or end of line', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + const { id } = await addNewPage(page); + await focusRichText(page); + + await type(page, '@'); + await pressEnter(page); + await type(page, '@'); + await pressEnter(page); + + await assertRichTexts(page, [' ']); + await type(page, '2'); + await assertRichTexts(page, [' 2']); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + await page.keyboard.press('ArrowLeft'); + await type(page, '3'); + await assertRichTexts(page, [' 3 2']); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + await page.keyboard.press('ArrowLeft'); + await type(page, '1'); + await assertRichTexts(page, ['1 3 2']); + + const snapshot = ` + + + + + + + + } + prop:type="text" +/>`; + await assertStoreMatchJSX(page, snapshot, paragraphId); + }); + + test('should the cursor move correctly around reference node', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + const { id } = await addNewPage(page); + await focusRichText(page); + + await type(page, '1'); + await type(page, '[['); + await pressEnter(page); + + await assertRichTexts(page, ['1 ']); + await type(page, '2'); + await assertRichTexts(page, ['1 2']); + await page.keyboard.press('ArrowLeft'); + await type(page, '3'); + await assertRichTexts(page, ['1 32']); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + await page.keyboard.press('ArrowLeft'); + await waitNextFrame(page); + await page.keyboard.press('ArrowLeft'); + + await type(page, '4'); + await assertRichTexts(page, ['14 32']); + + const snapshot = ` + + + + + + } + prop:type="text" +/>`; + await assertStoreMatchJSX(page, snapshot, paragraphId); + + await page.keyboard.press('ArrowRight'); + await captureHistory(page); + await pressBackspace(page); + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + await undoByKeyboard(page); + await assertStoreMatchJSX(page, snapshot, paragraphId); + await redoByKeyboard(page); + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + }); + + test('should create reference node works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const defaultPageId = 'doc:home'; + const { id: newId } = await addNewPage(page); + await switchToPage(page, newId); + await focusTitle(page); + await type(page, 'title'); + await switchToPage(page, defaultPageId); + + await focusRichText(page); + await type(page, '@'); + const { + linkedDocPopover, + refNode, + assertExistRefText: assertReferenceText, + } = getLinkedDocPopover(page); + await expect(linkedDocPopover).toBeVisible(); + await pressEnter(page); + await expect(linkedDocPopover).toBeHidden(); + await assertRichTexts(page, [' ']); + await expect(refNode).toBeVisible(); + await expect(refNode).toHaveCount(1); + await assertReferenceText('title'); + + await switchToPage(page, newId); + await focusTitle(page); + await pressBackspace(page); + await type(page, '1'); + await switchToPage(page, defaultPageId); + await assertReferenceText('titl1'); + }); + + test('can create linked page and jump', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'page0'); + + await focusRichText(page); + const { createLinkedDoc, findRefNode } = getLinkedDocPopover(page); + const linkedNode = await createLinkedDoc('page1'); + await linkedNode.click(); + + await assertTitle(page, 'page1'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + await focusRichText(page); + await type(page, '@page0'); + await pressEnter(page); + const refNode = await findRefNode('page0'); + await refNode.click(); + await assertTitle(page, 'page0'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); + }); + + test('should not merge consecutive identical reference nodes for rendering', async ({ + page, + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2136', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '[['); + await pressEnter(page); + await type(page, '[['); + await pressEnter(page); + + const { refNode } = getLinkedDocPopover(page); + await assertRichTexts(page, [' ']); + await expect(refNode).toHaveCount(2); + }); +}); + +test.describe('linked page popover', () => { + test('should show linked page popover show and hide', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + const { linkedDocPopover } = getLinkedDocPopover(page); + + await type(page, '[['); + await expect(linkedDocPopover).toBeVisible(); + await pressBackspace(page); + await expect(linkedDocPopover).toBeHidden(); + + await type(page, '@'); + await expect(linkedDocPopover).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(linkedDocPopover).toBeHidden(); + + await type(page, '@'); + await expect(linkedDocPopover).toBeVisible(); + await page.keyboard.press('ArrowRight'); + await expect(linkedDocPopover).toBeHidden(); + + await type(page, '@'); + await expect(linkedDocPopover).toBeVisible(); + await copyByKeyboard(page); + await expect(linkedDocPopover).toBeHidden(); + }); + + test('should fuzzy search works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const { + linkedDocPopover, + pageBtn, + assertExistRefText, + assertActivePageIdx, + } = getLinkedDocPopover(page); + + await focusTitle(page); + await type(page, 'page0'); + + const page1 = await addNewPage(page); + await switchToPage(page, page1.id); + await focusTitle(page); + await type(page, 'page1'); + + const page2 = await addNewPage(page); + await switchToPage(page, page2.id); + await focusTitle(page); + await type(page, 'page2'); + + await switchToPage(page); + await focusRichText(page); + await type(page, '@'); + await expect(linkedDocPopover).toBeVisible(); + await expect(pageBtn).toHaveCount(4); + + await assertActivePageIdx(0); + await page.keyboard.press('ArrowDown'); + await assertActivePageIdx(1); + + await page.keyboard.press('ArrowUp'); + await assertActivePageIdx(0); + await page.keyboard.press('Tab'); + await assertActivePageIdx(1); + await page.keyboard.press('Shift+Tab'); + await assertActivePageIdx(0); + + await expect(pageBtn).toHaveText([ + 'page1', + 'page2', + 'Create "Untitled" doc', + 'Import', + ]); + // page2 + // ^ ^ + await type(page, 'a2'); + await expect(pageBtn).toHaveCount(3); + await expect(pageBtn).toHaveText(['page2', 'Create "a2" doc', 'Import']); + await pressEnter(page); + await expect(linkedDocPopover).toBeHidden(); + await assertExistRefText('page2'); + }); + + test('should paste query works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await (async () => { + for (let index = 0; index < 3; index++) { + const newPage = await addNewPage(page); + await switchToPage(page, newPage.id); + await focusTitle(page); + await type(page, 'page' + index); + } + })(); + + await switchToPage(page); + await getDebugMenu(page).pagesBtn.click(); + await focusRichText(page); + await type(page, 'e2'); + await setInlineRangeInSelectedRichText(page, 0, 2); + await cutByKeyboard(page); + + const { pageBtn, linkedDocPopover } = getLinkedDocPopover(page); + await type(page, '@'); + await expect(linkedDocPopover).toBeVisible(); + await expect(pageBtn).toHaveText([ + 'page0', + 'page1', + 'page2', + 'Create "Untitled" doc', + 'Import', + ]); + + await page.keyboard.press(`${SHORT_KEY}+v`); + await expect(linkedDocPopover).toBeVisible(); + await expect(pageBtn).toHaveText(['page2', 'Create "e2" doc', 'Import']); + }); + + test('should multiple paste query not works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await (async () => { + for (let index = 0; index < 3; index++) { + const newPage = await addNewPage(page); + await switchToPage(page, newPage.id); + await focusTitle(page); + await type(page, 'page' + index); + } + })(); + + await switchToPage(page); + await getDebugMenu(page).pagesBtn.click(); + await focusRichText(page); + await type(page, 'pa'); + await pressEnter(page); + await type(page, 'ge'); + await pressEnter(page); + await type(page, '2'); + + await selectAllByKeyboard(page); + await waitNextFrame(page, 200); + await selectAllByKeyboard(page); + await waitNextFrame(page, 200); + await selectAllByKeyboard(page); + await waitNextFrame(page, 200); + await cutByKeyboard(page); + const note = page.locator('affine-note'); + await note.click({ force: true, position: { x: 100, y: 100 } }); + await waitNextFrame(page, 200); + + const { pageBtn, linkedDocPopover } = getLinkedDocPopover(page); + await type(page, '@'); + await expect(linkedDocPopover).toBeVisible(); + await expect(pageBtn).toHaveText([ + 'page0', + 'page1', + 'page2', + 'Create "Untitled" doc', + 'Import', + ]); + + await page.keyboard.press(`${SHORT_KEY}+v`); + await expect(linkedDocPopover).not.toBeVisible(); + }); + + test('should more docs works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await (async () => { + for (let index = 0; index < 10; index++) { + const newPage = await addNewPage(page); + await switchToPage(page, newPage.id); + await focusTitle(page); + await type(page, 'page' + index); + } + })(); + + await switchToPage(page); + await getDebugMenu(page).pagesBtn.click(); + await focusRichText(page); + await type(page, '@'); + + const { pageBtn, linkedDocPopover } = getLinkedDocPopover(page); + await expect(linkedDocPopover).toBeVisible(); + await expect(pageBtn).toHaveText([ + ...Array.from({ length: 6 }, (_, index) => `page${index}`), + '4 more docs', + 'Create "Untitled" doc', + 'Import', + ]); + + const moreNode = page.locator(`icon-button[data-id="Link to Doc More"]`); + await moreNode.click(); + await expect(pageBtn).toHaveText([ + ...Array.from({ length: 10 }, (_, index) => `page${index}`), + 'Create "Untitled" doc', + 'Import', + ]); + }); +}); + +test.describe('linked page with clipboard', () => { + test('paste linked page should paste as linked page', async ({ + page, + }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const { createLinkedDoc } = getLinkedDocPopover(page); + + await createLinkedDoc('page1'); + + await selectAllByKeyboard(page); + await copyByKeyboard(page); + await focusRichText(page); + await pasteByKeyboard(page); + const json = await getPageSnapshot(page, true); + expect(json).toMatchSnapshot(`${testInfo.title}.json`); + }); + + test('duplicated linked page should paste as linked page', async ({ + page, + }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const { createLinkedDoc } = getLinkedDocPopover(page); + + await createLinkedDoc('page0'); + + await type(page, '/duplicate'); + await pressEnter(page); + const json = await getPageSnapshot(page, true); + expect(json).toMatchSnapshot(`${testInfo.title}.json`); + }); +}); + +test('should [[Selected text]] converted to linked page', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2730', + }); + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '1234'); + + await dragBetweenIndices(page, [0, 1], [0, 2]); + await type(page, '['); + await assertRichTexts(page, ['1[2]34']); + await type(page, '['); + await assertStoreMatchJSX( + page, + ` + + + + + + } + prop:type="text" +/>`, + paragraphId + ); + await switchToPage(page, '3'); + await assertTitle(page, '2'); +}); + +test('add reference node before the other reference node', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + + const firstRefNode = page.locator('affine-reference').nth(0); + + await type(page, '@bbb'); + await pressEnter(page); + + expect(await firstRefNode.textContent()).toEqual( + expect.stringContaining('bbb') + ); + expect(await firstRefNode.textContent()).not.toEqual( + expect.stringContaining('ccc') + ); + + await pressArrowLeft(page, 3); + await type(page, '@ccc'); + await pressEnter(page); + + expect(await firstRefNode.textContent()).not.toEqual( + expect.stringContaining('bbb') + ); + expect(await firstRefNode.textContent()).toEqual( + expect.stringContaining('ccc') + ); +}); + +test('linked doc can be dragged from note to surface top level block', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await createAndConvertToEmbedLinkedDoc(page); + + await switchEditorMode(page); + await page.mouse.dblclick(450, 450); + + await dragBlockToPoint(page, '9', { x: 200, y: 200 }); + + await waitNextFrame(page); + await assertParentBlockFlavour(page, '9', 'affine:surface'); +}); + +// Aliases +test.describe('Customize linked doc title and description', () => { + // Inline View + test('should set a custom title for inline link', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'title0'); + await focusRichText(page); + await type(page, 'page0'); + await assertRichTexts(page, ['page0']); + + const { id } = await addNewPage(page); + await switchToPage(page, id); + await focusTitle(page); + await type(page, 'title1'); + await focusRichText(page); + await type(page, 'page1'); + await assertRichTexts(page, ['page1']); + + await getDebugMenu(page).pagesBtn.click(); + + await focusRichText(page); + await type(page, '@title0'); + await pressEnter(page); + + const { findRefNode } = getLinkedDocPopover(page); + const page0 = await findRefNode('title0'); + await page0.hover(); + + await waitNextFrame(page, 200); + const referencePopup = page.locator('.affine-reference-popover-container'); + await expect(referencePopup).toBeVisible(); + + const editButton = referencePopup.getByRole('button', { name: 'Edit' }); + await editButton.click(); + + await waitNextFrame(page, 200); + const popup = page.locator('.alias-form-popup'); + await expect(popup).toBeVisible(); + + const input = popup.locator('input'); + await expect(input).toBeFocused(); + + // title alias + await type(page, 'page0-title0'); + await pressEnter(page); + + await page.mouse.click(0, 0); + + await focusRichText(page); + await waitNextFrame(page, 200); + + const page0Alias = await findRefNode('page0-title0'); + await page0Alias.hover(); + + await waitNextFrame(page, 200); + await expect(referencePopup).toBeVisible(); + + // original title button + const docTitle = referencePopup.getByRole('button', { name: 'Doc title' }); + await expect(docTitle).toHaveText('title0', { useInnerText: true }); + + // reedit + await editButton.click(); + + await waitNextFrame(page, 200); + + // reset + await popup.getByRole('button', { name: 'Reset' }).click(); + + await waitNextFrame(page, 200); + + const resetedPage0 = await findRefNode('title0'); + await resetedPage0.hover(); + + await waitNextFrame(page, 200); + await expect(referencePopup).toBeVisible(); + await expect(docTitle).not.toBeVisible(); + }); + + // Card View + test('should set a custom title and description for card link', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'title0'); + + const { id } = await addNewPage(page); + await switchToPage(page, id); + await focusTitle(page); + await type(page, 'title1'); + + await getDebugMenu(page).pagesBtn.click(); + + await focusRichText(page); + await type(page, '@title0'); + await pressEnter(page); + + const { findRefNode } = getLinkedDocPopover(page); + const page0 = await findRefNode('title0'); + await page0.hover(); + + await waitNextFrame(page, 200); + const referencePopup = page.locator('.affine-reference-popover-container'); + + let editButton = referencePopup.getByRole('button', { name: 'Edit' }); + await editButton.click(); + + // title alias + await type(page, 'page0-title0'); + await pressEnter(page); + + await focusRichText(page); + await waitNextFrame(page, 200); + + const page0Alias = await findRefNode('page0-title0'); + await page0Alias.hover(); + + await waitNextFrame(page, 200); + const switchButton = referencePopup.getByRole('button', { + name: 'Switch view', + }); + await switchButton.click(); + + // switches to card view + const toCardButton = referencePopup.getByRole('button', { + name: 'Card view', + }); + await toCardButton.click(); + + await waitNextFrame(page, 200); + const linkedDocBlock = page.locator('affine-embed-linked-doc-block'); + await expect(linkedDocBlock).toBeVisible(); + + const linkedDocBlockTitle = linkedDocBlock.locator( + '.affine-embed-linked-doc-content-title-text' + ); + await expect(linkedDocBlockTitle).toHaveText('page0-title0'); + + await linkedDocBlock.click(); + + await waitNextFrame(page, 200); + const cardToolbar = page.locator('affine-embed-card-toolbar'); + const docTitleButton = cardToolbar.getByRole('button', { + name: 'Doc title', + }); + await expect(docTitleButton).toBeVisible(); + + editButton = cardToolbar.getByRole('button', { name: 'Edit' }); + await editButton.click(); + + await waitNextFrame(page, 200); + const editModal = page.locator('embed-card-edit-modal'); + const resetButton = editModal.getByRole('button', { name: 'Reset' }); + const saveButton = editModal.getByRole('button', { name: 'Save' }); + + // clears aliases + await resetButton.click(); + + await waitNextFrame(page, 200); + await expect(linkedDocBlockTitle).toHaveText('title0'); + + await linkedDocBlock.click(); + + await waitNextFrame(page, 200); + await editButton.click(); + + await waitNextFrame(page, 200); + + // title alias + await type(page, 'page0-title0'); + await page.keyboard.press('Tab'); + // description alias + await type(page, 'This is a new description'); + + // saves aliases + await saveButton.click(); + + await waitNextFrame(page, 200); + await expect(linkedDocBlockTitle).toHaveText('page0-title0'); + await expect( + page.locator('.affine-embed-linked-doc-content-note.alias') + ).toHaveText('This is a new description'); + }); + + // Embed View + test('should automatically switch to card view and set a custom title and description', async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'title0'); + + const { id } = await addNewPage(page); + await switchToPage(page, id); + await focusTitle(page); + await type(page, 'title1'); + + await getDebugMenu(page).pagesBtn.click(); + + await focusRichText(page); + await type(page, '@title0'); + await pressEnter(page); + + const { findRefNode } = getLinkedDocPopover(page); + const page0 = await findRefNode('title0'); + await page0.hover(); + + await waitNextFrame(page, 200); + const referencePopup = page.locator('.affine-reference-popover-container'); + + let editButton = referencePopup.getByRole('button', { name: 'Edit' }); + await editButton.click(); + + // title alias + await type(page, 'page0-title0'); + await pressEnter(page); + + await focusRichText(page); + await waitNextFrame(page, 200); + + const page0Alias = await findRefNode('page0-title0'); + await page0Alias.hover(); + + await waitNextFrame(page, 200); + let switchButton = referencePopup.getByRole('button', { + name: 'Switch view', + }); + await switchButton.click(); + + // switches to card view + const toCardButton = referencePopup.getByRole('button', { + name: 'Card view', + }); + await toCardButton.click(); + + await waitNextFrame(page, 200); + const linkedDocBlock = page.locator('affine-embed-linked-doc-block'); + + await linkedDocBlock.click(); + + await waitNextFrame(page, 200); + const cardToolbar = page.locator('affine-embed-card-toolbar'); + switchButton = cardToolbar.getByRole('button', { name: 'Switch view' }); + await switchButton.click(); + + await waitNextFrame(page, 200); + + // switches to embed view + const toEmbedButton = cardToolbar.getByRole('button', { + name: 'Embed view', + }); + await toEmbedButton.click(); + + await waitNextFrame(page, 200); + const syncedDocBlock = page.locator('affine-embed-synced-doc-block'); + + await syncedDocBlock.click(); + + await waitNextFrame(page, 200); + const syncedDocBlockTitle = syncedDocBlock.locator( + '.affine-embed-synced-doc-title' + ); + await expect(syncedDocBlockTitle).toHaveText('title0'); + + await syncedDocBlock.click(); + + await waitNextFrame(page, 200); + editButton = cardToolbar.getByRole('button', { name: 'Edit' }); + await editButton.click(); + + await waitNextFrame(page, 200); + const editModal = page.locator('embed-card-edit-modal'); + const cancelButton = editModal.getByRole('button', { name: 'Cancel' }); + const saveButton = editModal.getByRole('button', { name: 'Save' }); + + // closes edit-model + await cancelButton.click(); + + await waitNextFrame(page, 200); + await expect(editModal).not.toBeVisible(); + + await syncedDocBlock.click(); + + await waitNextFrame(page, 200); + await editButton.click(); + + await waitNextFrame(page, 200); + + // title alias + await type(page, 'page0-title0'); + await page.keyboard.press('Tab'); + // description alias + await type(page, 'This is a new description'); + + // saves aliases + await saveButton.click(); + + await waitNextFrame(page, 200); + + // automatically switch to card view + await expect(syncedDocBlock).not.toBeVisible(); + + await expect(linkedDocBlock).toBeVisible(); + const linkedDocBlockTitle = linkedDocBlock.locator( + '.affine-embed-linked-doc-content-title-text' + ); + await expect(linkedDocBlockTitle).toHaveText('page0-title0'); + await expect( + linkedDocBlock.locator('.affine-embed-linked-doc-content-note.alias') + ).toHaveText('This is a new description'); + }); + + // Embed View + test.fixme( + 'should automatically switch to card view and set a custom title and description on edgeless', + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await focusRichText(page); + await createAndConvertToEmbedLinkedDoc(page); + + await switchEditorMode(page); + await page.mouse.dblclick(450, 450); + + await dragBlockToPoint(page, '9', { x: 200, y: 200 }); + + await waitNextFrame(page); + + const toolbar = page.locator('editor-toolbar'); + await toolbar.getByRole('button', { name: 'Switch view' }).click(); + await toolbar.getByRole('button', { name: 'Embed view' }).click(); + + await waitNextFrame(page); + + await toolbar.getByRole('button', { name: 'Edit' }).click(); + + await waitNextFrame(page); + const editModal = page.locator('embed-card-edit-modal'); + const saveButton = editModal.getByRole('button', { name: 'Save' }); + + // title alias + await type(page, 'page0-title0'); + await page.keyboard.press('Tab'); + // description alias + await type(page, 'This is a new description'); + + // saves aliases + await saveButton.click(); + + await waitNextFrame(page); + + const syncedDocBlock = page.locator( + 'affine-embed-edgeless-synced-doc-block' + ); + + await expect(syncedDocBlock).toBeHidden(); + + const linkedDocBlock = page.locator( + 'affine-embed-edgeless-linked-doc-block' + ); + + await expect(linkedDocBlock).toBeVisible(); + + const linkedDocBlockTitle = linkedDocBlock.locator( + '.affine-embed-linked-doc-content-title-text' + ); + await expect(linkedDocBlockTitle).toHaveText('page0-title0'); + await expect( + linkedDocBlock.locator('.affine-embed-linked-doc-content-note.alias') + ).toHaveText('This is a new description'); + } + ); +}); diff --git a/blocksuite/tests-legacy/list.spec.ts b/blocksuite/tests-legacy/list.spec.ts new file mode 100644 index 0000000000000..3a8c3e85ea66e --- /dev/null +++ b/blocksuite/tests-legacy/list.spec.ts @@ -0,0 +1,842 @@ +import { expect, type Locator } from '@playwright/test'; +import { getFormatBar } from 'utils/query.js'; + +import { + dragBetweenIndices, + enterPlaygroundRoom, + enterPlaygroundWithList, + focusRichText, + getPageSnapshot, + initEmptyEdgelessState, + initEmptyParagraphState, + initThreeLists, + pressArrowLeft, + pressArrowUp, + pressBackspace, + pressBackspaceWithShortKey, + pressEnter, + pressShiftEnter, + pressShiftTab, + pressSpace, + pressTab, + redoByClick, + switchEditorMode, + switchReadonly, + type, + undoByClick, + undoByKeyboard, + updateBlockType, + waitNextFrame, +} from './utils/actions/index.js'; +import { + assertBlockChildrenFlavours, + assertBlockChildrenIds, + assertBlockCount, + assertBlockType, + assertListPrefix, + assertRichTextInlineRange, + assertRichTexts, + assertTextContent, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +async function isToggleIconVisible(toggleIcon: Locator) { + const connected = await toggleIcon.isVisible(); + if (!connected) return false; + const element = await toggleIcon.elementHandle(); + if (!element) return false; + const opacity = await element.evaluate(node => { + // https://stackoverflow.com/questions/11365296/how-do-i-get-the-opacity-of-an-element-using-javascript + return window.getComputedStyle(node).getPropertyValue('opacity'); + }); + if (!opacity || typeof opacity !== 'string') { + throw new Error('opacity is not a string'); + } + const isVisible = opacity !== '0'; + return isVisible; +} + +test('add new bulleted list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page, 0); + await updateBlockType(page, 'affine:list', 'bulleted'); + await focusRichText(page, 0); + await type(page, 'aa'); + await pressEnter(page); + await type(page, 'aa'); + await pressEnter(page); + + await assertRichTexts(page, ['aa', 'aa', '']); + await assertBlockCount(page, 'list', 3); +}); + +test('add new todo list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page, 0); + await updateBlockType(page, 'affine:list', 'todo'); + await focusRichText(page, 0); + + await type(page, 'aa'); + await assertRichTexts(page, ['aa']); + + const checkBox = page.locator('.affine-list-block__prefix'); + await expect(page.locator('.affine-list--checked')).toHaveCount(0); + await checkBox.click(); + await expect(page.locator('.affine-list--checked')).toHaveCount(1); + await checkBox.click(); + await expect(page.locator('.affine-list--checked')).toHaveCount(0); +}); + +test('add new toggle list', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page, 0); + await updateBlockType(page, 'affine:list', 'toggle'); + await focusRichText(page, 0); + await type(page, 'top'); + await pressTab(page); + await pressEnter(page); + await type(page, 'kid 1'); + await pressEnter(page); + + await assertRichTexts(page, ['top', 'kid 1', '']); + await assertBlockCount(page, 'list', 3); +}); + +test('convert nested paragraph to list', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'aaa\nbbb'); + await pressTab(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await dragBetweenIndices(page, [0, 1], [1, 2]); + const { openParagraphMenu, bulletedBtn } = getFormatBar(page); + await openParagraphMenu(); + await bulletedBtn.click(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test('convert to numbered list block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await focusRichText(page, 0); // created 0, 1, 2 + await updateBlockType(page, 'affine:list', 'bulleted'); // replaced 2 to 3 + await waitNextFrame(page); + await updateBlockType(page, 'affine:list', 'numbered'); + await focusRichText(page, 0); + + const listSelector = '.affine-list-rich-text-wrapper'; + const bulletIconSelector = `${listSelector} > div`; + await assertTextContent(page, bulletIconSelector, /1\./); + + await undoByClick(page); + // const numberIconSelector = `${listSelector} > svg`; + // await expect(page.locator(numberIconSelector)).toHaveCount(1); + + await redoByClick(page); + await focusRichText(page, 0); + await type(page, 'aa'); + await pressEnter(page); // created 4 + await assertBlockType(page, '4', 'numbered'); + + await type(page, 'aa'); + await pressEnter(page); // created 5 + await assertBlockType(page, '5', 'numbered'); + + await page.keyboard.press('Tab'); + await assertBlockType(page, '5', 'numbered'); +}); + +test('indent list block', async ({ page }) => { + await enterPlaygroundWithList(page); // 0(1(2,3,4)) + + await focusRichText(page, 1); + await type(page, 'hello'); + await assertRichTexts(page, ['', 'hello', '']); + + await page.keyboard.press('Tab'); // 0(1(2(3)4)) + await assertRichTexts(page, ['', 'hello', '']); + await assertBlockChildrenIds(page, '1', ['2', '4']); + await assertBlockChildrenIds(page, '2', ['3']); + + await undoByKeyboard(page); // 0(1(2,3,4)) + await assertBlockChildrenIds(page, '1', ['2', '3', '4']); +}); + +test('unindent list block', async ({ page }) => { + await enterPlaygroundWithList(page); // 0(1(2,3,4)) + + await focusRichText(page, 1); + await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3)4)) + + await assertBlockChildrenIds(page, '1', ['2', '4']); + await assertBlockChildrenIds(page, '2', ['3']); + + await pressShiftTab(page); // 0(1(2,3,4)) + await assertBlockChildrenIds(page, '1', ['2', '3', '4']); + + await pressShiftTab(page); + await assertBlockChildrenIds(page, '1', ['2', '3', '4']); +}); + +test('remove all indent for a list block', async ({ page }) => { + await enterPlaygroundWithList(page); // 0(1(2,3,4)) + + await focusRichText(page, 1); + await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3)4)) + await focusRichText(page, 2); + await page.keyboard.press('Tab', { delay: 50 }); + await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3(4)))) + await assertBlockChildrenIds(page, '3', ['4']); + await pressBackspaceWithShortKey(page); // 0(1(2(3)4)) + await assertBlockChildrenIds(page, '1', ['2', '4']); + await assertBlockChildrenIds(page, '2', ['3']); +}); + +test('insert new list block by enter', async ({ page }) => { + await enterPlaygroundWithList(page); + await assertRichTexts(page, ['', '', '']); + + await focusRichText(page, 1); + await type(page, 'hello'); + await assertRichTexts(page, ['', 'hello', '']); + + await pressEnter(page); + await type(page, 'world'); + await assertRichTexts(page, ['', 'hello', 'world', '']); + await assertBlockChildrenFlavours(page, '1', [ + 'affine:list', + 'affine:list', + 'affine:list', + 'affine:list', + ]); +}); + +test('delete at start of list block', async ({ page }) => { + await enterPlaygroundWithList(page); + await focusRichText(page, 1); + await page.keyboard.press('Backspace'); + await assertBlockChildrenFlavours(page, '1', [ + 'affine:list', + 'affine:paragraph', + 'affine:list', + ]); + await waitNextFrame(page, 200); + await assertRichTextInlineRange(page, 1, 0, 0); + + await undoByClick(page); + await assertBlockChildrenFlavours(page, '1', [ + 'affine:list', + 'affine:list', + 'affine:list', + ]); + await waitNextFrame(page); + //FIXME: it just failed in playwright + // await assertSelection(page, 1, 0, 0); +}); + +test('nested list blocks', async ({ page }, testInfo) => { + await enterPlaygroundWithList(page); + + await focusRichText(page, 0); + await type(page, '123'); + + await focusRichText(page, 1); + await pressTab(page); + await type(page, '456'); + + await focusRichText(page, 2); + await pressTab(page); + await pressTab(page); + await type(page, '789'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await focusRichText(page, 1); + await pressShiftTab(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); +}); + +test('update numbered list block prefix', async ({ page }) => { + await enterPlaygroundWithList(page, ['', '', ''], 'numbered'); // 0(1(2,3,4)) + + await focusRichText(page, 1); + await type(page, 'lunatic'); + await assertRichTexts(page, ['', 'lunatic', '']); + await assertListPrefix(page, ['1', '2', '3']); + + await page.keyboard.press('Tab'); + await assertListPrefix(page, ['1', 'a', '2']); + + await page.keyboard.press('Shift+Tab'); + await assertListPrefix(page, ['1', '2', '3']); + + await waitNextFrame(page, 200); + await page.keyboard.press('Enter'); + await assertListPrefix(page, ['1', '2', '3', '4']); + + await waitNextFrame(page, 200); + await type(page, 'concorde'); + await assertRichTexts(page, ['', 'lunatic', 'concorde', '']); + + await page.keyboard.press('Tab'); + await assertListPrefix(page, ['1', '2', 'a', '3']); +}); + +test('basic indent and unindent', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'text1'); + await pressEnter(page); + await type(page, 'text2'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await page.keyboard.press('Tab'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after_tab.json` + ); + + await page.waitForTimeout(100); + await pressShiftTab(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after_shift_tab.json` + ); +}); + +test('should indent todo block preserve todo status', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'text1'); + await pressEnter(page); + + await type(page, '[x]'); + await pressSpace(page); + + await type(page, 'todo item'); + await pressTab(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + await pressShiftTab(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test('enter list block with empty text', async ({ page }, testInfo) => { + await enterPlaygroundWithList(page); // 0(1(2,3,4)) + + /** + * - + * - + * - + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await focusRichText(page, 1); + await pressTab(page); + await focusRichText(page, 2); + await pressTab(page); + + /** + * - + * - + * -| + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_1.json` + ); + + await pressEnter(page); + + /** + * - + * - + * -| + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_2.json` + ); + + await pressEnter(page); + + /** + * - + * - + * | + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_3.json` + ); + + await undoByClick(page); + await undoByClick(page); + + /** + * - + * - + * -| + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_1.json` + ); + + /** + * - + * -| + * - + */ + await focusRichText(page, 1); + await waitNextFrame(page); + await pressEnter(page); + await waitNextFrame(page); + + /** + * - + * -| + * - + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_4.json` + ); + + await undoByClick(page); + + /** + * - + * - + * -| + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_1.json` + ); + + /** + * -| + * - + * - + */ + await focusRichText(page, 0); + await waitNextFrame(page); + await pressEnter(page); + await waitNextFrame(page); + + /** + * | + * - + * - + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_5.json` + ); + + await undoByClick(page); + + /** + * - + * - + * -| + */ + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_1.json` + ); +}); + +test('enter list block with non-empty text', async ({ page }) => { + await enterPlaygroundWithList(page); // 0(1(2,3,4)) + + await focusRichText(page, 0); + await type(page, 'aa'); + await focusRichText(page, 1); + await type(page, 'bb'); + await pressTab(page); + await focusRichText(page, 2); + await type(page, 'cc'); + await pressTab(page); + await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4))) + + await focusRichText(page, 1); + await pressEnter(page); + await assertBlockChildrenIds(page, '2', ['3', '5', '4']); + await undoByClick(page); + await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4))) + + await focusRichText(page, 0); + await pressEnter(page); + await assertBlockChildrenIds(page, '2', ['6', '3', '4']); // 0(1(2,(6,3,4))) + await waitNextFrame(page); + await undoByClick(page); + await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4))) +}); + +test.describe('indent correctly when deleting list item', () => { + test('delete the child item in the middle position', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page, 0); + + await type(page, '- a'); + await pressEnter(page); + await pressTab(page); + await type(page, 'b'); + await pressEnter(page); + await type(page, 'c'); + await pressEnter(page); + await type(page, 'd'); + await pressArrowUp(page); + await pressArrowLeft(page); + await pressBackspace(page); + await pressBackspace(page); + + await assertBlockChildrenIds(page, '3', ['4', '6']); + await assertRichTexts(page, ['a', 'bc', 'd']); + await assertRichTextInlineRange(page, 1, 1); + }); + + test('merge two lists', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page, 0); + + await type(page, '- a'); + await pressEnter(page); + await pressTab(page); + await type(page, 'b'); + await pressEnter(page); + await pressTab(page); + await type(page, 'c'); + await pressEnter(page); + await pressBackspace(page, 3); + await assertRichTexts(page, ['a', 'b', 'c', '']); + + await waitNextFrame(page); + await pressEnter(page); + await type(page, '- d'); + await pressEnter(page); + await pressTab(page); + await type(page, 'e'); + await pressEnter(page); + await pressTab(page); + await type(page, 'f'); + await pressArrowUp(page, 3); + await pressBackspace(page, 2); + + await waitNextFrame(page, 200); + await assertRichTexts(page, ['a', 'b', '', 'd', 'e', 'f']); + await assertBlockChildrenIds(page, '1', ['3', '9']); + await assertBlockChildrenIds(page, '3', ['4']); + await assertBlockChildrenIds(page, '4', ['5']); + await assertBlockChildrenIds(page, '10', ['11']); + }); +}); + +test('delete list item with nested children items', async ({ page }) => { + await enterPlaygroundWithList(page); + + await focusRichText(page, 0); + await type(page, '1'); + + await focusRichText(page, 1); + await pressTab(page); + await type(page, '2'); + + await focusRichText(page, 2); + await pressTab(page); + await pressTab(page); + await type(page, '3'); + + await pressEnter(page); + await type(page, '4'); + + await focusRichText(page, 1); + await pressArrowLeft(page); + // 1 + // |2 + // 3 + // 4 + + await pressBackspace(page); + await waitNextFrame(page); + // 1 + // |2 (transformed to paragraph) + // 3 + // 4 + + await pressBackspace(page); + await waitNextFrame(page); + // 1 + // |2 + // 3 + // 4 + + await pressBackspace(page); + await waitNextFrame(page); + // 1|2 + // 3 + // 4 + + await assertRichTextInlineRange(page, 0, 1); + await assertRichTexts(page, ['12', '3', '4']); + await assertBlockChildrenIds(page, '1', ['2', '4', '5']); +}); + +test('add number prefix to a todo item should not forcefully change it into numbered list, vice versa', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page, 0); + await type(page, '1. numberList'); + await assertListPrefix(page, ['1']); + await focusRichText(page, 0, { clickPosition: { x: 0, y: 0 } }); + await type(page, '[] '); + await assertListPrefix(page, ['1']); + await pressBackspace(page, 14); + await type(page, '[] todoList'); + await assertListPrefix(page, ['']); + await focusRichText(page, 0, { clickPosition: { x: 0, y: 0 } }); + await type(page, '1. '); + await assertListPrefix(page, ['']); +}); + +test('should not convert to a list when pressing space at the second line', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + await pressShiftEnter(page); + await type(page, '-'); + await pressSpace(page); + await type(page, 'bbb'); + await assertRichTexts(page, ['aaa\n- bbb']); +}); + +test.describe('toggle list', () => { + test('click toggle icon should collapsed list', async ({ + page, + }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeLists(page); + const toggleIcon = page.locator('.toggle-icon'); + const prefixes = page.locator('.affine-list-block__prefix'); + const listChildren = page + .locator('[data-block-id="4"] .affine-block-children-container') + .nth(0); + const parentPrefix = prefixes.nth(1); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await parentPrefix.hover(); + await waitNextFrame(page); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + + await expect(listChildren).toBeVisible(); + await toggleIcon.click(); + await expect(listChildren).not.toBeVisible(); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_toggle.json` + ); + + // Collapsed toggle icon should be show always + await page.mouse.move(0, 0); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + + await expect(listChildren).not.toBeVisible(); + await toggleIcon.click(); + await expect(listChildren).toBeVisible(); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await page.mouse.move(0, 0); + await waitNextFrame(page, 200); + expect(await isToggleIconVisible(toggleIcon)).toBe(false); + }); + + test('indent item should expand toggle', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeLists(page); + await focusRichText(page, 2); + await pressEnter(page); + await pressEnter(page); + await type(page, '012'); + + const toggleIcon = page.locator('.toggle-icon'); + const listChildren = page + .locator('[data-block-id="4"] .affine-block-children-container') + .nth(0); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await expect(listChildren).toBeVisible(); + await toggleIcon.click(); + await expect(listChildren).not.toBeVisible(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_toggle.json` + ); + + await focusRichText(page, 3); + await pressTab(page); + await waitNextFrame(page, 200); + await expect(listChildren).not.toBeVisible(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_finial.json` + ); + }); + + test('toggle icon should be show when hover', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeLists(page); + const toggleIcon = page.locator('.toggle-icon'); + + const prefixes = page.locator('.affine-list-block__prefix'); + const parentPrefix = prefixes.nth(1); + + expect(await isToggleIconVisible(toggleIcon)).toBe(false); + await parentPrefix.hover(); + await waitNextFrame(page, 200); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + + await page.mouse.move(0, 0); + await waitNextFrame(page, 300); + expect(await isToggleIconVisible(toggleIcon)).toBe(false); + }); +}); + +test.describe('readonly', () => { + test('can expand toggle in readonly mode', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeLists(page); + const toggleIcon = page.locator('.toggle-icon'); + const prefixes = page.locator('.affine-list-block__prefix'); + const listChildren = page + .locator('[data-block-id="4"] .affine-block-children-container') + .nth(0); + const parentPrefix = prefixes.nth(1); + + await parentPrefix.hover(); + await waitNextFrame(page, 200); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + + await expect(listChildren).toBeVisible(); + await toggleIcon.click(); + await expect(listChildren).not.toBeVisible(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_before_readonly.json` + ); + + await waitNextFrame(page, 200); + await switchReadonly(page); + + await waitNextFrame(page, 200); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + + await expect(listChildren).not.toBeVisible(); + await toggleIcon.click(); + await expect(listChildren).toBeVisible(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_before_readonly.json` + ); + + await toggleIcon.click(); + await expect(listChildren).not.toBeVisible(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_before_readonly.json` + ); + }); + + test('can not modify todo list in readonly mode', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const checkBox = page.locator('.affine-list-block__prefix'); + + { + await type(page, '[] todo'); + await switchReadonly(page); + await expect(page.locator('.affine-list--checked')).toHaveCount(0); + await checkBox.click(); + await expect(page.locator('.affine-list--checked')).toHaveCount(0); + } + + { + await switchReadonly(page, false); + await checkBox.click(); + await switchReadonly(page); + await expect(page.locator('.affine-list--checked')).toHaveCount(1); + await checkBox.click(); + await expect(page.locator('.affine-list--checked')).toHaveCount(1); + } + }); + + test('should render collapsed list correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + // await switchEditorMode(page); + await initThreeLists(page); + + const toggleIcon = page.locator('.toggle-icon'); + const listChildren = page + .locator('[data-block-id="5"] .affine-block-children-container') + .nth(0); + + await expect(listChildren).toBeVisible(); + await toggleIcon.click(); + await expect(listChildren).not.toBeVisible(); + + await switchReadonly(page); + // trick for render a readonly doc from scratch + await switchEditorMode(page); + await switchEditorMode(page); + + await expect(listChildren).not.toBeVisible(); + }); +}); diff --git a/blocksuite/tests-legacy/markdown.spec.ts b/blocksuite/tests-legacy/markdown.spec.ts new file mode 100644 index 0000000000000..19989a5f29bb5 --- /dev/null +++ b/blocksuite/tests-legacy/markdown.spec.ts @@ -0,0 +1,535 @@ +import { + enterPlaygroundRoom, + focusRichText, + getCursorBlockIdAndHeight, + initEmptyParagraphState, + pressArrowLeft, + pressBackspace, + pressEnter, + pressSpace, + redoByKeyboard, + resetHistory, + type, + undoByClick, + undoByKeyboard, + waitNextFrame, +} from './utils/actions/index.js'; +import { + assertBlockType, + assertRichTextInlineDeltas, + assertRichTextInlineRange, + assertRichTexts, + assertText, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +test('markdown shortcut', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await resetHistory(page); + + let id: string | null = null; + + await waitNextFrame(page); + await type(page, '[] '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'todo'); + await undoByClick(page); + await assertText(page, '[] '); + await undoByClick(page); + //FIXME: it just failed in playwright + await focusRichText(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '[ ] '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'todo'); + await undoByClick(page); + await assertText(page, '[ ] '); + await undoByClick(page); + //FIXME: it just failed in playwright + await focusRichText(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '[x] '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'todo'); + await undoByClick(page); + await assertText(page, '[x] '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '* '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'bulleted'); + await undoByClick(page); + await assertText(page, '* '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '- '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'bulleted'); + await undoByClick(page); + await assertText(page, '- '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '1. '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'numbered'); + await undoByClick(page); + await assertText(page, '1. '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '20. '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'numbered'); + await undoByClick(page); + await assertText(page, '20. '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '# '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'h1'); + await undoByClick(page); + await assertText(page, '# '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '## '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'h2'); + await undoByClick(page); + await assertText(page, '## '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '### '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'h3'); + await undoByClick(page); + await assertText(page, '### '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '#### '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'h4'); + await undoByClick(page); + await assertText(page, '#### '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '##### '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'h5'); + await undoByClick(page); + await assertText(page, '##### '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '###### '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'h6'); + await undoByClick(page); + await assertText(page, '###### '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '> '); + await waitNextFrame(page); + [id] = await getCursorBlockIdAndHeight(page); + await assertBlockType(page, id, 'quote'); + await undoByClick(page); + await assertText(page, '> '); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '--- '); + await undoByClick(page); + await assertRichTexts(page, ['--- ']); + await undoByClick(page); + await assertRichTexts(page, ['']); + await waitNextFrame(page); + await type(page, '*** '); + await undoByClick(page); + await assertRichTexts(page, ['*** ']); + await undoByClick(page); + await assertRichTexts(page, ['']); +}); + +test.describe('markdown inline-text', () => { + test.beforeEach(async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await resetHistory(page); + }); + + test('bolditalic', async ({ page }) => { + await type(page, 'aa***bb*** '); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bb', + attributes: { + bold: true, + italic: true, + }, + }, + ]); + await undoByKeyboard(page); + await assertRichTextInlineRange(page, 0, 11); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa***bb*** ', + }, + ]); + await redoByKeyboard(page); + await type(page, 'cc'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bbcc', + attributes: { + bold: true, + italic: true, + }, + }, + ]); + await undoByKeyboard(page); + await undoByKeyboard(page); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '***test *** '); + await assertRichTexts(page, ['***test *** ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + // *** + space will be converted to divider, so needn't test this case here + // await waitNextFrame(page); + // await type(page, '*** test*** '); + // await assertRichTexts(page, ['*** test*** ']); + // await undoByKeyboard(page); + // await assertRichTexts(page, ['']); + }); + + test('bold', async ({ page }) => { + await type(page, 'aa**bb** '); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bb', + attributes: { + bold: true, + }, + }, + ]); + await undoByKeyboard(page); + await assertRichTextInlineRange(page, 0, 9); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa**bb** ', + }, + ]); + await redoByKeyboard(page); + await type(page, 'cc'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bbcc', + attributes: { + bold: true, + }, + }, + ]); + await undoByKeyboard(page); + await undoByKeyboard(page); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '**test ** '); + await assertRichTexts(page, ['**test ** ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '** test** '); + await assertRichTexts(page, ['** test** ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + }); + + test('italic', async ({ page }) => { + await type(page, 'aa*bb* '); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bb', + attributes: { + italic: true, + }, + }, + ]); + await undoByKeyboard(page); + await assertRichTextInlineRange(page, 0, 7); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa*bb* ', + }, + ]); + await redoByKeyboard(page); + await type(page, 'cc'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bbcc', + attributes: { + italic: true, + }, + }, + ]); + await undoByKeyboard(page); + await undoByKeyboard(page); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '*test * '); + await assertRichTexts(page, ['*test * ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + // * + space will be converted to bulleted list, so needn't test this case here + // await waitNextFrame(page); + // await type(page, '* test* '); + // await assertRichTexts(page, ['* test* ']); + // await undoByKeyboard(page); + // await assertRichTexts(page, ['']); + }); + + test('strike', async ({ page }) => { + await type(page, 'aa~~bb~~ '); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bb', + attributes: { + strike: true, + }, + }, + ]); + await undoByKeyboard(page); + await assertRichTextInlineRange(page, 0, 9); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa~~bb~~ ', + }, + ]); + await redoByKeyboard(page); + await type(page, 'cc'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bbcc', + attributes: { + strike: true, + }, + }, + ]); + await undoByKeyboard(page); + await undoByKeyboard(page); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '~~test ~~ '); + await assertRichTexts(page, ['~~test ~~ ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '~~ test~~ '); + await assertRichTexts(page, ['~~ test~~ ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + }); + + test('underline', async ({ page }) => { + await type(page, 'aa~bb~ '); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bb', + attributes: { + underline: true, + }, + }, + ]); + await undoByKeyboard(page); + await assertRichTextInlineRange(page, 0, 7); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa~bb~ ', + }, + ]); + await redoByKeyboard(page); + await type(page, 'cc'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bbcc', + attributes: { + underline: true, + }, + }, + ]); + await undoByKeyboard(page); + await undoByKeyboard(page); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '~test ~ '); + await assertRichTexts(page, ['~test ~ ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '~ test~ '); + await assertRichTexts(page, ['~ test~ ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + }); + + test('code', async ({ page }) => { + await type(page, 'aa`bb` '); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bb', + attributes: { + code: true, + }, + }, + ]); + await undoByKeyboard(page); + await assertRichTextInlineRange(page, 0, 7); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa`bb` ', + }, + ]); + await redoByKeyboard(page); + await type(page, 'cc'); + await assertRichTextInlineDeltas(page, [ + { + insert: 'aa', + }, + { + insert: 'bb', + attributes: { + code: true, + }, + }, + { + insert: 'cc', + }, + ]); + await undoByKeyboard(page); + await undoByKeyboard(page); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '`test ` '); + await assertRichTexts(page, ['`test ` ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '` test` '); + await assertRichTexts(page, ['` test` ']); + await undoByKeyboard(page); + await assertRichTexts(page, ['']); + }); +}); + +test('inline code should work when pressing Enter followed by Backspace twice', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '`test`'); + await pressSpace(page); + await waitNextFrame(page); + await pressArrowLeft(page); + await waitNextFrame(page); + await pressEnter(page); + await waitNextFrame(page); + await pressBackspace(page); + await waitNextFrame(page); + await pressEnter(page); + await waitNextFrame(page); + await pressBackspace(page); + + await assertRichTexts(page, ['test']); +}); diff --git a/blocksuite/tests-legacy/multiple-editors/edgeless.spec.ts b/blocksuite/tests-legacy/multiple-editors/edgeless.spec.ts new file mode 100644 index 0000000000000..fa3d88fb02b46 --- /dev/null +++ b/blocksuite/tests-legacy/multiple-editors/edgeless.spec.ts @@ -0,0 +1,43 @@ +import { expect } from '@playwright/test'; + +import { + switchMultipleEditorsMode, + toggleMultipleEditors, +} from '../utils/actions/edgeless.js'; +import { + enterPlaygroundRoom, + initEmptyEdgelessState, + initThreeParagraphs, + waitNextFrame, +} from '../utils/actions/misc.js'; +import { test } from '../utils/playwright.js'; + +test('the shift pressing status should effect all editors', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await initThreeParagraphs(page); + await toggleMultipleEditors(page); + await switchMultipleEditorsMode(page); + + await waitNextFrame(page, 5000); + + const getShiftPressedStatus = async () => { + return page.evaluate(() => { + const edgelessBlocks = document.querySelectorAll('affine-edgeless-root'); + + return Array.from(edgelessBlocks).map(edgelessRoot => { + return edgelessRoot.gfx.keyboard.shiftKey$.peek(); + }); + }); + }; + + await page.keyboard.down('Shift'); + const pressed = await getShiftPressedStatus(); + expect(pressed).toEqual([true, true]); + + await page.keyboard.up('Shift'); + const released = await getShiftPressedStatus(); + expect(released).toEqual([false, false]); +}); diff --git a/blocksuite/tests-legacy/multiple-editors/selection.spec.ts b/blocksuite/tests-legacy/multiple-editors/selection.spec.ts new file mode 100644 index 0000000000000..7b42798ab880e --- /dev/null +++ b/blocksuite/tests-legacy/multiple-editors/selection.spec.ts @@ -0,0 +1,33 @@ +import { expect } from '@playwright/test'; + +import { dragBetweenCoords } from '../utils/actions/drag.js'; +import { toggleMultipleEditors } from '../utils/actions/edgeless.js'; +import { + enterPlaygroundRoom, + initEmptyParagraphState, + initThreeParagraphs, +} from '../utils/actions/misc.js'; +import { getRichTextBoundingBox } from '../utils/actions/selection.js'; +import { test } from '../utils/playwright.js'; + +test('should only show one format bar when multiple editors are toggled', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await toggleMultipleEditors(page); + + // Select some text + const box123 = await getRichTextBoundingBox(page, '2'); + const above123 = { x: box123.left + 10, y: box123.top + 2 }; + + const box789 = await getRichTextBoundingBox(page, '4'); + const bottomRight789 = { x: box789.right - 10, y: box789.bottom - 2 }; + + await dragBetweenCoords(page, above123, bottomRight789, { steps: 10 }); + + // should only show one format bar + const formatBar = page.locator('.affine-format-bar-widget'); + await expect(formatBar).toHaveCount(1); +}); diff --git a/blocksuite/tests-legacy/package.json b/blocksuite/tests-legacy/package.json new file mode 100644 index 0000000000000..be6c998777920 --- /dev/null +++ b/blocksuite/tests-legacy/package.json @@ -0,0 +1,20 @@ +{ + "name": "@blocksuite/legacy-e2e", + "private": true, + "type": "module", + "main": "index.js", + "scripts": { + "test": "yarn playwright test" + }, + "dependencies": { + "@blocksuite/affine-model": "workspace:*", + "@blocksuite/block-std": "workspace:*", + "@blocksuite/global": "workspace:*", + "@blocksuite/presets": "workspace:*", + "@playwright/test": "=1.49.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/toeverything/blocksuite.git" + } +} diff --git a/blocksuite/tests-legacy/paragraph.spec.ts b/blocksuite/tests-legacy/paragraph.spec.ts new file mode 100644 index 0000000000000..9c8a373170e65 --- /dev/null +++ b/blocksuite/tests-legacy/paragraph.spec.ts @@ -0,0 +1,1334 @@ +import type { DeltaInsert } from '@inline/types.js'; +import { expect } from '@playwright/test'; + +import { + captureHistory, + dragBetweenIndices, + dragOverTitle, + enterPlaygroundRoom, + focusRichText, + focusTitle, + getIndexCoordinate, + getPageSnapshot, + initEmptyEdgelessState, + initEmptyParagraphState, + initThreeDividers, + initThreeParagraphs, + pressArrowDown, + pressArrowLeft, + pressArrowRight, + pressArrowUp, + pressBackspace, + pressBackspaceWithShortKey, + pressEnter, + pressForwardDelete, + pressShiftEnter, + pressShiftTab, + pressSpace, + pressTab, + redoByClick, + redoByKeyboard, + resetHistory, + setSelection, + SHORT_KEY, + switchReadonly, + type, + undoByClick, + undoByKeyboard, + updateBlockType, + waitDefaultPageLoaded, + waitNextFrame, +} from './utils/actions/index.js'; +import { + assertBlockChildrenFlavours, + assertBlockChildrenIds, + assertBlockCount, + assertBlockSelections, + assertBlockType, + assertClassName, + assertDivider, + assertDocTitleFocus, + assertRichTextInlineRange, + assertRichTexts, + assertStoreMatchJSX, + assertTitle, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +test('init paragraph by page title enter at last', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await waitDefaultPageLoaded(page); + await focusTitle(page); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + + await assertTitle(page, 'hello'); + await assertRichTexts(page, ['world', '']); + + //#region Fixes: https://github.com/toeverything/blocksuite/issues/1007 + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/1007', + }); + await page.keyboard.press('ArrowLeft'); + await focusTitle(page); + await pressEnter(page); + await assertRichTexts(page, ['', 'world', '']); + //#endregion +}); + +test('init paragraph by page title enter in middle', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await waitDefaultPageLoaded(page); + await focusTitle(page); + await type(page, 'hello'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await pressEnter(page); + + await assertTitle(page, 'he'); + await assertRichTexts(page, ['llo', '']); +}); + +test('drag over paragraph title', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await waitDefaultPageLoaded(page); + await focusTitle(page); + await type(page, 'hello'); + await assertTitle(page, 'hello'); + await resetHistory(page); + + await dragOverTitle(page); + await page.keyboard.press('Backspace', { delay: 100 }); + await assertTitle(page, ''); + + await undoByKeyboard(page); + await assertTitle(page, 'hello'); +}); + +test('backspace and arrow on title', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await waitDefaultPageLoaded(page); + await focusTitle(page); + await type(page, 'hello'); + await assertTitle(page, 'hello'); + await resetHistory(page); + + await pressBackspace(page); + await assertTitle(page, 'hell'); + + await pressArrowLeft(page, 2); + await captureHistory(page); + await pressBackspace(page); + await assertTitle(page, 'hll'); + + await pressArrowDown(page); + await assertRichTextInlineRange(page, 0, 0, 0); + + await undoByKeyboard(page); + await assertTitle(page, 'hell'); + + await redoByClick(page); + await assertTitle(page, 'hll'); +}); + +for (const { initState, desc } of [ + { + initState: initEmptyParagraphState, + desc: 'without surface', + }, + { + initState: initEmptyEdgelessState, + desc: 'with surface', + }, +]) { + test(`backspace on line start of the first block (${desc})`, async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initState(page); + await waitDefaultPageLoaded(page); + await focusTitle(page); + await type(page, 'hello'); + await assertTitle(page, 'hello'); + await resetHistory(page); + + await focusRichText(page, 0); + await type(page, 'abc'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.waitForTimeout(300); + await assertRichTextInlineRange(page, 0, 0, 0); + + await pressBackspace(page); + await assertTitle(page, 'helloabc'); + + await pressEnter(page); + await assertTitle(page, 'hello'); + await assertRichTexts(page, ['abc', '']); + + await pressBackspace(page); + await assertTitle(page, 'helloabc'); + await assertRichTexts(page, ['']); + await undoByClick(page); + await assertTitle(page, 'hello'); + await assertRichTexts(page, ['abc', '']); + + await redoByClick(page); + await assertTitle(page, 'helloabc'); + await assertRichTexts(page, ['']); + }); + + test(`backspace on line start of the first empty block (${desc})`, async ({ + page, + }) => { + await enterPlaygroundRoom(page); + await initState(page); + await focusTitle(page); + + await pressArrowDown(page); + await pressBackspace(page); + await assertBlockCount(page, 'paragraph', 1); + + await pressArrowDown(page); + await assertRichTextInlineRange(page, 0, 0, 0); + }); +} + +test('append new paragraph block by enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await assertRichTextInlineRange(page, 0, 5, 0); + + await pressEnter(page); + await assertRichTexts(page, ['hello', '']); + await assertRichTextInlineRange(page, 1, 0, 0); + + await undoByKeyboard(page); + await waitNextFrame(page); + await assertRichTexts(page, ['hello']); + await assertRichTextInlineRange(page, 0, 5, 0); + + await redoByKeyboard(page); + await waitNextFrame(page); + await assertRichTexts(page, ['hello', '']); + await assertRichTextInlineRange(page, 1, 0, 0); +}); + +test('insert new paragraph block by enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await pressEnter(page); + await pressEnter(page); + await assertRichTexts(page, ['', '', '']); + + await focusRichText(page, 1); + await type(page, 'hello'); + await assertRichTexts(page, ['', 'hello', '']); + + await pressEnter(page); + await type(page, 'world'); + await assertRichTexts(page, ['', 'hello', 'world', '']); + await assertBlockChildrenFlavours(page, '1', [ + 'affine:paragraph', + 'affine:paragraph', + 'affine:paragraph', + 'affine:paragraph', + ]); +}); + +test('split paragraph block by enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + await pressArrowLeft(page, 3); + await page.waitForTimeout(300); + await assertRichTextInlineRange(page, 0, 2, 0); + + await pressEnter(page); + await assertRichTexts(page, ['he', 'llo']); + await assertBlockChildrenFlavours(page, '1', [ + 'affine:paragraph', + 'affine:paragraph', + ]); + await assertRichTextInlineRange(page, 1, 0, 0); + + await undoByKeyboard(page); + await assertRichTexts(page, ['hello']); + + await redoByKeyboard(page); + await assertRichTexts(page, ['he', 'llo']); +}); + +test('split paragraph block with selected text by enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + // Avoid Yjs history manager merge two operations + await captureHistory(page); + + // select 'll' + await page.keyboard.press('ArrowLeft'); + await page.keyboard.down('Shift'); + await page.waitForTimeout(100); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.waitForTimeout(300); + await page.keyboard.up('Shift'); + await assertRichTextInlineRange(page, 0, 2, 2); + + await pressEnter(page); + await assertRichTexts(page, ['he', 'o']); + await assertBlockChildrenFlavours(page, '1', [ + 'affine:paragraph', + 'affine:paragraph', + ]); + await assertRichTextInlineRange(page, 1, 0, 0); + + await undoByKeyboard(page); + await assertRichTexts(page, ['hello']); + + await redoByKeyboard(page); + await assertRichTexts(page, ['he', 'o']); +}); + +test('add multi line by soft enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'hello'); + await assertRichTexts(page, ['hello']); + + await pressArrowLeft(page, 3); + await assertRichTextInlineRange(page, 0, 2, 0); + // Avoid Yjs history manager merge two operations + await captureHistory(page); + + await pressShiftEnter(page); + await assertRichTexts(page, ['he\nllo']); + await assertRichTextInlineRange(page, 0, 3, 0); + + await undoByKeyboard(page); + await assertRichTexts(page, ['hello']); + + await redoByKeyboard(page); + await assertRichTexts(page, ['he\nllo']); +}); + +test('indent and unindent existing paragraph block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + + await pressEnter(page); + await focusRichText(page, 1); + await type(page, 'world'); + await assertRichTexts(page, ['hello', 'world']); + + // indent + await page.keyboard.press('Tab'); + await assertRichTexts(page, ['hello', 'world']); + await assertBlockChildrenIds(page, '1', ['2']); + await assertBlockChildrenIds(page, '2', ['3']); + + // unindent + await pressShiftTab(page); + await assertRichTexts(page, ['hello', 'world']); + await assertBlockChildrenIds(page, '1', ['2', '3']); + + await undoByKeyboard(page); + await assertBlockChildrenIds(page, '1', ['2']); + + await redoByKeyboard(page); + await assertBlockChildrenIds(page, '1', ['2', '3']); +}); + +test('remove all indent for a paragraph block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await pressTab(page); + await type(page, 'world'); + await pressEnter(page); + await pressTab(page); + await type(page, 'foo'); + await assertBlockChildrenIds(page, '3', ['4']); + await assertRichTexts(page, ['hello', 'world', 'foo']); + await pressBackspaceWithShortKey(page); + await assertRichTexts(page, ['hello', 'world', '']); + await pressBackspaceWithShortKey(page); + await assertBlockChildrenIds(page, '1', ['2', '4']); + await assertBlockChildrenIds(page, '2', ['3']); +}); + +test('update paragraph with children to head type', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'aaa'); + + await pressEnter(page); + await focusRichText(page, 1); + await type(page, 'bbb'); + await pressEnter(page); + await focusRichText(page, 2); + await type(page, 'ccc'); + await assertRichTexts(page, ['aaa', 'bbb', 'ccc']); + + // aaa + // bbc + // ccc + await focusRichText(page, 1); + await page.keyboard.press('Tab'); + await focusRichText(page, 2); + await page.keyboard.press('Tab'); + await assertRichTexts(page, ['aaa', 'bbb', 'ccc']); + await assertBlockChildrenIds(page, '1', ['2']); + await assertBlockChildrenIds(page, '2', ['3', '4']); + + await focusRichText(page); + await pressArrowLeft(page, 3); + + await type(page, '# '); + + await assertRichTexts(page, ['aaa', 'bbb', 'ccc']); + await assertBlockChildrenIds(page, '2', ['3', '4']); + + await undoByKeyboard(page); + await assertRichTexts(page, ['# aaa', 'bbb', 'ccc']); + await assertBlockChildrenIds(page, '1', ['2']); + await assertBlockChildrenIds(page, '2', ['3', '4']); + + await redoByKeyboard(page); + await assertRichTexts(page, ['aaa', 'bbb', 'ccc']); + await assertBlockChildrenIds(page, '2', ['3', '4']); +}); + +test('should indent and unindent works with children', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await pressEnter(page); + await type(page, '012'); + await pressEnter(page); + await type(page, '345'); + // 123 + // 456 + // 789 + // 012 + // 345 + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + //-- test indent --// + + // Focus 789 + await focusRichText(page, 2); + await pressTab(page); + // 123 + // 456 + // 789| + // 012 + // 345 + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_indent.json` + ); + + // Focus 012 + await focusRichText(page, 3); + await pressTab(page); + // 123 + // 456 + // 789 + // 012| + // 345 + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_indent_2.json` + ); + + // Focus 456 + await focusRichText(page, 1); + await pressTab(page); + // 123 + // 456| + // 789 + // 012 + // 345 + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_indent_3.json` + ); + + // Focus 345 + await focusRichText(page, 4); + await pressTab(page, 3); + // 123 + // 456 + // 789 + // 012 + // 345| + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_indent_4.json` + ); + //-- test unindent --// + + // Focus 456 + await focusRichText(page, 1); + await pressShiftTab(page); + // 123 + // 456| + // 789 + // 012 + // 345 + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_unindent_1.json` + ); + + // Focus 012 + await focusRichText(page, 3); + await pressShiftTab(page); + // 123 + // 456 + // 789 + // 012| + // 345 + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_unindent_2.json` + ); + + // Focus 789 + await focusRichText(page, 2); + await pressShiftTab(page); + // 123 + // 456 + // 789| + // 012 + // 345 + + // Focus 345 + await focusRichText(page, 4); + await pressShiftTab(page); + // 123 + // 456 + // 789 + // 012 + // 345| + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_unindent_3.json` + ); +}); + +test('paragraph with child block should work at enter', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '123'); + await pressEnter(page); + await type(page, '456'); + + await focusRichText(page, 1); + await page.keyboard.press('Tab'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + await focusRichText(page, 0); + await pressEnter(page); + await type(page, '789'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test('should delete paragraph block child can hold cursor in correct position', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '123'); + await pressEnter(page); + await pressTab(page); + await waitNextFrame(page); + await type(page, '4'); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await pressBackspace(page, 2); + await waitNextFrame(page); + await type(page, 'now'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test('switch between paragraph types', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + + const selector = '.affine-paragraph-rich-text-wrapper'; + + await updateBlockType(page, 'affine:paragraph', 'h1'); + await assertClassName(page, selector, /h1/); + + await updateBlockType(page, 'affine:paragraph', 'h2'); + await assertClassName(page, selector, /h2/); + + await updateBlockType(page, 'affine:paragraph', 'h3'); + await assertClassName(page, selector, /h3/); + + await undoByClick(page); + await assertClassName(page, selector, /h2/); + + await undoByClick(page); + await assertClassName(page, selector, /h1/); +}); + +test('delete at start of paragraph block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + + await pressEnter(page); + await type(page, 'a'); + + await updateBlockType(page, 'affine:paragraph', 'h1'); + await focusRichText(page, 1); + await assertBlockType(page, '2', 'text'); + await assertBlockType(page, '3', 'h1'); + + await pressBackspace(page); + await pressBackspace(page); + await assertBlockType(page, '3', 'text'); + await assertBlockChildrenIds(page, '1', ['2', '3']); + + await page.keyboard.press('Backspace'); + await assertBlockChildrenIds(page, '1', ['2']); + + await undoByClick(page); + await assertBlockChildrenIds(page, '1', ['2', '3']); +}); + +test('delete at start of paragraph immediately following list', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + + await pressEnter(page); + await type(page, 'a'); + + await captureHistory(page); + + await assertRichTexts(page, ['hello', 'a']); + await assertBlockChildrenIds(page, '1', ['2', '3']); + await assertBlockType(page, '3', 'text'); + + // text -> bulleted + await focusRichText(page, 1); + await updateBlockType(page, 'affine:list', 'bulleted'); + await assertBlockType(page, '2', 'text'); + await assertBlockType(page, '4', 'bulleted'); + await pressBackspace(page, 2); + await waitNextFrame(page); + await assertBlockType(page, '5', 'text'); + await assertBlockChildrenIds(page, '1', ['2', '5']); + await pressBackspace(page); + await assertBlockChildrenIds(page, '1', ['2']); + + // reset + await undoByClick(page); + await undoByClick(page); + await assertRichTexts(page, ['hello', 'a']); + await assertBlockChildrenIds(page, '1', ['2', '3']); + await assertBlockType(page, '3', 'text'); + + // text -> numbered + await focusRichText(page, 1); + await updateBlockType(page, 'affine:list', 'numbered'); + await assertBlockType(page, '2', 'text'); + await assertBlockType(page, '6', 'numbered'); + await pressBackspace(page, 2); + await waitNextFrame(page); + await assertBlockType(page, '7', 'text'); + await assertBlockChildrenIds(page, '1', ['2', '7']); + await pressBackspace(page); + await assertBlockChildrenIds(page, '1', ['2']); + + // reset + await undoByClick(page); + await undoByClick(page); + await assertRichTexts(page, ['hello', 'a']); + await assertBlockChildrenIds(page, '1', ['2', '3']); + await assertBlockType(page, '3', 'text'); + + // text -> todo + await focusRichText(page, 1); + await updateBlockType(page, 'affine:list', 'todo'); + await assertBlockType(page, '2', 'text'); + await assertBlockType(page, '8', 'todo'); + await pressBackspace(page, 2); + await waitNextFrame(page); + await assertBlockType(page, '9', 'text'); + await assertBlockChildrenIds(page, '1', ['2', '9']); + await pressBackspace(page); + await assertBlockChildrenIds(page, '1', ['2']); +}); + +test('delete at start of paragraph with content', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '123'); + + await pressEnter(page); + await type(page, '456'); + await assertRichTexts(page, ['123', '456']); + + await captureHistory(page); + + await pressArrowLeft(page, 3); + await assertRichTextInlineRange(page, 1, 0, 0); + + await pressBackspace(page); + await assertRichTexts(page, ['123456']); + + await undoByClick(page); + await assertRichTexts(page, ['123', '456']); +}); + +test('get focus from page title enter', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'hello'); + await assertRichTexts(page, ['']); + + await pressEnter(page); + await type(page, 'world'); + await assertRichTexts(page, ['world', '']); +}); + +test('handling keyup when cursor located in first paragraph', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusTitle(page); + await type(page, 'hello'); + await assertRichTexts(page, ['']); + + await pressEnter(page); + await type(page, 'world'); + await assertRichTexts(page, ['world', '']); + await pressArrowUp(page); + await waitNextFrame(page); + await pressArrowUp(page); + await assertDocTitleFocus(page); +}); + +test('after deleting a text row, cursor should jump to the end of previous list row', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await assertRichTextInlineRange(page, 0, 5, 0); + + await pressEnter(page); + await type(page, 'w'); + await assertRichTexts(page, ['hello', 'w']); + await assertRichTextInlineRange(page, 1, 1, 0); + await pressArrowUp(page); + await pressArrowDown(page); + + await pressArrowLeft(page); + await pressBackspace(page); + await assertRichTextInlineRange(page, 0, 5, 0); +}); + +test('press tab in paragraph children', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await waitDefaultPageLoaded(page); + await focusTitle(page); + await pressEnter(page); + await type(page, '1'); + await pressEnter(page); + await pressTab(page); + await type(page, '2'); + await pressEnter(page); + await pressTab(page); + await type(page, '3'); + await page.keyboard.press('ArrowUp', { delay: 50 }); + await page.keyboard.press('ArrowLeft', { delay: 50 }); + await type(page, '- '); + await assertRichTexts(page, ['1', '2', '3', '']); +}); + +test('press left in first paragraph start should not change cursor position', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '1'); + + await pressArrowLeft(page, 2); + await type(page, 'l'); + await assertRichTexts(page, ['l1']); + await assertTitle(page, ''); +}); + +test('press arrow down should move caret to the start of line', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + doc.addBlock( + 'affine:paragraph', + { + text: new doc.Text('0'.repeat(100)), + }, + note + ); + doc.addBlock( + 'affine:paragraph', + { + text: new doc.Text('1'), + }, + note + ); + }); + + // Focus the empty child paragraph + await focusRichText(page, 1); + await pressArrowLeft(page); + await pressArrowUp(page); + await pressArrowDown(page); + await type(page, '2'); + await assertRichTexts(page, ['0'.repeat(100), '21']); +}); + +test('press arrow up in the second line should move caret to the first line', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + const delta = Array.from({ length: 150 }, (_, i) => { + return i % 2 === 0 + ? { insert: 'i', attributes: { italic: true } } + : { insert: 'b', attributes: { bold: true } }; + }) as DeltaInsert[]; + const text = new doc.Text(delta); + doc.addBlock('affine:paragraph', { text }, note); + doc.addBlock('affine:paragraph', {}, note); + }); + + // Focus the empty paragraph + await focusRichText(page, 1); + await assertRichTexts(page, ['ib'.repeat(75), '']); + await pressArrowUp(page, 2); + await type(page, '0'); + await assertTitle(page, ''); + await assertRichTexts(page, ['0' + 'ib'.repeat(75), '']); + await pressArrowUp(page, 2); + + // At title + await type(page, '1'); + await assertTitle(page, '1'); + await assertRichTexts(page, ['0' + 'ib'.repeat(75), '']); + + // At the first line of the first paragraph + await pressArrowDown(page); + // At the second paragraph + await pressArrowDown(page, 3); + await pressArrowRight(page); + await type(page, '2'); + + await assertRichTexts(page, ['0' + 'ib'.repeat(75), '2']); + + // Go to the start of the second paragraph + await pressArrowLeft(page); + await pressArrowUp(page); + await pressArrowDown(page); + // Should be inserted at the start of the second paragraph + await type(page, '3'); + await assertRichTexts(page, ['0' + 'ib'.repeat(75), '32']); +}); + +test('press arrow down in indent line should not move caret to the start of line', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + const p1 = doc.addBlock('affine:paragraph', {}, note); + const p2 = doc.addBlock('affine:paragraph', {}, p1); + doc.addBlock('affine:paragraph', {}, p2); + doc.addBlock( + 'affine:paragraph', + { + text: new doc.Text('0'), + }, + note + ); + }); + + // Focus the empty child paragraph + await focusRichText(page, 2); + await pressArrowDown(page, 2); + await pressArrowRight(page); + await waitNextFrame(page); + // Now the caret should be at the end of the last paragraph + await type(page, '1'); + await assertRichTexts(page, ['', '', '', '01']); + + await focusRichText(page, 2); + await waitNextFrame(page); + // Insert a new long text to wrap the line + await page.keyboard.insertText('0'.repeat(100)); + await waitNextFrame(page); + + await focusRichText(page, 1); + // Through long text + await pressArrowDown(page, 3); + await pressArrowRight(page); + await type(page, '2'); + await assertRichTexts(page, ['', '', '0'.repeat(100), '012']); +}); + +test('should placeholder works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + const placeholder = page.locator('.affine-paragraph-placeholder.visible'); + await expect(placeholder).toBeVisible(); + await expect(placeholder).toHaveCount(1); + await expect(placeholder).toContainText("Type '/' for commands"); + + await type(page, '1'); + await expect(placeholder).not.toBeVisible(); + await pressBackspace(page); + + await expect(placeholder).toBeVisible(); + await updateBlockType(page, 'affine:paragraph', 'h1'); + + await expect(placeholder).toBeVisible(); + await expect(placeholder).toHaveText('Heading 1'); + await updateBlockType(page, 'affine:paragraph', 'text'); + await focusRichText(page, 0); + await expect(placeholder).toBeVisible(); + await expect(placeholder).toContainText("Type '/' for commands"); + + await pressEnter(page); + await expect(placeholder).toHaveCount(1); +}); + +test('should placeholder not show when multiple blocks are selected', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await pressEnter(page); + await assertRichTexts(page, ['', '']); + const coord = await getIndexCoordinate(page, [0, 0]); + // blur + await page.mouse.click(0, 0); + await page.mouse.move(coord.x - 26 - 24, coord.y - 10, { steps: 20 }); + await page.mouse.down(); + // โ† + await page.mouse.move(coord.x + 20, coord.y + 50, { steps: 20 }); + await page.mouse.up(); + const placeholder = page.locator('.affine-paragraph-placeholder.visible'); + await expect(placeholder).toBeHidden(); +}); + +test.describe('press ArrowDown when cursor is at the last line of a block', () => { + test.beforeEach(async ({ page }) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + doc.addBlock( + 'affine:paragraph', + { + text: new doc.Text('This is the 2nd last block.'), + }, + note + ); + doc.addBlock( + 'affine:paragraph', + { + text: new doc.Text('This is the last block.'), + }, + note + ); + }); + }); + + test('move cursor to next block if this block is _not_ the last block in the page', async ({ + page, + }) => { + // Click at the top-left corner of the 2nd last block to place the cursor at its start + await focusRichText(page, 0, { clickPosition: { x: 0, y: 0 } }); + // Cursor should have been moved to the start of the last block. + await pressArrowDown(page); + await type(page, "I'm here. "); + await assertRichTexts(page, [ + 'This is the 2nd last block.', + "I'm here. This is the last block.", + ]); + }); + test('move cursor to the end of line if the block is the last block in the page', async ({ + page, + }) => { + // Click at the top-left corner of the last block to place the cursor at its start + await focusRichText(page, 1, { clickPosition: { x: 0, y: 0 } }); + // Cursor should have been moved to the end of the only line. + await pressArrowDown(page, 2); + await pressArrowRight(page); + await type(page, " I'm here."); + await assertRichTexts(page, [ + 'This is the 2nd last block.', + "This is the last block. I'm here.", + ]); + }); +}); + +test('delete empty text paragraph block should keep children blocks when following custom blocks', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '--- '); + await assertDivider(page, 1); + + // Click blank area to add a paragraph block after divider + await page.mouse.click(100, 200); + await page.waitForTimeout(200); + + // Add to paragraph blocks + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + // Indent the second paragraph block + await focusRichText(page, 2); + await pressTab(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + // Delete the parent paragraph block + await focusRichText(page, 1); + await pressBackspace(page, 4); + + await assertRichTexts(page, ['123', '789']); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +test('delete first paragraph with children should keep children blocks', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '123'); + await pressEnter(page); + await pressTab(page); + await waitNextFrame(page); + await type(page, '456'); + await setSelection(page, 2, 1, 2, 1); + await pressBackspace(page, 2); + await waitNextFrame(page); + await assertTitle(page, '23'); + await assertRichTexts(page, ['456']); +}); + +test('paragraph indent and delete in line start', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'abc'); + await pressEnter(page); + await pressTab(page); + await type(page, 'efg'); + await pressEnter(page); + await pressTab(page); + await type(page, 'hij'); + await pressEnter(page); + await pressShiftTab(page); + await type(page, 'klm'); + await pressEnter(page); + await type(page, 'nop'); + await setSelection(page, 3, 1, 3, 1); + // abc + // e|fg + // hij + // klm + // nop + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + await pressBackspace(page, 2); + await setSelection(page, 5, 1, 5, 1); + // abcfg + // hij + // k|lm + // nop + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after_press_backspace.json` + ); + + await pressBackspace(page, 2); + await setSelection(page, 6, 1, 6, 1); + // abcfg + // hijlm + // n|op + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after_press_backspace_2.json` + ); + + await pressBackspace(page, 2); + // abcfg + // hijlm + // |op + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_after_press_backspace_3.json` + ); +}); + +test('delete at the start of paragraph (multiple notes)', async ({ page }) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + doc.addBlock('affine:surface', {}, rootId); + + ['123', '456'].forEach(text => { + const noteId = doc.addBlock('affine:note', {}, rootId); + doc.addBlock( + 'affine:paragraph', + { + text: new doc.Text(text), + }, + noteId + ); + }); + + doc.resetHistory(); + }); + + await assertBlockCount(page, 'note', 2); + + await assertRichTexts(page, ['123', '456']); + await focusRichText(page, 1); + await pressArrowLeft(page, 3); + await pressBackspace(page); + await assertRichTexts(page, ['123456']); +}); + +test('arrow up/down navigation within and across paragraphs containing different types of text', async ({ + page, +}) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/5155', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'a'.repeat(20)); + await assertRichTextInlineRange(page, 0, 20, 0); + await type(page, '*'); + await type(page, 'i'.repeat(5)); + await type(page, '*'); + await pressSpace(page); + await assertRichTextInlineRange(page, 0, 25, 0); + await type(page, 'a'.repeat(100)); + await assertRichTextInlineRange(page, 0, 125, 0); + await pressEnter(page); + + await type(page, 'a'.repeat(100)); + await assertRichTextInlineRange(page, 1, 100, 0); + await type(page, '*'); + await type(page, 'i'.repeat(5)); + await type(page, '*'); + await pressSpace(page); + await assertRichTextInlineRange(page, 1, 105, 0); + await type(page, 'a'.repeat(20)); + await assertRichTextInlineRange(page, 1, 125, 0); + + await pressArrowUp(page); + await assertRichTextInlineRange(page, 1, 32, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 0, 125, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 0, 35, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 0, 0, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 0, 125, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 1, 32, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 1, 125, 0); +}); + +test('select divider using delete keyboard from prev/next paragraph', async ({ + page, +}) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/4547', + }); + + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await initThreeDividers(page); + await assertDivider(page, 3); + await assertRichTexts(page, ['123', '123']); + + await focusRichText(page, 0); + await pressForwardDelete(page); + await assertBlockSelections(page, ['4']); + await assertDivider(page, 3); + + await focusRichText(page, 1); + await pressArrowLeft(page, 3); + await pressBackspace(page); + await assertBlockSelections(page, ['6']); + await assertDivider(page, 3); + + await assertRichTexts(page, ['123', '123']); +}); + +test.describe('readonly mode', () => { + test('should placeholder not show at readonly mode', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await pressEnter(page); + await updateBlockType(page, 'affine:paragraph', 'h1'); + + const placeholder = page.locator('.affine-paragraph-placeholder.visible'); + + await switchReadonly(page); + await focusRichText(page, 0); + await expect(placeholder).toBeHidden(); + + await focusRichText(page, 1); + await expect(placeholder).toBeHidden(); + }); + + test('should readonly mode not be able to modify text', async ({ page }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + + await focusRichText(page); + await type(page, 'hello'); + await switchReadonly(page); + + await pressBackspace(page, 5); + await type(page, 'world'); + await dragBetweenIndices(page, [0, 1], [0, 3]); + await page.keyboard.press(`${SHORT_KEY}+b`); + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + + await undoByKeyboard(page); + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + }); +}); diff --git a/blocksuite/tests-legacy/playwright.config.ts b/blocksuite/tests-legacy/playwright.config.ts new file mode 100644 index 0000000000000..15fe0b095dfd9 --- /dev/null +++ b/blocksuite/tests-legacy/playwright.config.ts @@ -0,0 +1,46 @@ +import process from 'node:process'; + +import type { PlaywrightWorkerOptions } from '@playwright/test'; +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + timeout: process.env.CI ? 40000 : 999999, + fullyParallel: true, + snapshotDir: 'snapshots', + snapshotPathTemplate: 'snapshots/{testFilePath}/{arg}{ext}', + webServer: { + command: process.env.CI + ? 'yarn workspace @blocksuite/playground run preview' + : 'yarn workspace @blocksuite/playground run dev', + port: process.env.CI ? 4173 : 5173, + reuseExistingServer: !process.env.CI, + env: { + COVERAGE: process.env.COVERAGE ?? '', + }, + }, + use: { + browserName: + (process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ?? + 'chromium', + viewport: { width: 960, height: 900 }, + // Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer + // You can open traces locally(`npx playwright show-trace trace.zip`) + // or in your browser on [Playwright Trace Viewer](https://trace.playwright.dev/). + trace: 'on-first-retry', + // Record video only when retrying a test for the first time. + video: 'on-first-retry', + // Timeout for each action + actionTimeout: 5_000, + permissions: + process.env.BROWSER && process.env.BROWSER !== 'chromium' + ? [] + : ['clipboard-read', 'clipboard-write'], + }, + workers: '80%', + retries: process.env.CI ? 3 : 0, + // 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot' + // default 'list' when running locally + // See https://playwright.dev/docs/test-reporters#github-actions-annotations + reporter: process.env.CI ? 'github' : 'list', +}); diff --git a/blocksuite/tests-legacy/quote.spec.ts b/blocksuite/tests-legacy/quote.spec.ts new file mode 100644 index 0000000000000..32add78bad150 --- /dev/null +++ b/blocksuite/tests-legacy/quote.spec.ts @@ -0,0 +1,102 @@ +import { + enterPlaygroundRoom, + focusRichText, + initEmptyParagraphState, + pressArrowDown, + pressArrowRight, + pressArrowUp, + pressEnter, + type, +} from './utils/actions/index.js'; +import { + assertRichTextInlineRange, + assertTextContain, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +test('prohibit creating divider within quote', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/995', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '>'); + await page.keyboard.press('Space', { delay: 50 }); + await focusRichText(page); + await type(page, '123'); + await pressEnter(page); + await type(page, '---'); + await page.keyboard.press('Space', { delay: 50 }); + await assertTextContain(page, '---'); +}); + +test('quote arrow up/down', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/2834', + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'aaaaa'); + await pressEnter(page); + await type(page, 'aaaaa'); + await pressEnter(page); + await type(page, 'aaa'); + await pressEnter(page); + await type(page, '> aaaaaaaaa'); + await pressEnter(page); + await type(page, 'aaa'); + await pressEnter(page); + await type(page, 'aaaaaaaaa'); + await pressEnter(page); + await pressEnter(page); + await type(page, 'aaaaa'); + await pressEnter(page); + await type(page, 'aaaaa'); + await pressEnter(page); + await type(page, 'aaa'); + + await assertRichTextInlineRange(page, 6, 3, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 5, 3, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 4, 3, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 3, 15, 0); + await pressArrowRight(page, 8); + await assertRichTextInlineRange(page, 3, 23, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 3, 13, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 3, 9, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 2, 3, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 1, 5, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 0, 5, 0); + await pressArrowUp(page); + await assertRichTextInlineRange(page, 0, 0, 0); + await pressArrowRight(page, 4); + await assertRichTextInlineRange(page, 0, 4, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 1, 4, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 2, 3, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 3, 2, 0); + await pressArrowRight(page, 8); + await assertRichTextInlineRange(page, 3, 10, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 3, 14, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 4, 2, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 5, 2, 0); + await pressArrowDown(page); + await assertRichTextInlineRange(page, 6, 2, 0); +}); diff --git a/blocksuite/tests-legacy/selection/block.spec.ts b/blocksuite/tests-legacy/selection/block.spec.ts new file mode 100644 index 0000000000000..5f2531f23305f --- /dev/null +++ b/blocksuite/tests-legacy/selection/block.spec.ts @@ -0,0 +1,1425 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import { + activeEmbed, + clickBlockDragHandle, + copyByKeyboard, + dragBetweenCoords, + dragBetweenIndices, + dragEmbedResizeByTopLeft, + enterPlaygroundRoom, + focusRichText, + getIndexCoordinate, + getPageSnapshot, + getRichTextBoundingBox, + initEmptyParagraphState, + initImageState, + initMultipleNoteWithParagraphState, + initParagraphsByCount, + initThreeLists, + initThreeParagraphs, + pasteByKeyboard, + pressBackspace, + pressEnter, + pressEscape, + pressForwardDelete, + pressShiftTab, + pressSpace, + pressTab, + redoByClick, + redoByKeyboard, + resetHistory, + selectAllByKeyboard, + shamefullyBlurActiveElement, + type, + undoByClick, + undoByKeyboard, + waitNextFrame, +} from '../utils/actions/index.js'; +import { + assertAlmostEqual, + assertBlockCount, + assertRichTexts, +} from '../utils/asserts.js'; +import { test } from '../utils/playwright.js'; + +test('block level range delete', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + await resetHistory(page); + + const box123 = await getRichTextBoundingBox(page, '2'); + const above123 = { x: box123.left, y: box123.top - 10 }; + + const box789 = await getRichTextBoundingBox(page, '4'); + const below789 = { x: box789.right - 10, y: box789.bottom + 10 }; + + await dragBetweenCoords(page, below789, above123); + await pressBackspace(page); + await assertBlockCount(page, 'paragraph', 1); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await undoByClick(page); + await assertRichTexts(page, ['123', '456', '789']); + + await redoByClick(page); + await assertRichTexts(page, ['']); +}); + +test('block level range delete by forwardDelete', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + await resetHistory(page); + + const box123 = await getRichTextBoundingBox(page, '2'); + const above123 = { x: box123.left, y: box123.top - 10 }; + + const box789 = await getRichTextBoundingBox(page, '4'); + const below789 = { x: box789.right - 10, y: box789.bottom + 10 }; + + await dragBetweenCoords(page, below789, above123); + await pressForwardDelete(page); + await waitNextFrame(page); + await assertBlockCount(page, 'paragraph', 1); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await undoByClick(page); + await assertRichTexts(page, ['123', '456', '789']); + + await redoByClick(page); + await assertRichTexts(page, ['']); +}); + +// XXX: Doesn't simulate full user operation due to backspace cursor issue in Playwright. +test('select all and delete', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await shamefullyBlurActiveElement(page); + await pressBackspace(page); + await focusRichText(page, 0); + await type(page, 'abc'); + await assertRichTexts(page, ['abc']); +}); + +test('select all and delete by forwardDelete', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await shamefullyBlurActiveElement(page); + await pressForwardDelete(page); + await focusRichText(page, 0); + await type(page, 'abc'); + await assertRichTexts(page, ['abc']); +}); + +test('select all should work for multiple notes in doc mode', async ({ + page, +}) => { + const n = 4; + await enterPlaygroundRoom(page); + await initMultipleNoteWithParagraphState(page, undefined, n); + + await focusRichText(page, 0); + await type(page, '123'); + await focusRichText(page, 1); + await type(page, '456'); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + const rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(n); +}); + +async function clickListIcon(page: Page, i = 0) { + const locator = page.locator('.affine-list-block__prefix').nth(i); + await locator.click({ force: true }); +} + +test('click the list icon can select and copy', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeLists(page); + await assertRichTexts(page, ['123', '456', '789']); + await clickListIcon(page, 0); + // copy 123 + await copyByKeyboard(page); + + await focusRichText(page, 2); + await pasteByKeyboard(page); + await assertRichTexts(page, ['123', '456', '789123']); + + // copy 789123 + await clickListIcon(page, 2); + await copyByKeyboard(page); + + await focusRichText(page, 0); + await pasteByKeyboard(page); + await assertRichTexts(page, ['123789123', '456', '789123']); +}); + +test('click the list icon can select and delete', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeLists(page); + await assertRichTexts(page, ['123', '456', '789']); + + await clickListIcon(page, 0); + await waitNextFrame(page); + await pressBackspace(page); + await assertRichTexts(page, ['', '456', '789']); + await clickListIcon(page, 0); + await waitNextFrame(page); + await pressBackspace(page); + await assertRichTexts(page, ['', '']); +}); + +test('click the list icon can select and delete by forwardDelete', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeLists(page); + await assertRichTexts(page, ['123', '456', '789']); + + await clickListIcon(page, 0); + await waitNextFrame(page); + await pressForwardDelete(page); + await assertRichTexts(page, ['', '456', '789']); + await clickListIcon(page, 0); + await waitNextFrame(page); + await pressForwardDelete(page); + await assertRichTexts(page, ['', '']); +}); + +test('selection on heavy page', async ({ page }) => { + await page + .locator('body') + .evaluate(element => (element.style.padding = '50px')); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + for (let i = 0; i < 5; i++) { + await type(page, `Line ${i + 1}`); + await pressEnter(page); + } + const [first, last] = await page.evaluate(() => { + const first = document.querySelector('[data-block-id="2"]'); + if (!first) { + throw new Error(); + } + + const last = document.querySelector('[data-block-id="6"]'); + if (!last) { + throw new Error(); + } + return [first.getBoundingClientRect(), last.getBoundingClientRect()]; + }); + await dragBetweenCoords( + page, + { + x: first.x - 1, + y: first.y - 1, + }, + { + x: last.x + 1, + y: last.y + 1, + }, + { + beforeMouseUp: async () => { + const rect = await page + .locator('.affine-page-dragging-area') + .evaluate(element => element.getBoundingClientRect()); + assertAlmostEqual(rect.x, first.x - 1, 1); + assertAlmostEqual(rect.y, first.y - 1, 1); + assertAlmostEqual(rect.right, last.x + 1, 1); + assertAlmostEqual(rect.bottom, last.y + 1, 1); + }, + } + ); + const rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(5); +}); + +test('should indent multi-selection block', async ({ page }, testInfo) => { + 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 - 26 - 24, coord.y - 10, { steps: 20 }); + await page.mouse.down(); + // โ† + await page.mouse.move(coord.x + 20, coord.y + 50, { steps: 20 }); + await page.mouse.up(); + + await page.keyboard.press('Tab'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); +}); + +test('should unindent multi-selection block', async ({ page }, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + let coord = await getIndexCoordinate(page, [1, 2]); + + // blur + await page.mouse.click(0, 0); + await page.mouse.move(coord.x - 26 - 24, coord.y - 10, { steps: 20 }); + await page.mouse.down(); + // โ† + await page.mouse.move(coord.x + 20, coord.y + 50, { steps: 20 }); + await page.mouse.up(); + + await page.keyboard.press('Tab'); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_init.json` + ); + + coord = await getIndexCoordinate(page, [1, 2]); + + // blur + await page.mouse.click(0, 0); + await page.mouse.move(coord.x - 26 - 50, coord.y, { steps: 20 }); + await page.mouse.down(); + // โ† + await page.mouse.move(coord.x + 20, coord.y + 30, { steps: 20 }); + await page.mouse.up(); + + await pressShiftTab(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_final.json` + ); +}); + +// โ†‘ +test('should keep selection state when scrolling backward', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + + await assertRichTexts(page, ['123', '456', '789']); + + for (let i = 0; i < 6; 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: 5 }, () => ''); + data.unshift('123', '456', '789'); + data.push('987', '654', '321'); + await assertRichTexts(page, data); + + const [, container, distance] = 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-block-children-container' + ); + if (!container) { + throw new Error(); + } + return [ + viewport.getBoundingClientRect(), + container.getBoundingClientRect(), + distance, + ] as const; + }); + + await page.mouse.move(0, 0); + await dragBetweenCoords( + page, + { + x: container.right + 1, + y: container.bottom, + }, + { + x: container.right - 1, + y: 1, + }, + { + // dont release mouse + beforeMouseUp: async () => { + const count = distance / (10 * 0.25); + await page.waitForTimeout((1000 / 60) * count); + }, + } + ); + + const scrollTop = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + const rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(3 + 5 + 3); + expect(scrollTop).toBe(0); +}); + +// โ†“ +test('should keep selection state when scrolling forward', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + for (let i = 0; i < 6; 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: 5 }, () => ''); + data.unshift('123', '456', '789'); + data.push('987', '654', '321'); + await assertRichTexts(page, data); + + const [viewport, container, distance] = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + const distance = viewport.scrollHeight - viewport.clientHeight; + const container = viewport.querySelector( + '.affine-block-children-container' + ); + if (!container) { + throw new Error(); + } + return [ + viewport.getBoundingClientRect(), + container.getBoundingClientRect(), + distance, + ] as const; + }); + + await page.mouse.move(0, 0); + + await dragBetweenCoords( + page, + { + x: container.right + 1, + y: container.top + 1, + }, + { + x: container.right - 1, + y: viewport.height - 1, + }, + { + // dont release mouse + beforeMouseUp: async () => { + const count = distance / (10 * 0.25); + await page.waitForTimeout((1000 / 60) * count); + }, + } + ); + + const scrollTop = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + const rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(3 + 5 + 3); + // See https://jestjs.io/docs/expect#tobeclosetonumber-numdigits + // Math.abs(scrollTop - distance) < Math.pow(10, -1 * -0.01)/2 = 0.511646496140377 + expect(scrollTop).toBeCloseTo(distance, -0.01); +}); + +// โ†‘ +test('should keep selection state 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 < 6; 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: 5 }, () => ''); + data.unshift('123', '456', '789'); + data.push('987', '654', '321'); + await assertRichTexts(page, data); + + const [last, distance] = 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 last = container.lastElementChild; + if (!last) { + throw new Error(); + } + return [last.getBoundingClientRect(), distance] as const; + }); + await page.waitForTimeout(250); + + await page.mouse.move(0, 0); + + await dragBetweenCoords( + page, + { + x: last.right + 1, + y: last.top + 1, + }, + { + x: last.right - 1, + y: last.top - 1, + }, + { + // dont release mouse + beforeMouseUp: async () => { + await page.mouse.wheel(0, -distance * 2); + await page.waitForTimeout(250); + }, + } + ); + + // get count with scroll wheel + const rects = page.locator('affine-block-selection').locator('visible=true'); + const count0 = await rects.count(); + const scrollTop0 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + await page.mouse.move(0, 0); + + 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); + }); + await page.waitForTimeout(250); + + await dragBetweenCoords( + page, + { + x: last.right + 1, + y: last.top + 1, + }, + { + x: last.right - 1, + y: last.top - 1 - distance, + } + ); + + // get count with moving mouse + const count1 = await rects.count(); + const scrollTop1 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + expect(count0).toBe(count1); + expect(scrollTop0).toBe(0); + expect(scrollTop1).toBeCloseTo(distance, -0.5); +}); + +// โ†“ +test('should keep selection state 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 < 6; 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: 5 }, () => ''); + data.unshift('123', '456', '789'); + data.push('987', '654', '321'); + await assertRichTexts(page, data); + + const [first, distance] = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + const distance = viewport.scrollHeight - viewport.clientHeight; + const container = viewport.querySelector( + 'affine-note .affine-block-children-container' + ); + if (!container) { + throw new Error(); + } + const first = container.firstElementChild; + if (!first) { + throw new Error(); + } + return [first.getBoundingClientRect(), distance] as const; + }); + + await page.mouse.move(0, 0); + + await dragBetweenCoords( + page, + { + x: first.left - 10, + y: first.top - 10, + }, + { + x: first.left + 1, + y: first.top + 1, + }, + { + // don't release mouse + beforeMouseUp: async () => { + await page.mouse.wheel(0, distance * 2); + await page.waitForTimeout(250); + }, + } + ); + + // get count with scroll wheel + const rects = page.locator('affine-block-selection').locator('visible=true'); + const count0 = await rects.count(); + const scrollTop0 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + await page.mouse.move(0, 0); + + await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + viewport.scrollTo(0, 0); + }); + await page.waitForTimeout(250); + + await dragBetweenCoords( + page, + { + x: first.left - 10, + y: first.top - 10, + }, + { + x: first.left + 1, + y: first.top + 1 + distance, + } + ); + + // get count with moving mouse + const count1 = await rects.count(); + const scrollTop1 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + expect(count0).toBe(count1); + expect(scrollTop0).toBeCloseTo(distance, -0.8); + expect(scrollTop1).toBe(0); +}); + +test('should not clear selected rects when clicking on scrollbar', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + for (let i = 0; i < 6; i++) { + await pressEnter(page); + } + + await type(page, '987'); + await pressEnter(page); + await type(page, '654'); + await pressEnter(page); + await type(page, '321'); + + const [viewport, first, distance] = 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 / 2); + const container = viewport.querySelector( + 'affine-note .affine-block-children-container' + ); + if (!container) { + throw new Error(); + } + const first = container.firstElementChild; + if (!first) { + throw new Error(); + } + return [ + viewport.getBoundingClientRect(), + first.getBoundingClientRect(), + distance, + ] as const; + }); + + await page.mouse.move(0, 0); + + await dragBetweenCoords( + page, + { + x: first.left - 10, + y: first.top - 10, + }, + { + x: first.right + 10, + y: first.bottom + distance / 2, + } + ); + + const rects = page.locator('affine-block-selection').locator('visible=true'); + const count0 = await rects.count(); + const scrollTop0 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + await page.mouse.click(viewport.right, distance / 2); + + const count1 = await rects.count(); + const scrollTop1 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + expect(count0).toBeGreaterThan(0); + expect(scrollTop0).toBeCloseTo(distance / 2, -0.01); + expect(count0).toBe(count1); + expect(scrollTop0).toBeCloseTo(scrollTop1, -0.01); +}); + +test('should not clear selected rects when scrolling the wheel', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + for (let i = 0; i < 6; i++) { + await pressEnter(page); + } + + await type(page, '987'); + await pressEnter(page); + await type(page, '654'); + await pressEnter(page); + await type(page, '321'); + + const [viewport, first, distance] = 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 / 2); + const container = viewport.querySelector( + 'affine-note .affine-block-children-container' + ); + if (!container) { + throw new Error(); + } + const first = container.firstElementChild; + if (!first) { + throw new Error(); + } + return [ + viewport.getBoundingClientRect(), + first.getBoundingClientRect(), + distance, + ] as const; + }); + + await page.mouse.move(0, 0); + + await dragBetweenCoords( + page, + { + x: first.left - 10, + y: first.top - 10, + }, + { + x: first.right + 10, + y: first.bottom + distance / 2, + } + ); + + const rects = page.locator('affine-block-selection').locator('visible=true'); + const count0 = await rects.count(); + + await page.mouse.wheel(viewport.right, -distance / 4); + await waitNextFrame(page); + + const count1 = await rects.count(); + + expect(count0).toBeGreaterThan(0); + expect(count0).toBe(count1); + + await page.mouse.wheel(viewport.right, distance / 4); + await waitNextFrame(page); + + const count2 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + const rects = viewport.querySelectorAll('affine-block-selection'); + const visibleRects = Array.from(rects).filter(rect => { + const display = window.getComputedStyle(rect).display; + return display !== 'none'; + }); + return visibleRects.length; + }); + + expect(count0).toBe(count2); +}); + +test('should refresh selected rects when resizing the window/viewport', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + for (let i = 0; i < 6; i++) { + await pressEnter(page); + } + + await type(page, '987'); + await pressEnter(page); + await type(page, '654'); + await pressEnter(page); + await type(page, '321'); + + const [viewport, first, distance] = 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 / 2); + const container = viewport.querySelector( + 'affine-note .affine-block-children-container' + ); + if (!container) { + throw new Error(); + } + const first = container.firstElementChild; + if (!first) { + throw new Error(); + } + return [ + viewport.getBoundingClientRect(), + first.getBoundingClientRect(), + distance, + ] as const; + }); + + await page.mouse.move(0, 0); + + await dragBetweenCoords( + page, + { + x: first.left - 1, + y: first.top - 1, + }, + { + x: first.left + 1, + y: first.top + distance / 2, + } + ); + + const rects = page.locator('affine-block-selection').locator('visible=true'); + const count0 = await rects.count(); + const scrollTop0 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + await page.mouse.click(viewport.right, first.top + distance / 2); + + const size = page.viewportSize(); + + if (!size) { + throw new Error(); + } + + await page.setViewportSize({ + width: size.width - 100, + height: size.height - 100, + }); + await page.waitForTimeout(250); + + const count1 = await rects.count(); + const scrollTop1 = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + + expect(count0).toBe(count1); + expect(scrollTop0).toBeCloseTo(scrollTop1, -0.01); +}); + +test('should clear block selection before native selection', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + // `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.left - 10, + y: first.top - 10, + }, + { + x: first.left + 1, + y: first.top + 1, + } + ); + + const rects = page.locator('affine-block-selection').locator('visible=true'); + const count0 = await rects.count(); + + await dragBetweenIndices( + page, + [1, 3], + [1, 0], + { x: 0, y: 0 }, + { x: 0, y: 0 } + ); + + const count1 = await rects.count(); + const textCount = await page.evaluate(() => { + return window.getSelection()?.rangeCount || 0; + }); + + expect(count0).toBe(1); + expect(count1).toBe(0); + expect(textCount).toBe(1); +}); + +test('should not be misaligned when the editor container has padding or margin', async ({ + page, +}) => { + await page.locator('body').evaluate(element => { + element.style.margin = '50px'; + element.style.padding = '50px'; + }); + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + // `123`, `789` + const [first, last] = 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 last = container.lastElementChild; + if (!last) { + throw new Error(); + } + return [first.getBoundingClientRect(), last.getBoundingClientRect()]; + }); + + await dragBetweenCoords( + page, + { + x: first.left - 10, + y: first.top - 10, + }, + { + x: last.left + 1, + y: last.top + 1, + } + ); + + const rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(3); +}); + +test('undo should clear block selection', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + await pressEnter(page); + + const rect = await getRichTextBoundingBox(page, '2'); + await dragBetweenCoords( + page, + { x: rect.x - 5, y: rect.y - 5 }, + { x: rect.x + 5, y: rect.y + rect.height } + ); + + await redoByKeyboard(page); + const selectedBlocks = page + .locator('affine-block-selection') + .locator('visible=true'); + await expect(selectedBlocks).toHaveCount(1); + + await undoByKeyboard(page); + await expect(selectedBlocks).toHaveCount(0); +}); + +test('should not draw rect for sub selected blocks when entering tab key', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + const coord = await getIndexCoordinate(page, [1, 3]); + + // blur + await page.mouse.click(20, 20); + + await dragBetweenCoords( + page, + { x: coord.x - 60, y: coord.y + 10 }, + { x: coord.x + 100, y: coord.y + 30 }, + { steps: 50 } + ); + + await pressTab(page); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); +}); + +test('should blur rich-text first on starting block selection', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await expect(page.locator('*:focus')).toHaveCount(1); + + const coord = await getIndexCoordinate(page, [1, 2]); + await dragBetweenCoords( + page, + { x: coord.x - 30, y: coord.y - 10 }, + { x: coord.x + 20, y: coord.y + 50 } + ); + + await expect(page.locator('*:focus')).toHaveCount(0); +}); + +test('should not show option menu of image on block selection', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await activeEmbed(page); + + await expect(page.locator('.affine-image-toolbar-container')).toHaveCount(1); + + await pressEnter(page); + + const imageRect = await page.locator('affine-image').boundingBox(); + if (!imageRect) { + throw new Error(); + } + + await dragBetweenCoords( + page, + { + x: imageRect.x + imageRect.width + 60, + y: imageRect.y + imageRect.height / 2 + 10, + }, + { + x: imageRect.x - 100, + y: imageRect.y + imageRect.height / 2, + } + ); + + await page.waitForTimeout(50); + + await expect(page.locator('.affine-image-toolbar-container')).toHaveCount(0); + await expect( + page.locator('affine-block-selection').locator('visible=true') + ).toHaveCount(1); +}); + +test('click bottom of page and if the last is embed block, editor should insert a new editable block', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initImageState(page); + await activeEmbed(page); + await dragEmbedResizeByTopLeft(page); + + const hostRect = await page.evaluate(() => { + const host = document.querySelector('editor-host'); + if (!host) { + throw new Error("Can't find doc viewport"); + } + return host.getBoundingClientRect(); + }); + + await page.mouse.click(hostRect.x + hostRect.width / 2, hostRect.bottom - 10); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); +}); + +test('should select blocks when pressing escape', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await focusRichText(page, 2); + await page.keyboard.press('Escape'); + await expect( + page.locator('affine-block-selection').locator('visible=true') + ).toHaveCount(1); + await page.keyboard.press('Escape'); + + const cords = await getIndexCoordinate(page, [1, 2]); + await page.mouse.move(cords.x + 10, cords.y + 10, { steps: 20 }); + await page.mouse.down(); + await page.mouse.move(cords.x + 20, cords.y + 30, { steps: 20 }); + await page.mouse.up(); + + await page.keyboard.press('Escape'); + await expect( + page.locator('affine-block-selection').locator('visible=true') + ).toHaveCount(1); +}); + +test('should un-select blocks when pressing escape', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initThreeParagraphs(page); + await assertRichTexts(page, ['123', '456', '789']); + + await focusRichText(page, 2); + await pressEscape(page); + await expect( + page.locator('affine-block-selection').locator('visible=true') + ).toHaveCount(1); + + await pressEscape(page); + await expect( + page.locator('affine-block-selection').locator('visible=true') + ).toHaveCount(0); + + await focusRichText(page, 2); + await pressEnter(page); + await type(page, '-'); + await pressSpace(page); + await clickListIcon(page, 0); + await expect( + page.locator('affine-block-selection').locator('visible=true') + ).toHaveCount(1); + + await pressEscape(page); + await expect( + page.locator('affine-block-selection').locator('visible=true') + ).toHaveCount(0); +}); + +test('verify cursor position after changing block type', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + const anchorOffset = await page.evaluate(() => { + return window.getSelection()?.anchorOffset || 0; + }); + expect(anchorOffset).toBe(5); + + await type(page, '/'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + + const todayBlock = page.getByTestId('Heading 1'); + await todayBlock.click(); + await expect(slashMenu).toBeHidden(); + + await type(page, 'w'); + const anchorOffset2 = await page.evaluate(() => { + return window.getSelection()?.anchorOffset || 0; + }); + expect(anchorOffset2).toBe(6); +}); + +// https://github.com/toeverything/blocksuite/issues/3613 +test('should scroll page properly by wheel after inserting a new block and selecting it', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + await test.step('Insert enough blocks to make page scrollable', async () => { + await focusRichText(page); + + for (let i = 0; i < 10; i++) { + await type(page, String(i)); + await pressEnter(page); + await pressEnter(page); + } + }); + + await type(page, 'new block'); + + const lastBlockId = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport')!; + const container = viewport.querySelector( + 'affine-note .affine-block-children-container' + ); + const last = container!.lastElementChild as HTMLElement; + if (!last) { + throw new Error(); + } + return last.dataset.blockId!; + }); + + // click drag handle to select block + await clickBlockDragHandle(page, lastBlockId); + + async function getViewportScrollTop() { + return page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + } + await page.mouse.move(0, 0); + // scroll to top by wheel + await page.mouse.wheel(0, -(await getViewportScrollTop()) * 2); + await page.waitForTimeout(250); + expect(await getViewportScrollTop()).toBe(0); + + // scroll to end by wheel + const distanceToEnd = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport')!; + return viewport.scrollHeight - viewport.clientHeight; + }); + await page.mouse.wheel(0, distanceToEnd * 2); + await page.waitForTimeout(250); + expect(await getViewportScrollTop()).toBe(distanceToEnd); +}); + +test('should not select parent block when dragging area only intersects with child', 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 - 26 - 24, coord.y - 10, { steps: 20 }); + await page.mouse.down(); + // โ† + await page.mouse.move(coord.x + 20, coord.y + 50, { steps: 20 }); + await page.mouse.up(); + + let rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(2); + + // indent children blocks + await pressTab(page); + + const secondCoord = await getIndexCoordinate(page, [1, 2]); + await page.mouse.click(0, 0); + await page.mouse.move(secondCoord.x - 100, secondCoord.y - 10, { + steps: 20, + }); + await page.mouse.down(); + // โ† + await page.mouse.move(secondCoord.x + 100, secondCoord.y + 10, { steps: 20 }); + await page.mouse.up(); + + rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(1); +}); + +test('scroll should update dragging area and select blocks when dragging', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await initParagraphsByCount(page, 20); + + await page.mouse.click(0, 0); + // eslint-disable-next-line sonarjs/no-identical-functions + async function getViewportScrollTop() { + return page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + return viewport.scrollTop; + }); + } + + await page.mouse.wheel(0, -(await getViewportScrollTop()) * 2); + await page.waitForTimeout(250); + expect(await getViewportScrollTop()).toBe(0); + + const coord = await getIndexCoordinate(page, [1, 1]); + + await page.mouse.move(coord.x - 26 - 24, coord.y - 30, { steps: 40 }); + await waitNextFrame(page, 300); + await page.mouse.down(); + await waitNextFrame(page, 300); + await page.mouse.move(coord.x + 100, coord.y + 10, { steps: 40 }); + await waitNextFrame(page, 300); + + let rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(2); + + // scroll to end by wheel + const distanceToEnd = await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport')!; + return viewport.scrollHeight - viewport.clientHeight; + }); + await page.mouse.wheel(0, distanceToEnd * 2); + await page.waitForTimeout(250); + expect(await getViewportScrollTop()).toBe(distanceToEnd); + + await page.mouse.up(); + + rects = page.locator('affine-block-selection').locator('visible=true'); + await expect(rects).toHaveCount(3); +}); diff --git a/blocksuite/tests-legacy/selection/native.spec.ts b/blocksuite/tests-legacy/selection/native.spec.ts new file mode 100644 index 0000000000000..bd2a8163fa490 --- /dev/null +++ b/blocksuite/tests-legacy/selection/native.spec.ts @@ -0,0 +1,1788 @@ +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); +}); diff --git a/blocksuite/tests-legacy/slash-menu.spec.ts b/blocksuite/tests-legacy/slash-menu.spec.ts new file mode 100644 index 0000000000000..ec3569d27a79f --- /dev/null +++ b/blocksuite/tests-legacy/slash-menu.spec.ts @@ -0,0 +1,990 @@ +import { expect } from '@playwright/test'; + +import { addNote, switchEditorMode } from './utils/actions/edgeless.js'; +import { + pressArrowDown, + pressArrowLeft, + pressArrowRight, + pressArrowUp, + pressBackspace, + pressEnter, + pressEscape, + pressShiftEnter, + pressShiftTab, + pressTab, + redoByKeyboard, + SHORT_KEY, + type, + undoByKeyboard, +} from './utils/actions/keyboard.js'; +import { + captureHistory, + enterPlaygroundRoom, + focusRichText, + getInlineSelectionText, + getPageSnapshot, + getSelectionRect, + initEmptyEdgelessState, + initEmptyParagraphState, + insertThreeLevelLists, + waitNextFrame, +} from './utils/actions/misc.js'; +import { + assertAlmostEqual, + assertBlockCount, + assertExists, + assertRichTexts, + assertStoreMatchJSX, +} from './utils/asserts.js'; +import { test } from './utils/playwright.js'; + +test.describe('slash menu should show and hide correctly', () => { + test.beforeEach(async ({ page }) => { + await enterPlaygroundRoom(page); + }); + + test("slash menu should show when user input '/'", async ({ page }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + }); + + // Playwright dose not support IME + // https://github.com/microsoft/playwright/issues/5777 + test.skip("slash menu should show when user input 'ใ€'", async ({ page }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, 'ใ€'); + + await expect(slashMenu).toBeVisible(); + }); + + test('slash menu should hide after click away', async ({ page }) => { + const id = await initEmptyParagraphState(page); + const paragraphId = id.paragraphId; + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + // Click outside should close slash menu + await page.mouse.click(0, 50); + await expect(slashMenu).toBeHidden(); + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + }); + + test('slash menu should hide after input whitespace', async ({ page }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + await type(page, ' '); + await expect(slashMenu).toBeHidden(); + await assertRichTexts(page, ['/ ']); + await pressBackspace(page); + await expect(slashMenu).toBeVisible(); + + await type(page, 'head'); + await expect(slashMenu).toBeVisible(); + await type(page, ' '); + await expect(slashMenu).toBeHidden(); + await pressBackspace(page); + await expect(slashMenu).toBeVisible(); + }); + + test('delete the slash symbol should close the slash menu', async ({ + page, + }) => { + const id = await initEmptyParagraphState(page); + const paragraphId = id.paragraphId; + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + await pressBackspace(page); + await expect(slashMenu).toBeHidden(); + await assertStoreMatchJSX( + page, + ` +`, + paragraphId + ); + }); + + test('typing something that does not match should close the slash menu', async ({ + page, + }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + await type(page, '_'); + await expect(slashMenu).toBeHidden(); + await assertRichTexts(page, ['/_']); + + // And pressing backspace immediately should reappear the slash menu + await pressBackspace(page); + await expect(slashMenu).toBeVisible(); + + await type(page, '__'); + await pressBackspace(page); + await expect(slashMenu).toBeHidden(); + }); + + test('pressing the slash key again should close the old slash menu and open new one', async ({ + page, + }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + await expect(slashMenu).toHaveCount(1); + await assertRichTexts(page, ['//']); + }); + + test('should position slash menu correctly', async ({ page }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + const box = await slashMenu.boundingBox(); + if (!box) { + throw new Error("slashMenu doesn't exist"); + } + const rect = await getSelectionRect(page); + const { x, y } = box; + assertAlmostEqual(x - rect.x, 0, 10); + assertAlmostEqual(y - rect.bottom, 5, 10); + }); + + test('should move up down with arrow key', async ({ page }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + const slashItems = slashMenu.locator('icon-button'); + + await pressArrowDown(page); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.nth(1)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(1).locator('.text')).toHaveText(['Heading 1']); + await assertRichTexts(page, ['/']); + + await pressArrowUp(page); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.first()).toHaveAttribute('hover', 'true'); + await expect(slashItems.first().locator('.text')).toHaveText(['Text']); + await assertRichTexts(page, ['/']); + + await pressArrowUp(page); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.last()).toHaveAttribute('hover', 'true'); + await expect(slashItems.last().locator('.text')).toHaveText(['Delete']); + await assertRichTexts(page, ['/']); + + await pressArrowDown(page); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.first()).toHaveAttribute('hover', 'true'); + await expect(slashItems.first().locator('.text')).toHaveText(['Text']); + await assertRichTexts(page, ['/']); + }); + + test('slash menu hover state', async ({ page }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + const slashItems = slashMenu.locator('icon-button'); + + await pressArrowDown(page); + await expect(slashItems.nth(1)).toHaveAttribute('hover', 'true'); + + await pressArrowUp(page); + await expect(slashItems.nth(1)).toHaveAttribute('hover', 'false'); + await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true'); + + await pressArrowDown(page); + await pressArrowDown(page); + await expect(slashItems.nth(2)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(1)).toHaveAttribute('hover', 'false'); + await expect(slashItems.nth(0)).toHaveAttribute('hover', 'false'); + + await slashItems.nth(0).hover(); + await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(2)).toHaveAttribute('hover', 'false'); + await expect(slashItems.nth(1)).toHaveAttribute('hover', 'false'); + }); + + test('should open tooltip when hover on item', async ({ page }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '/'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + + const slashItems = slashMenu.locator('icon-button'); + const tooltip = page.locator('.affine-tooltip'); + + await slashItems.nth(0).hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip.locator('.tooltip-caption')).toHaveText(['Text']); + await page.mouse.move(0, 0); + await expect(tooltip).toBeHidden(); + + await slashItems.nth(1).hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip.locator('.tooltip-caption')).toHaveText([ + 'Heading #1', + ]); + await page.mouse.move(0, 0); + await expect(tooltip).toBeHidden(); + + await expect(slashItems.nth(4).locator('.text')).toHaveText([ + 'Other Headings', + ]); + await slashItems.nth(4).hover(); + await expect(tooltip).toBeHidden(); + }); + + test('press tab should move up and down', async ({ page }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + const slashItems = slashMenu.locator('icon-button'); + + await pressTab(page); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.nth(1)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(1).locator('.text')).toHaveText(['Heading 1']); + await assertRichTexts(page, ['/']); + + await pressShiftTab(page); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.first()).toHaveAttribute('hover', 'true'); + await expect(slashItems.first().locator('.text')).toHaveText(['Text']); + await assertRichTexts(page, ['/']); + + await pressShiftTab(page); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.last()).toHaveAttribute('hover', 'true'); + await expect(slashItems.last().locator('.text')).toHaveText(['Delete']); + await assertRichTexts(page, ['/']); + + await pressTab(page); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.first()).toHaveAttribute('hover', 'true'); + await expect(slashItems.first().locator('.text')).toHaveText(['Text']); + await assertRichTexts(page, ['/']); + }); + + test('should move up down with ctrl/cmd+n and ctrl/cmd+p', async ({ + page, + }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + const slashItems = slashMenu.locator('icon-button'); + + await page.keyboard.press(`${SHORT_KEY}+n`); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.nth(1)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(1).locator('.text')).toHaveText(['Heading 1']); + await assertRichTexts(page, ['/']); + + await page.keyboard.press(`${SHORT_KEY}+p`); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.first()).toHaveAttribute('hover', 'true'); + await expect(slashItems.first().locator('.text')).toHaveText(['Text']); + await assertRichTexts(page, ['/']); + + await page.keyboard.press(`${SHORT_KEY}+p`); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.last()).toHaveAttribute('hover', 'true'); + await expect(slashItems.last().locator('.text')).toHaveText(['Delete']); + await assertRichTexts(page, ['/']); + + await page.keyboard.press(`${SHORT_KEY}+n`); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.first()).toHaveAttribute('hover', 'true'); + await expect(slashItems.first().locator('.text')).toHaveText(['Text']); + await assertRichTexts(page, ['/']); + }); + + test('should open sub menu when hover on SubMenuItem', async ({ page }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '/'); + const slashMenu = page.locator('.slash-menu[data-testid=sub-menu-0]'); + await expect(slashMenu).toBeVisible(); + + const slashItems = slashMenu.locator('icon-button'); + + const subMenu = page.locator('.slash-menu[data-testid=sub-menu-1]'); + + let rect = await slashItems.nth(4).boundingBox(); + assertExists(rect); + await page.mouse.move(rect.x + 10, rect.y + 10); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.nth(4)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(4).locator('.text')).toHaveText([ + 'Other Headings', + ]); + await expect(subMenu).toBeVisible(); + + rect = await slashItems.nth(3).boundingBox(); + assertExists(rect); + await page.mouse.move(rect.x + 10, rect.y + 10); + await expect(slashMenu).toBeVisible(); + await expect(slashItems.nth(3)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(3).locator('.text')).toHaveText(['Heading 3']); + await expect(subMenu).toBeHidden(); + }); + + test('should open and close menu when using left right arrow, Enter, Esc keys', async ({ + page, + }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + + const slashMenu = page.locator('.slash-menu[data-testid=sub-menu-0]'); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + await pressEscape(page); + await expect(slashMenu).toBeHidden(); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + await pressArrowLeft(page); + await expect(slashMenu).toBeHidden(); + + // Test sub menu case + const slashItems = slashMenu.locator('icon-button'); + + await type(page, '/'); + await pressArrowDown(page, 4); + await expect(slashItems.nth(4)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(4).locator('.text')).toHaveText([ + 'Other Headings', + ]); + + const subMenu = page.locator('.slash-menu[data-testid=sub-menu-1]'); + + await pressArrowRight(page); + await expect(slashMenu).toBeVisible(); + await expect(subMenu).toBeVisible(); + + await pressArrowLeft(page); + await expect(slashMenu).toBeVisible(); + await expect(subMenu).toBeHidden(); + + await pressEnter(page); + await expect(slashMenu).toBeVisible(); + await expect(subMenu).toBeVisible(); + + await pressEscape(page); + await expect(slashMenu).toBeVisible(); + await expect(subMenu).toBeHidden(); + }); + + test('show close current all submenu when typing', async ({ page }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + + const slashMenu = page.locator('.slash-menu[data-testid=sub-menu-0]'); + const subMenu = page.locator('.slash-menu[data-testid=sub-menu-1]'); + const slashItems = slashMenu.locator('icon-button'); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + await pressArrowDown(page, 4); + await expect(slashItems.nth(4)).toHaveAttribute('hover', 'true'); + await expect(slashItems.nth(4).locator('.text')).toHaveText([ + 'Other Headings', + ]); + await pressEnter(page); + await expect(subMenu).toBeVisible(); + + await type(page, 'h'); + await expect(subMenu).toBeHidden(); + }); + + test('should allow only pressing modifier key', async ({ page }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + + const slashMenu = page.locator(`.slash-menu`); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + await page.keyboard.press(SHORT_KEY); + await expect(slashMenu).toBeVisible(); + + await page.keyboard.press('Shift'); + await expect(slashMenu).toBeVisible(); + }); + + test('should allow other hotkey to passthrough', async ({ page }) => { + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + + const slashMenu = page.locator(`.slash-menu`); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + await page.keyboard.press(`${SHORT_KEY}+a`); + await expect(slashMenu).toBeHidden(); + await assertRichTexts(page, ['hello', 'world/']); + + const selected = await getInlineSelectionText(page); + expect(selected).toBe('world/'); + }); + + test('can input search input after click menu', async ({ page }) => { + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + await focusRichText(page); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + const box = await slashMenu.boundingBox(); + if (!box) { + throw new Error("slashMenu doesn't exist"); + } + const { x, y } = box; + await page.mouse.click(x + 10, y + 10); + await expect(slashMenu).toBeVisible(); + await type(page, 'a'); + await assertRichTexts(page, ['/a']); + }); +}); + +test.describe('slash menu should not be shown in ignored blocks', () => { + test('code block', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '```'); + await pressEnter(page); + await type(page, '/'); + await expect(page.locator('.slash-menu')).toBeHidden(); + }); +}); + +test('should slash menu works with fast type', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'a/text', 0); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); +}); + +test('should clean slash string after soft enter', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/toeverything/blocksuite/issues/1126', + }); + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, 'hello'); + await pressShiftEnter(page); + await waitNextFrame(page); + await type(page, '/copy'); + await pressEnter(page); + + await assertStoreMatchJSX( + page, + ` + `, + paragraphId + ); +}); + +test.describe('slash search', () => { + test('should slash menu search and keyboard works', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + const slashMenu = page.locator(`.slash-menu`); + const slashItems = slashMenu.locator('icon-button'); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + // search should active the first item + await type(page, 'co'); + await expect(slashItems).toHaveCount(3); + await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']); + await expect(slashItems.nth(1).locator('.text')).toHaveText(['Code Block']); + await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true'); + + await type(page, 'p'); + await expect(slashItems).toHaveCount(1); + await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']); + + // assert backspace works + await pressBackspace(page); + await expect(slashItems).toHaveCount(3); + await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']); + await expect(slashItems.nth(1).locator('.text')).toHaveText(['Code Block']); + await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true'); + }); + + test('slash menu supports fuzzy search', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + const slashMenu = page.locator(`.slash-menu`); + const slashItems = slashMenu.locator('icon-button'); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + await type(page, 'c'); + await expect(slashItems).toHaveCount(8); + await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']); + await expect(slashItems.nth(1).locator('.text')).toHaveText(['Italic']); + await expect(slashItems.nth(2).locator('.text')).toHaveText(['New Doc']); + await expect(slashItems.nth(3).locator('.text')).toHaveText(['Duplicate']); + await expect(slashItems.nth(4).locator('.text')).toHaveText(['Code Block']); + await expect(slashItems.nth(5).locator('.text')).toHaveText(['Linked Doc']); + await expect(slashItems.nth(6).locator('.text')).toHaveText(['Attachment']); + await type(page, 'b'); + await expect(slashItems.nth(0).locator('.text')).toHaveText(['Code Block']); + }); + + test('slash menu supports alias search', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '/'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + + const slashItems = slashMenu.locator('icon-button'); + await type(page, 'database'); + await expect(slashItems).toHaveCount(2); + await expect(slashItems.nth(0).locator('.text')).toHaveText(['Table View']); + await expect(slashItems.nth(1).locator('.text')).toHaveText([ + 'Kanban View', + ]); + await type(page, 'v'); + await expect(slashItems).toHaveCount(0); + }); +}); + +test('should focus on code blocks created by the slash menu', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + await type(page, '000'); + + await type(page, '/code'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + + const codeBlock = page.getByTestId('Code Block'); + await codeBlock.click(); + await expect(slashMenu).toBeHidden(); + + await focusRichText(page); // FIXME: flaky selection asserter + await type(page, '111'); + await assertRichTexts(page, ['000111']); +}); + +// Selection is not yet available in edgeless +test('slash menu should work in edgeless mode', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + + await addNote(page, '/', 30, 40); + await assertRichTexts(page, ['', '/']); + + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); +}); + +test.describe('slash menu with date & time', () => { + test("should insert Today's time string", async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '/'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + + const todayBlock = page.getByTestId('Today'); + await todayBlock.click(); + await expect(slashMenu).toBeHidden(); + + const date = new Date(); + const strTime = date.toISOString().split('T')[0]; + + await assertRichTexts(page, [strTime]); + }); + + test("should create Tomorrow's time string", async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '/'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + + const todayBlock = page.getByTestId('Tomorrow'); + await todayBlock.click(); + await expect(slashMenu).toBeHidden(); + + const date = new Date(); + date.setDate(date.getDate() + 1); + const strTime = date.toISOString().split('T')[0]; + + await assertRichTexts(page, [strTime]); + }); + + test("should insert Yesterday's time string", async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '/'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + + const todayBlock = page.getByTestId('Yesterday'); + await todayBlock.click(); + await expect(slashMenu).toBeHidden(); + + const date = new Date(); + date.setDate(date.getDate() - 1); + const strTime = date.toISOString().split('T')[0]; + + await assertRichTexts(page, [strTime]); + }); +}); + +test.describe('slash menu with style', () => { + test('should style text line works', async ({ page }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, 'hello/'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + const bold = page.getByTestId('Bold'); + await bold.click(); + await assertStoreMatchJSX( + page, + ` + + + + } + prop:type="text" +/>`, + paragraphId + ); + }); + + test('should style empty line works', async ({ page }) => { + await enterPlaygroundRoom(page); + const { paragraphId } = await initEmptyParagraphState(page); + await focusRichText(page); + + await type(page, '/'); + const slashMenu = page.locator(`.slash-menu`); + await expect(slashMenu).toBeVisible(); + const bold = page.getByTestId('Bold'); + await bold.click(); + await page.waitForTimeout(50); + await type(page, 'hello'); + await assertStoreMatchJSX( + page, + ` + + + + } + prop:type="text" +/>`, + paragraphId + ); + }); +}); + +test('should insert database', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + await assertBlockCount(page, 'paragraph', 1); + await type(page, '/'); + const tableBlock = page.getByTestId('Table View'); + await tableBlock.click(); + await assertBlockCount(page, 'paragraph', 0); + await assertBlockCount(page, 'database', 1); + + const database = page.locator('affine-database'); + await expect(database).toBeVisible(); + const tagColumn = page.locator('.affine-database-column').nth(1); + expect(await tagColumn.innerText()).toBe('Status'); + const defaultRows = page.locator('.affine-database-block-row'); + expect(await defaultRows.count()).toBe(4); +}); + +test.describe('slash menu with customize menu', () => { + test('can remove specified menus', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await page.evaluate(async () => { + // https://github.com/lit/lit/blob/84df6ef8c73fffec92384891b4b031d7efc01a64/packages/lit-html/src/static.ts#L93 + const fakeLiteral = (strings: TemplateStringsArray) => + ({ + ['_$litStatic$']: strings[0], + r: Symbol.for(''), + }) as const; + + const editor = document.querySelector('affine-editor-container'); + if (!editor) throw new Error("Can't find affine-editor-container"); + + const SlashMenuWidget = window.$blocksuite.blocks.AffineSlashMenuWidget; + class CustomSlashMenu extends SlashMenuWidget { + override config = { + ...SlashMenuWidget.DEFAULT_CONFIG, + items: [ + { groupName: 'custom-group' }, + ...SlashMenuWidget.DEFAULT_CONFIG.items + .filter(item => 'action' in item) + .slice(0, 5), + ], + }; + } + // Fix `Illegal constructor` error + // see https://stackoverflow.com/questions/41521812/illegal-constructor-with-ecmascript-6 + customElements.define('affine-custom-slash-menu', CustomSlashMenu); + + const pageSpecs = window.$blocksuite.blocks.PageEditorBlockSpecs; + editor.pageSpecs = [ + ...pageSpecs, + { + setup: di => { + di.override( + window.$blocksuite.identifiers.WidgetViewMapIdentifier( + 'affine:page' + ), + // @ts-ignore + () => ({ + 'affine-slash-menu-widget': fakeLiteral`affine-custom-slash-menu`, + }) + ); + }, + }, + ]; + await editor.updateComplete; + }); + + await focusRichText(page); + + const slashMenu = page.locator(`.slash-menu`); + const slashItems = slashMenu.locator('icon-button'); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + await expect(slashItems).toHaveCount(5); + }); + + test('can add some menus', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await page.evaluate(async () => { + // https://github.com/lit/lit/blob/84df6ef8c73fffec92384891b4b031d7efc01a64/packages/lit-html/src/static.ts#L93 + // eslint-disable-next-line sonarjs/no-identical-functions + const fakeLiteral = (strings: TemplateStringsArray) => + ({ + ['_$litStatic$']: strings[0], + r: Symbol.for(''), + }) as const; + + const editor = document.querySelector('affine-editor-container'); + if (!editor) throw new Error("Can't find affine-editor-container"); + const SlashMenuWidget = window.$blocksuite.blocks.AffineSlashMenuWidget; + + class CustomSlashMenu extends SlashMenuWidget { + config = { + ...SlashMenuWidget.DEFAULT_CONFIG, + items: [ + { groupName: 'Custom Menu' }, + { + name: 'Custom Menu Item', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + icon: '' as any, + action: () => { + // do nothing + }, + }, + { + name: 'Custom Menu Item', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + icon: '' as any, + action: () => { + // do nothing + }, + showWhen: () => false, + }, + ], + }; + } + // Fix `Illegal constructor` error + // see https://stackoverflow.com/questions/41521812/illegal-constructor-with-ecmascript-6 + customElements.define('affine-custom-slash-menu', CustomSlashMenu); + + const pageSpecs = window.$blocksuite.blocks.PageEditorBlockSpecs; + editor.pageSpecs = [ + ...pageSpecs, + { + setup: di => + di.override( + window.$blocksuite.identifiers.WidgetViewMapIdentifier( + 'affine:page' + ), + // @ts-ignore + () => ({ + 'affine-slash-menu-widget': fakeLiteral`affine-custom-slash-menu`, + }) + ), + }, + ]; + await editor.updateComplete; + }); + + await focusRichText(page); + + const slashMenu = page.locator(`.slash-menu`); + const slashItems = slashMenu.locator('icon-button'); + + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + await expect(slashItems).toHaveCount(1); + }); +}); + +test('move block up and down by slash menu', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const slashMenu = page.locator(`.slash-menu`); + + await focusRichText(page); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + await assertRichTexts(page, ['hello', 'world']); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + const moveUp = page.getByTestId('Move Up'); + await moveUp.click(); + await assertRichTexts(page, ['world', 'hello']); + await type(page, '/'); + await expect(slashMenu).toBeVisible(); + + const moveDown = page.getByTestId('Move Down'); + await moveDown.click(); + await assertRichTexts(page, ['hello', 'world']); +}); + +test('delete block by slash menu should remove children', async ({ + page, +}, testInfo) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await insertThreeLevelLists(page); + const slashMenu = page.locator(`.slash-menu`); + const slashItems = slashMenu.locator('icon-button'); + + await captureHistory(page); + await focusRichText(page, 1); + await waitNextFrame(page); + await type(page, '/'); + + await expect(slashMenu).toBeVisible(); + await type(page, 'remove'); + await expect(slashItems).toHaveCount(1); + await pressEnter(page); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}.json` + ); + + await undoByKeyboard(page); + await assertRichTexts(page, ['123', '456', '789']); + await redoByKeyboard(page); + await assertRichTexts(page, ['123']); +}); diff --git a/blocksuite/tests-legacy/snapshots/basic.spec.ts/automatic-identify-url-text-final.json b/blocksuite/tests-legacy/snapshots/basic.spec.ts/automatic-identify-url-text-final.json new file mode 100644 index 0000000000000..10d8e45e23e25 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/basic.spec.ts/automatic-identify-url-text-final.json @@ -0,0 +1,66 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "abc " + }, + { + "insert": "https://google.com", + "attributes": { + "link": "https://google.com" + } + }, + { + "insert": " " + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/basic.spec.ts/basic-test-default.json b/blocksuite/tests-legacy/snapshots/basic.spec.ts/basic-test-default.json new file mode 100644 index 0000000000000..d902db61ddad6 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/basic.spec.ts/basic-test-default.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-bookmark-url-by-copy-button-final.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-bookmark-url-by-copy-button-final.json new file mode 100644 index 0000000000000..b5c3f610836b1 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-bookmark-url-by-copy-button-final.json @@ -0,0 +1,80 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "http://localhost", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0 + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "http://localhost", + "attributes": { + "link": "http://localhost" + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-edgeless-mode-final.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-edgeless-mode-final.json new file mode 100644 index 0000000000000..972fa40850092 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-edgeless-mode-final.json @@ -0,0 +1,87 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,234]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "http://localhost" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "http://localhost", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0 + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-page-mode-final.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-page-mode-final.json new file mode 100644 index 0000000000000..b90751b9e75af --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-page-mode-final.json @@ -0,0 +1,77 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "http://localhost" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "http://localhost", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0 + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/covert-bookmark-block-to-link-text-final.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/covert-bookmark-block-to-link-text-final.json new file mode 100644 index 0000000000000..0d09533b20a88 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/covert-bookmark-block-to-link-text-final.json @@ -0,0 +1,60 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "http://localhost", + "attributes": { + "link": "http://localhost" + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/create-bookmark-by-slash-menu-final.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/create-bookmark-by-slash-menu-final.json new file mode 100644 index 0000000000000..cba48a995627f --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/create-bookmark-by-slash-menu-final.json @@ -0,0 +1,58 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "http://localhost", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0 + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/embed-figma.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/embed-figma.json new file mode 100644 index 0000000000000..a1cf2bc7e3462 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/embed-figma.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "*", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "*", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "*", + "flavour": "affine:embed-figma", + "version": 1, + "props": { + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0, + "style": "figma", + "url": "https://www.figma.com/design/JuXs6uOAICwf4I4tps0xKZ123", + "caption": null, + "title": "Figma", + "description": "https://www.figma.com/design/JuXs6uOAICwf4I4tps0xKZ123" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/embed-youtube.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/embed-youtube.json new file mode 100644 index 0000000000000..fa4138915dd8c --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/embed-youtube.json @@ -0,0 +1,61 @@ +{ + "type": "block", + "id": "*", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "*", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "*", + "flavour": "affine:embed-youtube", + "version": 1, + "props": { + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0, + "style": "video", + "url": "https://www.youtube.com/watch?v=fakeid", + "caption": null, + "image": null, + "title": null, + "description": null, + "creator": null, + "creatorUrl": null, + "creatorImage": null, + "videoId": "fakeid" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/horizontal-figma.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/horizontal-figma.json new file mode 100644 index 0000000000000..9a3c75eb53015 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/horizontal-figma.json @@ -0,0 +1,58 @@ +{ + "type": "block", + "id": "*", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "*", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "*", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "https://www.figma.com/design/JuXs6uOAICwf4I4tps0xKZ123", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0 + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/horizontal-youtube.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/horizontal-youtube.json new file mode 100644 index 0000000000000..f58738152c7b4 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/horizontal-youtube.json @@ -0,0 +1,58 @@ +{ + "type": "block", + "id": "*", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "*", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "*", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "https://www.youtube.com/watch?v=fakeid", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0 + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-add-paragraph.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-add-paragraph.json new file mode 100644 index 0000000000000..a89a9ca431909 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-add-paragraph.json @@ -0,0 +1,113 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "http://localhost", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "rotate": 0 + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "111" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "222" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "333" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-drag.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-drag.json new file mode 100644 index 0000000000000..c9b458d4395a9 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-drag.json @@ -0,0 +1,113 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "111" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "222" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "http://localhost", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "rotate": 0 + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "333" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-init.json b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-init.json new file mode 100644 index 0000000000000..d2090527c2176 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-init.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:bookmark", + "version": 1, + "props": { + "style": "horizontal", + "url": "http://localhost", + "caption": null, + "description": null, + "icon": null, + "image": null, + "title": null, + "index": "a0", + "xywh": "[0,0,0,0]", + "rotate": 0 + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/auto-identify-url-final.json b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/auto-identify-url-final.json new file mode 100644 index 0000000000000..c9e9f94e83414 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/auto-identify-url-final.json @@ -0,0 +1,63 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "test " + }, + { + "insert": "https://www.google.com", + "attributes": { + "link": "https://www.google.com" + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.html b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.html new file mode 100644 index 0000000000000..132277ee2df88 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.html @@ -0,0 +1,9 @@ +
    +

    bc

    +
    +
    +

    d

    +
    +
    +
    +
    \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.json b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.json new file mode 100644 index 0000000000000..ebac99dbab456 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.json @@ -0,0 +1,49 @@ +[ + { + "type": "block", + "id": "", + "flavour": "affine:note", + "props": {}, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "bc" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "d" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.md b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.md new file mode 100644 index 0000000000000..6833e8ae1cbd5 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.md @@ -0,0 +1,2 @@ +bc +d \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.html b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.html new file mode 100644 index 0000000000000..007c36cf41c15 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.html @@ -0,0 +1,8 @@ +
    +

    hi

    +
    +
    +
    +

    j

    +
    +
    \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.json b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.json new file mode 100644 index 0000000000000..76860336d5b61 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.json @@ -0,0 +1,48 @@ +[ + { + "type": "block", + "id": "", + "flavour": "affine:note", + "props": {}, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hi" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "j" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } +] \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.md b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.md new file mode 100644 index 0000000000000..02fa1f0285acf --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.md @@ -0,0 +1,2 @@ +hi +j \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.html b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.html new file mode 100644 index 0000000000000..b74b581cefa8d --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.html @@ -0,0 +1,11 @@ +
      +
    • aaa +
        +
      • bbb +
          +
        • ccc
        • +
        +
      • +
      +
    • +
    \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.json b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.json new file mode 100644 index 0000000000000..43b78e52be343 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.json @@ -0,0 +1,75 @@ +[ + { + "type": "block", + "id": "", + "flavour": "affine:note", + "props": {}, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "bbb" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ccc" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.md b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.md new file mode 100644 index 0000000000000..8cc58960380eb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.md @@ -0,0 +1,3 @@ +aaa +bbb +ccc \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/cut-will-delete-all-content-and-copy-will-reappear-content-after-cut.json b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/cut-will-delete-all-content-and-copy-will-reappear-content-after-cut.json new file mode 100644 index 0000000000000..b4999d04e2bd6 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/cut-will-delete-all-content-and-copy-will-reappear-content-after-cut.json @@ -0,0 +1,55 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/cut-will-delete-all-content-and-copy-will-reappear-content-after-paste.json b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/cut-will-delete-all-content-and-copy-will-reappear-content-after-paste.json new file mode 100644 index 0000000000000..5adfa07699158 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/cut-will-delete-all-content-and-copy-will-reappear-content-after-paste.json @@ -0,0 +1,123 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "7", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "1" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "2" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "9", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "3" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "10", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "4" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/should-keep-paragraph-block-s-type-when-pasting-at-the-start-of-empty-paragraph-block-except-type-text-after-paste-1.json b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/should-keep-paragraph-block-s-type-when-pasting-at-the-start-of-empty-paragraph-block-except-type-text-after-paste-1.json new file mode 100644 index 0000000000000..e6f186417e971 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/should-keep-paragraph-block-s-type-when-pasting-at-the-start-of-empty-paragraph-block-except-type-text-after-paste-1.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "quote", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/should-keep-paragraph-block-s-type-when-pasting-at-the-start-of-empty-paragraph-block-except-type-text-after-paste-2.json b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/should-keep-paragraph-block-s-type-when-pasting-at-the-start-of-empty-paragraph-block-except-type-text-after-paste-2.json new file mode 100644 index 0000000000000..3f8d89f149f21 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/clipboard/list.spec.ts/should-keep-paragraph-block-s-type-when-pasting-at-the-start-of-empty-paragraph-block-except-type-text-after-paste-2.json @@ -0,0 +1,76 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "quote", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/copy-paste.spec.ts/code-block-has-content-click-code-block-copy-menu-copy-whole-code-block-pasted.json b/blocksuite/tests-legacy/snapshots/code/copy-paste.spec.ts/code-block-has-content-click-code-block-copy-menu-copy-whole-code-block-pasted.json new file mode 100644 index 0000000000000..2140c127eeb28 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/copy-paste.spec.ts/code-block-has-content-click-code-block-copy-menu-copy-whole-code-block-pasted.json @@ -0,0 +1,78 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "use" + } + ] + }, + "language": "javascript", + "wrap": false, + "caption": "" + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "use" + } + ] + }, + "language": "javascript", + "wrap": false, + "caption": "" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/copy-paste.spec.ts/code-block-is-empty-click-code-block-copy-menu-copy-the-empty-code-block-pasted.json b/blocksuite/tests-legacy/snapshots/code/copy-paste.spec.ts/code-block-is-empty-click-code-block-copy-menu-copy-the-empty-code-block-pasted.json new file mode 100644 index 0000000000000..0ce43f6161599 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/copy-paste.spec.ts/code-block-is-empty-click-code-block-copy-menu-copy-the-empty-code-block-pasted.json @@ -0,0 +1,70 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "language": "javascript", + "wrap": false, + "caption": "" + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "language": "javascript", + "wrap": false, + "caption": "" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/delete-code-block-in-more-menu-final.json b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/delete-code-block-in-more-menu-final.json new file mode 100644 index 0000000000000..5db1d16b72d2a --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/delete-code-block-in-more-menu-final.json @@ -0,0 +1,37 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/duplicate-code-block-final.json b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/duplicate-code-block-final.json new file mode 100644 index 0000000000000..6a79a3a76e98e --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/duplicate-code-block-final.json @@ -0,0 +1,78 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "let a: u8 = 7" + } + ] + }, + "language": "rust", + "wrap": true, + "caption": "BlockSuite" + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "let a: u8 = 7" + } + ] + }, + "language": "rust", + "wrap": true, + "caption": "BlockSuite" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-format.json b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-format.json new file mode 100644 index 0000000000000..41b96fa369f8d --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-format.json @@ -0,0 +1,85 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "c" + }, + { + "insert": "o", + "attributes": { + "bold": true + } + }, + { + "insert": "ns" + }, + { + "insert": "t a", + "attributes": { + "bold": true + } + }, + { + "insert": "a" + }, + { + "insert": "a = 1000", + "attributes": { + "bold": true + } + }, + { + "insert": ";" + } + ] + }, + "language": "typescript", + "wrap": false, + "caption": "" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-init.json b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-init.json new file mode 100644 index 0000000000000..63f501b5eafd9 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-init.json @@ -0,0 +1,58 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "const aaa = 1000;" + } + ] + }, + "language": "typescript", + "wrap": false, + "caption": "" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-link.json b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-link.json new file mode 100644 index 0000000000000..e2a658224e5e7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/format-text-in-code-block-link.json @@ -0,0 +1,96 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "c" + }, + { + "insert": "o", + "attributes": { + "bold": true + } + }, + { + "insert": "ns" + }, + { + "insert": "t a", + "attributes": { + "link": "https://www.baidu.com", + "bold": true + } + }, + { + "insert": "a", + "attributes": { + "link": "https://www.baidu.com" + } + }, + { + "insert": "a ", + "attributes": { + "link": "https://www.baidu.com", + "bold": true + } + }, + { + "insert": "= 1000", + "attributes": { + "bold": true + } + }, + { + "insert": ";" + } + ] + }, + "language": "typescript", + "wrap": false, + "caption": "" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/use-markdown-syntax-can-create-code-block-init.json b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/use-markdown-syntax-can-create-code-block-init.json new file mode 100644 index 0000000000000..5c6d65e23cd0d --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/use-markdown-syntax-can-create-code-block-init.json @@ -0,0 +1,97 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "bbb" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ccc" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/use-markdown-syntax-can-create-code-block-markdown-syntax.json b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/use-markdown-syntax-can-create-code-block-markdown-syntax.json new file mode 100644 index 0000000000000..de7599d6d0817 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/code/crud.spec.ts/use-markdown-syntax-can-create-code-block-markdown-syntax.json @@ -0,0 +1,112 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "language": null, + "wrap": false, + "caption": "" + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "bbb" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ccc" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-3-4.json b/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-3-4.json new file mode 100644 index 0000000000000..f05e973912a2e --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-3-4.json @@ -0,0 +1,187 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "C" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "D" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "E" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "F" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "9", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "G" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "B" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "A" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-3-9.json b/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-3-9.json new file mode 100644 index 0000000000000..8aa721ad61799 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-3-9.json @@ -0,0 +1,187 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "B" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "C" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "D" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "E" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "F" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "9", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "G" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "A" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-4-3.json b/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-4-3.json new file mode 100644 index 0000000000000..822a308be925b --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-drag-4-3.json @@ -0,0 +1,187 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "C" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "D" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "E" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "F" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "9", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "G" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "A" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "B" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-init.json b/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-init.json new file mode 100644 index 0000000000000..51622ddeb6d78 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/drag.spec.ts/move-to-the-last-block-of-each-level-in-multi-level-nesting-init.json @@ -0,0 +1,187 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "A" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "B" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "C" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "D" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "E" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "F" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "9", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "G" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/drag.spec.ts/should-be-able-to-drag-drop-multiple-blocks-to-nested-block-finial.json b/blocksuite/tests-legacy/snapshots/drag.spec.ts/should-be-able-to-drag-drop-multiple-blocks-to-nested-block-finial.json new file mode 100644 index 0000000000000..faedc471497ba --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/drag.spec.ts/should-be-able-to-drag-drop-multiple-blocks-to-nested-block-finial.json @@ -0,0 +1,186 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "C" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "D" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "E" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "F" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "A" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "B" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "9", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "G" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/drag.spec.ts/should-be-able-to-drag-drop-multiple-blocks-to-nested-block-init.json b/blocksuite/tests-legacy/snapshots/drag.spec.ts/should-be-able-to-drag-drop-multiple-blocks-to-nested-block-init.json new file mode 100644 index 0000000000000..e04d86f006d36 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/drag.spec.ts/should-be-able-to-drag-drop-multiple-blocks-to-nested-block-init.json @@ -0,0 +1,186 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "A" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "B" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "C" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "D" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "E" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "F" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "9", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "G" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-add-linked-doc.json b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-add-linked-doc.json new file mode 100644 index 0000000000000..136df9a3faa74 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-add-linked-doc.json @@ -0,0 +1,110 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:edgeless-text", + "version": 1, + "props": { + "xywh": "[-25,-25,88.75,50]", + "index": "a1", + "lockedBySelf": false, + "color": "--affine-palette-line-blue", + "fontFamily": "blocksuite:surface:Inter", + "fontStyle": "normal", + "fontWeight": "400", + "textAlign": "left", + "scale": 1, + "rotate": 0, + "hasMaxWidth": false + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": " ", + "attributes": { + "reference": { + "type": "LinkedPage", + "pageId": "6" + } + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-drag.json b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-drag.json new file mode 100644 index 0000000000000..26b0b16c696ed --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-drag.json @@ -0,0 +1,101 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:edgeless-text", + "version": 1, + "props": { + "xywh": "[-25,-25,497,154]", + "index": "a1", + "lockedBySelf": false, + "color": "--affine-palette-line-blue", + "fontFamily": "blocksuite:surface:Inter", + "fontStyle": "normal", + "fontWeight": "400", + "textAlign": "left", + "scale": 1, + "rotate": 0, + "hasMaxWidth": true + }, + "children": [ + { + "type": "block", + "id": "11", + "flavour": "affine:embed-linked-doc", + "version": 1, + "props": { + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0, + "pageId": "6", + "style": "horizontal", + "caption": null + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-init.json b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-init.json new file mode 100644 index 0000000000000..38bceda452dea --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-init.json @@ -0,0 +1,100 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:edgeless-text", + "version": 1, + "props": { + "xywh": "[-25,-25,50,26]", + "index": "a1", + "lockedBySelf": false, + "color": "--affine-palette-line-blue", + "fontFamily": "blocksuite:surface:Inter", + "fontStyle": "normal", + "fontWeight": "400", + "textAlign": "left", + "scale": 1, + "rotate": 0, + "hasMaxWidth": false + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card-min-width.json b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card-min-width.json new file mode 100644 index 0000000000000..ad9c08c538bc1 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card-min-width.json @@ -0,0 +1,101 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:edgeless-text", + "version": 1, + "props": { + "xywh": "[-25,-25,452,154]", + "index": "a1", + "lockedBySelf": false, + "color": "--affine-palette-line-blue", + "fontFamily": "blocksuite:surface:Inter", + "fontStyle": "normal", + "fontWeight": "400", + "textAlign": "left", + "scale": 1, + "rotate": 0, + "hasMaxWidth": true + }, + "children": [ + { + "type": "block", + "id": "11", + "flavour": "affine:embed-linked-doc", + "version": 1, + "props": { + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0, + "pageId": "6", + "style": "horizontal", + "caption": null + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card.json b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card.json new file mode 100644 index 0000000000000..2915f2d3ea10c --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card.json @@ -0,0 +1,101 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:edgeless-text", + "version": 1, + "props": { + "xywh": "[-25,-25,452,154]", + "index": "a1", + "lockedBySelf": false, + "color": "--affine-palette-line-blue", + "fontFamily": "blocksuite:surface:Inter", + "fontStyle": "normal", + "fontWeight": "400", + "textAlign": "left", + "scale": 1, + "rotate": 0, + "hasMaxWidth": false + }, + "children": [ + { + "type": "block", + "id": "11", + "flavour": "affine:embed-linked-doc", + "version": 1, + "props": { + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0, + "pageId": "6", + "style": "horizontal", + "caption": null + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-finial.json b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-finial.json new file mode 100644 index 0000000000000..8696508b7b647 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-finial.json @@ -0,0 +1,108 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "bbb" + } + ] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:edgeless-text", + "version": 1, + "props": { + "xywh": "[-25,-25,50,26]", + "index": "a1", + "lockedBySelf": false, + "color": "--affine-palette-line-blue", + "fontFamily": "blocksuite:surface:Inter", + "fontStyle": "normal", + "fontWeight": "400", + "textAlign": "left", + "scale": 1, + "rotate": 0, + "hasMaxWidth": true + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,48]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-note-empty.json b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-note-empty.json new file mode 100644 index 0000000000000..b42fa3f133c7b --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-note-empty.json @@ -0,0 +1,88 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:edgeless-text", + "version": 1, + "props": { + "xywh": "[-25,-25,50,26]", + "index": "a1", + "lockedBySelf": false, + "color": "--affine-palette-line-blue", + "fontFamily": "blocksuite:surface:Inter", + "fontStyle": "normal", + "fontWeight": "400", + "textAlign": "left", + "scale": 1, + "rotate": 0, + "hasMaxWidth": true + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,48]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-note-not-empty.json b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-note-not-empty.json new file mode 100644 index 0000000000000..f79cfb43bb51f --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/edgeless/edgeless-text.spec.ts/press-backspace-at-the-start-of-first-line-when-edgeless-text-exist-note-not-empty.json @@ -0,0 +1,104 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:surface", + "version": 5, + "props": { + "elements": {} + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:edgeless-text", + "version": 1, + "props": { + "xywh": "[-25,-25,50,26]", + "index": "a1", + "lockedBySelf": false, + "color": "--affine-palette-line-blue", + "fontFamily": "blocksuite:surface:Inter", + "fontStyle": "normal", + "fontWeight": "400", + "textAlign": "left", + "scale": 1, + "rotate": 0, + "hasMaxWidth": true + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,48]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/create-linked-doc-from-block-selection-with-format-bar.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/create-linked-doc-from-block-selection-with-format-bar.json new file mode 100644 index 0000000000000..f1f55038c6557 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/create-linked-doc-from-block-selection-with-format-bar.json @@ -0,0 +1,54 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "11", + "flavour": "affine:embed-linked-doc", + "version": 1, + "props": { + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0, + "pageId": "5", + "style": "horizontal", + "caption": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-default-color.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-default-color.json new file mode 100644 index 0000000000000..35d5c4f9fc1cf --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-default-color.json @@ -0,0 +1,98 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "color": "var(--affine-text-highlight-foreground-red)" + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-init.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-init.json new file mode 100644 index 0000000000000..35d5c4f9fc1cf --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-init.json @@ -0,0 +1,98 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "color": "var(--affine-text-highlight-foreground-red)" + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-select-all.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-select-all.json new file mode 100644 index 0000000000000..821396ae94742 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-background-color-select-all.json @@ -0,0 +1,101 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123", + "attributes": { + "color": "var(--affine-text-highlight-foreground-red)" + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "color": "var(--affine-text-highlight-foreground-red)" + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-bulleted.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-bulleted.json new file mode 100644 index 0000000000000..3310a6113fe82 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-bulleted.json @@ -0,0 +1,97 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-finial.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-finial.json new file mode 100644 index 0000000000000..f7b8f9535280b --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-finial.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-init.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-init.json new file mode 100644 index 0000000000000..b8e6d44b34fc5 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-change-to-heading-paragraph-type-init.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h1", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-finial.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-finial.json new file mode 100644 index 0000000000000..1ccc87a0d2d8a --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-finial.json @@ -0,0 +1,99 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "strike": true, + "italic": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-init.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-init.json new file mode 100644 index 0000000000000..ba6bf3c7bd7e4 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-init.json @@ -0,0 +1,102 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "code": true, + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-when-select-multiple-line-finial.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-when-select-multiple-line-finial.json new file mode 100644 index 0000000000000..aea12cd56eeb7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-when-select-multiple-line-finial.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-when-select-multiple-line-init.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-when-select-multiple-line-init.json new file mode 100644 index 0000000000000..359a657870028 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-format-text-when-select-multiple-line-init.json @@ -0,0 +1,104 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-link-text-finial.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-link-text-finial.json new file mode 100644 index 0000000000000..aea12cd56eeb7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-link-text-finial.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-link-text-init.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-link-text-init.json new file mode 100644 index 0000000000000..425e623f2c8fd --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-be-able-to-link-text-init.json @@ -0,0 +1,98 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "link": "https://www.example.com" + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-show-after-convert-to-code-block.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-show-after-convert-to-code-block.json new file mode 100644 index 0000000000000..37b954d36c422 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-show-after-convert-to-code-block.json @@ -0,0 +1,58 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:code", + "version": 1, + "props": { + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123\n456\n789" + } + ] + }, + "language": null, + "wrap": false, + "caption": "" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-with-block-selection-works-when-update-block-type-final.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-with-block-selection-works-when-update-block-type-final.json new file mode 100644 index 0000000000000..e851e80ea9b07 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-with-block-selection-works-when-update-block-type-final.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h1", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "9", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h1", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "10", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h1", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-with-block-selection-works-when-update-block-type-init.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-with-block-selection-works-when-update-block-type-init.json new file mode 100644 index 0000000000000..48f1782b68acd --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-with-block-selection-works-when-update-block-type-init.json @@ -0,0 +1,101 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-work-in-multiple-block-selection.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-work-in-multiple-block-selection.json new file mode 100644 index 0000000000000..f76f2a7b7982b --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-work-in-multiple-block-selection.json @@ -0,0 +1,107 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123", + "attributes": { + "underline": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "underline": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789", + "attributes": { + "underline": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-work-in-single-block-selection.json b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-work-in-single-block-selection.json new file mode 100644 index 0000000000000..4df164a74ee1b --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/format-bar.spec.ts/should-format-quick-bar-work-in-single-block-selection.json @@ -0,0 +1,100 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "underline": true, + "italic": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/Enter-key-should-as-expected-after-setting-heading-by-shortkey.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/Enter-key-should-as-expected-after-setting-heading-by-shortkey.json new file mode 100644 index 0000000000000..749ef405126ed --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/Enter-key-should-as-expected-after-setting-heading-by-shortkey.json @@ -0,0 +1,75 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h1", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "world" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-init.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-init.json new file mode 100644 index 0000000000000..4a195bffe1d42 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-init.json @@ -0,0 +1,109 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "1" + }, + { + "insert": "23", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "78", + "attributes": { + "code": true + } + }, + { + "insert": "9" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-redo.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-redo.json new file mode 100644 index 0000000000000..4a195bffe1d42 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-redo.json @@ -0,0 +1,109 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "1" + }, + { + "insert": "23", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "78", + "attributes": { + "code": true + } + }, + { + "insert": "9" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-undo.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-undo.json new file mode 100644 index 0000000000000..4beb61dab0774 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/multi-line-rich-text-inline-code-hotkey-undo.json @@ -0,0 +1,94 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-multiple-line-init.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-multiple-line-init.json new file mode 100644 index 0000000000000..dda4f654e2703 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-multiple-line-init.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "19" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-multiple-line-undo.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-multiple-line-undo.json new file mode 100644 index 0000000000000..4beb61dab0774 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-multiple-line-undo.json @@ -0,0 +1,94 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-single-line-init.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-single-line-init.json new file mode 100644 index 0000000000000..05e911d31610a --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-single-line-init.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ho" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-single-line-undo.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-single-line-undo.json new file mode 100644 index 0000000000000..e284ca678aa42 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-cut-work-single-line-undo.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-init.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-init.json new file mode 100644 index 0000000000000..54aa2945a06c2 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-init.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h1", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-0.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-0.json new file mode 100644 index 0000000000000..2c33f911b5c88 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-0.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-6.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-6.json new file mode 100644 index 0000000000000..9c632abfd1561 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-6.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h6", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-8.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-8.json new file mode 100644 index 0000000000000..5c5730a0edcf1 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-8.json @@ -0,0 +1,58 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-9.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-9.json new file mode 100644 index 0000000000000..4493d992ed166 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-9.json @@ -0,0 +1,58 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "numbered", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "checked": false, + "collapsed": false, + "order": 1 + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-d.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-d.json new file mode 100644 index 0000000000000..3c9888aedc3fc --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-d.json @@ -0,0 +1,79 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:divider", + "version": 1, + "props": {}, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-multiple-line-format-hotkey-work-finial.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-multiple-line-format-hotkey-work-finial.json new file mode 100644 index 0000000000000..4beb61dab0774 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-multiple-line-format-hotkey-work-finial.json @@ -0,0 +1,94 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-multiple-line-format-hotkey-work-init.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-multiple-line-format-hotkey-work-init.json new file mode 100644 index 0000000000000..c7bbeae253c58 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-multiple-line-format-hotkey-work-init.json @@ -0,0 +1,118 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "1" + }, + { + "insert": "23", + "attributes": { + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "78", + "attributes": { + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + }, + { + "insert": "9" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-single-line-format-hotkey-work-finial.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-single-line-format-hotkey-work-finial.json new file mode 100644 index 0000000000000..e284ca678aa42 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-single-line-format-hotkey-work-finial.json @@ -0,0 +1,56 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-single-line-format-hotkey-work-init.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-single-line-format-hotkey-work-init.json new file mode 100644 index 0000000000000..e387c5f0c9740 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/should-single-line-format-hotkey-work-init.json @@ -0,0 +1,68 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "h" + }, + { + "insert": "ell", + "attributes": { + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + }, + { + "insert": "o" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-ggg.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-ggg.json new file mode 100644 index 0000000000000..8542cbc43b761 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-ggg.json @@ -0,0 +1,84 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + }, + { + "insert": "bbb", + "attributes": { + "italic": true + } + }, + { + "insert": "ccc", + "attributes": { + "italic": true, + "bold": true + } + }, + { + "insert": "ddd", + "attributes": { + "bold": true + } + }, + { + "insert": "eee" + }, + { + "insert": "fffggg", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-hhh.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-hhh.json new file mode 100644 index 0000000000000..2a2f09ac41eb7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-hhh.json @@ -0,0 +1,84 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaahhh" + }, + { + "insert": "bbb", + "attributes": { + "italic": true + } + }, + { + "insert": "ccc", + "attributes": { + "italic": true, + "bold": true + } + }, + { + "insert": "ddd", + "attributes": { + "bold": true + } + }, + { + "insert": "eee" + }, + { + "insert": "fffggg", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold.json new file mode 100644 index 0000000000000..46f310342b7bc --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold.json @@ -0,0 +1,84 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + }, + { + "insert": "bbb", + "attributes": { + "italic": true + } + }, + { + "insert": "ccc", + "attributes": { + "italic": true, + "bold": true + } + }, + { + "insert": "ddd", + "attributes": { + "bold": true + } + }, + { + "insert": "eee" + }, + { + "insert": "fff", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-init.json b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-init.json new file mode 100644 index 0000000000000..74969002ab4b7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey.spec.ts/use-formatted-cursor-with-hotkey-init.json @@ -0,0 +1,78 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + }, + { + "insert": "bbb", + "attributes": { + "italic": true + } + }, + { + "insert": "ccc", + "attributes": { + "italic": true, + "bold": true + } + }, + { + "insert": "ddd", + "attributes": { + "bold": true + } + }, + { + "insert": "eee" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/bracket.spec.ts/should-bracket-complete-with-backtick-works-undo.json b/blocksuite/tests-legacy/snapshots/hotkey/bracket.spec.ts/should-bracket-complete-with-backtick-works-undo.json new file mode 100644 index 0000000000000..f92e305be2f55 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/bracket.spec.ts/should-bracket-complete-with-backtick-works-undo.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello world" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/bracket.spec.ts/should-bracket-complete-with-backtick-works.json b/blocksuite/tests-legacy/snapshots/hotkey/bracket.spec.ts/should-bracket-complete-with-backtick-works.json new file mode 100644 index 0000000000000..b5eaddaf55c83 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/bracket.spec.ts/should-bracket-complete-with-backtick-works.json @@ -0,0 +1,66 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "he" + }, + { + "insert": "llo", + "attributes": { + "code": true + } + }, + { + "insert": " world" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/Enter-key-should-as-expected-after-setting-heading-by-shortkey.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/Enter-key-should-as-expected-after-setting-heading-by-shortkey.json new file mode 100644 index 0000000000000..563a1e7594358 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/Enter-key-should-as-expected-after-setting-heading-by-shortkey.json @@ -0,0 +1,76 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h1", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "world" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-cut-work-single-line-init.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-cut-work-single-line-init.json new file mode 100644 index 0000000000000..75e9f991214e7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-cut-work-single-line-init.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ho" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-cut-work-single-line-undo.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-cut-work-single-line-undo.json new file mode 100644 index 0000000000000..d902db61ddad6 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-cut-work-single-line-undo.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-init.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-init.json new file mode 100644 index 0000000000000..a7c1e7d4fa741 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-init.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h1", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-0.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-0.json new file mode 100644 index 0000000000000..140fdef87eabb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-0.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-6.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-6.json new file mode 100644 index 0000000000000..022ec800e4fdb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-6.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "h6", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-8.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-8.json new file mode 100644 index 0000000000000..d0e2cf4630396 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-8.json @@ -0,0 +1,59 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-9.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-9.json new file mode 100644 index 0000000000000..51c03e53476bc --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-9.json @@ -0,0 +1,59 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "numbered", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "checked": false, + "collapsed": false, + "order": 1 + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-d.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-d.json new file mode 100644 index 0000000000000..1b737ee3633ed --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-hotkey-work-in-paragraph-press-d.json @@ -0,0 +1,80 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:divider", + "version": 1, + "props": {}, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-single-line-format-hotkey-work-finial.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-single-line-format-hotkey-work-finial.json new file mode 100644 index 0000000000000..d902db61ddad6 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-single-line-format-hotkey-work-finial.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hello" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-single-line-format-hotkey-work-init.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-single-line-format-hotkey-work-init.json new file mode 100644 index 0000000000000..55cbe3dea6ef2 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/should-single-line-format-hotkey-work-init.json @@ -0,0 +1,69 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "h" + }, + { + "insert": "ell", + "attributes": { + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + }, + { + "insert": "o" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/type-character-jump-out-code-node-1.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/type-character-jump-out-code-node-1.json new file mode 100644 index 0000000000000..dc495e4bff729 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/type-character-jump-out-code-node-1.json @@ -0,0 +1,60 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "Hello", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/type-character-jump-out-code-node-2.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/type-character-jump-out-code-node-2.json new file mode 100644 index 0000000000000..dffb438146b45 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/type-character-jump-out-code-node-2.json @@ -0,0 +1,63 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "Hello", + "attributes": { + "code": true + } + }, + { + "insert": "block suite" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-at-empty-line-bold.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-at-empty-line-bold.json new file mode 100644 index 0000000000000..511d260c0a6c8 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-at-empty-line-bold.json @@ -0,0 +1,60 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-ggg.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-ggg.json new file mode 100644 index 0000000000000..dff5aa5b70242 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-ggg.json @@ -0,0 +1,85 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + }, + { + "insert": "bbb", + "attributes": { + "italic": true + } + }, + { + "insert": "ccc", + "attributes": { + "italic": true, + "bold": true + } + }, + { + "insert": "ddd", + "attributes": { + "bold": true + } + }, + { + "insert": "eee" + }, + { + "insert": "fffggg", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-hhh.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-hhh.json new file mode 100644 index 0000000000000..ea0483de0947e --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold-hhh.json @@ -0,0 +1,85 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaahhh" + }, + { + "insert": "bbb", + "attributes": { + "italic": true + } + }, + { + "insert": "ccc", + "attributes": { + "italic": true, + "bold": true + } + }, + { + "insert": "ddd", + "attributes": { + "bold": true + } + }, + { + "insert": "eee" + }, + { + "insert": "fffggg", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold.json new file mode 100644 index 0000000000000..9c38d0ce4bafa --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-bold.json @@ -0,0 +1,85 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + }, + { + "insert": "bbb", + "attributes": { + "italic": true + } + }, + { + "insert": "ccc", + "attributes": { + "italic": true, + "bold": true + } + }, + { + "insert": "ddd", + "attributes": { + "bold": true + } + }, + { + "insert": "eee" + }, + { + "insert": "fff", + "attributes": { + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-init.json b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-init.json new file mode 100644 index 0000000000000..5e58ca68db293 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/hotkey.spec.ts/use-formatted-cursor-with-hotkey-init.json @@ -0,0 +1,79 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + }, + { + "insert": "bbb", + "attributes": { + "italic": true + } + }, + { + "insert": "ccc", + "attributes": { + "italic": true, + "bold": true + } + }, + { + "insert": "ddd", + "attributes": { + "bold": true + } + }, + { + "insert": "eee" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-init.json b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-init.json new file mode 100644 index 0000000000000..2174dc3229027 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-init.json @@ -0,0 +1,110 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "1" + }, + { + "insert": "23", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "78", + "attributes": { + "code": true + } + }, + { + "insert": "9" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-redo.json b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-redo.json new file mode 100644 index 0000000000000..2174dc3229027 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-redo.json @@ -0,0 +1,110 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "1" + }, + { + "insert": "23", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "code": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "78", + "attributes": { + "code": true + } + }, + { + "insert": "9" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-undo.json b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-undo.json new file mode 100644 index 0000000000000..aea12cd56eeb7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/multi-line-rich-text-inline-code-hotkey-undo.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-cut-work-multiple-line-init.json b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-cut-work-multiple-line-init.json new file mode 100644 index 0000000000000..77f254be3688c --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-cut-work-multiple-line-init.json @@ -0,0 +1,57 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "19" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-cut-work-multiple-line-undo.json b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-cut-work-multiple-line-undo.json new file mode 100644 index 0000000000000..aea12cd56eeb7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-cut-work-multiple-line-undo.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-multiple-line-format-hotkey-work-finial.json b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-multiple-line-format-hotkey-work-finial.json new file mode 100644 index 0000000000000..aea12cd56eeb7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-multiple-line-format-hotkey-work-finial.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-multiple-line-format-hotkey-work-init.json b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-multiple-line-format-hotkey-work-init.json new file mode 100644 index 0000000000000..63cd6eff50854 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/hotkey/multiline.spec.ts/should-multiple-line-format-hotkey-work-init.json @@ -0,0 +1,119 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "1" + }, + { + "insert": "23", + "attributes": { + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456", + "attributes": { + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "78", + "attributes": { + "strike": true, + "underline": true, + "italic": true, + "bold": true + } + }, + { + "insert": "9" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-enter-finial.json b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-enter-finial.json new file mode 100644 index 0000000000000..ff3339caeabb1 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-enter-finial.json @@ -0,0 +1,68 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:latex", + "version": 1, + "props": { + "xywh": "[0,0,16,16]", + "index": "a0", + "lockedBySelf": false, + "scale": 1, + "rotate": 0, + "latex": "aaa" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-enter-init.json b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-enter-init.json new file mode 100644 index 0000000000000..5893152dc88eb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-enter-init.json @@ -0,0 +1,53 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-space-finial.json b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-space-finial.json new file mode 100644 index 0000000000000..ff3339caeabb1 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-space-finial.json @@ -0,0 +1,68 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:latex", + "version": 1, + "props": { + "xywh": "[0,0,16,16]", + "index": "a0", + "lockedBySelf": false, + "scale": 1, + "rotate": 0, + "latex": "aaa" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-space-init.json b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-space-init.json new file mode 100644 index 0000000000000..5893152dc88eb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-markdown-shortcut-with-space-init.json @@ -0,0 +1,53 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-slash-menu-finial.json b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-slash-menu-finial.json new file mode 100644 index 0000000000000..74e9e8fdc814c --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-slash-menu-finial.json @@ -0,0 +1,53 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:latex", + "version": 1, + "props": { + "xywh": "[0,0,16,16]", + "index": "a0", + "lockedBySelf": false, + "scale": 1, + "rotate": 0, + "latex": "aaa" + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-slash-menu-init.json b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-slash-menu-init.json new file mode 100644 index 0000000000000..5893152dc88eb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/latex/block.spec.ts/add-latex-block-using-slash-menu-init.json @@ -0,0 +1,53 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/link.spec.ts/basic-link.json b/blocksuite/tests-legacy/snapshots/link.spec.ts/basic-link.json new file mode 100644 index 0000000000000..d81b55acec365 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/link.spec.ts/basic-link.json @@ -0,0 +1,60 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "link2", + "attributes": { + "link": "https://github.com" + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/link.spec.ts/convert-link-to-card.json b/blocksuite/tests-legacy/snapshots/link.spec.ts/convert-link-to-card.json new file mode 100644 index 0000000000000..dc269e572796c --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/link.spec.ts/convert-link-to-card.json @@ -0,0 +1,85 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "a" + }, + { + "insert": "linkText", + "attributes": { + "link": "http://example.com" + } + }, + { + "insert": "a" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/can-create-linked-page-and-jump-final.json b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/can-create-linked-page-and-jump-final.json new file mode 100644 index 0000000000000..34fee791fbef7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/can-create-linked-page-and-jump-final.json @@ -0,0 +1,67 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "page0" + } + ] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": " ", + "attributes": { + "reference": { + "type": "LinkedPage", + "pageId": "3" + } + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/can-create-linked-page-and-jump-init.json b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/can-create-linked-page-and-jump-init.json new file mode 100644 index 0000000000000..34fee791fbef7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/can-create-linked-page-and-jump-init.json @@ -0,0 +1,67 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "page0" + } + ] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": " ", + "attributes": { + "reference": { + "type": "LinkedPage", + "pageId": "3" + } + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/duplicated-linked-page-should-paste-as-linked-page.json b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/duplicated-linked-page-should-paste-as-linked-page.json new file mode 100644 index 0000000000000..26669e0fa1601 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/duplicated-linked-page-should-paste-as-linked-page.json @@ -0,0 +1,88 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "8", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": " ", + "attributes": { + "reference": { + "type": "LinkedPage", + "pageId": "3" + } + } + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": " ", + "attributes": { + "reference": { + "type": "LinkedPage", + "pageId": "3" + } + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/paste-linked-page-should-paste-as-linked-page.json b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/paste-linked-page-should-paste-as-linked-page.json new file mode 100644 index 0000000000000..9d6b2f6f5f4f5 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/paste-linked-page-should-paste-as-linked-page.json @@ -0,0 +1,63 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": " ", + "attributes": { + "reference": { + "type": "LinkedPage", + "pageId": "3" + } + } + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/should-create-and-switch-page-work-final.json b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/should-create-and-switch-page-work-final.json new file mode 100644 index 0000000000000..d1f40cdb5d598 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/should-create-and-switch-page-work-final.json @@ -0,0 +1,61 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "title0" + } + ] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "page0" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/should-create-and-switch-page-work-init.json b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/should-create-and-switch-page-work-init.json new file mode 100644 index 0000000000000..d1f40cdb5d598 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/linked-page.spec.ts/should-create-and-switch-page-work-init.json @@ -0,0 +1,61 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "title0" + } + ] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "page0" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-after-shift-tab.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-after-shift-tab.json new file mode 100644 index 0000000000000..d49a8a8d19904 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-after-shift-tab.json @@ -0,0 +1,76 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "text1" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "text2" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-after-tab.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-after-tab.json new file mode 100644 index 0000000000000..d9da2bbc00c76 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-after-tab.json @@ -0,0 +1,77 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "text1" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "text2" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-init.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-init.json new file mode 100644 index 0000000000000..d49a8a8d19904 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/basic-indent-and-unindent-init.json @@ -0,0 +1,76 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "text1" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "text2" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/can-expand-toggle-in-readonly-mode-before-readonly.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/can-expand-toggle-in-readonly-mode-before-readonly.json new file mode 100644 index 0000000000000..efa91ab5ddd92 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/can-expand-toggle-in-readonly-mode-before-readonly.json @@ -0,0 +1,102 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": true, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/click-toggle-icon-should-collapsed-list-init.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/click-toggle-icon-should-collapsed-list-init.json new file mode 100644 index 0000000000000..5af9598dbc839 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/click-toggle-icon-should-collapsed-list-init.json @@ -0,0 +1,102 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/click-toggle-icon-should-collapsed-list-toggle.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/click-toggle-icon-should-collapsed-list-toggle.json new file mode 100644 index 0000000000000..efa91ab5ddd92 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/click-toggle-icon-should-collapsed-list-toggle.json @@ -0,0 +1,102 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": true, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/convert-nested-paragraph-to-list-final.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/convert-nested-paragraph-to-list-final.json new file mode 100644 index 0000000000000..f50c6cb9de5cb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/convert-nested-paragraph-to-list-final.json @@ -0,0 +1,81 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "bbb" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/convert-nested-paragraph-to-list-init.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/convert-nested-paragraph-to-list-init.json new file mode 100644 index 0000000000000..e1635548a5e61 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/convert-nested-paragraph-to-list-init.json @@ -0,0 +1,77 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "aaa" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "bbb" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-1.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-1.json new file mode 100644 index 0000000000000..8eb1d2a5d8903 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-1.json @@ -0,0 +1,90 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-2.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-2.json new file mode 100644 index 0000000000000..fbcb1adb5d328 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-2.json @@ -0,0 +1,90 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-3.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-3.json new file mode 100644 index 0000000000000..915995696c1f6 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-3.json @@ -0,0 +1,88 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-4.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-4.json new file mode 100644 index 0000000000000..eb5985cb32e09 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-4.json @@ -0,0 +1,90 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-5.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-5.json new file mode 100644 index 0000000000000..14784e2f8507f --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-5.json @@ -0,0 +1,88 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-init.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-init.json new file mode 100644 index 0000000000000..bd93841987cb3 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/enter-list-block-with-empty-text-init.json @@ -0,0 +1,89 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-finial.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-finial.json new file mode 100644 index 0000000000000..e9082f36e0c4e --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-finial.json @@ -0,0 +1,123 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": true, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-init.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-init.json new file mode 100644 index 0000000000000..ea46f779f8a0e --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-init.json @@ -0,0 +1,123 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-toggle.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-toggle.json new file mode 100644 index 0000000000000..ce1357e1463d0 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/indent-item-should-expand-toggle-toggle.json @@ -0,0 +1,123 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": true, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/nested-list-blocks-finial.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/nested-list-blocks-finial.json new file mode 100644 index 0000000000000..5f33886e45df2 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/nested-list-blocks-finial.json @@ -0,0 +1,102 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/nested-list-blocks-init.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/nested-list-blocks-init.json new file mode 100644 index 0000000000000..7618c3e7ff3c0 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/nested-list-blocks-init.json @@ -0,0 +1,103 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/should-indent-todo-block-preserve-todo-status-final.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/should-indent-todo-block-preserve-todo-status-final.json new file mode 100644 index 0000000000000..b142b1320c3e5 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/should-indent-todo-block-preserve-todo-status-final.json @@ -0,0 +1,78 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "text1" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "todo", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "todo item" + } + ] + }, + "checked": true, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/list.spec.ts/should-indent-todo-block-preserve-todo-status-init.json b/blocksuite/tests-legacy/snapshots/list.spec.ts/should-indent-todo-block-preserve-todo-status-init.json new file mode 100644 index 0000000000000..618e5372f49af --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/list.spec.ts/should-indent-todo-block-preserve-todo-status-init.json @@ -0,0 +1,79 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "text1" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "todo", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "todo item" + } + ] + }, + "checked": true, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/delete-empty-text-paragraph-block-should-keep-children-blocks-when-following-custom-blocks-final.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/delete-empty-text-paragraph-block-should-keep-children-blocks-when-following-custom-blocks-final.json new file mode 100644 index 0000000000000..628adfe41ea0f --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/delete-empty-text-paragraph-block-should-keep-children-blocks-when-following-custom-blocks-final.json @@ -0,0 +1,84 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:divider", + "version": 1, + "props": {}, + "children": [] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/delete-empty-text-paragraph-block-should-keep-children-blocks-when-following-custom-blocks-init.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/delete-empty-text-paragraph-block-should-keep-children-blocks-when-following-custom-blocks-init.json new file mode 100644 index 0000000000000..0487e165d1264 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/delete-empty-text-paragraph-block-should-keep-children-blocks-when-following-custom-blocks-init.json @@ -0,0 +1,104 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:divider", + "version": 1, + "props": {}, + "children": [] + }, + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace-2.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace-2.json new file mode 100644 index 0000000000000..434bad82d698e --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace-2.json @@ -0,0 +1,96 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "abcfg" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hijlm" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "nop" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace-3.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace-3.json new file mode 100644 index 0000000000000..62c225f682a87 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace-3.json @@ -0,0 +1,96 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "abcfg" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hijlm" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "op" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace.json new file mode 100644 index 0000000000000..934cea41964a2 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-after-press-backspace.json @@ -0,0 +1,115 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "abcfg" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hij" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "klm" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "nop" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-init.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-init.json new file mode 100644 index 0000000000000..84bc634380ad4 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-indent-and-delete-in-line-start-init.json @@ -0,0 +1,135 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "abc" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "efg" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "hij" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "klm" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "nop" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-with-child-block-should-work-at-enter-final.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-with-child-block-should-work-at-enter-final.json new file mode 100644 index 0000000000000..9395797d11107 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-with-child-block-should-work-at-enter-final.json @@ -0,0 +1,96 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-with-child-block-should-work-at-enter-init.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-with-child-block-should-work-at-enter-init.json new file mode 100644 index 0000000000000..06a75e586d5db --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/paragraph-with-child-block-should-work-at-enter-init.json @@ -0,0 +1,77 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-delete-paragraph-block-child-can-hold-cursor-in-correct-position-final.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-delete-paragraph-block-child-can-hold-cursor-in-correct-position-final.json new file mode 100644 index 0000000000000..948323f6a742f --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-delete-paragraph-block-child-can-hold-cursor-in-correct-position-final.json @@ -0,0 +1,76 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "now" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-delete-paragraph-block-child-can-hold-cursor-in-correct-position-init.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-delete-paragraph-block-child-can-hold-cursor-in-correct-position-init.json new file mode 100644 index 0000000000000..91aeefe9e9831 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-delete-paragraph-block-child-can-hold-cursor-in-correct-position-init.json @@ -0,0 +1,77 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "4" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-2.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-2.json new file mode 100644 index 0000000000000..6041eb9fae4f9 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-2.json @@ -0,0 +1,134 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "345" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-3.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-3.json new file mode 100644 index 0000000000000..b1d0cd2303c4a --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-3.json @@ -0,0 +1,135 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "345" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-4.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-4.json new file mode 100644 index 0000000000000..25e234e5713f9 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent-4.json @@ -0,0 +1,136 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "345" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent.json new file mode 100644 index 0000000000000..4a65ef72dfe6f --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-indent.json @@ -0,0 +1,134 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "345" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-init.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-init.json new file mode 100644 index 0000000000000..1ea7265dc5540 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-init.json @@ -0,0 +1,133 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "345" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-1.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-1.json new file mode 100644 index 0000000000000..5dcfb3dfa854b --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-1.json @@ -0,0 +1,135 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "345" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-2.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-2.json new file mode 100644 index 0000000000000..f493fae0542c3 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-2.json @@ -0,0 +1,135 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "345" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-3.json b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-3.json new file mode 100644 index 0000000000000..1ea7265dc5540 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/paragraph.spec.ts/should-indent-and-unindent-works-with-children-unindent-3.json @@ -0,0 +1,133 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "345" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/click-bottom-of-page-and-if-the-last-is-embed-block-editor-should-insert-a-new-editable-block.json b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/click-bottom-of-page-and-if-the-last-is-embed-block-editor-should-insert-a-new-editable-block.json new file mode 100644 index 0000000000000..4ea1706b5faba --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/click-bottom-of-page-and-if-the-last-is-embed-block-editor-should-insert-a-new-editable-block.json @@ -0,0 +1,71 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:image", + "version": 1, + "props": { + "caption": "", + "sourceId": "ejImogf-Tb7AuKY-v94uz1zuOJbClqK-tWBxVr_ksGA=", + "width": 358, + "height": 268.5, + "index": "a0", + "xywh": "[0,0,0,0]", + "lockedBySelf": false, + "rotate": 0, + "size": -1 + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-indent-multi-selection-block.json b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-indent-multi-selection-block.json new file mode 100644 index 0000000000000..f5b1715e1c0ed --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-indent-multi-selection-block.json @@ -0,0 +1,96 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-not-draw-rect-for-sub-selected-blocks-when-entering-tab-key.json b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-not-draw-rect-for-sub-selected-blocks-when-entering-tab-key.json new file mode 100644 index 0000000000000..f5b1715e1c0ed --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-not-draw-rect-for-sub-selected-blocks-when-entering-tab-key.json @@ -0,0 +1,96 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-unindent-multi-selection-block-final.json b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-unindent-multi-selection-block-final.json new file mode 100644 index 0000000000000..aea12cd56eeb7 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-unindent-multi-selection-block-final.json @@ -0,0 +1,95 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-unindent-multi-selection-block-init.json b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-unindent-multi-selection-block-init.json new file mode 100644 index 0000000000000..f5b1715e1c0ed --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/block.spec.ts/should-unindent-multi-selection-block-init.json @@ -0,0 +1,96 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/indent-native-multi-selection-block-after-shift-tab.json b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/indent-native-multi-selection-block-after-shift-tab.json new file mode 100644 index 0000000000000..d210121ee41f0 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/indent-native-multi-selection-block-after-shift-tab.json @@ -0,0 +1,114 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/indent-native-multi-selection-block-after-tab.json b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/indent-native-multi-selection-block-after-tab.json new file mode 100644 index 0000000000000..2a88fad730784 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/indent-native-multi-selection-block-after-tab.json @@ -0,0 +1,115 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "012" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-backspace.json b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-backspace.json new file mode 100644 index 0000000000000..294f40489cbeb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-backspace.json @@ -0,0 +1,76 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "12ef" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ghi" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-redo.json b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-redo.json new file mode 100644 index 0000000000000..294f40489cbeb --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-redo.json @@ -0,0 +1,76 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "12ef" + } + ] + }, + "collapsed": false + }, + "children": [] + }, + { + "type": "block", + "id": "7", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ghi" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-undo.json b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-undo.json new file mode 100644 index 0000000000000..4a3b0c356986f --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-after-undo.json @@ -0,0 +1,156 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "abc" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "def" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "7", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ghi" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-init.json b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-init.json new file mode 100644 index 0000000000000..4a3b0c356986f --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/selection/native.spec.ts/native-range-delete-with-indent-init.json @@ -0,0 +1,156 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "2", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "4", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + }, + { + "type": "block", + "id": "5", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "abc" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "6", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "def" + } + ] + }, + "collapsed": false + }, + "children": [ + { + "type": "block", + "id": "7", + "flavour": "affine:paragraph", + "version": 1, + "props": { + "type": "text", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "ghi" + } + ] + }, + "collapsed": false + }, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/snapshots/slash-menu.spec.ts/delete-block-by-slash-menu-should-remove-children.json b/blocksuite/tests-legacy/snapshots/slash-menu.spec.ts/delete-block-by-slash-menu-should-remove-children.json new file mode 100644 index 0000000000000..561a6677532a8 --- /dev/null +++ b/blocksuite/tests-legacy/snapshots/slash-menu.spec.ts/delete-block-by-slash-menu-should-remove-children.json @@ -0,0 +1,59 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "lockedBySelf": false, + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/blocksuite/tests-legacy/tsconfig.json b/blocksuite/tests-legacy/tsconfig.json new file mode 100644 index 0000000000000..8e57bdabb7f5b --- /dev/null +++ b/blocksuite/tests-legacy/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "noEmit": true, + "composite": false, + "paths": { + "@blocks/*": ["../blocks/src/*"], + "@inline/*": ["../framework/inline/src/*"], + "@store/*": ["../framework/store/src/*"], + "@playground/*": ["../playground/*"] + } + }, + "include": ["**.spec.ts", "**.test.ts", "**/**.ts"] +} diff --git a/blocksuite/tests-legacy/utils/actions/block.ts b/blocksuite/tests-legacy/utils/actions/block.ts new file mode 100644 index 0000000000000..fe11bde35b79e --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/block.ts @@ -0,0 +1,25 @@ +import type { Page } from '@playwright/test'; + +import { waitNextFrame } from './misc.js'; + +export async function updateBlockType( + page: Page, + flavour: BlockSuite.Flavour, + type?: string +) { + await page.evaluate( + ([flavour, type]) => { + window.host.std.command + .chain() + .updateBlockType({ + flavour, + props: { + type, + }, + }) + .run(); + }, + [flavour, type] as [BlockSuite.Flavour, string?] + ); + await waitNextFrame(page, 400); +} diff --git a/blocksuite/tests-legacy/utils/actions/click.ts b/blocksuite/tests-legacy/utils/actions/click.ts new file mode 100644 index 0000000000000..dc813e80f83c1 --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/click.ts @@ -0,0 +1,130 @@ +import type { IPoint } from '@blocksuite/global/utils'; +import type { Page } from '@playwright/test'; + +import { toViewCoord } from './edgeless.js'; +import { waitNextFrame } from './misc.js'; + +export function getDebugMenu(page: Page) { + const debugMenu = page.locator('starter-debug-menu'); + return { + debugMenu, + undoBtn: debugMenu.locator('sl-tooltip[content="Undo"]'), + redoBtn: debugMenu.locator('sl-tooltip[content="Redo"]'), + + blockTypeButton: debugMenu.getByRole('button', { name: 'Block Type' }), + testOperationsButton: debugMenu.getByRole('button', { + name: 'Test Operations', + }), + + pagesBtn: debugMenu.getByTestId('docs-button'), + }; +} + +export async function moveView(page: Page, point: [number, number]) { + const [x, y] = await toViewCoord(page, point); + await page.mouse.move(x, y); +} + +export async function click(page: Page, point: IPoint) { + await page.mouse.click(point.x, point.y); +} + +export async function clickView(page: Page, point: [number, number]) { + const [x, y] = await toViewCoord(page, point); + await page.mouse.click(x, y); +} + +export async function dblclickView(page: Page, point: [number, number]) { + const [x, y] = await toViewCoord(page, point); + await page.mouse.dblclick(x, y); +} + +export async function undoByClick(page: Page) { + await getDebugMenu(page).undoBtn.click(); +} + +export async function redoByClick(page: Page) { + await getDebugMenu(page).redoBtn.click(); +} + +export async function clickBlockById(page: Page, id: string) { + await page.click(`[data-block-id="${id}"]`); +} + +export async function doubleClickBlockById(page: Page, id: string) { + await page.dblclick(`[data-block-id="${id}"]`); +} + +export async function disconnectByClick(page: Page) { + await clickTestOperationsMenuItem(page, 'Disconnect'); +} + +export async function connectByClick(page: Page) { + await clickTestOperationsMenuItem(page, 'Connect'); +} + +export async function addNoteByClick(page: Page) { + await clickTestOperationsMenuItem(page, 'Add Note'); +} + +export async function addNewPage(page: Page) { + const { pagesBtn } = getDebugMenu(page); + if (!(await page.locator('docs-panel').isVisible())) { + await pagesBtn.click(); + } + await page.locator('.new-doc-button').click(); + const docMetas = await page.evaluate(() => { + const { collection } = window; + return collection.meta.docMetas; + }); + if (!docMetas.length) throw new Error('Add new doc failed'); + return docMetas[docMetas.length - 1]; +} + +export async function switchToPage(page: Page, docId?: string) { + await page.evaluate(docId => { + const { collection, editor } = window; + + if (!docId) { + const docMetas = collection.meta.docMetas; + if (!docMetas.length) return; + docId = docMetas[0].id; + } + + const doc = collection.getDoc(docId); + if (!doc) return; + editor.doc = doc; + }, docId); +} + +export async function clickTestOperationsMenuItem(page: Page, name: string) { + const menuButton = getDebugMenu(page).testOperationsButton; + await menuButton.click(); + await waitNextFrame(page); // wait for animation ended + + const menuItem = page.getByRole('menuitem', { name }); + await menuItem.click(); + await menuItem.waitFor({ state: 'hidden' }); // wait for animation ended +} + +export async function switchReadonly(page: Page, value = true) { + await page.evaluate(_value => { + const defaultPage = document.querySelector( + 'affine-page-root' + ) as HTMLElement & { + doc: { + awarenessStore: { setFlag: (key: string, value: unknown) => void }; + }; + }; + const doc = defaultPage.doc; + doc.awarenessStore.setFlag('readonly', { 'doc:home': _value }); + }, value); +} + +export async function activeEmbed(page: Page) { + await page.click('.resizable-img'); +} + +export async function toggleDarkMode(page: Page) { + await page.click('sl-tooltip[content="Toggle Dark Mode"] sl-button'); +} diff --git a/blocksuite/tests-legacy/utils/actions/drag.ts b/blocksuite/tests-legacy/utils/actions/drag.ts new file mode 100644 index 0000000000000..c824f4f544704 --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/drag.ts @@ -0,0 +1,271 @@ +import type { Page } from '@playwright/test'; +import { assertImageOption } from 'utils/asserts.js'; + +import { getIndexCoordinate, waitNextFrame } from './misc.js'; + +export async function dragBetweenCoords( + page: Page, + from: { x: number; y: number }, + to: { x: number; y: number }, + options?: { + beforeMouseUp?: () => Promise; + steps?: number; + click?: boolean; + button?: 'left' | 'right' | 'middle'; + } +) { + const steps = options?.steps ?? 20; + const button: 'left' | 'right' | 'middle' = options?.button ?? 'left'; + + const { x: x1, y: y1 } = from; + const { x: x2, y: y2 } = to; + options?.click && (await page.mouse.click(x1, y1)); + await page.mouse.move(x1, y1); + await page.mouse.down({ button }); + await page.mouse.move(x2, y2, { steps }); + await options?.beforeMouseUp?.(); + await page.mouse.up({ button }); +} + +export async function dragBetweenIndices( + page: Page, + [startRichTextIndex, startVIndex]: [number, number], + [endRichTextIndex, endVIndex]: [number, number], + startCoordOffSet: { x: number; y: number } = { x: 0, y: 0 }, + endCoordOffSet: { x: number; y: number } = { x: 0, y: 0 }, + options?: { + beforeMouseUp?: () => Promise; + steps?: number; + click?: boolean; + } +) { + const finalOptions = { + steps: 50, + ...options, + }; + const startCoord = await getIndexCoordinate( + page, + [startRichTextIndex, startVIndex], + startCoordOffSet + ); + const endCoord = await getIndexCoordinate( + page, + [endRichTextIndex, endVIndex], + endCoordOffSet + ); + + await dragBetweenCoords(page, startCoord, endCoord, finalOptions); +} + +export async function dragOverTitle(page: Page) { + const { from, to } = await page.evaluate(() => { + const titleInput = document.querySelector( + 'doc-title rich-text' + ) as HTMLTextAreaElement; + const titleBound = titleInput.getBoundingClientRect(); + + return { + from: { x: titleBound.left + 1, y: titleBound.top + 1 }, + to: { x: titleBound.right - 1, y: titleBound.bottom - 1 }, + }; + }); + await dragBetweenCoords(page, from, to, { + steps: 5, + }); +} + +export async function dragEmbedResizeByTopRight(page: Page) { + const { from, to } = await page.evaluate(() => { + const bottomRightButton = document.querySelector( + '.top-right' + ) as HTMLInputElement; + const bottomRightButtonBound = bottomRightButton.getBoundingClientRect(); + const y = bottomRightButtonBound.top; + return { + from: { x: bottomRightButtonBound.left + 5, y: y + 5 }, + to: { x: bottomRightButtonBound.left + 5 - 200, y }, + }; + }); + await dragBetweenCoords(page, from, to, { + steps: 10, + }); +} + +export async function dragEmbedResizeByTopLeft(page: Page) { + const { from, to } = await page.evaluate(() => { + const bottomRightButton = document.querySelector( + '.top-left' + ) as HTMLInputElement; + const bottomRightButtonBound = bottomRightButton.getBoundingClientRect(); + const y = bottomRightButtonBound.top; + return { + from: { x: bottomRightButtonBound.left + 5, y: y + 5 }, + to: { x: bottomRightButtonBound.left + 5 + 200, y }, + }; + }); + await dragBetweenCoords(page, from, to, { + steps: 10, + }); +} + +export async function dragHandleFromBlockToBlockBottomById( + page: Page, + sourceId: string, + targetId: string, + bottom = true, + offset?: number, + beforeMouseUp?: () => Promise +) { + const sourceBlock = await page + .locator(`[data-block-id="${sourceId}"]`) + .boundingBox(); + const targetBlock = await page + .locator(`[data-block-id="${targetId}"]`) + .boundingBox(); + if (!sourceBlock || !targetBlock) { + throw new Error(); + } + await page.mouse.move( + sourceBlock.x + sourceBlock.width / 2, + sourceBlock.y + sourceBlock.height / 2 + ); + await waitNextFrame(page); + const dragHandleContainer = page.locator('.affine-drag-handle-container'); + await dragHandleContainer.hover(); + const handle = await dragHandleContainer.boundingBox(); + if (!handle) { + throw new Error(); + } + await page.mouse.move( + handle.x + handle.width / 2, + handle.y + handle.height / 2, + { steps: 10 } + ); + await page.mouse.down(); + await page.mouse.move( + targetBlock.x, + targetBlock.y + (bottom ? targetBlock.height - 1 : 1), + { + steps: 50, + } + ); + + if (offset) { + await page.mouse.move( + targetBlock.x + offset, + targetBlock.y + (bottom ? targetBlock.height - 1 : 1), + { + steps: 50, + } + ); + } + + if (beforeMouseUp) { + await beforeMouseUp(); + } + + await page.mouse.up(); +} + +export async function dragBlockToPoint( + page: Page, + sourceId: string, + point: { x: number; y: number } +) { + const sourceBlock = await page + .locator(`[data-block-id="${sourceId}"]`) + .boundingBox(); + if (!sourceBlock) { + throw new Error(); + } + await page.mouse.move( + sourceBlock.x + sourceBlock.width / 2, + sourceBlock.y + sourceBlock.height / 2 + ); + const handle = await page + .locator('.affine-drag-handle-container') + .boundingBox(); + if (!handle) { + throw new Error(); + } + await page.mouse.move( + handle.x + handle.width / 2, + handle.y + handle.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(point.x, point.y, { + steps: 50, + }); + + await page.mouse.up(); +} + +export async function moveToImage(page: Page) { + const { x, y } = await page.evaluate(() => { + const bottomRightButton = document.querySelector('img') as HTMLElement; + const imageClient = bottomRightButton.getBoundingClientRect(); + const y = imageClient.top; + return { + x: imageClient.left + 30, + y: y + 30, + }; + }); + await page.mouse.move(x, y); +} + +export async function popImageMoreMenu(page: Page) { + await moveToImage(page); + await assertImageOption(page); + const moreButton = page.locator('.image-toolbar-button.more'); + await moreButton.click(); + const menu = page.locator('.image-more-popup-menu'); + + const turnIntoCardButton = page.locator('editor-menu-action', { + hasText: 'Turn into card view', + }); + + const copyButton = page.locator('editor-menu-action', { + hasText: 'Copy', + }); + + const duplicateButton = page.locator('editor-menu-action', { + hasText: 'Duplicate', + }); + + const deleteButton = page.locator('editor-menu-action', { + hasText: 'Delete', + }); + + return { + menu, + copyButton, + turnIntoCardButton, + duplicateButton, + deleteButton, + }; +} + +export async function clickBlockDragHandle(page: Page, blockId: string) { + const blockBox = await page + .locator(`[data-block-id="${blockId}"]`) + .boundingBox(); + + if (!blockBox) { + throw new Error(); + } + await page.mouse.move( + blockBox.x + blockBox.width / 2, + blockBox.y + blockBox.height / 2 + ); + + const handleBox = await page + .locator('.affine-drag-handle-container') + .boundingBox(); + if (!handleBox) { + throw new Error(); + } + await page.mouse.click( + handleBox.x + handleBox.width / 2, + handleBox.y + handleBox.height / 2 + ); +} diff --git a/blocksuite/tests-legacy/utils/actions/edgeless.ts b/blocksuite/tests-legacy/utils/actions/edgeless.ts new file mode 100644 index 0000000000000..633baf4286245 --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/edgeless.ts @@ -0,0 +1,1918 @@ +import '../declare-test-window.js'; + +import type { NoteBlockModel, NoteDisplayMode } from '@blocks/index.js'; +import type { IPoint, IVec } from '@blocksuite/global/utils'; +import { assertExists, sleep } from '@blocksuite/global/utils'; +import type { Locator, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import type { Bound } from '../asserts.js'; +import { clickView } from './click.js'; +import { dragBetweenCoords } from './drag.js'; +import { + pressBackspace, + pressEnter, + pressEscape, + selectAllByKeyboard, + SHIFT_KEY, + SHORT_KEY, + type, +} from './keyboard.js'; +import { + enterPlaygroundRoom, + getEditorLocator, + initEmptyEdgelessState, + resetHistory, + waitNextFrame, +} from './misc.js'; + +const rotWith = (A: number[], C: number[], r = 0): number[] => { + if (r === 0) return A; + + const s = Math.sin(r); + const c = Math.cos(r); + + const px = A[0] - C[0]; + const py = A[1] - C[1]; + + const nx = px * c - py * s; + const ny = px * s + py * c; + + return [nx + C[0], ny + C[1]]; +}; + +const AWAIT_TIMEOUT = 500; +export const ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH = 1200; +export type Point = { x: number; y: number }; +export enum Shape { + Diamond = 'Diamond', + Ellipse = 'Ellipse', + 'Rounded rectangle' = 'Rounded rectangle', + Square = 'Square', + Triangle = 'Triangle', +} + +export enum LassoMode { + FreeHand = 'freehand', + Polygonal = 'polygonal', +} + +export enum ConnectorMode { + Straight, + Orthogonal, + Curve, +} + +export async function getNoteRect(page: Page, noteId: string) { + const xywh: string | null = await page.evaluate( + ([noteId]) => { + const doc = window.collection.getDoc('doc:home'); + const block = doc?.getBlockById(noteId); + if (block?.flavour === 'affine:note') { + return (block as NoteBlockModel).xywh; + } else { + return null; + } + }, + [noteId] as const + ); + expect(xywh).not.toBeNull(); + const [x, y, w, h] = JSON.parse(xywh as string); + return { x, y, w, h }; +} + +export async function getNoteProps(page: Page, noteId: string) { + const props = await page.evaluate( + ([id]) => { + const doc = window.collection.getDoc('doc:home'); + const block = doc?.getBlockById(id); + if (block?.flavour === 'affine:note') { + return (block as NoteBlockModel).keys.reduce( + (pre, key) => { + pre[key] = block[key as keyof typeof block] as string; + return pre; + }, + {} as Record + ); + } else { + return null; + } + }, + [noteId] as const + ); + return props; +} + +export async function extendFormatBar(page: Page) { + await page.click('sl-button:text("Test Operations")'); + await page.click('sl-menu-item:text("Extend Format Bar")'); + await waitNextFrame(page); +} + +export async function toggleFramePanel(page: Page) { + await page.click('sl-button:text("Test Operations")'); + await page.click('sl-menu-item:text("Toggle Frame Panel")'); + await waitNextFrame(page); +} + +export async function toggleMultipleEditors(page: Page) { + await page.click('sl-button:text("Test Operations")'); + await page.click('sl-menu-item:text("Toggle Multiple Editors")'); + await waitNextFrame(page); +} + +export async function switchEditorMode(page: Page) { + await page.click('sl-tooltip[content="Switch Editor"]'); + // FIXME: listen to editor loaded event + await waitNextFrame(page); +} + +export async function switchMultipleEditorsMode(page: Page) { + await page.evaluate(() => { + const containers = document.querySelectorAll('affine-editor-container'); + const mode = containers[0].mode === 'edgeless' ? 'page' : 'edgeless'; + + containers.forEach(container => { + container.mode = mode; + }); + }); +} + +export async function switchEditorEmbedMode(page: Page) { + await page.click('sl-button:text("Test Operations")'); + await page.click('sl-menu-item:text("Switch Offset Mode")'); +} + +export async function enterPresentationMode(page: Page) { + await page.click('sl-tooltip[content="Enter presentation mode"]'); + await waitNextFrame(page); +} + +export async function toggleEditorReadonly(page: Page) { + await page.click('sl-button:text("Test Operations")'); + await page.click('sl-menu-item:text("Toggle Readonly")'); + await waitNextFrame(page); +} + +type EdgelessTool = + | 'default' + | 'pan' + | 'note' + | 'shape' + | 'brush' + | 'eraser' + | 'text' + | 'connector' + | 'frame' + | 'frameNavigator' + | 'lasso'; +type ZoomToolType = 'zoomIn' | 'zoomOut' | 'fitToScreen'; +type ComponentToolType = 'shape' | 'thin' | 'thick' | 'brush' | 'more'; + +type PresentationToolType = 'previous' | 'next'; + +const locatorEdgelessToolButtonSenior = async ( + page: Page, + selector: string +): Promise => { + const target = page.locator(selector); + const visible = await target.isVisible(); + if (visible) return target; + // try to click next page + const nextButton = page.locator( + '.senior-nav-button-wrapper.next > icon-button' + ); + const nextExists = await nextButton.count(); + const isDisabled = + (await nextButton.getAttribute('data-test-disabled')) === 'true'; + if (!nextExists || isDisabled) return target; + await nextButton.click(); + await page.waitForTimeout(200); + return locatorEdgelessToolButtonSenior(page, selector); +}; + +export async function locatorEdgelessToolButton( + page: Page, + type: EdgelessTool, + innerContainer = true +) { + const selector = { + default: '.edgeless-default-button', + pan: '.edgeless-default-button', + shape: '.edgeless-shape-button', + brush: '.edgeless-brush-button', + eraser: '.edgeless-eraser-button', + text: '.edgeless-mindmap-button', + connector: '.edgeless-connector-button', + note: '.edgeless-note-button', + frame: '.edgeless-frame-button', + frameNavigator: '.edgeless-frame-navigator-button', + lasso: '.edgeless-lasso-button', + }[type]; + + let buttonType; + switch (type) { + case 'brush': + case 'text': + case 'eraser': + case 'shape': + case 'note': + buttonType = 'edgeless-toolbar-button'; + break; + default: + buttonType = 'edgeless-tool-icon-button'; + } + // TODO: quickTool locator is different + const button = await locatorEdgelessToolButtonSenior( + page, + `edgeless-toolbar-widget ${buttonType}${selector}` + ); + + return innerContainer ? button.locator('.icon-container') : button; +} + +export async function toggleZoomBarWhenSmallScreenWidth(page: Page) { + const toggleZoomBarButton = page.locator( + '.toggle-button edgeless-tool-icon-button' + ); + const isClosed = (await toggleZoomBarButton.count()) === 1; + if (isClosed) { + await toggleZoomBarButton.click(); + await page.waitForTimeout(200); + } +} + +export async function locatorEdgelessZoomToolButton( + page: Page, + type: ZoomToolType, + innerContainer = true +) { + const text = { + zoomIn: 'Zoom in', + zoomOut: 'Zoom out', + fitToScreen: 'Fit to screen', + }[type]; + + const screenWidth = page.viewportSize()?.width; + assertExists(screenWidth); + let zoomBarClass = 'horizontal'; + if (screenWidth < ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH) { + await toggleZoomBarWhenSmallScreenWidth(page); + zoomBarClass = 'vertical'; + } + + const button = page + .locator( + `.edgeless-zoom-toolbar-container.${zoomBarClass} edgeless-tool-icon-button` + ) + .filter({ + hasText: text, + }); + + return innerContainer ? button.locator('.icon-container') : button; +} + +export function locatorEdgelessComponentToolButton( + page: Page, + type: ComponentToolType, + innerContainer = true +) { + const text = { + shape: 'Shape', + brush: 'Color', + thin: 'Thin', + thick: 'Thick', + more: 'More', + }[type]; + const button = page + .locator('edgeless-element-toolbar-widget editor-icon-button') + .filter({ + hasText: text, + }); + + return innerContainer ? button.locator('.icon-container') : button; +} + +export function locatorPresentationToolbarButton( + page: Page, + type: PresentationToolType +) { + const text = { + previous: 'Previous', + next: 'Next', + }[type]; + const button = page + .locator('presentation-toolbar edgeless-tool-icon-button') + .filter({ + hasText: text, + }); + + return button; +} + +export async function setEdgelessTool( + page: Page, + mode: EdgelessTool, + shape = Shape.Square +) { + switch (mode) { + // text tool is removed, use shortcut to trigger + case 'text': + await page.keyboard.press('t', { delay: 100 }); + break; + case 'default': { + const button = await locatorEdgelessToolButton(page, 'default', false); + const classes = (await button.getAttribute('class'))?.split(' '); + if (!classes?.includes('default')) { + await button.click(); + await sleep(100); + } + break; + } + case 'pan': { + const button = await locatorEdgelessToolButton(page, 'default', false); + const classes = (await button.getAttribute('class'))?.split(' '); + if (classes?.includes('default')) { + await button.click(); + await sleep(100); + } else if (classes?.includes('pan')) { + await button.click(); // change to default + await sleep(100); + await button.click(); // change to pan + await sleep(100); + } + break; + } + case 'lasso': + case 'note': + case 'brush': + case 'eraser': + case 'frame': + case 'connector': { + const button = await locatorEdgelessToolButton(page, mode, false); + await button.click(); + break; + } + case 'shape': { + const shapeToolButton = await locatorEdgelessToolButton( + page, + 'shape', + false + ); + // Avoid clicking on the shape-element (will trigger dragging mode) + await shapeToolButton.click({ position: { x: 5, y: 5 } }); + + const squareShapeButton = page + .locator('edgeless-slide-menu edgeless-tool-icon-button') + .filter({ hasText: shape }); + await squareShapeButton.click(); + break; + } + } +} +export type ShapeName = + | 'rect' + | 'ellipse' + | 'diamond' + | 'triangle' + | 'roundedRect'; + +export async function assertEdgelessShapeType(page: Page, type: ShapeName) { + const curType = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) { + throw new Error('Missing edgeless page'); + } + const tool = container.gfx.tool.currentToolOption$.peek(); + if (tool.type !== 'shape') throw new Error('Expected shape tool'); + + return tool.shapeName; + }); + + expect(type).toEqual(curType); +} + +export async function assertEdgelessTool(page: Page, mode: EdgelessTool) { + const type = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) { + throw new Error('Missing edgeless page'); + } + return container.gfx.tool.currentToolOption$.peek().type; + }); + expect(type).toEqual(mode); +} + +export async function assertEdgelessConnectorToolMode( + page: Page, + mode: ConnectorMode +) { + const tool = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) { + throw new Error('Missing edgeless page'); + } + return container.gfx.tool.currentToolOption$.peek(); + }); + if (tool.type !== 'connector') { + throw new Error('Expected connector tool'); + } + expect(tool.mode).toEqual(mode); +} + +export async function assertEdgelessLassoToolMode(page: Page, mode: LassoMode) { + const tool = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) { + throw new Error('Missing edgeless page'); + } + return container.gfx.tool.currentToolOption$.peek(); + }); + if (tool.type !== 'lasso') { + throw new Error('Expected lasso tool'); + } + expect(tool.mode).toEqual(mode === LassoMode.FreeHand ? 0 : 1); +} + +export async function getEdgelessBlockChild(page: Page) { + const block = page.locator('affine-edgeless-note'); + const blockBox = await block.boundingBox(); + if (blockBox === null) throw new Error('Missing edgeless block child rect'); + return blockBox; +} + +export async function getEdgelessSelectedRect(page: Page) { + const selectedBox = await page.evaluate(() => { + const selected = document + .querySelector('edgeless-selected-rect') + ?.shadowRoot?.querySelector('.affine-edgeless-selected-rect'); + if (!selected) { + throw new Error('Missing edgeless selected rect'); + } + return selected.getBoundingClientRect(); + }); + return selectedBox; +} + +export async function getEdgelessSelectedRectModel(page: Page): Promise { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + const bound = container.service.selection.selectedBound; + return [bound.x, bound.y, bound.w, bound.h]; + }); +} + +export async function decreaseZoomLevel(page: Page) { + const btn = await locatorEdgelessZoomToolButton(page, 'zoomOut', false); + await btn.click(); + await sleep(AWAIT_TIMEOUT); +} + +export async function increaseZoomLevel(page: Page) { + const btn = await locatorEdgelessZoomToolButton(page, 'zoomIn', false); + await btn.click(); + await sleep(AWAIT_TIMEOUT); +} + +export async function autoFit(page: Page) { + const btn = await locatorEdgelessZoomToolButton(page, 'fitToScreen', false); + await btn.click(); + await sleep(AWAIT_TIMEOUT); +} + +export async function addBasicBrushElement( + page: Page, + start: Point, + end: Point, + auto = true +) { + await setEdgelessTool(page, 'brush'); + await dragBetweenCoords(page, start, end, { steps: 100 }); + auto && (await setEdgelessTool(page, 'default')); +} + +export async function addBasicRectShapeElement( + page: Page, + start: Point, + end: Point +) { + await setEdgelessTool(page, 'shape'); + await dragBetweenCoords(page, start, end, { steps: 50 }); +} + +export async function addBasicShapeElement( + page: Page, + start: Point, + end: Point, + shape: Shape +) { + await setEdgelessTool(page, 'shape', shape); + await dragBetweenCoords(page, start, end, { steps: 50 }); + return (await getSelectedIds(page))[0]; +} + +export async function addBasicConnectorElement( + page: Page, + start: Point, + end: Point +) { + await setEdgelessTool(page, 'connector'); + await dragBetweenCoords(page, start, end, { steps: 100 }); +} + +export async function addBasicFrameElement( + page: Page, + start: Point, + end: Point +) { + await setEdgelessTool(page, 'frame'); + await dragBetweenCoords(page, start, end, { steps: 50 }); +} + +export async function addBasicEdgelessText( + page: Page, + text: string, + x: number, + y: number +) { + await setEdgelessTool(page, 'text'); + await page.mouse.click(x, y); + await page.locator('affine-edgeless-text').waitFor({ state: 'visible' }); + await waitNextFrame(page, 100); + await type(page, text, 20); + await pressEscape(page, 2); + await setEdgelessTool(page, 'default'); +} + +export async function addNote(page: Page, text: string, x: number, y: number) { + await setEdgelessTool(page, 'note'); + await page.mouse.click(x, y); + await waitNextFrame(page); + + const paragraphs = text.split('\n'); + let i = 0; + for (const paragraph of paragraphs) { + ++i; + await type(page, paragraph, 20); + + if (i < paragraphs.length) { + await pressEnter(page); + } + } + + const { id } = await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + + return { + id: container.service.selection.selectedIds[0], + }; + }); + + return id; +} + +export async function exitEditing(page: Page) { + await page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + + container.service.selection.set({ + elements: [], + editing: false, + }); + }); +} + +export async function resizeElementByHandle( + page: Page, + delta: Point, + corner: + | 'top-left' + | 'top-right' + | 'bottom-right' + | 'bottom-left' = 'top-left', + steps = 1 +) { + const handle = page.locator(`.handle[aria-label="${corner}"] .resize`); + const box = await handle.boundingBox(); + if (box === null) throw new Error(); + const offset = 5; + await dragBetweenCoords( + page, + { x: box.x + offset, y: box.y + offset }, + { x: box.x + delta.x + offset, y: box.y + delta.y + offset }, + { + steps, + } + ); +} + +export async function rotateElementByHandle( + page: Page, + deg = 0, + corner: + | 'top-left' + | 'top-right' + | 'bottom-right' + | 'bottom-left' = 'top-left', + steps = 1 +) { + const rect = await page + .locator('.affine-edgeless-selected-rect') + .boundingBox(); + if (rect === null) throw new Error(); + const box = await page + .locator(`.handle[aria-label="${corner}"] .rotate`) + .boundingBox(); + if (box === null) throw new Error(); + + const cx = rect.x + rect.width / 2; + const cy = rect.y + rect.height / 2; + const x = box.x + box.width / 2; + const y = box.y + box.height / 2; + + const t = rotWith([x, y], [cx, cy], (deg * Math.PI) / 180); + + await dragBetweenCoords( + page, + { x, y }, + { x: t[0], y: t[1] }, + { + steps, + } + ); +} + +export async function selectBrushColor(page: Page, color: string) { + const colorButton = page.locator( + `edgeless-brush-menu .color-unit[aria-label="${color.toLowerCase()}"]` + ); + await colorButton.click(); +} + +export async function selectBrushSize(page: Page, size: string) { + const sizeIndexMap: Record = { + two: 1, + four: 2, + six: 3, + eight: 4, + ten: 5, + twelve: 6, + }; + const sizeButton = page.locator( + `edgeless-brush-menu .line-width-panel .line-width-button:nth-child(${sizeIndexMap[size]})` + ); + await sizeButton.click(); +} + +export async function pickColorAtPoints(page: Page, points: number[][]) { + const pickedColors: `#${string}`[] = await page.evaluate(points => { + const node = document.querySelector( + '.affine-edgeless-surface-block-container canvas' + ) as HTMLCanvasElement; + const w = node.width; + const h = node.height; + const ctx = node?.getContext('2d'); + if (!ctx) throw new Error('Cannot get canvas context'); + const pixelData = ctx.getImageData(0, 0, w, h).data; + + const colors = points.map(([x, y]) => { + const startPosition = (y * w + x) * 4; + return ('#' + + ( + (1 << 24) + + (pixelData[startPosition] << 16) + + (pixelData[startPosition + 1] << 8) + + pixelData[startPosition + 2] + ) + .toString(16) + .slice(1)) as `#${string}`; + }); + return colors; + }, points); + return pickedColors; +} + +export async function getNoteBoundBoxInEdgeless(page: Page, noteId: string) { + const editor = getEditorLocator(page); + const note = editor.locator( + `affine-edgeless-note[data-block-id="${noteId}"]` + ); + const bound = await note.boundingBox(); + if (!bound) { + throw new Error(`Missing note: ${noteId}`); + } + return bound; +} + +export async function getAllNoteIds(page: Page) { + return page.evaluate(() => { + return Array.from(document.querySelectorAll('affine-note')).map( + note => note.model.id + ); + }); +} + +export async function getAllEdgelessNoteIds(page: Page) { + return page.evaluate(() => { + return Array.from(document.querySelectorAll('affine-edgeless-note')).map( + note => note.model.id + ); + }); +} + +export async function getAllEdgelessTextIds(page: Page) { + return page.evaluate(() => { + return Array.from(document.querySelectorAll('affine-edgeless-text')).map( + text => text.model.id + ); + }); +} + +export async function countBlock(page: Page, flavour: string) { + return page.evaluate( + ([flavour]) => { + return Array.from(document.querySelectorAll(flavour)).length; + }, + [flavour] + ); +} + +export async function activeNoteInEdgeless(page: Page, noteId: string) { + const bound = await getNoteBoundBoxInEdgeless(page, noteId); + await page.mouse.dblclick( + bound.x + bound.width / 2, + bound.y + bound.height / 2 + ); +} + +export async function selectNoteInEdgeless(page: Page, noteId: string) { + const bound = await getNoteBoundBoxInEdgeless(page, noteId); + await page.mouse.click(bound.x + bound.width / 2, bound.y + bound.height / 2); +} + +export function locatorNoteDisplayModeButton( + page: Page, + mode: NoteDisplayMode +) { + return page + .locator('edgeless-change-note-button') + .locator('note-display-mode-panel') + .locator(`.item.${mode}`); +} + +export function locatorScalePanelButton(page: Page, scale: number) { + return page.locator('edgeless-scale-panel').locator(`.scale-${scale}`); +} + +export async function changeNoteDisplayMode(page: Page, mode: NoteDisplayMode) { + const button = locatorNoteDisplayModeButton(page, mode); + await button.click(); +} + +export async function changeNoteDisplayModeWithId( + page: Page, + noteId: string, + mode: NoteDisplayMode +) { + await selectNoteInEdgeless(page, noteId); + await triggerComponentToolbarAction(page, 'changeNoteDisplayMode'); + await waitNextFrame(page); + await changeNoteDisplayMode(page, mode); +} + +export async function updateExistedBrushElementSize( + page: Page, + nthSizeButton: 1 | 2 | 3 | 4 | 5 | 6 +) { + // get the nth brush size button + const btn = page.locator( + `.line-width-panel > div:nth-child(${nthSizeButton})` + ); + + await btn.click(); +} + +export async function openComponentToolbarMoreMenu(page: Page) { + const btn = page.locator( + 'edgeless-element-toolbar-widget edgeless-more-button editor-menu-button' + ); + + await btn.click(); +} + +export async function clickComponentToolbarMoreMenuButton( + page: Page, + button: 'delete' +) { + const text = { + delete: 'Delete', + }[button]; + + const btn = locatorComponentToolbarMoreButton(page) + .locator('editor-menu-action') + .filter({ hasText: text }); + + await btn.click(); +} + +// stepX/Y may not equal to wheel event delta. +// Chromium reports deltaX/deltaY scaled by host device scale factor. +// https://bugs.chromium.org/p/chromium/issues/detail?id=1324819 +export async function zoomByMouseWheel( + page: Page, + stepX: number, + stepY: number +) { + await page.keyboard.down(SHORT_KEY); + await page.mouse.wheel(stepX, stepY); + await page.keyboard.up(SHORT_KEY); +} + +// touch screen is not supported by Playwright now +// use pointer event mock instead +// https://github.com/microsoft/playwright/issues/2903 +export async function multiTouchDown(page: Page, points: Point[]) { + await page.evaluate(points => { + const target = document.querySelector('affine-edgeless-root'); + if (!target) { + throw new Error('Missing edgeless page'); + } + points.forEach((point, index) => { + const clientX = point.x; + const clientY = point.y; + + target.dispatchEvent( + new PointerEvent('pointerdown', { + clientX, + clientY, + bubbles: true, + pointerType: 'touch', + pointerId: index, + isPrimary: index === 0, + }) + ); + }); + }, points); +} + +export async function multiTouchMove( + page: Page, + from: Point[], + to: Point[], + step = 5 +) { + await page.evaluate( + async ({ from, to, step }) => { + const target = document.querySelector('affine-edgeless-root'); + if (!target) { + throw new Error('Missing edgeless page'); + } + + if (from.length !== to.length) { + throw new Error('from and to should have the same length'); + } + + if (step !== 0) { + for (const [i] of Array.from({ length: step }).entries()) { + from.forEach((point, index) => { + const clientX = + point.x + ((to[index].x - point.x) / step) * (i + 1); + const clientY = + point.y + ((to[index].y - point.y) / step) * (i + 1); + + target.dispatchEvent( + new PointerEvent('pointermove', { + clientX, + clientY, + bubbles: true, + pointerType: 'touch', + pointerId: index, + isPrimary: index === 0, + }) + ); + }); + await new Promise(resolve => setTimeout(resolve, 16)); + } + } + }, + { from, to, step } + ); +} + +export async function multiTouchUp(page: Page, points: Point[]) { + await page.evaluate(points => { + const target = document.querySelector('affine-edgeless-root'); + if (!target) { + throw new Error('Missing edgeless page'); + } + points.forEach((point, index) => { + const clientX = point.x; + const clientY = point.y; + + target.dispatchEvent( + new PointerEvent('pointerup', { + clientX, + clientY, + bubbles: true, + pointerType: 'touch', + pointerId: index, + isPrimary: index === 0, + }) + ); + }); + }, points); +} + +export async function zoomFitByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+1`, { delay: 100 }); + await waitNextFrame(page, 300); +} + +export async function zoomOutByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+-`, { delay: 100 }); + await waitNextFrame(page, 300); +} + +export async function zoomResetByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+0`, { delay: 50 }); + // Wait for animation + await waitNextFrame(page, 300); +} + +export async function zoomInByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+=`, { delay: 50 }); + await waitNextFrame(page, 300); +} + +export async function getZoomLevel(page: Page) { + const screenWidth = page.viewportSize()?.width; + assertExists(screenWidth); + let zoomBarClass = 'horizontal'; + if (screenWidth < ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH) { + await toggleZoomBarWhenSmallScreenWidth(page); + zoomBarClass = 'vertical'; + } + const span = page.locator( + `.edgeless-zoom-toolbar-container.${zoomBarClass} .zoom-percent` + ); + await waitNextFrame(page); + const text = await span.textContent(); + if (!text) { + throw new Error('Missing .zoom-percent'); + } + return Number(text.replace('%', '')); +} + +export async function optionMouseDrag( + page: Page, + start: number[], + end: number[] +) { + start = await toViewCoord(page, start); + end = await toViewCoord(page, end); + await page.keyboard.down('Alt'); + await dragBetweenCoords( + page, + { x: start[0], y: start[1] }, + { x: end[0], y: end[1] }, + { steps: 30 } + ); + await page.keyboard.up('Alt'); +} + +export async function shiftClick(page: Page, point: IPoint) { + await page.keyboard.down(SHIFT_KEY); + await page.mouse.click(point.x, point.y); + await page.keyboard.up(SHIFT_KEY); +} + +export async function shiftClickView(page: Page, point: [number, number]) { + await page.keyboard.down(SHIFT_KEY); + await clickView(page, point); + await page.keyboard.up(SHIFT_KEY); +} + +export async function deleteAll(page: Page) { + await clickView(page, [0, 0]); + await selectAllByKeyboard(page); + await pressBackspace(page); +} + +export async function deleteAllConnectors(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + container.service.getElementsByType('connector').forEach(c => { + container.service.removeElement(c.id); + }); + }); +} + +export function locatorComponentToolbar(page: Page) { + return page.locator('edgeless-element-toolbar-widget'); +} + +export function locatorComponentToolbarMoreButton(page: Page) { + const moreButton = locatorComponentToolbar(page).locator( + 'edgeless-more-button' + ); + return moreButton; +} +type Action = + | 'bringToFront' + | 'bringForward' + | 'sendBackward' + | 'sendToBack' + | 'copyAsPng' + | 'changeNoteColor' + | 'changeShapeStyle' + | 'changeShapeFillColor' + | 'changeShapeStrokeColor' + | 'changeShapeStrokeStyles' + | 'changeConnectorStrokeColor' + | 'changeConnectorStrokeStyles' + | 'changeConnectorShape' + | 'addFrame' + | 'addGroup' + | 'addMindmap' + | 'createGroupOnMoreOption' + | 'ungroup' + | 'releaseFromGroup' + | 'createFrameOnMoreOption' + | 'duplicate' + | 'renameGroup' + | 'autoSize' + | 'changeNoteDisplayMode' + | 'changeNoteSlicerSetting' + | 'changeNoteScale' + | 'addText' + | 'quickConnect' + | 'turnIntoLinkedDoc' + | 'createLinkedDoc' + | 'openLinkedDoc' + | 'toCardView' + | 'toEmbedView' + | 'autoArrange' + | 'autoResize'; + +export async function triggerComponentToolbarAction( + page: Page, + action: Action +) { + switch (action) { + case 'bringToFront': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Bring to Front', + }); + await actionButton.click(); + break; + } + case 'bringForward': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Bring Forward', + }); + await actionButton.click(); + break; + } + case 'sendBackward': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Send Backward', + }); + await actionButton.click(); + break; + } + case 'sendToBack': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Send to Back', + }); + await actionButton.click(); + break; + } + case 'copyAsPng': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Copy as PNG', + }); + await actionButton.click(); + break; + } + case 'createFrameOnMoreOption': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Frame Section', + }); + await actionButton.click(); + break; + } + case 'duplicate': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Duplicate', + }); + await actionButton.click(); + break; + } + case 'changeShapeFillColor': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-shape-button') + .getByRole('button', { name: 'Fill color' }); + await button.click(); + break; + } + case 'changeShapeStrokeStyles': + case 'changeShapeStrokeColor': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-shape-button') + .getByRole('button', { name: 'Border style' }); + await button.click(); + break; + } + case 'changeShapeStyle': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-shape-button') + .getByRole('button', { name: /^Style$/ }); + await button.click(); + break; + } + case 'changeConnectorStrokeColor': { + const button = page + .locator('edgeless-change-connector-button') + .getByRole('button', { name: 'Stroke style' }); + await button.click(); + break; + } + case 'changeConnectorStrokeStyles': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-connector-button') + .getByRole('button', { name: 'Stroke style' }); + await button.click(); + break; + } + case 'changeConnectorShape': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-connector-button') + .getByRole('button', { name: 'Shape' }); + await button.click(); + break; + } + case 'addFrame': { + const button = locatorComponentToolbar(page).locator( + 'edgeless-add-frame-button' + ); + await button.click(); + break; + } + case 'addGroup': { + const button = locatorComponentToolbar(page).locator( + 'edgeless-add-group-button' + ); + await button.click(); + break; + } + case 'addMindmap': { + const button = page.locator('edgeless-mindmap-tool-button'); + await button.click(); + await page.mouse.move(400, 400); + await page.mouse.click(400, 400); + break; + } + case 'createGroupOnMoreOption': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Group Section', + }); + await actionButton.click(); + break; + } + case 'ungroup': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-group-button') + .getByRole('button', { name: 'Ungroup' }); + await button.click(); + break; + } + case 'renameGroup': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-group-button') + .getByRole('button', { name: 'Rename' }); + await button.click(); + break; + } + case 'releaseFromGroup': { + const button = locatorComponentToolbar(page).locator( + 'edgeless-release-from-group-button' + ); + await button.click(); + break; + } + case 'changeNoteColor': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-note-button') + .getByRole('button', { name: 'Background' }); + await button.click(); + break; + } + case 'changeNoteDisplayMode': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-note-button') + .getByRole('button', { name: 'Mode' }); + await button.click(); + break; + } + case 'changeNoteSlicerSetting': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-note-button') + .getByRole('button', { name: 'Slicer' }); + await button.click(); + break; + } + case 'changeNoteScale': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-note-button') + .getByRole('button', { name: 'Scale' }); + await button.click(); + break; + } + case 'autoSize': { + const button = locatorComponentToolbar(page) + .locator('edgeless-change-note-button') + .getByRole('button', { name: 'Size' }); + await button.click(); + break; + } + case 'addText': { + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Add text', + }); + await button.click(); + break; + } + case 'quickConnect': { + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Draw connector', + }); + await button.click(); + break; + } + case 'turnIntoLinkedDoc': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Turn into linked doc', + }); + await actionButton.click(); + break; + } + case 'createLinkedDoc': { + const moreButton = locatorComponentToolbarMoreButton(page); + await moreButton.click(); + + const actionButton = moreButton + .locator('.more-actions-container editor-menu-action') + .filter({ + hasText: 'Create linked doc', + }); + await actionButton.click(); + break; + } + case 'openLinkedDoc': { + const openButton = locatorComponentToolbar(page).getByRole('button', { + name: 'Open', + }); + await openButton.click(); + + const button = locatorComponentToolbar(page).getByRole('button', { + name: 'Open this doc', + }); + await button.click(); + break; + } + case 'toCardView': { + const button = locatorComponentToolbar(page) + .locator('edgeless-tool-icon-button') + .filter({ + hasText: 'Card view', + }); + await button.click(); + break; + } + case 'toEmbedView': { + const button = locatorComponentToolbar(page) + .locator('edgeless-tool-icon-button') + .filter({ + hasText: 'Embed view', + }); + await button.click(); + break; + } + case 'autoArrange': { + const button = locatorComponentToolbar(page).locator( + 'edgeless-align-button' + ); + await button.click(); + const arrange = button.locator('editor-icon-button').filter({ + hasText: 'Auto arrange', + }); + await arrange.click(); + break; + } + case 'autoResize': { + const button = locatorComponentToolbar(page).locator( + 'edgeless-align-button' + ); + await button.click(); + const resize = button.locator('editor-icon-button').filter({ + hasText: 'Resize & Align', + }); + await resize.click(); + break; + } + } +} + +export async function changeEdgelessNoteBackground(page: Page, color: string) { + const colorButton = page + .locator('edgeless-change-note-button') + .locator(`.color-unit[aria-label="${color}"]`); + await colorButton.click(); +} + +export async function changeShapeFillColor(page: Page, color: string) { + const colorButton = page + .locator('edgeless-change-shape-button') + .locator('edgeless-color-picker-button.fill-color') + .locator('edgeless-color-panel') + .locator(`.color-unit[aria-label="${color}"]`); + await colorButton.click({ force: true }); +} + +export async function changeShapeFillColorToTransparent(page: Page) { + const colorButton = page + .locator('edgeless-change-shape-button') + .locator('edgeless-color-picker-button.fill-color') + .locator('edgeless-color-panel') + .locator('edgeless-color-custom-button'); + await colorButton.click({ force: true }); + + const input = page.locator('edgeless-color-picker').locator('label.alpha'); + await input.focus(); + await input.press('ArrowRight'); + await input.press('ArrowRight'); + await input.press('ArrowRight'); + await input.press('Backspace'); + await input.press('Backspace'); + await input.press('Backspace'); +} + +export async function changeShapeStrokeColor(page: Page, color: string) { + const colorButton = page + .locator('edgeless-change-shape-button') + .locator('edgeless-color-picker-button.border-style') + .locator(`.color-unit[aria-label="${color}"]`); + await colorButton.click(); +} + +export async function resizeConnectorByStartCapitalHandler( + page: Page, + delta: { x: number; y: number }, + steps = 1 +) { + const handler = page.locator( + '.affine-edgeless-selected-rect .line-controller.line-start' + ); + const box = await handler.boundingBox(); + if (box === null) throw new Error(); + const offset = 5; + await dragBetweenCoords( + page, + { x: box.x + offset, y: box.y + offset }, + { x: box.x + delta.x + offset, y: box.y + delta.y + offset }, + { + steps, + } + ); +} + +export function getEdgelessLineWidthPanel(page: Page) { + return page + .locator('edgeless-change-shape-button') + .locator('edgeless-line-width-panel') + .locator('.line-width-panel'); +} +export async function changeShapeStrokeWidth(page: Page) { + const lineWidthPanel = getEdgelessLineWidthPanel(page); + const lineWidthPanelRect = await lineWidthPanel.boundingBox(); + assertExists(lineWidthPanelRect); + // click line width panel by position + const x = lineWidthPanelRect.x + 40; + const y = lineWidthPanelRect.y + 10; + await page.mouse.click(x, y); +} + +export function locatorShapeStrokeStyleButton( + page: Page, + mode: 'solid' | 'dash' | 'none' +) { + return page + .locator('edgeless-change-shape-button') + .locator(`.line-style-button.mode-${mode}`); +} + +export async function changeShapeStrokeStyle( + page: Page, + mode: 'solid' | 'dash' | 'none' +) { + const button = locatorShapeStrokeStyleButton(page, mode); + await button.click(); +} + +export function locatorShapeStyleButton( + page: Page, + style: 'general' | 'scribbled' +) { + return page + .locator('edgeless-change-shape-button') + .locator('edgeless-shape-style-panel') + .getByRole('button', { name: style }); +} + +export async function changeShapeStyle( + page: Page, + style: 'general' | 'scribbled' +) { + const button = locatorShapeStyleButton(page, style); + await button.click(); +} + +export async function changeConnectorStrokeColor(page: Page, color: string) { + const colorButton = page + .locator('edgeless-change-connector-button') + .locator('edgeless-color-panel') + .getByLabel(color); + await colorButton.click(); +} + +export function locatorConnectorStrokeWidthButton( + page: Page, + buttonPosition: number +) { + return page + .locator('edgeless-change-connector-button') + .locator(`edgeless-line-width-panel`) + .locator(`.line-width-button:nth-child(${buttonPosition})`); +} +export async function changeConnectorStrokeWidth( + page: Page, + buttonPosition: number +) { + const button = locatorConnectorStrokeWidthButton(page, buttonPosition); + await button.click(); +} + +export function locatorConnectorStrokeStyleButton( + page: Page, + mode: 'solid' | 'dash' | 'none' +) { + return page + .locator('edgeless-change-connector-button') + .locator(`.line-style-button.mode-${mode}`); +} +export async function changeConnectorStrokeStyle( + page: Page, + mode: 'solid' | 'dash' | 'none' +) { + const button = locatorConnectorStrokeStyleButton(page, mode); + await button.click(); +} + +export async function initThreeOverlapFilledShapes(page: Page) { + const rect0 = { + start: { x: 100, y: 100 }, + end: { x: 200, y: 200 }, + }; + await addBasicRectShapeElement(page, rect0.start, rect0.end); + await page.mouse.click(rect0.start.x + 5, rect0.start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + await changeShapeFillColor(page, '--affine-palette-shape-teal'); + + const rect1 = { + start: { x: 130, y: 130 }, + end: { x: 230, y: 230 }, + }; + await addBasicRectShapeElement(page, rect1.start, rect1.end); + await page.mouse.click(rect1.start.x + 5, rect1.start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + await changeShapeFillColor(page, '--affine-palette-shape-black'); + + const rect2 = { + start: { x: 160, y: 160 }, + end: { x: 260, y: 260 }, + }; + await addBasicRectShapeElement(page, rect2.start, rect2.end); + await page.mouse.click(rect2.start.x + 5, rect2.start.y + 5); + await triggerComponentToolbarAction(page, 'changeShapeFillColor'); + await changeShapeFillColor(page, '--affine-palette-shape-white'); +} + +export async function initThreeOverlapNotes(page: Page, x = 130, y = 140) { + await addNote(page, 'abc', x, y); + await addNote(page, 'efg', x + 30, y); + await addNote(page, 'hij', x + 60, y); +} + +export async function initThreeNotes(page: Page) { + await addNote(page, 'abc', 30 + 100, 40 + 100); + await addNote(page, 'efg', 30 + 130, 40 + 200); + await addNote(page, 'hij', 30 + 160, 40 + 300); +} + +export async function toViewCoord(page: Page, point: number[]) { + return page.evaluate(point => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.viewport.toViewCoord(point[0], point[1]); + }, point); +} + +export async function dragBetweenViewCoords( + page: Page, + start: number[], + end: number[], + options?: Parameters[3] +) { + const [startX, startY] = await toViewCoord(page, start); + const [endX, endY] = await toViewCoord(page, end); + await dragBetweenCoords( + page, + { x: startX, y: startY }, + { x: endX, y: endY }, + options + ); +} + +export async function toModelCoord(page: Page, point: number[]) { + return page.evaluate(point => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.viewport.toModelCoord(point[0], point[1]); + }, point); +} + +export async function getConnectorSourceConnection(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.getElementsByType('connector')[0].source; + }); +} + +export async function getConnectorPath(page: Page, index = 0): Promise { + return page.evaluate( + ([index]) => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + const connectors = container.service.getElementsByType('connector'); + return connectors[index].absolutePath; + }, + [index] + ); +} + +export async function getEdgelessElementBound( + page: Page, + elementId: string +): Promise<[number, number, number, number]> { + return page.evaluate( + ([elementId]) => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + const element = container.service.getElementById(elementId); + if (!element) throw new Error(`element not found: ${elementId}`); + return JSON.parse(element.xywh); + }, + [elementId] + ); +} + +export async function getSelectedIds(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.selection.selectedElements.map(e => e.id); + }); +} + +export async function getSelectedBoundCount(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.selection.selectedElements.length; + }); +} + +export async function getSelectedBound( + page: Page, + index = 0 +): Promise<[number, number, number, number]> { + return page.evaluate( + ([index]) => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + const selected = container.service.selection.selectedElements[index]; + return JSON.parse(selected.xywh); + }, + [index] + ); +} + +export async function getContainerOfElements(page: Page, ids: string[]) { + return page.evaluate( + ([ids]) => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + + return ids.map(id => container.service.surface.getGroup(id)?.id ?? null); + }, + [ids] + ); +} + +export async function getContainerIds(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.elements.map(el => el.group?.id ?? 'null'); + }); +} + +export async function getContainerChildIds(page: Page, id: string) { + return page.evaluate( + ([id]) => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + const gfxModel = container.service.getElementById(id); + + return gfxModel && container.service.surface.isGroup(gfxModel) + ? gfxModel.childIds + : []; + }, + [id] + ); +} + +export async function getCanvasElementsCount(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.elements.length; + }); +} + +export async function getSortedIds(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.layer.canvasElements.map(e => e.id); + }); +} + +export async function getAllSortedIds(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.edgelessElements.map(e => e.id); + }); +} + +export async function getTypeById(page: Page, id: string) { + return page.evaluate( + ([id]) => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + const element = container.service.getElementById(id)!; + return 'flavour' in element ? element.flavour : element.type; + }, + [id] + ); +} + +export async function getIds(page: Page, filterGroup = false) { + return page.evaluate( + ([filterGroup]) => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.elements + .filter(el => !filterGroup || el.type !== 'group') + .map(e => e.id); + }, + [filterGroup] + ); +} + +export async function getFirstContainerId(page: Page, exclude: string[] = []) { + return page.evaluate( + ([exclude]) => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return ( + container.service.edgelessElements.find( + e => container.service.surface.isGroup(e) && !exclude.includes(e.id) + )?.id ?? '' + ); + }, + [exclude] + ); +} + +export async function getIndexes(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + return container.service.elements.map(e => e.index); + }); +} + +export async function getSortedIdsInViewport(page: Page) { + return page.evaluate(() => { + const container = document.querySelector('affine-edgeless-root'); + if (!container) throw new Error('container not found'); + const { service } = container; + return service.gfx.grid + .search(service.viewport.viewportBounds, { + filter: ['canvas'], + }) + .map(e => e.id); + }); +} + +export async function edgelessCommonSetup(page: Page) { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await deleteAll(page); + await resetHistory(page); +} + +export async function createFrame( + page: Page, + coord1: [number, number], + coord2: [number, number] +) { + await page.keyboard.press('f'); + await dragBetweenViewCoords(page, coord1, coord2); + const id = (await getSelectedIds(page))[0]; + await page.keyboard.press('Escape'); + return id; +} + +export async function createShapeElement( + page: Page, + coord1: number[], + coord2: number[], + shape = Shape.Square +) { + const start = await toViewCoord(page, coord1); + const end = await toViewCoord(page, coord2); + const shapeId = await addBasicShapeElement( + page, + { x: start[0], y: start[1] }, + { x: end[0], y: end[1] }, + shape + ); + return shapeId; +} + +export async function createConnectorElement( + page: Page, + coord1: number[], + coord2: number[] +) { + const start = await toViewCoord(page, coord1); + const end = await toViewCoord(page, coord2); + await addBasicConnectorElement( + page, + { x: start[0], y: start[1] }, + { x: end[0], y: end[1] } + ); +} + +export async function createFrameElement( + page: Page, + coord1: number[], + coord2: number[] +) { + const start = await toViewCoord(page, coord1); + const end = await toViewCoord(page, coord2); + await addBasicFrameElement( + page, + { x: start[0], y: start[1] }, + { x: end[0], y: end[1] } + ); +} + +export async function createBrushElement( + page: Page, + coord1: number[], + coord2: number[], + auto = true +) { + const start = await toViewCoord(page, coord1); + const end = await toViewCoord(page, coord2); + await addBasicBrushElement( + page, + { x: start[0], y: start[1] }, + { x: end[0], y: end[1] }, + auto + ); +} + +export async function createEdgelessText( + page: Page, + coord: number[], + text = 'text' +) { + const position = await toViewCoord(page, coord); + await addBasicEdgelessText(page, text, position[0], position[1]); +} + +export async function createMindmap(page: Page, coord: number[]) { + const position = await toViewCoord(page, coord); + await page.keyboard.press('m'); + await page.mouse.click(position[0], position[1]); +} + +export async function createNote( + page: Page, + coord1: number[], + content?: string +) { + const start = await toViewCoord(page, coord1); + return addNote(page, content || 'note', start[0], start[1]); +} + +export async function hoverOnNote(page: Page, id: string, offset = [0, 0]) { + const blockRect = await page.locator(`[data-block-id="${id}"]`).boundingBox(); + + assertExists(blockRect); + + await page.mouse.move( + blockRect.x + blockRect.width / 2 + offset[0], + blockRect.y + blockRect.height / 2 + offset[1] + ); +} + +export function toIdCountMap(ids: string[]) { + return ids.reduce( + (pre, cur) => { + pre[cur] = (pre[cur] ?? 0) + 1; + return pre; + }, + {} as Record + ); +} + +export function getFrameTitle(page: Page, frame: string) { + return page.locator(`affine-frame-title[data-id="${frame}"]`); +} diff --git a/blocksuite/tests-legacy/utils/actions/index.ts b/blocksuite/tests-legacy/utils/actions/index.ts new file mode 100644 index 0000000000000..0f20f0a3b6a31 --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/index.ts @@ -0,0 +1,7 @@ +export * from './block.js'; +export * from './click.js'; +export * from './drag.js'; +export * from './edgeless.js'; +export * from './keyboard.js'; +export * from './misc.js'; +export * from './selection.js'; diff --git a/blocksuite/tests-legacy/utils/actions/keyboard.ts b/blocksuite/tests-legacy/utils/actions/keyboard.ts new file mode 100644 index 0000000000000..3ea10d879be49 --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/keyboard.ts @@ -0,0 +1,241 @@ +import type { Page } from '@playwright/test'; + +const IS_MAC = process.platform === 'darwin'; +// const IS_WINDOWS = process.platform === 'win32'; +// const IS_LINUX = !IS_MAC && !IS_WINDOWS; + +/** + * The key will be 'Meta' on Macs and 'Control' on other platforms + * @example + * ```ts + * await page.keyboard.press(`${SHORT_KEY}+a`); + * ``` + */ +export const SHORT_KEY = IS_MAC ? 'Meta' : 'Control'; +/** + * The key will be 'Alt' on Macs and 'Shift' on other platforms + * @example + * ```ts + * await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+1`); + * ``` + */ +export const MODIFIER_KEY = IS_MAC ? 'Alt' : 'Shift'; + +export const SHIFT_KEY = 'Shift'; + +export async function type(page: Page, content: string, delay = 20) { + await page.keyboard.type(content, { delay }); +} + +export async function withPressKey( + page: Page, + key: string, + fn: () => Promise +) { + await page.keyboard.down(key); + await fn(); + await page.keyboard.up(key); +} + +export async function defaultTool(page: Page) { + await page.keyboard.press('v', { delay: 20 }); +} + +export async function pressBackspace(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press('Backspace', { delay: 20 }); + } +} + +export async function pressSpace(page: Page) { + await page.keyboard.press('Space', { delay: 20 }); +} + +export async function pressArrowLeft(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press('ArrowLeft', { delay: 20 }); + } +} +export async function pressArrowRight(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press('ArrowRight', { delay: 20 }); + } +} + +export async function pressArrowDown(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press('ArrowDown', { delay: 20 }); + } +} + +export async function pressArrowUp(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press('ArrowUp', { delay: 20 }); + } +} + +export async function pressArrowDownWithShiftKey(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press(`${SHIFT_KEY}+ArrowDown`, { delay: 20 }); + } +} + +export async function pressArrowUpWithShiftKey(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press(`${SHIFT_KEY}+ArrowUp`, { delay: 20 }); + } +} + +export async function pressEnter(page: Page, count = 1) { + // avoid flaky test by simulate real user input + for (let i = 0; i < count; i++) { + await page.keyboard.press('Enter', { delay: 30 }); + } +} + +export async function pressEnterWithShortkey(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+Enter`, { delay: 20 }); +} + +export async function pressEscape(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press('Escape', { delay: 20 }); + } +} + +export async function undoByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+z`, { delay: 20 }); +} + +export async function formatType(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+1`, { + delay: 20, + }); +} + +export async function redoByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+Shift+Z`, { delay: 20 }); +} + +export async function selectAllByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+a`, { + delay: 20, + }); +} + +export async function selectAllBlocksByKeyboard(page: Page) { + for (let i = 0; i < 3; i++) { + await selectAllByKeyboard(page); + } +} + +export async function pressTab(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press('Tab', { delay: 20 }); + } +} + +export async function pressShiftTab(page: Page) { + await page.keyboard.press('Shift+Tab', { delay: 20 }); +} + +export async function pressBackspaceWithShortKey(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press(`${SHORT_KEY}+Backspace`, { delay: 20 }); + } +} + +export async function pressShiftEnter(page: Page) { + await page.keyboard.press('Shift+Enter', { delay: 20 }); +} + +export async function inlineCode(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+e`, { delay: 20 }); +} + +export async function strikethrough(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+Shift+s`, { delay: 20 }); +} + +export async function copyByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+c`, { delay: 20 }); +} + +export async function cutByKeyboard(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+x`, { delay: 20 }); +} + +/** + * Notice: this method will try to click closest editor by default + */ +export async function pasteByKeyboard(page: Page, forceFocus = true) { + if (forceFocus) { + const isEditorActive = await page.evaluate(() => + document.activeElement?.closest('affine-editor-container') + ); + if (!isEditorActive) { + await page.click('affine-editor-container'); + } + } + + await page.keyboard.press(`${SHORT_KEY}+v`, { delay: 20 }); +} + +export async function createCodeBlock(page: Page) { + await page.keyboard.press(`${SHORT_KEY}+Alt+c`); +} + +export async function getCursorBlockIdAndHeight( + page: Page +): Promise<[string | null, number | null]> { + return page.evaluate(() => { + const selection = window.getSelection() as Selection; + + const range = selection.getRangeAt(0); + const startContainer = + range.startContainer instanceof Text + ? (range.startContainer.parentElement as HTMLElement) + : (range.startContainer as HTMLElement); + + const startComponent = startContainer.closest(`[data-block-id]`); + const { height } = (startComponent as HTMLElement).getBoundingClientRect(); + const id = (startComponent as HTMLElement).dataset.blockId!; + return [id, height]; + }); +} + +/** + * fill a line by keep triggering key input + * @param page + * @param toNext if true, fill until soft wrap + */ +export async function fillLine(page: Page, toNext = false) { + const [id, height] = await getCursorBlockIdAndHeight(page); + if (id && height) { + let nextHeight; + // type until current block height is changed, means has new line + do { + await page.keyboard.type('a', { delay: 20 }); + [, nextHeight] = await getCursorBlockIdAndHeight(page); + } while (nextHeight === height); + if (!toNext) { + await page.keyboard.press('Backspace'); + } + } +} + +export async function pressForwardDelete(page: Page) { + if (IS_MAC) { + await page.keyboard.press('Control+d', { delay: 20 }); + } else { + await page.keyboard.press('Delete', { delay: 20 }); + } +} + +export async function pressForwardDeleteWord(page: Page) { + if (IS_MAC) { + await page.keyboard.press('Alt+Delete', { delay: 20 }); + } else { + await page.keyboard.press('Control+Delete', { delay: 20 }); + } +} diff --git a/blocksuite/tests-legacy/utils/actions/linked-doc.ts b/blocksuite/tests-legacy/utils/actions/linked-doc.ts new file mode 100644 index 0000000000000..04ab36c2c9343 --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/linked-doc.ts @@ -0,0 +1,70 @@ +import { expect, type Page } from '@playwright/test'; + +import { pressEnter, type } from './keyboard.js'; + +export function getLinkedDocPopover(page: Page) { + const REFERENCE_NODE = ' ' as const; + const refNode = page.locator('affine-reference'); + const linkedDocPopover = page.locator('.linked-doc-popover'); + const pageBtn = linkedDocPopover.locator('.group > icon-button'); + + const findRefNode = async (title: string) => { + const refNode = page.locator(`affine-reference`, { + has: page.locator(`.affine-reference-title[data-title="${title}"]`), + }); + await expect(refNode).toBeVisible(); + return refNode; + }; + const assertExistRefText = async (text: string) => { + await expect(refNode).toBeVisible(); + const refTitleNode = refNode.locator('.affine-reference-title'); + // Since the text is in the pseudo element + // we need to use `toHaveAttribute` to assert it. + // And it's not a good strict way to assert the text. + await expect(refTitleNode).toHaveAttribute('data-title', text); + }; + + const createDoc = async ( + pageType: 'LinkedPage' | 'Subpage', + pageName?: string + ) => { + await type(page, '@'); + await expect(linkedDocPopover).toBeVisible(); + if (pageName) { + await type(page, pageName); + } else { + pageName = 'Untitled'; + } + + await page.keyboard.press('ArrowUp'); + if (pageType === 'LinkedPage') { + await page.keyboard.press('ArrowUp'); + } + await pressEnter(page); + return findRefNode(pageName); + }; + + const assertActivePageIdx = async (idx: number) => { + if (idx !== 0) { + await expect(pageBtn.nth(0)).toHaveAttribute('hover', 'false'); + } + await expect(pageBtn.nth(idx)).toHaveAttribute('hover', 'true'); + }; + + return { + REFERENCE_NODE, + linkedDocPopover, + refNode, + pageBtn, + + findRefNode, + assertExistRefText, + createLinkedDoc: async (pageName?: string) => + createDoc('LinkedPage', pageName), + /** + * @deprecated + */ + createSubpage: async (pageName?: string) => createDoc('Subpage', pageName), + assertActivePageIdx, + }; +} diff --git a/blocksuite/tests-legacy/utils/actions/misc.ts b/blocksuite/tests-legacy/utils/actions/misc.ts new file mode 100644 index 0000000000000..b71f6d7f198ca --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/misc.ts @@ -0,0 +1,1464 @@ +import '../declare-test-window.js'; + +import type { DatabaseBlockModel, ListType, RichText } from '@blocks/index.js'; +import type { EditorHost, ExtensionType } from '@blocksuite/block-std'; +import type { BlockSuiteFlags } from '@blocksuite/global/types'; +import { assertExists } from '@blocksuite/global/utils'; +import type { AffineEditorContainer } from '@blocksuite/presets'; +import type { InlineRange, InlineRootElement } from '@inline/index.js'; +import type { CustomFramePanel } from '@playground/apps/_common/components/custom-frame-panel.js'; +import type { CustomOutlinePanel } from '@playground/apps/_common/components/custom-outline-panel.js'; +import type { CustomOutlineViewer } from '@playground/apps/_common/components/custom-outline-viewer.js'; +import type { DocsPanel } from '@playground/apps/_common/components/docs-panel.js'; +import type { StarterDebugMenu } from '@playground/apps/_common/components/starter-debug-menu.js'; +import type { ConsoleMessage, Locator, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { BlockModel } from '@store/schema/index.js'; +import { uuidv4 } from '@store/utils/id-generator.js'; +import lz from 'lz-string'; + +import { currentEditorIndex, multiEditor } from '../multiple-editor.js'; +import { + pressArrowRight, + pressEnter, + pressEscape, + pressSpace, + pressTab, + selectAllBlocksByKeyboard, + SHORT_KEY, + type, +} from './keyboard.js'; + +declare global { + interface WindowEventMap { + 'blocksuite:doc-ready': CustomEvent; + } +} + +export const defaultPlaygroundURL = new URL( + `http://localhost:${process.env.CI ? 4173 : 5173}/starter/` +); + +const NEXT_FRAME_TIMEOUT = 50; +const DEFAULT_PLAYGROUND = defaultPlaygroundURL.toString(); +const RICH_TEXT_SELECTOR = '.inline-editor'; + +function generateRandomRoomId() { + return `playwright-${uuidv4()}`; +} + +export const getSelectionRect = async (page: Page): Promise => { + const rect = await page.evaluate(() => { + return getSelection()?.getRangeAt(0).getBoundingClientRect(); + }); + assertExists(rect); + return rect; +}; + +/** + * @example + * ```ts + * await initEmptyEditor(page, { enable_some_flag: true }); + * ``` + */ +async function initEmptyEditor({ + page, + flags = {}, + noInit = false, + multiEditor = false, +}: { + page: Page; + flags?: Partial; + noInit?: boolean; + multiEditor?: boolean; +}) { + await page.evaluate( + ([flags, noInit, multiEditor]) => { + const { collection } = window; + + async function waitForMountPageEditor( + doc: ReturnType + ) { + if (!doc.ready) doc.load(); + + if (!doc.root) { + await new Promise(resolve => doc.slots.rootAdded.once(resolve)); + } + + for (const [key, value] of Object.entries(flags)) { + doc.awarenessStore.setFlag(key as keyof typeof flags, value); + } + // add app root from https://github.com/toeverything/blocksuite/commit/947201981daa64c5ceeca5fd549460c34e2dabfa + const appRoot = document.querySelector('#app'); + if (!appRoot) { + throw new Error('Cannot find app root element(#app).'); + } + const createEditor = () => { + const editor = document.createElement('affine-editor-container'); + editor.doc = doc; + editor.autofocus = true; + const defaultExtensions: ExtensionType[] = [ + ...window.$blocksuite.defaultExtensions(), + { + setup: di => { + di.addImpl(window.$blocksuite.identifiers.ParseDocUrlService, { + parseDocUrl() { + return undefined; + }, + }); + }, + }, + { + setup: di => { + di.override( + window.$blocksuite.identifiers.DocModeProvider, + window.$blocksuite.mockServices.mockDocModeService( + () => editor.mode, + mode => editor.switchEditor(mode) + ) + ); + }, + }, + ]; + editor.pageSpecs = [...editor.pageSpecs, ...defaultExtensions]; + editor.edgelessSpecs = [ + ...editor.edgelessSpecs, + ...defaultExtensions, + ]; + + editor.std + .get(window.$blocksuite.identifiers.RefNodeSlotsProvider) + .docLinkClicked.on(({ pageId: docId }) => { + const newDoc = collection.getDoc(docId); + if (!newDoc) { + throw new Error(`Failed to jump to page ${docId}`); + } + editor.doc = newDoc; + }); + appRoot.append(editor); + return editor; + }; + + const editor = createEditor(); + if (multiEditor) createEditor(); + + editor.updateComplete + .then(() => { + const debugMenu: StarterDebugMenu = + document.createElement('starter-debug-menu'); + const docsPanel: DocsPanel = document.createElement('docs-panel'); + const framePanel: CustomFramePanel = + document.createElement('custom-frame-panel'); + const outlinePanel: CustomOutlinePanel = document.createElement( + 'custom-outline-panel' + ); + const outlineViewer: CustomOutlineViewer = document.createElement( + 'custom-outline-viewer' + ); + docsPanel.editor = editor; + framePanel.editor = editor; + outlinePanel.editor = editor; + outlineViewer.editor = editor; + debugMenu.collection = collection; + debugMenu.editor = editor; + debugMenu.docsPanel = docsPanel; + debugMenu.framePanel = framePanel; + debugMenu.outlineViewer = outlineViewer; + debugMenu.outlinePanel = outlinePanel; + const leftSidePanel = document.createElement('left-side-panel'); + debugMenu.leftSidePanel = leftSidePanel; + document.body.append(debugMenu); + document.body.append(leftSidePanel); + document.body.append(framePanel); + document.body.append(outlinePanel); + document.body.append(outlineViewer); + + window.debugMenu = debugMenu; + window.editor = editor; + window.doc = doc; + Object.defineProperty(globalThis, 'host', { + get() { + return document.querySelector('editor-host'); + }, + }); + Object.defineProperty(globalThis, 'std', { + get() { + return document.querySelector('editor-host')?.std; + }, + }); + window.dispatchEvent( + new CustomEvent('blocksuite:doc-ready', { detail: doc.id }) + ); + }) + .catch(console.error); + } + + if (noInit) { + const firstDoc = collection.docs.values().next().value?.getDoc() as + | ReturnType + | undefined; + if (firstDoc) { + window.doc = firstDoc; + waitForMountPageEditor(firstDoc).catch; + } else { + collection.slots.docAdded.on(docId => { + const doc = collection.getDoc(docId); + if (!doc) { + throw new Error(`Failed to get doc ${docId}`); + } + window.doc = doc; + waitForMountPageEditor(doc).catch(console.error); + }); + } + } else { + collection.meta.initialize(); + const doc = collection.createDoc({ id: 'doc:home' }); + window.doc = doc; + waitForMountPageEditor(doc).catch(console.error); + } + }, + [flags, noInit, multiEditor] as const + ); + await waitNextFrame(page); +} + +export const getEditorLocator = (page: Page) => { + return page.locator('affine-editor-container').nth(currentEditorIndex); +}; + +export const getEditorHostLocator = (page: Page) => { + return page.locator('editor-host').nth(currentEditorIndex); +}; + +type TaggedConsoleMessage = ConsoleMessage & { __ignore?: boolean }; +function ignoredLog(message: ConsoleMessage) { + (message as TaggedConsoleMessage).__ignore = true; +} +function isIgnoredLog( + message: ConsoleMessage +): message is TaggedConsoleMessage { + return '__ignore' in message && !!message.__ignore; +} + +/** + * Expect console message to be called in the test. + * + * This function **should** be called before the `enterPlaygroundRoom` function! + * + * ```ts + * expectConsoleMessage(page, 'Failed to load resource'); // Default type is 'error' + * expectConsoleMessage(page, '[vite] connected.', 'warning'); // Specify type + * ``` + */ +export function expectConsoleMessage( + page: Page, + logPrefixOrRegex: string | RegExp, + type: + | 'log' + | 'debug' + | 'info' + | 'error' + | 'warning' + | 'dir' + | 'dirxml' + | 'table' + | 'trace' + | 'clear' + | 'startGroup' + | 'startGroupCollapsed' + | 'endGroup' + | 'assert' + | 'profile' + | 'profileEnd' + | 'count' + | 'timeEnd' = 'error' +) { + page.on('console', (message: ConsoleMessage) => { + const sameType = message.type() === type; + const textMatch = + logPrefixOrRegex instanceof RegExp + ? logPrefixOrRegex.test(message.text()) + : message.text().startsWith(logPrefixOrRegex); + if (sameType && textMatch) { + ignoredLog(message); + } + }); +} + +export type PlaygroundRoomOptions = { + flags?: Partial; + room?: string; + blobSource?: ('idb' | 'mock')[]; + noInit?: boolean; +}; +export async function enterPlaygroundRoom( + page: Page, + ops?: PlaygroundRoomOptions +) { + const url = new URL(DEFAULT_PLAYGROUND); + let room = ops?.room; + const blobSource = ops?.blobSource; + if (!room) { + room = generateRandomRoomId(); + } + url.searchParams.set('room', room); + url.searchParams.set('blobSource', blobSource?.join(',') || 'idb'); + await page.goto(url.toString()); + + // See https://github.com/microsoft/playwright/issues/5546 + page.on('console', message => { + if ( + [ + '', + // React devtools: + '%cDownload the React DevTools for a better development experience: https://reactjs.org/link/react-devtools font-weight:bold', + // Vite: + '[vite] connected.', + '[vite] connecting...', + // Figma embed: + 'Fullscreen: Using 4GB WASM heap', + // Lit: + 'Lit is in dev mode. Not recommended for production! See https://lit.dev/msg/dev-mode for more information.', + // Figma embed: + 'Running frontend commit', + ].includes(message.text()) + ) { + return; + } + const ignore = isIgnoredLog(message) || !process.env.CI; + if (!ignore) { + expect + .soft('Unexpected console message: ' + message.text()) + .toBe( + 'Please remove the "console.log" or declare `expectConsoleMessage` before `enterPlaygroundRoom`. It is advised not to output logs in a production environment.' + ); + } + console.log(`Console ${message.type()}: ${message.text()}`); + }); + + // Log all uncaught errors + page.on('pageerror', exception => { + throw new Error(`Uncaught exception: "${exception}"\n${exception.stack}`); + }); + + await initEmptyEditor({ + page, + flags: ops?.flags, + noInit: ops?.noInit, + multiEditor, + }); + + const locator = page.locator('affine-editor-container'); + await locator.isVisible(); + await page.evaluate(async () => { + const dom = document.querySelector( + 'affine-editor-container' + ); + if (dom) { + await dom.updateComplete; + } + }); + + await page.evaluate(() => { + if (typeof window.$blocksuite !== 'object') { + throw new Error('window.$blocksuite is not object'); + } + }, []); + return room; +} + +export async function waitDefaultPageLoaded(page: Page) { + await page.waitForSelector('affine-page-root[data-block-id="0"]'); +} + +export async function waitEmbedLoaded(page: Page) { + await page.waitForSelector('.resizable-img'); +} + +export async function waitNextFrame( + page: Page, + frameTimeout = NEXT_FRAME_TIMEOUT +) { + await page.waitForTimeout(frameTimeout); +} + +export async function clearLog(page: Page) { + await page.evaluate(() => console.clear()); +} + +export async function captureHistory(page: Page) { + await page.evaluate(() => { + window.doc.captureSync(); + }); +} + +export async function resetHistory(page: Page) { + await page.evaluate(() => { + const space = window.doc; + space.resetHistory(); + }); +} + +// XXX: This doesn't add surface yet, the page state should not be switched to edgeless. +export async function enterPlaygroundWithList( + page: Page, + contents: string[] = ['', '', ''], + type: ListType = 'bulleted' +) { + const room = generateRandomRoomId(); + await page.goto(`${DEFAULT_PLAYGROUND}?room=${room}`); + await initEmptyEditor({ page }); + + await page.evaluate( + ({ contents, type }: { contents: string[]; type: ListType }) => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const noteId = doc.addBlock('affine:note', {}, rootId); + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < contents.length; i++) { + doc.addBlock( + 'affine:list', + contents.length > 0 + ? { text: new doc.Text(contents[i]), type } + : { type }, + noteId + ); + } + }, + { contents, type } + ); + await waitNextFrame(page); +} + +// XXX: This doesn't add surface yet, the doc state should not be switched to edgeless. +export async function initEmptyParagraphState(page: Page, rootId?: string) { + const ids = await page.evaluate(rootId => { + const { doc } = window; + doc.captureSync(); + if (!rootId) { + rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + } + + const noteId = doc.addBlock('affine:note', {}, rootId); + const paragraphId = doc.addBlock('affine:paragraph', {}, noteId); + // doc.addBlock('affine:surface', {}, rootId); + doc.captureSync(); + + return { rootId, noteId, paragraphId }; + }, rootId); + return ids; +} + +export async function initMultipleNoteWithParagraphState( + page: Page, + rootId?: string, + count = 2 +) { + const ids = await page.evaluate( + ({ rootId, count }) => { + const { doc } = window; + doc.captureSync(); + if (!rootId) { + rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + } + + const ids = Array.from({ length: count }) + .fill(0) + .map(() => { + const noteId = doc.addBlock('affine:note', {}, rootId); + const paragraphId = doc.addBlock('affine:paragraph', {}, noteId); + return { noteId, paragraphId }; + }); + + // doc.addBlock('affine:surface', {}, rootId); + doc.captureSync(); + + return { rootId, ids }; + }, + { rootId, count } + ); + return ids; +} + +export async function initEmptyEdgelessState(page: Page) { + const ids = await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + doc.addBlock('affine:surface', {}, rootId); + const noteId = doc.addBlock('affine:note', {}, rootId); + const paragraphId = doc.addBlock('affine:paragraph', {}, noteId); + + doc.resetHistory(); + + return { rootId, noteId, paragraphId }; + }); + return ids; +} + +export async function initEmptyDatabaseState(page: Page, rootId?: string) { + const ids = await page.evaluate(async rootId => { + const { doc } = window; + doc.captureSync(); + if (!rootId) { + rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + } + const noteId = doc.addBlock('affine:note', {}, rootId); + const databaseId = doc.addBlock( + 'affine:database', + { + title: new doc.Text('Database 1'), + }, + noteId + ); + const model = doc.getBlockById(databaseId) as DatabaseBlockModel; + await new Promise(resolve => setTimeout(resolve, 100)); + const databaseBlock = document.querySelector('affine-database'); + const databaseService = databaseBlock?.service; + if (databaseService) { + databaseService.databaseViewInitEmpty( + model, + databaseService.viewPresets.tableViewMeta.type + ); + databaseService.applyColumnUpdate(model); + } + + doc.captureSync(); + return { rootId, noteId, databaseId }; + }, rootId); + return ids; +} + +export async function initKanbanViewState( + page: Page, + config: { + rows: string[]; + columns: { type: string; value?: unknown[] }[]; + }, + rootId?: string +) { + const ids = await page.evaluate( + async ({ rootId, config }) => { + const { doc } = window; + + doc.captureSync(); + if (!rootId) { + rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + } + const noteId = doc.addBlock('affine:note', {}, rootId); + const databaseId = doc.addBlock( + 'affine:database', + { + title: new doc.Text('Database 1'), + }, + noteId + ); + const model = doc.getBlockById(databaseId) as DatabaseBlockModel; + await new Promise(resolve => setTimeout(resolve, 100)); + const databaseBlock = document.querySelector('affine-database'); + const databaseService = databaseBlock?.service; + if (databaseService) { + const rowIds = config.rows.map(rowText => { + const rowId = doc.addBlock( + 'affine:paragraph', + { type: 'text', text: new doc.Text(rowText) }, + databaseId + ); + return rowId; + }); + config.columns.forEach(column => { + const columnId = databaseService.addColumn(model, 'end', { + data: {}, + name: column.type, + type: column.type, + }); + rowIds.forEach((rowId, index) => { + const value = column.value?.[index]; + if (value !== undefined) { + databaseService.updateCell(model, rowId, { + columnId, + value: + column.type === 'rich-text' + ? new doc.Text(value as string) + : value, + }); + } + }); + }); + databaseService.databaseViewInitEmpty( + model, + databaseService.viewPresets.kanbanViewMeta.type + ); + databaseService.applyColumnUpdate(model); + } + doc.captureSync(); + return { rootId, noteId, databaseId }; + }, + { rootId, config } + ); + return ids; +} + +export async function initEmptyDatabaseWithParagraphState( + page: Page, + rootId?: string +) { + const ids = await page.evaluate(async rootId => { + const { doc } = window; + doc.captureSync(); + if (!rootId) { + rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + } + const noteId = doc.addBlock('affine:note', {}, rootId); + const databaseId = doc.addBlock( + 'affine:database', + { + title: new doc.Text('Database 1'), + }, + noteId + ); + const model = doc.getBlockById(databaseId) as DatabaseBlockModel; + await new Promise(resolve => setTimeout(resolve, 100)); + const databaseBlock = document.querySelector('affine-database'); + const databaseService = databaseBlock?.service; + if (databaseService) { + databaseService.databaseViewInitEmpty( + model, + databaseService.viewPresets.tableViewMeta.type + ); + databaseService.applyColumnUpdate(model); + } + doc.addBlock('affine:paragraph', {}, noteId); + + doc.captureSync(); + return { rootId, noteId, databaseId }; + }, rootId); + return ids; +} + +export async function initDatabaseRow(page: Page) { + const editorHost = getEditorHostLocator(page); + const addRow = editorHost.locator('.data-view-table-group-add-row'); + await addRow.click(); +} + +export async function initDatabaseRowWithData(page: Page, data: string) { + await initDatabaseRow(page); + await waitNextFrame(page, 50); + await type(page, data); +} +export const getAddRow = (page: Page): Locator => { + return page.locator('.data-view-table-group-add-row'); +}; +export async function initDatabaseDynamicRowWithData( + page: Page, + data: string, + addRow = false, + index = 0 +) { + const editorHost = getEditorHostLocator(page); + if (addRow) { + await initDatabaseRow(page); + await waitNextFrame(page, 100); + await pressEscape(page); + } + const lastRow = editorHost.locator('.affine-database-block-row').last(); + const cell = lastRow.locator('.database-cell').nth(index + 1); + await cell.click(); + await waitNextFrame(page); + await pressEnter(page); + await waitNextFrame(page); + await type(page, data); + await pressEnter(page); +} + +export async function focusDatabaseTitle(page: Page) { + const dbTitle = page.locator('[data-block-is-database-title="true"]'); + await dbTitle.click(); + + await page.evaluate(() => { + const dbTitle = document.querySelector( + 'affine-database-title textarea' + ) as HTMLTextAreaElement | null; + if (!dbTitle) { + throw new Error('Cannot find database title'); + } + + dbTitle.focus(); + }); + await selectAllBlocksByKeyboard(page); + await pressArrowRight(page); + await waitNextFrame(page); +} + +export async function assertDatabaseColumnOrder(page: Page, order: string[]) { + const columns = await page + .locator('affine-database-column-header') + .locator('affine-database-header-column') + .all(); + expect(await Promise.all(columns.slice(1).map(v => v.innerText()))).toEqual( + order + ); +} + +export async function initEmptyCodeBlockState( + page: Page, + codeBlockProps = {} as { language?: string } +) { + const ids = await page.evaluate(codeBlockProps => { + const { doc } = window; + doc.captureSync(); + const rootId = doc.addBlock('affine:page'); + const noteId = doc.addBlock('affine:note', {}, rootId); + const codeBlockId = doc.addBlock('affine:code', codeBlockProps, noteId); + doc.captureSync(); + + return { rootId, noteId, codeBlockId }; + }, codeBlockProps); + await page.waitForSelector(`[data-block-id="${ids.codeBlockId}"] rich-text`); + return ids; +} + +type FocusRichTextOptions = { + clickPosition?: { x: number; y: number }; +}; + +export async function focusRichText( + page: Page, + i = 0, + options?: FocusRichTextOptions +) { + await page.mouse.move(0, 0); + const editor = getEditorHostLocator(page); + const locator = editor.locator(RICH_TEXT_SELECTOR).nth(i); + // need to set `force` to true when clicking on `affine-selected-blocks` + await locator.click({ force: true, position: options?.clickPosition }); +} + +export async function focusRichTextEnd(page: Page, i = 0) { + await page.evaluate( + ([i, currentEditorIndex]) => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const richTexts = Array.from(editorHost.querySelectorAll('rich-text')); + + richTexts[i].inlineEditor?.focusEnd(); + }, + [i, currentEditorIndex] + ); + await waitNextFrame(page); +} + +export async function initThreeParagraphs(page: Page) { + await focusRichText(page); + await type(page, '123'); + await pressEnter(page); + await type(page, '456'); + await pressEnter(page); + await type(page, '789'); + await resetHistory(page); +} + +export async function initSixParagraphs(page: Page) { + await focusRichText(page); + await type(page, '1'); + await pressEnter(page); + await type(page, '2'); + await pressEnter(page); + await type(page, '3'); + await pressEnter(page); + await type(page, '4'); + await pressEnter(page); + await type(page, '5'); + await pressEnter(page); + await type(page, '6'); + await resetHistory(page); +} + +export async function initThreeLists(page: Page) { + await focusRichText(page); + await type(page, '-'); + await pressSpace(page); + await type(page, '123'); + await pressEnter(page); + await type(page, '456'); + await pressEnter(page); + await pressTab(page); + await type(page, '789'); +} + +export async function insertThreeLevelLists(page: Page, i = 0) { + await focusRichText(page, i); + await type(page, '-'); + await pressSpace(page); + await type(page, '123'); + await pressEnter(page); + await pressTab(page); + await type(page, '456'); + await pressEnter(page); + await pressTab(page); + await type(page, '789'); +} + +export async function initThreeDividers(page: Page) { + await focusRichText(page); + await type(page, '123'); + await pressEnter(page); + await type(page, '---'); + await pressSpace(page); + await type(page, '---'); + await pressSpace(page); + await type(page, '---'); + await pressSpace(page); + await type(page, '123'); +} + +export async function initParagraphsByCount(page: Page, count: number) { + await focusRichText(page); + for (let i = 0; i < count; i++) { + await type(page, `paragraph ${i}`); + await pressEnter(page); + } + await resetHistory(page); +} + +export async function getInlineSelectionIndex(page: Page) { + return page.evaluate(() => { + const selection = window.getSelection() as Selection; + + const range = selection.getRangeAt(0); + const component = range.startContainer.parentElement?.closest('rich-text'); + const index = component?.inlineEditor?.getInlineRange()?.index; + return index !== undefined ? index : -1; + }); +} + +export async function getInlineSelectionText(page: Page) { + return page.evaluate(() => { + const selection = window.getSelection() as Selection; + const range = selection.getRangeAt(0); + const component = range.startContainer.parentElement?.closest('rich-text'); + return component?.inlineEditor?.yText.toString() ?? ''; + }); +} + +export async function getSelectedTextByInlineEditor(page: Page) { + return page.evaluate(() => { + const selection = window.getSelection() as Selection; + const range = selection.getRangeAt(0); + const component = range.startContainer.parentElement?.closest('rich-text'); + + const inlineRange = component?.inlineEditor?.getInlineRange(); + if (!inlineRange) return ''; + + const { index, length } = inlineRange; + return ( + component?.inlineEditor?.yText.toString().slice(index, index + length) || + '' + ); + }); +} + +export async function getSelectedText(page: Page) { + return page.evaluate(() => { + let content = ''; + const selection = window.getSelection() as Selection; + + if (selection.rangeCount === 0) return content; + + const range = selection.getRangeAt(0); + const components = + range.commonAncestorContainer.parentElement?.querySelectorAll( + 'rich-text' + ) || []; + + components.forEach(component => { + const inlineRange = component.inlineEditor?.getInlineRange(); + if (!inlineRange) return; + const { index, length } = inlineRange; + content += + component?.inlineEditor?.yText + .toString() + .slice(index, index + length) || ''; + }); + + return content; + }); +} + +export async function setInlineRangeInSelectedRichText( + page: Page, + index: number, + length: number +) { + await page.evaluate( + ({ index, length }) => { + const selection = window.getSelection() as Selection; + + const range = selection.getRangeAt(0); + const component = + range.startContainer.parentElement?.closest('rich-text'); + component?.inlineEditor?.setInlineRange({ + index, + length, + }); + }, + { index, length } + ); + await waitNextFrame(page); +} + +export async function setInlineRangeInInlineEditor( + page: Page, + inlineRange: InlineRange, + i = 0 +) { + await page.evaluate( + ({ i, inlineRange }) => { + const inlineEditor = document.querySelectorAll( + '[data-v-root="true"]' + )[i]?.inlineEditor; + if (!inlineEditor) { + throw new Error('Cannot find inline editor'); + } + inlineEditor.setInlineRange(inlineRange); + }, + { i, inlineRange } + ); + await waitNextFrame(page); +} + +export async function pasteContent( + page: Page, + clipData: Record +) { + await page.evaluate( + ({ clipData }) => { + const e = new ClipboardEvent('paste', { + clipboardData: new DataTransfer(), + }); + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + Object.keys(clipData).forEach(key => { + e.clipboardData?.setData(key, clipData[key] as string); + }); + document.dispatchEvent(e); + }, + { clipData } + ); + await waitNextFrame(page); +} + +export async function pasteTestImage(page: Page) { + await page.evaluate(async () => { + const imageBlob = await fetch(`${location.origin}/test-card-1.png`).then( + response => response.blob() + ); + + const imageFile = new File([imageBlob], 'test-card-1.png', { + type: 'image/png', + }); + + const e = new ClipboardEvent('paste', { + clipboardData: new DataTransfer(), + }); + + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + + e.clipboardData?.items.add(imageFile); + document.dispatchEvent(e); + }); + await waitNextFrame(page); +} + +export async function getClipboardHTML(page: Page) { + const dataInClipboard = await page.evaluate(async () => { + function format(node: HTMLElement, level: number) { + const indentBefore = ' '.repeat(level++); + const indentAfter = ' '.repeat(level >= 2 ? level - 2 : 0); + let textNode; + + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < node.children.length; i++) { + textNode = document.createTextNode('\n' + indentBefore); + node.insertBefore(textNode, node.children[i]); + + format(node.children[i] as HTMLElement, level); + + if (node.lastElementChild == node.children[i]) { + textNode = document.createTextNode('\n' + indentAfter); + node.append(textNode); + } + } + + return node; + } + const clipItems = await navigator.clipboard.read(); + const item = clipItems.find(item => item.types.includes('text/html')); + const data = await item?.getType('text/html'); + const text = await data?.text(); + const html = new DOMParser().parseFromString(text ?? '', 'text/html'); + const container = html.querySelector( + '[data-blocksuite-snapshot]' + ); + if (!container) { + return ''; + } + return format(container, 0).innerHTML.trim(); + }); + + return dataInClipboard; +} + +export async function getClipboardText(page: Page) { + const dataInClipboard = await page.evaluate(async () => { + const clipItems = await navigator.clipboard.read(); + const item = clipItems.find(item => item.types.includes('text/plain')); + const data = await item?.getType('text/plain'); + const text = await data?.text(); + return text ?? ''; + }); + return dataInClipboard; +} + +export async function getClipboardCustomData(page: Page, type: string) { + const dataInClipboard = await page.evaluate(async () => { + const clipItems = await navigator.clipboard.read(); + const item = clipItems.find(item => item.types.includes('text/html')); + const data = await item?.getType('text/html'); + const text = await data?.text(); + const html = new DOMParser().parseFromString(text ?? '', 'text/html'); + const container = html.querySelector( + '[data-blocksuite-snapshot]' + ); + return container?.dataset.blocksuiteSnapshot ?? ''; + }); + + const decompressed = lz.decompressFromEncodedURIComponent(dataInClipboard); + let json: Record | null = null; + try { + json = JSON.parse(decompressed); + } catch { + throw new Error(`Invalid snapshot in clipboard: ${dataInClipboard}`); + } + + return json?.[type]; +} + +export async function getClipboardSnapshot(page: Page) { + const dataInClipboard = await getClipboardCustomData( + page, + 'BLOCKSUITE/SNAPSHOT' + ); + assertExists(dataInClipboard); + const json = JSON.parse(dataInClipboard as string); + return json; +} + +export async function getPageSnapshot(page: Page, toJSON?: boolean) { + const json = await page.evaluate(() => { + const { job, doc } = window; + const snapshot = job.docToSnapshot(doc); + if (!snapshot) { + throw new Error('Failed to get snapshot'); + } + return snapshot.blocks; + }); + if (toJSON) { + return JSON.stringify(json, null, 2); + } + return json; +} + +export async function setSelection( + page: Page, + anchorBlockId: number, + anchorOffset: number, + focusBlockId: number, + focusOffset: number +) { + await page.evaluate( + ({ + anchorBlockId, + anchorOffset, + focusBlockId, + focusOffset, + currentEditorIndex, + }) => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const anchorRichText = editorHost.querySelector( + `[data-block-id="${anchorBlockId}"] rich-text` + )!; + const anchorRichTextRange = anchorRichText.inlineEditor!.toDomRange({ + index: anchorOffset, + length: 0, + })!; + const focusRichText = editorHost.querySelector( + `[data-block-id="${focusBlockId}"] rich-text` + )!; + const focusRichTextRange = focusRichText.inlineEditor!.toDomRange({ + index: focusOffset, + length: 0, + })!; + + const sl = getSelection(); + if (!sl) throw new Error('Cannot get selection'); + const range = document.createRange(); + range.setStart( + anchorRichTextRange.startContainer, + anchorRichTextRange.startOffset + ); + range.setEnd( + focusRichTextRange.startContainer, + focusRichTextRange.startOffset + ); + sl.removeAllRanges(); + sl.addRange(range); + }, + { + anchorBlockId, + anchorOffset, + focusBlockId, + focusOffset, + currentEditorIndex, + } + ); +} + +export async function readClipboardText( + page: Page, + type: 'input' | 'textarea' = 'input' +) { + const id = 'clipboard-test'; + const selector = `#${id}`; + await page.evaluate( + ({ type, id }) => { + const input = document.createElement(type); + input.setAttribute('id', id); + document.body.append(input); + }, + { type, id } + ); + const input = page.locator(selector); + await input.focus(); + await page.keyboard.press(`${SHORT_KEY}+v`); + const text = await input.inputValue(); + await page.evaluate( + ({ selector }) => { + const input = document.querySelector(selector); + input?.remove(); + }, + { selector } + ); + return text; +} + +export const getCenterPositionByLocator: ( + page: Page, + locator: Locator +) => Promise<{ x: number; y: number }> = async ( + _page: Page, + locator: Locator +) => { + const box = await locator.boundingBox(); + if (!box) { + throw new Error("Failed to getCenterPosition! Can't get bounding box"); + } + return { + x: box.x + box.width / 2, + y: box.y + box.height / 2, + }; +}; + +/** + * @deprecated Use `page.locator(selector).boundingBox()` instead + */ +export const getBoundingClientRect: ( + page: Page, + selector: string +) => Promise = async (page: Page, selector: string) => { + return page.evaluate((selector: string) => { + return document.querySelector(selector)?.getBoundingClientRect() as DOMRect; + }, selector); +}; + +export async function getBoundingBox(locator: Locator) { + const box = await locator.boundingBox(); + if (!box) throw new Error('Missing column box'); + return box; +} + +export async function getBlockModel( + page: Page, + blockId: string +) { + const result: BlockModel | null | undefined = await page.evaluate(blockId => { + return window.doc?.getBlock(blockId)?.model; + }, blockId); + expect(result).not.toBeNull(); + return result as Model; +} + +export async function getIndexCoordinate( + page: Page, + [richTextIndex, vIndex]: [number, number], + coordOffSet: { x: number; y: number } = { x: 0, y: 0 } +) { + const coord = await page.evaluate( + ({ richTextIndex, vIndex, coordOffSet, currentEditorIndex }) => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const richText = editorHost.querySelectorAll('rich-text')[ + richTextIndex + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any; + const domRange = richText.inlineEditor.toDomRange({ + index: vIndex, + length: 0, + }); + const pointBound = domRange.getBoundingClientRect(); + return { + x: pointBound.left + coordOffSet.x, + y: pointBound.top + pointBound.height / 2 + coordOffSet.y, + }; + }, + { + richTextIndex, + vIndex, + coordOffSet, + currentEditorIndex, + } + ); + return coord; +} + +export function inlineEditorInnerTextToString(innerText: string): string { + return innerText.replace('\u200B', '').trim(); +} + +export async function focusTitle(page: Page) { + await page.locator('doc-title rich-text').click(); + await page.evaluate(i => { + const docTitle = document.querySelectorAll('doc-title')[i]; + if (!docTitle) { + throw new Error('Doc title component not found'); + } + const docTitleRichText = docTitle.querySelector('rich-text'); + if (!docTitleRichText) { + throw new Error('Doc title rich text component not found'); + } + if (!docTitleRichText.inlineEditor) { + throw new Error('Doc title inline editor not found'); + } + docTitleRichText.inlineEditor.focusEnd(); + }, currentEditorIndex); + await waitNextFrame(page, 200); +} + +/** + * XXX: this is a workaround for the bug in Playwright + */ +export async function shamefullyBlurActiveElement(page: Page) { + await page.evaluate(() => { + if ( + !document.activeElement || + !(document.activeElement instanceof HTMLElement) + ) { + throw new Error("document.activeElement doesn't exist"); + } + document.activeElement.blur(); + }); +} + +/** + * FIXME: + * Sometimes inline editor state is not updated in time. Bad case like below: + * + * ``` + * await focusRichText(page); + * await type(page, 'hello'); + * await assertRichTexts(page, ['hello']); + * ``` + * + * output(failed or flaky): + * + * ``` + * - Expected - 1 + * + Received + 1 + * Array [ + * - "hello", + * + "ello", + * ] + * ``` + * + */ +export async function waitForInlineEditorStateUpdated(page: Page) { + return page.evaluate(async () => { + const selection = window.getSelection() as Selection; + + if (selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + const component = range.startContainer.parentElement?.closest('rich-text'); + await component?.inlineEditor?.waitForUpdate(); + }); +} + +export async function initImageState(page: Page, prependParagraph = false) { + await page.evaluate(async prepend => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const noteId = doc.addBlock('affine:note', {}, rootId); + + await new Promise(res => setTimeout(res, 200)); + + const pageRoot = document.querySelector('affine-page-root'); + if (!pageRoot) throw new Error('Cannot find doc page'); + const imageBlob = await fetch(`${location.origin}/test-card-1.png`).then( + response => response.blob() + ); + const storage = pageRoot.doc.blobSync; + const sourceId = await storage.set(imageBlob); + if (prepend) { + doc.addBlock('affine:paragraph', {}, noteId); + } + const imageId = doc.addBlock( + 'affine:image', + { + sourceId, + }, + noteId + ); + + doc.resetHistory(); + + return { rootId, noteId, imageId }; + }, prependParagraph); + + // due to pasting img calls fetch, so we need timeout for downloading finished. + await page.waitForTimeout(500); +} + +export async function getCurrentEditorDocId(page: Page) { + return page.evaluate(index => { + const editor = document.querySelectorAll('affine-editor-container')[index]; + if (!editor) throw new Error("Can't find affine-editor-container"); + const docId = editor.doc.id; + return docId; + }, currentEditorIndex); +} + +export async function getCurrentHTMLTheme(page: Page) { + const root = page.locator('html'); + // eslint-disable-next-line unicorn/prefer-dom-node-dataset + return root.getAttribute('data-theme'); +} + +export async function getCurrentEditorTheme(page: Page) { + const mode = await page + .locator('affine-editor-container') + .first() + .evaluate(() => + window + .getComputedStyle(document.documentElement) + .getPropertyValue('--affine-theme-mode') + .trim() + ); + return mode; +} + +export async function getCurrentThemeCSSPropertyValue( + page: Page, + property: string +) { + const value = await page + .locator('affine-editor-container') + .evaluate( + (_, property) => + window + .getComputedStyle(document.documentElement) + .getPropertyValue(property) + .trim(), + property + ); + return value; +} + +export async function scrollToTop(page: Page) { + await page.mouse.wheel(0, -1000); + + await page.waitForFunction(() => { + const scrollContainer = document.querySelector('.affine-page-viewport'); + if (!scrollContainer) { + throw new Error("Can't find scroll container"); + } + return scrollContainer.scrollTop < 10; + }); +} + +export async function scrollToBottom(page: Page) { + // await page.mouse.wheel(0, 1000); + + await page + .locator('.affine-page-viewport') + .evaluate(node => + node.scrollTo({ left: 0, top: 1000, behavior: 'smooth' }) + ); + // TODO switch to `scrollend` + // See https://developer.chrome.com/en/blog/scrollend-a-new-javascript-event/ + await page.waitForFunction(() => { + const scrollContainer = document.querySelector('.affine-page-viewport'); + if (!scrollContainer) { + throw new Error("Can't find scroll container"); + } + + return ( + // Wait for scrolled to the bottom + // Refer to https://stackoverflow.com/questions/3898130/check-if-a-user-has-scrolled-to-the-bottom-not-just-the-window-but-any-element + Math.abs( + scrollContainer.scrollHeight - + scrollContainer.scrollTop - + scrollContainer.clientHeight + ) < 10 + ); + }); +} + +export async function mockParseDocUrlService( + page: Page, + mapping: Record +) { + await page.evaluate(mapping => { + const parseDocUrlService = window.host.std.get( + window.$blocksuite.identifiers.ParseDocUrlService + ); + parseDocUrlService.parseDocUrl = (url: string) => { + const docId = mapping[url]; + if (docId) { + return { docId }; + } + return; + }; + }, mapping); +} diff --git a/blocksuite/tests-legacy/utils/actions/selection.ts b/blocksuite/tests-legacy/utils/actions/selection.ts new file mode 100644 index 0000000000000..7a45259103aab --- /dev/null +++ b/blocksuite/tests-legacy/utils/actions/selection.ts @@ -0,0 +1,45 @@ +import type { Page } from '@playwright/test'; + +export async function getRichTextBoundingBox( + page: Page, + blockId: string +): Promise { + return page.evaluate(id => { + const paragraph = document.querySelector( + `[data-block-id="${id}"] .inline-editor` + ); + const bbox = paragraph?.getBoundingClientRect() as DOMRect; + return bbox; + }, blockId); +} + +interface Rect { + x: number; + y: number; + width: number; + height: number; +} + +export async function clickInEdge(page: Page, rect: Rect) { + const edgeX = rect.x + rect.width / 2; + const edgeY = rect.y + rect.height - 5; + await page.mouse.click(edgeX, edgeY); +} + +export async function clickInCenter(page: Page, rect: Rect) { + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + await page.mouse.click(centerX, centerY); +} + +export async function getBoundingRect( + page: Page, + selector: string +): Promise { + const div = page.locator(selector); + const boundingRect = await div.boundingBox(); + if (!boundingRect) { + throw new Error(`Missing ${selector}`); + } + return boundingRect; +} diff --git a/blocksuite/tests-legacy/utils/asserts.ts b/blocksuite/tests-legacy/utils/asserts.ts new file mode 100644 index 0000000000000..225694c6977de --- /dev/null +++ b/blocksuite/tests-legacy/utils/asserts.ts @@ -0,0 +1,1344 @@ +import './declare-test-window.js'; + +import type { + AffineInlineEditor, + NoteBlockModel, + RichText, + RootBlockModel, +} from '@blocks/index.js'; +import { + DEFAULT_NOTE_HEIGHT, + DEFAULT_NOTE_WIDTH, +} from '@blocksuite/affine-model'; +import type { BlockComponent, EditorHost } from '@blocksuite/block-std'; +import { BLOCK_ID_ATTR } from '@blocksuite/block-std'; +import { assertExists } from '@blocksuite/global/utils'; +import type { InlineRootElement } from '@inline/inline-editor.js'; +import { expect, type Locator, type Page } from '@playwright/test'; +import { COLLECTION_VERSION, PAGE_VERSION } from '@store/consts.js'; +import type { BlockModel } from '@store/index.js'; +import type { JSXElement } from '@store/utils/jsx.js'; +import { + format as prettyFormat, + plugins as prettyFormatPlugins, +} from 'pretty-format'; + +import { + getCanvasElementsCount, + getConnectorPath, + getContainerChildIds, + getContainerIds, + getContainerOfElements, + getEdgelessElementBound, + getNoteRect, + getSelectedBound, + getSortedIdsInViewport, + getZoomLevel, + toIdCountMap, + toModelCoord, +} from './actions/edgeless.js'; +import { + pressArrowLeft, + pressArrowRight, + pressBackspace, + redoByKeyboard, + SHORT_KEY, + type, + undoByKeyboard, +} from './actions/keyboard.js'; +import { + captureHistory, + getClipboardCustomData, + getCurrentEditorDocId, + getCurrentThemeCSSPropertyValue, + getEditorLocator, + inlineEditorInnerTextToString, +} from './actions/misc.js'; +import { getStringFromRichText } from './inline-editor.js'; +import { currentEditorIndex } from './multiple-editor.js'; + +export { assertExists }; + +export const defaultStore = { + meta: { + pages: [ + { + id: 'doc:home', + title: '', + tags: [], + }, + ], + blockVersions: { + 'affine:paragraph': 1, + 'affine:page': 2, + 'affine:database': 3, + 'affine:data-view': 1, + 'affine:list': 1, + 'affine:note': 1, + 'affine:divider': 1, + 'affine:embed-youtube': 1, + 'affine:embed-figma': 1, + 'affine:embed-github': 1, + 'affine:embed-loom': 1, + 'affine:embed-html': 1, + 'affine:embed-linked-doc': 1, + 'affine:embed-synced-doc': 1, + 'affine:image': 1, + 'affine:latex': 1, + 'affine:frame': 1, + 'affine:code': 1, + 'affine:surface': 5, + 'affine:bookmark': 1, + 'affine:attachment': 1, + 'affine:surface-ref': 1, + 'affine:edgeless-text': 1, + }, + workspaceVersion: COLLECTION_VERSION, + pageVersion: PAGE_VERSION, + }, + spaces: { + 'doc:home': { + blocks: { + '0': { + 'prop:title': '', + 'sys:id': '0', + 'sys:flavour': 'affine:page', + 'sys:children': ['1'], + 'sys:version': 2, + }, + '1': { + 'sys:flavour': 'affine:note', + 'sys:id': '1', + 'sys:children': ['2'], + 'sys:version': 1, + 'prop:xywh': `[0,0,${DEFAULT_NOTE_WIDTH}, ${DEFAULT_NOTE_HEIGHT}]`, + 'prop:background': '--affine-note-background-white', + 'prop:index': 'a0', + 'prop:hidden': false, + 'prop:displayMode': 'both', + 'prop:edgeless': { + style: { + borderRadius: 8, + borderSize: 4, + borderStyle: 'none', + shadowType: '--affine-note-shadow-box', + }, + }, + }, + '2': { + 'sys:flavour': 'affine:paragraph', + 'sys:id': '2', + 'sys:children': [], + 'sys:version': 1, + 'prop:text': 'hello', + 'prop:type': 'text', + }, + }, + }, + }, +}; + +export type Bound = [x: number, y: number, w: number, h: number]; + +export async function assertEmpty(page: Page) { + await assertRichTexts(page, ['']); +} + +export async function assertTitle(page: Page, text: string) { + const editor = getEditorLocator(page); + const inlineEditor = editor.locator('.doc-title-container').first(); + const vText = inlineEditorInnerTextToString(await inlineEditor.innerText()); + expect(vText).toBe(text); +} + +export async function assertInlineEditorDeltas( + page: Page, + deltas: unknown[], + i = 0 +) { + const actual = await page.evaluate(i => { + const inlineRoot = document.querySelectorAll( + '[data-v-root="true"]' + )[i]; + return inlineRoot.inlineEditor.yTextDeltas; + }, i); + expect(actual).toEqual(deltas); +} + +export async function assertRichTextInlineDeltas( + page: Page, + deltas: unknown[], + i = 0 +) { + const actual = await page.evaluate( + ([i, currentEditorIndex]) => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const inlineRoot = editorHost.querySelectorAll( + 'rich-text [data-v-root="true"]' + )[i]; + return inlineRoot.inlineEditor.yTextDeltas; + }, + [i, currentEditorIndex] + ); + expect(actual).toEqual(deltas); +} + +export async function assertText(page: Page, text: string, i = 0) { + const actual = await getStringFromRichText(page, i); + expect(actual).toBe(text); +} + +export async function assertTextContain(page: Page, text: string, i = 0) { + const actual = await getStringFromRichText(page, i); + expect(actual).toContain(text); +} + +export async function assertRichTexts(page: Page, texts: string[]) { + const actualTexts = await page.evaluate(currentEditorIndex => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const richTexts = Array.from( + editorHost?.querySelectorAll('rich-text') ?? [] + ); + return richTexts.map(richText => { + const editor = richText.inlineEditor as AffineInlineEditor; + return editor.yText.toString(); + }); + }, currentEditorIndex); + expect(actualTexts).toEqual(texts); +} + +export async function assertEdgelessCanvasText(page: Page, text: string) { + const actualTexts = await page.evaluate(() => { + const editor = document.querySelector( + [ + 'edgeless-text-editor', + 'edgeless-shape-text-editor', + 'edgeless-frame-title-editor', + 'edgeless-group-title-editor', + 'edgeless-connector-label-editor', + ].join(',') + ); + if (!editor) { + throw new Error('editor not found'); + } + // @ts-ignore + const inlineEditor = editor.inlineEditor; + return inlineEditor?.yText.toString(); + }); + expect(actualTexts).toEqual(text); +} + +export async function assertRichImage(page: Page, count: number) { + const editor = getEditorLocator(page); + await expect(editor.locator('.resizable-img')).toHaveCount(count); +} + +export async function assertDivider(page: Page, count: number) { + await expect(page.locator('affine-divider')).toHaveCount(count); +} + +export async function assertRichDragButton(page: Page) { + await expect(page.locator('.resize')).toHaveCount(4); +} + +export async function assertImageSize( + page: Page, + size: { width: number; height: number } +) { + const actual = await page.locator('.resizable-img').boundingBox(); + expect(size).toEqual({ + width: Math.floor(actual?.width ?? NaN), + height: Math.floor(actual?.height ?? NaN), + }); +} + +export async function assertImageOption(page: Page) { + // const actual = await page.locator('.embed-editing-state').count(); + // expect(actual).toEqual(1); + const locator = page.locator('.affine-image-toolbar-container'); + await expect(locator).toBeVisible(); +} + +export async function assertDocTitleFocus(page: Page) { + const locator = page.locator('doc-title .inline-editor').nth(0); + await expect(locator).toBeFocused(); +} + +export async function assertListPrefix( + page: Page, + predict: (string | RegExp)[], + range?: [number, number] +) { + const prefixs = page.locator('.affine-list-block__prefix'); + + let start = 0; + let end = await prefixs.count(); + if (range) { + [start, end] = range; + } + + for (let i = start; i < end; i++) { + const prefix = await prefixs.nth(i).innerText(); + expect(prefix).toContain(predict[i]); + } +} + +export async function assertBlockCount( + page: Page, + flavour: string, + count: number +) { + await expect(page.locator(`affine-${flavour}`)).toHaveCount(count); +} +export async function assertRowCount(page: Page, count: number) { + await expect(page.locator('.affine-database-block-row')).toHaveCount(count); +} + +export async function assertVisibleBlockCount( + page: Page, + flavour: string, + count: number +) { + // not only count, but also check if all the blocks are visible + const locator = page.locator(`affine-${flavour}`); + let visibleCount = 0; + for (let i = 0; i < count; i++) { + if (await locator.nth(i).isVisible()) { + visibleCount++; + } + } + expect(visibleCount).toEqual(count); +} + +export async function assertRichTextInlineRange( + page: Page, + richTextIndex: number, + rangeIndex: number, + rangeLength = 0 +) { + const actual = await page.evaluate( + ([richTextIndex, currentEditorIndex]) => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const richText = editorHost?.querySelectorAll('rich-text')[richTextIndex]; + const inlineEditor = richText.inlineEditor; + return inlineEditor?.getInlineRange(); + }, + [richTextIndex, currentEditorIndex] + ); + expect(actual).toEqual({ index: rangeIndex, length: rangeLength }); +} + +export async function assertNativeSelectionRangeCount( + page: Page, + count: number +) { + const actual = await page.evaluate(() => { + const selection = window.getSelection(); + return selection?.rangeCount; + }); + expect(actual).toEqual(count); +} + +export async function assertNoteXYWH( + page: Page, + expected: [number, number, number, number] +) { + const actual = await page.evaluate(() => { + const rootModel = window.doc.root as RootBlockModel; + const note = rootModel.children.find( + x => x.flavour === 'affine:note' + ) as NoteBlockModel; + return JSON.parse(note.xywh) as number[]; + }); + expect(actual[0]).toBeCloseTo(expected[0]); + expect(actual[1]).toBeCloseTo(expected[1]); + expect(actual[2]).toBeCloseTo(expected[2]); + expect(actual[3]).toBeCloseTo(expected[3]); +} + +export async function assertTextFormat( + page: Page, + richTextIndex: number, + index: number, + resultObj: unknown +) { + const actual = await page.evaluate( + ({ richTextIndex, index, currentEditorIndex }) => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const richText = editorHost.querySelectorAll('rich-text')[richTextIndex]; + const inlineEditor = richText.inlineEditor; + if (!inlineEditor) { + throw new Error('Inline editor is undefined'); + } + + const result = inlineEditor.getFormat({ + index, + length: 0, + }); + return result; + }, + { richTextIndex, index, currentEditorIndex } + ); + expect(actual).toEqual(resultObj); +} + +export async function assertRichTextModelType( + page: Page, + type: string, + index = 0 +) { + const actual = await page.evaluate( + ({ index, BLOCK_ID_ATTR, currentEditorIndex }) => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const richText = editorHost.querySelectorAll('rich-text')[index]; + const block = richText.closest(`[${BLOCK_ID_ATTR}]`); + + if (!block) { + throw new Error('block component is undefined'); + } + return (block.model as BlockModel<{ type: string }>).type; + }, + { index, BLOCK_ID_ATTR, currentEditorIndex } + ); + expect(actual).toEqual(type); +} + +export async function assertTextFormats(page: Page, resultObj: unknown[]) { + const actual = await page.evaluate(index => { + const editorHost = document.querySelectorAll('editor-host')[index]; + const elements = editorHost.querySelectorAll('rich-text'); + return Array.from(elements).map(el => { + const inlineEditor = el.inlineEditor; + if (!inlineEditor) { + throw new Error('Inline editor is undefined'); + } + + const result = inlineEditor.getFormat({ + index: 0, + length: inlineEditor.yText.length, + }); + return result; + }); + }, currentEditorIndex); + expect(actual).toEqual(resultObj); +} + +export async function assertStore( + page: Page, + expected: Record +) { + const actual = await page.evaluate(() => { + const json = window.collection.doc.toJSON(); + delete json.meta.pages[0].createDate; + return json; + }); + expect(actual).toEqual(expected); +} + +export async function assertBlockChildrenIds( + page: Page, + blockId: string, + ids: string[] +) { + const actual = await page.evaluate( + ({ blockId }) => { + const element = document.querySelector(`[data-block-id="${blockId}"]`); + // @ts-ignore + const model = element.model as BlockModel; + return model.children.map(child => child.id); + }, + { blockId } + ); + expect(actual).toEqual(ids); +} + +export async function assertBlockChildrenFlavours( + page: Page, + blockId: string, + flavours: string[] +) { + const actual = await page.evaluate( + ({ blockId }) => { + const element = document.querySelector(`[data-block-id="${blockId}"]`); + // @ts-ignore + const model = element.model as BlockModel; + return model.children.map(child => child.flavour); + }, + { blockId } + ); + expect(actual).toEqual(flavours); +} + +export async function assertParentBlockId( + page: Page, + blockId: string, + parentId: string +) { + const actual = await page.evaluate( + ({ blockId }) => { + const model = window.doc?.getBlock(blockId)?.model; + if (!model) { + throw new Error(`Block with id ${blockId} not found`); + } + return model.doc.getParent(model)?.id; + }, + { blockId } + ); + expect(actual).toEqual(parentId); +} + +export async function assertParentBlockFlavour( + page: Page, + blockId: string, + flavour: string +) { + const actual = await page.evaluate( + ({ blockId }) => { + const model = window.doc?.getBlock(blockId)?.model; + if (!model) { + throw new Error(`Block with id ${blockId} not found`); + } + return model.doc.getParent(model)?.flavour; + }, + { blockId } + ); + expect(actual).toEqual(flavour); +} + +export async function assertClassName( + page: Page, + selector: string, + className: RegExp +) { + const locator = page.locator(selector); + await expect(locator).toHaveClass(className); +} + +export async function assertTextContent( + page: Page, + selector: string, + text: RegExp +) { + const locator = page.locator(selector); + await expect(locator).toHaveText(text); +} + +export async function assertBlockType( + page: Page, + id: string | number | null, + type: string +) { + const actual = await page.evaluate( + ({ id }) => { + const element = document.querySelector( + `[data-block-id="${id}"]` + ); + + if (!element) { + throw new Error(`Element with id ${id} not found`); + } + + const model = element.model; + // @ts-ignore + return model.type; + }, + { id } + ); + expect(actual).toBe(type); +} + +export async function assertBlockFlavour( + page: Page, + id: string | number, + flavour: BlockSuite.Flavour +) { + const actual = await page.evaluate( + ({ id }) => { + const element = document.querySelector( + `[data-block-id="${id}"]` + ); + + if (!element) { + throw new Error(`Element with id ${id} not found`); + } + + const model = element.model; + return model.flavour; + }, + { id } + ); + expect(actual).toBe(flavour); +} + +export async function assertBlockTextContent( + page: Page, + id: string | number, + str: string +) { + const actual = await page.evaluate( + ({ id }) => { + const element = document.querySelector( + `[data-block-id="${id}"]` + ); + + if (!element) { + throw new Error(`Element with id ${id} not found`); + } + + const model = element.model; + return model.text?.toString() ?? ''; + }, + { id } + ); + expect(actual).toBe(str); +} + +export async function assertBlockProps( + page: Page, + id: string, + props: Record +) { + const actual = await page.evaluate( + ([id, props]) => { + const element = document.querySelector(`[data-block-id="${id}"]`); + // @ts-ignore + const model = element.model as BlockModel; + return Object.fromEntries( + // @ts-ignore + Object.keys(props).map(key => [key, (model[key] as unknown).toString()]) + ); + }, + [id, props] as const + ); + expect(actual).toEqual(props); +} + +export async function assertBlockTypes(page: Page, blockTypes: string[]) { + const actual = await page.evaluate(index => { + const editor = document.querySelectorAll('affine-editor-container')[index]; + const elements = editor?.querySelectorAll('[data-block-id]'); + return ( + Array.from(elements) + .slice(2) + // @ts-ignore + .map(el => el.model.type) + ); + }, currentEditorIndex); + expect(actual).toEqual(blockTypes); +} + +/** + * @example + * ```ts + * await assertMatchMarkdown( + * page, + * `title + * text1 + * text2` + * ); + * ``` + * @deprecated experimental, use {@link assertStoreMatchJSX} instead + */ +export async function assertMatchMarkdown(page: Page, text: string) { + const jsonDoc = (await page.evaluate(() => + window.collection.doc.toJSON() + )) as Record>; + const titleNode = jsonDoc['doc:home']['0'] as Record; + + const markdownVisitor = (node: Record): string => { + // TODO use schema + if (node['sys:flavour'] === 'affine:page') { + return (node['prop:title'] as Text).toString() ?? ''; + } + if (!('prop:type' in node)) { + return '[? unknown node]'; + } + if (node['prop:type'] === 'text') { + return node['prop:text'] as string; + } + if (node['prop:type'] === 'bulleted') { + return `- ${node['prop:text']}`; + } + // TODO please fix this + return `[? ${node['prop:type']} node]`; + }; + + const INDENT_SIZE = 2; + const visitNodes = ( + node: Record, + visitor: (node: Record) => string + ): string[] => { + if (!('sys:children' in node) || !Array.isArray(node['sys:children'])) { + throw new Error("Failed to visit nodes: 'sys:children' is not an array"); + // return visitor(node); + } + + const children = node['sys:children'].map(id => jsonDoc['doc:home'][id]); + return [ + visitor(node), + ...children.flatMap(child => + visitNodes(child as Record, visitor).map(line => { + if (node['sys:flavour'] === 'affine:page') { + // Ad hoc way to remove the title indent + return line; + } + + return ' '.repeat(INDENT_SIZE) + line; + }) + ), + ]; + }; + const visitRet = visitNodes(titleNode, markdownVisitor); + const actual = visitRet.join('\n'); + + expect(actual).toEqual(text); +} + +export async function assertStoreMatchJSX( + page: Page, + snapshot: string, + blockId?: string +) { + const docId = await getCurrentEditorDocId(page); + const element = (await page.evaluate( + ([blockId, docId]) => window.collection.exportJSX(blockId, docId), + [blockId, docId] + )) as JSXElement; + + // Fix symbol can not be serialized, we need to set $$typeof manually + // If the function passed to the page.evaluate(pageFunction[, arg]) returns a non-Serializable value, + // then page.evaluate(pageFunction[, arg]) resolves to undefined. + // See https://playwright.dev/docs/api/class-page#page-evaluate + const testSymbol = Symbol.for('react.test.json'); + const markSymbol = (node: JSXElement) => { + node.$$typeof = testSymbol; + if (!node.children) { + return; + } + const propText = node.props['prop:text']; + if (propText && typeof propText === 'object') { + markSymbol(propText); + } + node.children.forEach(child => { + if (!(typeof child === 'object')) { + return; + } + markSymbol(child); + }); + }; + + markSymbol(element); + + // See https://github.com/facebook/jest/blob/main/packages/pretty-format + const formattedJSX = prettyFormat(element, { + plugins: [prettyFormatPlugins.ReactTestComponent], + printFunctionName: false, + }); + expect(formattedJSX, formattedJSX).toEqual(snapshot.trimStart()); +} + +type MimeType = 'text/plain' | 'blocksuite/x-c+w' | 'text/html'; + +export function assertClipItems(_page: Page, _key: MimeType, _value: unknown) { + // FIXME: use original clipboard API + // const clipItems = await page.evaluate(() => { + // return document + // .getElementsByTagName('affine-editor-container')[0] + // .clipboard['_copy']['_getClipItems'](); + // }); + // const actual = clipItems.find(item => item.mimeType === key)?.data; + // expect(actual).toEqual(value); + return true; +} + +export function assertAlmostEqual( + actual: number, + expected: number, + precision = 0.001 +) { + expect( + Math.abs(actual - expected), + `expected: ${expected}, but actual: ${actual}` + ).toBeLessThan(precision); +} + +export function assertPointAlmostEqual( + actual: number[], + expected: number[], + precision = 0.001 +) { + assertAlmostEqual(actual[0], expected[0], precision); + assertAlmostEqual(actual[1], expected[1], precision); +} + +/** + * Assert the locator is visible in the viewport. + * It will check the bounding box of the locator is within the viewport. + * + * See also https://playwright.dev/docs/actionability#visible + */ +export async function assertLocatorVisible( + page: Page, + locator: Locator, + visible = true +) { + const bodyRect = await page.locator('body').boundingBox(); + const rect = await locator.boundingBox(); + expect(rect).toBeTruthy(); + expect(bodyRect).toBeTruthy(); + if (!rect || !bodyRect) { + throw new Error('Unreachable'); + } + if (visible) { + // Assert the locator is **fully** visible + await expect(locator).toBeVisible(); + expect(rect.x).toBeGreaterThanOrEqual(0); + expect(rect.y).toBeGreaterThanOrEqual(0); + expect(rect.x + rect.width).toBeLessThanOrEqual( + bodyRect.x + bodyRect.width + ); + expect(rect.y + rect.height).toBeLessThanOrEqual( + bodyRect.x + bodyRect.height + ); + } else { + // Assert the locator is **fully** invisible + const locatorIsVisible = await locator.isVisible(); + if (!locatorIsVisible) { + // If the locator is invisible, we don't need to check the bounding box + return; + } + const isInVisible = + rect.x > bodyRect.x + bodyRect.width || + rect.y > bodyRect.y + bodyRect.height || + rect.x + rect.width < bodyRect.x || + rect.y + rect.height < bodyRect.y; + expect(isInVisible).toBe(true); + } +} + +/** + * Assert basic keyboard operation works in input + * + * NOTICE: + * - it will clear the input value. + * - it will pollute undo/redo history. + */ +export async function assertKeyboardWorkInInput(page: Page, locator: Locator) { + await expect(locator).toBeVisible(); + await locator.focus(); + // Clear input before test + await locator.clear(); + // type/backspace + await type(page, '12/34'); + await expect(locator).toHaveValue('12/34'); + await captureHistory(page); + await pressBackspace(page); + await expect(locator).toHaveValue('12/3'); + + // undo/redo + await undoByKeyboard(page); + await expect(locator).toHaveValue('12/34'); + await redoByKeyboard(page); + await expect(locator).toHaveValue('12/3'); + + // keyboard + await pressArrowLeft(page, 2); + await pressArrowRight(page, 1); + await pressBackspace(page); + await expect(locator).toHaveValue('123'); + await pressBackspace(page); + await expect(locator).toHaveValue('13'); + + // copy/cut/paste + await page.keyboard.press(`${SHORT_KEY}+a`, { delay: 50 }); + await page.keyboard.press(`${SHORT_KEY}+c`, { delay: 50 }); + await pressBackspace(page); + await expect(locator).toHaveValue(''); + await page.keyboard.press(`${SHORT_KEY}+v`, { delay: 50 }); + await expect(locator).toHaveValue('13'); + await page.keyboard.press(`${SHORT_KEY}+a`, { delay: 50 }); + await page.keyboard.press(`${SHORT_KEY}+x`, { delay: 50 }); + await expect(locator).toHaveValue(''); +} + +export function assertSameColor(c1?: `#${string}`, c2?: `#${string}`) { + expect(c1?.toLowerCase()).toEqual(c2?.toLowerCase()); +} + +type Rect = { x: number; y: number; w: number; h: number }; + +export async function assertNoteRectEqual( + page: Page, + noteId: string, + expected: Rect +) { + const rect = await getNoteRect(page, noteId); + assertRectEqual(rect, expected); +} + +export function assertRectEqual(a: Rect, b: Rect) { + expect(a.x).toBeCloseTo(b.x, 0); + expect(a.y).toBeCloseTo(b.y, 0); + expect(a.w).toBeCloseTo(b.w, 0); + expect(a.h).toBeCloseTo(b.h, 0); +} + +export function assertDOMRectEqual(a: DOMRect, b: DOMRect) { + expect(a.x).toBeCloseTo(b.x, 0); + expect(a.y).toBeCloseTo(b.y, 0); + expect(a.width).toBeCloseTo(b.width, 0); + expect(a.height).toBeCloseTo(b.height, 0); +} + +export async function assertEdgelessDraggingArea(page: Page, xywh: number[]) { + const [x, y, w, h] = xywh; + const editor = getEditorLocator(page); + const draggingArea = editor + .locator('edgeless-dragging-area-rect') + .locator('.affine-edgeless-dragging-area'); + + const box = await draggingArea.boundingBox(); + if (!box) throw new Error('Missing edgeless dragging area'); + + expect(box.x).toBeCloseTo(x, 0); + expect(box.y).toBeCloseTo(y, 0); + expect(box.width).toBeCloseTo(w, 0); + expect(box.height).toBeCloseTo(h, 0); +} + +export async function getSelectedRect(page: Page) { + const editor = getEditorLocator(page); + const selectedRect = editor + .locator('edgeless-selected-rect') + .locator('.affine-edgeless-selected-rect'); + // FIXME: remove this timeout + await page.waitForTimeout(50); + const box = await selectedRect.boundingBox(); + if (!box) throw new Error('Missing edgeless selected rect'); + return box; +} + +// Better to use xxSelectedModelRect +export async function assertEdgelessSelectedRect(page: Page, xywh: number[]) { + const [x, y, w, h] = xywh; + const box = await getSelectedRect(page); + + expect(box.x).toBeCloseTo(x, 0); + expect(box.y).toBeCloseTo(y, 0); + expect(box.width).toBeCloseTo(w, 0); + expect(box.height).toBeCloseTo(h, 0); +} + +export async function assertEdgelessSelectedModelRect( + page: Page, + xywh: number[] +) { + const [x, y, w, h] = xywh; + const box = await getSelectedRect(page); + const [mX, mY] = await toModelCoord(page, [box.x, box.y]); + + expect(mX).toBeCloseTo(x, 0); + expect(mY).toBeCloseTo(y, 0); + expect(box.width).toBeCloseTo(w, 0); + expect(box.height).toBeCloseTo(h, 0); +} + +export async function assertEdgelessSelectedElementHandleCount( + page: Page, + count: number +) { + const editor = getEditorLocator(page); + const handles = editor.locator('.element-handle'); + await expect(handles).toHaveCount(count); +} + +// Better to use xxSelectedModelRect +export async function assertEdgelessRemoteSelectedRect( + page: Page, + xywh: number[], + index = 0 +) { + const [x, y, w, h] = xywh; + const editor = getEditorLocator(page); + const remoteSelectedRect = editor + .locator('affine-edgeless-remote-selection-widget') + .locator('.remote-rect') + .nth(index); + + const box = await remoteSelectedRect.boundingBox(); + if (!box) throw new Error('Missing edgeless remote selected rect'); + + expect(box.x).toBeCloseTo(x, 0); + expect(box.y).toBeCloseTo(y, 0); + expect(box.width).toBeCloseTo(w, 0); + expect(box.height).toBeCloseTo(h, 0); +} + +export async function assertEdgelessRemoteSelectedModelRect( + page: Page, + xywh: number[], + index = 0 +) { + const [x, y, w, h] = xywh; + const editor = getEditorLocator(page); + const remoteSelectedRect = editor + .locator('affine-edgeless-remote-selection-widget') + .locator('.remote-rect') + .nth(index); + + const box = await remoteSelectedRect.boundingBox(); + if (!box) throw new Error('Missing edgeless remote selected rect'); + + const [mX, mY] = await toModelCoord(page, [box.x, box.y]); + expect(mX).toBeCloseTo(x, 0); + expect(mY).toBeCloseTo(y, 0); + expect(box.width).toBeCloseTo(w, 0); + expect(box.height).toBeCloseTo(h, 0); +} + +export async function assertEdgelessSelectedRectRotation(page: Page, deg = 0) { + const editor = getEditorLocator(page); + const selectedRect = editor + .locator('edgeless-selected-rect') + .locator('.affine-edgeless-selected-rect'); + + const transform = await selectedRect.evaluate(el => el.style.transform); + const r = new RegExp(`rotate\\(${deg}deg\\)`); + expect(transform).toMatch(r); +} + +export async function assertEdgelessSelectedReactCursor( + page: Page, + expected: ( + | { + mode: 'resize'; + handle: + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'top-left' + | 'top-right' + | 'bottom-right' + | 'bottom-left'; + } + | { + mode: 'rotate'; + handle: 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left'; + } + ) & { + cursor: string; + } +) { + const editor = getEditorLocator(page); + const selectedRect = editor + .locator('edgeless-selected-rect') + .locator('.affine-edgeless-selected-rect'); + + const handle = selectedRect + .getByLabel(expected.handle, { exact: true }) + .locator(`.${expected.mode}`); + + await handle.hover(); + await expect(handle).toHaveCSS('cursor', expected.cursor); +} + +export async function assertEdgelessNonSelectedRect(page: Page) { + const rect = page.locator('edgeless-selected-rect'); + await expect(rect).toBeHidden(); +} + +export async function assertSelectionInNote( + page: Page, + noteId: string, + blockNote: string = 'affine-note' +) { + const closestNoteId = await page.evaluate(blockNote => { + const selection = window.getSelection(); + const note = selection?.anchorNode?.parentElement?.closest(blockNote); + return note?.getAttribute('data-block-id'); + }, blockNote); + expect(closestNoteId).toEqual(noteId); +} + +export async function assertEdgelessNoteBackground( + page: Page, + noteId: string, + color: string +) { + const editor = getEditorLocator(page); + const backgroundColor = await editor + .locator(`affine-edgeless-note[data-block-id="${noteId}"]`) + .evaluate(ele => { + const noteWrapper = + ele?.querySelector('.note-background'); + if (!noteWrapper) { + throw new Error(`Could not find note: ${noteId}`); + } + return noteWrapper.style.backgroundColor; + }); + + expect(backgroundColor).toEqual(`var(${color})`); +} + +function toHex(color: string) { + let r, g, b; + + if (color.startsWith('#')) { + color = color.substr(1); + if (color.length === 3) { + color = color.replace(/./g, '$&$&'); + } + [r, g, b] = color.match(/.{2}/g)?.map(hex => parseInt(hex, 16)) ?? []; + } else if (color.startsWith('rgba')) { + [r, g, b] = color.match(/\d+/g)?.map(Number) ?? []; + } else if (color.startsWith('rgb')) { + [r, g, b] = color.match(/\d+/g)?.map(Number) ?? []; + } else { + throw new Error('Invalid color format'); + } + + if (r === undefined || g === undefined || b === undefined) { + throw new Error('Invalid color format'); + } + + const hex = ((r << 16) | (g << 8) | b).toString(16); + return '#' + '0'.repeat(6 - hex.length) + hex; +} + +export async function assertEdgelessColorSameWithHexColor( + page: Page, + edgelessColor: string, + hexColor: `#${string}` +) { + const themeColor = await getCurrentThemeCSSPropertyValue(page, edgelessColor); + expect(themeColor).toBeTruthy(); + const edgelessHexColor = toHex(themeColor); + + assertSameColor(hexColor, edgelessHexColor as `#${string}`); +} + +export async function assertZoomLevel(page: Page, zoom: number) { + const z = await getZoomLevel(page); + expect(z).toBe(Math.ceil(zoom)); +} + +export async function assertConnectorPath( + page: Page, + path: number[][], + index = 0 +) { + const actualPath = await getConnectorPath(page, index); + actualPath.every((p, i) => assertPointAlmostEqual(p, path[i], 0.1)); +} + +export function assertRectExist( + rect: { x: number; y: number; width: number; height: number } | null +): asserts rect is { x: number; y: number; width: number; height: number } { + expect(rect).not.toBe(null); +} + +export async function assertEdgelessElementBound( + page: Page, + elementId: string, + bound: Bound +) { + const actual = await getEdgelessElementBound(page, elementId); + assertBound(actual, bound); +} + +export async function assertSelectedBound( + page: Page, + expected: Bound, + index = 0 +) { + const bound = await getSelectedBound(page, index); + assertBound(bound, expected); +} + +/** + * asserts all groups and they children count at the same time + * @param page + * @param expected the expected group id and the count of of its children + */ +export async function assertContainerIds( + page: Page, + expected: Record +) { + const ids = await getContainerIds(page); + const result = toIdCountMap(ids); + + expect(result).toEqual(expected); +} + +export async function assertSortedIds(page: Page, expected: string[]) { + const ids = await getSortedIdsInViewport(page); + expect(ids).toEqual(expected); +} + +export async function assertContainerChildIds( + page: Page, + expected: Record, + id: string +) { + const ids = await getContainerChildIds(page, id); + const result = toIdCountMap(ids); + + expect(result).toEqual(expected); +} + +export async function assertContainerOfElements( + page: Page, + elements: string[], + containerId: string | null +) { + const elementContainers = await getContainerOfElements(page, elements); + + elementContainers.forEach(elementContainer => { + expect(elementContainer).toEqual(containerId); + }); +} + +/** + * Assert the given container has the expected children count. + * And the children's container id should equal to the given container id. + * @param page + * @param containerId + * @param childrenCount + */ +export async function assertContainerChildCount( + page: Page, + containerId: string, + childrenCount: number +) { + const ids = await getContainerChildIds(page, containerId); + + await assertContainerOfElements(page, ids, containerId); + expect(new Set(ids).size).toBe(childrenCount); +} + +export async function assertCanvasElementsCount(page: Page, expected: number) { + const number = await getCanvasElementsCount(page); + expect(number).toEqual(expected); +} +export function assertBound(received: Bound, expected: Bound) { + expect(received[0]).toBeCloseTo(expected[0], 0); + expect(received[1]).toBeCloseTo(expected[1], 0); + expect(received[2]).toBeCloseTo(expected[2], 0); + expect(received[3]).toBeCloseTo(expected[3], 0); +} + +export async function assertClipboardItem( + page: Page, + data: unknown, + type: string +) { + type Args = [type: string]; + const dataInClipboard = await page.evaluate( + async ([type]: Args) => { + const clipItems = await navigator.clipboard.read(); + const item = clipItems.find(item => item.types.includes(type)); + const data = await item?.getType(type); + return data?.text(); + }, + [type] as Args + ); + + expect(dataInClipboard).toBe(data); +} + +export async function assertClipboardCustomData( + page: Page, + type: string, + data: unknown +) { + const dataInClipboard = await getClipboardCustomData(page, type); + expect(dataInClipboard).toBe(data); +} + +export function assertClipData( + clipItems: { mimeType: string; data: unknown }[], + expectClipItems: { mimeType: string; data: unknown }[], + type: string +) { + expect(clipItems.find(item => item.mimeType === type)?.data).toBe( + expectClipItems.find(item => item.mimeType === type)?.data + ); +} + +export async function assertHasClass(locator: Locator, className: string) { + expect( + (await locator.getAttribute('class'))?.split(' ').includes(className) + ).toEqual(true); +} + +export async function assertNotHasClass(locator: Locator, className: string) { + expect( + (await locator.getAttribute('class'))?.split(' ').includes(className) + ).toEqual(false); +} + +export async function assertNoteSequence(page: Page, expected: string) { + const actual = await page.locator('.page-visible-index-label').innerText(); + expect(expected).toBe(actual); +} + +export async function assertBlockSelections(page: Page, paths: string[]) { + const selections = await page.evaluate(() => { + const host = document.querySelector('editor-host'); + if (!host) { + throw new Error('editor-host host not found'); + } + return host.selection.filter('block'); + }); + const actualPaths = selections.map(selection => selection.blockId); + expect(actualPaths).toEqual(paths); +} + +export async function assertTextSelection( + page: Page, + from?: { + blockId: string; + index: number; + length: number; + }, + to?: { + blockId: string; + index: number; + length: number; + } +) { + const selection = await page.evaluate(() => { + const host = document.querySelector('editor-host'); + if (!host) { + throw new Error('editor-host host not found'); + } + return host.selection.find('text'); + }); + + if (!from && !to) { + expect(selection).toBeUndefined(); + return; + } + + if (from) { + expect(selection?.from).toEqual(from); + } + if (to) { + expect(selection?.to).toEqual(to); + } +} + +export async function assertConnectorStrokeColor(page: Page, color: string) { + const colorButton = page + .locator('edgeless-change-connector-button') + .locator('edgeless-color-panel') + .locator(`.color-unit[aria-label="${color}"]`); + + expect(await colorButton.count()).toBe(1); +} diff --git a/blocksuite/tests-legacy/utils/declare-test-window.ts b/blocksuite/tests-legacy/utils/declare-test-window.ts new file mode 100644 index 0000000000000..3f57d33d3d787 --- /dev/null +++ b/blocksuite/tests-legacy/utils/declare-test-window.ts @@ -0,0 +1,48 @@ +import type { RefNodeSlotsProvider, TestUtils } from '@blocks/index.js'; +import type { + EditorHost, + ExtensionType, + WidgetViewMapIdentifier, +} from '@blocksuite/block-std'; +import type { AffineEditorContainer } from '@blocksuite/presets'; +import type { StarterDebugMenu } from '@playground/apps/_common/components/starter-debug-menu.js'; +import type { BlockModel, Doc, DocCollection, Job } from '@store/index.js'; + +declare global { + interface Window { + /** Available on playground window + * the following instance are initialized in `packages/playground/apps/starter/main.ts` + */ + $blocksuite: { + store: typeof import('../../packages/framework/store/src/index.js'); + blocks: typeof import('../../packages/blocks/src/index.js'); + global: { + utils: typeof import('../../packages/framework/global/src/utils.js'); + }; + editor: typeof import('../../packages/presets/src/index.js'); + identifiers: { + WidgetViewMapIdentifier: typeof WidgetViewMapIdentifier; + QuickSearchProvider: typeof import('../../packages/affine/shared/src/services/quick-search-service.js').QuickSearchProvider; + DocModeProvider: typeof import('../../packages/affine/shared/src/services/doc-mode-service.js').DocModeProvider; + ThemeProvider: typeof import('../../packages/affine/shared/src/services/theme-service.js').ThemeProvider; + RefNodeSlotsProvider: typeof RefNodeSlotsProvider; + ParseDocUrlService: typeof import('../../packages/affine/shared/src/services/parse-url-service.js').ParseDocUrlProvider; + }; + defaultExtensions: () => ExtensionType[]; + extensions: { + WidgetViewMapExtension: typeof import('../../packages/framework/block-std/src/extension/widget-view-map.js').WidgetViewMapExtension; + }; + mockServices: { + mockDocModeService: typeof import('../../packages/playground/apps/_common/mock-services.js').mockDocModeService; + }; + }; + collection: DocCollection; + blockSchema: Record; + doc: Doc; + debugMenu: StarterDebugMenu; + editor: AffineEditorContainer; + host: EditorHost; + testUtils: TestUtils; + job: Job; + } +} diff --git a/blocksuite/tests-legacy/utils/ignore.ts b/blocksuite/tests-legacy/utils/ignore.ts new file mode 100644 index 0000000000000..a0859d8b7581d --- /dev/null +++ b/blocksuite/tests-legacy/utils/ignore.ts @@ -0,0 +1,28 @@ +import type { BlockSnapshot } from '@store/index.js'; + +export function ignoreFields(target: unknown, keys: string[]): unknown { + if (Array.isArray(target)) { + return target.map((item: unknown) => ignoreFields(item, keys)); + } else if (typeof target === 'object' && target !== null) { + return Object.keys(target).reduce( + (acc: Record, key: string) => { + if (keys.includes(key)) { + acc[key] = '*'; + } else { + acc[key] = ignoreFields( + (target as Record)[key], + keys + ); + } + return acc; + }, + {} + ); + } + return target; +} + +export function ignoreSnapshotId(snapshot: BlockSnapshot) { + const ignored = ignoreFields(snapshot, ['id']); + return JSON.stringify(ignored, null, 2); +} diff --git a/blocksuite/tests-legacy/utils/inline-editor.ts b/blocksuite/tests-legacy/utils/inline-editor.ts new file mode 100644 index 0000000000000..0558b256fb14a --- /dev/null +++ b/blocksuite/tests-legacy/utils/inline-editor.ts @@ -0,0 +1,26 @@ +import type { Page } from '@playwright/test'; + +import { currentEditorIndex } from './multiple-editor.js'; + +export async function getStringFromRichText( + page: Page, + index = 0 +): Promise { + await page.waitForTimeout(50); + return page.evaluate( + ([index, currentEditorIndex]) => { + const editorHost = + document.querySelectorAll('editor-host')[currentEditorIndex]; + const richTexts = editorHost.querySelectorAll('rich-text'); + + if (!richTexts) { + throw new Error('Cannot find rich-text'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const editor = (richTexts[index] as any).inlineEditor; + return editor.yText.toString(); + }, + [index, currentEditorIndex] + ); +} diff --git a/blocksuite/tests-legacy/utils/multiple-editor.ts b/blocksuite/tests-legacy/utils/multiple-editor.ts new file mode 100644 index 0000000000000..90d3b0a32520c --- /dev/null +++ b/blocksuite/tests-legacy/utils/multiple-editor.ts @@ -0,0 +1,15 @@ +import process from 'node:process'; + +const editorIndex = { + 0: 0, + 1: 1, +}[process.env.MULTIPLE_EDITOR_INDEX ?? '']; +export const scope = + editorIndex == null + ? undefined + : editorIndex === 0 + ? 'FIRST | ' + : 'SECOND | '; +export const multiEditor = scope != null; + +export const currentEditorIndex = editorIndex ?? 0; diff --git a/blocksuite/tests-legacy/utils/playwright.ts b/blocksuite/tests-legacy/utils/playwright.ts new file mode 100644 index 0000000000000..bd7f2f4cf4454 --- /dev/null +++ b/blocksuite/tests-legacy/utils/playwright.ts @@ -0,0 +1,110 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +import { expect, type Page, test as baseTest } from '@playwright/test'; + +import { + enterPlaygroundRoom, + initEmptyParagraphState, +} from './actions/misc.js'; +import { currentEditorIndex, scope } from './multiple-editor.js'; + +const istanbulTempDir = process.env.ISTANBUL_TEMP_DIR + ? path.resolve(process.env.ISTANBUL_TEMP_DIR) + : path.join(process.cwd(), '.nyc_output'); + +function generateUUID() { + return crypto.randomUUID(); +} + +const enableCoverage = !!process.env.CI || !!process.env.COVERAGE; +export const scoped = (stringsArray: TemplateStringsArray) => { + return `${scope ?? ''}${stringsArray.join()}`; +}; +export const test = baseTest.extend<{}>({ + context: async ({ context }, use) => { + if (enableCoverage) { + await context.addInitScript(() => + window.addEventListener('beforeunload', () => + // @ts-expect-error + window.collectIstanbulCoverage(JSON.stringify(window.__coverage__)) + ) + ); + + await fs.promises.mkdir(istanbulTempDir, { recursive: true }); + await context.exposeFunction( + 'collectIstanbulCoverage', + (coverageJSON?: string) => { + if (coverageJSON) + fs.writeFileSync( + path.join( + istanbulTempDir, + `playwright_coverage_${generateUUID()}.json` + ), + coverageJSON + ); + } + ); + } + await use(context); + if (enableCoverage) { + for (const page of context.pages()) { + await page.evaluate(() => + // @ts-expect-error + window.collectIstanbulCoverage(JSON.stringify(window.__coverage__)) + ); + } + } + }, +}); +if (scope) { + test.beforeEach(async ({ browser }, testInfo) => { + if (scope && !testInfo.title.startsWith(scope)) { + testInfo.fn = () => { + testInfo.skip(); + }; + testInfo.skip(); + await browser.close(); + } + }); + + let page: Page; + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + }); + + // eslint-disable-next-line no-empty-pattern + test.afterAll(async ({}, testInfo) => { + if (!scope || !testInfo.title.startsWith(scope)) { + return; + } + const focusInSecondEditor = await page.evaluate( + ([currentEditorIndex]) => { + const editor = document.querySelectorAll('affine-editor-container')[ + currentEditorIndex + ]; + const selection = getSelection(); + if (!selection || selection.rangeCount === 0) { + return true; + } + // once the range exists, it must be in the corresponding editor + return editor.contains(selection.getRangeAt(0).startContainer); + }, + [currentEditorIndex] + ); + expect(focusInSecondEditor).toBe(true); + }); + + test('ensure enable two editor', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + const count = await page.evaluate(() => { + return document.querySelectorAll('affine-editor-container').length; + }); + + expect(count).toBe(2); + }); +} diff --git a/blocksuite/tests-legacy/utils/query.ts b/blocksuite/tests-legacy/utils/query.ts new file mode 100644 index 0000000000000..d8c40d15af3ef --- /dev/null +++ b/blocksuite/tests-legacy/utils/query.ts @@ -0,0 +1,125 @@ +import { expect, type Page } from '@playwright/test'; + +import { waitNextFrame } from './actions/misc.js'; +import { assertAlmostEqual } from './asserts.js'; + +export function getFormatBar(page: Page) { + const formatBar = page.locator('.affine-format-bar-widget'); + const boldBtn = formatBar.getByTestId('bold'); + const italicBtn = formatBar.getByTestId('italic'); + const underlineBtn = formatBar.getByTestId('underline'); + const strikeBtn = formatBar.getByTestId('strike'); + const codeBtn = formatBar.getByTestId('code'); + const linkBtn = formatBar.getByTestId('link'); + // highlight + const highlightBtn = formatBar.locator('.highlight-icon'); + const redForegroundBtn = formatBar.getByTestId( + 'var(--affine-text-highlight-foreground-red)' + ); + const createLinkedDocBtn = formatBar.getByTestId('convert-to-linked-doc'); + const defaultColorBtn = formatBar.getByTestId('unset'); + const highlight = { + highlightBtn, + redForegroundBtn, + defaultColorBtn, + }; + + const paragraphBtn = formatBar.locator(`.paragraph-button`); + const openParagraphMenu = async () => { + await expect(formatBar).toBeVisible(); + await paragraphBtn.hover(); + }; + + const textBtn = formatBar.getByTestId('affine:paragraph/text'); + const h1Btn = formatBar.getByTestId('affine:paragraph/h1'); + const bulletedBtn = formatBar.getByTestId('affine:list/bulleted'); + const codeBlockBtn = formatBar.getByTestId('affine:code/'); + + const moreBtn = formatBar.getByRole('button', { name: 'More' }); + const copyBtn = formatBar.getByRole('button', { name: 'Copy' }); + const duplicateBtn = formatBar.getByRole('button', { name: 'Duplicate' }); + const deleteBtn = formatBar.getByRole('button', { name: 'Delete' }); + const openMoreMenu = async () => { + await expect(formatBar).toBeVisible(); + await moreBtn.click(); + }; + + const assertBoundingBox = async (x: number, y: number) => { + const boundingBox = await formatBar.boundingBox(); + if (!boundingBox) { + throw new Error("formatBar doesn't exist"); + } + assertAlmostEqual(boundingBox.x, x, 6); + assertAlmostEqual(boundingBox.y, y, 6); + }; + + return { + formatBar, + boldBtn, + italicBtn, + underlineBtn, + strikeBtn, + codeBtn, + linkBtn, + highlight, + createLinkedDocBtn, + + openParagraphMenu, + textBtn, + h1Btn, + bulletedBtn, + codeBlockBtn, + + moreBtn, + openMoreMenu, + copyBtn, + duplicateBtn, + deleteBtn, + + assertBoundingBox, + }; +} + +export function getEmbedCardToolbar(page: Page) { + const embedCardToolbar = page.locator('.embed-card-toolbar'); + function createButtonLocator(name: string) { + return embedCardToolbar.getByRole('button', { name }); + } + const copyButton = createButtonLocator('copy'); + const editButton = createButtonLocator('edit'); + const cardStyleButton = createButtonLocator('card style'); + const captionButton = createButtonLocator('caption'); + const moreButton = createButtonLocator('more'); + + const cardStyleHorizontalButton = embedCardToolbar.getByRole('button', { + name: 'Large horizontal style', + }); + const cardStyleListButton = embedCardToolbar.getByRole('button', { + name: 'Small horizontal style', + }); + + const openCardStyleMenu = async () => { + await expect(embedCardToolbar).toBeVisible(); + await cardStyleButton.click(); + await waitNextFrame(page); + }; + + const openMoreMenu = async () => { + await expect(embedCardToolbar).toBeVisible(); + await moreButton.click(); + await waitNextFrame(page); + }; + + return { + embedCardToolbar, + copyButton, + editButton, + cardStyleButton, + captionButton, + moreButton, + openCardStyleMenu, + openMoreMenu, + cardStyleHorizontalButton, + cardStyleListButton, + }; +} diff --git a/blocksuite/tests-legacy/worker.spec.ts b/blocksuite/tests-legacy/worker.spec.ts new file mode 100644 index 0000000000000..813034cae36e7 --- /dev/null +++ b/blocksuite/tests-legacy/worker.spec.ts @@ -0,0 +1,33 @@ +import { expect } from '@playwright/test'; + +import { + enterPlaygroundRoom, + initEmptyParagraphState, +} from './utils/actions/index.js'; +import { test } from './utils/playwright.js'; + +declare global { + interface Window { + testWorker: Worker; + } +} + +test.skip('should the worker in the playground work fine.', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + + const ok = await page.evaluate(async () => { + return new Promise(resolve => { + window.testWorker.postMessage('ping'); + window.testWorker.addEventListener('message', event => { + if (event.data === 'pong') { + resolve(true); + } + }); + }); + }); + + expect(ok).toBeTruthy(); +}); diff --git a/blocksuite/tests-legacy/zero-width.spec.ts b/blocksuite/tests-legacy/zero-width.spec.ts new file mode 100644 index 0000000000000..d386969d5859d --- /dev/null +++ b/blocksuite/tests-legacy/zero-width.spec.ts @@ -0,0 +1,145 @@ +import './utils/declare-test-window.js'; + +import { + enterPlaygroundRoom, + initEmptyCodeBlockState, +} from './utils/actions/index.js'; +import { assertBlockChildrenIds, assertBlockFlavour } from './utils/asserts.js'; +import { scoped, test } from './utils/playwright.js'; + +const bookMarkUrl = 'http://localhost'; +const embedUrl = 'https://github.com/toeverything/blocksuite/pull/7217'; + +test.beforeEach(async ({ page }) => { + await page.route( + 'https://affine-worker.toeverything.workers.dev/api/worker/link-preview', + async route => { + await route.fulfill({ + json: {}, + }); + } + ); +}); + +test( + scoped`create a paragraph-block while clicking zero-width of code-block which is the last block of page`, + async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + const codeComponent = page.locator('affine-code'); + const rect = await codeComponent.boundingBox(); + if (!rect) { + throw new Error('code-block not found'); + } + // click the zero width + await page.mouse.click(rect.x + 20, rect.y + rect.height + 8); + await assertBlockFlavour(page, '3', 'affine:paragraph'); + } +); + +test( + scoped`don't create a paragraph-block while clicking zero-width of code-block before a paragraph-block`, + async ({ page }) => { + await enterPlaygroundRoom(page); + await page.evaluate(() => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + doc.addBlock('affine:code', {}, note); + doc.addBlock('affine:paragraph', {}, note); + }); + const codeComponent = page.locator('affine-code'); + const codeComponentrect = await codeComponent.boundingBox(); + if (!codeComponentrect) { + throw new Error('code-block not found'); + } + await page.mouse.click( + codeComponentrect.x + 20, + codeComponentrect.y + codeComponentrect.height + 8 + ); + await assertBlockChildrenIds(page, '1', ['2', '3']); + } +); + +test( + scoped`create a paragraph-block while clicking between two non-paragraph-block`, + async ({ page }) => { + await enterPlaygroundRoom(page); + await page.evaluate( + async ({ bookMarkUrl, embedUrl }) => { + const { doc } = window; + const rootId = doc.addBlock('affine:page', { + title: new doc.Text(), + }); + const note = doc.addBlock('affine:note', {}, rootId); + doc.addBlock('affine:code', {}, note); + doc.addBlock('affine:divider', {}, note); + doc.addBlock('affine:bookmark', { url: bookMarkUrl }, note); + await new Promise(res => setTimeout(res, 200)); + const pageRoot = document.querySelector('affine-page-root'); + if (!pageRoot) throw new Error('Cannot find doc page'); + const imageBlob = await fetch( + `${location.origin}/test-card-1.png` + ).then(response => response.blob()); + const storage = doc.blobSync; + const sourceId = await storage.set(imageBlob); + doc.addBlock('affine:image', { sourceId }, note); + doc.addBlock('affine:embed-github', { url: embedUrl }, note); + }, + { bookMarkUrl, embedUrl } + ); + const codeComponent = page.locator('affine-code'); + const codeComponentrect = await codeComponent.boundingBox(); + if (!codeComponentrect) { + throw new Error('code-block not found'); + } + await page.mouse.click( + codeComponentrect.x + 20, + codeComponentrect.y + codeComponentrect.height + 8 + ); + await assertBlockFlavour(page, '7', 'affine:paragraph'); + + const dividerComponent = page.locator('affine-divider'); + const dividerComponentRect = await dividerComponent.boundingBox(); + if (!dividerComponentRect) { + throw new Error('divider-block not found'); + } + await page.mouse.click( + dividerComponentRect.x + 20, + dividerComponentRect.y + dividerComponentRect.height + 8 + ); + await assertBlockFlavour(page, '8', 'affine:paragraph'); + + const bookmarkComponent = page.locator('affine-bookmark'); + const bookmarkComponentRect = await bookmarkComponent.boundingBox(); + if (!bookmarkComponentRect) { + throw new Error('bookmark-block not found'); + } + await page.mouse.click( + bookmarkComponentRect.x + 20, + bookmarkComponentRect.y + bookmarkComponentRect.height + 8 + ); + await assertBlockFlavour(page, '9', 'affine:paragraph'); + + await page.evaluate(() => { + const viewport = document.querySelector('.affine-page-viewport'); + if (!viewport) { + throw new Error(); + } + viewport.scrollTo(0, 600); + }); + + const imageComponent = page.locator('affine-image'); + const imageComponentRect = await imageComponent.boundingBox(); + if (!imageComponentRect) { + throw new Error('image-block not found'); + } + await page.mouse.click( + imageComponentRect.x + 20, + imageComponentRect.y + imageComponentRect.height + 8 + ); + await assertBlockFlavour(page, '10', 'affine:paragraph'); + } +); diff --git a/oxlint.json b/oxlint.json index e5fe6d31d522a..4b02f12885d80 100644 --- a/oxlint.json +++ b/oxlint.json @@ -194,6 +194,15 @@ "typescript/no-non-null-assertion": "off", "unicorn/prefer-array-some": "off" } + }, + { + "files": ["blocksuite/tests-legacy/**/*.ts"], + "rules": { + "typescript/ban-ts-comment": "off", + "unicorn/prefer-dom-node-dataset": "off", + "typescript/consistent-type-imports": "off", + "no-cycle": "off" + } } ] } diff --git a/package.json b/package.json index 69128e0b88bda..bb0a001e30e59 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@types/node": "^20.17.10", "@typescript-eslint/parser": "^8.18.0", "@vanilla-extract/vite-plugin": "^4.0.18", + "@vitest/browser": "2.1.8", "@vitest/coverage-istanbul": "2.1.8", "@vitest/ui": "2.1.8", "cross-env": "^7.0.3", diff --git a/scripts/vitest-global.js b/scripts/vitest-global.js new file mode 100644 index 0000000000000..370cddd32ad17 --- /dev/null +++ b/scripts/vitest-global.js @@ -0,0 +1,3 @@ +export const setup = () => { + process.env.TZ = 'Asia/Singapore'; +}; diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 89cb8a2833816..87ef83e7695d8 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1 +1,5 @@ -export default ['.', './packages/frontend/apps/electron']; +export default [ + '.', + './packages/frontend/apps/electron', + './blocksuite/**/*/vitest.config.ts', +]; diff --git a/yarn.lock b/yarn.lock index 427e86030ff79..1df85624e949a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -635,6 +635,7 @@ __metadata: "@types/node": "npm:^20.17.10" "@typescript-eslint/parser": "npm:^8.18.0" "@vanilla-extract/vite-plugin": "npm:^4.0.18" + "@vitest/browser": "npm:2.1.8" "@vitest/coverage-istanbul": "npm:2.1.8" "@vitest/ui": "npm:2.1.8" cross-env: "npm:^7.0.3" @@ -3413,6 +3414,7 @@ __metadata: "@lit/context": "npm:^1.1.2" "@preact/signals-core": "npm:^1.8.0" "@types/hast": "npm:^3.0.4" + dompurify: "npm:^3.1.6" fractional-indexing: "npm:^3.2.0" lib0: "npm:^0.2.97" lit: "npm:^3.2.0" @@ -3547,6 +3549,18 @@ __metadata: languageName: unknown linkType: soft +"@blocksuite/legacy-e2e@workspace:blocksuite/tests-legacy": + version: 0.0.0-use.local + resolution: "@blocksuite/legacy-e2e@workspace:blocksuite/tests-legacy" + dependencies: + "@blocksuite/affine-model": "workspace:*" + "@blocksuite/block-std": "workspace:*" + "@blocksuite/global": "workspace:*" + "@blocksuite/presets": "workspace:*" + "@playwright/test": "npm:=1.49.1" + languageName: unknown + linkType: soft + "@blocksuite/playground@workspace:blocksuite/playground": version: 0.0.0-use.local resolution: "@blocksuite/playground@workspace:blocksuite/playground" @@ -3575,6 +3589,8 @@ __metadata: lz-string: "npm:^1.5.0" magic-string: "npm:^0.30.11" tweakpane: "npm:^4.0.4" + vite: "npm:^6.0.3" + vite-plugin-istanbul: "npm:^6.0.2" vite-plugin-wasm: "npm:^3.3.0" vite-plugin-web-components-hmr: "npm:^0.1.3" y-indexeddb: "npm:^9.0.12" @@ -6914,6 +6930,19 @@ __metadata: languageName: node linkType: hard +"@istanbuljs/load-nyc-config@npm:^1.1.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: "npm:^5.3.1" + find-up: "npm:^4.1.0" + get-package-type: "npm:^0.1.0" + js-yaml: "npm:^3.13.1" + resolve-from: "npm:^5.0.0" + checksum: 10/b000a5acd8d4fe6e34e25c399c8bdbb5d3a202b4e10416e17bfc25e12bab90bb56d33db6089ae30569b52686f4b35ff28ef26e88e21e69821d2b85884bd055b8 + languageName: node + linkType: hard + "@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": version: 0.1.3 resolution: "@istanbuljs/schema@npm:0.1.3" @@ -13696,7 +13725,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/user-event@npm:14.5.2": +"@testing-library/user-event@npm:14.5.2, @testing-library/user-event@npm:^14.5.2": version: 14.5.2 resolution: "@testing-library/user-event@npm:14.5.2" peerDependencies: @@ -15199,6 +15228,34 @@ __metadata: languageName: node linkType: hard +"@vitest/browser@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/browser@npm:2.1.8" + dependencies: + "@testing-library/dom": "npm:^10.4.0" + "@testing-library/user-event": "npm:^14.5.2" + "@vitest/mocker": "npm:2.1.8" + "@vitest/utils": "npm:2.1.8" + magic-string: "npm:^0.30.12" + msw: "npm:^2.6.4" + sirv: "npm:^3.0.0" + tinyrainbow: "npm:^1.2.0" + ws: "npm:^8.18.0" + peerDependencies: + playwright: "*" + vitest: 2.1.8 + webdriverio: "*" + peerDependenciesMeta: + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + checksum: 10/6063e02222440347bbc23b2c54e259078aa83a29869337b9ffd642be5a4321ac3ddf3c0bbe4eac5237eb0bb8b9fa17d21d2c31299376de407716e3c7dd3b704c + languageName: node + linkType: hard + "@vitest/coverage-istanbul@npm:2.1.8": version: 2.1.8 resolution: "@vitest/coverage-istanbul@npm:2.1.8" @@ -17217,7 +17274,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.0.0": +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: 10/e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b @@ -21875,6 +21932,13 @@ __metadata: languageName: node linkType: hard +"get-package-type@npm:^0.1.0": + version: 0.1.0 + resolution: "get-package-type@npm:0.1.0" + checksum: 10/bba0811116d11e56d702682ddef7c73ba3481f114590e705fc549f4d868972263896af313c57a25c076e3c0d567e11d919a64ba1b30c879be985fc9d44f96148 + languageName: node + linkType: hard + "get-source@npm:^2.0.12": version: 2.0.12 resolution: "get-source@npm:2.0.12" @@ -23892,7 +23956,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:^6.0.3": +"istanbul-lib-instrument@npm:^6.0.2, istanbul-lib-instrument@npm:^6.0.3": version: 6.0.3 resolution: "istanbul-lib-instrument@npm:6.0.3" dependencies: @@ -24092,7 +24156,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.14.1": +"js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" dependencies: @@ -26402,9 +26466,9 @@ __metadata: languageName: node linkType: hard -"msw@npm:^2.6.8": - version: 2.6.9 - resolution: "msw@npm:2.6.9" +"msw@npm:^2.6.4, msw@npm:^2.6.8": + version: 2.7.0 + resolution: "msw@npm:2.7.0" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.1" "@bundled-es-modules/statuses": "npm:^1.0.1" @@ -26415,12 +26479,12 @@ __metadata: "@open-draft/until": "npm:^2.1.0" "@types/cookie": "npm:^0.6.0" "@types/statuses": "npm:^2.0.4" - chalk: "npm:^4.1.2" graphql: "npm:^16.8.1" headers-polyfill: "npm:^4.0.2" is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.3" path-to-regexp: "npm:^6.3.0" + picocolors: "npm:^1.1.1" strict-event-emitter: "npm:^0.5.1" type-fest: "npm:^4.26.1" yargs: "npm:^17.7.2" @@ -26431,7 +26495,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/20a74a5e49b780567aa3430c0de9f27830208f931d6109087a24566fcb3e68f058ff51b022891bfe6fe0f320afad22b4039593722b928aa9c4ab5b05c2746c4a + checksum: 10/165ccf37d90da0d5271fdb8e01f89f48f7a60fb810038ff73d18c0e5e5ddfdb1266002d19cde61b9ae689ef37c39499b10d9d07e0d16662a31630ce9adce1d77 languageName: node linkType: hard @@ -31111,7 +31175,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.7.3": +"source-map@npm:^0.7.3, source-map@npm:^0.7.4": version: 0.7.4 resolution: "source-map@npm:0.7.4" checksum: 10/a0f7c9b797eda93139842fd28648e868a9a03ea0ad0d9fa6602a0c1f17b7fb6a7dcca00c144476cccaeaae5042e99a285723b1a201e844ad67221bf5d428f1dc @@ -32066,6 +32130,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^6.0.0": + version: 6.0.0 + resolution: "test-exclude@npm:6.0.0" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^7.1.4" + minimatch: "npm:^3.0.4" + checksum: 10/8fccb2cb6c8fcb6bb4115394feb833f8b6cf4b9503ec2485c2c90febf435cac62abe882a0c5c51a37b9bbe70640cdd05acf5f45e486ac4583389f4b0855f69e5 + languageName: node + linkType: hard + "test-exclude@npm:^7.0.1": version: 7.0.1 resolution: "test-exclude@npm:7.0.1" @@ -33478,6 +33553,22 @@ __metadata: languageName: node linkType: hard +"vite-plugin-istanbul@npm:^6.0.2": + version: 6.0.2 + resolution: "vite-plugin-istanbul@npm:6.0.2" + dependencies: + "@istanbuljs/load-nyc-config": "npm:^1.1.0" + espree: "npm:^10.0.1" + istanbul-lib-instrument: "npm:^6.0.2" + picocolors: "npm:^1.0.0" + source-map: "npm:^0.7.4" + test-exclude: "npm:^6.0.0" + peerDependencies: + vite: ">=4 <=6" + checksum: 10/fddd8367fa02159a047179c9c576c309f9a14bb970116e35fe543d40e3acc615c1671387b9de2394a5bc75d2ab0cfb7b7ebb9e29ec4ddff5411fa7bfee663e42 + languageName: node + linkType: hard + "vite-plugin-wasm@npm:^3.3.0": version: 3.4.1 resolution: "vite-plugin-wasm@npm:3.4.1"