test(editor): move tests of toc to affine (#9833)
This commit is contained in:
@@ -190,10 +190,14 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.edgeless?.service.selection.set({
|
if (this.edgeless) {
|
||||||
elements: selectedVisibleNotes,
|
this.edgeless.service.selection.set({
|
||||||
editing: false,
|
elements: selectedVisibleNotes,
|
||||||
});
|
editing: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._selectedNotes$.value = selectedVisibleNotes;
|
||||||
|
}
|
||||||
|
|
||||||
this._dragging = true;
|
this._dragging = true;
|
||||||
|
|
||||||
@@ -452,10 +456,14 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
selectedNotes = [id];
|
selectedNotes = [id];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.edgeless?.service.selection.set({
|
if (this.edgeless) {
|
||||||
elements: selectedNotes,
|
this.edgeless?.service.selection.set({
|
||||||
editing: false,
|
elements: selectedNotes,
|
||||||
});
|
editing: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._selectedNotes$.value = selectedNotes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setDocDisposables() {
|
private _setDocDisposables() {
|
||||||
@@ -549,25 +557,15 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const { std, doc, mode } = this.editor;
|
const { std, doc, mode } = this.editor;
|
||||||
|
if (mode !== 'edgeless') return;
|
||||||
|
|
||||||
const currSelectedNotes =
|
const currSelectedNotes = std.selection
|
||||||
mode === 'edgeless'
|
.filter(SurfaceSelection)
|
||||||
? std.selection
|
.filter(({ blockId }) => {
|
||||||
.filter(SurfaceSelection)
|
const model = doc.getBlock(blockId)?.model;
|
||||||
.filter(({ blockId }) => {
|
return !!model && matchFlavours(model, ['affine:note']);
|
||||||
const model = doc.getBlock(blockId)?.model;
|
})
|
||||||
return !!model && matchFlavours(model, ['affine:note']);
|
.map(({ blockId }) => blockId);
|
||||||
})
|
|
||||||
.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 preSelected = this._selectedNotes$.peek();
|
const preSelected = this._selectedNotes$.peek();
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -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() {
|
override disconnectedCallback() {
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ test('should not switch shapes in editing', async ({ page }) => {
|
|||||||
|
|
||||||
await type(page, 'hello');
|
await type(page, 'hello');
|
||||||
await page.keyboard.press('Shift+s');
|
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 setEdgelessTool(page, 'shape');
|
||||||
await assertEdgelessShapeType(page, 'rect');
|
await assertEdgelessShapeType(page, 'rect');
|
||||||
|
|
||||||
@@ -143,8 +143,9 @@ test('should not switch shapes in editing', async ({ page }) => {
|
|||||||
await page.mouse.dblclick(250, 200);
|
await page.mouse.dblclick(250, 200);
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
await page.keyboard.press('Shift+S');
|
await page.keyboard.press('Shift+S');
|
||||||
await page.keyboard.press('Escape');
|
await pressEscape(page);
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
|
await waitNextFrame(page, 200);
|
||||||
await setEdgelessTool(page, 'shape');
|
await setEdgelessTool(page, 'shape');
|
||||||
await assertEdgelessShapeType(page, 'rect');
|
await assertEdgelessShapeType(page, 'rect');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
371
tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts
Normal file
371
tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,12 +2,10 @@ import { test } from '@affine-test/kit/playwright';
|
|||||||
import {
|
import {
|
||||||
clickEdgelessModeButton,
|
clickEdgelessModeButton,
|
||||||
clickPageModeButton,
|
clickPageModeButton,
|
||||||
clickView,
|
|
||||||
createEdgelessNoteBlock,
|
createEdgelessNoteBlock,
|
||||||
locateElementToolbar,
|
|
||||||
locateModeSwitchButton,
|
|
||||||
} from '@affine-test/kit/utils/editor';
|
} from '@affine-test/kit/utils/editor';
|
||||||
import {
|
import {
|
||||||
|
pressBackspace,
|
||||||
pressEnter,
|
pressEnter,
|
||||||
selectAllByKeyboard,
|
selectAllByKeyboard,
|
||||||
} from '@affine-test/kit/utils/keyboard';
|
} from '@affine-test/kit/utils/keyboard';
|
||||||
@@ -15,60 +13,135 @@ import { openHomePage } from '@affine-test/kit/utils/load-page';
|
|||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
createLinkedPage,
|
createLinkedPage,
|
||||||
getBlockSuiteEditorTitle,
|
|
||||||
type,
|
type,
|
||||||
waitForEditorLoad,
|
waitForEditorLoad,
|
||||||
waitForEmptyEditor,
|
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} 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 { expect, type Locator, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createHeadings,
|
||||||
|
createTitle,
|
||||||
|
getVerticalCenterFromLocator,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function getIndicators(container: Page | Locator) {
|
function getIndicators(container: Page | Locator) {
|
||||||
return container.locator('affine-outline-viewer .outline-viewer-indicator');
|
return container.locator('affine-outline-viewer .outline-viewer-indicator');
|
||||||
}
|
}
|
||||||
|
|
||||||
test('outline viewer is useable', async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
|
});
|
||||||
|
|
||||||
const title = getBlockSuiteEditorTitle(page);
|
test('should display indicators when non-empty headings exists', async ({
|
||||||
await title.click();
|
page,
|
||||||
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');
|
|
||||||
|
|
||||||
const indicators = getIndicators(page);
|
const indicators = getIndicators(page);
|
||||||
await expect(indicators).toHaveCount(3);
|
await createHeadings(page);
|
||||||
await expect(indicators.nth(0)).toBeVisible();
|
|
||||||
await expect(indicators.nth(1)).toBeVisible();
|
|
||||||
await expect(indicators.nth(2)).toBeVisible();
|
|
||||||
|
|
||||||
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 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 }) => {
|
test('outline viewer should hide in edgeless mode', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await createTitle(page);
|
||||||
await waitForEditorLoad(page);
|
await pressEnter(page);
|
||||||
await clickNewPageButton(page);
|
|
||||||
await waitForEditorLoad(page);
|
|
||||||
|
|
||||||
const title = getBlockSuiteEditorTitle(page);
|
await type(page, '# ');
|
||||||
await title.click();
|
await type(page, 'Heading 1');
|
||||||
await title.pressSequentially('Title');
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
await expect(title).toHaveText('Title');
|
|
||||||
await page.keyboard.type('# ');
|
|
||||||
await page.keyboard.type('Heading 1');
|
|
||||||
|
|
||||||
const indicators = getIndicators(page);
|
const indicators = getIndicators(page);
|
||||||
await expect(indicators).toHaveCount(2);
|
await expect(indicators).toHaveCount(2);
|
||||||
@@ -80,15 +153,34 @@ test('outline viewer should hide in edgeless mode', async ({ page }) => {
|
|||||||
await expect(indicators).toHaveCount(2);
|
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 ({
|
test('outline viewer should be useable in doc peek preview', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await openHomePage(page);
|
await pressEnter(page);
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickNewPageButton(page);
|
|
||||||
await waitForEmptyEditor(page);
|
|
||||||
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
await createLinkedPage(page, 'Test Page');
|
await createLinkedPage(page, 'Test Page');
|
||||||
|
|
||||||
await page.locator('affine-reference').hover();
|
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');
|
const title = peekView.locator('doc-title .inline-editor');
|
||||||
await title.click();
|
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++) {
|
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 outlineViewer = peekView.locator('affine-outline-viewer');
|
||||||
const outlineViewerBound = await outlineViewer.boundingBox();
|
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();
|
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);
|
|
||||||
});
|
|
||||||
34
tests/affine-local/e2e/blocksuite/outline/utils.ts
Normal file
34
tests/affine-local/e2e/blocksuite/outline/utils.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -25,9 +25,11 @@ export const withCtrlOrMeta = async (page: Page, fn: () => Promise<void>) => {
|
|||||||
await keyUpCtrlOrMeta(page);
|
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
|
// 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) {
|
export async function pressTab(page: Page) {
|
||||||
@@ -46,6 +48,12 @@ export async function pressShiftEnter(page: Page) {
|
|||||||
await page.keyboard.up('Shift');
|
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) {
|
export async function copyByKeyboard(page: Page) {
|
||||||
await keyDownCtrlOrMeta(page);
|
await keyDownCtrlOrMeta(page);
|
||||||
await page.keyboard.press('c', { delay: 50 });
|
await page.keyboard.press('c', { delay: 50 });
|
||||||
|
|||||||
@@ -50,3 +50,10 @@ export async function clickUserInfoCard(page: Page) {
|
|||||||
delay: 50,
|
delay: 50,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function closeSettingModal(page: Page) {
|
||||||
|
await page
|
||||||
|
.getByTestId('setting-modal')
|
||||||
|
.getByTestId('modal-close-button')
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user