mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
991 lines
31 KiB
TypeScript
991 lines
31 KiB
TypeScript
import { expect } from '@playwright/test';
|
|
|
|
import { addNote, switchEditorMode } from './utils/actions/edgeless.js';
|
|
import {
|
|
pressArrowDown,
|
|
pressArrowLeft,
|
|
pressArrowRight,
|
|
pressArrowUp,
|
|
pressBackspace,
|
|
pressEnter,
|
|
pressEscape,
|
|
pressShiftEnter,
|
|
pressShiftTab,
|
|
pressTab,
|
|
redoByKeyboard,
|
|
SHORT_KEY,
|
|
type,
|
|
undoByKeyboard,
|
|
} from './utils/actions/keyboard.js';
|
|
import {
|
|
captureHistory,
|
|
enterPlaygroundRoom,
|
|
focusRichText,
|
|
getInlineSelectionText,
|
|
getPageSnapshot,
|
|
getSelectionRect,
|
|
initEmptyEdgelessState,
|
|
initEmptyParagraphState,
|
|
insertThreeLevelLists,
|
|
waitNextFrame,
|
|
} from './utils/actions/misc.js';
|
|
import {
|
|
assertAlmostEqual,
|
|
assertBlockCount,
|
|
assertExists,
|
|
assertRichTexts,
|
|
assertStoreMatchJSX,
|
|
} from './utils/asserts.js';
|
|
import { test } from './utils/playwright.js';
|
|
|
|
test.describe('slash menu should show and hide correctly', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
});
|
|
|
|
test("slash menu should show when user input '/'", async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
});
|
|
|
|
// Playwright dose not support IME
|
|
// https://github.com/microsoft/playwright/issues/5777
|
|
test.skip("slash menu should show when user input '、'", async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '、');
|
|
|
|
await expect(slashMenu).toBeVisible();
|
|
});
|
|
|
|
test('slash menu should hide after click away', async ({ page }) => {
|
|
const id = await initEmptyParagraphState(page);
|
|
const paragraphId = id.paragraphId;
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
// Click outside should close slash menu
|
|
await page.mouse.click(0, 50);
|
|
await expect(slashMenu).toBeHidden();
|
|
await assertStoreMatchJSX(
|
|
page,
|
|
`
|
|
<affine:paragraph
|
|
prop:collapsed={false}
|
|
prop:text="/"
|
|
prop:type="text"
|
|
/>`,
|
|
paragraphId
|
|
);
|
|
});
|
|
|
|
test('slash menu should hide after input whitespace', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
await type(page, ' ');
|
|
await expect(slashMenu).toBeHidden();
|
|
await assertRichTexts(page, ['/ ']);
|
|
await pressBackspace(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await type(page, 'head');
|
|
await expect(slashMenu).toBeVisible();
|
|
await type(page, ' ');
|
|
await expect(slashMenu).toBeHidden();
|
|
await pressBackspace(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
});
|
|
|
|
test('delete the slash symbol should close the slash menu', async ({
|
|
page,
|
|
}) => {
|
|
const id = await initEmptyParagraphState(page);
|
|
const paragraphId = id.paragraphId;
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await pressBackspace(page);
|
|
await expect(slashMenu).toBeHidden();
|
|
await assertStoreMatchJSX(
|
|
page,
|
|
`
|
|
<affine:paragraph
|
|
prop:collapsed={false}
|
|
prop:type="text"
|
|
/>`,
|
|
paragraphId
|
|
);
|
|
});
|
|
|
|
test('typing something that does not match should close the slash menu', async ({
|
|
page,
|
|
}) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await type(page, '_');
|
|
await expect(slashMenu).toBeHidden();
|
|
await assertRichTexts(page, ['/_']);
|
|
|
|
// And pressing backspace immediately should reappear the slash menu
|
|
await pressBackspace(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await type(page, '__');
|
|
await pressBackspace(page);
|
|
await expect(slashMenu).toBeHidden();
|
|
});
|
|
|
|
test('pressing the slash key again should close the old slash menu and open new one', async ({
|
|
page,
|
|
}) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashMenu).toHaveCount(1);
|
|
await assertRichTexts(page, ['//']);
|
|
});
|
|
|
|
test('should position slash menu correctly', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const box = await slashMenu.boundingBox();
|
|
if (!box) {
|
|
throw new Error("slashMenu doesn't exist");
|
|
}
|
|
const rect = await getSelectionRect(page);
|
|
const { x, y } = box;
|
|
assertAlmostEqual(x - rect.x, 0, 10);
|
|
assertAlmostEqual(y - rect.bottom, 5, 10);
|
|
});
|
|
|
|
test('should move up down with arrow key', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await pressArrowDown(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.nth(1)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Heading 1']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await pressArrowUp(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.first()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.first().locator('.text')).toHaveText(['Text']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await pressArrowUp(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.last()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.last().locator('.text')).toHaveText(['Delete']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await pressArrowDown(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.first()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.first().locator('.text')).toHaveText(['Text']);
|
|
await assertRichTexts(page, ['/']);
|
|
});
|
|
|
|
test('slash menu hover state', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await pressArrowDown(page);
|
|
await expect(slashItems.nth(1)).toHaveAttribute('hover', 'true');
|
|
|
|
await pressArrowUp(page);
|
|
await expect(slashItems.nth(1)).toHaveAttribute('hover', 'false');
|
|
await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true');
|
|
|
|
await pressArrowDown(page);
|
|
await pressArrowDown(page);
|
|
await expect(slashItems.nth(2)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(1)).toHaveAttribute('hover', 'false');
|
|
await expect(slashItems.nth(0)).toHaveAttribute('hover', 'false');
|
|
|
|
await slashItems.nth(0).hover();
|
|
await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(2)).toHaveAttribute('hover', 'false');
|
|
await expect(slashItems.nth(1)).toHaveAttribute('hover', 'false');
|
|
});
|
|
|
|
test('should open tooltip when hover on item', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
const tooltip = page.locator('.affine-tooltip');
|
|
|
|
await slashItems.nth(0).hover();
|
|
await expect(tooltip).toBeVisible();
|
|
await expect(tooltip.locator('.tooltip-caption')).toHaveText(['Text']);
|
|
await page.mouse.move(0, 0);
|
|
await expect(tooltip).toBeHidden();
|
|
|
|
await slashItems.nth(1).hover();
|
|
await expect(tooltip).toBeVisible();
|
|
await expect(tooltip.locator('.tooltip-caption')).toHaveText([
|
|
'Heading #1',
|
|
]);
|
|
await page.mouse.move(0, 0);
|
|
await expect(tooltip).toBeHidden();
|
|
|
|
await expect(slashItems.nth(4).locator('.text')).toHaveText([
|
|
'Other Headings',
|
|
]);
|
|
await slashItems.nth(4).hover();
|
|
await expect(tooltip).toBeHidden();
|
|
});
|
|
|
|
test('press tab should move up and down', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await pressTab(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.nth(1)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Heading 1']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await pressShiftTab(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.first()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.first().locator('.text')).toHaveText(['Text']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await pressShiftTab(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.last()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.last().locator('.text')).toHaveText(['Delete']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await pressTab(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.first()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.first().locator('.text')).toHaveText(['Text']);
|
|
await assertRichTexts(page, ['/']);
|
|
});
|
|
|
|
test('should move up down with ctrl/cmd+n and ctrl/cmd+p', async ({
|
|
page,
|
|
}) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await page.keyboard.press(`${SHORT_KEY}+n`);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.nth(1)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Heading 1']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await page.keyboard.press(`${SHORT_KEY}+p`);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.first()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.first().locator('.text')).toHaveText(['Text']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await page.keyboard.press(`${SHORT_KEY}+p`);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.last()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.last().locator('.text')).toHaveText(['Delete']);
|
|
await assertRichTexts(page, ['/']);
|
|
|
|
await page.keyboard.press(`${SHORT_KEY}+n`);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.first()).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.first().locator('.text')).toHaveText(['Text']);
|
|
await assertRichTexts(page, ['/']);
|
|
});
|
|
|
|
test('should open sub menu when hover on SubMenuItem', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, '/');
|
|
const slashMenu = page.locator('.slash-menu[data-testid=sub-menu-0]');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
const subMenu = page.locator('.slash-menu[data-testid=sub-menu-1]');
|
|
|
|
let rect = await slashItems.nth(4).boundingBox();
|
|
assertExists(rect);
|
|
await page.mouse.move(rect.x + 10, rect.y + 10);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.nth(4)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(4).locator('.text')).toHaveText([
|
|
'Other Headings',
|
|
]);
|
|
await expect(subMenu).toBeVisible();
|
|
|
|
rect = await slashItems.nth(3).boundingBox();
|
|
assertExists(rect);
|
|
await page.mouse.move(rect.x + 10, rect.y + 10);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems.nth(3)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(3).locator('.text')).toHaveText(['Heading 3']);
|
|
await expect(subMenu).toBeHidden();
|
|
});
|
|
|
|
test('should open and close menu when using left right arrow, Enter, Esc keys', async ({
|
|
page,
|
|
}) => {
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
const slashMenu = page.locator('.slash-menu[data-testid=sub-menu-0]');
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
await pressEscape(page);
|
|
await expect(slashMenu).toBeHidden();
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
await pressArrowLeft(page);
|
|
await expect(slashMenu).toBeHidden();
|
|
|
|
// Test sub menu case
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await type(page, '/');
|
|
await pressArrowDown(page, 4);
|
|
await expect(slashItems.nth(4)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(4).locator('.text')).toHaveText([
|
|
'Other Headings',
|
|
]);
|
|
|
|
const subMenu = page.locator('.slash-menu[data-testid=sub-menu-1]');
|
|
|
|
await pressArrowRight(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(subMenu).toBeVisible();
|
|
|
|
await pressArrowLeft(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(subMenu).toBeHidden();
|
|
|
|
await pressEnter(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(subMenu).toBeVisible();
|
|
|
|
await pressEscape(page);
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(subMenu).toBeHidden();
|
|
});
|
|
|
|
test('show close current all submenu when typing', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
const slashMenu = page.locator('.slash-menu[data-testid=sub-menu-0]');
|
|
const subMenu = page.locator('.slash-menu[data-testid=sub-menu-1]');
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
await pressArrowDown(page, 4);
|
|
await expect(slashItems.nth(4)).toHaveAttribute('hover', 'true');
|
|
await expect(slashItems.nth(4).locator('.text')).toHaveText([
|
|
'Other Headings',
|
|
]);
|
|
await pressEnter(page);
|
|
await expect(subMenu).toBeVisible();
|
|
|
|
await type(page, 'h');
|
|
await expect(subMenu).toBeHidden();
|
|
});
|
|
|
|
test('should allow only pressing modifier key', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await page.keyboard.press(SHORT_KEY);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await page.keyboard.press('Shift');
|
|
await expect(slashMenu).toBeVisible();
|
|
});
|
|
|
|
test('should allow other hotkey to passthrough', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
await type(page, 'hello');
|
|
await pressEnter(page);
|
|
await type(page, 'world');
|
|
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await page.keyboard.press(`${SHORT_KEY}+a`);
|
|
await expect(slashMenu).toBeHidden();
|
|
await assertRichTexts(page, ['hello', 'world/']);
|
|
|
|
const selected = await getInlineSelectionText(page);
|
|
expect(selected).toBe('world/');
|
|
});
|
|
|
|
test('can input search input after click menu', async ({ page }) => {
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await focusRichText(page);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const box = await slashMenu.boundingBox();
|
|
if (!box) {
|
|
throw new Error("slashMenu doesn't exist");
|
|
}
|
|
const { x, y } = box;
|
|
await page.mouse.click(x + 10, y + 10);
|
|
await expect(slashMenu).toBeVisible();
|
|
await type(page, 'a');
|
|
await assertRichTexts(page, ['/a']);
|
|
});
|
|
});
|
|
|
|
test.describe('slash menu should not be shown in ignored blocks', () => {
|
|
test('code block', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, '```');
|
|
await pressEnter(page);
|
|
await type(page, '/');
|
|
await expect(page.locator('.slash-menu')).toBeHidden();
|
|
});
|
|
});
|
|
|
|
test('should slash menu works with fast type', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, 'a/text', 0);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
});
|
|
|
|
test('should clean slash string after soft enter', async ({ page }) => {
|
|
test.info().annotations.push({
|
|
type: 'issue',
|
|
description: 'https://github.com/toeverything/blocksuite/issues/1126',
|
|
});
|
|
await enterPlaygroundRoom(page);
|
|
const { paragraphId } = await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
await type(page, 'hello');
|
|
await pressShiftEnter(page);
|
|
await waitNextFrame(page);
|
|
await type(page, '/copy');
|
|
await pressEnter(page);
|
|
|
|
await assertStoreMatchJSX(
|
|
page,
|
|
`
|
|
<affine:paragraph
|
|
prop:collapsed={false}
|
|
prop:text="hello\n"
|
|
prop:type="text"
|
|
/>`,
|
|
paragraphId
|
|
);
|
|
});
|
|
|
|
test.describe('slash search', () => {
|
|
test('should slash menu search and keyboard works', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
// search should active the first item
|
|
await type(page, 'co');
|
|
await expect(slashItems).toHaveCount(3);
|
|
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
|
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Code Block']);
|
|
await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true');
|
|
|
|
await type(page, 'p');
|
|
await expect(slashItems).toHaveCount(1);
|
|
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
|
|
|
// assert backspace works
|
|
await pressBackspace(page);
|
|
await expect(slashItems).toHaveCount(3);
|
|
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
|
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Code Block']);
|
|
await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true');
|
|
});
|
|
|
|
test('slash menu supports fuzzy search', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
await type(page, 'c');
|
|
await expect(slashItems).toHaveCount(8);
|
|
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
|
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Italic']);
|
|
await expect(slashItems.nth(2).locator('.text')).toHaveText(['New Doc']);
|
|
await expect(slashItems.nth(3).locator('.text')).toHaveText(['Duplicate']);
|
|
await expect(slashItems.nth(4).locator('.text')).toHaveText(['Code Block']);
|
|
await expect(slashItems.nth(5).locator('.text')).toHaveText(['Linked Doc']);
|
|
await expect(slashItems.nth(6).locator('.text')).toHaveText(['Attachment']);
|
|
await type(page, 'b');
|
|
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Code Block']);
|
|
});
|
|
|
|
test('slash menu supports alias search', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, '/');
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
await type(page, 'database');
|
|
await expect(slashItems).toHaveCount(2);
|
|
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Table View']);
|
|
await expect(slashItems.nth(1).locator('.text')).toHaveText([
|
|
'Kanban View',
|
|
]);
|
|
await type(page, 'v');
|
|
await expect(slashItems).toHaveCount(0);
|
|
});
|
|
});
|
|
|
|
test('should focus on code blocks created by the slash menu', async ({
|
|
page,
|
|
}) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
await type(page, '000');
|
|
|
|
await type(page, '/code');
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const codeBlock = page.getByTestId('Code Block');
|
|
await codeBlock.click();
|
|
await expect(slashMenu).toBeHidden();
|
|
|
|
await focusRichText(page); // FIXME: flaky selection asserter
|
|
await type(page, '111');
|
|
await assertRichTexts(page, ['000111']);
|
|
});
|
|
|
|
// Selection is not yet available in edgeless
|
|
test('slash menu should work in edgeless mode', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyEdgelessState(page);
|
|
|
|
await switchEditorMode(page);
|
|
|
|
await addNote(page, '/', 30, 40);
|
|
await assertRichTexts(page, ['', '/']);
|
|
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
});
|
|
|
|
test.describe('slash menu with date & time', () => {
|
|
test("should insert Today's time string", async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, '/');
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const todayBlock = page.getByTestId('Today');
|
|
await todayBlock.click();
|
|
await expect(slashMenu).toBeHidden();
|
|
|
|
const date = new Date();
|
|
const strTime = date.toISOString().split('T')[0];
|
|
|
|
await assertRichTexts(page, [strTime]);
|
|
});
|
|
|
|
test("should create Tomorrow's time string", async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, '/');
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const todayBlock = page.getByTestId('Tomorrow');
|
|
await todayBlock.click();
|
|
await expect(slashMenu).toBeHidden();
|
|
|
|
const date = new Date();
|
|
date.setDate(date.getDate() + 1);
|
|
const strTime = date.toISOString().split('T')[0];
|
|
|
|
await assertRichTexts(page, [strTime]);
|
|
});
|
|
|
|
test("should insert Yesterday's time string", async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, '/');
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const todayBlock = page.getByTestId('Yesterday');
|
|
await todayBlock.click();
|
|
await expect(slashMenu).toBeHidden();
|
|
|
|
const date = new Date();
|
|
date.setDate(date.getDate() - 1);
|
|
const strTime = date.toISOString().split('T')[0];
|
|
|
|
await assertRichTexts(page, [strTime]);
|
|
});
|
|
});
|
|
|
|
test.describe('slash menu with style', () => {
|
|
test('should style text line works', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
const { paragraphId } = await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, 'hello/');
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
const bold = page.getByTestId('Bold');
|
|
await bold.click();
|
|
await assertStoreMatchJSX(
|
|
page,
|
|
`
|
|
<affine:paragraph
|
|
prop:collapsed={false}
|
|
prop:text={
|
|
<>
|
|
<text
|
|
bold={true}
|
|
insert="hello"
|
|
/>
|
|
</>
|
|
}
|
|
prop:type="text"
|
|
/>`,
|
|
paragraphId
|
|
);
|
|
});
|
|
|
|
test('should style empty line works', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
const { paragraphId } = await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await type(page, '/');
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
await expect(slashMenu).toBeVisible();
|
|
const bold = page.getByTestId('Bold');
|
|
await bold.click();
|
|
await page.waitForTimeout(50);
|
|
await type(page, 'hello');
|
|
await assertStoreMatchJSX(
|
|
page,
|
|
`
|
|
<affine:paragraph
|
|
prop:collapsed={false}
|
|
prop:text={
|
|
<>
|
|
<text
|
|
bold={true}
|
|
insert="hello"
|
|
/>
|
|
</>
|
|
}
|
|
prop:type="text"
|
|
/>`,
|
|
paragraphId
|
|
);
|
|
});
|
|
});
|
|
|
|
test('should insert database', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await focusRichText(page);
|
|
|
|
await assertBlockCount(page, 'paragraph', 1);
|
|
await type(page, '/');
|
|
const tableBlock = page.getByTestId('Table View');
|
|
await tableBlock.click();
|
|
await assertBlockCount(page, 'paragraph', 0);
|
|
await assertBlockCount(page, 'database', 1);
|
|
|
|
const database = page.locator('affine-database');
|
|
await expect(database).toBeVisible();
|
|
const tagColumn = page.locator('.affine-database-column').nth(1);
|
|
expect(await tagColumn.innerText()).toBe('Status');
|
|
const defaultRows = page.locator('.affine-database-block-row');
|
|
expect(await defaultRows.count()).toBe(4);
|
|
});
|
|
|
|
test.describe('slash menu with customize menu', () => {
|
|
test('can remove specified menus', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await page.evaluate(async () => {
|
|
// https://github.com/lit/lit/blob/84df6ef8c73fffec92384891b4b031d7efc01a64/packages/lit-html/src/static.ts#L93
|
|
const fakeLiteral = (strings: TemplateStringsArray) =>
|
|
({
|
|
['_$litStatic$']: strings[0],
|
|
r: Symbol.for(''),
|
|
}) as const;
|
|
|
|
const editor = document.querySelector('affine-editor-container');
|
|
if (!editor) throw new Error("Can't find affine-editor-container");
|
|
|
|
const SlashMenuWidget = window.$blocksuite.blocks.AffineSlashMenuWidget;
|
|
class CustomSlashMenu extends SlashMenuWidget {
|
|
override config = {
|
|
...SlashMenuWidget.DEFAULT_CONFIG,
|
|
items: [
|
|
{ groupName: 'custom-group' },
|
|
...SlashMenuWidget.DEFAULT_CONFIG.items
|
|
.filter(item => 'action' in item)
|
|
.slice(0, 5),
|
|
],
|
|
};
|
|
}
|
|
// Fix `Illegal constructor` error
|
|
// see https://stackoverflow.com/questions/41521812/illegal-constructor-with-ecmascript-6
|
|
customElements.define('affine-custom-slash-menu', CustomSlashMenu);
|
|
|
|
const pageSpecs = window.$blocksuite.blocks.PageEditorBlockSpecs;
|
|
editor.pageSpecs = [
|
|
...pageSpecs,
|
|
{
|
|
setup: di => {
|
|
di.override(
|
|
window.$blocksuite.identifiers.WidgetViewMapIdentifier(
|
|
'affine:page'
|
|
),
|
|
// @ts-ignore
|
|
() => ({
|
|
'affine-slash-menu-widget': fakeLiteral`affine-custom-slash-menu`,
|
|
})
|
|
);
|
|
},
|
|
},
|
|
];
|
|
await editor.updateComplete;
|
|
});
|
|
|
|
await focusRichText(page);
|
|
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems).toHaveCount(5);
|
|
});
|
|
|
|
test('can add some menus', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await page.evaluate(async () => {
|
|
// https://github.com/lit/lit/blob/84df6ef8c73fffec92384891b4b031d7efc01a64/packages/lit-html/src/static.ts#L93
|
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
const fakeLiteral = (strings: TemplateStringsArray) =>
|
|
({
|
|
['_$litStatic$']: strings[0],
|
|
r: Symbol.for(''),
|
|
}) as const;
|
|
|
|
const editor = document.querySelector('affine-editor-container');
|
|
if (!editor) throw new Error("Can't find affine-editor-container");
|
|
const SlashMenuWidget = window.$blocksuite.blocks.AffineSlashMenuWidget;
|
|
|
|
class CustomSlashMenu extends SlashMenuWidget {
|
|
config = {
|
|
...SlashMenuWidget.DEFAULT_CONFIG,
|
|
items: [
|
|
{ groupName: 'Custom Menu' },
|
|
{
|
|
name: 'Custom Menu Item',
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
icon: '' as any,
|
|
action: () => {
|
|
// do nothing
|
|
},
|
|
},
|
|
{
|
|
name: 'Custom Menu Item',
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
icon: '' as any,
|
|
action: () => {
|
|
// do nothing
|
|
},
|
|
showWhen: () => false,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
// Fix `Illegal constructor` error
|
|
// see https://stackoverflow.com/questions/41521812/illegal-constructor-with-ecmascript-6
|
|
customElements.define('affine-custom-slash-menu', CustomSlashMenu);
|
|
|
|
const pageSpecs = window.$blocksuite.blocks.PageEditorBlockSpecs;
|
|
editor.pageSpecs = [
|
|
...pageSpecs,
|
|
{
|
|
setup: di =>
|
|
di.override(
|
|
window.$blocksuite.identifiers.WidgetViewMapIdentifier(
|
|
'affine:page'
|
|
),
|
|
// @ts-ignore
|
|
() => ({
|
|
'affine-slash-menu-widget': fakeLiteral`affine-custom-slash-menu`,
|
|
})
|
|
),
|
|
},
|
|
];
|
|
await editor.updateComplete;
|
|
});
|
|
|
|
await focusRichText(page);
|
|
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
await expect(slashItems).toHaveCount(1);
|
|
});
|
|
});
|
|
|
|
test('move block up and down by slash menu', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
|
|
await focusRichText(page);
|
|
await type(page, 'hello');
|
|
await pressEnter(page);
|
|
await type(page, 'world');
|
|
await assertRichTexts(page, ['hello', 'world']);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const moveUp = page.getByTestId('Move Up');
|
|
await moveUp.click();
|
|
await assertRichTexts(page, ['world', 'hello']);
|
|
await type(page, '/');
|
|
await expect(slashMenu).toBeVisible();
|
|
|
|
const moveDown = page.getByTestId('Move Down');
|
|
await moveDown.click();
|
|
await assertRichTexts(page, ['hello', 'world']);
|
|
});
|
|
|
|
test('delete block by slash menu should remove children', async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyParagraphState(page);
|
|
await insertThreeLevelLists(page);
|
|
const slashMenu = page.locator(`.slash-menu`);
|
|
const slashItems = slashMenu.locator('icon-button');
|
|
|
|
await captureHistory(page);
|
|
await focusRichText(page, 1);
|
|
await waitNextFrame(page);
|
|
await type(page, '/');
|
|
|
|
await expect(slashMenu).toBeVisible();
|
|
await type(page, 'remove');
|
|
await expect(slashItems).toHaveCount(1);
|
|
await pressEnter(page);
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}.json`
|
|
);
|
|
|
|
await undoByKeyboard(page);
|
|
await assertRichTexts(page, ['123', '456', '789']);
|
|
await redoByKeyboard(page);
|
|
await assertRichTexts(page, ['123']);
|
|
});
|