test(editor): move tests of toc to affine (#9833)

This commit is contained in:
L-Sun
2025-01-21 13:34:59 +00:00
parent 24fce7a708
commit 16d4430ec9
11 changed files with 595 additions and 728 deletions

View File

@@ -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 (

View File

@@ -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() {

View File

@@ -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');
});

View File

@@ -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);
});
});

View File

@@ -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();
});
});

View File

@@ -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;
}