From 16d4430ec9a5a09222c4b7de63705813737105ba Mon Sep 17 00:00:00 2001 From: L-Sun Date: Tue, 21 Jan 2025 13:34:59 +0000 Subject: [PATCH] test(editor): move tests of toc to affine (#9833) --- .../outline/body/outline-panel-body.ts | 50 ++- .../src/fragments/outline/outline-viewer.ts | 6 + .../tests-legacy/edgeless/shortcut.spec.ts | 7 +- .../fragments/outline/outline-panel.spec.ts | 352 ----------------- .../fragments/outline/toc-viewer.spec.ts | 208 ---------- .../tests-legacy/fragments/outline/utils.ts | 27 -- .../blocksuite/outline/outline-panel.spec.ts | 371 ++++++++++++++++++ .../outline-viewer.spec.ts} | 249 ++++++------ .../e2e/blocksuite/outline/utils.ts | 34 ++ tests/kit/src/utils/keyboard.ts | 12 +- tests/kit/src/utils/setting.ts | 7 + 11 files changed, 595 insertions(+), 728 deletions(-) delete mode 100644 blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts delete mode 100644 blocksuite/tests-legacy/fragments/outline/toc-viewer.spec.ts delete mode 100644 blocksuite/tests-legacy/fragments/outline/utils.ts create mode 100644 tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts rename tests/affine-local/e2e/blocksuite/{outline.spec.ts => outline/outline-viewer.spec.ts} (52%) create mode 100644 tests/affine-local/e2e/blocksuite/outline/utils.ts diff --git a/blocksuite/presets/src/fragments/outline/body/outline-panel-body.ts b/blocksuite/presets/src/fragments/outline/body/outline-panel-body.ts index bbfef2ff34..fe794940ca 100644 --- a/blocksuite/presets/src/fragments/outline/body/outline-panel-body.ts +++ b/blocksuite/presets/src/fragments/outline/body/outline-panel-body.ts @@ -190,10 +190,14 @@ export class OutlinePanelBody extends SignalWatcher( ) return; - this.edgeless?.service.selection.set({ - elements: selectedVisibleNotes, - editing: false, - }); + if (this.edgeless) { + this.edgeless.service.selection.set({ + elements: selectedVisibleNotes, + editing: false, + }); + } else { + this._selectedNotes$.value = selectedVisibleNotes; + } this._dragging = true; @@ -452,10 +456,14 @@ export class OutlinePanelBody extends SignalWatcher( selectedNotes = [id]; } - this.edgeless?.service.selection.set({ - elements: selectedNotes, - editing: false, - }); + if (this.edgeless) { + this.edgeless?.service.selection.set({ + elements: selectedNotes, + editing: false, + }); + } else { + this._selectedNotes$.value = selectedNotes; + } } private _setDocDisposables() { @@ -549,25 +557,15 @@ export class OutlinePanelBody extends SignalWatcher( this.disposables.add( effect(() => { const { std, doc, mode } = this.editor; + if (mode !== 'edgeless') return; - const currSelectedNotes = - mode === 'edgeless' - ? std.selection - .filter(SurfaceSelection) - .filter(({ blockId }) => { - const model = doc.getBlock(blockId)?.model; - return !!model && matchFlavours(model, ['affine:note']); - }) - .map(({ blockId }) => blockId) - : (std.command.exec('getSelectedModels').selectedModels ?? []) - .map(model => { - let parent = model.parent; - while (parent && !matchFlavours(parent, ['affine:note'])) { - parent = parent.parent; - } - return parent ? [parent.id] : []; - }) - .flat(); + const currSelectedNotes = std.selection + .filter(SurfaceSelection) + .filter(({ blockId }) => { + const model = doc.getBlock(blockId)?.model; + return !!model && matchFlavours(model, ['affine:note']); + }) + .map(({ blockId }) => blockId); const preSelected = this._selectedNotes$.peek(); if ( diff --git a/blocksuite/presets/src/fragments/outline/outline-viewer.ts b/blocksuite/presets/src/fragments/outline/outline-viewer.ts index e7aad26c0f..02da29f7ec 100644 --- a/blocksuite/presets/src/fragments/outline/outline-viewer.ts +++ b/blocksuite/presets/src/fragments/outline/outline-viewer.ts @@ -182,6 +182,12 @@ export class OutlineViewer extends SignalWatcher(WithDisposable(LitElement)) { } ) ); + + this.disposables.add( + this.editor.doc.workspace.meta.docMetaUpdated.on(() => { + this.requestUpdate(); + }) + ); } override disconnectedCallback() { diff --git a/blocksuite/tests-legacy/edgeless/shortcut.spec.ts b/blocksuite/tests-legacy/edgeless/shortcut.spec.ts index 2ae0579b65..33d2f480e1 100644 --- a/blocksuite/tests-legacy/edgeless/shortcut.spec.ts +++ b/blocksuite/tests-legacy/edgeless/shortcut.spec.ts @@ -134,8 +134,8 @@ test('should not switch shapes in editing', async ({ page }) => { await type(page, 'hello'); await page.keyboard.press('Shift+s'); - await page.keyboard.press('Escape'); - await waitNextFrame(page); + await pressEscape(page); + await waitNextFrame(page, 200); await setEdgelessTool(page, 'shape'); await assertEdgelessShapeType(page, 'rect'); @@ -143,8 +143,9 @@ test('should not switch shapes in editing', async ({ page }) => { await page.mouse.dblclick(250, 200); await waitNextFrame(page); await page.keyboard.press('Shift+S'); - await page.keyboard.press('Escape'); + await pressEscape(page); await waitNextFrame(page); + await waitNextFrame(page, 200); await setEdgelessTool(page, 'shape'); await assertEdgelessShapeType(page, 'rect'); }); diff --git a/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts b/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts deleted file mode 100644 index 981d2d8c6c..0000000000 --- a/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts +++ /dev/null @@ -1,352 +0,0 @@ -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 { NoteDisplayMode } from '../../utils/bs-alternative.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 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.click(fromRect!.x + 10, fromRect!.y + 10); - await page.mouse.down(); - await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 20 }); - 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); - 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); - 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); - 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 deleted file mode 100644 index 6818613b79..0000000000 --- a/blocksuite/tests-legacy/fragments/outline/toc-viewer.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -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 deleted file mode 100644 index 2e68620164..0000000000 --- a/blocksuite/tests-legacy/fragments/outline/utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -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/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts new file mode 100644 index 0000000000..9f86f3de64 --- /dev/null +++ b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts @@ -0,0 +1,371 @@ +import { test } from '@affine-test/kit/playwright'; +import { + clickEdgelessModeButton, + clickPageModeButton, + clickView, + createEdgelessNoteBlock, + locateElementToolbar, +} from '@affine-test/kit/utils/editor'; +import { + pressBackspace, + pressEnter, + selectAllByKeyboard, +} from '@affine-test/kit/utils/keyboard'; +import { openHomePage } from '@affine-test/kit/utils/load-page'; +import { + clickNewPageButton, + type, + waitForEditorLoad, +} from '@affine-test/kit/utils/page-logic'; +import { + closeSettingModal, + confirmExperimentalPrompt, + openExperimentalFeaturesPanel, + openSettingModal, +} from '@affine-test/kit/utils/setting'; +import { openRightSideBar } from '@affine-test/kit/utils/sidebar'; +import { expect, type Locator, type Page } from '@playwright/test'; + +import { + createHeadings, + createTitle, + getVerticalCenterFromLocator, +} from './utils'; + +async function openTocPanel(page: Page) { + await openRightSideBar(page, 'outline'); + const toc = page.locator('affine-outline-panel'); + await toc.waitFor({ state: 'visible' }); + return toc; +} + +function getTocHeading(panel: Locator, level: number) { + return panel.locator(`.h${level} span`); +} + +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: 20 }); + await page.mouse.up(); +} + +test.beforeEach(async ({ page }) => { + await openHomePage(page); + await clickNewPageButton(page); + await waitForEditorLoad(page); +}); + +test('should display title and headings when there are non-empty headings in editor', async ({ + page, +}) => { + await createTitle(page); + await createHeadings(page); + + const toc = await openTocPanel(page); + + await expect(toc.locator('.title')).toBeVisible(); + for (let i = 1; i <= 6; i++) { + await expect(getTocHeading(toc, i)).toBeVisible(); + await expect(getTocHeading(toc, i)).toContainText(`Heading ${i}`); + } +}); + +test('should display placeholder when no headings', async ({ page }) => { + const toc = await openTocPanel(page); + const noHeadingPlaceholder = toc.locator('.note-placeholder'); + + await createTitle(page); + await pressEnter(page); + await type(page, 'hello world'); + + await expect(noHeadingPlaceholder).toBeVisible(); +}); + +test('should not display headings when there are only empty headings', async ({ + page, +}) => { + await createTitle(page); + + // create empty headings + for (let i = 1; i <= 6; i++) { + await type(page, `${'#'.repeat(i)} `); + await pressEnter(page); + } + + const toc = await openTocPanel(page); + + await expect(toc.locator('.title')).toBeHidden(); + for (let i = 1; i <= 6; i++) { + await expect(getTocHeading(toc, i)).toBeHidden(); + } +}); + +test('should update panel when modify or clear title or headings', async ({ + page, +}) => { + const title = await createTitle(page); + const headings = await createHeadings(page); + + const toc = await openTocPanel(page); + + await title.scrollIntoViewIfNeeded(); + await title.click(); + await type(page, 'xxx'); + await expect(toc.locator('.title')).toContainText(['Titlexxx']); + await selectAllByKeyboard(page); + await pressBackspace(page); + await expect(toc.locator('.title')).toBeHidden(); + + for (let i = 1; i <= 6; i++) { + await headings[i - 1].click(); + await type(page, 'xxx'); + await expect(getTocHeading(toc, i)).toContainText(`Heading ${i}xxx`); + await selectAllByKeyboard(page); + await pressBackspace(page); + await expect(getTocHeading(toc, i)).toBeHidden(); + } +}); + +test('should add padding to sub-headings', async ({ page }) => { + await createHeadings(page); + + const toc = await openTocPanel(page); + + let prev = getTocHeading(toc, 1); + for (let i = 2; i <= 6; i++) { + const curr = getTocHeading(toc, i); + + const prevRect = await prev.boundingBox(); + const currRect = await curr.boundingBox(); + + expect(prevRect).not.toBeNull(); + expect(currRect).not.toBeNull(); + + expect(prevRect!.x).toBeLessThan(currRect!.x); + prev = curr; + } +}); + +test('should highlight heading when scroll to area before viewport center', async ({ + page, +}) => { + const title = await createTitle(page); + for (let i = 0; i < 3; i++) { + await pressEnter(page); + } + const headings = await createHeadings(page, 10); + await title.scrollIntoViewIfNeeded(); + + const toc = await openTocPanel(page); + + const viewportCenter = await getVerticalCenterFromLocator( + page.locator('body') + ); + + const activeHeadingContainer = toc.locator( + 'affine-outline-panel-body .active' + ); + + await title.click(); + await expect(activeHeadingContainer).toContainText('Title'); + + for (let i = 0; i < headings.length; i++) { + const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]); + await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 20); + await page.waitForTimeout(10); + + await expect(activeHeadingContainer).toContainText(`Heading ${i + 1}`); + } +}); + +test('should scroll to heading and highlight heading when click item in outline panel', async ({ + page, +}) => { + const headings = await createHeadings(page, 10); + const toc = await openTocPanel(page); + + const activeHeadingContainer = toc.locator( + 'affine-outline-panel-body .active' + ); + + const headingsInPanel = Array.from({ length: 6 }, (_, i) => + getTocHeading(toc, 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, +}) => { + const title = await createTitle(page); + await pressEnter(page); + await createHeadings(page, 10); + + const toc = await openTocPanel(page); + + const titleInPanel = toc.locator('.title'); + + await expect(title).not.toBeInViewport(); + await titleInPanel.click(); + await expect(title).toBeVisible(); +}); + +test('visibility sorting should be enabled in edgeless mode and disabled in page mode by default, and can be changed', async ({ + page, +}) => { + await pressEnter(page); + await type(page, '# Heading 1'); + + const toc = await openTocPanel(page); + + const sortingButton = toc.locator('.note-sorting-button'); + await expect(sortingButton).not.toHaveClass(/active/); + expect(toc.locator('[data-sortable="false"]')).toHaveCount(1); + + await clickEdgelessModeButton(page); + await expect(sortingButton).toHaveClass(/active/); + expect(toc.locator('[data-sortable="true"]')).toHaveCount(1); + + await sortingButton.click(); + await expect(sortingButton).not.toHaveClass(/active/); + expect(toc.locator('[data-sortable="false"]')).toHaveCount(1); +}); + +test('should reorder notes when drag and drop note in outline panel', async ({ + page, +}) => { + await clickEdgelessModeButton(page); + await createEdgelessNoteBlock(page, [100, 100]); + await type(page, 'hello'); + await createEdgelessNoteBlock(page, [200, 200]); + await type(page, 'world'); + + const toc = await openTocPanel(page); + + const docVisibleCards = toc.locator( + '.card-container[data-invisible="false"]' + ); + const docInvisibleCards = toc.locator( + '.card-container[data-invisible="true"]' + ); + + await expect(docVisibleCards).toHaveCount(1); + await expect(docInvisibleCards).toHaveCount(2); + + while ((await docInvisibleCards.count()) > 0) { + const card = docInvisibleCards.first(); + await card.hover(); + await card.locator('.display-mode-button').click(); + await card.locator('note-display-mode-panel').locator('.item.both').click(); + } + + await expect(docVisibleCards).toHaveCount(3); + const noteCard3 = docVisibleCards.nth(2); + const noteCard1 = docVisibleCards.nth(0); + + await dragNoteCard(page, noteCard3, noteCard1); + + await clickPageModeButton(page); + const paragraphs = page + .locator('affine-paragraph') + .locator('[data-v-text="true"]'); + await expect(paragraphs).toHaveCount(3); + await expect(paragraphs.nth(0)).toContainText('world'); + await expect(paragraphs.nth(1)).toContainText(''); + await expect(paragraphs.nth(2)).toContainText('hello'); + + // FIXME(@L-Sun): drag and drop is not working in page mode + await dragNoteCard(page, noteCard3, noteCard1); + + await expect(paragraphs.nth(0)).toContainText('hello'); + await expect(paragraphs.nth(1)).toContainText('world'); + await expect(paragraphs.nth(2)).toContainText(''); +}); + +test.describe('advanced visibility control', () => { + test.beforeEach(async ({ page }) => { + await openSettingModal(page); + await openExperimentalFeaturesPanel(page); + await confirmExperimentalPrompt(page); + await page.getByTestId('enable_advanced_block_visibility').click(); + await closeSettingModal(page); + await page.reload(); + }); + + test('should update notes when change note display mode from note toolbar', async ({ + page, + }) => { + await clickEdgelessModeButton(page); + await createEdgelessNoteBlock(page, [100, 100]); + await type(page, 'hello'); + await clickView(page, [200, 200]); + + const toc = await openTocPanel(page); + + const docVisibleCard = toc.locator( + '.card-container[data-invisible="false"]' + ); + const docInvisibleCard = toc.locator( + '.card-container[data-invisible="true"]' + ); + + await expect(docVisibleCard).toHaveCount(1); + await expect(docInvisibleCard).toHaveCount(1); + + await clickView(page, [100, 100]); + const noteButtons = locateElementToolbar(page).locator( + 'edgeless-change-note-button' + ); + + await noteButtons.getByRole('button', { name: 'Mode' }).click(); + await noteButtons.locator('note-display-mode-panel .item.both').click(); + + await expect(docVisibleCard).toHaveCount(2); + await expect(docInvisibleCard).toHaveCount(0); + }); + + test('should update notes after slicing note', async ({ page }) => { + await clickEdgelessModeButton(page); + await createEdgelessNoteBlock(page, [200, 100]); + await type(page, 'hello'); + await pressEnter(page); + await type(page, 'world'); + + const toc = await openTocPanel(page); + + const docVisibleCard = toc.locator( + '.card-container[data-invisible="false"]' + ); + const docInvisibleCard = toc.locator( + '.card-container[data-invisible="true"]' + ); + + await expect(docVisibleCard).toHaveCount(1); + await expect(docInvisibleCard).toHaveCount(1); + + await docInvisibleCard.hover(); + await docInvisibleCard.locator('.display-mode-button').click(); + await docInvisibleCard + .locator('note-display-mode-panel .item.both') + .click(); + + await expect(docVisibleCard).toHaveCount(2); + + await clickView(page, [200, 100]); + const changeNoteButtons = locateElementToolbar(page).locator( + 'edgeless-change-note-button' + ); + await changeNoteButtons.getByRole('button', { name: 'Slicer' }).click(); + await expect(page.locator('.note-slicer-button')).toBeVisible(); + await page.locator('.note-slicer-button').click(); + + await expect(docVisibleCard).toHaveCount(3); + }); +}); diff --git a/tests/affine-local/e2e/blocksuite/outline.spec.ts b/tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts similarity index 52% rename from tests/affine-local/e2e/blocksuite/outline.spec.ts rename to tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts index 761422780c..e37032ca9b 100644 --- a/tests/affine-local/e2e/blocksuite/outline.spec.ts +++ b/tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts @@ -2,12 +2,10 @@ import { test } from '@affine-test/kit/playwright'; import { clickEdgelessModeButton, clickPageModeButton, - clickView, createEdgelessNoteBlock, - locateElementToolbar, - locateModeSwitchButton, } from '@affine-test/kit/utils/editor'; import { + pressBackspace, pressEnter, selectAllByKeyboard, } from '@affine-test/kit/utils/keyboard'; @@ -15,60 +13,135 @@ import { openHomePage } from '@affine-test/kit/utils/load-page'; import { clickNewPageButton, createLinkedPage, - getBlockSuiteEditorTitle, type, waitForEditorLoad, - waitForEmptyEditor, } from '@affine-test/kit/utils/page-logic'; -import { openRightSideBar } from '@affine-test/kit/utils/sidebar'; import { expect, type Locator, type Page } from '@playwright/test'; +import { + createHeadings, + createTitle, + getVerticalCenterFromLocator, +} from './utils'; + function getIndicators(container: Page | Locator) { return container.locator('affine-outline-viewer .outline-viewer-indicator'); } -test('outline viewer is useable', async ({ page }) => { +test.beforeEach(async ({ page }) => { await openHomePage(page); - await waitForEditorLoad(page); await clickNewPageButton(page); await waitForEditorLoad(page); +}); - const title = getBlockSuiteEditorTitle(page); - await title.click(); - await title.pressSequentially('Title'); - await expect(title).toContainText('Title'); - await page.keyboard.press('Enter'); - await page.keyboard.type('# '); - await page.keyboard.type('Heading 1'); - await page.keyboard.press('Enter'); - await page.keyboard.type('## '); - await page.keyboard.type('Heading 2'); - await page.keyboard.press('Enter'); - +test('should display indicators when non-empty headings exists', async ({ + page, +}) => { const indicators = getIndicators(page); - await expect(indicators).toHaveCount(3); - await expect(indicators.nth(0)).toBeVisible(); - await expect(indicators.nth(1)).toBeVisible(); - await expect(indicators.nth(2)).toBeVisible(); + await createHeadings(page); - const viewer = page.locator('affine-outline-viewer'); + await expect(indicators).toHaveCount(6); + for (let i = 0; i < 6; i++) { + await expect(indicators.nth(i)).toBeVisible(); + } +}); + +test('should be hidden when only empty headings exists', async ({ page }) => { + const indicators = getIndicators(page); + await expect(indicators).toHaveCount(0); + + for (let i = 1; i <= 6; i++) { + // empty heading + await type(page, `${'#'.repeat(i)} `); + await pressEnter(page); + } + + await expect(indicators).toHaveCount(0); +}); + +test('should update indicator when clear title or headings', async ({ + page, +}) => { + const indicators = getIndicators(page); + const title = await createTitle(page); + const headings = await createHeadings(page); + + await expect(indicators).toHaveCount(7); + + await title.scrollIntoViewIfNeeded(); + await title.click(); + await selectAllByKeyboard(page); + await pressBackspace(page); + await expect(indicators).toHaveCount(6); + + for (let i = 1; i <= 6; i++) { + await headings[i - 1].click(); + await selectAllByKeyboard(page); + await pressBackspace(page); + await expect(indicators).toHaveCount(6 - i); + } +}); + +test('should display simple outline panel when hovering over indicators', async ({ + page, +}) => { + const indicators = getIndicators(page); + await createTitle(page); + await createHeadings(page); await indicators.first().hover({ force: true }); - await expect(viewer).toBeVisible(); + + const items = page.locator('.outline-viewer-item'); + await expect(items).toHaveCount(8); + await expect(items.nth(0)).toContainText(['Table of Contents']); + await expect(items.nth(1)).toContainText(['Title']); + for (let i = 2; i <= 7; i++) { + await expect(items.nth(i)).toContainText([`Heading ${i - 1}`]); + } +}); + +test('should highlight indicator when scrolling', async ({ page }) => { + const indicators = getIndicators(page); + const title = await createTitle(page); + for (let i = 1; i <= 3; i++) { + await pressEnter(page); + } + const headings = await createHeadings(page, 10); + await title.scrollIntoViewIfNeeded(); + + const viewportCenter = await getVerticalCenterFromLocator( + page.locator('body') + ); + for (let i = 0; i < headings.length; i++) { + const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]); + await expect(indicators.nth(i)).toHaveClass(/active/); + await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 20); + await page.waitForTimeout(10); + } +}); + +test('should highlight indicator when click item in outline panel', async ({ + page, +}) => { + const viewer = page.locator('affine-outline-viewer'); + const indicators = getIndicators(page); + const headings = await createHeadings(page, 10); + + 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('outline viewer should hide in edgeless mode', async ({ page }) => { - await openHomePage(page); - await waitForEditorLoad(page); - await clickNewPageButton(page); - await waitForEditorLoad(page); + await createTitle(page); + await pressEnter(page); - const title = getBlockSuiteEditorTitle(page); - await title.click(); - await title.pressSequentially('Title'); - await page.keyboard.press('Enter'); - await expect(title).toHaveText('Title'); - await page.keyboard.type('# '); - await page.keyboard.type('Heading 1'); + await type(page, '# '); + await type(page, 'Heading 1'); const indicators = getIndicators(page); await expect(indicators).toHaveCount(2); @@ -80,15 +153,34 @@ test('outline viewer should hide in edgeless mode', async ({ page }) => { await expect(indicators).toHaveCount(2); }); +test('should hide edgeless-only note headings', async ({ page }) => { + await createTitle(page); + await pressEnter(page); + await type(page, '# Heading 1'); + await pressEnter(page); + await type(page, '## Heading 2'); + + await clickEdgelessModeButton(page); + await createEdgelessNoteBlock(page, [100, 100]); + await type(page, '# Edgeless'); + + await clickPageModeButton(page); + await waitForEditorLoad(page); + const indicators = getIndicators(page); + await expect(indicators).toHaveCount(3); + await indicators.first().hover({ force: true }); + + const viewer = page.locator('affine-outline-viewer'); + await expect(viewer).toBeVisible(); + const h1InPanel = viewer.locator('.h1 > span'); + await h1InPanel.waitFor({ state: 'visible' }); + expect(h1InPanel).toContainText(['Heading 1']); +}); + test('outline viewer should be useable in doc peek preview', async ({ page, }) => { - await openHomePage(page); - await waitForEditorLoad(page); - await clickNewPageButton(page); - await waitForEmptyEditor(page); - - await page.keyboard.press('Enter'); + await pressEnter(page); await createLinkedPage(page, 'Test Page'); await page.locator('affine-reference').hover(); @@ -109,15 +201,15 @@ test('outline viewer should be useable in doc peek preview', async ({ const title = peekView.locator('doc-title .inline-editor'); await title.click(); - await page.keyboard.press('Enter'); + await pressEnter(page); - await page.keyboard.type('# Heading 1'); + await type(page, '# Heading 1'); for (let i = 0; i < 10; i++) { - await page.keyboard.press('Enter'); + await pressEnter(page); } - await page.keyboard.type('## Heading 2'); + await type(page, '## Heading 2'); const outlineViewer = peekView.locator('affine-outline-viewer'); const outlineViewerBound = await outlineViewer.boundingBox(); @@ -166,66 +258,3 @@ test('outline viewer should be useable in doc peek preview', async ({ await expect(page.locator('affine-outline-panel')).toBeVisible(); } }); - -test('visibility sorting should be enabled in edgeless mode and disabled in page mode by default, and can be changed', async ({ - page, -}) => { - await openHomePage(page); - await clickNewPageButton(page); - await waitForEditorLoad(page); - await pressEnter(page); - await type(page, '# Heading 1'); - await openRightSideBar(page, 'outline'); - - const toc = page.locator('affine-outline-panel'); - const sortingButton = toc.locator('.note-sorting-button'); - await expect(sortingButton).not.toHaveClass(/active/); - expect(toc.locator('[data-sortable="false"]')).toHaveCount(1); - - await clickEdgelessModeButton(page); - await expect(sortingButton).toHaveClass(/active/); - expect(toc.locator('[data-sortable="true"]')).toHaveCount(1); - - await sortingButton.click(); - await expect(sortingButton).not.toHaveClass(/active/); - expect(toc.locator('[data-sortable="false"]')).toHaveCount(1); -}); - -test('note cards of TOC should be highlight when selections contains the corresponding notes', async ({ - page, -}) => { - await openHomePage(page); - await clickNewPageButton(page); - await locateModeSwitchButton(page, 'edgeless').click(); - await waitForEditorLoad(page); - await openRightSideBar(page, 'outline'); - - const toc = page.locator('affine-outline-panel'); - const highlightNoteCards = toc.locator( - 'affine-outline-note-card > .selected' - ); - - await expect(highlightNoteCards).toHaveCount(0); - - await clickView(page, [0, 0]); - await selectAllByKeyboard(page); - await expect(highlightNoteCards).toHaveCount(1); - - await createEdgelessNoteBlock(page, [100, 100]); - await expect(highlightNoteCards).toHaveCount(1); - - await clickView(page, [200, 200]); - await selectAllByKeyboard(page); - await expect(highlightNoteCards).toHaveCount(2); - - await clickView(page, [100, 100]); - const toolbar = locateElementToolbar(page); - await toolbar.getByTestId('display-in-page').click(); - await clickPageModeButton(page); - await page.keyboard.press('ArrowDown'); - await expect(highlightNoteCards).toHaveCount(1); - await selectAllByKeyboard(page); - await selectAllByKeyboard(page); - await selectAllByKeyboard(page); - await expect(highlightNoteCards).toHaveCount(2); -}); diff --git a/tests/affine-local/e2e/blocksuite/outline/utils.ts b/tests/affine-local/e2e/blocksuite/outline/utils.ts new file mode 100644 index 0000000000..5ee4e5462c --- /dev/null +++ b/tests/affine-local/e2e/blocksuite/outline/utils.ts @@ -0,0 +1,34 @@ +import { locateEditorContainer } from '@affine-test/kit/utils/editor'; +import { pressEnter } from '@affine-test/kit/utils/keyboard'; +import { + getBlockSuiteEditorTitle, + type, +} from '@affine-test/kit/utils/page-logic'; +import type { Locator, Page } from '@playwright/test'; + +export async function createTitle(page: Page) { + const title = getBlockSuiteEditorTitle(page); + await title.scrollIntoViewIfNeeded(); + await title.click(); + await type(page, 'Title'); + await pressEnter(page); + return title; +} + +export async function createHeadings(page: Page, gap = 0) { + const editorContainer = locateEditorContainer(page); + + const headings: Locator[] = []; + await pressEnter(page, gap + 1); + for (let i = 1; i <= 6; i++) { + await type(page, `${'#'.repeat(i)} Heading ${i}`); + headings.push(editorContainer.locator(`.h${i}`)); + await pressEnter(page, gap + 1); + } + return headings; +} + +export async function getVerticalCenterFromLocator(locator: Locator) { + const rect = await locator.boundingBox(); + return rect!.y + rect!.height / 2; +} diff --git a/tests/kit/src/utils/keyboard.ts b/tests/kit/src/utils/keyboard.ts index 814f0b2ca4..3ed65c7808 100644 --- a/tests/kit/src/utils/keyboard.ts +++ b/tests/kit/src/utils/keyboard.ts @@ -25,9 +25,11 @@ export const withCtrlOrMeta = async (page: Page, fn: () => Promise) => { await keyUpCtrlOrMeta(page); }; -export async function pressEnter(page: Page) { +export async function pressEnter(page: Page, count = 1) { // avoid flaky test by simulate real user input - await page.keyboard.press('Enter', { delay: 50 }); + for (let i = 0; i < count; i++) { + await page.keyboard.press('Enter', { delay: 50 }); + } } export async function pressTab(page: Page) { @@ -46,6 +48,12 @@ export async function pressShiftEnter(page: Page) { await page.keyboard.up('Shift'); } +export async function pressBackspace(page: Page, count = 1) { + for (let i = 0; i < count; i++) { + await page.keyboard.press('Backspace', { delay: 50 }); + } +} + export async function copyByKeyboard(page: Page) { await keyDownCtrlOrMeta(page); await page.keyboard.press('c', { delay: 50 }); diff --git a/tests/kit/src/utils/setting.ts b/tests/kit/src/utils/setting.ts index 293b20b544..fd882c2535 100644 --- a/tests/kit/src/utils/setting.ts +++ b/tests/kit/src/utils/setting.ts @@ -50,3 +50,10 @@ export async function clickUserInfoCard(page: Page) { delay: 50, }); } + +export async function closeSettingModal(page: Page) { + await page + .getByTestId('setting-modal') + .getByTestId('modal-close-button') + .click(); +}