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
@@ -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 {
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);
});
@@ -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;
}
+10 -2
View File
@@ -25,9 +25,11 @@ export const withCtrlOrMeta = async (page: Page, fn: () => Promise<void>) => {
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 });
+7
View File
@@ -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();
}