chore(editor): fix imports in legacy tests (#10300)

This commit is contained in:
Saul-Mirone
2025-02-20 03:30:05 +00:00
parent e0b2b2b33c
commit c3fc0a0d88
119 changed files with 96 additions and 90 deletions

View File

@@ -0,0 +1,455 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { sleep } from '@blocksuite/global/utils';
import { expect, type Page } from '@playwright/test';
import { dragBlockToPoint, popImageMoreMenu } from './utils/actions/drag.js';
import { switchEditorMode } from './utils/actions/edgeless.js';
import {
pressArrowDown,
pressArrowUp,
pressBackspace,
pressEnter,
pressEscape,
pressShiftTab,
pressTab,
redoByKeyboard,
SHORT_KEY,
type,
undoByKeyboard,
} from './utils/actions/keyboard.js';
import {
captureHistory,
enterPlaygroundRoom,
focusRichText,
getPageSnapshot,
initEmptyEdgelessState,
initEmptyParagraphState,
resetHistory,
waitNextFrame,
} from './utils/actions/misc.js';
import {
assertBlockChildrenIds,
assertBlockCount,
assertBlockFlavour,
assertBlockSelections,
assertKeyboardWorkInInput,
assertParentBlockFlavour,
assertRichImage,
assertRichTextInlineRange,
} from './utils/asserts.js';
import { test } from './utils/playwright.js';
const FILE_NAME = 'test-card-1.png';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FILE_PATH = path.resolve(
__dirname,
`../../playground/public/${FILE_NAME}`
);
function getAttachment(page: Page) {
const attachment = page.locator('affine-attachment');
const loading = attachment.locator('.affine-attachment-card.loading');
const toolbar = page.locator('.affine-attachment-toolbar');
const switchViewButton = toolbar.getByRole('button', { name: 'Switch view' });
const renameBtn = toolbar.getByRole('button', { name: 'Rename' });
const renameInput = page.locator('.affine-attachment-rename-container input');
const insertAttachment = async () => {
await page.evaluate(() => {
// Force fallback to input[type=file] in tests
// See https://github.com/microsoft/playwright/issues/8850
window.showOpenFilePicker = undefined;
});
const slashMenu = page.locator(`.slash-menu`);
await waitNextFrame(page);
await type(page, '/');
await resetHistory(page);
await expect(slashMenu).toBeVisible();
await type(page, 'file', 100);
await expect(slashMenu).toBeVisible();
const fileChooser = page.waitForEvent('filechooser');
await pressEnter(page);
await sleep(100);
await (await fileChooser).setFiles(FILE_PATH);
// Try to break the undo redo test
await captureHistory(page);
await expect(attachment).toBeVisible();
};
const getName = () =>
attachment.locator('.affine-attachment-content-title-text').innerText();
return {
// locators
attachment,
toolbar,
switchViewButton,
renameBtn,
renameInput,
// actions
insertAttachment,
/**
* Wait for the attachment upload to finish
*/
waitLoading: () => loading.waitFor({ state: 'hidden' }),
getName,
getSize: () =>
attachment.locator('.affine-attachment-content-info').innerText(),
turnToEmbed: async () => {
await expect(switchViewButton).toBeVisible();
await switchViewButton.click();
await page.getByRole('button', { name: 'Embed view' }).click();
await assertRichImage(page, 1);
},
rename: async (newName: string) => {
await attachment.hover();
await expect(toolbar).toBeVisible();
await renameBtn.click();
await page.keyboard.press(`${SHORT_KEY}+a`, { delay: 50 });
await pressBackspace(page);
await type(page, newName);
await pressEnter(page);
expect(await getName()).toContain(newName);
},
// external
turnImageToCard: async () => {
const { turnIntoCardButton } = await popImageMoreMenu(page);
await turnIntoCardButton.click();
await expect(attachment).toBeVisible();
},
};
}
test('can insert attachment from slash menu', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { insertAttachment, waitLoading, getName, getSize } =
getAttachment(page);
await focusRichText(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
expect(await getName()).toBe(FILE_NAME);
expect(await getSize()).toBe('45.8 kB');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
test('should undo/redo works for attachment', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { insertAttachment, waitLoading } = getAttachment(page);
await focusRichText(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
await undoByKeyboard(page);
await waitNextFrame(page);
// The loading/error state should not be restored after undo
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
await redoByKeyboard(page);
await waitNextFrame(page);
// The loading/error state should not be restored after undo
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_3.json`
);
});
test('should rename attachment works', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/4534',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const {
attachment,
renameBtn,
renameInput,
insertAttachment,
waitLoading,
getName,
rename,
} = getAttachment(page);
await focusRichText(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
expect(await getName()).toBe(FILE_NAME);
await attachment.hover();
await expect(renameBtn).toBeVisible();
await renameBtn.click();
await assertKeyboardWorkInInput(page, renameInput);
await pressEscape(page);
await expect(renameInput).not.toBeVisible();
await rename('new-name');
expect(await getName()).toBe('new-name.png');
await rename('');
expect(await getName()).toBe('.png');
await rename('abc');
expect(await getName()).toBe('abc');
});
test('should turn attachment to image works', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { insertAttachment, waitLoading, turnToEmbed, turnImageToCard } =
getAttachment(page);
await focusRichText(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
await turnToEmbed();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
await turnImageToCard();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
});
test('should attachment can be deleted', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { attachment, insertAttachment, waitLoading } = getAttachment(page);
await focusRichText(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
await attachment.click();
await pressBackspace(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
test(`support dragging attachment block directly`, async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { insertAttachment, waitLoading, getName, getSize } =
getAttachment(page);
await focusRichText(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
expect(await getName()).toBe(FILE_NAME);
expect(await getSize()).toBe('45.8 kB');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
const attachmentBlock = page.locator('affine-attachment');
const rect = await attachmentBlock.boundingBox();
if (!rect) {
throw new Error('image not found');
}
// add new paragraph blocks
await page.mouse.click(rect.x + 20, rect.y + rect.height + 20);
await focusRichText(page);
await type(page, '111');
await page.waitForTimeout(200);
await pressEnter(page);
await type(page, '222');
await page.waitForTimeout(200);
await pressEnter(page);
await type(page, '333');
await page.waitForTimeout(200);
await page.waitForTimeout(200);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
// drag bookmark block
await page.mouse.move(rect.x + 20, rect.y + 20);
await page.mouse.down();
await page.mouse.move(rect.x + 40, rect.y + rect.height + 80, { steps: 20 });
await page.mouse.up();
const rects = page.locator('affine-block-selection').locator('visible=true');
await expect(rects).toHaveCount(1);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_3.json`
);
});
test('press backspace after bookmark block can select bookmark block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { insertAttachment, waitLoading } = getAttachment(page);
await focusRichText(page);
await pressEnter(page);
await pressArrowUp(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
await focusRichText(page);
await assertBlockCount(page, 'paragraph', 1);
await assertRichTextInlineRange(page, 0, 0);
await pressBackspace(page);
await assertBlockSelections(page, ['4']);
await assertBlockCount(page, 'paragraph', 0);
});
test('cancel file picker with input element resolves', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { attachment } = getAttachment(page);
await focusRichText(page);
await pressEnter(page);
await pressArrowUp(page);
await page.evaluate(() => {
// Force fallback to input[type=file]
window.showOpenFilePicker = undefined;
});
const slashMenu = page.locator(`.slash-menu`);
await waitNextFrame(page);
await type(page, '/file', 100);
await expect(slashMenu).toBeVisible();
const fileChooser = page.waitForEvent('filechooser');
await pressEnter(page);
const inputFile = page.locator("input[type='file']");
await expect(inputFile).toHaveCount(1);
// This does not trigger `cancel` event and,
// therefore, the test isn't representative.
// Waiting for https://github.com/microsoft/playwright/issues/27524
await (await fileChooser).setFiles([]);
await expect(attachment).toHaveCount(0);
await expect(inputFile).toHaveCount(0);
});
test('indent attachment block to paragraph', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { insertAttachment, waitLoading } = getAttachment(page);
await focusRichText(page);
await pressEnter(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
await assertBlockChildrenIds(page, '1', ['2', '4']);
await assertBlockFlavour(page, '1', 'affine:note');
await assertBlockFlavour(page, '2', 'affine:paragraph');
await assertBlockFlavour(page, '4', 'affine:attachment');
await focusRichText(page);
await pressArrowDown(page);
await assertBlockSelections(page, ['4']);
await pressTab(page);
await assertBlockChildrenIds(page, '1', ['2']);
await assertBlockChildrenIds(page, '2', ['4']);
await pressShiftTab(page);
await assertBlockChildrenIds(page, '1', ['2', '4']);
});
test('indent attachment block to list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const { insertAttachment, waitLoading } = getAttachment(page);
await focusRichText(page);
await type(page, '- a');
await pressEnter(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
await assertBlockChildrenIds(page, '1', ['3', '5']);
await assertBlockFlavour(page, '1', 'affine:note');
await assertBlockFlavour(page, '3', 'affine:list');
await assertBlockFlavour(page, '5', 'affine:attachment');
await focusRichText(page);
await pressArrowDown(page);
await assertBlockSelections(page, ['5']);
await pressTab(page);
await assertBlockChildrenIds(page, '1', ['3']);
await assertBlockChildrenIds(page, '3', ['5']);
await pressShiftTab(page);
await assertBlockChildrenIds(page, '1', ['3', '5']);
});
test('attachment can be dragged from note to surface top level block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
const { insertAttachment, waitLoading } = getAttachment(page);
await focusRichText(page);
await insertAttachment();
// Wait for the attachment to be uploaded
await waitLoading();
await switchEditorMode(page);
await page.mouse.dblclick(450, 450);
await dragBlockToPoint(page, '4', { x: 200, y: 200 });
await waitNextFrame(page);
await assertParentBlockFlavour(page, '4', 'affine:surface');
});

View File

@@ -0,0 +1,589 @@
import type { DeltaInsert } from '@blocksuite/inline';
import { expect } from '@playwright/test';
import {
addNoteByClick,
captureHistory,
click,
disconnectByClick,
enterPlaygroundRoom,
focusRichText,
focusTitle,
getCurrentEditorTheme,
getCurrentHTMLTheme,
getPageSnapshot,
initEmptyEdgelessState,
initEmptyParagraphState,
pressArrowLeft,
pressArrowRight,
pressBackspace,
pressEnter,
pressForwardDelete,
pressForwardDeleteWord,
pressShiftEnter,
redoByClick,
redoByKeyboard,
setSelection,
switchEditorMode,
toggleDarkMode,
type,
undoByClick,
undoByKeyboard,
waitDefaultPageLoaded,
waitNextFrame,
} from './utils/actions/index.js';
import {
assertBlockChildrenIds,
assertEmpty,
assertRichTextInlineDeltas,
assertRichTexts,
assertText,
assertTitle,
} from './utils/asserts.js';
import { scoped, test } from './utils/playwright.js';
import { getFormatBar } from './utils/query.js';
const BASIC_DEFAULT_SNAPSHOT = 'basic test default';
test(scoped`basic input`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await test.expect(page).toHaveTitle(/BlockSuite/);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${BASIC_DEFAULT_SNAPSHOT}.json`
);
await assertText(page, 'hello');
});
test(scoped`basic init with external text`, async ({ page }) => {
await enterPlaygroundRoom(page);
await page.evaluate(() => {
const { doc } = window;
const rootId = doc.addBlock('affine:page', {
title: new window.$blocksuite.store.Text('hello'),
});
const note = doc.addBlock('affine:note', {}, rootId);
const text = new window.$blocksuite.store.Text('world');
doc.addBlock('affine:paragraph', { text }, note);
const delta = [
{ insert: 'foo ' },
{ insert: 'bar', attributes: { bold: true } },
];
doc.addBlock(
'affine:paragraph',
{
text: new window.$blocksuite.store.Text(delta as DeltaInsert[]),
},
note
);
});
await assertTitle(page, 'hello');
await assertRichTexts(page, ['world', 'foo bar']);
await focusRichText(page);
});
test(scoped`basic multi user state`, async ({ context, page: pageA }) => {
const room = await enterPlaygroundRoom(pageA);
await initEmptyParagraphState(pageA);
await waitNextFrame(pageA);
await waitDefaultPageLoaded(pageA);
await focusTitle(pageA);
await type(pageA, 'hello');
const pageB = await context.newPage();
await enterPlaygroundRoom(pageB, {
room,
noInit: true,
});
await waitDefaultPageLoaded(pageB);
await focusTitle(pageB);
await assertTitle(pageB, 'hello');
await type(pageB, ' world');
await assertTitle(pageA, 'hello world');
});
test(
scoped`A open and edit, then joins B`,
async ({ context, page: pageA }) => {
const room = await enterPlaygroundRoom(pageA);
await initEmptyParagraphState(pageA);
await waitNextFrame(pageA);
await focusRichText(pageA);
await type(pageA, 'hello');
const pageB = await context.newPage();
await enterPlaygroundRoom(pageB, {
flags: {},
room,
noInit: true,
});
// wait until pageB content updated
await assertText(pageB, 'hello');
await Promise.all([
assertText(pageA, 'hello'),
expect(await getPageSnapshot(pageA, true)).toMatchSnapshot(
`${BASIC_DEFAULT_SNAPSHOT}.json`
),
expect(await getPageSnapshot(pageB, true)).toMatchSnapshot(
`${BASIC_DEFAULT_SNAPSHOT}.json`
),
assertBlockChildrenIds(pageA, '0', ['1']),
assertBlockChildrenIds(pageB, '0', ['1']),
]);
}
);
test(scoped`A first open, B first edit`, async ({ context, page: pageA }) => {
const room = await enterPlaygroundRoom(pageA);
await initEmptyParagraphState(pageA);
await waitNextFrame(pageA);
await focusRichText(pageA);
const pageB = await context.newPage();
await enterPlaygroundRoom(pageB, {
room,
noInit: true,
});
await pageB.waitForTimeout(500);
await focusRichText(pageB);
await waitNextFrame(pageA);
await waitNextFrame(pageB);
await type(pageB, 'hello');
await pageA.waitForTimeout(500);
// wait until pageA content updated
await assertText(pageA, 'hello');
await assertText(pageB, 'hello');
await Promise.all([
expect(await getPageSnapshot(pageA, true)).toMatchSnapshot(
`${BASIC_DEFAULT_SNAPSHOT}.json`
),
expect(await getPageSnapshot(pageB, true)).toMatchSnapshot(
`${BASIC_DEFAULT_SNAPSHOT}.json`
),
]);
});
test(
scoped`does not sync when disconnected`,
async ({ browser, page: pageA }) => {
test.fail();
const room = await enterPlaygroundRoom(pageA);
const pageB = await browser.newPage();
await enterPlaygroundRoom(pageB, { flags: {}, room });
await disconnectByClick(pageA);
await disconnectByClick(pageB);
// click together, both init with default id should lead to conflicts
await initEmptyParagraphState(pageA);
await initEmptyParagraphState(pageB);
await waitNextFrame(pageA);
await focusRichText(pageA);
await waitNextFrame(pageB);
await focusRichText(pageB);
await waitNextFrame(pageA);
await type(pageA, '');
await waitNextFrame(pageB);
await type(pageB, '');
await waitNextFrame(pageA);
await type(pageA, 'hello');
await waitNextFrame(pageB);
await assertText(pageB, 'hello');
await assertText(pageA, 'hello'); // actually '\n'
}
);
test(scoped`basic paired undo/redo`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await assertText(page, 'hello');
await undoByClick(page);
await assertEmpty(page);
await redoByClick(page);
await assertText(page, 'hello');
await undoByClick(page);
await assertEmpty(page);
await redoByClick(page);
await assertText(page, 'hello');
});
test(scoped`undo/redo with keyboard`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await assertText(page, 'hello');
await undoByKeyboard(page);
await assertEmpty(page);
await redoByClick(page);
await assertText(page, 'hello');
});
test(scoped`undo after adding block twice`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await pressEnter(page);
await type(page, 'world');
await undoByKeyboard(page);
await assertRichTexts(page, ['hello']);
await redoByKeyboard(page);
await assertRichTexts(page, ['hello', 'world']);
});
test(scoped`undo/redo twice after adding block twice`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await pressEnter(page);
await type(page, 'world');
await assertRichTexts(page, ['hello', 'world']);
await undoByKeyboard(page);
await assertRichTexts(page, ['hello']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await redoByClick(page);
await assertRichTexts(page, ['hello']);
await redoByKeyboard(page);
await assertRichTexts(page, ['hello', 'world']);
});
test(scoped`should undo/redo works on title`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await waitNextFrame(page);
await focusTitle(page);
await type(page, 'title');
await focusRichText(page);
await type(page, 'hello world');
await assertTitle(page, 'title');
await assertRichTexts(page, ['hello world']);
await captureHistory(page);
await pressBackspace(page, 5);
await captureHistory(page);
await focusTitle(page);
await type(page, ' something');
await assertTitle(page, 'title something');
await assertRichTexts(page, ['hello ']);
await focusRichText(page);
await undoByKeyboard(page);
await assertTitle(page, 'title');
await assertRichTexts(page, ['hello ']);
await undoByKeyboard(page);
await assertTitle(page, 'title');
await assertRichTexts(page, ['hello world']);
await redoByKeyboard(page);
await assertTitle(page, 'title');
await assertRichTexts(page, ['hello ']);
await redoByKeyboard(page);
await assertTitle(page, 'title something');
await assertRichTexts(page, ['hello ']);
});
test(scoped`undo multi notes`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await addNoteByClick(page);
await assertRichTexts(page, ['', '']);
await undoByClick(page);
await assertRichTexts(page, ['']);
await redoByClick(page);
await assertRichTexts(page, ['', '']);
});
test(scoped`change theme`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
const currentTheme = await getCurrentHTMLTheme(page);
await toggleDarkMode(page);
const expectNextTheme = currentTheme === 'light' ? 'dark' : 'light';
const nextHTMLTheme = await getCurrentHTMLTheme(page);
expect(nextHTMLTheme).toBe(expectNextTheme);
const nextEditorTheme = await getCurrentEditorTheme(page);
expect(nextEditorTheme).toBe(expectNextTheme);
});
test(
scoped`should be able to delete an emoji completely by pressing backspace once`,
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2138',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '🌷🙅‍♂️🏳️‍🌈');
await pressBackspace(page);
await pressBackspace(page);
await pressBackspace(page);
await assertText(page, '');
}
);
test(scoped`delete emoji in the middle of the text`, async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2138',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '1🌷1🙅1🏳🌈1👨👩👧👦1');
await pressArrowLeft(page, 1);
await pressBackspace(page);
await pressArrowLeft(page, 1);
await pressBackspace(page);
await pressArrowLeft(page, 1);
await pressBackspace(page);
await pressArrowLeft(page, 1);
await pressBackspace(page);
await assertText(page, '11111');
});
test(scoped`delete emoji forward`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '1🌷1🙅1🏳🌈1👨👩👧👦1');
await pressArrowLeft(page, 8);
await pressForwardDelete(page);
await pressArrowRight(page, 1);
await pressForwardDelete(page);
await pressArrowRight(page, 1);
await pressForwardDelete(page);
await pressArrowRight(page, 1);
await pressForwardDelete(page);
await assertText(page, '11111');
});
test(
scoped`ZERO_WIDTH_SPACE should be counted by one cursor position`,
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await pressShiftEnter(page);
await type(page, 'asdfg');
await pressEnter(page);
await undoByKeyboard(page);
await page.waitForTimeout(300);
await pressBackspace(page);
await assertRichTexts(page, ['\nasdf']);
}
);
test('when no note block, click editing area auto add a new note block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await page.locator('affine-edgeless-note').click({ force: true });
await pressBackspace(page);
await switchEditorMode(page);
const edgelessNote = await page.evaluate(() => {
return document.querySelector('affine-edgeless-note');
});
expect(edgelessNote).toBeNull();
await click(page, { x: 200, y: 280 });
const pageNote = await page.evaluate(() => {
return document.querySelector('affine-note');
});
expect(pageNote).not.toBeNull();
});
test(scoped`automatic identify url text`, async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'abc https://google.com ');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test('ctrl+delete to delete one word forward', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa bbb ccc');
await pressArrowLeft(page, 8);
await pressForwardDeleteWord(page);
await assertText(page, 'aaa ccc');
});
test('extended inline format', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaabbbaaa');
const { boldBtn, italicBtn, underlineBtn, strikeBtn, codeBtn } =
getFormatBar(page);
await setSelection(page, 0, 3, 0, 6);
await boldBtn.click();
await italicBtn.click();
await underlineBtn.click();
await strikeBtn.click();
await codeBtn.click();
await assertRichTextInlineDeltas(page, [
{
insert: 'aaa',
},
{
insert: 'bbb',
attributes: {
bold: true,
italic: true,
underline: true,
strike: true,
code: true,
},
},
{
insert: 'aaa',
},
]);
// aaa|bbbccc
await setSelection(page, 2, 3, 2, 3);
await captureHistory(page);
await type(page, 'c');
await assertRichTextInlineDeltas(page, [
{
insert: 'aaac',
},
{
insert: 'bbb',
attributes: {
bold: true,
italic: true,
underline: true,
strike: true,
code: true,
},
},
{
insert: 'aaa',
},
]);
await undoByKeyboard(page);
// aaab|bbccc
await setSelection(page, 2, 4, 2, 4);
await type(page, 'c');
await assertRichTextInlineDeltas(page, [
{
insert: 'aaa',
},
{
insert: 'bcbb',
attributes: {
bold: true,
italic: true,
underline: true,
strike: true,
code: true,
},
},
{
insert: 'aaa',
},
]);
await undoByKeyboard(page);
// aaab|b|bccc
await setSelection(page, 2, 4, 2, 5);
await type(page, 'c');
await assertRichTextInlineDeltas(page, [
{
insert: 'aaa',
},
{
insert: 'bcb',
attributes: {
bold: true,
italic: true,
underline: true,
strike: true,
code: true,
},
},
{
insert: 'aaa',
},
]);
await undoByKeyboard(page);
// aaabbb|ccc
await setSelection(page, 2, 6, 2, 6);
await type(page, 'c');
await assertRichTextInlineDeltas(page, [
{
insert: 'aaa',
},
{
insert: 'bbb',
attributes: {
bold: true,
italic: true,
underline: true,
strike: true,
code: true,
},
},
{
insert: 'c',
attributes: {
bold: true,
italic: true,
underline: true,
strike: true,
},
},
{
insert: 'aaa',
},
]);
});

View File

@@ -0,0 +1,461 @@
import './utils/declare-test-window.js';
import type { BlockSnapshot } from '@blocksuite/store';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import {
activeNoteInEdgeless,
copyByKeyboard,
dragBlockToPoint,
enterPlaygroundRoom,
expectConsoleMessage,
focusRichText,
getPageSnapshot,
initEmptyEdgelessState,
initEmptyParagraphState,
pasteByKeyboard,
pressArrowDown,
pressArrowRight,
pressArrowUp,
pressBackspace,
pressEnter,
pressShiftTab,
pressTab,
selectAllByKeyboard,
setInlineRangeInSelectedRichText,
SHORT_KEY,
switchEditorMode,
type,
waitForInlineEditorStateUpdated,
waitNextFrame,
} from './utils/actions/index.js';
import {
assertAlmostEqual,
assertBlockChildrenIds,
assertBlockCount,
assertBlockFlavour,
assertBlockSelections,
assertExists,
assertParentBlockFlavour,
assertRichTextInlineRange,
} from './utils/asserts.js';
import { ignoreSnapshotId } from './utils/ignore.js';
import { scoped, test } from './utils/playwright.js';
import { getEmbedCardToolbar } from './utils/query.js';
const LOCAL_HOST_URL = 'http://localhost';
const YOUTUBE_URL = 'https://www.youtube.com/watch?v=fakeid';
const FIGMA_URL = 'https://www.figma.com/design/JuXs6uOAICwf4I4tps0xKZ123';
test.beforeEach(async ({ page }) => {
await page.route(
'https://affine-worker.toeverything.workers.dev/api/worker/link-preview',
async route => {
await route.fulfill({
json: {},
});
}
);
});
const createBookmarkBlockBySlashMenu = async (
page: Page,
url = LOCAL_HOST_URL
) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await page.waitForTimeout(100);
await type(page, '/link', 100);
await pressEnter(page);
await page.waitForTimeout(100);
await type(page, url);
await pressEnter(page);
};
test(scoped`create bookmark by slash menu`, async ({ page }, testInfo) => {
await createBookmarkBlockBySlashMenu(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test(scoped`covert bookmark block to link text`, async ({ page }, testInfo) => {
await createBookmarkBlockBySlashMenu(page);
const bookmark = page.locator('affine-bookmark');
await bookmark.click();
await page.waitForTimeout(100);
await page.getByRole('button', { name: 'Switch view' }).click();
await page.getByRole('button', { name: 'Inline view' }).click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test(
scoped`copy url to create bookmark in page mode`,
async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, LOCAL_HOST_URL);
await setInlineRangeInSelectedRichText(page, 0, LOCAL_HOST_URL.length);
await copyByKeyboard(page);
await focusRichText(page);
await type(page, '/link');
await pressEnter(page);
await page.keyboard.press(`${SHORT_KEY}+v`);
await pressEnter(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
}
);
test(
scoped`copy url to create bookmark in edgeless mode`,
async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
const ids = await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, LOCAL_HOST_URL);
await switchEditorMode(page);
await activeNoteInEdgeless(page, ids.noteId);
await waitForInlineEditorStateUpdated(page);
await selectAllByKeyboard(page);
await copyByKeyboard(page);
await pressArrowRight(page);
await waitNextFrame(page);
await type(page, '/link', 100);
await pressEnter(page);
await page.waitForTimeout(100);
await waitNextFrame(page);
await page.keyboard.press(`${SHORT_KEY}+v`);
await pressEnter(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
}
);
test.fixme(
scoped`support dragging bookmark block directly`,
async ({ page }, testInfo) => {
await createBookmarkBlockBySlashMenu(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
const bookmark = page.locator('affine-bookmark');
const rect = await bookmark.boundingBox();
if (!rect) {
throw new Error('image not found');
}
// add new paragraph blocks
await page.mouse.click(rect.x + 20, rect.y + rect.height + 20);
await focusRichText(page);
await type(page, '111');
await page.waitForTimeout(200);
await pressEnter(page);
await type(page, '222');
await page.waitForTimeout(200);
await pressEnter(page);
await type(page, '333');
await page.waitForTimeout(200);
await page.waitForTimeout(200);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after_add_paragraph.json`
);
// drag bookmark block
await page.mouse.move(rect.x + 20, rect.y + 20);
await page.mouse.down();
await page.waitForTimeout(200);
await page.mouse.move(rect.x + 40, rect.y + rect.height + 80, {
steps: 5,
});
await page.waitForTimeout(200);
await page.mouse.up();
await page.waitForTimeout(200);
const rects = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(rects).toHaveCount(1);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after_drag.json`
);
}
);
test('press backspace after bookmark block can select bookmark block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await pressEnter(page);
await pressArrowUp(page);
await type(page, '/link');
await pressEnter(page);
await page.waitForTimeout(100);
await type(page, LOCAL_HOST_URL);
await pressEnter(page);
await focusRichText(page);
await assertBlockCount(page, 'paragraph', 1);
await assertRichTextInlineRange(page, 0, 0);
await pressBackspace(page);
await assertBlockSelections(page, ['4']);
await assertBlockCount(page, 'paragraph', 0);
});
test.describe('embed card toolbar', () => {
async function showEmbedCardToolbar(page: Page) {
await createBookmarkBlockBySlashMenu(page);
const bookmark = page.locator('affine-bookmark');
await bookmark.click();
await page.waitForTimeout(100);
const { embedCardToolbar } = getEmbedCardToolbar(page);
await expect(embedCardToolbar).toBeVisible();
}
test('show toolbar when bookmark selected', async ({ page }) => {
await showEmbedCardToolbar(page);
});
test('copy bookmark url by copy button', async ({ page }, testInfo) => {
await showEmbedCardToolbar(page);
const { copyButton } = getEmbedCardToolbar(page);
await copyButton.click();
await page.mouse.click(600, 600);
await waitNextFrame(page);
await pasteByKeyboard(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test('change card style', async ({ page }) => {
await showEmbedCardToolbar(page);
const bookmark = page.locator('affine-bookmark');
const { openCardStyleMenu } = getEmbedCardToolbar(page);
await openCardStyleMenu();
const { cardStyleHorizontalButton, cardStyleListButton } =
getEmbedCardToolbar(page);
await cardStyleListButton.click();
await waitNextFrame(page);
const listStyleBookmarkBox = await bookmark.boundingBox();
assertExists(listStyleBookmarkBox);
assertAlmostEqual(listStyleBookmarkBox.width, 752, 2);
assertAlmostEqual(listStyleBookmarkBox.height, 48, 2);
await openCardStyleMenu();
await cardStyleHorizontalButton.click();
await waitNextFrame(page);
const horizontalStyleBookmarkBox = await bookmark.boundingBox();
assertExists(horizontalStyleBookmarkBox);
assertAlmostEqual(horizontalStyleBookmarkBox.width, 752, 2);
assertAlmostEqual(horizontalStyleBookmarkBox.height, 116, 2);
});
});
test('indent bookmark block to paragraph', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await pressEnter(page);
await type(page, '/link', 100);
await pressEnter(page);
await type(page, LOCAL_HOST_URL);
await pressEnter(page);
await assertBlockChildrenIds(page, '1', ['2', '4']);
await assertBlockFlavour(page, '1', 'affine:note');
await assertBlockFlavour(page, '2', 'affine:paragraph');
await assertBlockFlavour(page, '4', 'affine:bookmark');
await focusRichText(page);
await pressArrowDown(page);
await assertBlockSelections(page, ['4']);
await pressTab(page);
await assertBlockChildrenIds(page, '1', ['2']);
await assertBlockChildrenIds(page, '2', ['4']);
await pressShiftTab(page);
await assertBlockChildrenIds(page, '1', ['2', '4']);
});
test('indent bookmark block to list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '- a');
await pressEnter(page);
await type(page, '/link', 100);
await pressEnter(page);
await type(page, LOCAL_HOST_URL);
await pressEnter(page);
await assertBlockChildrenIds(page, '1', ['3', '5']);
await assertBlockFlavour(page, '1', 'affine:note');
await assertBlockFlavour(page, '3', 'affine:list');
await assertBlockFlavour(page, '5', 'affine:bookmark');
await focusRichText(page);
await pressArrowDown(page);
await assertBlockSelections(page, ['5']);
await pressTab(page);
await assertBlockChildrenIds(page, '1', ['3']);
await assertBlockChildrenIds(page, '3', ['5']);
await pressShiftTab(page);
await assertBlockChildrenIds(page, '1', ['3', '5']);
});
test('bookmark can be dragged from note to surface top level block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await page.waitForTimeout(100);
await type(page, '/link', 100);
await pressEnter(page);
await page.waitForTimeout(100);
await type(page, LOCAL_HOST_URL);
await pressEnter(page);
await switchEditorMode(page);
await page.mouse.dblclick(450, 450);
await dragBlockToPoint(page, '4', { x: 200, y: 200 });
await waitNextFrame(page);
await assertParentBlockFlavour(page, '4', 'affine:surface');
});
test.describe('embed youtube card', () => {
test(scoped`create youtube card by slash menu`, async ({ page }) => {
expectConsoleMessage(page, /Unrecognized feature/, 'warning');
expectConsoleMessage(page, /Failed to load resource/);
await createBookmarkBlockBySlashMenu(page, YOUTUBE_URL);
const snapshot = (await getPageSnapshot(page)) as BlockSnapshot;
expect(ignoreSnapshotId(snapshot)).toMatchSnapshot('embed-youtube.json');
});
test(scoped`change youtube card style`, async ({ page }) => {
expectConsoleMessage(page, /Unrecognized feature/, 'warning');
expectConsoleMessage(page, /Failed to load resource/);
await createBookmarkBlockBySlashMenu(page, YOUTUBE_URL);
const youtube = page.locator('affine-embed-youtube-block');
await youtube.click();
await page.waitForTimeout(100);
// change to card view
const embedToolbar = page.locator('affine-embed-card-toolbar');
await expect(embedToolbar).toBeVisible();
const embedView = page.locator('editor-menu-button', {
hasText: 'embed view',
});
await expect(embedView).toBeVisible();
await embedView.click();
const cardView = page.locator('editor-menu-action', {
hasText: 'card view',
});
await expect(cardView).toBeVisible();
await cardView.click();
const snapshot = (await getPageSnapshot(page)) as BlockSnapshot;
expect(ignoreSnapshotId(snapshot)).toMatchSnapshot(
'horizontal-youtube.json'
);
// change to embed view
const bookmark = page.locator('affine-bookmark');
await bookmark.click();
await page.waitForTimeout(100);
const cardView2 = page.locator('editor-icon-button', {
hasText: 'card view',
});
await expect(cardView2).toBeVisible();
await cardView2.click();
const embedView2 = page.locator('editor-menu-action', {
hasText: 'embed view',
});
await expect(embedView2).toBeVisible();
await embedView2.click();
const snapshot2 = (await getPageSnapshot(page)) as BlockSnapshot;
expect(ignoreSnapshotId(snapshot2)).toMatchSnapshot('embed-youtube.json');
});
});
test.describe('embed figma card', () => {
test(scoped`create figma card by slash menu`, async ({ page }) => {
expectConsoleMessage(page, /Failed to load resource/);
expectConsoleMessage(page, /Refused to frame/);
await createBookmarkBlockBySlashMenu(page, FIGMA_URL);
const snapshot = (await getPageSnapshot(page)) as BlockSnapshot;
expect(ignoreSnapshotId(snapshot)).toMatchSnapshot('embed-figma.json');
});
test(scoped`change figma card style`, async ({ page }) => {
expectConsoleMessage(page, /Failed to load resource/);
expectConsoleMessage(page, /Refused to frame/);
expectConsoleMessage(page, /Running frontend commit/, 'log');
await createBookmarkBlockBySlashMenu(page, FIGMA_URL);
const youtube = page.locator('affine-embed-figma-block');
await youtube.click();
await page.waitForTimeout(100);
// change to card view
const embedToolbar = page.locator('affine-embed-card-toolbar');
await expect(embedToolbar).toBeVisible();
const embedView = page.locator('editor-menu-button', {
hasText: 'embed view',
});
await expect(embedView).toBeVisible();
await embedView.click();
const cardView = page.locator('editor-menu-action', {
hasText: 'card view',
});
await expect(cardView).toBeVisible();
await cardView.click();
const snapshot = (await getPageSnapshot(page)) as BlockSnapshot;
expect(ignoreSnapshotId(snapshot)).toMatchSnapshot('horizontal-figma.json');
// change to embed view
const bookmark = page.locator('affine-bookmark');
await bookmark.click();
await page.waitForTimeout(100);
const cardView2 = page.locator('editor-icon-button', {
hasText: 'card view',
});
await expect(cardView2).toBeVisible();
await cardView2.click();
const embedView2 = page.locator('editor-menu-action', {
hasText: 'embed view',
});
await expect(embedView2).toBeVisible();
await embedView2.click();
const snapshot2 = (await getPageSnapshot(page)) as BlockSnapshot;
expect(ignoreSnapshotId(snapshot2)).toMatchSnapshot('embed-figma.json');
});
});

View File

@@ -0,0 +1,409 @@
import '../utils/declare-test-window.js';
import { expect } from '@playwright/test';
import {
captureHistory,
copyByKeyboard,
dragBetweenCoords,
dragOverTitle,
enterPlaygroundRoom,
focusRichText,
focusTitle,
getClipboardHTML,
getClipboardSnapshot,
getClipboardText,
getCurrentEditorDocId,
getEditorLocator,
getPageSnapshot,
initEmptyParagraphState,
mockParseDocUrlService,
pasteByKeyboard,
pasteContent,
pressEnter,
pressShiftTab,
pressTab,
resetHistory,
setInlineRangeInSelectedRichText,
setSelection,
SHORT_KEY,
type,
undoByClick,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertBlockTypes,
assertClipItems,
assertExists,
assertRichTexts,
assertText,
assertTitle,
} from '../utils/asserts.js';
import { scoped, test } from '../utils/playwright.js';
test(scoped`clipboard copy paste`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'test');
await setInlineRangeInSelectedRichText(page, 0, 3);
await waitNextFrame(page);
await copyByKeyboard(page);
await focusRichText(page);
await page.keyboard.press(`${SHORT_KEY}+v`);
await assertText(page, 'testtes');
});
test(scoped`clipboard copy paste title`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusTitle(page);
await type(page, 'test');
await dragOverTitle(page);
await waitNextFrame(page);
await copyByKeyboard(page);
await focusTitle(page);
await page.keyboard.press(`${SHORT_KEY}+v`);
await assertTitle(page, 'testtest');
});
test(scoped`clipboard paste html`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
// set up clipboard data using html
const clipData = {
'text/html': `<span>aaa</span><span>bbb</span><span>ccc</span><bdi>ddd</bdi>`,
};
await waitNextFrame(page);
await page.evaluate(
({ clipData }) => {
const dT = new DataTransfer();
const e = new ClipboardEvent('paste', { clipboardData: dT });
Object.defineProperty(e, 'target', {
writable: false,
value: document,
});
e.clipboardData?.setData('text/html', clipData['text/html']);
document.dispatchEvent(e);
},
{ clipData }
);
await assertText(page, 'aaabbbcccddd');
});
test(scoped`split block when paste`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await resetHistory(page);
const clipData = {
'text/plain': `# text
# h1
`,
};
await type(page, 'abc');
await captureHistory(page);
await setInlineRangeInSelectedRichText(page, 1, 1);
await pasteContent(page, clipData);
await waitNextFrame(page);
await assertRichTexts(page, ['atext', 'h1c']);
await undoByClick(page);
await assertRichTexts(page, ['abc']);
await type(page, 'aa');
await pressEnter(page);
await type(page, 'bb');
const topLeft123 = await getEditorLocator(page)
.locator('[data-block-id="2"] .inline-editor')
.boundingBox();
const bottomRight789 = await getEditorLocator(page)
.locator('[data-block-id="4"] .inline-editor')
.boundingBox();
assertExists(topLeft123);
assertExists(bottomRight789);
await dragBetweenCoords(page, topLeft123, bottomRight789);
// FIXME see https://github.com/toeverything/blocksuite/pull/878
// await pasteContent(page, clipData);
// await assertRichTexts(page, ['aaa', 'bbc', 'text', 'h1']);
});
test(scoped`copy clipItems format`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await captureHistory(page);
const clipData = `
- aa
- bb
- cc
- dd
`;
await pasteContent(page, { 'text/plain': clipData });
await page.waitForTimeout(100);
await setSelection(page, 4, 1, 5, 1);
assertClipItems(page, 'text/plain', 'bc');
assertClipItems(page, 'text/html', '<ul><li>b<ul><li>c</li></ul></li></ul>');
await undoByClick(page);
await assertRichTexts(page, ['']);
});
test(scoped`copy partially selected text`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '123 456 789');
// select 456
await setInlineRangeInSelectedRichText(page, 4, 3);
await copyByKeyboard(page);
assertClipItems(page, 'text/plain', '456');
// move to line end
await setInlineRangeInSelectedRichText(page, 11, 0);
await pressEnter(page);
await pasteByKeyboard(page);
await waitNextFrame(page);
await assertRichTexts(page, ['123 456 789', '456']);
});
test(scoped`copy & paste outside editor`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await page.evaluate(() => {
const input = document.createElement('input');
input.setAttribute('id', 'input-test');
input.value = '123';
document.body.querySelector('#app')?.append(input);
});
await page.focus('#input-test');
await page.dblclick('#input-test');
await copyByKeyboard(page);
await focusRichText(page);
await pasteByKeyboard(page);
await waitNextFrame(page);
await assertRichTexts(page, ['123']);
});
test('should keep first line format when pasted into a new line', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const clipData = `
- [ ] aaa
`;
await pasteContent(page, { 'text/plain': clipData });
await waitNextFrame(page);
await assertRichTexts(page, ['aaa']);
await assertBlockTypes(page, ['todo']);
});
test(scoped`auto identify url`, async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
// set up clipboard data using html
const clipData = {
'text/plain': `test https://www.google.com`,
};
await waitNextFrame(page);
await page.evaluate(
({ clipData }) => {
const dT = new DataTransfer();
const e = new ClipboardEvent('paste', { clipboardData: dT });
Object.defineProperty(e, 'target', {
writable: false,
value: document,
});
e.clipboardData?.setData('text/plain', clipData['text/plain']);
document.dispatchEvent(e);
},
{ clipData }
);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test(scoped`pasting internal url`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusTitle(page);
await type(page, 'test page');
await focusRichText(page);
const docId = await getCurrentEditorDocId(page);
await mockParseDocUrlService(page, {
'http://workspace/doc-id': docId,
});
await pasteContent(page, {
'text/plain': 'http://workspace/doc-id',
});
await expect(page.locator('affine-reference')).toContainText('test page');
});
test(scoped`pasting internal url with params`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusTitle(page);
await type(page, 'test page');
await focusRichText(page);
const docId = await getCurrentEditorDocId(page);
await mockParseDocUrlService(page, {
'http://workspace/doc-id?mode=page&blockIds=rL2_GXbtLU2SsJVfCSmh_': docId,
});
await pasteContent(page, {
'text/plain':
'http://workspace/doc-id?mode=page&blockIds=rL2_GXbtLU2SsJVfCSmh_',
});
await expect(page.locator('affine-reference')).toContainText('test page');
});
test(
scoped`pasting an external URL from clipboard to automatically creating a link from selection`,
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusTitle(page);
await type(page, 'test page');
await focusRichText(page);
await type(page, 'title alias');
await setSelection(page, 1, 6, 1, 11);
await pasteContent(page, {
'text/plain': 'https://affine.pro/',
});
await expect(page.locator('affine-link')).toContainText('alias');
}
);
test(
scoped`pasting an internal URL from clipboard to automatically creating a link from selection`,
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusTitle(page);
await type(page, 'test page');
await focusRichText(page);
await type(page, 'title alias');
await setSelection(page, 1, 6, 1, 11);
const docId = await getCurrentEditorDocId(page);
await mockParseDocUrlService(page, {
'http://workspace/doc-id': docId,
});
await pasteContent(page, {
'text/plain': 'http://workspace/doc-id',
});
await expect(page.locator('affine-reference')).toContainText('alias');
}
);
test(scoped`paste parent block`, async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/3153',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'This is parent');
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await type(page, 'This is child 1');
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await type(page, 'This is child 2');
await setInlineRangeInSelectedRichText(page, 0, 3);
await copyByKeyboard(page);
await focusRichText(page, 2);
await page.keyboard.press(`${SHORT_KEY}+v`);
await assertRichTexts(page, [
'This is parent',
'This is child 1',
'This is child 2Thi',
]);
});
test(scoped`clipboard copy multi selection`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'abc');
await pressEnter(page);
await type(page, 'def');
await setSelection(page, 2, 1, 3, 1);
await waitNextFrame(page);
await copyByKeyboard(page);
await waitNextFrame(page);
await focusRichText(page, 1);
await pasteByKeyboard(page);
await waitNextFrame(page);
await type(page, 'cursor');
await waitNextFrame(page);
await assertRichTexts(page, ['abc', 'defbc', 'dcursor']);
});
test(scoped`clipboard copy nested items`, async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'abc');
await pressEnter(page);
await pressTab(page);
await type(page, 'def');
await pressEnter(page);
await pressTab(page);
await type(page, 'ghi');
await pressEnter(page);
await pressShiftTab(page);
await pressShiftTab(page);
await type(page, 'jkl');
await setSelection(page, 2, 1, 3, 1);
await waitNextFrame(page);
await copyByKeyboard(page);
const text = await getClipboardText(page);
const html = await getClipboardHTML(page);
const snapshot = await getClipboardSnapshot(page);
expect(text).toMatchSnapshot(`${testInfo.title}-clipboard.md`);
expect(JSON.stringify(snapshot.snapshot.content, null, 2)).toMatchSnapshot(
`${testInfo.title}-clipboard.json`
);
expect(html).toMatchSnapshot(`${testInfo.title}-clipboard.html`);
await setSelection(page, 4, 1, 5, 1);
await waitNextFrame(page);
await copyByKeyboard(page);
const text2 = await getClipboardText(page);
const html2 = await getClipboardHTML(page);
const snapshot2 = await getClipboardSnapshot(page);
expect(text2).toMatchSnapshot(`${testInfo.title}-clipboard2.md`);
expect(JSON.stringify(snapshot2.snapshot.content, null, 2)).toMatchSnapshot(
`${testInfo.title}-clipboard2.json`
);
expect(html2).toMatchSnapshot(`${testInfo.title}-clipboard2.html`);
});

View File

@@ -0,0 +1,58 @@
import {
enterPlaygroundRoom,
focusRichText,
initEmptyParagraphState,
pasteContent,
pressArrowDown,
pressArrowUp,
pressEscape,
waitEmbedLoaded,
} from '../utils/actions/index.js';
import { assertRichImage, assertText } from '../utils/asserts.js';
import { scoped, test } from '../utils/playwright.js';
test(
scoped`clipboard paste end with image, the cursor should be controlled by up/down keys`,
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/3639',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
// set up clipboard data using html
const clipData = {
'text/html': `<p>Lorem Ipsum placeholder text.</p>
<figure ><img src='https://placehold.co/600x400' /></figure>
`,
};
await page.evaluate(
({ clipData }) => {
const dT = new DataTransfer();
const e = new ClipboardEvent('paste', { clipboardData: dT });
Object.defineProperty(e, 'target', {
writable: false,
value: document,
});
e.clipboardData?.setData('text/html', clipData['text/html']);
document.dispatchEvent(e);
},
{ clipData }
);
const str = 'Lorem Ipsum placeholder text.';
await waitEmbedLoaded(page);
await assertRichImage(page, 1);
await pressEscape(page);
await pressArrowUp(page, 1);
await pasteContent(page, clipData);
await assertRichImage(page, 2);
await assertText(page, str + str);
await pressArrowDown(page, 1);
await pressEscape(page);
await pasteContent(page, clipData);
await assertRichImage(page, 3);
await assertText(page, 'Lorem Ipsum placeholder text.', 1);
}
);

View File

@@ -0,0 +1,645 @@
import type { BlockSnapshot } from '@blocksuite/store';
import { expect } from '@playwright/test';
import { lightThemeV2 } from '@toeverything/theme/v2';
import { initDatabaseColumn } from '../database/actions.js';
import {
activeNoteInEdgeless,
changeEdgelessNoteBackground,
copyByKeyboard,
createShapeElement,
cutByKeyboard,
dragBetweenCoords,
enterPlaygroundRoom,
focusRichText,
getAllNoteIds,
getClipboardHTML,
getClipboardSnapshot,
getClipboardText,
getEdgelessSelectedRectModel,
getInlineSelectionIndex,
getInlineSelectionText,
getPageSnapshot,
getRichTextBoundingBox,
initDatabaseDynamicRowWithData,
initEmptyDatabaseWithParagraphState,
initEmptyEdgelessState,
initEmptyParagraphState,
initThreeParagraphs,
pasteByKeyboard,
pasteContent,
pressArrowLeft,
pressArrowRight,
pressEnter,
pressEscape,
pressShiftTab,
pressSpace,
pressTab,
selectAllByKeyboard,
selectNoteInEdgeless,
setInlineRangeInSelectedRichText,
SHORT_KEY,
switchEditorMode,
toViewCoord,
triggerComponentToolbarAction,
type,
undoByKeyboard,
waitForInlineEditorStateUpdated,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertBlockTypes,
assertEdgelessNoteBackground,
assertEdgelessSelectedModelRect,
assertExists,
assertRichTextModelType,
assertRichTexts,
assertText,
} from '../utils/asserts.js';
import { scoped, test } from '../utils/playwright.js';
test('paste a non-nested list to a non-nested list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const clipData = {
'text/plain': `
- a
`,
};
await type(page, '-');
await pressSpace(page);
await type(page, '123');
await page.keyboard.press('Control+ArrowLeft');
// paste on start
await waitNextFrame(page);
await pasteContent(page, clipData);
await pressArrowLeft(page);
await assertRichTexts(page, ['a123']);
// paste in middle
await pressArrowRight(page, 2);
await pasteContent(page, clipData);
await pressArrowRight(page);
await assertRichTexts(page, ['a1a23']);
// paste on end
await pressArrowRight(page);
await pasteContent(page, clipData);
await waitNextFrame(page);
await assertRichTexts(page, ['a1a23a']);
await assertBlockTypes(page, ['bulleted']);
});
test('copy a nested list by clicking button, the clipboard data should be complete', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const clipData = {
'text/plain': `
- aaa
- bbb
- ccc
`,
};
await pasteContent(page, clipData);
const rootListBound = await page.locator('affine-list').first().boundingBox();
assertExists(rootListBound);
// use drag element to test.
await dragBetweenCoords(
page,
{ x: rootListBound.x + 1, y: rootListBound.y - 1 },
{ x: rootListBound.x + 1, y: rootListBound.y + rootListBound.height - 1 }
);
await copyByKeyboard(page);
const text = await getClipboardText(page);
const html = await getClipboardHTML(page);
const snapshot = await getClipboardSnapshot(page);
expect(text).toMatchSnapshot(`${testInfo.title}-clipboard.md`);
expect(JSON.stringify(snapshot.snapshot.content, null, 2)).toMatchSnapshot(
`${testInfo.title}-clipboard.json`
);
expect(html).toMatchSnapshot(`${testInfo.title}-clipboard.html`);
});
test('paste a nested list to a nested list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const clipData = {
'text/plain': `
- aaa
- bbb
- ccc
`,
};
await pasteContent(page, clipData);
await focusRichText(page, 1);
// paste on start
await page.keyboard.press('Control+ArrowLeft');
/**
* - aaa
* - |bbb
* - ccc
*/
await pasteContent(page, clipData);
/**
* - aaa
* - aaa
* - bbb
* - ccc|bbb
* -ccc
*/
await assertRichTexts(page, ['aaa', 'aaa', 'bbb', 'cccbbb', 'ccc']);
expect(await getInlineSelectionText(page)).toEqual('cccbbb');
expect(await getInlineSelectionIndex(page)).toEqual(3);
// paste in middle
await undoByKeyboard(page);
await pressArrowRight(page);
/**
* - aaa
* - b|bb
* - ccc
*/
await pasteContent(page, clipData);
/**
* - aaa
* - baaa
* - bbb
* - ccc|bb
* - ccc
*/
await assertRichTexts(page, ['aaa', 'baaa', 'bbb', 'cccbb', 'ccc']);
expect(await getInlineSelectionText(page)).toEqual('cccbb');
expect(await getInlineSelectionIndex(page)).toEqual(3);
// paste on end
await undoByKeyboard(page);
await page.keyboard.press('Control+ArrowRight');
/**
* - aaa
* - bbb|
* - ccc
*/
await pasteContent(page, clipData);
/**
* - aaa
* - bbbaaa
* - bbb
* - ccc|
* - ccc
*/
await assertRichTexts(page, ['aaa', 'bbbaaa', 'bbb', 'ccc', 'ccc']);
expect(await getInlineSelectionText(page)).toEqual('ccc');
expect(await getInlineSelectionIndex(page)).toEqual(3);
});
test('paste nested lists to a nested list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const clipData = {
'text/plain': `
- aaa
- bbb
- ccc
`,
};
await pasteContent(page, clipData);
await focusRichText(page, 1);
const clipData2 = {
'text/plain': `
- 111
- 222
- 111
- 222
`,
};
// paste on start
await page.keyboard.press('Control+ArrowLeft');
/**
* - aaa
* - |bbb
* - ccc
*/
await pasteContent(page, clipData2);
/**
* - aaa
* - 111
* - 222
* - 111
* - 222|bbb
* - ccc
*/
await assertRichTexts(page, ['aaa', '111', '222', '111', '222bbb', 'ccc']);
expect(await getInlineSelectionText(page)).toEqual('222bbb');
expect(await getInlineSelectionIndex(page)).toEqual(3);
// paste in middle
await undoByKeyboard(page);
await pressArrowRight(page);
/**
* - aaa
* - b|bb
* - ccc
*/
await pasteContent(page, clipData2);
/**
* - aaa
* - b111
* - 222
* - 111
* - 222|bb
* - ccc
*/
await assertRichTexts(page, ['aaa', 'b111', '222', '111', '222bb', 'ccc']);
expect(await getInlineSelectionText(page)).toEqual('222bb');
expect(await getInlineSelectionIndex(page)).toEqual(3);
// paste on end
await undoByKeyboard(page);
await page.keyboard.press('Control+ArrowRight');
/**
* - aaa
* - bbb|
* - ccc
*/
await pasteContent(page, clipData2);
/**
* - aaa
* - bbb111
* - 222
* - 111
* - 222|
* - ccc
*/
await assertRichTexts(page, ['aaa', 'bbb111', '222', '111', '222', 'ccc']);
expect(await getInlineSelectionText(page)).toEqual('222');
expect(await getInlineSelectionIndex(page)).toEqual(3);
});
test('paste non-nested lists to a nested list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const clipData = {
'text/plain': `
- aaa
- bbb
`,
};
await pasteContent(page, clipData);
await focusRichText(page, 0);
const clipData2 = {
'text/plain': `
- 123
- 456
`,
};
// paste on start
await page.keyboard.press('Control+ArrowLeft');
/**
* - |aaa
* - bbb
*/
await pasteContent(page, clipData2);
/**
* - 123
* - 456|aaa
* - bbb
*/
await assertRichTexts(page, ['123', '456aaa', 'bbb']);
expect(await getInlineSelectionText(page)).toEqual('456aaa');
expect(await getInlineSelectionIndex(page)).toEqual(3);
});
test(scoped`cut should work for multi-block selection`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'a');
await pressEnter(page);
await type(page, 'b');
await pressEnter(page);
await type(page, 'c');
await selectAllByKeyboard(page);
await selectAllByKeyboard(page);
await selectAllByKeyboard(page);
await cutByKeyboard(page);
await page.locator('.affine-page-viewport').click();
await waitNextFrame(page);
await assertText(page, '');
});
test(
scoped`pasting into empty list should not convert the list into paragraph`,
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'test');
await setInlineRangeInSelectedRichText(page, 0, 4);
await copyByKeyboard(page);
await type(page, '- ');
await page.keyboard.press(`${SHORT_KEY}+v`);
await assertRichTexts(page, ['test']);
await assertRichTextModelType(page, 'bulleted');
}
);
test('cut will delete all content, and copy will reappear content', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '-');
await pressSpace(page);
await type(page, '1');
await pressEnter(page);
await pressTab(page);
await type(page, '2');
await pressEnter(page);
await type(page, '3');
await pressEnter(page);
await pressShiftTab(page);
await type(page, '4');
const box123 = await getRichTextBoundingBox(page, '1');
const inside123 = { x: box123.left + 1, y: box123.top + 1 };
const box789 = await getRichTextBoundingBox(page, '6');
const inside789 = { x: box789.right - 1, y: box789.bottom - 1 };
// from top to bottom
await dragBetweenCoords(page, inside123, inside789);
await cutByKeyboard(page);
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after-cut.json`
);
await waitNextFrame(page);
await focusRichText(page);
await pasteByKeyboard(page);
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after-paste.json`
);
});
test(scoped`should copy and paste of database work`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseWithParagraphState(page);
// init database columns and rows
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, 'abc', true);
await pressEscape(page);
await focusRichText(page, 1);
await selectAllByKeyboard(page);
await selectAllByKeyboard(page);
await copyByKeyboard(page);
await pressEnter(page);
await pasteByKeyboard(page);
await page.waitForTimeout(100);
let pageJson = await getPageSnapshot(page, false);
let note = (pageJson as BlockSnapshot).children[0];
const database = note.children[0];
expect(database.flavour).toBe('affine:database');
await undoByKeyboard(page);
pageJson = await getPageSnapshot(page, false);
note = (pageJson as BlockSnapshot).children[0];
const db = note.children.find(child => child.flavour === 'affine:database');
expect(db).toBeDefined();
});
test(`copy canvas element and text note in edgeless mode`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await initThreeParagraphs(page);
await createShapeElement(page, [0, 0], [100, 100]);
await selectAllByKeyboard(page);
const bound = await getEdgelessSelectedRectModel(page);
await copyByKeyboard(page);
const coord = await toViewCoord(page, [
bound[0] + bound[2] / 2,
bound[1] + bound[3] / 2 + 200,
]);
await page.mouse.move(coord[0], coord[1]);
await page.waitForTimeout(300);
await pasteByKeyboard(page, false);
bound[1] = bound[1] + 200;
await assertEdgelessSelectedModelRect(page, bound);
});
test(scoped`copy when text note active in edgeless`, async ({ page }) => {
await enterPlaygroundRoom(page);
const ids = await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, '1234');
await switchEditorMode(page);
await activeNoteInEdgeless(page, ids.noteId);
await waitForInlineEditorStateUpdated(page);
await setInlineRangeInSelectedRichText(page, 0, 4);
await copyByKeyboard(page);
await pressArrowRight(page);
await type(page, '555');
await pasteByKeyboard(page, false);
await assertText(page, '12345551234');
});
test(scoped`paste note block with background`, async ({ page }) => {
await enterPlaygroundRoom(page);
const ids = await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, '1234');
await switchEditorMode(page);
await selectNoteInEdgeless(page, ids.noteId);
await triggerComponentToolbarAction(page, 'changeNoteColor');
await changeEdgelessNoteBackground(page, 'White');
await assertEdgelessNoteBackground(
page,
ids.noteId,
lightThemeV2['edgeless/note/white']
);
await copyByKeyboard(page);
await page.mouse.move(0, 0);
await pasteByKeyboard(page, false);
const noteIds = await getAllNoteIds(page);
for (const noteId of noteIds) {
await assertEdgelessNoteBackground(
page,
noteId,
lightThemeV2['edgeless/note/white']
);
}
});
test(scoped`copy and paste to selection block selection`, async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2265',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '1234');
await selectAllByKeyboard(page);
await copyByKeyboard(page);
await pressArrowRight(page);
await pasteByKeyboard(page, false);
await waitNextFrame(page);
await assertRichTexts(page, ['12341234']);
});
test(
scoped`should keep paragraph block's type when pasting at the start of empty paragraph block except type text`,
async ({ page }, testInfo) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2336',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await focusRichText(page);
await type(page, '>');
await page.keyboard.press('Space', { delay: 50 });
await page.evaluate(() => {
const input = document.createElement('input');
input.setAttribute('id', 'input-test');
input.value = '123';
document.body.querySelector('#app')?.append(input);
});
await page.focus('#input-test');
await page.dblclick('#input-test');
await copyByKeyboard(page);
await focusRichText(page);
await pasteByKeyboard(page);
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after-paste-1.json`
);
await pressEnter(page);
await waitNextFrame(page);
await pressEnter(page);
await waitNextFrame(page);
await pasteByKeyboard(page, false);
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after-paste-2.json`
);
}
);
test(scoped`paste from FeiShu list format`, async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2438',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
// set up clipboard data using html
const clipData = {
'text/html': `<div><li><div><span>aaaa</span></div></li></div>`,
};
await waitNextFrame(page);
await page.evaluate(
({ clipData }) => {
const dT = new DataTransfer();
const e = new ClipboardEvent('paste', { clipboardData: dT });
Object.defineProperty(e, 'target', {
writable: false,
value: document,
});
e.clipboardData?.setData('text/html', clipData['text/html']);
document.dispatchEvent(e);
},
{ clipData }
);
await assertText(page, 'aaaa');
await assertBlockTypes(page, ['bulleted']);
});
test(scoped`paste in list format`, async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2281',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '- test');
await focusRichText(page);
const clipData = {
'text/html': `<ul><li>111<ul><li>222</li></ul></li></ul>`,
};
await waitNextFrame(page);
await page.evaluate(
({ clipData }) => {
const dT = new DataTransfer();
const e = new ClipboardEvent('paste', { clipboardData: dT });
Object.defineProperty(e, 'target', {
writable: false,
value: document,
});
e.clipboardData?.setData('text/html', clipData['text/html']);
document.dispatchEvent(e);
},
{ clipData }
);
await assertRichTexts(page, ['test111', '222']);
});

View File

@@ -0,0 +1,170 @@
import {
enterPlaygroundRoom,
focusRichText,
initEmptyParagraphState,
pasteContent,
resetHistory,
undoByClick,
waitEmbedLoaded,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertBlockTypes,
assertRichImage,
assertRichTexts,
assertTextFormats,
} from '../utils/asserts.js';
import { scoped, test } from '../utils/playwright.js';
test(scoped`markdown format parse`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await resetHistory(page);
let clipData = {
'text/plain': `# h1
## h2
### h3
#### h4
##### h5
###### h6
- [ ] todo
- [ ] todo
- [x] todo
* bulleted
- bulleted
1. numbered
> quote
`,
};
await waitNextFrame(page);
await pasteContent(page, clipData);
await page.waitForTimeout(200);
await assertBlockTypes(page, [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'todo',
'todo',
'todo',
'bulleted',
'bulleted',
'numbered',
'quote',
]);
await assertRichTexts(page, [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'todo',
'todo',
'todo',
'bulleted',
'bulleted',
'numbered',
'quote',
]);
await undoByClick(page);
await assertRichTexts(page, ['']);
await focusRichText(page);
clipData = {
'text/plain': `# ***bolditalic***
# **bold**
*italic*
~~strikethrough~~
[link](linktest)
\`code\`
`,
};
await waitNextFrame(page);
await pasteContent(page, clipData);
await page.waitForTimeout(200);
await assertTextFormats(page, [
{ bold: true, italic: true },
{ bold: true },
{ italic: true },
{ strike: true },
{ link: 'linktest' },
{ code: true },
]);
await undoByClick(page);
await assertRichTexts(page, ['']);
});
test(scoped`import markdown`, async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await resetHistory(page);
const clipData = `# text
# h1
`;
await pasteContent(page, { 'text/plain': clipData });
await page.waitForTimeout(100);
await assertRichTexts(page, ['text', 'h1']);
await undoByClick(page);
await assertRichTexts(page, ['']);
});
test(
scoped`clipboard paste HTML containing markdown syntax code and image `,
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2855',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
// set up clipboard data using html
const clipData = {
'text/html': `<p>符合 Markdown 格式的 URL 放到笔记中,此时需要的格式如下:</p>
<pre><code>md [任务管理这件事 - 少数派](https://sspai.com/post/61092)</code></pre>
<p>(将一段文字包裹在<code >[[]]</code>中)此时需要的格式如下:</p>
<figure ><img src="https://placehold.co/600x400"></figure>
<p>上图中,当我们处在 Obsidian 的「预览模式」时,点击这个「双向链接」</p>
`,
};
await page.evaluate(
({ clipData }) => {
const dT = new DataTransfer();
const e = new ClipboardEvent('paste', { clipboardData: dT });
Object.defineProperty(e, 'target', {
writable: false,
value: document,
});
e.clipboardData?.setData('text/html', clipData['text/html']);
document.dispatchEvent(e);
},
{ clipData }
);
await waitEmbedLoaded(page);
// await page.waitForTimeout(500);
await assertRichImage(page, 1);
}
);

View File

@@ -0,0 +1,149 @@
import { expect } from '@playwright/test';
import {
copyByKeyboard,
pasteByKeyboard,
pressArrowLeft,
pressEnter,
pressEnterWithShortkey,
selectAllByKeyboard,
type,
} from '../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
focusRichText,
getInlineSelectionText,
getPageSnapshot,
initEmptyCodeBlockState,
setSelection,
} from '../utils/actions/misc.js';
import { assertRichTextInlineRange } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
import { getCodeBlock } from './utils.js';
test('keyboard selection and copy paste', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'use');
await page.keyboard.down('Shift');
await pressArrowLeft(page, 'use'.length);
await page.keyboard.up('Shift');
await copyByKeyboard(page);
await pressArrowLeft(page, 1);
await pasteByKeyboard(page);
const content = await getInlineSelectionText(page);
expect(content).toBe('useuse');
await assertRichTextInlineRange(page, 0, 3, 0);
});
test('paste with more than one continuous breakline should remain in code block, ', async ({
page,
}) => {
await page.setContent(`<div contenteditable>use super::*;
use fern::{
colors::{Color, ColoredLevelConfig},
Dispatch,
};
<br><br>
#[inline]</div>`);
await page.focus('div');
await selectAllByKeyboard(page);
await copyByKeyboard(page);
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await pasteByKeyboard(page);
const locator = page.locator('affine-paragraph');
await expect(locator).toBeHidden();
});
test('drag copy paste', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'use');
await setSelection(page, 2, 0, 2, 3);
await copyByKeyboard(page);
await pressArrowLeft(page);
await pasteByKeyboard(page);
const content = await getInlineSelectionText(page);
expect(content).toBe('useuse');
await assertRichTextInlineRange(page, 0, 3, 0);
});
test.skip('use keyboard copy inside code block copy', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'use');
await page.keyboard.down('Shift');
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < 'use'.length; i++) {
await page.keyboard.press('ArrowLeft');
}
await page.keyboard.up('Shift');
await copyByKeyboard(page);
await page.keyboard.press('ArrowRight');
await pressEnter(page);
await pressEnter(page);
await pasteByKeyboard(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_pasted.json`
);
});
test('code block has content, click code block copy menu, copy whole code block', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page, { language: 'javascript' });
await focusRichText(page);
await page.keyboard.type('use');
await pressEnterWithShortkey(page);
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
await expect(codeBlockController.copyButton).toBeVisible();
await codeBlockController.copyButton.click();
await focusRichText(page, 1);
await pasteByKeyboard(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_pasted.json`
);
});
test('code block is empty, click code block copy menu, copy the empty code block', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page, { language: 'javascript' });
await focusRichText(page);
await pressEnterWithShortkey(page);
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
await expect(codeBlockController.copyButton).toBeVisible();
await codeBlockController.copyButton.click();
await focusRichText(page, 1);
await pasteByKeyboard(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_pasted.json`
);
});

View File

@@ -0,0 +1,604 @@
import { expect } from '@playwright/test';
import { updateBlockType } from '../utils/actions/block.js';
import { dragBetweenIndices } from '../utils/actions/drag.js';
import {
createCodeBlock,
pressArrowLeft,
pressArrowUp,
pressBackspace,
pressEnter,
pressEscape,
pressShiftTab,
pressTab,
redoByKeyboard,
type,
undoByKeyboard,
} from '../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
focusRichText,
focusRichTextEnd,
getPageSnapshot,
initEmptyCodeBlockState,
initEmptyParagraphState,
setSelection,
waitNextFrame,
} from '../utils/actions/misc.js';
import {
assertBlockCount,
assertRichTexts,
assertTitle,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
import { getFormatBar } from '../utils/query.js';
import { getCodeBlock } from './utils.js';
test('use debug menu can create code block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await updateBlockType(page, 'affine:code');
const locator = page.locator('affine-code');
await expect(locator).toBeVisible();
});
test('use markdown syntax can create code block', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa');
await pressEnter(page);
await type(page, 'bbb');
await pressTab(page);
await pressEnter(page);
await type(page, 'ccc');
await pressTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await setSelection(page, 2, 0, 2, 0);
// |aaa
// bbb
// ccc
await type(page, '``` ');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_markdown_syntax.json`
);
});
test('use markdown syntax with trailing characters can create code block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '```JavaScript');
await type(page, ' ');
const locator = page.locator('affine-code');
await expect(locator).toBeVisible();
});
test('support ```[lang] to add code block with language', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/1314',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '```ts');
await type(page, ' ');
const codeBlockController = getCodeBlock(page);
const codeLocator = codeBlockController.codeBlock;
await expect(codeLocator).toBeVisible();
const codeRect = await codeLocator.boundingBox();
if (!codeRect) {
throw new Error('Failed to get bounding box of code block.');
}
const position = {
x: codeRect.x + codeRect.width / 2,
y: codeRect.y + codeRect.height / 2,
};
await page.mouse.move(position.x, position.y);
const languageButton = codeBlockController.languageButton;
await expect(languageButton).toBeVisible();
await expect(languageButton).toHaveText('TypeScript');
});
test('use more than three backticks can not create code block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '`````');
await type(page, ' ');
const codeBlockLocator = page.locator('affine-code');
await expect(codeBlockLocator).toBeHidden();
const inlineCodelocator = page.getByText('```');
await expect(inlineCodelocator).toBeVisible();
expect(await inlineCodelocator.count()).toEqual(1);
});
test('use shortcut can create code block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await createCodeBlock(page);
const locator = page.locator('affine-code');
await expect(locator).toBeVisible();
});
test('change code language can work', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
await codeBlockController.clickLanguageButton();
const locator = codeBlockController.langList;
await expect(locator).toBeVisible();
await type(page, 'rust');
await page.click(
'.affine-filterable-list > .items-container > icon-button:nth-child(1)'
);
await expect(locator).toBeHidden();
await codeBlockController.codeBlock.hover();
await expect(codeBlockController.languageButton).toHaveText('Rust');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
await undoByKeyboard(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
// Can switch to another language
await codeBlockController.clickLanguageButton();
await type(page, 'ty');
await pressEnter(page);
await expect(locator).toBeHidden();
await expect(codeBlockController.languageButton).toHaveText('TypeScript');
});
test('duplicate code block', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page, { language: 'javascript' });
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
// change language
await codeBlockController.clickLanguageButton();
const langLocator = codeBlockController.langList;
await expect(langLocator).toBeVisible();
await type(page, 'rust');
await page.click(
'.affine-filterable-list > .items-container > icon-button:nth-child(1)'
);
// add text
await focusRichTextEnd(page);
await type(page, 'let a: u8 = 7');
await pressEscape(page);
await waitNextFrame(page, 100);
// add a caption
await codeBlockController.codeBlock.hover();
await codeBlockController.captionButton.click();
await type(page, 'BlockSuite');
await pressEnter(page);
await pressBackspace(page); // remove paragraph
await waitNextFrame(page, 100);
// turn on wrap
await codeBlockController.codeBlock.hover();
await (await codeBlockController.openMore()).wrapButton.click();
// duplicate
await codeBlockController.codeBlock.hover();
await (await codeBlockController.openMore()).duplicateButton.click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test('delete code block in more menu', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page, { language: 'javascript' });
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
const moreMenu = await codeBlockController.openMore();
await expect(moreMenu.menu).toBeVisible();
await moreMenu.deleteButton.click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test('undo and redo works in code block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'const a = 10;');
await assertRichTexts(page, ['const a = 10;']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await redoByKeyboard(page);
await assertRichTexts(page, ['const a = 10;']);
});
test('toggle code block wrap can work', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
const codeBlockController = getCodeBlock(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
await codeBlockController.codeBlock.hover();
await (await codeBlockController.openMore()).wrapButton.click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
await codeBlockController.codeBlock.hover();
await (await codeBlockController.openMore()).cancelWrapButton.click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_3.json`
);
});
test('add caption works', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
await codeBlockController.captionButton.click();
await type(page, 'BlockSuite');
await pressEnter(page);
await waitNextFrame(page, 100);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
test('undo code block wrap can work', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const codeBlockController = getCodeBlock(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
await codeBlockController.codeBlock.hover();
await (await codeBlockController.openMore()).wrapButton.click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
await focusRichText(page);
await undoByKeyboard(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_3.json`
);
});
test('code block toolbar widget can appear and disappear during mousemove', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const position = await page.locator('affine-code').boundingBox();
if (!position) throw new Error('Failed to get affine code position');
await page.mouse.move(position.x, position.y);
const locator = page.locator('.code-toolbar-container');
const toolbarPosition = await locator.boundingBox();
if (!toolbarPosition) throw new Error('Failed to get option position');
await page.mouse.move(toolbarPosition.x, toolbarPosition.y);
await expect(locator).toBeVisible();
await page.mouse.move(position.x - 10, position.y - 10);
await expect(locator).toBeHidden();
});
test('should tab works in code block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'const a = 10;');
await assertRichTexts(page, ['const a = 10;']);
await page.keyboard.press('Tab', { delay: 50 });
await assertRichTexts(page, [' const a = 10;']);
await page.keyboard.press(`Shift+Tab`, { delay: 50 });
await assertRichTexts(page, ['const a = 10;']);
await page.keyboard.press('Enter', { delay: 50 });
await type(page, 'const b = "NothingToSay');
await page.keyboard.press('ArrowUp', { delay: 50 });
await page.keyboard.press('Enter', { delay: 50 });
await page.keyboard.press('Tab', { delay: 50 });
await assertRichTexts(page, ['const a = 10;\n \nconst b = "NothingToSay"']);
});
test('should open more menu and close on selecting', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
await expect(codeBlockController.codeToolbar).toBeVisible();
const moreMenu = await codeBlockController.openMore();
await expect(moreMenu.menu).toBeVisible();
await moreMenu.wrapButton.click();
await expect(moreMenu.menu).toBeHidden();
});
test('should code block lang input supports alias', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const codeBlockController = getCodeBlock(page);
const codeBlock = codeBlockController.codeBlock;
await codeBlock.hover();
await codeBlockController.clickLanguageButton();
await expect(codeBlockController.langList).toBeVisible();
await type(page, '文言');
await pressEnter(page);
await expect(codeBlockController.languageButton).toHaveText('Wenyan');
});
test('multi-line indent', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'aaa');
await pressEnter(page);
await type(page, 'bbb');
await pressEnter(page);
await type(page, 'ccc');
await page.keyboard.down('Shift');
await pressArrowUp(page, 2);
await page.keyboard.up('Shift');
await pressTab(page);
await assertRichTexts(page, [' aaa\n bbb\n ccc']);
await pressShiftTab(page);
await assertRichTexts(page, ['aaa\nbbb\nccc']);
await pressShiftTab(page);
await assertRichTexts(page, ['aaa\nbbb\nccc']);
});
test('should bracket complete works in code block', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/1800',
});
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'const a = "');
await assertRichTexts(page, ['const a = ""']);
await type(page, 'str');
await assertRichTexts(page, ['const a = "str"']);
await type(page, '(');
await assertRichTexts(page, ['const a = "str()"']);
await type(page, ']');
await assertRichTexts(page, ['const a = "str(])"']);
});
test('auto scroll horizontally when typing', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '``` ');
for (let i = 0; i < 100; i++) {
await type(page, String(i));
}
const richTextScrollLeft1 = await page.evaluate(() => {
const richText = document.querySelector('affine-code rich-text');
if (!richText) {
throw new Error('Failed to get rich text');
}
return richText.scrollLeft;
});
expect(richTextScrollLeft1).toBeGreaterThan(200);
await pressArrowLeft(page, 5);
await type(page, 'aa');
const richTextScrollLeft2 = await page.evaluate(() => {
const richText = document.querySelector('affine-code rich-text');
if (!richText) {
throw new Error('Failed to get rich text');
}
return richText.scrollLeft;
});
expect(richTextScrollLeft2).toEqual(richTextScrollLeft1);
});
test('code hotkey should not effect in global', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await pressEnter(page);
await type(page, '``` ');
await assertTitle(page, '');
await assertBlockCount(page, 'paragraph', 1);
await assertBlockCount(page, 'code', 1);
await pressArrowUp(page);
await pressBackspace(page);
await type(page, 'aaa');
await assertTitle(page, 'aaa');
await assertBlockCount(page, 'paragraph', 0);
await assertBlockCount(page, 'code', 1);
});
test('language selection list should not close when hovering out of code block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page, { language: 'javascript' });
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
await codeBlockController.clickLanguageButton();
const langLocator = codeBlockController.langList;
await expect(langLocator).toBeVisible();
const bBox = await codeBlockController.codeBlock.boundingBox();
if (!bBox) throw new Error('Expected bounding box');
const { x, y, width, height } = bBox;
// hovering inside the code block should keep the list open
await page.mouse.move(x + width / 2, y + height / 2);
await expect(langLocator).toBeVisible();
// hovering out should not close the list
await page.mouse.move(x - 10, y - 10);
await waitNextFrame(page);
await expect(langLocator).toBeVisible();
});
test('language selection list should not change when hovering over its elements', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
await codeBlockController.clickLanguageButton();
await waitNextFrame(page, 100);
const langListLocator = codeBlockController.langList;
const langItemsLocator = langListLocator.locator('icon-button');
// checking first 4 language list items
for (let i = 0; i < 3; i++) {
const item = langItemsLocator.nth(i); // current item in language list
const nextItem = langItemsLocator.nth(i + 1); // next item in language list
await item.hover();
const initialItemText = await item.textContent();
const initialNextItemText = await nextItem.textContent();
await nextItem.hover();
const currentItemText = await item.textContent();
const currentNextItemText = await nextItem.textContent();
// text content should remain unchanged after next item receives focus
expect(initialItemText).toBe(currentItemText);
expect(initialNextItemText).toBe(currentNextItemText);
}
});
test('format text in code block', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '```ts ');
await waitNextFrame(page, 100);
await type(page, 'const aaa = 1000;');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
const line = page.locator('affine-code rich-text v-line > div');
expect(await line.innerText()).toBe('const aaa = 1000;');
const { boldBtn, linkBtn } = getFormatBar(page);
await dragBetweenIndices(page, [0, 1], [0, 2]);
await boldBtn.click();
expect(await line.innerText()).toBe('const aaa = 1000;');
await dragBetweenIndices(page, [0, 4], [0, 7]);
await boldBtn.click();
expect(await line.innerText()).toBe('const aaa = 1000;');
await dragBetweenIndices(page, [0, 8], [0, 16]);
await boldBtn.click();
expect(await line.innerText()).toBe('const aaa = 1000;');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_format.json`
);
await dragBetweenIndices(page, [0, 4], [0, 10]);
await linkBtn.click();
await type(page, 'https://www.baidu.com');
await pressEnter(page);
expect(await line.innerText()).toBe('const aaa = 1000;');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_link.json`
);
});

View File

@@ -0,0 +1,59 @@
import { expect } from '@playwright/test';
import { switchReadonly } from '../utils/actions/click.js';
import {
pressBackspace,
pressEnter,
pressTab,
type,
} from '../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
focusRichText,
focusRichTextEnd,
initEmptyCodeBlockState,
} from '../utils/actions/misc.js';
import { assertRichTexts } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
import { getCodeBlock } from './utils.js';
test('should code block widget be disabled in read only mode', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichTextEnd(page);
await page.waitForTimeout(300);
await switchReadonly(page);
const codeBlockController = getCodeBlock(page);
const codeBlock = codeBlockController.codeBlock;
await codeBlock.hover();
await codeBlockController.clickLanguageButton();
await expect(codeBlockController.langList).toBeHidden();
await codeBlock.hover();
await expect(codeBlockController.codeToolbar).toBeVisible();
await expect(codeBlockController.moreButton).toHaveAttribute('disabled');
await expect(codeBlockController.copyButton).toBeVisible();
await expect(codeBlockController.moreMenu).toBeHidden();
});
test('should not be able to modify code block in readonly mode', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'const a = 10;');
await assertRichTexts(page, ['const a = 10;']);
await switchReadonly(page);
await pressBackspace(page, 3);
await pressTab(page, 3);
await pressEnter(page, 2);
await assertRichTexts(page, ['const a = 10;']);
});

View File

@@ -0,0 +1,195 @@
import { expect } from '@playwright/test';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import {
pressArrowLeft,
pressBackspace,
pressEnter,
pressEnterWithShortkey,
redoByKeyboard,
type,
undoByKeyboard,
} from '../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
focusRichText,
getInlineSelectionIndex,
getInlineSelectionText,
initEmptyCodeBlockState,
} from '../utils/actions/misc.js';
import {
assertBlockCount,
assertBlockSelections,
assertRichTextInlineRange,
assertRichTexts,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
import { getCodeBlock } from './utils.js';
test('click outside should close language list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const codeBlock = getCodeBlock(page);
await codeBlock.clickLanguageButton();
const locator = codeBlock.langList;
await expect(locator).toBeVisible();
const rect = await page.locator('affine-filterable-list').boundingBox();
if (!rect) throw new Error('Failed to get bounding box of code block.');
await page.mouse.click(rect.x - 10, rect.y - 10);
await expect(locator).toBeHidden();
});
test('split code by enter', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'hello');
// he|llo
await pressArrowLeft(page, 3);
await pressEnter(page);
await assertRichTexts(page, ['he\nllo']);
await undoByKeyboard(page);
await assertRichTexts(page, ['hello']);
await redoByKeyboard(page);
await assertRichTexts(page, ['he\nllo']);
});
test('split code with selection by enter', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'hello');
// select 'll'
await pressArrowLeft(page, 1);
await page.keyboard.down('Shift');
await pressArrowLeft(page, 2);
await page.keyboard.up('Shift');
await pressEnter(page);
await assertRichTexts(page, ['he\no']);
await undoByKeyboard(page);
await assertRichTexts(page, ['hello']);
await redoByKeyboard(page);
await assertRichTexts(page, ['he\no']);
});
test('drag select code block can delete it', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const codeBlock = page.locator('affine-code');
const bbox = await codeBlock.boundingBox();
if (!bbox) {
throw new Error("Failed to get code block's bounding box");
}
const position = {
startX: bbox.x - 10,
startY: bbox.y - 10,
endX: bbox.x + bbox.width,
endY: bbox.y + bbox.height / 2,
};
await dragBetweenCoords(
page,
{ x: position.startX, y: position.startY },
{ x: position.endX, y: position.endY },
{ steps: 20 }
);
await page.waitForTimeout(10);
await page.keyboard.press('Backspace');
const locator = page.locator('affine-code');
await expect(locator).toBeHidden();
});
test('press short key and enter at end of code block can jump out', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await pressEnterWithShortkey(page);
const locator = page.locator('affine-paragraph');
await expect(locator).toBeVisible();
});
test('press short key and enter at end of code block with content can jump out', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, 'const a = 10;');
await pressEnterWithShortkey(page);
const locator = page.locator('affine-paragraph');
await expect(locator).toBeVisible();
});
test('press backspace inside should select code block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const codeBlock = page.locator('affine-code');
const selectedRects = page
.locator('affine-block-selection')
.locator('visible=true');
await page.keyboard.press('Backspace');
await expect(selectedRects).toHaveCount(1);
await expect(codeBlock).toBeVisible();
await page.keyboard.press('Backspace');
await expect(selectedRects).toHaveCount(0);
await expect(codeBlock).toBeHidden();
});
test('press backspace after code block can select code block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const code = 'const a = 1;';
await type(page, code);
await assertRichTextInlineRange(page, 0, 12);
await pressEnterWithShortkey(page);
await assertRichTextInlineRange(page, 1, 0);
await assertBlockCount(page, 'paragraph', 1);
await pressBackspace(page);
await assertBlockSelections(page, ['2']);
await assertBlockCount(page, 'paragraph', 0);
});
test('press ArrowUp after code block can enter code block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const code = 'const a = 1;';
await type(page, code);
await pressEnterWithShortkey(page);
await page.keyboard.press('ArrowUp');
const index = await getInlineSelectionIndex(page);
expect(index).toBe(0);
const text = await getInlineSelectionText(page);
expect(text).toBe(code);
});

View File

@@ -0,0 +1,61 @@
import type { Page } from '@playwright/test';
/**
* @example
* ```ts
* const codeBlockController = getCodeBlock(page);
* const codeBlock = codeBlockController.codeBlock;
* ```
*/
export function getCodeBlock(page: Page) {
const codeBlock = page.locator('affine-code');
const languageButton = page.getByTestId('lang-button');
const clickLanguageButton = async () => {
await codeBlock.hover();
await languageButton.click({ delay: 50 });
};
const langList = page.locator('affine-filterable-list');
const langFilterInput = langList.locator('#filter-input');
const codeToolbar = page.locator('affine-code-toolbar');
const copyButton = codeToolbar.getByRole('button', { name: 'Copy code' });
const captionButton = codeToolbar.getByRole('button', { name: 'Caption' });
const moreButton = codeToolbar.getByRole('button', { name: 'More' });
const menu = page.locator('.more-popup-menu');
const openMore = async () => {
await moreButton.click();
const wrapButton = menu.getByRole('button', { name: 'Wrap' });
const cancelWrapButton = menu.getByRole('button', { name: 'Cancel wrap' });
const duplicateButton = menu.getByRole('button', { name: 'Duplicate' });
const deleteButton = menu.getByRole('button', { name: 'Delete' });
return {
menu,
wrapButton,
cancelWrapButton,
duplicateButton,
deleteButton,
};
};
return {
codeBlock,
codeToolbar,
captionButton,
languageButton,
langList,
copyButton,
moreButton,
langFilterInput,
moreMenu: menu,
openMore,
clickLanguageButton,
};
}

View File

@@ -0,0 +1,592 @@
import type {
RichTextCell,
RichTextCellEditing,
} from '@blocksuite/affine-block-database';
import { ZERO_WIDTH_SPACE } from '@blocksuite/inline';
import { expect, type Locator, type Page } from '@playwright/test';
import {
pressEnter,
selectAllByKeyboard,
type,
} from '../utils/actions/keyboard.js';
import {
getBoundingBox,
getBoundingClientRect,
getEditorLocator,
waitNextFrame,
} from '../utils/actions/misc.js';
export async function press(page: Page, content: string) {
await page.keyboard.press(content, { delay: 50 });
await page.waitForTimeout(50);
}
export async function initDatabaseColumn(page: Page, title = '') {
const editor = getEditorLocator(page);
await editor.locator('affine-data-view-table-group').first().hover();
const columnAddBtn = editor.locator('.header-add-column-button');
await columnAddBtn.click();
await waitNextFrame(page, 200);
if (title) {
await selectAllByKeyboard(page);
await type(page, title);
await waitNextFrame(page);
await pressEnter(page);
} else {
await pressEnter(page);
}
}
export const renameColumn = async (page: Page, name: string) => {
const column = page.locator('affine-database-header-column', {
hasText: name,
});
await column.click();
};
export async function performColumnAction(
page: Page,
name: string,
action: string
) {
await renameColumn(page, name);
const actionMenu = page.locator(`.affine-menu-button`, { hasText: action });
await actionMenu.click();
}
export async function switchColumnType(
page: Page,
columnType: string,
columnIndex = 1
) {
const { typeIcon } = await getDatabaseHeaderColumn(page, columnIndex);
await typeIcon.click();
await clickColumnType(page, columnType);
}
export function clickColumnType(page: Page, columnType: string) {
const typeMenu = page.locator(`.affine-menu-button`, {
hasText: new RegExp(`${columnType}`),
});
return typeMenu.click();
}
export function getDatabaseBodyRows(page: Page) {
const rowContainer = page.locator('.affine-database-block-rows');
return rowContainer.locator('.database-row');
}
export function getDatabaseBodyRow(page: Page, rowIndex = 0) {
const rows = getDatabaseBodyRows(page);
return rows.nth(rowIndex);
}
export function getDatabaseTableContainer(page: Page) {
const container = page.locator('.affine-database-table-container');
return container;
}
export async function assertDatabaseTitleColumnText(
page: Page,
title: string,
index = 0
) {
const text = await page.evaluate(index => {
const rowContainer = document.querySelector('.affine-database-block-rows');
const row = rowContainer?.querySelector(
`.database-row:nth-child(${index + 1})`
);
const titleColumnCell = row?.querySelector('.database-cell:nth-child(1)');
const titleSpan = titleColumnCell?.querySelector(
'.data-view-header-area-rich-text'
) as HTMLElement;
if (!titleSpan) throw new Error('Cannot find database title column editor');
return titleSpan.innerText;
}, index);
if (title === '') {
expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_SPACE}])$`));
} else {
expect(text).toBe(title);
}
}
export function getDatabaseBodyCell(
page: Page,
{
rowIndex,
columnIndex,
}: {
rowIndex: number;
columnIndex: number;
}
) {
const row = getDatabaseBodyRow(page, rowIndex);
const cell = row.locator('.database-cell').nth(columnIndex);
return cell;
}
export function getDatabaseBodyCellContent(
page: Page,
{
rowIndex,
columnIndex,
cellClass,
}: {
rowIndex: number;
columnIndex: number;
cellClass: string;
}
) {
const cell = getDatabaseBodyCell(page, { rowIndex, columnIndex });
const cellContent = cell.locator(`.${cellClass}`);
return cellContent;
}
export function getFirstColumnCell(page: Page, cellClass: string) {
const cellContent = getDatabaseBodyCellContent(page, {
rowIndex: 0,
columnIndex: 1,
cellClass,
});
return cellContent;
}
export async function clickSelectOption(page: Page, index = 0) {
await page.locator('.select-option-icon').nth(index).click();
}
export async function performSelectColumnTagAction(
page: Page,
name: string,
index = 0
) {
await clickSelectOption(page, index);
await page
.locator('.affine-menu-button', { hasText: new RegExp(name) })
.click();
}
export async function assertSelectedStyle(
page: Page,
key: keyof CSSStyleDeclaration,
value: string
) {
const style = await getElementStyle(page, '.select-selected', key);
expect(style).toBe(value);
}
export async function clickDatabaseOutside(page: Page) {
const docTitle = page.locator('.doc-title-container');
await docTitle.click();
}
export async function assertColumnWidth(locator: Locator, width: number) {
const box = await getBoundingBox(locator);
expect(box.width).toBe(width + 1);
return box;
}
export async function assertDatabaseCellRichTexts(
page: Page,
{
rowIndex = 0,
columnIndex = 1,
text,
}: {
rowIndex?: number;
columnIndex?: number;
text: string;
}
) {
const cellContainer = page.locator(
`affine-database-cell-container[data-row-index='${rowIndex}'][data-column-index='${columnIndex}']`
);
const cellEditing = cellContainer.locator(
'affine-database-rich-text-cell-editing'
);
const cell = cellContainer.locator('affine-database-rich-text-cell');
const richText = (await cellEditing.count()) === 0 ? cell : cellEditing;
const actualTexts = await richText.evaluate(ele => {
return (ele as RichTextCellEditing).inlineEditor?.yTextString;
});
expect(actualTexts).toEqual(text);
}
export async function assertDatabaseCellNumber(
page: Page,
{
rowIndex = 0,
columnIndex = 1,
text,
}: {
rowIndex?: number;
columnIndex?: number;
text: string;
}
) {
const actualText = await page
.locator('.affine-database-block-rows')
.locator('.database-row')
.nth(rowIndex)
.locator('.database-cell')
.nth(columnIndex)
.locator('.number')
.textContent();
expect(actualText?.trim()).toEqual(text);
}
export async function assertDatabaseCellLink(
page: Page,
{
rowIndex = 0,
columnIndex = 1,
text,
}: {
rowIndex?: number;
columnIndex?: number;
text: string;
}
) {
const actualTexts = await page.evaluate(
({ rowIndex, columnIndex }) => {
const rows = document.querySelector('.affine-database-block-rows');
const row = rows?.querySelector(
`.database-row:nth-child(${rowIndex + 1})`
);
const cell = row?.querySelector(
`.database-cell:nth-child(${columnIndex + 1})`
);
const richText =
cell?.querySelector<RichTextCell>('affine-database-link-cell') ??
cell?.querySelector<RichTextCellEditing>(
'affine-database-link-cell-editing'
);
if (!richText) throw new Error('Missing database rich text cell');
return richText.inlineEditor!.yText.toString();
},
{ rowIndex, columnIndex }
);
expect(actualTexts).toEqual(text);
}
export async function assertDatabaseTitleText(page: Page, text: string) {
const dbTitle = page.locator('[data-block-is-database-title="true"]');
expect(await dbTitle.inputValue()).toEqual(text);
}
export async function waitSearchTransitionEnd(page: Page) {
await waitNextFrame(page, 400);
}
export async function assertDatabaseSearching(
page: Page,
isSearching: boolean
) {
const searchExpand = page.locator('.search-container-expand');
const count = await searchExpand.count();
expect(count).toBe(isSearching ? 1 : 0);
}
export async function focusDatabaseSearch(page: Page) {
await (await getDatabaseMouse(page)).mouseOver();
const searchExpand = page.locator('.search-container-expand');
const count = await searchExpand.count();
if (count === 1) {
const input = page.locator('.affine-database-search-input');
await input.click();
} else {
const searchIcon = page.locator('.affine-database-search-input-icon');
await searchIcon.click();
await waitSearchTransitionEnd(page);
}
}
export async function blurDatabaseSearch(page: Page) {
const dbTitle = page.locator('[data-block-is-database-title="true"]');
await dbTitle.click();
}
export async function focusDatabaseHeader(page: Page, columnIndex = 0) {
const column = page.locator('.affine-database-column').nth(columnIndex);
const box = await getBoundingBox(column);
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await waitNextFrame(page);
return column;
}
export async function getDatabaseMouse(page: Page) {
const databaseRect = await getBoundingClientRect(
page,
'.affine-database-table'
);
return {
mouseOver: async () => {
await page.mouse.move(databaseRect.x, databaseRect.y);
},
mouseLeave: async () => {
await page.mouse.move(databaseRect.x - 1, databaseRect.y - 1);
},
};
}
export async function getDatabaseHeaderColumn(page: Page, index = 0) {
const column = page.locator('.affine-database-column').nth(index);
const box = await getBoundingBox(column);
const textElement = column.locator('.affine-database-column-text-input');
const text = await textElement.innerText();
const typeIcon = column.locator('.affine-database-column-type-icon');
return {
column,
box,
text,
textElement,
typeIcon,
};
}
export async function assertRowsSelection(
page: Page,
rowIndexes: [start: number, end: number]
) {
const rows = page.locator('data-view-table-row');
const startIndex = rowIndexes[0];
const endIndex = rowIndexes[1];
for (let i = startIndex; i <= endIndex; i++) {
const row = rows.nth(i);
await row.locator('.row-select-checkbox .selected').isVisible();
}
}
export async function assertCellsSelection(
page: Page,
cellIndexes: {
start: [rowIndex: number, columnIndex: number];
end?: [rowIndex: number, columnIndex: number];
}
) {
const { start, end } = cellIndexes;
if (!end) {
// single cell
const focus = page.locator('.database-focus');
const focusBox = await getBoundingBox(focus);
const [rowIndex, columnIndex] = start;
const cell = getDatabaseBodyCell(page, { rowIndex, columnIndex });
const cellBox = await getBoundingBox(cell);
expect(focusBox).toEqual({
x: cellBox.x,
y: cellBox.y - 1,
height: cellBox.height + 2,
width: cellBox.width + 1,
});
} else {
// multi cells
const selection = page.locator('.database-selection');
const selectionBox = await getBoundingBox(selection);
const [startRowIndex, startColumnIndex] = start;
const [endRowIndex, endColumnIndex] = end;
const rowIndexStart = Math.min(startRowIndex, endRowIndex);
const rowIndexEnd = Math.max(startRowIndex, endRowIndex);
const columnIndexStart = Math.min(startColumnIndex, endColumnIndex);
const columnIndexEnd = Math.max(startColumnIndex, endColumnIndex);
let height = 0;
let width = 0;
let x = 0;
let y = 0;
for (let i = rowIndexStart; i <= rowIndexEnd; i++) {
const cell = getDatabaseBodyCell(page, {
rowIndex: i,
columnIndex: columnIndexStart,
});
const box = await getBoundingBox(cell);
height += box.height + 1;
if (i === rowIndexStart) {
y = box.y;
}
}
for (let j = columnIndexStart; j <= columnIndexEnd; j++) {
const cell = getDatabaseBodyCell(page, {
rowIndex: rowIndexStart,
columnIndex: j,
});
const box = await getBoundingBox(cell);
width += box.width;
if (j === columnIndexStart) {
x = box.x;
}
}
expect(selectionBox).toEqual({
x,
y,
height,
width: width + 1,
});
}
}
export async function getElementStyle(
page: Page,
selector: string,
key: keyof CSSStyleDeclaration
) {
const style = await page.evaluate(
({ key, selector }) => {
const el = document.querySelector<HTMLElement>(selector);
if (!el) throw new Error(`Missing ${selector} tag`);
// @ts-ignore
return el.style[key];
},
{
key,
selector,
}
);
return style;
}
export async function focusKanbanCardHeader(page: Page, index = 0) {
const cardHeader = page.locator('data-view-header-area-text').nth(index);
await cardHeader.click();
}
export async function clickKanbanCardHeader(page: Page, index = 0) {
const cardHeader = page.locator('data-view-header-area-text').nth(index);
await cardHeader.click();
await cardHeader.click();
}
export async function assertKanbanCardHeaderText(
page: Page,
text: string,
index = 0
) {
const cardHeader = page.locator('data-view-header-area-text').nth(index);
await expect(cardHeader).toHaveText(text);
}
export async function assertKanbanCellSelected(
page: Page,
{
groupIndex,
cardIndex,
cellIndex,
}: {
groupIndex: number;
cardIndex: number;
cellIndex: number;
}
) {
const border = await page.evaluate(
({ groupIndex, cardIndex, cellIndex }) => {
const group = document.querySelector(
`affine-data-view-kanban-group:nth-child(${groupIndex + 1})`
);
const card = group?.querySelector(
`affine-data-view-kanban-card:nth-child(${cardIndex + 1})`
);
const cells = Array.from(
card?.querySelectorAll<HTMLElement>(`affine-data-view-kanban-cell`) ??
[]
);
const cell = cells[cellIndex];
if (!cell) throw new Error(`Missing cell tag`);
return cell.style.border;
},
{
groupIndex,
cardIndex,
cellIndex,
}
);
expect(border).toEqual('1px solid var(--affine-primary-color)');
}
export async function assertKanbanCardSelected(
page: Page,
{
groupIndex,
cardIndex,
}: {
groupIndex: number;
cardIndex: number;
}
) {
const border = await page.evaluate(
({ groupIndex, cardIndex }) => {
const group = document.querySelector(
`affine-data-view-kanban-group:nth-child(${groupIndex + 1})`
);
const card = group?.querySelector<HTMLElement>(
`affine-data-view-kanban-card:nth-child(${cardIndex + 1})`
);
if (!card) throw new Error(`Missing card tag`);
return card.style.border;
},
{
groupIndex,
cardIndex,
}
);
expect(border).toEqual('1px solid var(--affine-primary-color)');
}
export function getKanbanCard(
page: Page,
{
groupIndex,
cardIndex,
}: {
groupIndex: number;
cardIndex: number;
}
) {
const group = page.locator('affine-data-view-kanban-group').nth(groupIndex);
const card = group.locator('affine-data-view-kanban-card').nth(cardIndex);
return card;
}
export const moveToCenterOf = async (page: Page, locator: Locator) => {
const box = (await locator.boundingBox())!;
expect(box).toBeDefined();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
};
export const changeColumnType = async (
page: Page,
column: number,
name: string
) => {
await waitNextFrame(page);
await page.locator('affine-database-header-column').nth(column).click();
await waitNextFrame(page, 200);
await pressKey(page, 'Escape');
await pressKey(page, 'ArrowDown');
await pressKey(page, 'Enter');
await type(page, name);
await pressKey(page, 'ArrowDown');
await pressKey(page, 'Enter');
};
export const pressKey = async (page: Page, key: string, count: number = 1) => {
for (let i = 0; i < count; i++) {
await waitNextFrame(page);
await press(page, key);
}
await waitNextFrame(page);
};

View File

@@ -0,0 +1,176 @@
import { expect } from '@playwright/test';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import {
copyByKeyboard,
pasteByKeyboard,
pressArrowDown,
pressArrowUp,
pressEnter,
pressEscape,
type,
} from '../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
focusRichText,
getBoundingBox,
initDatabaseDynamicRowWithData,
initDatabaseRowWithData,
initEmptyDatabaseState,
initEmptyDatabaseWithParagraphState,
waitNextFrame,
} from '../utils/actions/misc.js';
import { assertRichTexts } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
import {
assertDatabaseTitleColumnText,
getDatabaseBodyCell,
getElementStyle,
initDatabaseColumn,
switchColumnType,
} from './actions.js';
test.describe('copy&paste when editing', () => {
test.skip('should support copy&paste of the title column', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseWithParagraphState(page);
await initDatabaseColumn(page);
await initDatabaseRowWithData(page, 'abc123');
await pressEscape(page);
await pressEnter(page);
await page.keyboard.down('Shift');
for (let i = 0; i < 4; i++) {
await page.keyboard.press('ArrowLeft');
}
await page.keyboard.up('Shift');
await copyByKeyboard(page);
const bgValue = await getElementStyle(page, '.database-focus', 'boxShadow');
expect(bgValue).not.toBe('unset');
await focusRichText(page, 1);
await pasteByKeyboard(page);
await assertRichTexts(page, ['Database 1', 'c123']);
});
});
test.describe('copy&paste when selecting', () => {
test.skip('should support copy&paste of a single cell', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseRowWithData(page, 'abc123');
await initDatabaseRowWithData(page, '');
await pressEscape(page);
await waitNextFrame(page, 100);
await pressArrowUp(page);
await copyByKeyboard(page);
await pressArrowDown(page);
await pasteByKeyboard(page, false);
await waitNextFrame(page);
await assertDatabaseTitleColumnText(page, 'abc123', 1);
});
test.skip('should support copy&paste of multi cells', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseRowWithData(page, 'text1');
await initDatabaseDynamicRowWithData(page, '123', false);
await pressEscape(page);
await initDatabaseRowWithData(page, 'text2');
await initDatabaseDynamicRowWithData(page, 'a', false);
await pressEscape(page);
await initDatabaseRowWithData(page, '');
await initDatabaseRowWithData(page, '');
const startCell = getDatabaseBodyCell(page, {
rowIndex: 0,
columnIndex: 0,
});
const startCellBox = await getBoundingBox(startCell);
const endCell = getDatabaseBodyCell(page, { rowIndex: 1, columnIndex: 1 });
const endCellBox = await getBoundingBox(endCell);
const startX = startCellBox.x + startCellBox.width / 2;
const startY = startCellBox.y + startCellBox.height / 2;
const endX = endCellBox.x + endCellBox.width / 2;
const endY = endCellBox.y + endCellBox.height / 2;
await dragBetweenCoords(
page,
{ x: startX, y: startY },
{ x: endX, y: endY },
{
steps: 50,
}
);
await copyByKeyboard(page);
await pressArrowDown(page);
await pressArrowDown(page);
await waitNextFrame(page);
await pasteByKeyboard(page, false);
await assertDatabaseTitleColumnText(page, 'text1', 2);
await assertDatabaseTitleColumnText(page, 'text2', 3);
const selectCell21 = getDatabaseBodyCell(page, {
rowIndex: 2,
columnIndex: 1,
});
const selectCell31 = getDatabaseBodyCell(page, {
rowIndex: 3,
columnIndex: 1,
});
expect(await selectCell21.innerText()).toBe('123');
expect(await selectCell31.innerText()).toBe('a');
});
test.skip('should support copy&paste of a single row', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseRowWithData(page, 'text1');
await pressEscape(page);
await waitNextFrame(page, 100);
await initDatabaseDynamicRowWithData(page, 'abc', false);
await pressEscape(page);
await waitNextFrame(page, 100);
await initDatabaseColumn(page);
await switchColumnType(page, 'Number', 2);
const numberCell = getDatabaseBodyCell(page, {
rowIndex: 0,
columnIndex: 2,
});
await numberCell.click();
await waitNextFrame(page);
await type(page, '123');
await pressEscape(page);
await pressEscape(page);
await copyByKeyboard(page);
await initDatabaseRowWithData(page, '');
await pressEscape(page);
await pasteByKeyboard(page, false);
await waitNextFrame(page);
await assertDatabaseTitleColumnText(page, 'text1', 1);
const selectCell = getDatabaseBodyCell(page, {
rowIndex: 1,
columnIndex: 1,
});
expect(await selectCell.innerText()).toBe('abc');
const selectNumberCell = getDatabaseBodyCell(page, {
rowIndex: 1,
columnIndex: 2,
});
expect(await selectNumberCell.innerText()).toBe('123');
});
});

View File

@@ -0,0 +1,599 @@
import { expect } from '@playwright/test';
import {
assertDatabaseColumnOrder,
dragBetweenCoords,
enterPlaygroundRoom,
getBoundingBox,
initDatabaseDynamicRowWithData,
initEmptyDatabaseState,
pressArrowRight,
pressArrowUp,
pressArrowUpWithShiftKey,
pressBackspace,
pressEnter,
pressEscape,
redoByClick,
selectAllByKeyboard,
type,
undoByClick,
waitNextFrame,
} from '../utils/actions/index.js';
import { test } from '../utils/playwright.js';
import {
assertDatabaseCellNumber,
assertDatabaseCellRichTexts,
assertSelectedStyle,
changeColumnType,
clickDatabaseOutside,
clickSelectOption,
getDatabaseHeaderColumn,
getFirstColumnCell,
initDatabaseColumn,
performColumnAction,
performSelectColumnTagAction,
switchColumnType,
} from './actions.js';
test.describe('column operations', () => {
test('should support rename column', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, 'abc');
const { textElement } = await getDatabaseHeaderColumn(page, 1);
expect(await textElement.innerText()).toBe('abc');
await textElement.click();
await waitNextFrame(page, 200);
await pressArrowRight(page);
await type(page, '123');
await pressEnter(page);
expect(await textElement.innerText()).toBe('abc123');
await undoByClick(page);
expect(await textElement.innerText()).toBe('abc');
await redoByClick(page);
expect(await textElement.innerText()).toBe('abc123');
});
test('should support add new column', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await pressEscape(page);
const { text: title1 } = await getDatabaseHeaderColumn(page, 1);
expect(title1).toBe('Column 1');
const selected = getFirstColumnCell(page, 'select-selected');
expect(await selected.innerText()).toBe('123');
await initDatabaseColumn(page, 'abc');
const { text: title2 } = await getDatabaseHeaderColumn(page, 2);
expect(title2).toBe('abc');
await initDatabaseColumn(page);
const { text: title3 } = await getDatabaseHeaderColumn(page, 3);
expect(title3).toBe('Column 2');
});
test('should support right insert column', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, '1');
await performColumnAction(page, '1', 'Insert right');
await selectAllByKeyboard(page);
await type(page, '2');
await pressEnter(page);
const columns = page.locator('.affine-database-column');
expect(await columns.count()).toBe(3);
await assertDatabaseColumnOrder(page, ['1', '2']);
});
test('should support left insert column', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, '1');
await performColumnAction(page, '1', 'Insert left');
await selectAllByKeyboard(page);
await type(page, '2');
await pressEnter(page);
const columns = page.locator('.affine-database-column');
expect(await columns.count()).toBe(3);
await assertDatabaseColumnOrder(page, ['2', '1']);
});
test('should support delete column', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, '1');
const columns = page.locator('.affine-database-column');
expect(await columns.count()).toBe(2);
await performColumnAction(page, '1', 'Delete');
expect(await columns.count()).toBe(1);
});
test('should support duplicate column', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, '1');
await initDatabaseDynamicRowWithData(page, '123', true);
await pressEscape(page);
await performColumnAction(page, '1', 'duplicate');
await pressEscape(page);
const cells = page.locator('affine-database-multi-select-cell');
expect(await cells.count()).toBe(2);
const secondCell = cells.nth(1);
const selected = secondCell.locator('.select-selected');
expect(await selected.innerText()).toBe('123');
});
test('should support move column right', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, '1');
await initDatabaseDynamicRowWithData(page, '123', true);
await pressEscape(page);
await initDatabaseColumn(page, '2');
await initDatabaseDynamicRowWithData(page, 'abc', false, 1);
await pressEscape(page);
await assertDatabaseColumnOrder(page, ['1', '2']);
await waitNextFrame(page, 350);
await performColumnAction(page, '1', 'Move right');
await assertDatabaseColumnOrder(page, ['2', '1']);
await undoByClick(page);
const { column } = await getDatabaseHeaderColumn(page, 2);
await column.click();
const moveLeft = page.locator('.action', { hasText: 'Move right' });
expect(await moveLeft.count()).toBe(0);
});
test('should support move column left', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, '1');
await initDatabaseDynamicRowWithData(page, '123', true);
await pressEscape(page);
await initDatabaseColumn(page, '2');
await initDatabaseDynamicRowWithData(page, 'abc', false, 1);
await pressEscape(page);
await assertDatabaseColumnOrder(page, ['1', '2']);
const { column } = await getDatabaseHeaderColumn(page, 0);
await column.click();
const moveLeft = page.locator('.action', { hasText: 'Move left' });
expect(await moveLeft.count()).toBe(0);
await waitNextFrame(page, 200);
await pressEscape(page);
await pressEscape(page);
await performColumnAction(page, '2', 'Move left');
await assertDatabaseColumnOrder(page, ['2', '1']);
});
});
test.describe('switch column type', () => {
test('switch to number', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123abc', true);
await pressEscape(page);
await changeColumnType(page, 1, 'Number');
const cell = getFirstColumnCell(page, 'number');
await assertDatabaseCellNumber(page, {
text: '',
});
await pressEnter(page);
await type(page, '123abc');
await pressEscape(page);
expect((await cell.textContent())?.trim()).toBe('123');
});
test('switch to rich-text', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123abc', true);
await pressEscape(page);
await switchColumnType(page, 'Text');
// For now, rich-text will only be initialized on click
// Therefore, for the time being, here is to detect whether there is '.affine-database-rich-text'
const cell = getFirstColumnCell(page, 'affine-database-rich-text');
expect(await cell.count()).toBe(1);
await pressEnter(page);
await type(page, '123');
await pressEscape(page);
await pressEnter(page);
await type(page, 'abc');
await pressEscape(page);
await assertDatabaseCellRichTexts(page, { text: '123abc123abc' });
});
test('switch between multi-select and select', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await type(page, 'abc');
await pressEnter(page);
await pressEscape(page);
const cell = getFirstColumnCell(page, 'select-selected');
expect(await cell.count()).toBe(2);
await switchColumnType(page, 'Select', 1);
expect(await cell.count()).toBe(1);
expect(await cell.innerText()).toBe('123');
await pressEnter(page);
await type(page, 'def');
await pressEnter(page);
expect(await cell.innerText()).toBe('def');
await switchColumnType(page, 'Multi-select');
await pressEnter(page);
await type(page, '666');
await pressEnter(page);
await pressEscape(page);
expect(await cell.count()).toBe(2);
expect(await cell.nth(0).innerText()).toBe('def');
expect(await cell.nth(1).innerText()).toBe('666');
await switchColumnType(page, 'Select');
expect(await cell.count()).toBe(1);
expect(await cell.innerText()).toBe('def');
await pressEnter(page);
await type(page, '888');
await pressEnter(page);
expect(await cell.innerText()).toBe('888');
});
test('switch between number and rich-text', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '123abc', true);
await assertDatabaseCellNumber(page, {
text: '123',
});
await switchColumnType(page, 'Text');
await pressEnter(page);
await type(page, 'abc');
await pressEscape(page);
await assertDatabaseCellRichTexts(page, { text: '123abc' });
await switchColumnType(page, 'Number');
await assertDatabaseCellNumber(page, {
text: '123',
});
});
test('switch number to select', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '123', true);
const cell = getFirstColumnCell(page, 'number');
expect((await cell.textContent())?.trim()).toBe('123');
await switchColumnType(page, 'Select');
await initDatabaseDynamicRowWithData(page, 'abc');
const selectCell = getFirstColumnCell(page, 'select-selected');
expect(await selectCell.innerText()).toBe('abc');
await switchColumnType(page, 'Number');
await assertDatabaseCellNumber(page, {
text: '',
});
});
test('switch to checkbox', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await changeColumnType(page, 1, 'Checkbox');
const checkbox = getFirstColumnCell(page, 'checkbox');
await expect(checkbox).not.toHaveClass('checked');
await waitNextFrame(page, 500);
await checkbox.click();
await expect(checkbox).toHaveClass(/checked/);
await undoByClick(page);
await expect(checkbox).not.toHaveClass('checked');
});
test('checkbox to text', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await changeColumnType(page, 1, 'Checkbox');
let checkbox = getFirstColumnCell(page, 'checkbox');
await expect(checkbox).not.toHaveClass('checked');
// checked
await checkbox.click();
await changeColumnType(page, 1, 'Text');
await clickDatabaseOutside(page);
await waitNextFrame(page, 100);
await assertDatabaseCellRichTexts(page, { text: 'Yes' });
await clickDatabaseOutside(page);
await waitNextFrame(page, 100);
await changeColumnType(page, 1, 'Checkbox');
checkbox = getFirstColumnCell(page, 'checkbox');
await expect(checkbox).toHaveClass(/checked/);
// not checked
await checkbox.click();
await changeColumnType(page, 1, 'Text');
await clickDatabaseOutside(page);
await waitNextFrame(page, 100);
await assertDatabaseCellRichTexts(page, { text: 'No' });
await clickDatabaseOutside(page);
await waitNextFrame(page, 100);
await changeColumnType(page, 1, 'Checkbox');
checkbox = getFirstColumnCell(page, 'checkbox');
await expect(checkbox).not.toHaveClass('checked');
});
test('switch to progress', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await switchColumnType(page, 'Progress');
const progress = getFirstColumnCell(page, 'progress');
expect(await progress.textContent()).toBe('0');
await waitNextFrame(page, 500);
const progressBg = page.locator('.affine-database-progress-bg');
const {
x: progressBgX,
y: progressBgY,
width: progressBgWidth,
} = await getBoundingBox(progressBg);
await page.mouse.move(progressBgX, progressBgY);
await page.mouse.click(progressBgX, progressBgY);
const dragHandle = page.locator('.affine-database-progress-drag-handle');
const {
x: dragX,
y: dragY,
width,
height,
} = await getBoundingBox(dragHandle);
const dragCenterX = dragX + width / 2;
const dragCenterY = dragY + height / 2;
await page.mouse.move(dragCenterX, dragCenterY);
const endX = dragCenterX + progressBgWidth;
await dragBetweenCoords(
page,
{ x: dragCenterX, y: dragCenterY },
{ x: endX, y: dragCenterY }
);
expect(await progress.textContent()).toBe('100');
await pressEscape(page);
await undoByClick(page);
expect(await progress.textContent()).toBe('0');
});
test('switch to link', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await switchColumnType(page, 'Link');
const linkText = 'http://example.com';
const cell = getFirstColumnCell(page, 'affine-database-link');
await pressEnter(page);
await type(page, linkText);
await pressEscape(page);
const link = cell.locator('affine-database-link-node > a');
const linkContent = link.locator('.link-node-text');
await expect(link).toHaveAttribute('href', linkText);
expect(await linkContent.textContent()).toBe(linkText);
// not link text
await cell.hover();
const linkEdit = getFirstColumnCell(page, 'affine-database-link-icon');
await linkEdit.click();
await selectAllByKeyboard(page);
await type(page, 'abc');
await pressEnter(page);
await expect(link).toBeHidden();
});
});
test.describe('select column tag action', () => {
test('should support select tag renaming', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await type(page, 'abc');
await pressEnter(page);
await clickSelectOption(page);
await waitNextFrame(page);
await pressArrowRight(page);
await type(page, '4567abc00');
await pressEnter(page);
const options = page.locator('.select-options-container .tag-text');
expect(await options.nth(0).innerText()).toBe('abc4567abc00');
expect(await options.nth(1).innerText()).toBe('123');
});
test('should select tag renaming support shortcut key', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await clickSelectOption(page);
await waitNextFrame(page);
await pressArrowRight(page);
await type(page, '456');
// esc
await pressEscape(page);
await pressEscape(page);
const options = page.locator('.select-options-container .tag-text');
const option1 = options.nth(0);
expect(await option1.innerText()).toBe('123456');
});
test('should support select tag deletion', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await performSelectColumnTagAction(page, 'Delete');
const options = page.locator('.select-option-name');
expect(await options.count()).toBe(0);
});
test('should support modifying select tag color', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await performSelectColumnTagAction(page, 'Red');
await pressEscape(page);
await assertSelectedStyle(
page,
'backgroundColor',
'var(--affine-v2-chip-label-red)'
);
});
});
test.describe('drag-to-fill', () => {
test('should show when cell in focus and hide on blur', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
const dragToFillHandle = page.locator('.drag-to-fill');
await expect(dragToFillHandle).toBeVisible();
await pressEscape(page);
await expect(dragToFillHandle).toBeHidden();
});
test('should not show in multi (row or column) selection', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
const dragToFillHandle = page.locator('.drag-to-fill');
await expect(dragToFillHandle).toBeVisible();
await pressArrowUpWithShiftKey(page);
await expect(dragToFillHandle).toBeHidden();
await pressArrowUp(page);
await expect(dragToFillHandle).toBeVisible();
});
test('should fill columns with data', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, 'thing', true);
await pressEscape(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressBackspace(page);
await type(page, 'aaa');
await pressEnter(page);
await pressEnter(page);
await pressEscape(page);
await pressArrowUp(page);
const cells = page.locator('affine-database-multi-select-cell');
expect(await cells.nth(0).innerText()).toBe('thing');
expect(await cells.nth(1).innerText()).toBe('aaa');
const dragToFillHandle = page.locator('.drag-to-fill');
await expect(dragToFillHandle).toBeVisible();
const bbox = await getBoundingBox(dragToFillHandle);
if (!bbox) throw new Error('Expected a bounding box');
await dragBetweenCoords(
page,
{ x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 },
{ x: bbox.x, y: bbox.y + 200 }
);
expect(await cells.nth(0).innerText()).toBe('thing');
expect(await cells.nth(1).innerText()).toBe('thing');
});
});

View File

@@ -0,0 +1,670 @@
import { expect } from '@playwright/test';
import {
dragBetweenCoords,
enterPlaygroundRoom,
focusDatabaseTitle,
getBoundingBox,
initDatabaseDynamicRowWithData,
initDatabaseRowWithData,
initEmptyDatabaseState,
pressArrowLeft,
pressArrowRight,
pressBackspace,
pressEnter,
pressEscape,
pressShiftEnter,
redoByKeyboard,
selectAllByKeyboard,
setInlineRangeInInlineEditor,
switchReadonly,
type,
undoByClick,
undoByKeyboard,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertBlockProps,
assertInlineEditorDeltas,
assertRowCount,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
import { getFormatBar } from '../utils/query.js';
import {
assertColumnWidth,
assertDatabaseCellRichTexts,
assertDatabaseSearching,
assertDatabaseTitleText,
blurDatabaseSearch,
clickColumnType,
clickDatabaseOutside,
focusDatabaseHeader,
focusDatabaseSearch,
getDatabaseBodyCell,
getDatabaseHeaderColumn,
getFirstColumnCell,
initDatabaseColumn,
switchColumnType,
} from './actions.js';
test('edit database block title and create new rows', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
const locator = page.locator('affine-database');
await expect(locator).toBeVisible();
const dbTitle = 'Database 1';
await assertBlockProps(page, '2', {
title: dbTitle,
});
await focusDatabaseTitle(page);
await selectAllByKeyboard(page);
await pressBackspace(page);
const expected = 'hello';
await type(page, expected);
await assertBlockProps(page, '2', {
title: 'hello',
});
await undoByClick(page);
await assertBlockProps(page, '2', {
title: 'Database 1',
});
await initDatabaseRowWithData(page, '');
await initDatabaseRowWithData(page, '');
await assertRowCount(page, 2);
await waitNextFrame(page, 100);
await pressEscape(page);
await undoByClick(page);
await undoByClick(page);
await assertRowCount(page, 0);
});
test('edit column title', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, '1');
// first added column
const { column } = await getDatabaseHeaderColumn(page, 1);
expect(await column.innerText()).toBe('1');
await undoByClick(page);
expect(await column.innerText()).toBe('Column 1');
});
test('should modify the value when the input loses focus', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '1', true);
await clickDatabaseOutside(page);
const cell = getFirstColumnCell(page, 'number');
const text = await cell.textContent();
expect(text?.trim()).toBe('1');
});
test('should rich-text column support soft enter', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, '123', true);
const cell = getFirstColumnCell(page, 'affine-database-rich-text');
await cell.click();
await pressArrowLeft(page);
await pressEnter(page);
await assertDatabaseCellRichTexts(page, { text: '123' });
await cell.click();
await pressArrowRight(page);
await pressArrowLeft(page);
await pressShiftEnter(page);
await pressEnter(page);
await assertDatabaseCellRichTexts(page, { text: '12\n3' });
});
test('should the multi-select mode work correctly', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '1', true);
await pressEscape(page);
await initDatabaseDynamicRowWithData(page, '2');
await pressEscape(page);
const cell = getFirstColumnCell(page, 'select-selected');
expect(await cell.count()).toBe(2);
expect(await cell.nth(0).innerText()).toBe('1');
expect(await cell.nth(1).innerText()).toBe('2');
});
test('should database search work', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseRowWithData(page, 'text1');
await initDatabaseDynamicRowWithData(page, '123', false);
await pressEscape(page);
await initDatabaseRowWithData(page, 'text2');
await initDatabaseDynamicRowWithData(page, 'a', false);
await pressEscape(page);
await initDatabaseRowWithData(page, 'text3');
await initDatabaseDynamicRowWithData(page, '26', false);
await pressEscape(page);
// search for '2'
await focusDatabaseSearch(page);
await type(page, '2');
const rows = page.locator('.affine-database-block-row');
expect(await rows.count()).toBe(3);
// search for '23'
await type(page, '3');
expect(await rows.count()).toBe(1);
const cell = page.locator('.select-selected');
expect(await cell.innerText()).toBe('123');
// clear search input
const closeIcon = page.locator('.close-icon');
await closeIcon.click();
expect(await rows.count()).toBe(3);
});
test('should database search input displayed correctly', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await focusDatabaseSearch(page);
await blurDatabaseSearch(page);
await assertDatabaseSearching(page, false);
await focusDatabaseSearch(page);
await type(page, '2');
await blurDatabaseSearch(page);
await assertDatabaseSearching(page, true);
await focusDatabaseSearch(page);
await pressBackspace(page);
await blurDatabaseSearch(page);
await assertDatabaseSearching(page, false);
await focusDatabaseSearch(page);
await type(page, '2');
const closeIcon = page.locator('.close-icon');
await closeIcon.click();
await blurDatabaseSearch(page);
await assertDatabaseSearching(page, false);
await focusDatabaseSearch(page);
await type(page, '2');
await pressEscape(page);
await blurDatabaseSearch(page);
await assertDatabaseSearching(page, false);
});
test('should database title and rich-text support undo/redo', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, '123', true);
await undoByKeyboard(page);
await assertDatabaseCellRichTexts(page, { text: '' });
await pressEscape(page);
await redoByKeyboard(page);
await assertDatabaseCellRichTexts(page, { text: '123' });
await focusDatabaseTitle(page);
await type(page, 'abc');
await assertDatabaseTitleText(page, 'Database 1abc');
await undoByKeyboard(page);
await assertDatabaseTitleText(page, 'Database 1');
await redoByKeyboard(page);
await assertDatabaseTitleText(page, 'Database 1abc');
});
test('should support drag to change column width', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
const headerColumns = page.locator('.affine-database-column');
const titleColumn = headerColumns.nth(0);
const normalColumn = headerColumns.nth(1);
const dragDistance = 100;
const titleColumnWidth = 260;
const normalColumnWidth = 180;
await assertColumnWidth(titleColumn, titleColumnWidth - 1);
const box = await assertColumnWidth(normalColumn, normalColumnWidth - 1);
await dragBetweenCoords(
page,
{ x: box.x, y: box.y },
{ x: box.x + dragDistance, y: box.y },
{
steps: 50,
beforeMouseUp: async () => {
await waitNextFrame(page);
},
}
);
await assertColumnWidth(titleColumn, titleColumnWidth + dragDistance);
await assertColumnWidth(normalColumn, normalColumnWidth - 1);
await undoByClick(page);
await assertColumnWidth(titleColumn, titleColumnWidth - 1);
await assertColumnWidth(normalColumn, normalColumnWidth - 1);
});
test('should display the add column button on the right side of database correctly', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
const normalColumn = page.locator('.affine-database-column').nth(1);
const addColumnBtn = page.locator('.header-add-column-button');
const box = await getBoundingBox(normalColumn);
await dragBetweenCoords(
page,
{ x: box.x, y: box.y },
{ x: box.x + 400, y: box.y },
{
steps: 50,
beforeMouseUp: async () => {
await waitNextFrame(page);
},
}
);
await focusDatabaseHeader(page);
await expect(addColumnBtn).toBeVisible();
});
test('should support drag and drop to move columns', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page, 'column1');
await initDatabaseColumn(page, 'column2');
await initDatabaseColumn(page, 'column3');
const column1 = await focusDatabaseHeader(page, 1);
const moveIcon = column1.locator('.affine-database-column-move');
const moveIconBox = await getBoundingBox(moveIcon);
const x = moveIconBox.x + moveIconBox.width / 2;
const y = moveIconBox.y + moveIconBox.height / 2;
await dragBetweenCoords(
page,
{ x, y },
{ x: x + 100, y },
{
steps: 50,
beforeMouseUp: async () => {
await waitNextFrame(page);
const indicator = page.locator('.vertical-indicator').first();
await expect(indicator).toBeVisible();
const { box } = await getDatabaseHeaderColumn(page, 2);
const indicatorBox = await getBoundingBox(indicator);
expect(box.x + box.width - indicatorBox.x < 10).toBe(true);
},
}
);
const { text } = await getDatabaseHeaderColumn(page, 2);
expect(text).toBe('column1');
});
test('should title column support quick renaming', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, 'a', true);
await pressEscape(page);
await focusDatabaseHeader(page, 1);
const { textElement } = await getDatabaseHeaderColumn(page, 1);
await textElement.click();
await waitNextFrame(page);
await selectAllByKeyboard(page);
await type(page, '123');
await pressEnter(page);
expect(await textElement.innerText()).toBe('123');
await undoByClick(page);
expect(await textElement.innerText()).toBe('Column 1');
await textElement.click();
await waitNextFrame(page);
await selectAllByKeyboard(page);
await type(page, '123');
await pressEnter(page);
expect(await textElement.innerText()).toBe('123');
});
test('should title column support quick changing of column type', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, 'a', true);
await pressEscape(page);
await initDatabaseDynamicRowWithData(page, 'b');
await pressEscape(page);
await focusDatabaseHeader(page, 1);
const { typeIcon } = await getDatabaseHeaderColumn(page, 1);
await typeIcon.click();
await waitNextFrame(page);
await clickColumnType(page, 'Select');
const cell = getFirstColumnCell(page, 'select-selected');
expect(await cell.count()).toBe(1);
});
test('database format-bar in header and text column', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, 'column', true);
await pressArrowLeft(page);
await pressEnter(page);
await type(page, 'header');
// Title | Column1
// ----------------
// header | column
const formatBar = getFormatBar(page);
await setInlineRangeInInlineEditor(page, { index: 1, length: 4 }, 1);
expect(await formatBar.formatBar.isVisible()).toBe(true);
// Title | Column1
// ----------------
// h|eade|r | column
await assertInlineEditorDeltas(
page,
[
{
insert: 'header',
},
],
1
);
await formatBar.boldBtn.click();
await assertInlineEditorDeltas(
page,
[
{
insert: 'h',
},
{
insert: 'eade',
attributes: {
bold: true,
},
},
{
insert: 'r',
},
],
1
);
await pressEscape(page);
await pressArrowRight(page);
await pressEnter(page);
await setInlineRangeInInlineEditor(page, { index: 2, length: 2 }, 2);
expect(await formatBar.formatBar.isVisible()).toBe(true);
// Title | Column1
// ----------------
// header | co|lu|mn
await assertInlineEditorDeltas(
page,
[
{
insert: 'column',
},
],
2
);
await formatBar.boldBtn.click();
await assertInlineEditorDeltas(
page,
[
{
insert: 'co',
},
{
insert: 'lu',
attributes: {
bold: true,
},
},
{
insert: 'mn',
},
],
2
);
});
test.describe('readonly mode', () => {
test('database title should not be edited in readonly mode', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
const locator = page.locator('affine-database');
await expect(locator).toBeVisible();
const dbTitle = 'Database 1';
await assertBlockProps(page, '2', {
title: dbTitle,
});
await focusDatabaseTitle(page);
await selectAllByKeyboard(page);
await pressBackspace(page);
await type(page, 'hello');
await assertBlockProps(page, '2', {
title: 'hello',
});
await switchReadonly(page);
await type(page, ' world');
await assertBlockProps(page, '2', {
title: 'hello',
});
await pressBackspace(page, 'hello world'.length);
await assertBlockProps(page, '2', {
title: 'hello',
});
});
test('should rich-text not be edited in readonly mode', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, '', true);
const cell = getFirstColumnCell(page, 'affine-database-rich-text');
await cell.click();
await type(page, '123');
await assertDatabaseCellRichTexts(page, { text: '123' });
await switchReadonly(page);
await pressBackspace(page);
await type(page, '789');
await assertDatabaseCellRichTexts(page, { text: '123' });
});
test('should hide edit widget after switch to readonly mode', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, '', true);
const database = page.locator('affine-database');
await expect(database).toBeVisible();
const databaseMenu = database.locator('.database-ops');
await expect(databaseMenu).toBeVisible();
const addViewButton = database.getByTestId('database-add-view-button');
await expect(addViewButton).toBeVisible();
const titleHeader = page.locator('affine-database-header-column').filter({
hasText: 'Title',
});
await titleHeader.hover();
const columnDragBar = titleHeader.locator('.control-r');
await expect(columnDragBar).toBeVisible();
const filter = database.locator('data-view-header-tools-filter');
const search = database.locator('data-view-header-tools-search');
const options = database.locator('data-view-header-tools-view-options');
const headerAddRow = database.locator('data-view-header-tools-add-row');
await database.hover();
await expect(filter).toBeVisible();
await expect(search).toBeVisible();
await expect(options).toBeVisible();
await expect(headerAddRow).toBeVisible();
const row = database.locator('data-view-table-row');
const rowOptions = row.locator('.row-op');
const rowDragBar = row.locator('.data-view-table-view-drag-handler>div');
await row.hover();
await expect(rowOptions).toHaveCount(2);
await expect(rowOptions.nth(0)).toBeVisible();
await expect(rowOptions.nth(1)).toBeVisible();
await expect(rowDragBar).toBeVisible();
const addRow = database.locator('.data-view-table-group-add-row');
await expect(addRow).toBeVisible();
// Readonly Mode
{
await switchReadonly(page);
await expect(databaseMenu).toBeHidden();
await expect(addViewButton).toBeHidden();
await titleHeader.hover();
await expect(columnDragBar).toBeHidden();
await database.hover();
await expect(filter).toBeHidden();
await expect(search).toBeVisible(); // Note the search should not be hidden
await expect(options).toBeHidden();
await expect(headerAddRow).toBeHidden();
await row.hover();
await expect(rowOptions.nth(0)).toBeHidden();
await expect(rowOptions.nth(1)).toBeHidden();
await expect(rowDragBar).toBeHidden();
await expect(addRow).toBeHidden();
}
});
test('should hide focus border after switch to readonly mode', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, '', true);
const database = page.locator('affine-database');
await expect(database).toBeVisible();
const cell = getFirstColumnCell(page, 'affine-database-rich-text');
await cell.click();
const focusBorder = database.locator(
'data-view-table-selection .database-focus'
);
await expect(focusBorder).toBeVisible();
await switchReadonly(page);
await expect(focusBorder).toBeHidden();
});
test('should hide selection after switch to readonly mode', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, '', true);
const database = page.locator('affine-database');
await expect(database).toBeVisible();
const startCell = getDatabaseBodyCell(page, {
rowIndex: 0,
columnIndex: 0,
});
const endCell = getDatabaseBodyCell(page, {
rowIndex: 0,
columnIndex: 1,
});
const startBox = await getBoundingBox(startCell);
const endBox = await getBoundingBox(endCell);
const startX = startBox.x + startBox.width / 2;
const startY = startBox.y + startBox.height / 2;
const endX = endBox.x + endBox.width / 2;
const endY = endBox.y + endBox.height / 2;
await dragBetweenCoords(
page,
{ x: startX, y: startY },
{ x: endX, y: endY }
);
const selection = database.locator(
'data-view-table-selection .database-selection'
);
await expect(selection).toBeVisible();
await switchReadonly(page);
await expect(selection).toBeHidden();
});
});

View File

@@ -0,0 +1,567 @@
import { expect } from '@playwright/test';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import { shiftClick } from '../utils/actions/edgeless.js';
import {
pressArrowDown,
pressArrowDownWithShiftKey,
pressArrowLeft,
pressArrowRight,
pressArrowUp,
pressArrowUpWithShiftKey,
pressBackspace,
pressEnter,
pressEscape,
type,
} from '../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
getBoundingBox,
initDatabaseDynamicRowWithData,
initDatabaseRowWithData,
initEmptyDatabaseState,
initKanbanViewState,
waitNextFrame,
} from '../utils/actions/misc.js';
import { test } from '../utils/playwright.js';
import {
assertCellsSelection,
assertDatabaseTitleColumnText,
assertKanbanCardHeaderText,
assertKanbanCardSelected,
assertKanbanCellSelected,
assertRowsSelection,
clickKanbanCardHeader,
focusKanbanCardHeader,
getDatabaseBodyCell,
getKanbanCard,
initDatabaseColumn,
switchColumnType,
} from './actions.js';
test.describe('focus', () => {
test('should support move focus by arrow key', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await pressEscape(page);
await waitNextFrame(page, 100);
await pressEscape(page);
await assertRowsSelection(page, [0, 0]);
});
test('should support multi row selection', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '123', true);
const selectColumn = getDatabaseBodyCell(page, {
rowIndex: 1,
columnIndex: 1,
});
const endBox = await getBoundingBox(selectColumn);
const endX = endBox.x + endBox.width / 2;
await dragBetweenCoords(
page,
{ x: endX, y: endBox.y },
{ x: endX, y: endBox.y + endBox.height }
);
await pressEscape(page);
await assertRowsSelection(page, [0, 1]);
});
test('should support row selection with dynamic height', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123123', true);
await type(page, '456456');
await pressEnter(page);
await type(page, 'abcabc');
await pressEnter(page);
await type(page, 'defdef');
await pressEnter(page);
await pressEscape(page);
await pressEscape(page);
await assertRowsSelection(page, [0, 0]);
});
});
test.describe('row-level selection', () => {
test('should support title selection', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseRowWithData(page, 'title');
await pressEscape(page);
await waitNextFrame(page, 100);
await assertCellsSelection(page, {
start: [0, 0],
});
await pressEscape(page);
await assertRowsSelection(page, [0, 0]);
});
test('should support pressing esc to trigger row selection', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await pressEscape(page);
await waitNextFrame(page, 100);
await pressEscape(page);
await assertRowsSelection(page, [0, 0]);
});
test('should support multi row selection', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '123', true);
const selectColumn = getDatabaseBodyCell(page, {
rowIndex: 1,
columnIndex: 1,
});
const endBox = await getBoundingBox(selectColumn);
const endX = endBox.x + endBox.width / 2;
await dragBetweenCoords(
page,
{ x: endX, y: endBox.y },
{ x: endX, y: endBox.y + endBox.height }
);
await pressEscape(page);
await assertRowsSelection(page, [0, 1]);
});
test('should support row selection with dynamic height', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123123', true);
await type(page, '456456');
await pressEnter(page);
await type(page, 'abcabc');
await pressEnter(page);
await type(page, 'defdef');
await pressEnter(page);
await pressEscape(page);
await pressEscape(page);
await assertRowsSelection(page, [0, 0]);
});
test('move row selection with (up | down)', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
// add two rows
await initDatabaseDynamicRowWithData(page, '123123', true);
await pressEscape(page);
await initDatabaseDynamicRowWithData(page, '123123', true);
await pressEscape(page);
await pressEscape(page); // switch to row selection
await assertRowsSelection(page, [1, 1]);
await pressArrowUp(page);
await assertRowsSelection(page, [0, 0]);
// should not allow under selection
await pressArrowUp(page);
await assertRowsSelection(page, [0, 0]);
await pressArrowDown(page);
await assertRowsSelection(page, [1, 1]);
// should not allow over selection
await pressArrowDown(page);
await assertRowsSelection(page, [1, 1]);
});
test('increment decrement row selection with shift+(up | down)', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
// add two rows
await initDatabaseDynamicRowWithData(page, '123123', true);
await pressEscape(page);
await initDatabaseDynamicRowWithData(page, '123123', true);
await pressEscape(page);
await pressEscape(page); // switch to row selection
await pressArrowUpWithShiftKey(page);
await assertRowsSelection(page, [0, 1]);
await pressArrowDownWithShiftKey(page);
await assertRowsSelection(page, [1, 1]); // should decrement back
await pressArrowUp(page); // go to first row
await pressArrowDownWithShiftKey(page);
await assertRowsSelection(page, [0, 1]);
await pressArrowUpWithShiftKey(page);
await assertRowsSelection(page, [0, 0]);
});
});
test.describe('cell-level selection', () => {
test('should support multi cell selection', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '123', true);
const startCell = getDatabaseBodyCell(page, {
rowIndex: 0,
columnIndex: 0,
});
const endCell = getDatabaseBodyCell(page, {
rowIndex: 1,
columnIndex: 1,
});
const startBox = await getBoundingBox(startCell);
const endBox = await getBoundingBox(endCell);
const startX = startBox.x + startBox.width / 2;
const startY = startBox.y + startBox.height / 2;
const endX = endBox.x + endBox.width / 2;
const endY = endBox.y + endBox.height / 2;
await dragBetweenCoords(
page,
{ x: startX, y: startY },
{ x: endX, y: endY }
);
await assertCellsSelection(page, {
start: [0, 0],
end: [1, 1],
});
});
test("should support backspace key to delete cell's content", async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseColumn(page);
await initDatabaseRowWithData(page, 'row1');
await initDatabaseDynamicRowWithData(page, 'abc', false);
await pressEscape(page);
await initDatabaseRowWithData(page, 'row2');
await initDatabaseDynamicRowWithData(page, '123', false);
await pressEscape(page);
const startCell = getDatabaseBodyCell(page, {
rowIndex: 0,
columnIndex: 0,
});
const endCell = getDatabaseBodyCell(page, {
rowIndex: 1,
columnIndex: 1,
});
const startBox = await getBoundingBox(startCell);
const endBox = await getBoundingBox(endCell);
const startX = startBox.x + startBox.width / 2;
const startY = startBox.y + startBox.height / 2;
const endX = endBox.x + endBox.width / 2;
const endY = endBox.y + endBox.height / 2;
await dragBetweenCoords(
page,
{ x: startX, y: startY },
{ x: endX, y: endY }
);
await pressBackspace(page);
await assertDatabaseTitleColumnText(page, '', 0);
await assertDatabaseTitleColumnText(page, '', 1);
const selectCell1 = getDatabaseBodyCell(page, {
rowIndex: 0,
columnIndex: 1,
});
expect(await selectCell1.innerText()).toBe('');
const selectCell2 = getDatabaseBodyCell(page, {
rowIndex: 1,
columnIndex: 1,
});
expect(await selectCell2.innerText()).toBe('');
});
});
test.describe('kanban view selection', () => {
test("should support move cell's focus by arrow key(up&down) within a card", async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initKanbanViewState(page, {
rows: ['row1'],
columns: [
{
type: 'number',
value: [1],
},
{
type: 'rich-text',
value: ['text'],
},
],
});
await focusKanbanCardHeader(page);
await assertKanbanCellSelected(page, {
// group by `number` column, the first(groupIndex: 0) group is `Ungroups`
groupIndex: 1,
cardIndex: 0,
cellIndex: 0,
});
await pressArrowDown(page, 3);
await assertKanbanCellSelected(page, {
groupIndex: 1,
cardIndex: 0,
cellIndex: 0,
});
await pressArrowUp(page);
await assertKanbanCellSelected(page, {
groupIndex: 1,
cardIndex: 0,
cellIndex: 2,
});
});
test("should support move cell's focus by arrow key(up&down) within multi cards", async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initKanbanViewState(page, {
rows: ['row1', 'row2'],
columns: [
{
type: 'number',
value: [1, 2],
},
{
type: 'rich-text',
value: ['text'],
},
],
});
await focusKanbanCardHeader(page);
await pressArrowUp(page);
await assertKanbanCellSelected(page, {
groupIndex: 1,
cardIndex: 1,
cellIndex: 2,
});
await pressArrowDown(page);
await assertKanbanCellSelected(page, {
groupIndex: 1,
cardIndex: 0,
cellIndex: 0,
});
});
test("should support move cell's focus by arrow key(left&right)", async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initKanbanViewState(page, {
rows: ['row1', 'row2', 'row3'],
columns: [
{
type: 'number',
value: [undefined, 1, 10],
},
],
});
await focusKanbanCardHeader(page);
await pressArrowRight(page, 3);
await assertKanbanCellSelected(page, {
groupIndex: 0,
cardIndex: 0,
cellIndex: 0,
});
await pressArrowLeft(page);
await assertKanbanCellSelected(page, {
groupIndex: 2,
cardIndex: 0,
cellIndex: 0,
});
});
test("should support move card's focus by arrow key(up&down)", async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initKanbanViewState(page, {
rows: ['row1', 'row2', 'row3'],
columns: [
{
type: 'number',
value: [undefined, undefined, undefined],
},
],
});
await focusKanbanCardHeader(page);
await pressEscape(page);
await pressEscape(page);
await assertKanbanCardSelected(page, {
groupIndex: 0,
cardIndex: 0,
});
await pressArrowDown(page, 3);
await assertKanbanCardSelected(page, {
groupIndex: 0,
cardIndex: 0,
});
await pressArrowUp(page);
await assertKanbanCardSelected(page, {
groupIndex: 0,
cardIndex: 2,
});
});
test("should support move card's focus by arrow key(left&right)", async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initKanbanViewState(page, {
rows: ['row1', 'row2', 'row3'],
columns: [
{
type: 'number',
value: [undefined, 1, 10],
},
],
});
await focusKanbanCardHeader(page);
await pressEscape(page);
await pressEscape(page);
await pressArrowRight(page, 3);
await assertKanbanCardSelected(page, {
groupIndex: 0,
cardIndex: 0,
});
await pressArrowLeft(page);
await assertKanbanCardSelected(page, {
groupIndex: 2,
cardIndex: 0,
});
});
test('should support multi card selection', async ({ page }) => {
await enterPlaygroundRoom(page);
await initKanbanViewState(page, {
rows: ['row1', 'row2'],
columns: [
{
type: 'number',
value: [undefined, 1],
},
],
});
await focusKanbanCardHeader(page);
await pressEscape(page);
await pressEscape(page);
const card = getKanbanCard(page, {
groupIndex: 1,
cardIndex: 0,
});
const box = await getBoundingBox(card);
await shiftClick(page, {
x: box.x + box.width / 2,
y: box.y + box.height / 2,
});
await assertKanbanCardSelected(page, {
groupIndex: 0,
cardIndex: 0,
});
await assertKanbanCardSelected(page, {
groupIndex: 1,
cardIndex: 0,
});
});
test("should support move cursor in card's title by arrow key(left&right)", async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initKanbanViewState(page, {
rows: ['row1'],
columns: [
{
type: 'rich-text',
value: ['text'],
},
],
});
await clickKanbanCardHeader(page);
await type(page, 'abc');
await pressArrowLeft(page, 2);
await pressArrowRight(page);
await pressBackspace(page);
await pressEscape(page);
await assertKanbanCardHeaderText(page, 'row1ac');
});
});

View File

@@ -0,0 +1,112 @@
import { expect, type Locator } from '@playwright/test';
import {
enterPlaygroundRoom,
initDatabaseDynamicRowWithData,
initEmptyDatabaseState,
waitNextFrame,
} from '../utils/actions/index.js';
import { test } from '../utils/playwright.js';
import { initDatabaseColumn, switchColumnType } from './actions.js';
test('database sort with multiple rules', async ({ page }) => {
// Initialize database
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
// Add test columns: Name (text) and Age (number)
await initDatabaseColumn(page, 'Name');
await switchColumnType(page, 'Text', 1);
await initDatabaseColumn(page, 'Age');
await switchColumnType(page, 'Number', 2);
// Add test data
const testData = [
{ name: 'Alice', age: '25' },
{ name: 'Bob', age: '30' },
{ name: 'Alice', age: '20' },
{ name: 'Charlie', age: '25' },
];
for (const data of testData) {
await initDatabaseDynamicRowWithData(page, data.name, true, 0);
await initDatabaseDynamicRowWithData(page, data.age, false, 1);
}
// Open sort menu
const sortButton = page.locator('data-view-header-tools-sort');
await sortButton.click();
// Add first sort rule: Name ascending
await page.locator('affine-menu').getByText('Name').click();
await waitNextFrame(page);
// Add second sort rule: Age ascending
await page.getByText('Add sort').click();
await page.locator('affine-menu').getByText('Age').click();
await waitNextFrame(page);
// Get all rows after sorting
const rows = await page.locator('affine-database-row').all();
const getCellText = async (row: Locator, index: number) => {
const cell = row.locator('.cell').nth(index);
return cell.innerText();
};
// Verify sorting results
// Should be sorted by Name first, then by Age
const expectedOrder = [
{ name: 'Alice', age: '20' },
{ name: 'Alice', age: '25' },
{ name: 'Bob', age: '30' },
{ name: 'Charlie', age: '25' },
];
for (let i = 0; i < rows.length; i++) {
const name = await getCellText(rows[i], 1);
const age = await getCellText(rows[i], 2);
expect(name).toBe(expectedOrder[i].name);
expect(age).toBe(expectedOrder[i].age);
}
// Change sort order of Name to descending
await page.locator('.sort-item').first().getByText('Ascending').click();
await page.getByText('Descending').click();
await waitNextFrame(page);
// Verify new sorting results
const expectedOrderDesc = [
{ name: 'Charlie', age: '25' },
{ name: 'Bob', age: '30' },
{ name: 'Alice', age: '20' },
{ name: 'Alice', age: '25' },
];
const rowsAfterDesc = await page.locator('affine-database-row').all();
for (let i = 0; i < rowsAfterDesc.length; i++) {
const name = await getCellText(rowsAfterDesc[i], 1);
const age = await getCellText(rowsAfterDesc[i], 2);
expect(name).toBe(expectedOrderDesc[i].name);
expect(age).toBe(expectedOrderDesc[i].age);
}
// Remove first sort rule
await page.locator('.sort-item').first().getByRole('img').last().click();
await waitNextFrame(page);
// Verify sorting now only by Age
const expectedOrderAgeOnly = [
{ name: 'Alice', age: '20' },
{ name: 'Alice', age: '25' },
{ name: 'Charlie', age: '25' },
{ name: 'Bob', age: '30' },
];
const rowsAfterRemove = await page.locator('affine-database-row').all();
for (let i = 0; i < rowsAfterRemove.length; i++) {
const name = await getCellText(rowsAfterRemove[i], 1);
const age = await getCellText(rowsAfterRemove[i], 2);
expect(name).toBe(expectedOrderAgeOnly[i].name);
expect(age).toBe(expectedOrderAgeOnly[i].age);
}
});

View File

@@ -0,0 +1,107 @@
import { expect, type Page } from '@playwright/test';
import { type } from '../utils/actions/index.js';
import {
enterPlaygroundRoom,
getAddRow,
initEmptyDatabaseState,
waitNextFrame,
} from '../utils/actions/misc.js';
import { test } from '../utils/playwright.js';
import {
changeColumnType,
moveToCenterOf,
press,
pressKey,
} from './actions.js';
const addRow = async (page: Page, count: number = 1) => {
await waitNextFrame(page);
const addRow = getAddRow(page);
for (let i = 0; i < count; i++) {
await addRow.click();
}
await press(page, 'Escape');
await waitNextFrame(page);
};
const insertRightColumn = async (page: Page, index = 0) => {
await waitNextFrame(page);
await page.locator('affine-database-header-column').nth(index).click();
await waitNextFrame(page, 200);
await pressKey(page, 'Escape');
const menu = page.locator('.affine-menu-button', {
hasText: new RegExp('Insert Right'),
});
await menu.click();
await waitNextFrame(page, 200);
await pressKey(page, 'Enter');
};
const menuSelect = async (page: Page, selectors: string[]) => {
await waitNextFrame(page);
for (const name of selectors) {
const menu = page.locator('.affine-menu-button', {
hasText: new RegExp(name),
});
await menu.click();
}
};
test.describe('title', () => {
test('empty count', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await addRow(page, 3);
const statCell = page.locator('affine-database-column-stats-cell').nth(0);
await moveToCenterOf(page, statCell);
await statCell.click();
await menuSelect(page, ['Count', 'Count Empty']);
const value = statCell.locator('.value');
expect((await value.textContent())?.trim()).toBe('3');
await page.locator('affine-database-cell-container').nth(0).click();
await pressKey(page, 'Enter');
await type(page, 'asd');
await pressKey(page, 'Escape');
expect((await value.textContent())?.trim()).toBe('2');
});
});
test.describe('rich-text', () => {
test('empty count', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await addRow(page, 3);
await insertRightColumn(page);
await changeColumnType(page, 1, 'text');
const statCell = page.locator('affine-database-column-stats-cell').nth(1);
await moveToCenterOf(page, statCell);
await statCell.click();
await menuSelect(page, ['Count', 'Count Empty']);
const value = statCell.locator('.value');
expect((await value.textContent())?.trim()).toBe('3');
await page.locator('affine-database-cell-container').nth(1).click();
await pressKey(page, 'Enter');
await type(page, 'asd');
await pressKey(page, 'Escape');
expect((await value.textContent())?.trim()).toBe('2');
});
});
test.describe('select', () => {
test('empty count', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await addRow(page, 3);
await insertRightColumn(page);
await changeColumnType(page, 1, 'select');
const statCell = page.locator('affine-database-column-stats-cell').nth(1);
await moveToCenterOf(page, statCell);
await statCell.click();
await menuSelect(page, ['Count', 'Count Empty']);
const value = statCell.locator('.value');
expect((await value.textContent())?.trim()).toBe('3');
await page.locator('affine-database-cell-container').nth(1).click();
await pressKey(page, 'Enter');
await type(page, 'select');
await pressKey(page, 'Enter');
expect((await value.textContent())?.trim()).toBe('2');
});
});

View File

@@ -0,0 +1,19 @@
import { expect } from '@playwright/test';
import {
enterPlaygroundRoom,
initDatabaseDynamicRowWithData,
initEmptyDatabaseState,
} from '../utils/actions/misc.js';
import { test } from '../utils/playwright.js';
import { press } from './actions.js';
test.describe('title', () => {
test('should able to link doc by press @', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyDatabaseState(page);
await initDatabaseDynamicRowWithData(page, '123', true);
await press(page, '@');
await expect(page.locator('.linked-doc-popover')).toBeVisible();
});
});

View File

@@ -0,0 +1,767 @@
import { expect } from '@playwright/test';
import {
dragBetweenCoords,
dragBetweenIndices,
dragHandleFromBlockToBlockBottomById,
enterPlaygroundRoom,
focusRichText,
initEmptyParagraphState,
initThreeLists,
initThreeParagraphs,
pressEnter,
pressShiftTab,
pressTab,
type,
} from './utils/actions/index.js';
import {
getBoundingClientRect,
getEditorHostLocator,
getPageSnapshot,
initParagraphsByCount,
} from './utils/actions/misc.js';
import { assertRichTexts } from './utils/asserts.js';
import { BLOCK_CHILDREN_CONTAINER_PADDING_LEFT } from './utils/bs-alternative.js';
import { test } from './utils/playwright.js';
test('only have one drag handle in screen', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
const topLeft = await page.evaluate(() => {
const paragraph = document.querySelector('[data-block-id="2"]');
const box = paragraph?.getBoundingClientRect();
if (!box) {
throw new Error();
}
return { x: box.left, y: box.top + 2 };
}, []);
const bottomRight = await page.evaluate(() => {
const paragraph = document.querySelector('[data-block-id="4"]');
const box = paragraph?.getBoundingClientRect();
if (!box) {
throw new Error();
}
return { x: box.right, y: box.bottom - 2 };
}, []);
await page.mouse.move(topLeft.x, topLeft.y);
const length1 = await page.evaluate(() => {
const handles = document.querySelectorAll('affine-drag-handle-widget');
return handles.length;
}, []);
expect(length1).toBe(1);
await page.mouse.move(bottomRight.x, bottomRight.y);
const length2 = await page.evaluate(() => {
const handles = document.querySelectorAll('affine-drag-handle-widget');
return handles.length;
}, []);
expect(length2).toBe(1);
});
test('move drag handle in paragraphs', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await dragHandleFromBlockToBlockBottomById(page, '2', '4');
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['456', '789', '123']);
});
test('move drag handle in list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
await assertRichTexts(page, ['123', '456', '789']);
await dragHandleFromBlockToBlockBottomById(page, '5', '3', false);
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['123', '789', '456']);
});
test('move drag handle in nested block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '-');
await page.keyboard.press('Space', { delay: 50 });
await type(page, '1');
await pressEnter(page);
await type(page, '2');
await pressEnter(page);
await pressTab(page);
await type(page, '21');
await pressEnter(page);
await type(page, '22');
await pressEnter(page);
await type(page, '23');
await pressEnter(page);
await pressShiftTab(page);
await type(page, '3');
await assertRichTexts(page, ['1', '2', '21', '22', '23', '3']);
await dragHandleFromBlockToBlockBottomById(page, '5', '7');
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['1', '2', '22', '23', '21', '3']);
await dragHandleFromBlockToBlockBottomById(page, '3', '8');
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['2', '22', '23', '21', '3', '1']);
});
test('move drag handle into another block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '-');
await page.keyboard.press('Space', { delay: 50 });
await type(page, '1');
await pressEnter(page);
await type(page, '2');
await pressEnter(page);
await pressTab(page);
await type(page, '21');
await pressEnter(page);
await type(page, '22');
await pressEnter(page);
await type(page, '23');
await pressEnter(page);
await pressShiftTab(page);
await type(page, '3');
await assertRichTexts(page, ['1', '2', '21', '22', '23', '3']);
await dragHandleFromBlockToBlockBottomById(
page,
'5',
'7',
true,
2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT
);
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['1', '2', '22', '23', '21', '3']);
// FIXME(DND)
// await assertBlockChildrenIds(page, '7', ['5']);
// await dragHandleFromBlockToBlockBottomById(
// page,
// '3',
// '8',
// true,
// 2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT
// );
// await expect(page.locator('.affine-drop-indicator')).toBeHidden();
// await assertRichTexts(page, ['2', '22', '23', '21', '3', '1']);
// await assertBlockChildrenIds(page, '8', ['3']);
});
test('move to the last block of each level in multi-level nesting', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '-');
await page.keyboard.press('Space', { delay: 50 });
await type(page, 'A');
await pressEnter(page);
await type(page, 'B');
await pressEnter(page);
await type(page, 'C');
await pressEnter(page);
await pressTab(page);
await type(page, 'D');
await pressEnter(page);
await type(page, 'E');
await pressEnter(page);
await pressTab(page);
await type(page, 'F');
await pressEnter(page);
await type(page, 'G');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await dragHandleFromBlockToBlockBottomById(page, '3', '9');
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_drag_3_9.json`
);
await dragHandleFromBlockToBlockBottomById(
page,
'4',
'3',
true,
-(1 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
);
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_drag_4_3.json`
);
await assertRichTexts(page, ['C', 'D', 'E', 'F', 'G', 'A', 'B']);
await dragHandleFromBlockToBlockBottomById(
page,
'3',
'4',
true,
-(2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
);
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
// FIXME(DND)
// expect(await getPageSnapshot(page, true)).toMatchSnapshot(
// `${testInfo.title}_drag_3_4.json`
// );
//
// await assertRichTexts(page, ['C', 'D', 'E', 'F', 'G', 'B', 'A']);
});
test('should sync selected-blocks to session-manager when clicking drag handle', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await focusRichText(page, 1);
const rect = await getBoundingClientRect(page, '[data-block-id="1"]');
if (!rect) {
throw new Error();
}
await page.mouse.move(rect.x + 10, rect.y + 10, { steps: 2 });
const handle = page.locator('.affine-drag-handle-container');
await handle.click();
await page.keyboard.press('Backspace');
await assertRichTexts(page, ['456', '789']);
});
test.fixme(
'should be able to drag & drop multiple blocks',
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await dragBetweenIndices(
page,
[0, 0],
[1, 3],
{ x: -60, y: 0 },
{ x: 80, y: 0 },
{
steps: 50,
}
);
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(2);
await dragHandleFromBlockToBlockBottomById(page, '2', '4', true);
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['789', '123', '456']);
// Selection is still 2 after drop
await expect(blockSelections).toHaveCount(2);
}
);
test.fixme(
'should be able to drag & drop multiple blocks to nested block',
async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '-');
await page.keyboard.press('Space', { delay: 50 });
await type(page, 'A');
await pressEnter(page);
await type(page, 'B');
await pressEnter(page);
await type(page, 'C');
await pressEnter(page);
await pressTab(page);
await type(page, 'D');
await pressEnter(page);
await type(page, 'E');
await pressEnter(page);
await pressTab(page);
await type(page, 'F');
await pressEnter(page);
await type(page, 'G');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await dragBetweenIndices(
page,
[0, 0],
[1, 1],
{ x: -80, y: 0 },
{ x: 80, y: 0 },
{
steps: 50,
}
);
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(2);
await dragHandleFromBlockToBlockBottomById(page, '3', '8');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
}
);
test('should blur rich-text first on starting block selection', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await expect(page.locator('*:focus')).toHaveCount(1);
await dragHandleFromBlockToBlockBottomById(page, '2', '4');
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['456', '789', '123']);
await expect(page.locator('*:focus')).toHaveCount(0);
});
test('hide drag handle when mouse is hovering over the title', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
const rect = await getBoundingClientRect(
page,
'.affine-note-block-container'
);
const dragHandle = page.locator('.affine-drag-handle-container');
// When there is a gap between paragraph blocks, it is the correct behavior for the drag handle to appear
// when the mouse is over the gap. Therefore, we use rect.y - 20 to make the Y offset greater than the gap between the
// paragraph blocks.
await page.mouse.move(rect.x, rect.y - 20, { steps: 2 });
await expect(dragHandle).toBeHidden();
await page.mouse.move(rect.x, rect.y, { steps: 2 });
expect(await dragHandle.isVisible()).toBe(true);
await expect(dragHandle).toBeVisible();
});
test.fixme('should create preview when dragging', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
const dragPreview = page.locator('affine-drag-preview');
await dragBetweenIndices(
page,
[0, 0],
[1, 3],
{ x: -60, y: 0 },
{ x: 80, y: 0 },
{
steps: 50,
}
);
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(2);
await dragHandleFromBlockToBlockBottomById(
page,
'2',
'4',
true,
undefined,
async () => {
await expect(dragPreview).toBeVisible();
await expect(dragPreview.locator('[data-block-id]')).toHaveCount(4);
}
);
});
test.fixme(
'should drag and drop blocks under block-level selection',
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await dragBetweenIndices(
page,
[0, 0],
[1, 3],
{ x: -60, y: 0 },
{ x: 80, y: 0 },
{
steps: 50,
}
);
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(2);
const editorHost = getEditorHostLocator(page);
const editors = editorHost.locator('rich-text');
const editorRect0 = await editors.nth(0).boundingBox();
const editorRect2 = await editors.nth(2).boundingBox();
if (!editorRect0 || !editorRect2) {
throw new Error();
}
await dragBetweenCoords(
page,
{
x: editorRect0.x - 10,
y: editorRect0.y + editorRect0.height / 2,
},
{
x: editorRect2.x + 10,
y: editorRect2.y + editorRect2.height / 2 + 10,
},
{
steps: 50,
}
);
await assertRichTexts(page, ['789', '123', '456']);
await expect(blockSelections).toHaveCount(2);
}
);
test('should trigger click event on editor container when clicking on blocks under block-level selection', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await dragBetweenIndices(
page,
[0, 0],
[1, 3],
{ x: -60, y: 0 },
{ x: 80, y: 0 },
{
steps: 50,
}
);
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(2);
await expect(page.locator('*:focus')).toHaveCount(0);
const editorHost = getEditorHostLocator(page);
const editors = editorHost.locator('rich-text');
const editorRect0 = await editors.nth(0).boundingBox();
if (!editorRect0) {
throw new Error();
}
await page.mouse.move(
editorRect0.x + 10,
editorRect0.y + editorRect0.height / 2
);
await page.mouse.down();
await page.mouse.up();
await expect(blockSelections).toHaveCount(0);
await expect(page.locator('*:focus')).toHaveCount(1);
});
test('should get to selected block when dragging unselected block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '123');
await pressEnter(page);
await type(page, '456');
await assertRichTexts(page, ['123', '456']);
const editorHost = getEditorHostLocator(page);
const editors = editorHost.locator('rich-text');
const editorRect0 = await editors.nth(0).boundingBox();
const editorRect1 = await editors.nth(1).boundingBox();
if (!editorRect0 || !editorRect1) {
throw new Error();
}
await page.mouse.move(editorRect1.x - 5, editorRect0.y);
await page.mouse.down();
await page.mouse.up();
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(1);
await page.mouse.move(editorRect1.x - 5, editorRect0.y);
await page.mouse.down();
await page.mouse.move(
editorRect1.x - 5,
editorRect1.y + editorRect1.height / 2 + 1,
{
steps: 10,
}
);
await page.mouse.up();
await expect(blockSelections).toHaveCount(1);
// FIXME(DND)
// await assertRichTexts(page, ['456', '123']);
});
test.fixme(
'should clear the currently selected block when clicked again',
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '123');
await pressEnter(page);
await type(page, '456');
await assertRichTexts(page, ['123', '456']);
const editorHost = getEditorHostLocator(page);
const editors = editorHost.locator('rich-text');
const editorRect0 = await editors.nth(0).boundingBox();
const editorRect1 = await editors.nth(1).boundingBox();
if (!editorRect0 || !editorRect1) {
throw new Error();
}
await page.mouse.move(
editorRect1.x + 5,
editorRect1.y + editorRect1.height / 2
);
await page.mouse.move(
editorRect1.x - 10,
editorRect1.y + editorRect1.height / 2
);
await page.mouse.down();
await page.mouse.up();
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(1);
let selectedBlockRect = await blockSelections.nth(0).boundingBox();
if (!selectedBlockRect) {
throw new Error();
}
expect(editorRect1).toEqual(selectedBlockRect);
await page.mouse.move(
editorRect0.x - 10,
editorRect0.y + editorRect0.height / 2
);
await page.mouse.down();
await page.mouse.up();
await expect(blockSelections).toHaveCount(1);
selectedBlockRect = await blockSelections.nth(0).boundingBox();
if (!selectedBlockRect) {
throw new Error();
}
expect(editorRect0).toEqual(selectedBlockRect);
}
);
test.fixme(
'should support moving blocks from multiple notes',
async ({ page }) => {
await enterPlaygroundRoom(page);
await page.evaluate(() => {
const { doc } = window;
const rootId = doc.addBlock('affine:page', {
title: new window.$blocksuite.store.Text(),
});
doc.addBlock('affine:surface', {}, rootId);
['123', '456', '789', '987', '654', '321'].forEach(text => {
const noteId = doc.addBlock('affine:note', {}, rootId);
doc.addBlock(
'affine:paragraph',
{
text: new window.$blocksuite.store.Text(text),
},
noteId
);
});
doc.resetHistory();
});
await dragBetweenIndices(
page,
[1, 0],
[2, 3],
{ x: -60, y: 0 },
{ x: 80, y: 0 },
{
steps: 50,
}
);
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(2);
const editorHost = getEditorHostLocator(page);
const editors = editorHost.locator('rich-text');
const editorRect1 = await editors.nth(1).boundingBox();
const editorRect3 = await editors.nth(3).boundingBox();
if (!editorRect1 || !editorRect3) {
throw new Error();
}
await dragBetweenCoords(
page,
{
x: editorRect1.x - 10,
y: editorRect1.y + editorRect1.height / 2,
},
{
x: editorRect3.x + 10,
y: editorRect3.y + editorRect3.height / 2 + 10,
},
{
steps: 50,
}
);
await assertRichTexts(page, ['123', '987', '456', '789', '654', '321']);
await expect(blockSelections).toHaveCount(2);
await dragBetweenIndices(
page,
[5, 0],
[4, 3],
{ x: -60, y: 0 },
{ x: 80, y: 0 },
{
steps: 50,
}
);
const editorRect0 = await editors.nth(0).boundingBox();
const editorRect5 = await editors.nth(5).boundingBox();
if (!editorRect0 || !editorRect5) {
throw new Error();
}
await dragBetweenCoords(
page,
{
x: editorRect5.x - 10,
y: editorRect5.y + editorRect5.height / 2,
},
{
x: editorRect0.x + 10,
y: editorRect0.y + editorRect0.height / 2 - 5,
},
{
steps: 50,
}
);
await assertRichTexts(page, ['654', '321', '123', '987', '456', '789']);
await expect(blockSelections).toHaveCount(2);
}
);
test('drag handle should show on right block when scroll viewport', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initParagraphsByCount(page, 30);
await page.mouse.wheel(0, 200);
const editorHost = getEditorHostLocator(page);
const editors = editorHost.locator('rich-text');
const blockRect28 = await editors.nth(28).boundingBox();
if (!blockRect28) {
throw new Error();
}
await page.mouse.move(blockRect28.x + 10, blockRect28.y + 10);
const dragHandle = page.locator('.affine-drag-handle-container');
await expect(dragHandle).toBeVisible();
await page.mouse.move(
blockRect28.x - 10,
blockRect28.y + blockRect28.height / 2
);
await page.mouse.down();
await page.mouse.up();
const blockSelections = page
.locator('affine-block-selection')
.locator('visible=true');
await expect(blockSelections).toHaveCount(1);
const selectedBlockRect = await blockSelections.nth(0).boundingBox();
if (!selectedBlockRect) {
throw new Error();
}
expect(blockRect28).toEqual(selectedBlockRect);
});

View File

@@ -0,0 +1,435 @@
import { expect } from '@playwright/test';
import {
addBasicBrushElement,
createConnectorElement,
createFrameElement,
createNote,
createShapeElement,
setEdgelessTool,
Shape,
toViewCoord,
triggerComponentToolbarAction,
} from '../utils/actions/edgeless.js';
import {
clickView,
edgelessCommonSetup as commonSetup,
selectAllByKeyboard,
type,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertEdgelessSelectedModelRect,
getSelectedRect,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.describe('auto arrange align', () => {
test('arrange shapes', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await createShapeElement(page, [100, -100], [300, 100], Shape.Ellipse);
await createShapeElement(page, [200, 300], [300, 400], Shape.Square);
await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle);
await createShapeElement(
page,
[0, 200],
[100, 300],
Shape['Rounded rectangle']
);
await page.mouse.click(0, 0);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, -100, 500, 500]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 560, 320]);
});
test('arrange rotated shapes', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Ellipse);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
const point = await toViewCoord(page, [100, 100]);
await page.mouse.click(point[0] + 50, point[1] + 50);
await page.mouse.move(point[0] - 5, point[1] - 5);
await page.mouse.down();
await page.mouse.move(point[0] - 5, point[1] + 45);
await page.mouse.up();
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 220, 220]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 261, 141]);
});
test('arrange connected shapes', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 100], [200, 200], Shape.Ellipse);
await createConnectorElement(page, [50, 100], [150, 100]);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 200, 200]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, -21, 220, 141.4]);
});
test('arrange connector', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createConnectorElement(page, [200, 200], [300, 200]);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 300, 200]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 220, 100]);
});
test('arrange edgeless text', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
const point = await toViewCoord(page, [200, -100]);
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
await waitNextFrame(page);
await type(page, 'a');
await page.mouse.click(0, 0);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, -125, 395, 225]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 340, 100]);
});
test('arrange note', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [200, 200], 'Hello World');
await page.mouse.click(0, 0);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 668, 252]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 618, 100]);
});
test('arrange group', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [200, 300], [300, 400], Shape.Square);
await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 500, 400]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 420, 300]);
});
test('arrange frame', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [200, 300], [300, 400], Shape.Square);
await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle);
await selectAllByKeyboard(page);
await createFrameElement(page, [150, 50], [550, 450]);
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await page.mouse.click(0, 0);
await page.mouse.move(75, 395);
await page.mouse.down();
await page.mouse.move(900, 900);
await page.mouse.up();
await assertEdgelessSelectedModelRect(page, [0, 0, 550, 450]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 520, 400]);
});
// TODO mindmap size different on CI
test('arrange mindmap', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await page.keyboard.press('m');
await clickView(page, [500, 200]);
await selectAllByKeyboard(page);
const box1 = await getSelectedRect(page);
expect(box1.width).toBeGreaterThan(700);
expect(box1.height).toBeGreaterThan(300);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
const box2 = await getSelectedRect(page);
expect(box2.width).toBeLessThan(550);
expect(box2.height).toBeLessThan(210);
});
test('arrange shape, note, connector, brush and edgeless text', async ({
page,
}) => {
await commonSetup(page);
// shape
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [150, 150], [300, 300], Shape.Ellipse);
//note
await createNote(page, [200, 100], 'Hello World');
// connector
await createConnectorElement(page, [200, -200], [400, -100]);
// brush
const start = { x: 400, y: 400 };
const end = { x: 480, y: 480 };
await addBasicBrushElement(page, start, end);
// edgeless text
const point = await toViewCoord(page, [-100, -100]);
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
await waitNextFrame(page);
await type(page, 'edgeless text');
await page.mouse.click(0, 0);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [-125, -200, 793, 500]);
// arrange
await triggerComponentToolbarAction(page, 'autoArrange');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [-125, -125, 668, 270]);
});
});
test.describe('auto resize align', () => {
test('resize and arrange shapes', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await createShapeElement(page, [100, -100], [300, 100], Shape.Ellipse);
await createShapeElement(page, [200, 300], [300, 400], Shape.Square);
await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle);
await createShapeElement(
page,
[0, 200],
[100, 300],
Shape['Rounded rectangle']
);
await page.mouse.click(0, 0);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, -100, 500, 500]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 860, 420]);
});
test('resize and arrange rotated shapes', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Ellipse);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
const point = await toViewCoord(page, [100, 100]);
await page.mouse.click(point[0] + 50, point[1] + 50);
await page.mouse.move(point[0] - 5, point[1] - 5);
await page.mouse.down();
await page.mouse.move(point[0] - 5, point[1] + 45);
await page.mouse.up();
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 220, 220]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 420, 200]);
});
test('resize and arrange connected shapes', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 100], [200, 200], Shape.Ellipse);
await createConnectorElement(page, [50, 100], [150, 100]);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 200, 200]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, -16, 420, 232]);
});
test('resize and arrange connector', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createConnectorElement(page, [200, 200], [300, 200]);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 300, 200]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 320, 200]);
});
test('resize and arrange edgeless text', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
const point = await toViewCoord(page, [200, -100]);
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
await waitNextFrame(page);
await type(page, 'a');
await page.mouse.click(0, 0);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, -125, 395, 225]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 1912.296875, 200]);
});
test('resize and arrange note', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [200, 200], 'Hello World');
await page.mouse.click(0, 0);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 668, 252]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 1302.5, 200]);
});
test('resize and arrange group', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [200, 300], [300, 400], Shape.Square);
await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 500, 400]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 420, 200]);
});
test('resize and arrange frame', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [200, 300], [300, 400], Shape.Square);
await createShapeElement(page, [400, 100], [500, 200], Shape.Triangle);
await selectAllByKeyboard(page);
await createFrameElement(page, [150, 50], [550, 450]);
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await page.mouse.click(0, 0);
await page.mouse.move(75, 395);
await page.mouse.down();
await page.mouse.move(900, 900);
await page.mouse.up();
await assertEdgelessSelectedModelRect(page, [0, 0, 550, 450]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 420, 200]);
});
// TODO mindmap size different on CI
test('resize and arrange mindmap', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await page.keyboard.press('m');
await clickView(page, [500, 200]);
await selectAllByKeyboard(page);
const box1 = await getSelectedRect(page);
expect(box1.width).toBeGreaterThan(700);
expect(box1.height).toBeGreaterThan(300);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
const box2 = await getSelectedRect(page);
expect(box2.width).toBeLessThan(650);
expect(box2.height).toBeLessThan(210);
});
test('resize and arrange shape, note, connector, brush and text', async ({
page,
}) => {
await commonSetup(page);
// shape
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [150, 150], [300, 300], Shape.Ellipse);
//note
await createNote(page, [200, 100], 'Hello World');
// connector
await createConnectorElement(page, [200, -200], [400, -100]);
// brush
const start = { x: 400, y: 400 };
const end = { x: 480, y: 480 };
await addBasicBrushElement(page, start, end);
// edgeless text
const point = await toViewCoord(page, [-100, -100]);
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
await waitNextFrame(page);
await type(page, 'edgeless text');
await page.mouse.click(0, 0);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [-125, -200, 793, 500]);
// arrange
await triggerComponentToolbarAction(page, 'autoResize');
await waitNextFrame(page, 200);
await assertEdgelessSelectedModelRect(page, [0, 0, 2352.296875, 420]);
});
});

View File

@@ -0,0 +1,250 @@
import { expect, type Page } from '@playwright/test';
import { lightThemeV2 } from '@toeverything/theme/v2';
import { clickView, moveView } from '../utils/actions/click.js';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import {
addNote,
changeEdgelessNoteBackground,
changeShapeFillColor,
changeShapeStrokeColor,
createShapeElement,
deleteAll,
dragBetweenViewCoords,
edgelessCommonSetup,
getEdgelessSelectedRectModel,
Shape,
switchEditorMode,
toViewCoord,
triggerComponentToolbarAction,
} from '../utils/actions/edgeless.js';
import {
enterPlaygroundRoom,
initEmptyEdgelessState,
waitForInlineEditorStateUpdated,
waitNextFrame,
} from '../utils/actions/misc.js';
import {
assertConnectorStrokeColor,
assertEdgelessCanvasText,
assertEdgelessNoteBackground,
assertExists,
assertRichTexts,
assertSelectedBound,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
function getAutoCompletePanelButton(page: Page, type: string) {
return page
.locator('.auto-complete-panel-container')
.locator('edgeless-tool-icon-button')
.filter({ hasText: `${type}` });
}
test.describe('auto-complete', () => {
test.describe('click on auto-complete button', () => {
test('click on right auto-complete button', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]);
await clickView(page, [120, 50]);
await assertSelectedBound(page, [200, 0, 100, 100]);
});
test('click on bottom auto-complete button', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]);
await clickView(page, [50, 120]);
await assertSelectedBound(page, [0, 200, 100, 100]);
});
test('click on left auto-complete button', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]);
await clickView(page, [-20, 50]);
await assertSelectedBound(page, [-200, 0, 100, 100]);
});
test('click on top auto-complete button', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]);
await clickView(page, [50, -20]);
await assertSelectedBound(page, [0, -200, 100, 100]);
});
test('click on note auto-complete button', async ({ page }) => {
await edgelessCommonSetup(page);
await addNote(page, 'note', 100, 100);
await page.mouse.click(600, 50);
await page.mouse.click(300, 50);
await page.mouse.click(150, 120);
const rect = await getEdgelessSelectedRectModel(page);
await moveView(page, [rect[0] + rect[2] + 30, rect[1] + rect[3] / 2]);
await clickView(page, [rect[0] + rect[2] + 30, rect[1] + rect[3] / 2]);
const newRect = await getEdgelessSelectedRectModel(page);
expect(rect[0]).not.toEqual(newRect[0]);
expect(rect[1]).toEqual(newRect[1]);
expect(rect[2]).toEqual(newRect[2]);
expect(rect[3]).toEqual(newRect[3]);
});
});
test.describe('drag on auto-complete button', () => {
test('drag on right auto-complete button to add shape', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]);
await dragBetweenViewCoords(page, [120, 50], [200, 0]);
const ellipseButton = getAutoCompletePanelButton(page, 'ellipse');
await expect(ellipseButton).toBeVisible();
await ellipseButton.click();
await assertSelectedBound(page, [200, -50, 100, 100]);
});
test('drag on right auto-complete button to add canvas text', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await page.evaluate(() => {
window.doc
.get(window.$blocksuite.blocks.FeatureFlagService)
.setFlag('enable_edgeless_text', false);
});
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await deleteAll(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]);
await dragBetweenViewCoords(page, [120, 50], [200, 0]);
const canvasTextButton = getAutoCompletePanelButton(page, 'text');
await expect(canvasTextButton).toBeVisible();
await canvasTextButton.click();
await waitForInlineEditorStateUpdated(page);
await waitNextFrame(page);
await page.keyboard.type('hello');
await assertEdgelessCanvasText(page, 'hello');
});
test('drag on right auto-complete button to add note', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
await changeShapeStrokeColor(page, 'MediumRed');
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
await changeShapeFillColor(page, 'HeavyGreen');
await dragBetweenViewCoords(page, [120, 50], [200, 0]);
const noteButton = getAutoCompletePanelButton(page, 'note');
await expect(noteButton).toBeVisible();
await noteButton.click();
await waitNextFrame(page);
const edgelessNote = page.locator('affine-edgeless-note');
expect(await edgelessNote.count()).toBe(1);
const [x, y] = await toViewCoord(page, [240, 20]);
await page.mouse.click(x, y);
await page.keyboard.type('hello');
await waitNextFrame(page);
await assertRichTexts(page, ['hello']);
const noteId = await page.evaluate(() => {
const note = document.body.querySelector('affine-edgeless-note');
return note?.getAttribute('data-block-id');
});
assertExists(noteId);
await assertEdgelessNoteBackground(
page,
noteId,
lightThemeV2['edgeless/note/white']
);
const rect = await edgelessNote.boundingBox();
assertExists(rect);
// blur note block
await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height * 3);
await waitNextFrame(page);
// select connector
await dragBetweenViewCoords(page, [140, 50], [160, 0]);
await waitNextFrame(page);
await assertConnectorStrokeColor(
page,
'MediumRed',
lightThemeV2['edgeless/palette/medium/redMedium']
);
// select note block
await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height / 2);
await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'changeNoteColor');
await changeEdgelessNoteBackground(page, 'Red');
// move to arrow icon
await page.mouse.move(
rect.x + rect.width + 20,
rect.y + rect.height / 2,
{ steps: 5 }
);
await waitNextFrame(page);
// drag arrow
await dragBetweenCoords(
page,
{
x: rect.x + rect.width + 20,
y: rect.y + rect.height / 2,
},
{
x: rect.x + rect.width + 20 + 50,
y: rect.y + rect.height / 2 + 50,
}
);
// `Add a same object` button has the same type.
const noteButton2 = getAutoCompletePanelButton(page, 'note').nth(0);
await expect(noteButton2).toBeVisible();
await noteButton2.click();
await waitNextFrame(page);
const noteId2 = await page.evaluate(() => {
const note = document.body.querySelectorAll('affine-edgeless-note')[1];
return note?.getAttribute('data-block-id');
});
assertExists(noteId2);
await assertEdgelessNoteBackground(
page,
noteId,
lightThemeV2['edgeless/note/red']
);
expect(await edgelessNote.count()).toBe(2);
});
test('drag on right auto-complete button to add frame', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await assertSelectedBound(page, [0, 0, 100, 100]);
await dragBetweenViewCoords(page, [120, 50], [200, 0]);
expect(await page.locator('.affine-frame-container').count()).toBe(0);
const frameButton = getAutoCompletePanelButton(page, 'frame');
await expect(frameButton).toBeVisible();
await frameButton.click();
expect(await page.locator('.affine-frame-container').count()).toBe(1);
});
});
});

View File

@@ -0,0 +1,180 @@
import { assertExists } from '@blocksuite/global/utils';
import { expect, type Page } from '@playwright/test';
import {
addNote,
changeNoteDisplayModeWithId,
dragBetweenViewCoords,
edgelessCommonSetup,
getNoteBoundBoxInEdgeless,
getSelectedBound,
selectNoteInEdgeless,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import { assertSelectedBound } from '../utils/asserts.js';
import { NoteDisplayMode } from '../utils/bs-alternative.js';
import { test } from '../utils/playwright.js';
test.describe('auto-connect', () => {
async function init(page: Page) {
await edgelessCommonSetup(page);
}
test('navigator', async ({ page }) => {
await init(page);
const id1 = await addNote(page, 'page1', 200, 300);
const id2 = await addNote(page, 'page2', 300, 500);
const id3 = await addNote(page, 'page3', 400, 700);
await page.mouse.click(200, 50);
// Notes added in edgeless mode only visible in edgeless mode
// To use index label navigator, we need to change display mode to PageAndEdgeless
await changeNoteDisplayModeWithId(
page,
id1,
NoteDisplayMode.DocAndEdgeless
);
await changeNoteDisplayModeWithId(
page,
id2,
NoteDisplayMode.DocAndEdgeless
);
await changeNoteDisplayModeWithId(
page,
id3,
NoteDisplayMode.DocAndEdgeless
);
await selectNoteInEdgeless(page, id1);
const bound = await getSelectedBound(page, 0);
await page.locator('.page-visible-index-label').nth(0).click();
await assertSelectedBound(page, bound);
await page.locator('.edgeless-auto-connect-next-button').click();
bound[0] += 100;
bound[1] += 200;
await assertSelectedBound(page, bound);
await page.locator('.edgeless-auto-connect-next-button').click();
bound[0] += 100;
bound[1] += 200;
await assertSelectedBound(page, bound);
});
test('should display index label when select note', async ({ page }) => {
await init(page);
const id1 = await addNote(page, 'page1', 200, 300);
const id2 = await addNote(page, 'page2', 300, 500);
await page.mouse.click(200, 50);
await changeNoteDisplayModeWithId(
page,
id1,
NoteDisplayMode.DocAndEdgeless
);
await selectNoteInEdgeless(page, id2);
const edgelessOnlyIndexLabel = page.locator('.edgeless-only-index-label');
await expect(edgelessOnlyIndexLabel).toBeVisible();
await expect(edgelessOnlyIndexLabel).toHaveCount(1);
await selectNoteInEdgeless(page, id1);
const pageVisibleIndexLabel = page.locator('.page-visible-index-label');
await expect(pageVisibleIndexLabel).toBeVisible();
await expect(pageVisibleIndexLabel).toHaveCount(1);
});
test('should hide index label when dragging note', async ({ page }) => {
await init(page);
const id1 = await addNote(page, 'page1', 200, 300);
await page.mouse.click(200, 50);
await changeNoteDisplayModeWithId(
page,
id1,
NoteDisplayMode.DocAndEdgeless
);
const pageVisibleIndexLabel = page.locator('.page-visible-index-label');
await expect(pageVisibleIndexLabel).toBeVisible();
await expect(pageVisibleIndexLabel).toHaveCount(1);
const bound = await getNoteBoundBoxInEdgeless(page, id1);
await page.mouse.move(
bound.x + bound.width / 2,
bound.y + bound.height / 2
);
await page.mouse.down();
await page.mouse.move(
bound.x + bound.width * 2,
bound.y + bound.height * 2
);
await expect(pageVisibleIndexLabel).not.toBeVisible();
await page.mouse.up();
await expect(pageVisibleIndexLabel).toBeVisible();
});
test('should update index label position after dragging', async ({
page,
}) => {
await init(page);
await zoomResetByKeyboard(page);
const id1 = await addNote(page, 'page1', 200, 300);
const id2 = await addNote(page, 'page2', 300, 500);
await page.mouse.click(200, 50);
await changeNoteDisplayModeWithId(
page,
id1,
NoteDisplayMode.DocAndEdgeless
);
await selectNoteInEdgeless(page, id2);
const edgelessOnlyIndexLabel = page.locator('.edgeless-only-index-label');
await expect(edgelessOnlyIndexLabel).toBeVisible();
// check initial index label position
const noteBound = await getNoteBoundBoxInEdgeless(page, id2);
const edgelessOnlyIndexLabelBound =
await edgelessOnlyIndexLabel.boundingBox();
assertExists(edgelessOnlyIndexLabelBound);
const border = 1;
const offset = 16;
expect(edgelessOnlyIndexLabelBound.x).toBeCloseTo(
noteBound.x +
noteBound.width / 2 -
edgelessOnlyIndexLabelBound.width / 2 +
border
);
expect(edgelessOnlyIndexLabelBound.y).toBeCloseTo(
noteBound.y + noteBound.height + offset
);
// move note
await dragBetweenViewCoords(
page,
[noteBound.x + noteBound.width / 2, noteBound.y + noteBound.height / 2],
[noteBound.x + noteBound.width, noteBound.y + noteBound.height]
);
// check new index label position
const newNoteBound = await getNoteBoundBoxInEdgeless(page, id2);
const newEdgelessOnlyIndexLabelBound =
await edgelessOnlyIndexLabel.boundingBox();
assertExists(newEdgelessOnlyIndexLabelBound);
expect(newEdgelessOnlyIndexLabelBound.x).toBeCloseTo(
newNoteBound.x +
newNoteBound.width / 2 -
newEdgelessOnlyIndexLabelBound.width / 2 +
border
);
expect(newEdgelessOnlyIndexLabelBound.y).toBeCloseTo(
newNoteBound.y + newNoteBound.height + offset
);
});
});

View File

@@ -0,0 +1,407 @@
import { assertExists } from '@blocksuite/global/utils';
import { expect } from '@playwright/test';
import {
createShapeElement,
decreaseZoomLevel,
deleteAll,
edgelessCommonSetup,
increaseZoomLevel,
locatorEdgelessComponentToolButton,
multiTouchDown,
multiTouchMove,
multiTouchUp,
optionMouseDrag,
Shape,
shiftClickView,
switchEditorMode,
ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH,
zoomByMouseWheel,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import {
addBasicBrushElement,
addBasicRectShapeElement,
captureHistory,
clickView,
enterPlaygroundRoom,
focusRichText,
initEmptyEdgelessState,
redoByClick,
switchReadonly,
type,
undoByClick,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertEdgelessNonSelectedRect,
assertEdgelessSelectedModelRect,
assertEdgelessSelectedRect,
assertNoteXYWH,
assertRichTextInlineRange,
assertRichTexts,
assertSelectedBound,
assertZoomLevel,
} from '../utils/asserts.js';
import {
DEFAULT_NOTE_HEIGHT,
DEFAULT_NOTE_WIDTH,
} from '../utils/bs-alternative.js';
import { test } from '../utils/playwright.js';
const CENTER_X = 450;
const CENTER_Y = 450;
test('switch to edgeless mode', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
await assertRichTextInlineRange(page, 0, 5, 0);
await switchEditorMode(page);
const locator = page.locator('affine-edgeless-root gfx-viewport');
await expect(locator).toHaveCount(1);
await assertRichTexts(page, ['hello']);
await waitNextFrame(page);
// FIXME: got very flaky result on cursor keeping
// await assertNativeSelectionRangeCount(page, 1);
});
test('can zoom viewport', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
await page.mouse.click(CENTER_X, CENTER_Y);
const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT];
await assertEdgelessSelectedModelRect(page, original);
await assertZoomLevel(page, 100);
await decreaseZoomLevel(page);
await assertZoomLevel(page, 75);
await decreaseZoomLevel(page);
await assertZoomLevel(page, 50);
const zoomed = [0, 0, original[2] * 0.5, original[3] * 0.5];
await assertEdgelessSelectedModelRect(page, zoomed);
await increaseZoomLevel(page);
await assertZoomLevel(page, 75);
await increaseZoomLevel(page);
await assertZoomLevel(page, 100);
await assertEdgelessSelectedModelRect(page, original);
});
test('zoom by mouse', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertZoomLevel(page, 100);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
await page.mouse.click(CENTER_X, CENTER_Y);
const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT];
await assertEdgelessSelectedModelRect(page, original);
await zoomByMouseWheel(page, 0, 125);
await assertZoomLevel(page, 90);
const zoomed = [0, 0, original[2] * 0.9, original[3] * 0.9];
await assertEdgelessSelectedModelRect(page, zoomed);
});
test('zoom by mouse without ctrl pressed when edgelessScrollZoom is enabled', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertZoomLevel(page, 100);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
await page.mouse.click(CENTER_X, CENTER_Y);
const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT];
await assertEdgelessSelectedModelRect(page, original);
// enable edgelessScrollZoom
await page.evaluate(() => {
// @ts-expect-error set a setting
window.editorSetting$.value = {
// @ts-expect-error set a setting
...window.editorSetting$.value,
edgelessScrollZoom: true,
};
});
// can zoom without ctrl pressed
await zoomByMouseWheel(page, 0, 125, false);
await assertZoomLevel(page, 90);
const zoomed = [0, 0, original[2] * 0.9, original[3] * 0.9];
await assertEdgelessSelectedModelRect(page, zoomed);
// disable edgelessScrollZoom
await page.evaluate(() => {
// @ts-expect-error set a setting
window.editorSetting$.value = {
// @ts-expect-error set a setting
...window.editorSetting$.value,
edgelessScrollZoom: false,
};
});
// can't zoom without ctrl pressed
await zoomByMouseWheel(page, 0, 125, false);
await assertZoomLevel(page, 90);
});
test('zoom by pinch', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertZoomLevel(page, 100);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
await page.mouse.click(CENTER_X, CENTER_Y);
const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT];
await assertEdgelessSelectedModelRect(page, original);
const from = [
{ x: CENTER_X - 100, y: CENTER_Y },
{ x: CENTER_X + 100, y: CENTER_Y },
];
const to = [
{ x: CENTER_X - 50, y: CENTER_Y - 35 },
{ x: CENTER_X + 50, y: CENTER_Y + 35 },
];
await multiTouchDown(page, from);
await multiTouchMove(page, from, to);
await multiTouchUp(page, to);
await assertZoomLevel(page, 50);
const zoomed = [0, 0, 0.5 * DEFAULT_NOTE_WIDTH, 46];
await assertEdgelessSelectedModelRect(page, zoomed);
});
test('zoom by pinch when edgeless is readonly', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertZoomLevel(page, 100);
await switchReadonly(page);
const from = [
{ x: CENTER_X - 100, y: CENTER_Y },
{ x: CENTER_X + 100, y: CENTER_Y },
];
const to = [
{ x: CENTER_X - 50, y: CENTER_Y - 35 },
{ x: CENTER_X + 50, y: CENTER_Y + 35 },
];
await multiTouchDown(page, from);
await multiTouchMove(page, from, to);
await multiTouchUp(page, to);
await switchReadonly(page, false);
await waitNextFrame(page);
await assertZoomLevel(page, 50);
});
test('move by pan', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertZoomLevel(page, 100);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
await page.mouse.click(CENTER_X, CENTER_Y);
const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT];
await assertEdgelessSelectedModelRect(page, original);
const from = [
{ x: CENTER_X - 100, y: CENTER_Y },
{ x: CENTER_X + 100, y: CENTER_Y },
];
const to = [
{ x: CENTER_X - 50, y: CENTER_Y + 50 },
{ x: CENTER_X + 150, y: CENTER_Y + 50 },
];
await multiTouchDown(page, from);
await multiTouchMove(page, from, to);
await multiTouchUp(page, to);
const moved = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT];
await assertEdgelessSelectedModelRect(page, moved);
});
test('option/alt mouse drag duplicate a new element', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await deleteAll(page);
const start = [0, 0];
const end = [100, 100];
await createShapeElement(page, start, end, Shape.Square);
await optionMouseDrag(page, [50, 50], [150, 50]);
await assertSelectedBound(page, [100, 0, 100, 100]);
await captureHistory(page);
await undoByClick(page);
await assertSelectedBound(page, [0, 0, 100, 100]);
await redoByClick(page);
await assertSelectedBound(page, [100, 0, 100, 100]);
});
test('should cancel select when the selected point is outside the current selected element', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
const firstStart = { x: 100, y: 100 };
const firstEnd = { x: 200, y: 200 };
await addBasicRectShapeElement(page, firstStart, firstEnd);
const secondStart = { x: 300, y: 300 };
const secondEnd = { x: 400, y: 400 };
await addBasicRectShapeElement(page, secondStart, secondEnd);
// select the first rect
await page.mouse.click(110, 150);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
// click outside the selected rect
await page.mouse.click(200, 200);
await assertEdgelessNonSelectedRect(page);
});
test('the tooltip of more button should be hidden when the action menu is shown', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicBrushElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
const moreButton = locatorEdgelessComponentToolButton(page, 'more');
await expect(moreButton).toBeVisible();
const moreButtonBox = await moreButton.boundingBox();
const tooltip = page.locator('.affine-tooltip');
assertExists(moreButtonBox);
// need to wait for previous tooltip to be hidden
await page.waitForTimeout(100);
await page.mouse.move(moreButtonBox.x + 10, moreButtonBox.y + 10);
await expect(tooltip).toBeVisible();
await page.mouse.click(moreButtonBox.x + 10, moreButtonBox.y + 10);
await expect(tooltip).toBeHidden();
await page.mouse.click(moreButtonBox.x + 10, moreButtonBox.y + 10);
await expect(tooltip).toBeVisible();
});
test('shift click multi select and de-select', async ({ page }) => {
await edgelessCommonSetup(page);
const start = [0, 0];
const end = [100, 100];
await createShapeElement(page, start, end, Shape.Square);
start[0] = 100;
end[0] = 200;
await createShapeElement(page, start, end, Shape.Square);
await clickView(page, [50, 0]);
await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]);
await shiftClickView(page, [150, 50]);
await assertEdgelessSelectedModelRect(page, [0, 0, 200, 100]);
// we will try to write text on a shape element when we dbclick it
await waitNextFrame(page, 500);
await shiftClickView(page, [150, 50]);
await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]);
});
test('Before and after switching to Edgeless, the previous zoom ratio and position when Edgeless was opened should be remembered', async ({
page,
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2479',
});
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertZoomLevel(page, 100);
await increaseZoomLevel(page);
await assertZoomLevel(page, 125);
await switchEditorMode(page);
await switchEditorMode(page);
await assertZoomLevel(page, 125);
});
test('should close zoom bar when click blank area', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const screenWidth = page.viewportSize()?.width;
assertExists(screenWidth);
if (screenWidth > ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH) {
await page.setViewportSize({
width: 1000,
height: 1000,
});
}
await zoomResetByKeyboard(page);
await assertZoomLevel(page, 100);
await increaseZoomLevel(page);
await assertZoomLevel(page, 125);
const verticalZoomBar = '.edgeless-zoom-toolbar-container.vertical';
const zoomBar = page.locator(verticalZoomBar);
await expect(zoomBar).toBeVisible();
// Click Blank Area
await page.mouse.click(10, 100);
await expect(zoomBar).toBeHidden();
});

View File

@@ -0,0 +1,195 @@
import { expect } from '@playwright/test';
import { lightThemeV2 } from '@toeverything/theme/v2';
import {
assertEdgelessTool,
deleteAll,
pickColorAtPoints,
selectBrushColor,
selectBrushSize,
setEdgelessTool,
switchEditorMode,
updateExistedBrushElementSize,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import {
addBasicBrushElement,
click,
dragBetweenCoords,
enterPlaygroundRoom,
initEmptyEdgelessState,
resizeElementByHandle,
} from '../utils/actions/index.js';
import {
assertEdgelessColorSameWithHexColor,
assertEdgelessSelectedRect,
assertSameColor,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('change editor mode when brush color palette opening', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'brush');
const brushMenu = page.locator('edgeless-brush-menu');
await expect(brushMenu).toBeVisible();
await switchEditorMode(page);
await expect(brushMenu).toBeHidden();
});
test('add brush element', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicBrushElement(page, start, end, false);
await assertEdgelessTool(page, 'brush');
});
test('resize brush element', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicBrushElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
await page.mouse.click(start.x + 5, start.y + 5);
const delta = { x: 20, y: 40 };
await resizeElementByHandle(page, delta, 'top-left', 10);
await page.mouse.click(start.x + 25, start.y + 45);
await assertEdgelessSelectedRect(page, [118, 138, 84, 64]);
});
test('add brush element with color', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'brush');
await selectBrushColor(page, 'Blue');
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await dragBetweenCoords(page, start, end, { steps: 100 });
const [pickedColor] = await pickColorAtPoints(page, [[110, 110]]);
const color = lightThemeV2['edgeless/palette/medium/blueMedium'];
await assertEdgelessColorSameWithHexColor(page, color, pickedColor);
});
test('keep same color when mouse mode switched back to brush', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await deleteAll(page);
await setEdgelessTool(page, 'brush');
await selectBrushColor(page, 'Blue');
const start = { x: 200, y: 200 };
const end = { x: 300, y: 300 };
await dragBetweenCoords(page, start, end, { steps: 100 });
await setEdgelessTool(page, 'default');
await click(page, { x: 50, y: 50 });
await setEdgelessTool(page, 'brush');
const origin = { x: 100, y: 100 };
await dragBetweenCoords(page, origin, start, { steps: 100 });
const [pickedColor] = await pickColorAtPoints(page, [[110, 110]]);
const color = lightThemeV2['edgeless/palette/medium/blueMedium'];
await assertEdgelessColorSameWithHexColor(page, color, pickedColor);
});
test('add brush element with different size', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'brush');
await selectBrushSize(page, 'ten');
await selectBrushColor(page, 'Blue');
const start = { x: 100, y: 100 };
const end = { x: 200, y: 100 };
await dragBetweenCoords(page, start, end, { steps: 100 });
const [topEdge, bottomEdge, nearTopEdge, nearBottomEdge] =
await pickColorAtPoints(page, [
// Select two points on the top and bottom border of the line,
// their color should be the same as the specified color
[110, 95],
[110, 104],
// Select two points close to the upper and lower boundaries of the line,
// their color should be different from the specified color
[110, 94],
[110, 105],
]);
const color = lightThemeV2['edgeless/palette/medium/blueMedium'];
await assertEdgelessColorSameWithHexColor(page, color, topEdge);
await assertEdgelessColorSameWithHexColor(page, color, bottomEdge);
assertSameColor(nearTopEdge, '#53b2ef');
assertSameColor(nearBottomEdge, '#53b2ef');
});
test('change brush element size by component-toolbar', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicBrushElement(page, start, end);
// wait for menu hide animation
await page.waitForTimeout(500);
// change to line width 12
await page.mouse.click(110, 110);
await updateExistedBrushElementSize(page, 6);
await assertEdgelessSelectedRect(page, [94, 94, 112, 112]);
// change to line width 10
await page.mouse.click(110, 110);
await updateExistedBrushElementSize(page, 5);
await assertEdgelessSelectedRect(page, [95, 95, 110, 110]);
// change to line width 8
await page.mouse.click(110, 110);
await updateExistedBrushElementSize(page, 4);
await assertEdgelessSelectedRect(page, [96, 96, 108, 108]);
// change to line width 6
await page.mouse.click(110, 110);
await updateExistedBrushElementSize(page, 3);
await assertEdgelessSelectedRect(page, [97, 97, 106, 106]);
// change to line width 4
await page.mouse.click(110, 110);
await updateExistedBrushElementSize(page, 2);
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
// change to line width 2
await page.mouse.click(110, 110);
await updateExistedBrushElementSize(page, 1);
await assertEdgelessSelectedRect(page, [99, 99, 102, 102]);
});

View File

@@ -0,0 +1,235 @@
import { expect } from '@playwright/test';
import {
createNote,
createShapeElement,
decreaseZoomLevel,
deleteAll,
getAllSortedIds,
Shape,
switchEditorMode,
toViewCoord,
triggerComponentToolbarAction,
} from '../utils/actions/edgeless.js';
import {
copyByKeyboard,
cutByKeyboard,
edgelessCommonSetup as commonSetup,
enterPlaygroundRoom,
expectConsoleMessage,
focusTitle,
getCurrentEditorDocId,
initEmptyEdgelessState,
mockParseDocUrlService,
pasteByKeyboard,
pasteContent,
selectAllByKeyboard,
type,
waitNextFrame,
} from '../utils/actions/index.js';
import { assertRichImage } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.describe('mime', () => {
test('should paste svg in text/plain mime', async ({ page }) => {
expectConsoleMessage(page, 'Error: Image sourceId is missing!', 'warning');
await commonSetup(page);
const content = {
'text/plain': `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
<script>alert("Malicious script executed!");</script>
</svg>
`,
};
await pasteContent(page, content);
// wait for paste
await page.waitForTimeout(200);
await assertRichImage(page, 1);
});
test('should not paste bad svg', async ({ page }) => {
expectConsoleMessage(page, 'BlockSuiteError: val does not exist', 'error');
expectConsoleMessage(page, 'Error: Image sourceId is missing!', 'warning');
await commonSetup(page);
const contents = [
{
'text/plain': `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
<script>alert("Malicious script executed!");</script>
`,
},
{
'text/plain': `<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
<script>alert("Malicious script executed!");</script>
</svg>
`,
},
];
for (const content of contents) {
await pasteContent(page, content);
}
await assertRichImage(page, 0);
});
});
test.describe('frame clipboard', () => {
test('copy and paste frame with shape elements inside', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [100, -100]);
await page.mouse.click(10, 50);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addFrame');
const originIds = await getAllSortedIds(page);
expect(originIds.length).toBe(3);
await copyByKeyboard(page);
const move = await toViewCoord(page, [250, 250]);
await page.mouse.move(move[0], move[1]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, true);
await waitNextFrame(page, 500);
const sortedIds = await getAllSortedIds(page);
expect(sortedIds.length).toBe(6);
});
test('copy and paste frame with group elements inside', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [100, -100]);
await page.mouse.click(10, 50);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await triggerComponentToolbarAction(page, 'createFrameOnMoreOption');
const originIds = await getAllSortedIds(page);
expect(originIds.length).toBe(5);
await selectAllByKeyboard(page);
await copyByKeyboard(page);
const move = await toViewCoord(page, [250, 250]);
await page.mouse.move(move[0], move[1]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, true);
await waitNextFrame(page, 500);
const sortedIds = await getAllSortedIds(page);
expect(sortedIds.length).toBe(10);
});
test('copy and paste frame with frame inside', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [100, -100]);
await page.mouse.click(10, 50);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addFrame');
await decreaseZoomLevel(page);
await createShapeElement(page, [700, 0], [800, 100], Shape.Square);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addFrame');
const originIds = await getAllSortedIds(page);
expect(originIds.length).toBe(5);
await copyByKeyboard(page);
const move = await toViewCoord(page, [250, 250]);
await page.mouse.move(move[0], move[1]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, true);
await waitNextFrame(page, 500);
const sortedIds = await getAllSortedIds(page);
expect(sortedIds.length).toBe(10);
});
test('cut frame with shape elements inside', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [100, -100]);
await page.mouse.click(10, 50);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addFrame');
const originIds = await getAllSortedIds(page);
expect(originIds.length).toBe(3);
await cutByKeyboard(page);
const move = await toViewCoord(page, [250, 250]);
await page.mouse.move(move[0], move[1]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, true);
await waitNextFrame(page, 500);
const sortedIds = await getAllSortedIds(page);
expect(sortedIds.length).toBe(3);
});
});
test.describe('pasting URLs', () => {
test('pasting github pr url', async ({ page }) => {
await commonSetup(page);
await waitNextFrame(page);
await pasteContent(page, {
'text/plain': 'https://github.com/toeverything/blocksuite/pull/7217',
});
await expect(
page.locator('affine-embed-edgeless-github-block')
).toBeVisible();
});
test('pasting internal link', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await waitNextFrame(page);
await focusTitle(page);
const docId = await getCurrentEditorDocId(page);
await type(page, 'doc title');
await switchEditorMode(page);
await deleteAll(page);
await mockParseDocUrlService(page, {
'http://workspace/doc-id': docId,
});
await pasteContent(page, {
'text/plain': 'http://workspace/doc-id',
});
await expect(
page.locator('affine-embed-edgeless-linked-doc-block')
).toBeVisible();
await expect(
page.locator('.affine-embed-linked-doc-content-title')
).toHaveText('doc title');
});
test('pasting external link', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await waitNextFrame(page);
await focusTitle(page);
await type(page, 'doc title');
await switchEditorMode(page);
await deleteAll(page);
await waitNextFrame(page);
await pasteContent(page, {
'text/plain': 'https://affine.pro',
});
await expect(page.locator('bookmark-card')).toBeVisible();
});
});

View File

@@ -0,0 +1,368 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import {
addBasicShapeElement,
Shape,
switchEditorMode,
triggerComponentToolbarAction,
} from '../utils/actions/edgeless.js';
import {
enterPlaygroundRoom,
initEmptyEdgelessState,
} from '../utils/actions/misc.js';
import { parseStringToRgba } from '../utils/bs-alternative.js';
import { test } from '../utils/playwright.js';
async function setupWithColorPickerFunction(page: Page) {
await enterPlaygroundRoom(page, { flags: { enable_color_picker: true } });
await initEmptyEdgelessState(page);
await switchEditorMode(page);
}
function getColorPickerButtonWithClass(page: Page, classes: string) {
return page.locator(`edgeless-color-picker-button.${classes}`);
}
function getCurrentColorUnitButton(locator: Locator) {
return locator.locator('edgeless-color-button').locator('.color-unit').nth(0);
}
function getCurrentColor(locator: Locator) {
return locator.evaluate(ele =>
getComputedStyle(ele.querySelector('svg')!).getPropertyValue('fill')
);
}
function getCustomButton(locator: Locator) {
return locator.locator('edgeless-color-custom-button');
}
function getColorPickerPanel(locator: Locator) {
return locator.locator('edgeless-color-picker');
}
function getPaletteControl(locator: Locator) {
return locator.locator('.color-palette');
}
function getHueControl(locator: Locator) {
return locator.locator('.color-slider-wrapper.hue .color-slider');
}
function getAlphaControl(locator: Locator) {
return locator.locator('.color-slider-wrapper.alpha .color-slider');
}
function getHexInput(locator: Locator) {
return locator.locator('label.color input');
}
function getAlphaInput(locator: Locator) {
return locator.locator('label.alpha input');
}
// Basic functions
test.describe('basic functions', () => {
test('custom color button should be displayed', async ({ page }) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
await expect(fillColorButton).toBeVisible();
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const customButton = getCustomButton(fillColorButton);
await expect(customButton).toBeVisible();
});
test('should open color-picker panel when clicking on custom color button', async ({
page,
}) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const customButton = getCustomButton(fillColorButton);
await customButton.click();
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await expect(colorPickerPanel).toBeVisible();
});
test('should close color-picker panel when clicking on outside', async ({
page,
}) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const currentColorUnit = getCurrentColorUnitButton(fillColorButton);
const value = await getCurrentColor(currentColorUnit);
await expect(currentColorUnit.locator('svg')).toHaveCSS('fill', value);
const customButton = getCustomButton(fillColorButton);
await customButton.click();
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await expect(colorPickerPanel).toBeVisible();
await colorPickerPanel.click({ position: { x: 0, y: 0 } });
await expect(colorPickerPanel).toBeVisible();
await page.mouse.click(0, 0);
await expect(colorPickerPanel).toBeHidden();
});
test('should return to the palette panel when re-clicking the color button', async ({
page,
}) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const customButton = getCustomButton(fillColorButton);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click();
await expect(colorPickerPanel).toBeVisible();
await page.mouse.click(0, 0);
await expect(colorPickerPanel).toBeHidden();
await dragBetweenCoords(page, { x: 125, y: 75 }, { x: 175, y: 225 });
await fillColorButton.click();
await expect(customButton).toBeVisible();
await expect(colorPickerPanel).toBeHidden();
});
test('should pick a color when clicking on the palette canvas', async ({
page,
}) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const customButton = getCustomButton(fillColorButton);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click();
const paletteControl = getPaletteControl(colorPickerPanel);
const hexInput = getHexInput(colorPickerPanel);
const value = await hexInput.inputValue();
await paletteControl.click();
const newValue = await hexInput.inputValue();
expect(value).not.toEqual(newValue);
});
test('should pick a color when clicking on the hue control', async ({
page,
}) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const customButton = getCustomButton(fillColorButton);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click();
const hueControl = getHueControl(colorPickerPanel);
const hexInput = getHexInput(colorPickerPanel);
const value = await hexInput.inputValue();
await hueControl.click();
const newValue = await hexInput.inputValue();
expect(value).not.toEqual(newValue);
});
test('should update color when changing the hex input', async ({ page }) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const customButton = getCustomButton(fillColorButton);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click();
const hexInput = getHexInput(colorPickerPanel);
await hexInput.fill('fff');
await page.keyboard.press('Enter');
await expect(hexInput).toHaveValue('ffffff');
await hexInput.fill('000000');
await page.keyboard.press('Enter');
await expect(hexInput).toHaveValue('000000');
await hexInput.fill('fff$');
await page.keyboard.press('Enter');
await expect(hexInput).toHaveValue('ffffff');
await hexInput.fill('#f0f');
await page.keyboard.press('Enter');
await expect(hexInput).toHaveValue('ff00ff');
});
test('should adjust alpha when clicking on the alpha control', async ({
page,
}) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const customButton = getCustomButton(fillColorButton);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click();
const alphaControl = getAlphaControl(colorPickerPanel);
const alphaInput = getAlphaInput(colorPickerPanel);
const value = await alphaInput.inputValue();
await alphaControl.click();
const newValue = await alphaInput.inputValue();
expect(value).not.toEqual(newValue);
});
test('should adjust alpha when changing the alpha input', async ({
page,
}) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const customButton = getCustomButton(fillColorButton);
const colorPickerPanel = getColorPickerPanel(fillColorButton);
await customButton.click();
const alphaInput = getAlphaInput(colorPickerPanel);
await alphaInput.fill('101');
await expect(alphaInput).toHaveValue('100');
await alphaInput.fill('-1');
await expect(alphaInput).toHaveValue('1');
await alphaInput.pressSequentially('--1');
await expect(alphaInput).toHaveValue('1');
await alphaInput.pressSequentially('++1');
await expect(alphaInput).toHaveValue('1');
await alphaInput.pressSequentially('-+1');
await expect(alphaInput).toHaveValue('1');
await alphaInput.pressSequentially('+-1');
await expect(alphaInput).toHaveValue('1');
await alphaInput.fill('23');
await expect(alphaInput).toHaveValue('23');
});
test('the computed style should be parsed correctly', async ({ page }) => {
await setupWithColorPickerFunction(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicShapeElement(page, start0, end0, Shape.Square);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
const fillColorButton = getColorPickerButtonWithClass(page, 'fill-color');
const currentColorUnit = getCurrentColorUnitButton(fillColorButton);
const value = await getCurrentColor(currentColorUnit);
let rgba = parseStringToRgba(value);
expect(rgba.a).toEqual(1);
rgba = parseStringToRgba('rgb(25.5,0,0)');
expect(rgba.r).toBeCloseTo(0.1);
rgba = parseStringToRgba('rgba(233,233,233, .5)');
expect(rgba.a).toEqual(0.5);
rgba = parseStringToRgba('transparent');
expect(rgba).toEqual({ r: 1, g: 1, b: 1, a: 0 });
rgba = parseStringToRgba('--blocksuite-transparent');
expect(rgba).toEqual({ r: 1, g: 1, b: 1, a: 0 });
rgba = parseStringToRgba('--affine-palette-transparent');
expect(rgba).toEqual({ r: 1, g: 1, b: 1, a: 0 });
rgba = parseStringToRgba('#ff0');
expect(rgba).toEqual({ r: 1, g: 1, b: 0, a: 1 });
rgba = parseStringToRgba('#ff09');
expect(rgba).toEqual({ r: 1, g: 1, b: 0, a: 0.6 });
});
});

View File

@@ -0,0 +1,142 @@
import { expect } from '@playwright/test';
import {
copyByKeyboard,
createConnectorElement,
createNote,
createShapeElement,
edgelessCommonSetup as commonSetup,
getAllSortedIds,
getTypeById,
pasteByKeyboard,
selectAllByKeyboard,
Shape,
toViewCoord,
triggerComponentToolbarAction,
waitNextFrame,
} from '../../utils/actions/index.js';
import { assertConnectorPath } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test.describe('connector clipboard', () => {
test('copy and paste connector whose both sides connect nothing', async ({
page,
}) => {
await commonSetup(page);
await createConnectorElement(page, [0, 0], [200, 100]);
await waitNextFrame(page);
await copyByKeyboard(page);
const move = await toViewCoord(page, [100, -50]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, false);
await waitNextFrame(page);
await assertConnectorPath(
page,
[
[0, -100],
[100, -100],
[100, 0],
[200, 0],
],
1
);
});
test('copy and paste connector whose both sides connect elements', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [60, 50], [240, 50]);
await selectAllByKeyboard(page);
await copyByKeyboard(page);
const move = await toViewCoord(page, [150, -50]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, false);
await waitNextFrame(page);
await assertConnectorPath(
page,
[
[100, -50],
[200, -50],
],
1
);
});
test('copy and paste connector whose both sides connect elements, but only paste connector', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [70, 50], [230, 50]);
await copyByKeyboard(page);
const move = await toViewCoord(page, [150, -50]);
await page.mouse.move(move[0], move[1]);
await pasteByKeyboard(page, false);
await waitNextFrame(page);
await assertConnectorPath(
page,
[
[100, -50],
[200, -50],
],
1
);
});
test('copy and paste connector whose one side connects elements', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createConnectorElement(page, [55, 50], [200, 50]);
await selectAllByKeyboard(page);
await copyByKeyboard(page);
const move = await toViewCoord(page, [100, -50]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, false);
await assertConnectorPath(
page,
[
[100, -50],
[200, -50],
],
1
);
});
test('original relative index should keep same when copy and paste group with note and shape', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [100, 50]);
await page.mouse.click(10, 50);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await copyByKeyboard(page);
const move = await toViewCoord(page, [250, 250]);
await page.mouse.move(move[0], move[1]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, true);
await waitNextFrame(page, 500);
const sortedIds = await getAllSortedIds(page);
expect(sortedIds.length).toBe(6);
expect(await getTypeById(page, sortedIds[0])).toBe(
await getTypeById(page, sortedIds[3])
);
expect(await getTypeById(page, sortedIds[1])).toBe(
await getTypeById(page, sortedIds[4])
);
expect(await getTypeById(page, sortedIds[2])).toBe(
await getTypeById(page, sortedIds[5])
);
});
});

View File

@@ -0,0 +1,320 @@
import { expect } from '@playwright/test';
import {
addBasicConnectorElement,
changeConnectorStrokeColor,
changeConnectorStrokeStyle,
changeConnectorStrokeWidth,
createConnectorElement,
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup as commonSetup,
pickColorAtPoints,
rotateElementByHandle,
Shape,
toModelCoord,
toViewCoord,
triggerComponentToolbarAction,
} from '../../utils/actions/edgeless.js';
import { pressBackspace, waitNextFrame } from '../../utils/actions/index.js';
import {
assertConnectorPath,
assertEdgelessNonSelectedRect,
assertEdgelessSelectedRect,
assertExists,
} from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test('path #1, the upper line is parallel with the lower line of antoher, and anchor from top to bottom of another', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, -100], [300, 0], Shape.Square);
await createConnectorElement(page, [50, 0], [250, 0]);
await waitNextFrame(page);
await assertConnectorPath(page, [
[50, 0],
[50, -20],
[150, -20],
[150, 20],
[250, 20],
[250, 0],
]);
});
test('path #2, the top-right point is overlapped with the bottom-left point of another, and anchor from top to bottom of another', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, -100], [200, 0], Shape.Square);
await createConnectorElement(page, [50, 0], [150, 0]);
await assertConnectorPath(page, [
[50, 0],
[50, -120],
[220, -120],
[220, 20],
[150, 20],
[150, 0],
]);
});
test('path #3, the two shape are parallel in x axis, the anchor from the right to right', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [100, 50], [300, 50]);
await assertConnectorPath(page, [
[100, 50],
[150, 50],
[150, 120],
[320, 120],
[320, 50],
[300, 50],
]);
});
test('when element is removed, connector should be deleted too', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createConnectorElement(page, [100, 50], [200, 0]);
//select
await dragBetweenViewCoords(page, [10, -10], [20, 20]);
await pressBackspace(page);
await dragBetweenViewCoords(page, [100, 50], [0, 50]);
await assertEdgelessNonSelectedRect(page);
});
test('connector connects triangle shape', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Triangle);
await createConnectorElement(page, [75, 50], [100, 50]);
await assertConnectorPath(page, [
[75, 50],
[100, 50],
]);
});
test('connector connects diamond shape', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Diamond);
await createConnectorElement(page, [100, 50], [200, 50]);
await assertConnectorPath(page, [
[100, 50],
[200, 50],
]);
});
test('connector connects rotated Square shape', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createConnectorElement(page, [50, 0], [50, -100]);
await dragBetweenViewCoords(page, [-10, 50], [60, 60]);
await rotateElementByHandle(page, 30, 'top-left');
await assertConnectorPath(page, [
[75, 6.7],
[75, -46.65],
[50, -46.65],
[50, -100],
]);
await rotateElementByHandle(page, 30, 'top-left');
await assertConnectorPath(page, [
[93.3, 25],
[138.3, 25],
[138.3, -38.3],
[50, -38.3],
[50, -100],
]);
});
test('change connector line width', async ({ page }) => {
await commonSetup(page);
const start = { x: 100, y: 200 };
const end = { x: 300, y: 300 };
await addBasicConnectorElement(page, start, end);
await page.mouse.click(start.x + 5, start.y);
await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor');
await changeConnectorStrokeColor(page, 'MediumGrey');
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
await changeConnectorStrokeWidth(page, 5);
await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
const pickedColor = await pickColorAtPoints(page, [
[start.x + 5, start.y],
[start.x + 10, start.y],
]);
expect(pickedColor[0]).toBe(pickedColor[1]);
});
test('change connector stroke style', async ({ page }) => {
await commonSetup(page);
const start = { x: 100, y: 200 };
const end = { x: 300, y: 300 };
await addBasicConnectorElement(page, start, end);
await page.mouse.click(start.x + 5, start.y);
await triggerComponentToolbarAction(page, 'changeConnectorStrokeColor');
await changeConnectorStrokeColor(page, 'MediumGrey');
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
await changeConnectorStrokeStyle(page, 'dash');
await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
const pickedColor = await pickColorAtPoints(page, [[start.x + 20, start.y]]);
expect(pickedColor[0]).toBe('#000000');
});
test.describe('quick connect', () => {
test('should create a connector when clicking on button', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
const [x, y] = await toViewCoord(page, [50, 50]);
await page.mouse.click(x, y);
const quickConnectBtn = page.getByRole('button', {
name: 'Draw connector',
});
await expect(quickConnectBtn).toBeVisible();
await quickConnectBtn.click();
await expect(quickConnectBtn).toBeHidden();
await assertConnectorPath(page, [
[100, 50],
[x, y],
]);
});
test('should be uncreated if the target is not found after clicking', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
const [x, y] = await toViewCoord(page, [50, 50]);
await page.mouse.click(x, y);
const quickConnectBtn = page.getByRole('button', {
name: 'Draw connector',
});
const bounds = await quickConnectBtn.boundingBox();
assertExists(bounds);
await quickConnectBtn.click();
await page.mouse.click(bounds.x, bounds.y);
await assertEdgelessSelectedRect(page, [x - 50, y - 50, 100, 100]);
});
test('should be uncreated if the target is not found after pressing ESC', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
// select shape
const [x, y] = await toViewCoord(page, [50, 50]);
await page.mouse.click(x, y);
// click button
await triggerComponentToolbarAction(page, 'quickConnect');
await page.keyboard.press('Escape');
await assertEdgelessNonSelectedRect(page);
});
test('should be connected if the target is found', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
// select shape
const [x, y] = await toViewCoord(page, [50, 50]);
await page.mouse.click(x, y);
// click button
await triggerComponentToolbarAction(page, 'quickConnect');
// click target
const [tx, ty] = await toViewCoord(page, [200, 50]);
await page.mouse.click(tx, ty);
await assertConnectorPath(page, [
[100, 50],
[200, 50],
]);
});
test('should follow the mouse to automatically select the starting point', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
const shapeBounds = await toViewCoord(page, [0, 0]);
// select shape
const [x, y] = await toViewCoord(page, [50, 50]);
await page.mouse.click(x, y);
// click button
const quickConnectBtn = page.getByRole('button', {
name: 'Draw connector',
});
const bounds = await quickConnectBtn.boundingBox();
assertExists(bounds);
await quickConnectBtn.click();
// at right
let point: [number, number] = [bounds.x, bounds.y];
let endpoint = await toModelCoord(page, point);
await assertConnectorPath(page, [[100, 50], endpoint]);
// at top
point = [shapeBounds[0] + 50, shapeBounds[1] - 50];
endpoint = await toModelCoord(page, point);
await page.mouse.move(...point);
await waitNextFrame(page);
await assertConnectorPath(page, [[50, 0], endpoint]);
// at left
point = [shapeBounds[0] - 50, shapeBounds[1] + 50];
endpoint = await toModelCoord(page, point);
await page.mouse.move(...point);
await assertConnectorPath(page, [[0, 50], endpoint]);
// at bottom
point = [shapeBounds[0] + 50, shapeBounds[1] + 100 + 50];
endpoint = await toModelCoord(page, point);
await page.mouse.move(...point);
await assertConnectorPath(page, [[50, 100], endpoint]);
});
});

View File

@@ -0,0 +1,206 @@
import {
assertEdgelessConnectorToolMode,
ConnectorMode,
createConnectorElement,
createShapeElement,
deleteAllConnectors,
dragBetweenViewCoords,
edgelessCommonSetup as commonSetup,
redoByClick,
setEdgelessTool,
Shape,
undoByClick,
} from '../../utils/actions/index.js';
import { assertConnectorPath } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test('elbow connector without node and width greater than height', async ({
page,
}) => {
await commonSetup(page);
await setEdgelessTool(page, 'connector');
await assertEdgelessConnectorToolMode(page, ConnectorMode.Curve);
await dragBetweenViewCoords(page, [0, 0], [200, 100]);
await assertConnectorPath(page, [
[0, 0],
[100, 0],
[100, 100],
[200, 100],
]);
});
test('elbow connector without node and width less than height', async ({
page,
}) => {
await commonSetup(page);
await createConnectorElement(page, [0, 0], [100, 200]);
await assertConnectorPath(page, [
[0, 0],
[0, 100],
[100, 100],
[100, 200],
]);
});
test('elbow connector one side attached element another side free', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createConnectorElement(page, [51, 50], [200, 0]);
await assertConnectorPath(page, [
[100, 50],
[150, 50],
[150, 0],
[200, 0],
]);
await deleteAllConnectors(page);
await createConnectorElement(page, [50, 50], [125, 0]);
await assertConnectorPath(page, [
[50, 0],
[125, 50],
[125, 0],
]);
});
test('elbow connector both side attatched element', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [50, 50], [249, 50]);
await assertConnectorPath(page, [
[50, 0],
[200, 50],
]);
// Could drag directly
// because the default shape type change to general style with filled color
await dragBetweenViewCoords(page, [250, 50], [250, 0]);
await assertConnectorPath(page, [
[50, 0],
[150, 50],
[150, 0],
[200, 0],
]);
await dragBetweenViewCoords(page, [250, 0], [150, -50]);
await assertConnectorPath(page, [
[50, 0],
[50, -50],
[100, -50],
]);
await dragBetweenViewCoords(page, [150, -50], [150, -150]);
await assertConnectorPath(page, [
[50, 0],
[50, -50],
[150, -50],
[150, -100],
]);
await undoByClick(page);
await assertConnectorPath(page, [
[50, 0],
[50, -50],
[100, -50],
]);
await undoByClick(page);
await assertConnectorPath(page, [
[50, 0],
[150, 50],
[150, 0],
[200, 0],
]);
await undoByClick(page);
await assertConnectorPath(page, [
[50, 0],
[200, 50],
]);
await redoByClick(page);
await assertConnectorPath(page, [
[50, 0],
[150, 50],
[150, 0],
[200, 0],
]);
await redoByClick(page);
await assertConnectorPath(page, [
[50, 0],
[50, -50],
[100, -50],
]);
await redoByClick(page);
await assertConnectorPath(page, [
[50, 0],
[50, -50],
[150, -50],
[150, -100],
]);
});
test('elbow connector both side attached element with one attach element and other is fixed', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [50, 0], [250, 50]);
await assertConnectorPath(page, [
[50, 0],
[50, -20],
[150, -20],
[150, 50],
[200, 50],
]);
// select
await dragBetweenViewCoords(page, [255, -10], [255, 55]);
await dragBetweenViewCoords(page, [250, 50], [250, 0]);
await assertConnectorPath(page, [
[50, 0],
[50, -20],
[150, -20],
[150, 0],
[200, 0],
]);
await dragBetweenViewCoords(page, [250, 0], [250, -20]);
await assertConnectorPath(page, [
[50, 0],
[50, -20],
[200, -20],
]);
await dragBetweenViewCoords(page, [250, -20], [150, -150]);
await assertConnectorPath(page, [
[50, 0],
[50, -50],
[150, -50],
[150, -100],
]);
});
test('elbow connector both side attached element with all fixed', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [50, 0], [300, 50]);
await assertConnectorPath(page, [
[50, 0],
[50, -20],
[320, -20],
[320, 50],
[300, 50],
]);
});

View File

@@ -0,0 +1,107 @@
import type { Page } from '@playwright/test';
import {
clickView,
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup as commonSetup,
moveView,
selectAllByKeyboard,
Shape,
triggerComponentToolbarAction,
waitNextFrame,
} from '../../utils/actions/index.js';
import { assertConnectorPath } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test.describe('groups connections', () => {
async function groupsSetup(page: Page) {
await commonSetup(page);
// group 1
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
// group 2
await createShapeElement(page, [500, 0], [600, 100], Shape.Square);
await createShapeElement(page, [600, 100], [700, 200], Shape.Square);
await dragBetweenViewCoords(page, [550, -50], [650, 250]);
await triggerComponentToolbarAction(page, 'addGroup');
await waitNextFrame(page);
}
test('should connect to other groups', async ({ page }) => {
await groupsSetup(page);
// click button
await triggerComponentToolbarAction(page, 'quickConnect');
// move to group 1
await moveView(page, [200, 50]);
await waitNextFrame(page);
await assertConnectorPath(page, [
[500, 100],
[200, 50],
]);
});
test('should connect to elements within other groups', async ({ page }) => {
await groupsSetup(page);
// click button
await triggerComponentToolbarAction(page, 'quickConnect');
// move to group 1
await moveView(page, [200, 100]);
await waitNextFrame(page);
await assertConnectorPath(page, [
[500, 100],
[200, 100],
]);
// move to elements within group 1
await moveView(page, [190, 150]);
await waitNextFrame(page);
await assertConnectorPath(page, [
[500, 100],
[200, 150],
]);
});
test('elements within groups should connect to other groups', async ({
page,
}) => {
await groupsSetup(page);
// click elements within group 1
await clickView(page, [40, 40]);
await clickView(page, [60, 60]);
// click button
await triggerComponentToolbarAction(page, 'quickConnect');
// move to elements within group 2
await moveView(page, [610, 50]);
await waitNextFrame(page);
await assertConnectorPath(page, [
[100, 50],
[600, 50],
]);
// move to group 2
await moveView(page, [600, 100]);
await waitNextFrame(page);
await assertConnectorPath(page, [
[100, 50],
[600, 100],
]);
});
});

View File

@@ -0,0 +1,334 @@
import { assertExists } from '@blocksuite/global/utils';
import { expect, type Page } from '@playwright/test';
import {
addBasicConnectorElement,
createConnectorElement,
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup as commonSetup,
locatorComponentToolbar,
setEdgelessTool,
Shape,
SHORT_KEY,
toViewCoord,
triggerComponentToolbarAction,
type,
waitNextFrame,
} from '../../utils/actions/index.js';
import {
assertConnectorPath,
assertEdgelessCanvasText,
assertPointAlmostEqual,
} from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test.describe('connector label with straight shape', () => {
async function getEditorCenter(page: Page) {
const bounds = await page
.locator('edgeless-connector-label-editor rich-text')
.boundingBox();
assertExists(bounds);
const cx = bounds.x + bounds.width / 2;
const cy = bounds.y + bounds.height / 2;
return [cx, cy];
}
function calcOffsetDistance(s: number[], e: number[], p: number[]) {
const p1 = Math.hypot(s[1] - p[1], s[0] - p[0]);
const f1 = Math.hypot(s[1] - e[1], s[0] - e[0]);
return p1 / f1;
}
test('should insert in the middle of the path when clicking on the button', async ({
page,
}) => {
await commonSetup(page);
const start = { x: 100, y: 200 };
const end = { x: 300, y: 300 };
await addBasicConnectorElement(page, start, end);
await page.mouse.click(105, 200);
await triggerComponentToolbarAction(page, 'addText');
await type(page, ' a ');
await assertEdgelessCanvasText(page, ' a ');
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.click(105, 200);
const addTextBtn = locatorComponentToolbar(page).getByRole('button', {
name: 'Add text',
});
await expect(addTextBtn).toBeHidden();
await page.mouse.dblclick(200, 250);
await assertEdgelessCanvasText(page, 'a');
await page.keyboard.press('Backspace');
await assertEdgelessCanvasText(page, '');
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.click(200, 250);
await expect(addTextBtn).toBeVisible();
});
test('should insert at the place when double clicking on the path', async ({
page,
}) => {
await commonSetup(page);
await setEdgelessTool(page, 'connector');
await page.mouse.move(0, 0);
const menu = page.locator('edgeless-connector-menu');
await expect(menu).toBeVisible();
const straightBtn = menu.locator('edgeless-tool-icon-button', {
hasText: 'Straight',
});
await expect(straightBtn).toBeVisible();
await straightBtn.click();
const start = { x: 250, y: 250 };
const end = { x: 500, y: 250 };
await addBasicConnectorElement(page, start, end);
await page.mouse.dblclick(300, 250);
await type(page, 'a');
await assertEdgelessCanvasText(page, 'a');
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.dblclick(300, 250);
await waitNextFrame(page);
await page.keyboard.press('ArrowRight');
await type(page, 'b');
await assertEdgelessCanvasText(page, 'ab');
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.dblclick(300, 250);
await waitNextFrame(page);
await type(page, 'c');
await assertEdgelessCanvasText(page, 'c');
await waitNextFrame(page);
const [cx, cy] = await getEditorCenter(page);
assertPointAlmostEqual([cx, cy], [300, 250]);
expect((cx - 250) / (500 - 250)).toBeCloseTo(50 / 250);
});
test('should move alone the path', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [100, 50], [200, 50]);
await dragBetweenViewCoords(page, [140, 40], [160, 60]);
await triggerComponentToolbarAction(page, 'changeConnectorShape');
const straightBtn = locatorComponentToolbar(page).getByRole('button', {
name: 'Straight',
});
await straightBtn.click();
await assertConnectorPath(page, [
[100, 50],
[200, 50],
]);
const [x, y] = await toViewCoord(page, [150, 50]);
await page.mouse.dblclick(x, y);
await type(page, 'label');
await assertEdgelessCanvasText(page, 'label');
await waitNextFrame(page);
let [cx, cy] = await getEditorCenter(page);
assertPointAlmostEqual([cx, cy], [x, y]);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await dragBetweenViewCoords(page, [150, 50], [130, 30]);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.dblclick(x - 20, y);
await waitNextFrame(page);
[cx, cy] = await getEditorCenter(page);
assertPointAlmostEqual([cx, cy], [x - 20, y]);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await dragBetweenViewCoords(page, [130, 50], [170, 70]);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.dblclick(x + 20, y);
await waitNextFrame(page);
[cx, cy] = await getEditorCenter(page);
assertPointAlmostEqual([cx, cy], [x + 20, y]);
});
test('should only move within constraints', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [100, 50], [200, 50]);
await assertConnectorPath(page, [
[100, 50],
[200, 50],
]);
const [x, y] = await toViewCoord(page, [150, 50]);
await page.mouse.dblclick(x, y);
await type(page, 'label');
await assertEdgelessCanvasText(page, 'label');
await waitNextFrame(page);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await dragBetweenViewCoords(page, [150, 50], [300, 110]);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.dblclick(x + 55, y);
await waitNextFrame(page);
let [cx, cy] = await getEditorCenter(page);
assertPointAlmostEqual([cx, cy], [x + 50, y]);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await dragBetweenViewCoords(page, [200, 50], [0, 50]);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.dblclick(x - 55, y);
await waitNextFrame(page);
[cx, cy] = await getEditorCenter(page);
assertPointAlmostEqual([cx, cy], [x - 50, y]);
});
test('should automatically adjust position via offset distance', async ({
page,
}) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createConnectorElement(page, [100, 50], [200, 50]);
await dragBetweenViewCoords(page, [140, 40], [160, 60]);
await triggerComponentToolbarAction(page, 'changeConnectorShape');
const straightBtn = locatorComponentToolbar(page).getByRole('button', {
name: 'Straight',
});
await straightBtn.click();
const point = [170, 50];
const offsetDistance = calcOffsetDistance([100, 50], [200, 50], point);
let [x, y] = await toViewCoord(page, point);
await page.mouse.dblclick(x, y);
await type(page, 'label');
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.dblclick(x, y);
await waitNextFrame(page);
let [cx, cy] = await getEditorCenter(page);
assertPointAlmostEqual([cx, cy], [x, y]);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.click(50, 50);
await waitNextFrame(page);
await dragBetweenViewCoords(page, [50, 50], [-50, 50]);
await waitNextFrame(page);
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.click(250, 50);
await waitNextFrame(page);
await dragBetweenViewCoords(page, [250, 50], [350, 50]);
await waitNextFrame(page);
const start = [0, 50];
const end = [300, 50];
const mx = start[0] + offsetDistance * (end[0] - start[0]);
const my = start[1] + offsetDistance * (end[1] - start[1]);
[x, y] = await toViewCoord(page, [mx, my]);
await page.mouse.dblclick(x, y);
await waitNextFrame(page);
[cx, cy] = await getEditorCenter(page);
assertPointAlmostEqual([cx, cy], [x, y]);
});
test('should enter the label editing state when pressing `Enter`', async ({
page,
}) => {
await commonSetup(page);
const start = { x: 100, y: 200 };
const end = { x: 300, y: 300 };
await addBasicConnectorElement(page, start, end);
await page.mouse.click(105, 200);
await page.keyboard.press('Enter');
await type(page, ' a ');
await assertEdgelessCanvasText(page, ' a ');
});
test('should exit the label editing state when pressing `Mod-Enter` or `Escape`', async ({
page,
}) => {
await commonSetup(page);
const start = { x: 100, y: 200 };
const end = { x: 300, y: 300 };
await addBasicConnectorElement(page, start, end);
await page.mouse.click(105, 200);
await page.keyboard.press('Enter');
await waitNextFrame(page);
await type(page, ' a ');
await assertEdgelessCanvasText(page, ' a ');
await page.keyboard.press(`${SHORT_KEY}+Enter`);
await page.keyboard.press('Enter');
await waitNextFrame(page);
await type(page, 'b');
await assertEdgelessCanvasText(page, 'b');
await page.keyboard.press('Escape');
await page.keyboard.press('Enter');
await waitNextFrame(page);
await type(page, 'c');
await assertEdgelessCanvasText(page, 'c');
});
});

View File

@@ -0,0 +1,598 @@
import type { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
import { Bound } from '@blocksuite/global/utils';
import { expect, type Page } from '@playwright/test';
import {
autoFit,
captureHistory,
cutByKeyboard,
dragBetweenIndices,
enterPlaygroundRoom,
getEdgelessSelectedRect,
getPageSnapshot,
initEmptyEdgelessState,
pasteByKeyboard,
pressArrowDown,
pressArrowLeft,
pressArrowRight,
pressArrowUp,
pressBackspace,
pressEnter,
pressEscape,
selectAllByKeyboard,
setEdgelessTool,
switchEditorMode,
toViewCoord,
type,
undoByKeyboard,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertBlockChildrenIds,
assertBlockFlavour,
assertBlockTextContent,
assertRichTextInlineDeltas,
assertRichTextInlineRange,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
import { getFormatBar } from '../utils/query.js';
async function assertEdgelessTextModelRect(
page: Page,
id: string,
bound: Bound
) {
const realXYWH = await page.evaluate(id => {
const block = window.host.view.getBlock(id) as EdgelessTextBlockComponent;
return block?.model.xywh;
}, id);
const realBound = Bound.deserialize(realXYWH);
expect(realBound.x).toBeCloseTo(bound.x, 0);
expect(realBound.y).toBeCloseTo(bound.y, 0);
expect(realBound.w).toBeCloseTo(bound.w, 0);
expect(realBound.h).toBeCloseTo(bound.h, 0);
}
test.describe('edgeless text block', () => {
test.beforeEach(async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
});
test('add text block in default mode', async ({ page }) => {
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140, {
delay: 100,
});
await waitNextFrame(page);
// https://github.com/toeverything/blocksuite/pull/8574
await pressBackspace(page);
await type(page, 'aaa');
await pressEnter(page);
await type(page, 'bbb');
await pressEnter(page);
await type(page, 'ccc');
await assertBlockFlavour(page, 4, 'affine:edgeless-text');
await assertBlockFlavour(page, 5, 'affine:paragraph');
await assertBlockFlavour(page, 6, 'affine:paragraph');
await assertBlockFlavour(page, 7, 'affine:paragraph');
await assertBlockChildrenIds(page, '4', ['5', '6', '7']);
await assertBlockTextContent(page, 5, 'aaa');
await assertBlockTextContent(page, 6, 'bbb');
await assertBlockTextContent(page, 7, 'ccc');
await dragBetweenIndices(page, [1, 1], [3, 2]);
await captureHistory(page);
await pressBackspace(page);
await assertBlockChildrenIds(page, '4', ['5']);
await assertBlockTextContent(page, 5, 'ac');
await undoByKeyboard(page);
await assertBlockChildrenIds(page, '4', ['5', '6', '7']);
await assertBlockTextContent(page, 5, 'aaa');
await assertBlockTextContent(page, 6, 'bbb');
await assertBlockTextContent(page, 7, 'ccc');
const { boldBtn } = getFormatBar(page);
await boldBtn.click();
await assertRichTextInlineDeltas(
page,
[
{
insert: 'a',
},
{
insert: 'aa',
attributes: {
bold: true,
},
},
],
1
);
await assertRichTextInlineDeltas(
page,
[
{
insert: 'bbb',
attributes: {
bold: true,
},
},
],
2
);
await assertRichTextInlineDeltas(
page,
[
{
insert: 'cc',
attributes: {
bold: true,
},
},
{
insert: 'c',
},
],
3
);
await pressArrowRight(page);
await assertRichTextInlineRange(page, 3, 2);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 2, 2);
});
test('edgeless text width auto-adjusting', async ({ page }) => {
await setEdgelessTool(page, 'default');
const point = await toViewCoord(page, [0, 0]);
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
await waitNextFrame(page);
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 220, 26));
await type(page, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
await waitNextFrame(page, 1000);
// just width changed
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 323, 26));
await type(page, '\nbbb');
// width not changed, height changed
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 323, 50));
await type(page, '\nccccccccccccccccccccccccccccccccccccccccccccccccc');
await waitNextFrame(page, 1000);
// width and height changed
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 395, 74));
// blur, max width set to true
await page.mouse.click(point[0] - 50, point[1], {
delay: 100,
});
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
// to end of line
await pressArrowDown(page, 3);
await type(page, 'dddddddddddddddddddd');
await waitNextFrame(page, 1000);
// width not changed, height changed
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 395, 98));
});
test('edgeless text width fixed when drag moving', async ({ page }) => {
// https://github.com/toeverything/blocksuite/pull/7486
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140, {
delay: 100,
});
await waitNextFrame(page);
await type(page, 'aaaaaa bbbb ');
await pressEscape(page);
await waitNextFrame(page);
await page.mouse.click(130, 140);
await page.mouse.down();
await page.mouse.move(800, 800, {
steps: 15,
});
const rect = await page.evaluate(() => {
const container = document.querySelector(
'.edgeless-text-block-container'
)!;
return container.getBoundingClientRect();
});
const modelXYWH = await page.evaluate(() => {
const block = window.host.view.getBlock(
'4'
) as EdgelessTextBlockComponent;
return block.model.xywh;
});
const bound = Bound.deserialize(modelXYWH);
expect(rect.width).toBeCloseTo(bound.w);
expect(rect.height).toBeCloseTo(bound.h);
});
test('When creating edgeless text, if the input is empty, it will be automatically deleted', async ({
page,
}) => {
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140, {
delay: 100,
});
await waitNextFrame(page);
let block = page.locator('affine-edgeless-text[data-block-id="4"]');
expect(await block.isVisible()).toBe(true);
await page.mouse.click(0, 0);
expect(await block.isVisible()).toBe(false);
block = page.locator('affine-edgeless-text[data-block-id="6"]');
expect(await block.isVisible()).not.toBe(true);
await page.mouse.dblclick(130, 140, {
delay: 100,
});
expect(await block.isVisible()).toBe(true);
await type(page, '\na');
expect(await block.isVisible()).toBe(true);
await page.mouse.click(0, 0);
expect(await block.isVisible()).not.toBe(false);
});
test('edgeless text should maintain selection when deleting across multiple lines', async ({
page,
}) => {
// https://github.com/toeverything/blocksuite/pull/7443
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140, {
delay: 100,
});
await waitNextFrame(page);
await type(page, 'aaaa\nbbbb');
await assertBlockTextContent(page, 5, 'aaaa');
await assertBlockTextContent(page, 6, 'bbbb');
await pressArrowLeft(page);
await page.keyboard.down('Shift');
await pressArrowLeft(page, 3);
await pressArrowUp(page);
await pressArrowRight(page);
await page.keyboard.up('Shift');
await pressBackspace(page);
await assertBlockTextContent(page, 5, 'ab');
await type(page, 'sss\n');
await assertBlockTextContent(page, 5, 'asss');
await assertBlockTextContent(page, 7, 'b');
});
test('edgeless text should not blur after pressing backspace', async ({
page,
}) => {
// https://github.com/toeverything/blocksuite/pull/7555
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140, {
delay: 100,
});
await waitNextFrame(page);
await type(page, 'a');
await assertBlockTextContent(page, 5, 'a');
await pressBackspace(page);
await type(page, 'b');
await assertBlockTextContent(page, 5, 'b');
});
// FIXME(@flrande): This test fails randomly on CI
test.fixme('edgeless text max width', async ({ page }) => {
await setEdgelessTool(page, 'default');
const point = await toViewCoord(page, [0, 0]);
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
await waitNextFrame(page);
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 50, 56));
await type(page, 'aaaaaa');
await waitNextFrame(page);
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 71, 56));
await type(page, 'bbb');
await waitNextFrame(page, 200);
// height not changed
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 98, 56));
// blur
await page.mouse.click(0, 0);
// select text element
await page.mouse.click(point[0] + 10, point[1] + 10);
let selectedRect = await getEdgelessSelectedRect(page);
// move cursor to the right edge and drag it to resize the width of text
// from left to right
await page.mouse.move(
selectedRect.x + selectedRect.width,
selectedRect.y + selectedRect.height / 2
);
await page.mouse.down();
await page.mouse.move(
selectedRect.x + selectedRect.width + 30,
selectedRect.y + selectedRect.height / 2,
{
steps: 10,
}
);
await page.mouse.up();
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 128, 56));
selectedRect = await getEdgelessSelectedRect(page);
let textRect = await page
.locator('affine-edgeless-text[data-block-id="4"]')
.boundingBox();
expect(selectedRect).not.toBeNull();
expect(selectedRect.width).toBeCloseTo(textRect!.width);
expect(selectedRect.height).toBeCloseTo(textRect!.height);
expect(selectedRect.x).toBeCloseTo(textRect!.x);
expect(selectedRect.y).toBeCloseTo(textRect!.y);
// from right to left
await page.mouse.move(
selectedRect.x + selectedRect.width,
selectedRect.y + selectedRect.height / 2
);
await page.mouse.down();
await page.mouse.move(
selectedRect.x + selectedRect.width - 45,
selectedRect.y + selectedRect.height / 2,
{
steps: 10,
}
);
await page.mouse.up();
// height changed
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 83, 80));
selectedRect = await getEdgelessSelectedRect(page);
textRect = await page
.locator('affine-edgeless-text[data-block-id="4"]')
.boundingBox();
expect(selectedRect).not.toBeNull();
expect(selectedRect.width).toBeCloseTo(textRect!.width);
expect(selectedRect.height).toBeCloseTo(textRect!.height);
expect(selectedRect.x).toBeCloseTo(textRect!.x);
expect(selectedRect.y).toBeCloseTo(textRect!.y);
});
test('min width limit for embed block', async ({ page }, testInfo) => {
await setEdgelessTool(page, 'default');
const point = await toViewCoord(page, [0, 0]);
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
await waitNextFrame(page, 2000);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await type(page, '@');
await pressEnter(page);
await waitNextFrame(page, 200);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_add_linked_doc.json`
);
await page.locator('affine-reference').hover();
await page.getByLabel('Switch view').click();
await page.getByTestId('link-to-card').click();
await autoFit(page);
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_link_to_card.json`
);
// blur
await page.mouse.click(0, 0);
// select text element
await page.mouse.click(point[0] + 10, point[1] + 10);
await waitNextFrame(page, 200);
const selectedRect0 = await getEdgelessSelectedRect(page);
// from right to left
await page.mouse.move(
selectedRect0.x + selectedRect0.width,
selectedRect0.y + selectedRect0.height / 2
);
await page.mouse.down();
await page.mouse.move(
selectedRect0.x,
selectedRect0.y + selectedRect0.height / 2,
{
steps: 10,
}
);
await page.mouse.up();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_link_to_card_min_width.json`
);
const selectedRect1 = await getEdgelessSelectedRect(page);
// from left to right
await page.mouse.move(
selectedRect1.x + selectedRect1.width,
selectedRect1.y + selectedRect1.height / 2
);
await page.mouse.down();
await page.mouse.move(
selectedRect0.x + selectedRect0.width + 45,
selectedRect1.y + selectedRect1.height / 2,
{
steps: 10,
}
);
await page.mouse.up();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_drag.json`
);
});
test('cut edgeless text', async ({ page }) => {
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140, {
delay: 100,
});
await waitNextFrame(page);
await type(page, 'aaaa\nbbbb\ncccc');
const edgelessText = page.locator('affine-edgeless-text');
const paragraph = page.locator('affine-edgeless-text affine-paragraph');
expect(await edgelessText.count()).toBe(1);
expect(await paragraph.count()).toBe(3);
await page.mouse.click(50, 50, {
delay: 100,
});
await waitNextFrame(page);
await page.mouse.click(130, 140, {
delay: 100,
});
await cutByKeyboard(page);
expect(await edgelessText.count()).toBe(0);
expect(await paragraph.count()).toBe(0);
await pasteByKeyboard(page);
expect(await edgelessText.count()).toBe(1);
expect(await paragraph.count()).toBe(3);
});
test('latex in edgeless text', async ({ page }) => {
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140, {
delay: 100,
});
await waitNextFrame(page);
await type(page, '$$bbb$$ ');
await assertRichTextInlineDeltas(
page,
[
{
insert: ' ',
attributes: {
latex: 'bbb',
},
},
],
1
);
await page.locator('affine-latex-node').click();
await waitNextFrame(page);
await type(page, 'ccc');
const menu = page.locator('latex-editor-menu');
const confirm = menu.locator('.latex-editor-confirm');
await confirm.click();
await assertRichTextInlineDeltas(
page,
[
{
insert: ' ',
attributes: {
latex: 'bbbccc',
},
},
],
1
);
await page.locator('affine-latex-node').click();
await page.locator('.latex-editor-hint').click();
await type(page, 'sss');
await assertRichTextInlineDeltas(
page,
[
{
insert: ' ',
attributes: {
latex: 'bbbccc',
},
},
],
1
);
await page.locator('latex-editor-unit').click();
await selectAllByKeyboard(page);
await type(page, 'sss');
await confirm.click();
await assertRichTextInlineDeltas(
page,
[
{
insert: ' ',
attributes: {
latex: 'sss',
},
},
],
1
);
});
});
test('press backspace at the start of first line when edgeless text exist', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await page.evaluate(() => {
const { doc } = window;
const rootId = doc.addBlock('affine:page', {
title: new window.$blocksuite.store.Text(),
});
doc.addBlock('affine:surface', {}, rootId);
doc.addBlock('affine:note', {}, rootId);
// do not add paragraph block
doc.resetHistory();
});
await switchEditorMode(page);
await setEdgelessTool(page, 'default');
const point = await toViewCoord(page, [0, 0]);
await page.mouse.dblclick(point[0], point[1], {
delay: 100,
});
await waitNextFrame(page, 2000);
await type(page, 'aaa');
await waitNextFrame(page);
await switchEditorMode(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_note_empty.json`
);
await page.locator('.affine-page-root-block-container').click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_note_not_empty.json`
);
await type(page, 'bbb');
await pressArrowLeft(page, 3);
await pressBackspace(page);
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});

View File

@@ -0,0 +1,88 @@
import { expect } from '@playwright/test';
import {
addBasicRectShapeElement,
locatorComponentToolbar,
resizeElementByHandle,
selectNoteInEdgeless,
switchEditorMode,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import {
enterPlaygroundRoom,
initEmptyEdgelessState,
} from '../utils/actions/misc.js';
import { test } from '../utils/playwright.js';
test('toolbar should appear when select note', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
const toolbar = locatorComponentToolbar(page);
await expect(toolbar).toBeVisible();
});
test('tooltip should be hidden after clicking on button', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
const toolbar = locatorComponentToolbar(page);
const modeBtn = toolbar.getByRole('button', { name: 'Mode' });
await modeBtn.hover();
await expect(page.locator('.blocksuite-portal')).toBeVisible();
await modeBtn.click();
await expect(page.locator('.blocksuite-portal')).toBeHidden();
await expect(page.locator('note-display-mode-panel')).toBeVisible();
await modeBtn.click();
await expect(page.locator('.blocksuite-portal')).toBeVisible();
await expect(page.locator('note-display-mode-panel')).toBeHidden();
await modeBtn.click();
await expect(page.locator('.blocksuite-portal')).toBeHidden();
await expect(page.locator('note-display-mode-panel')).toBeVisible();
const colorBtn = toolbar.getByRole('button', {
name: 'Background',
});
await colorBtn.hover();
await expect(page.locator('.blocksuite-portal')).toBeVisible();
await colorBtn.click();
await expect(page.locator('.blocksuite-portal')).toBeHidden();
await expect(page.locator('note-display-mode-panel')).toBeHidden();
await expect(page.locator('edgeless-color-panel')).toBeVisible();
});
test('should be hidden when resizing element', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await addBasicRectShapeElement(page, { x: 210, y: 110 }, { x: 310, y: 210 });
await page.mouse.click(220, 120);
const toolbar = locatorComponentToolbar(page);
await expect(toolbar).toBeVisible();
await resizeElementByHandle(page, { x: 400, y: 300 }, 'top-left', 30);
await page.mouse.move(450, 300);
await expect(toolbar).toBeEmpty();
await page.mouse.move(320, 220);
await expect(toolbar).toBeEmpty();
await page.mouse.up();
await expect(toolbar).toBeVisible();
});

View File

@@ -0,0 +1,49 @@
import { click } from '../utils/actions/click.js';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import {
addBasicRectShapeElement,
deleteAll,
getNoteBoundBoxInEdgeless,
setEdgelessTool,
switchEditorMode,
} from '../utils/actions/edgeless.js';
import {
enterPlaygroundRoom,
initEmptyEdgelessState,
} from '../utils/actions/misc.js';
import {
assertBlockCount,
assertEdgelessNonSelectedRect,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('erase shape', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await deleteAll(page);
await addBasicRectShapeElement(page, { x: 0, y: 0 }, { x: 100, y: 100 });
await setEdgelessTool(page, 'eraser');
await dragBetweenCoords(page, { x: 50, y: 150 }, { x: 50, y: 50 });
await click(page, { x: 50, y: 50 });
await assertEdgelessNonSelectedRect(page);
});
test('erase note', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await assertBlockCount(page, 'edgeless-note', 1);
await setEdgelessTool(page, 'eraser');
const box = await getNoteBoundBoxInEdgeless(page, noteId);
await dragBetweenCoords(
page,
{ x: 0, y: 0 },
{ x: box.x + 10, y: box.y + 10 }
);
await assertBlockCount(page, 'edgeless-note', 0);
});

View File

@@ -0,0 +1,157 @@
import type { Page } from '@playwright/test';
import { clickView, moveView } from '../../utils/actions/click.js';
import {
autoFit,
createFrame as _createFrame,
createShapeElement,
deleteAll,
dragBetweenViewCoords,
edgelessCommonSetup,
getAllSortedIds,
getFirstContainerId,
getIds,
Shape,
shiftClickView,
triggerComponentToolbarAction,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
copyByKeyboard,
pasteByKeyboard,
pressBackspace,
pressEscape,
} from '../../utils/actions/keyboard.js';
import { assertContainerOfElements } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
const createFrame = async (
page: Page,
coord1: [number, number],
coord2: [number, number]
) => {
await _createFrame(page, coord1, coord2);
await autoFit(page);
};
test.beforeEach(async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
});
test.describe('frame copy and paste', () => {
test('copy of frame should keep relationship of child elements', async ({
page,
}) => {
await createFrame(page, [50, 50], [450, 450]);
await createShapeElement(page, [200, 200], [300, 300], Shape.Square);
const frameTitle = page.locator('affine-frame-title');
await pressEscape(page);
await frameTitle.click();
await copyByKeyboard(page);
await deleteAll(page);
await moveView(page, [500, 500]); // center copy
await pasteByKeyboard(page);
const frameId = await getFirstContainerId(page);
const shapeId = (await getAllSortedIds(page)).filter(id => id !== frameId);
await assertContainerOfElements(page, shapeId, frameId);
});
test('copy of frame by alt/option dragging should keep relationship of child elements', async ({
page,
}) => {
await createFrame(page, [50, 50], [450, 450]);
await createShapeElement(page, [200, 200], [300, 300], Shape.Square);
await createShapeElement(page, [250, 250], [350, 350], Shape.Square);
await createShapeElement(page, [300, 300], [400, 400], Shape.Square);
await pressEscape(page);
const frameTitles = page.locator('affine-frame-title');
await shiftClickView(page, [260, 260]);
await shiftClickView(page, [310, 310]);
await triggerComponentToolbarAction(page, 'addGroup');
await pressEscape(page);
await frameTitles.nth(0).click();
await page.keyboard.down('Alt');
await dragBetweenViewCoords(page, [60, 60], [460, 460]);
await page.keyboard.up('Alt');
await pressEscape(page);
await frameTitles.nth(0).click({ modifiers: ['Shift'] });
await shiftClickView(page, [250, 250]);
await shiftClickView(page, [350, 350]);
await pressBackspace(page); // remove original elements
const frameId = await getFirstContainerId(page);
const groupId = await getFirstContainerId(page, [frameId]);
const shapeIds = (await getIds(page)).filter(
id => ![frameId, groupId].includes(id)
);
await assertContainerOfElements(page, [groupId], frameId);
await assertContainerOfElements(page, [shapeIds[0]], frameId);
await assertContainerOfElements(page, [shapeIds[1]], groupId);
await assertContainerOfElements(page, [shapeIds[2]], groupId);
});
test('duplicate element in frame', async ({ page }) => {
await createFrame(page, [50, 50], [450, 450]);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await pressEscape(page);
const frameTitles = page.locator('affine-frame-title');
await frameTitles.nth(0).click();
await page.locator('edgeless-more-button').click();
await page.locator('editor-menu-action', { hasText: 'Duplicate' }).click();
await pressEscape(page);
await frameTitles.nth(0).click();
await shiftClickView(page, [150, 150]);
await pressBackspace(page); // remove original elements
const frameId = await getFirstContainerId(page);
const shapeIds = (await getIds(page)).filter(id => id !== frameId);
await assertContainerOfElements(page, shapeIds, frameId);
});
test('copy of element by alt/option dragging in frame should belong to frame', async ({
page,
}) => {
await createFrame(page, [50, 50], [450, 450]);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await pressEscape(page);
await clickView(page, [150, 150]);
await page.keyboard.down('Alt');
await dragBetweenViewCoords(page, [150, 150], [250, 250]);
await page.keyboard.up('Alt');
const frameId = await getFirstContainerId(page);
const shapeIds = (await getIds(page)).filter(id => id !== frameId);
await assertContainerOfElements(page, shapeIds, frameId);
});
test('copy of element by alt/option dragging out of frame should not belong to frame', async ({
page,
}) => {
await createFrame(page, [50, 50], [450, 450]);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await pressEscape(page);
await clickView(page, [150, 150]);
await page.keyboard.down('Alt');
await dragBetweenViewCoords(page, [150, 150], [550, 550]);
await page.keyboard.up('Alt');
const frameId = await getFirstContainerId(page);
const shapeIds = (await getIds(page)).filter(id => id !== frameId);
await assertContainerOfElements(page, [shapeIds[0]], frameId);
await assertContainerOfElements(page, [shapeIds[1]], null);
});
});

View File

@@ -0,0 +1,224 @@
import type { Page } from '@playwright/test';
import { clickView } from '../../utils/actions/click.js';
import {
createFrame,
dragBetweenViewCoords as _dragBetweenViewCoords,
edgelessCommonSetup,
getFirstContainerId,
getSelectedBound,
toViewCoord,
triggerComponentToolbarAction,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import { pressEscape } from '../../utils/actions/keyboard.js';
import { waitNextFrame } from '../../utils/actions/misc.js';
import { assertContainerOfElements } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
const dragBetweenViewCoords = async (
page: Page,
start: number[],
end: number[]
) => {
// dragging slowly may drop frame if mindmap is existed, so for test we drag quickly
await _dragBetweenViewCoords(page, start, end, { steps: 2 });
await waitNextFrame(page);
};
test.beforeEach(async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
});
test('drag root node of mindmap into frame partially, then drag root node of mindmap out.', async ({
page,
}) => {
await createFrame(page, [50, 50], [550, 550]);
await pressEscape(page);
const frameId = await getFirstContainerId(page);
await triggerComponentToolbarAction(page, 'addMindmap');
const mindmapId = await getFirstContainerId(page, [frameId]);
// drag in
{
const mindmapBound = await getSelectedBound(page);
await clickView(page, [
mindmapBound[0] + 10,
mindmapBound[1] + 0.5 * mindmapBound[3],
]);
await dragBetweenViewCoords(
page,
[mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]],
[100, 100]
);
}
await assertContainerOfElements(page, [mindmapId], frameId);
// drag out
{
const mindmapBound = await getSelectedBound(page);
await clickView(page, [
mindmapBound[0] + 10,
mindmapBound[1] + 0.5 * mindmapBound[3],
]);
await dragBetweenViewCoords(
page,
[mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]],
[-100, -100]
);
}
await assertContainerOfElements(page, [mindmapId], null);
});
test('drag root node of mindmap into frame fully, then drag root node of mindmap out.', async ({
page,
}) => {
await createFrame(page, [50, 50], [550, 550]);
const frameId = await getFirstContainerId(page);
await pressEscape(page);
await triggerComponentToolbarAction(page, 'addMindmap');
const mindmapId = await getFirstContainerId(page, [frameId]);
// drag in
{
const mindmapBound = await getSelectedBound(page);
await clickView(page, [
mindmapBound[0] + 10,
mindmapBound[1] + 0.5 * mindmapBound[3],
]);
await dragBetweenViewCoords(
page,
[mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]],
[100, 200]
);
}
await assertContainerOfElements(page, [mindmapId], frameId);
// drag out
{
const mindmapBound = await getSelectedBound(page);
await clickView(page, [
mindmapBound[0] + 10,
mindmapBound[1] + 0.5 * mindmapBound[3],
]);
await dragBetweenViewCoords(
page,
[mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]],
[-100, -100]
);
}
await assertContainerOfElements(page, [mindmapId], null);
});
test('drag whole mindmap into frame, then drag root node of mindmap out.', async ({
page,
}) => {
await createFrame(page, [50, 50], [550, 550]);
const frameId = await getFirstContainerId(page);
await pressEscape(page);
await triggerComponentToolbarAction(page, 'addMindmap');
const mindmapId = await getFirstContainerId(page, [frameId]);
// drag in
{
const mindmapBound = await getSelectedBound(page);
const rootNodePos = [
mindmapBound[0] + 10,
mindmapBound[1] + 0.5 * mindmapBound[3],
];
await dragBetweenViewCoords(page, rootNodePos, [
rootNodePos[0] - 20,
rootNodePos[1] + 200,
]);
}
await assertContainerOfElements(page, [mindmapId], frameId);
// drag out
{
const mindmapBound = await getSelectedBound(page);
const rootNodePos = [
mindmapBound[0] + 10,
mindmapBound[1] + 0.5 * mindmapBound[3],
];
await dragBetweenViewCoords(page, rootNodePos, [-100, -100]);
}
await assertContainerOfElements(page, [mindmapId], null);
});
test('add mindmap into frame, then drag root node of mindmap out.', async ({
page,
}) => {
await createFrame(page, [50, 50], [550, 550]);
const frameId = await getFirstContainerId(page);
await pressEscape(page);
const button = page.locator('edgeless-mindmap-tool-button');
await button.click();
await toViewCoord(page, [100, 200]);
await clickView(page, [100, 200]);
const mindmapId = await getFirstContainerId(page, [frameId]);
await assertContainerOfElements(page, [mindmapId], frameId);
// drag out
{
const mindmapBound = await getSelectedBound(page);
pressEscape(page);
await clickView(page, [
mindmapBound[0] + 10,
mindmapBound[1] + 0.5 * mindmapBound[3],
]);
await dragBetweenViewCoords(
page,
[mindmapBound[0] + 10, mindmapBound[1] + 0.5 * mindmapBound[3]],
[-20, -20]
);
}
await assertContainerOfElements(page, [mindmapId], null);
});
test('add mindmap out of frame and add new node in frame then drag frame', async ({
page,
}) => {
await createFrame(page, [500, 50], [1000, 550]);
await pressEscape(page);
const button = page.locator('edgeless-mindmap-tool-button');
await button.click();
await toViewCoord(page, [20, 200]);
await clickView(page, [20, 200]);
await waitNextFrame(page, 100);
const mindmapId = await getFirstContainerId(page);
// add new node
{
const mindmapBound = await getSelectedBound(page);
await pressEscape(page);
await waitNextFrame(page, 500);
await clickView(page, [
mindmapBound[2] - 50,
mindmapBound[1] + 0.5 * mindmapBound[3],
]);
await waitNextFrame(page, 500);
await clickView(page, [
mindmapBound[2] + 10,
mindmapBound[1] + 0.5 * mindmapBound[3],
]);
await pressEscape(page, 2);
}
await assertContainerOfElements(page, [mindmapId], null);
});

View File

@@ -0,0 +1,158 @@
import { expect, type Page } from '@playwright/test';
import {
addNote,
autoFit,
createFrame as _createFrame,
dragBetweenViewCoords,
edgelessCommonSetup,
getFrameTitle,
zoomOutByKeyboard,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
pressBackspace,
pressEnter,
pressEscape,
type,
} from '../../utils/actions/keyboard.js';
import { waitNextFrame } from '../../utils/actions/misc.js';
import { test } from '../../utils/playwright.js';
const createFrame = async (
page: Page,
coord1: [number, number],
coord2: [number, number]
) => {
const frame = await _createFrame(page, coord1, coord2);
await autoFit(page);
return frame;
};
test.beforeEach(async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
});
const enterFrameTitleEditor = async (page: Page) => {
const frameTitle = page.locator('affine-frame-title');
await frameTitle.dblclick();
const frameTitleEditor = page.locator('edgeless-frame-title-editor');
await frameTitleEditor.waitFor({
state: 'attached',
});
return frameTitleEditor;
};
test.describe('frame title rendering', () => {
test('frame title should be displayed', async ({ page }) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
const frameTitle = getFrameTitle(page, frame);
await expect(frameTitle).toBeVisible();
await expect(frameTitle).toHaveText('Frame 1');
});
test('frame title should be rendered on the top', async ({ page }) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
const frameTitle = getFrameTitle(page, frame);
await expect(frameTitle).toBeVisible();
const frameTitleBounding = await frameTitle.boundingBox();
expect(frameTitleBounding).not.toBeNull();
if (!frameTitleBounding) return;
const frameTitleCenter = [
frameTitleBounding.x + frameTitleBounding.width / 2,
frameTitleBounding.y + frameTitleBounding.height / 2,
];
await addNote(page, '', frameTitleCenter[0], frameTitleCenter[1]);
await pressEscape(page, 3);
await waitNextFrame(page, 500);
try {
// if the frame title is rendered on the top, it should be clickable
await frameTitle.click();
} catch {
expect(true, 'frame title should be rendered on the top').toBeFalsy();
}
});
test('should not display frame title component when title is empty', async ({
page,
}) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
await enterFrameTitleEditor(page);
await pressBackspace(page);
await pressEnter(page);
const frameTitle = getFrameTitle(page, frame);
await expect(frameTitle).toBeHidden();
});
});
test.describe('frame title editing', () => {
test('edit frame title by db-click title', async ({ page }) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
const frameTitle = getFrameTitle(page, frame);
await enterFrameTitleEditor(page);
await type(page, 'ABC');
await pressEnter(page);
await expect(frameTitle).toHaveText('ABC');
});
test('frame title can be edited repeatedly', async ({ page }) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
const frameTitle = getFrameTitle(page, frame);
await enterFrameTitleEditor(page);
await type(page, 'ABC');
await pressEnter(page);
await enterFrameTitleEditor(page);
await type(page, 'DEF');
await pressEnter(page);
await expect(frameTitle).toHaveText('DEF');
});
test('edit frame after zoom', async ({ page }) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
const frameTitle = getFrameTitle(page, frame);
await zoomOutByKeyboard(page);
await enterFrameTitleEditor(page);
await type(page, 'ABC');
await pressEnter(page);
await expect(frameTitle).toHaveText('ABC');
});
test('edit frame title after drag', async ({ page }) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
const frameTitle = getFrameTitle(page, frame);
await dragBetweenViewCoords(page, [50 + 10, 50 + 10], [50 + 20, 50 + 20]);
await enterFrameTitleEditor(page);
await type(page, 'ABC');
await pressEnter(page);
await expect(frameTitle).toHaveText('ABC');
});
test('blur unmount frame editor', async ({ page }) => {
await createFrame(page, [50, 50], [150, 150]);
const frameTitleEditor = await enterFrameTitleEditor(page);
await page.mouse.click(10, 10);
await expect(frameTitleEditor).toHaveCount(0);
});
test('enter unmount frame editor', async ({ page }) => {
await createFrame(page, [50, 50], [150, 150]);
const frameTitleEditor = await enterFrameTitleEditor(page);
await pressEnter(page);
await expect(frameTitleEditor).toHaveCount(0);
});
});

View File

@@ -0,0 +1,414 @@
import { Bound } from '@blocksuite/global/utils';
import { expect, type Page } from '@playwright/test';
import { clickView } from '../../utils/actions/click.js';
import {
addNote,
autoFit,
createFrame as _createFrame,
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup,
getFirstContainerId,
getIds,
getSelectedBound,
getSelectedIds,
pickColorAtPoints,
setEdgelessTool,
Shape,
shiftClickView,
toViewCoord,
triggerComponentToolbarAction,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
pressBackspace,
pressEscape,
SHORT_KEY,
} from '../../utils/actions/keyboard.js';
import {
assertCanvasElementsCount,
assertContainerChildCount,
assertEdgelessElementBound,
assertSelectedBound,
} from '../../utils/asserts.js';
import {
DEFAULT_NOTE_HEIGHT,
DEFAULT_NOTE_WIDTH,
} from '../../utils/bs-alternative.js';
import { test } from '../../utils/playwright.js';
const createFrame = async (
page: Page,
coord1: [number, number],
coord2: [number, number]
) => {
const frameId = await _createFrame(page, coord1, coord2);
await autoFit(page);
await pressEscape(page);
return frameId;
};
test.beforeEach(async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
});
test.describe('add a frame', () => {
const createThreeShapesAndSelectTowShape = async (page: Page) => {
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 0], [200, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await clickView(page, [50, 50]);
await shiftClickView(page, [150, 50]);
};
test('multi select and add frame by shortcut F', async ({ page }) => {
await createThreeShapesAndSelectTowShape(page);
await page.keyboard.press('f');
await expect(page.locator('affine-frame')).toHaveCount(1);
await assertSelectedBound(page, [-40, -40, 280, 180]);
const frameId = await getFirstContainerId(page);
await assertContainerChildCount(page, frameId, 2);
});
test('multi select and add frame by component toolbar', async ({ page }) => {
await createThreeShapesAndSelectTowShape(page);
await triggerComponentToolbarAction(page, 'addFrame');
await expect(page.locator('affine-frame')).toHaveCount(1);
await assertSelectedBound(page, [-40, -40, 280, 180]);
const frameId = await getFirstContainerId(page);
await assertContainerChildCount(page, frameId, 2);
});
test('multi select and add frame by more option create frame', async ({
page,
}) => {
await createThreeShapesAndSelectTowShape(page);
await triggerComponentToolbarAction(page, 'createFrameOnMoreOption');
await expect(page.locator('affine-frame')).toHaveCount(1);
await assertSelectedBound(page, [-40, -40, 280, 180]);
const frameId = await getFirstContainerId(page);
await assertContainerChildCount(page, frameId, 2);
});
test('multi select add frame by edgeless toolbar', async ({ page }) => {
await createThreeShapesAndSelectTowShape(page);
await autoFit(page);
await setEdgelessTool(page, 'frame');
const frameMenu = page.locator('edgeless-frame-menu');
await expect(frameMenu).toBeVisible();
const button = page.locator('.frame-add-button[data-name="1:1"]');
await button.click();
await assertSelectedBound(page, [-450, -550, 1200, 1200]);
// the third should be inner frame because
const frameId = await getFirstContainerId(page);
await assertContainerChildCount(page, frameId, 3);
});
test('add frame by dragging with shortcut F', async ({ page }) => {
await createThreeShapesAndSelectTowShape(page);
await pressEscape(page); // unselect
await page.keyboard.press('f');
await dragBetweenViewCoords(page, [-10, -10], [210, 110]);
await expect(page.locator('affine-frame')).toHaveCount(1);
await assertSelectedBound(page, [-10, -10, 220, 120]);
const frameId = await getFirstContainerId(page);
await assertContainerChildCount(page, frameId, 2);
});
test('add inner frame', async ({ page }) => {
await createFrame(page, [50, 50], [450, 450]);
await createShapeElement(page, [200, 200], [300, 300], Shape.Square);
await pressEscape(page);
await shiftClickView(page, [250, 250]);
await page.keyboard.press('f');
const innerFrameBound = await getSelectedBound(page);
expect(
new Bound(50, 50, 400, 400).contains(Bound.fromXYWH(innerFrameBound))
).toBeTruthy();
});
});
test.describe('add element to frame and then move frame', () => {
test.describe('add single element', () => {
test('element should be moved since it is created in frame', async ({
page,
}) => {
const frameId = await createFrame(page, [50, 50], [550, 550]);
const shapeId = await createShapeElement(
page,
[100, 100],
[200, 200],
Shape.Square
);
const noteCoord = await toViewCoord(page, [200, 200]);
const noteId = await addNote(page, '', noteCoord[0], noteCoord[1]);
const frameTitle = page.locator('affine-frame-title');
await pressEscape(page);
await frameTitle.click();
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeId, [150, 150, 100, 100]);
await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]);
await assertEdgelessElementBound(page, noteId, [
220,
210,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
});
test('element should be not moved since it is created not in frame', async ({
page,
}) => {
const frameId = await createFrame(page, [50, 50], [550, 550]);
const shapeId = await createShapeElement(
page,
[600, 600],
[500, 500],
Shape.Square
);
await pressEscape(page);
const frameTitle = page.locator('affine-frame-title');
await frameTitle.click();
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeId, [500, 500, 100, 100]);
await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]);
});
});
test.describe('add group', () => {
// Group
// |<150px>|
// ┌────┐ ─
// │ ┌─┼──┐ 150 px
// └──┼─┘ │ |
// └────┘ ─
test('group should be moved since it is fully contained in frame', async ({
page,
}) => {
const [frameId, ...shapeIds] = [
await createFrame(page, [50, 50], [550, 550]),
await createShapeElement(page, [100, 100], [200, 200], Shape.Square),
await createShapeElement(page, [150, 150], [250, 250], Shape.Square),
];
await pressEscape(page);
const frameTitle = page.locator('affine-frame-title');
await shiftClickView(page, [110, 110]);
await shiftClickView(page, [160, 160]);
await page.keyboard.press(`${SHORT_KEY}+g`);
const groupId = (await getSelectedIds(page))[0];
await pressEscape(page);
await frameTitle.click();
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeIds[0], [150, 150, 100, 100]);
await assertEdgelessElementBound(page, shapeIds[1], [200, 200, 100, 100]);
await assertEdgelessElementBound(page, groupId, [150, 150, 150, 150]);
await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]);
});
test('group should be moved since its center is in frame', async ({
page,
}) => {
const [frameId, ...shapeIds] = [
await createFrame(page, [50, 50], [550, 550]),
await createShapeElement(page, [450, 450], [550, 550], Shape.Square),
await createShapeElement(page, [500, 500], [600, 600], Shape.Square),
];
await pressEscape(page);
const frameTitle = page.locator('affine-frame-title');
await shiftClickView(page, [460, 460]);
await shiftClickView(page, [510, 510]);
await page.keyboard.press(`${SHORT_KEY}+g`);
const groupId = (await getSelectedIds(page))[0];
await pressEscape(page);
await frameTitle.click();
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeIds[0], [500, 500, 100, 100]);
await assertEdgelessElementBound(page, shapeIds[1], [550, 550, 100, 100]);
await assertEdgelessElementBound(page, groupId, [500, 500, 150, 150]);
await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]);
});
});
test.describe('add inner frame', () => {
test('the inner frame and its children should be moved since it is fully contained in frame', async ({
page,
}) => {
const [frameId, innerId, shapeId] = [
await createFrame(page, [50, 50], [550, 550]),
await createFrame(page, [100, 100], [300, 300]),
await createShapeElement(page, [150, 150], [250, 250], Shape.Square),
];
await pressEscape(page);
const frameTitles = page.locator('affine-frame-title');
await frameTitles.nth(0).click();
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeId, [200, 200, 100, 100]);
await assertEdgelessElementBound(page, innerId, [150, 150, 200, 200]);
await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]);
});
test('the inner frame and its children should be moved since its center is in frame', async ({
page,
}) => {
const [frameId, innerId, shapeId] = [
await createFrame(page, [50, 50], [550, 550]),
await createFrame(page, [400, 400], [600, 600]),
await createShapeElement(page, [550, 550], [600, 600], Shape.Square),
];
await pressEscape(page);
const frameTitles = page.locator('affine-frame-title');
await frameTitles.nth(0).click();
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeId, [600, 600, 50, 50]);
await assertEdgelessElementBound(page, innerId, [450, 450, 200, 200]);
await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]);
});
test('the inner frame and its children should also be moved even though its center is not in frame', async ({
page,
}) => {
const [frameId, innerId, shapeId] = [
await createFrame(page, [50, 50], [550, 550]),
await createFrame(page, [500, 500], [600, 600]),
await createShapeElement(page, [550, 550], [600, 600], Shape.Square),
];
const frameTitles = page.locator('affine-frame-title');
await frameTitles.nth(0).click();
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeId, [600, 600, 50, 50]);
await assertEdgelessElementBound(page, innerId, [550, 550, 100, 100]);
await assertEdgelessElementBound(page, frameId, [100, 100, 500, 500]);
});
});
});
test.describe('resize frame then move ', () => {
test('resize frame to warp shape', async ({ page }) => {
const [frameId, shapeId] = [
await createFrame(page, [50, 50], [150, 150]),
await createShapeElement(page, [200, 200], [300, 300], Shape.Square),
];
await pressEscape(page);
const frameTitle = page.locator('affine-frame-title');
await frameTitle.click();
await dragBetweenViewCoords(page, [150, 150], [450, 450]);
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeId, [250, 250, 100, 100]);
await assertEdgelessElementBound(page, frameId, [100, 100, 400, 400]);
});
test('resize frame to unwrap shape', async ({ page }) => {
const [frameId, shapeId] = [
await createFrame(page, [50, 50], [450, 450]),
await createShapeElement(page, [200, 200], [300, 300], Shape.Square),
];
await pressEscape(page);
const frameTitle = page.locator('affine-frame-title');
await frameTitle.click();
await dragBetweenViewCoords(page, [450, 450], [150, 150]);
await dragBetweenViewCoords(page, [60, 60], [110, 110]);
await assertEdgelessElementBound(page, shapeId, [200, 200, 100, 100]);
await assertEdgelessElementBound(page, frameId, [100, 100, 100, 100]);
});
});
test('delete frame should also delete its children', async ({ page }) => {
await createFrame(page, [50, 50], [450, 450]);
await createShapeElement(page, [200, 200], [300, 300], Shape.Square);
await pressEscape(page);
const frameTitle = page.locator('affine-frame-title');
await frameTitle.click();
await pressBackspace(page);
await expect(page.locator('affine-frame')).toHaveCount(0);
await assertCanvasElementsCount(page, 0);
});
test('delete frame by click ungroup should not delete its children', async ({
page,
}) => {
await createFrame(page, [50, 50], [450, 450]);
const shapeId = await createShapeElement(
page,
[200, 200],
[300, 300],
Shape.Square
);
await pressEscape(page);
const frameTitle = page.locator('affine-frame-title');
await frameTitle.click();
const elementToolbar = page.locator('edgeless-element-toolbar-widget');
const ungroupButton = elementToolbar.getByLabel('Ungroup');
await ungroupButton.click();
await assertCanvasElementsCount(page, 1);
expect(await getIds(page)).toEqual([shapeId]);
});
test('outline should keep updated during a new frame created by frame-tool dragging', async ({
page,
}) => {
await page.keyboard.press('f');
const start = await toViewCoord(page, [0, 0]);
const end = await toViewCoord(page, [100, 100]);
await page.mouse.move(start[0], start[1]);
await page.mouse.down();
await page.mouse.move(end[0], end[1], { steps: 10 });
await page.waitForTimeout(50);
expect(
await pickColorAtPoints(page, [start, [end[0] - 1, end[1] - 1]])
).toEqual(['#1e96eb', '#1e96eb']);
});

View File

@@ -0,0 +1,67 @@
import { expect } from '@playwright/test';
import {
createFrame,
createNote,
createShapeElement,
edgelessCommonSetup,
getAllSortedIds,
getEdgelessSelectedRectModel,
Shape,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
pressEscape,
selectAllByKeyboard,
} from '../../utils/actions/keyboard.js';
import { test } from '../../utils/playwright.js';
test.beforeEach(async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
});
test.describe('layer logic of frame block', () => {
test('a new frame should be on the bottom layer', async ({ page }) => {
const shapeId = await createShapeElement(
page,
[100, 100],
[200, 200],
Shape.Square
);
const noteId = await createNote(page, [200, 200]);
await pressEscape(page, 3);
await selectAllByKeyboard(page);
const [x, y, w, h] = await getEdgelessSelectedRectModel(page);
await pressEscape(page);
const frameAId = await createFrame(
page,
[x - 10, y - 10],
[x + w + 10, y + h + 10]
);
let sortedIds = await getAllSortedIds(page);
expect(
sortedIds[0],
'a new frame created by frame-tool should be on the bottom layer'
).toBe(frameAId);
expect(sortedIds[1]).toBe(shapeId);
expect(sortedIds[2]).toBe(noteId);
await selectAllByKeyboard(page);
await page.keyboard.press('f');
sortedIds = await getAllSortedIds(page);
const frameBId = sortedIds.find(
id => ![frameAId, noteId, shapeId].includes(id)
);
expect(
sortedIds[0],
'a new frame created by short-cut should also be on the bottom layer'
).toBe(frameBId);
expect(sortedIds[1]).toBe(frameAId);
expect(sortedIds[2]).toBe(shapeId);
expect(sortedIds[3]).toBe(noteId);
});
});

View File

@@ -0,0 +1,158 @@
import { expect, type Page } from '@playwright/test';
import { click, clickView, dblclickView } from '../../utils/actions/click.js';
import {
addNote,
autoFit,
createFrame as _createFrame,
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup,
getFrameTitle,
getSelectedBoundCount,
getSelectedIds,
Shape,
toViewCoord,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
pressBackspace,
pressEnter,
pressEscape,
selectAllByKeyboard,
type,
} from '../../utils/actions/keyboard.js';
import { waitNextFrame } from '../../utils/actions/misc.js';
import {
assertEdgelessCanvasText,
assertRichTexts,
assertSelectedBound,
} from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
const createFrame = async (
page: Page,
coord1: [number, number],
coord2: [number, number]
) => {
const frame = await _createFrame(page, coord1, coord2);
await autoFit(page);
return frame;
};
test.beforeEach(async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
});
test.describe('frame selection', () => {
test('frame can not be selected by click blank area of frame if it has title', async ({
page,
}) => {
await createFrame(page, [50, 50], [150, 150]);
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
await clickView(page, [100, 100]);
expect(await getSelectedBoundCount(page)).toBe(0);
});
test('frame can selected by click blank area of frame if it has not title', async ({
page,
}) => {
await createFrame(page, [50, 50], [150, 150]);
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
await page.locator('affine-frame-title').dblclick();
await pressBackspace(page);
await pressEnter(page);
await clickView(page, [100, 100]);
expect(await getSelectedBoundCount(page)).toBe(1);
});
test('frame can be selected by click frame title', async ({ page }) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
const frameTitle = getFrameTitle(page, frame);
await frameTitle.click();
expect(await getSelectedBoundCount(page)).toBe(1);
await assertSelectedBound(page, [50, 50, 100, 100]);
});
test('frame can be selected by click frame title when a note overlap on it', async ({
page,
}) => {
const frame = await createFrame(page, [50, 50], [150, 150]);
await pressEscape(page);
const frameTitle = getFrameTitle(page, frame);
const frameTitleBox = await frameTitle.boundingBox();
expect(frameTitleBox).not.toBeNull();
if (frameTitleBox === null) return;
const frameTitleCenter = {
x: frameTitleBox.x + frameTitleBox.width / 2,
y: frameTitleBox.y + frameTitleBox.height / 2,
};
await addNote(page, '', frameTitleCenter.x - 10, frameTitleCenter.y);
await pressEscape(page, 3);
await waitNextFrame(page, 500);
expect(await getSelectedBoundCount(page)).toBe(0);
await click(page, frameTitleCenter);
expect(await getSelectedBoundCount(page)).toBe(1);
const selectedIds = await getSelectedIds(page);
expect(selectedIds.length).toBe(1);
expect(selectedIds[0]).toBe(frame);
});
test('shape inside frame can be selected and edited', async ({ page }) => {
await createFrame(page, [50, 50], [150, 150]);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await pressEscape(page);
await clickView(page, [150, 150]);
expect(await getSelectedBoundCount(page)).toBe(1);
await assertSelectedBound(page, [100, 100, 100, 100]);
await dblclickView(page, [150, 150]);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
});
test('dom inside frame can be selected and edited', async ({ page }) => {
await createFrame(page, [50, 50], [150, 150]);
const noteCoord = await toViewCoord(page, [100, 100]);
await addNote(page, '', noteCoord[0], noteCoord[1]);
await page.mouse.click(noteCoord[0] - 80, noteCoord[1]);
await dblclickView(page, [150, 150]);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
});
test('element in frame should not be selected when frame is selected by drag or Cmd/Ctrl + A', async ({
page,
}) => {
await createFrame(page, [50, 50], [200, 200]);
await createShapeElement(page, [100, 100], [150, 150], Shape.Square);
await pressEscape(page);
await dragBetweenViewCoords(page, [0, 0], [250, 250]);
expect(await getSelectedBoundCount(page)).toBe(1);
await assertSelectedBound(page, [50, 50, 150, 150]);
await pressEscape(page);
expect(await getSelectedBoundCount(page)).toBe(0);
await selectAllByKeyboard(page);
expect(await getSelectedBoundCount(page)).toBe(1);
await assertSelectedBound(page, [50, 50, 150, 150]);
});
});

View File

@@ -0,0 +1,152 @@
import { expect } from '@playwright/test';
import {
copyByKeyboard,
createConnectorElement,
createNote,
createShapeElement,
decreaseZoomLevel,
edgelessCommonSetup as commonSetup,
edgelessCommonSetup,
getAllSortedIds,
getFirstContainerId,
pasteByKeyboard,
selectAllByKeyboard,
Shape,
toViewCoord,
triggerComponentToolbarAction,
waitNextFrame,
} from '../../utils/actions/index.js';
import {
assertContainerChildCount,
assertContainerIds,
} from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test.describe('clipboard', () => {
test('copy and paste group', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 0], [200, 100], Shape.Square);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
const originGroupId = await getFirstContainerId(page);
await copyByKeyboard(page);
await waitNextFrame(page, 100);
const move = await toViewCoord(page, [100, -50]);
await page.mouse.click(move[0], move[1]);
await waitNextFrame(page, 1000);
await pasteByKeyboard(page, false);
const copyedGroupId = await getFirstContainerId(page, [originGroupId]);
await assertContainerIds(page, {
[originGroupId]: 2,
[copyedGroupId]: 2,
null: 2,
});
await assertContainerChildCount(page, originGroupId, 2);
await assertContainerChildCount(page, copyedGroupId, 2);
});
test('copy and paste group with connector', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 0], [200, 100], Shape.Square);
await createConnectorElement(page, [100, 50], [200, 50]);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
const originGroupId = await getFirstContainerId(page);
await copyByKeyboard(page);
await waitNextFrame(page, 100);
const move = await toViewCoord(page, [100, -50]);
await page.mouse.click(move[0], move[1]);
await waitNextFrame(page, 1000);
await pasteByKeyboard(page, false);
const copyedGroupId = await getFirstContainerId(page, [originGroupId]);
await assertContainerIds(page, {
[originGroupId]: 3,
[copyedGroupId]: 3,
null: 2,
});
await assertContainerChildCount(page, originGroupId, 3);
await assertContainerChildCount(page, copyedGroupId, 3);
});
});
test.describe('group clipboard', () => {
test('copy and paste group with shape and note inside', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [100, -100]);
await page.mouse.click(10, 50);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
const originIds = await getAllSortedIds(page);
expect(originIds.length).toBe(3);
await copyByKeyboard(page);
const move = await toViewCoord(page, [250, 250]);
await page.mouse.move(move[0], move[1]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, true);
await waitNextFrame(page, 500);
const sortedIds = await getAllSortedIds(page);
expect(sortedIds.length).toBe(6);
});
test('copy and paste group with group inside', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await createNote(page, [100, -100]);
await page.mouse.click(10, 50);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'createGroupOnMoreOption');
const originIds = await getAllSortedIds(page);
expect(originIds.length).toBe(5);
await copyByKeyboard(page);
const move = await toViewCoord(page, [250, 250]);
await page.mouse.move(move[0], move[1]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, true);
await waitNextFrame(page, 500);
const sortedIds = await getAllSortedIds(page);
expect(sortedIds.length).toBe(10);
});
test('copy and paste group with frame inside', async ({ page }) => {
await commonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createNote(page, [100, -100]);
await page.mouse.click(10, 50);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addFrame');
await decreaseZoomLevel(page);
await createShapeElement(page, [700, 0], [800, 100], Shape.Square);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
const originIds = await getAllSortedIds(page);
expect(originIds.length).toBe(5);
await copyByKeyboard(page);
const move = await toViewCoord(page, [250, 250]);
await page.mouse.move(move[0], move[1]);
await page.mouse.click(move[0], move[1]);
await pasteByKeyboard(page, true);
await waitNextFrame(page, 500);
const sortedIds = await getAllSortedIds(page);
expect(sortedIds.length).toBe(10);
});
});

View File

@@ -0,0 +1,123 @@
import type { Page } from '@playwright/test';
import {
captureHistory,
clickView,
createShapeElement,
edgelessCommonSetup,
getFirstContainerId,
getIds,
redoByKeyboard,
selectAllByKeyboard,
Shape,
shiftClickView,
toIdCountMap,
triggerComponentToolbarAction,
undoByKeyboard,
} from '../../utils/actions/index.js';
import {
assertContainerChildCount,
assertContainerChildIds,
assertContainerIds,
assertSelectedBound,
} from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
let initShapes: string[] = [];
async function init(page: Page) {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 0], [200, 100], Shape.Square);
initShapes = await getIds(page);
}
test.describe('group and ungroup in group', () => {
let outterGroupId: string;
let newAddedShape: string;
test.beforeEach(async ({ page }) => {
await init(page);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
newAddedShape = (await getIds(page)).filter(
id => !initShapes.includes(id)
)[0];
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
outterGroupId = await getFirstContainerId(page);
});
test('group in group', async ({ page }) => {
await clickView(page, [50, 50]);
await shiftClickView(page, [150, 50]);
await captureHistory(page);
await triggerComponentToolbarAction(page, 'addGroup');
const groupId = await getFirstContainerId(page, [outterGroupId]);
await assertSelectedBound(page, [0, 0, 200, 100]);
await assertContainerIds(page, {
[groupId]: 2,
[outterGroupId]: 2,
null: 1,
});
await assertContainerChildCount(page, groupId, 2);
await assertContainerChildCount(page, outterGroupId, 2);
// undo the creation
await undoByKeyboard(page);
await assertContainerIds(page, {
[outterGroupId]: 3,
null: 1,
});
await assertContainerChildCount(page, outterGroupId, 3);
// redo the creation
await redoByKeyboard(page);
await assertContainerIds(page, {
[groupId]: 2,
[outterGroupId]: 2,
null: 1,
});
await assertContainerChildCount(page, groupId, 2);
await assertContainerChildCount(page, outterGroupId, 2);
});
test('ungroup in group', async ({ page }) => {
await clickView(page, [50, 50]);
await shiftClickView(page, [150, 50]);
await triggerComponentToolbarAction(page, 'addGroup');
await captureHistory(page);
const groupId = await getFirstContainerId(page, [outterGroupId]);
await triggerComponentToolbarAction(page, 'ungroup');
await assertContainerIds(page, { [outterGroupId]: 3, null: 1 });
await assertContainerChildIds(
page,
toIdCountMap(await getIds(page, true)),
outterGroupId
);
// undo, group should in group again
await undoByKeyboard(page);
await assertContainerIds(page, {
[outterGroupId]: 2,
[groupId]: 2,
null: 1,
});
await assertContainerChildIds(page, toIdCountMap(initShapes), groupId);
await assertContainerChildIds(
page,
{
[groupId]: 1,
[newAddedShape]: 1,
},
outterGroupId
);
// redo, group should be ungroup again
await redoByKeyboard(page);
await assertContainerIds(page, { [outterGroupId]: 3, null: 1 });
await assertContainerChildIds(
page,
toIdCountMap(await getIds(page, true)),
outterGroupId
);
});
});

View File

@@ -0,0 +1,260 @@
import { expect, type Page } from '@playwright/test';
import { clickView } from '../../utils/actions/click.js';
import {
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup,
getFirstContainerId,
Shape,
shiftClickView,
triggerComponentToolbarAction,
} from '../../utils/actions/edgeless.js';
import {
pressBackspace,
redoByKeyboard,
selectAllByKeyboard,
SHORT_KEY,
undoByKeyboard,
} from '../../utils/actions/keyboard.js';
import { captureHistory } from '../../utils/actions/misc.js';
import {
assertCanvasElementsCount,
assertContainerChildCount,
assertContainerIds,
assertEdgelessNonSelectedRect,
assertSelectedBound,
} from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
export const GROUP_ROOT_ID = 'GROUP_ROOT';
test.describe('group', () => {
async function init(page: Page) {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 0], [200, 100], Shape.Square);
}
test.describe('group create', () => {
test.beforeEach(async ({ page }) => {
await init(page);
});
test('create group button not show when single select', async ({
page,
}) => {
await clickView(page, [50, 50]);
await expect(
page.locator('edgeless-element-toolbar-widget')
).toBeVisible();
await expect(page.locator('edgeless-add-group-button')).not.toBeVisible();
});
test('create button show up when multi select', async ({ page }) => {
await selectAllByKeyboard(page);
await expect(page.locator('edgeless-add-group-button')).toBeVisible();
});
test('create group by component toolbar', async ({ page }) => {
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await assertSelectedBound(page, [0, 0, 200, 100]);
});
test('create group by shortcut mod + G', async ({ page }) => {
await selectAllByKeyboard(page);
await page.keyboard.press(`${SHORT_KEY}+g`);
await assertSelectedBound(page, [0, 0, 200, 100]);
});
test('create group and undo, redo', async ({ page }) => {
await selectAllByKeyboard(page);
await captureHistory(page);
await page.keyboard.press(`${SHORT_KEY}+g`);
await assertSelectedBound(page, [0, 0, 200, 100]);
await undoByKeyboard(page);
await assertSelectedBound(page, [0, 0, 100, 100]);
await redoByKeyboard(page);
await assertSelectedBound(page, [0, 0, 200, 100]);
});
});
test.describe('ungroup', () => {
test.beforeEach(async ({ page }) => {
await init(page);
});
test('ungroup by component toolbar', async ({ page }) => {
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await assertSelectedBound(page, [0, 0, 200, 100]);
await triggerComponentToolbarAction(page, 'ungroup');
await assertEdgelessNonSelectedRect(page);
});
test('ungroup by shortcut mod + shift + G', async ({ page }) => {
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await assertSelectedBound(page, [0, 0, 200, 100]);
await page.keyboard.press(`${SHORT_KEY}+Shift+g`);
await assertEdgelessNonSelectedRect(page);
});
test('ungroup and undo, redo', async ({ page }) => {
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await assertSelectedBound(page, [0, 0, 200, 100]);
await captureHistory(page);
await page.keyboard.press(`${SHORT_KEY}+Shift+g`);
await assertEdgelessNonSelectedRect(page);
await undoByKeyboard(page);
await assertSelectedBound(page, [0, 0, 200, 100]);
await redoByKeyboard(page);
await assertEdgelessNonSelectedRect(page);
});
});
test.describe('drag group', () => {
test.beforeEach(async ({ page }) => {
await init(page);
});
test('drag group to move', async ({ page }) => {
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await dragBetweenViewCoords(page, [100, 50], [110, 50]);
await assertSelectedBound(page, [10, 0, 200, 100]);
});
});
test.describe('select', () => {
test.beforeEach(async ({ page }) => {
await init(page);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
});
test('select group by click', async ({ page }) => {
await clickView(page, [300, -100]);
await assertEdgelessNonSelectedRect(page);
await clickView(page, [50, 50]);
await assertSelectedBound(page, [0, 0, 200, 100]);
});
test('select sub-element by first select group', async ({ page }) => {
await clickView(page, [50, 50]);
await assertSelectedBound(page, [0, 0, 100, 100]);
});
test('select element when enter gorup', async ({ page }) => {
await clickView(page, [50, 50]);
await assertSelectedBound(page, [0, 0, 100, 100]);
await clickView(page, [150, 50]);
await assertSelectedBound(page, [100, 0, 100, 100]);
});
});
test.describe('delete', () => {
test.beforeEach(async ({ page }) => {
await init(page);
});
test('delete root group', async ({ page }) => {
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
const groupId = await getFirstContainerId(page);
await captureHistory(page);
await pressBackspace(page);
await assertCanvasElementsCount(page, 0);
// undo the delete
await undoByKeyboard(page);
await assertCanvasElementsCount(page, 3);
await assertContainerIds(page, {
[groupId]: 2,
null: 1,
});
await assertContainerChildCount(page, groupId, 2);
// redo the delete
await redoByKeyboard(page);
await assertCanvasElementsCount(page, 0);
});
test('delete sub-element in group', async ({ page }) => {
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
const groupId = await getFirstContainerId(page);
await captureHistory(page);
await clickView(page, [50, 50]);
await pressBackspace(page);
await assertCanvasElementsCount(page, 2);
await assertContainerIds(page, {
[groupId]: 1,
null: 1,
});
await assertContainerChildCount(page, groupId, 1);
// undo the delete
await undoByKeyboard(page);
await assertCanvasElementsCount(page, 3);
await assertContainerIds(page, {
[groupId]: 2,
null: 1,
});
await assertContainerChildCount(page, groupId, 2);
// redo the delete
await redoByKeyboard(page);
await assertCanvasElementsCount(page, 2);
await assertContainerIds(page, {
[groupId]: 1,
null: 1,
});
await assertContainerChildCount(page, groupId, 1);
});
test('delete group in group', async ({ page }) => {
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
const firstGroup = await getFirstContainerId(page);
await clickView(page, [50, 50]);
await shiftClickView(page, [150, 50]);
await triggerComponentToolbarAction(page, 'addGroup');
const secondGroup = await getFirstContainerId(page, [firstGroup]);
await captureHistory(page);
// delete group in group
await pressBackspace(page);
await assertCanvasElementsCount(page, 2);
await assertContainerIds(page, {
[firstGroup]: 1,
null: 1,
});
await assertContainerChildCount(page, firstGroup, 1);
// undo the delete
await undoByKeyboard(page);
await assertCanvasElementsCount(page, 5);
await assertContainerIds(page, {
[firstGroup]: 2,
[secondGroup]: 2,
null: 1,
});
await assertContainerChildCount(page, firstGroup, 2);
await assertContainerChildCount(page, secondGroup, 2);
// redo the delete
await redoByKeyboard(page);
await assertCanvasElementsCount(page, 2);
await assertContainerIds(page, {
[firstGroup]: 1,
null: 1,
});
await assertContainerChildCount(page, firstGroup, 1);
});
});
});

View File

@@ -0,0 +1,116 @@
import type { Page } from '@playwright/test';
import {
captureHistory,
clickView,
createShapeElement,
edgelessCommonSetup,
getFirstContainerId,
redoByKeyboard,
selectAllByKeyboard,
Shape,
shiftClickView,
triggerComponentToolbarAction,
undoByKeyboard,
} from '../../utils/actions/index.js';
import {
assertContainerChildCount,
assertContainerIds,
assertSelectedBound,
} from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
async function init(page: Page) {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 0], [200, 100], Shape.Square);
}
test.describe('release from group', () => {
let outterGroupId: string;
test.beforeEach(async ({ page }) => {
await init(page);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
outterGroupId = await getFirstContainerId(page);
});
test('release element from group', async ({ page }) => {
await clickView(page, [50, 50]);
await captureHistory(page);
await triggerComponentToolbarAction(page, 'releaseFromGroup');
await assertContainerIds(page, {
[outterGroupId]: 2,
null: 2,
});
await assertContainerChildCount(page, outterGroupId, 2);
await assertSelectedBound(page, [0, 0, 100, 100]);
// undo the release
await undoByKeyboard(page);
await assertContainerIds(page, {
[outterGroupId]: 3,
null: 1,
});
await assertContainerChildCount(page, outterGroupId, 3);
await assertSelectedBound(page, [0, 0, 100, 100]);
// redo the release
await redoByKeyboard(page);
await assertContainerIds(page, {
[outterGroupId]: 2,
null: 2,
});
await assertContainerChildCount(page, outterGroupId, 2);
await assertSelectedBound(page, [0, 0, 100, 100]);
});
test('release group from group', async ({ page }) => {
await clickView(page, [50, 50]);
await shiftClickView(page, [150, 50]);
await triggerComponentToolbarAction(page, 'addGroup');
await captureHistory(page);
const groupId = await getFirstContainerId(page, [outterGroupId]);
await assertContainerIds(page, {
[groupId]: 2,
[outterGroupId]: 2,
null: 1,
});
await assertContainerChildCount(page, groupId, 2);
await assertContainerChildCount(page, outterGroupId, 2);
// release group from group
await triggerComponentToolbarAction(page, 'releaseFromGroup');
await assertContainerIds(page, {
[groupId]: 2,
[outterGroupId]: 1,
null: 2,
});
await assertContainerChildCount(page, outterGroupId, 1);
await assertContainerChildCount(page, groupId, 2);
// undo the release
await undoByKeyboard(page);
await assertContainerIds(page, {
[groupId]: 2,
[outterGroupId]: 2,
null: 1,
});
await assertContainerChildCount(page, groupId, 2);
await assertContainerChildCount(page, outterGroupId, 2);
// redo the release
await redoByKeyboard(page);
await assertContainerIds(page, {
[groupId]: 2,
[outterGroupId]: 1,
null: 2,
});
await assertContainerChildCount(page, outterGroupId, 1);
await assertContainerChildCount(page, groupId, 2);
});
});

View File

@@ -0,0 +1,72 @@
import { expect, type Page } from '@playwright/test';
import {
createShapeElement,
dblclickView,
edgelessCommonSetup,
getSelectedBound,
pressEnter,
selectAllByKeyboard,
Shape,
triggerComponentToolbarAction,
type,
} from '../../utils/actions/index.js';
import { assertEdgelessCanvasText } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
async function init(page: Page) {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 0], [200, 100], Shape.Square);
}
test.describe('group title', () => {
test.beforeEach(async ({ page }) => {
await init(page);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
});
test('edit group title by component toolbar', async ({ page }) => {
expect(await page.locator('edgeless-group-title-editor').count()).toBe(0);
await triggerComponentToolbarAction(page, 'renameGroup');
await page.locator('edgeless-group-title-editor').waitFor({
state: 'attached',
});
});
test('edit group title by dbclick', async ({ page }) => {
expect(await page.locator('edgeless-group-title-editor').count()).toBe(0);
const bound = await getSelectedBound(page);
await dblclickView(page, [bound[0] + 10, bound[1] - 10]);
await page.locator('edgeless-group-title-editor').waitFor({
state: 'attached',
});
await type(page, 'ABC');
await assertEdgelessCanvasText(page, 'ABC');
});
test('blur unmount group editor', async ({ page }) => {
const bound = await getSelectedBound(page);
await dblclickView(page, [bound[0] + 10, bound[1] - 10]);
await page.locator('edgeless-group-title-editor').waitFor({
state: 'attached',
});
await page.mouse.click(10, 10);
expect(await page.locator('edgeless-group-title-editor').count()).toBe(0);
});
test('enter unmount group editor', async ({ page }) => {
const bound = await getSelectedBound(page);
await dblclickView(page, [bound[0] + 10, bound[1] - 10]);
await page.locator('edgeless-group-title-editor').waitFor({
state: 'attached',
});
await pressEnter(page);
expect(await page.locator('edgeless-group-title-editor').count()).toBe(0);
});
});

View File

@@ -0,0 +1,262 @@
import { sleep } from '@blocksuite/global/utils';
import { expect } from '@playwright/test';
import {
addBasicRectShapeElement,
assertEdgelessTool,
edgelessCommonSetup as commonSetup,
setEdgelessTool,
} from '../utils/actions/edgeless.js';
import {
dragBetweenCoords,
selectAllByKeyboard,
} from '../utils/actions/index.js';
import {
assertEdgelessNonSelectedRect,
assertEdgelessSelectedRect,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.skip('lasso tool should deselect when dragging in an empty area', async ({
page,
}) => {
await commonSetup(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicRectShapeElement(page, start, end);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await setEdgelessTool(page, 'lasso');
await assertEdgelessTool(page, 'lasso');
await dragBetweenCoords(page, { x: 10, y: 10 }, { x: 15, y: 15 });
await assertEdgelessNonSelectedRect(page);
});
test.skip('freehand lasso basic test', async ({ page }) => {
await commonSetup(page);
await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 });
await page.mouse.click(10, 10); // deselect
await setEdgelessTool(page, 'lasso');
await assertEdgelessTool(page, 'lasso');
await assertEdgelessNonSelectedRect(page);
// simulate a basic lasso selection to select both the rects
const points: [number, number][] = [
[500, 100],
[500, 500],
[90, 500],
];
await page.mouse.move(90, 90);
await page.mouse.down();
for (const point of points) await page.mouse.move(...point);
await page.mouse.up();
await assertEdgelessSelectedRect(page, [100, 100, 200, 200]);
});
test.skip('freehand lasso add to selection', async ({ page }) => {
await commonSetup(page);
await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 });
await page.mouse.click(10, 10); // deselect
await setEdgelessTool(page, 'lasso');
await assertEdgelessTool(page, 'lasso');
await assertEdgelessNonSelectedRect(page);
// some random selection covering the rectangle
let points: [number, number][] = [
[250, 90],
[250, 300],
[10, 300],
];
await page.mouse.move(90, 90);
await page.mouse.down();
for (const point of points) await page.mouse.move(...point);
await page.mouse.up();
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
points = [
[400, 250],
[400, 450],
[250, 450],
];
await page.keyboard.down('Shift'); // addition selection
await page.mouse.move(250, 250);
await page.mouse.down();
for (const point of points) await page.mouse.move(...point);
await page.mouse.up();
await assertEdgelessSelectedRect(page, [100, 100, 200, 200]);
});
test.skip('freehand lasso subtract from selection', async ({ page }) => {
await commonSetup(page);
await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 });
await setEdgelessTool(page, 'default');
await selectAllByKeyboard(page);
await setEdgelessTool(page, 'lasso');
const points: [number, number][] = [
[410, 290],
[410, 410],
[290, 410],
];
await page.keyboard.down('Alt');
await page.mouse.move(290, 290);
await page.mouse.down();
for (const point of points) await page.mouse.move(...point);
await page.mouse.up();
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]); // only the first rectangle should be selected
});
test.skip('polygonal lasso basic test', async ({ page }) => {
await commonSetup(page);
await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 });
await page.mouse.click(10, 10); // deselect
await assertEdgelessNonSelectedRect(page);
await setEdgelessTool(page, 'lasso');
await setEdgelessTool(page, 'lasso'); // switch to polygonal lasso
await sleep(100);
const points: [number, number][] = [
[90, 90],
[500, 90],
[500, 500],
[90, 500],
[90, 90],
];
for (const point of points) {
await page.mouse.click(...point);
}
await assertEdgelessSelectedRect(page, [100, 100, 200, 200]);
});
test.skip('polygonal lasso add to selection by holding Shift Key', async ({
page,
}) => {
await commonSetup(page);
await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 });
await page.mouse.click(10, 10); // deselect
await assertEdgelessNonSelectedRect(page);
await setEdgelessTool(page, 'lasso');
await setEdgelessTool(page, 'lasso');
await sleep(100);
let points: [number, number][] = [
[90, 90],
[150, 90],
[150, 150],
[90, 150],
[90, 90],
];
// select the first rectangle
for (const point of points) await page.mouse.click(...point);
points = [
[290, 290],
[350, 290],
[350, 350],
[290, 350],
[290, 290],
];
await page.keyboard.down('Shift'); // add to selection
// selects the second rectangle
for (const point of points) await page.mouse.click(...point);
// by the end both of the rects should be selected
await assertEdgelessSelectedRect(page, [100, 100, 200, 200]);
});
test.skip('polygonal lasso subtract from selection by holding Alt', async ({
page,
}) => {
await commonSetup(page);
await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await addBasicRectShapeElement(page, { x: 300, y: 300 }, { x: 400, y: 400 });
await selectAllByKeyboard(page);
const points: [number, number][] = [
[290, 290],
[350, 290],
[350, 350],
[290, 350],
[290, 290],
];
// switch to polygonal lasso tool
await setEdgelessTool(page, 'lasso');
await setEdgelessTool(page, 'lasso');
await sleep(100);
await page.keyboard.down('Alt'); // subtract from selection
for (const point of points) await page.mouse.click(...point);
// By the end the second rectangle must be deselected leaving the first rect selection
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test.skip('polygonal lasso should complete selection when clicking the last point', async ({
page,
}) => {
await commonSetup(page);
// switch to polygonal lasso
await setEdgelessTool(page, 'lasso');
await setEdgelessTool(page, 'lasso');
await sleep(100);
const lassoPoints: [number, number][] = [
[100, 100],
[200, 200],
[250, 150],
[100, 100],
];
for (const point of lassoPoints) await page.mouse.click(...point);
const isSelecting = await page.evaluate(() => {
const edgeless = document.querySelector('affine-edgeless-root');
if (!edgeless) throw new Error('Missing edgless root block');
const curController = edgeless.gfx.tool.currentTool$.peek();
if (curController?.toolName !== 'lasso')
throw new Error('expected lasso tool controller');
return (curController as any)['_isSelecting'];
});
expect(isSelecting).toBe(false);
});

View File

@@ -0,0 +1,354 @@
import { assertNotExists } from '@blocksuite/global/utils';
import { expect } from '@playwright/test';
import {
activeNoteInEdgeless,
createConnectorElement,
createNote,
createShapeElement,
edgelessCommonSetup,
getConnectorPath,
locatorComponentToolbarMoreButton,
selectNoteInEdgeless,
Shape,
triggerComponentToolbarAction,
} from '../utils/actions/edgeless.js';
import {
addBasicBrushElement,
pressEnter,
selectAllByKeyboard,
type,
waitNextFrame,
} from '../utils/actions/index.js';
import { assertConnectorPath, assertExists } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.describe('note to linked doc', () => {
test('select a note and turn it into a linked doc', async ({ page }) => {
await edgelessCommonSetup(page);
const noteId = await createNote(page, [100, 0], '');
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 200);
await type(page, 'Hello');
await pressEnter(page);
await type(page, 'World');
await page.mouse.click(10, 50);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc');
await waitNextFrame(page, 200);
const embedSyncedBlock = page.locator('affine-embed-synced-doc-block');
assertExists(embedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
const noteBlock = page.locator('affine-edgeless-note');
assertExists(noteBlock);
const noteContent = await noteBlock.innerText();
expect(noteContent).toBe('Hello\nWorld');
});
test('turn note into a linked doc, connector keeps', async ({ page }) => {
await edgelessCommonSetup(page);
const noteId = await createNote(page, [100, 0]);
await createShapeElement(page, [100, 100], [100, 100], Shape.Square);
await createConnectorElement(page, [100, 150], [100, 10]);
const connectorPath = await getConnectorPath(page);
await page.mouse.click(10, 50);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc');
await waitNextFrame(page, 200);
const embedSyncedBlock = page.locator('affine-embed-synced-doc-block');
assertExists(embedSyncedBlock);
await assertConnectorPath(page, [connectorPath[0], connectorPath[1]], 0);
});
// TODO FIX ME
test.skip('embed-synced-doc card can not turn into linked doc', async ({
page,
}) => {
await edgelessCommonSetup(page);
const noteId = await createNote(page, [100, 0]);
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 200);
await type(page, 'Hello World');
await page.mouse.click(10, 50);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc');
const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click();
const turnButton = page.locator('.turn-into-linked-doc');
assertNotExists(turnButton);
});
// TODO FIX ME
test.skip('embed-linked-doc card can not turn into linked doc', async ({
page,
}) => {
await edgelessCommonSetup(page);
const noteId = await createNote(page, [100, 0]);
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 200);
await type(page, 'Hello World');
await page.mouse.click(10, 50);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc');
await triggerComponentToolbarAction(page, 'toCardView');
const moreButton = locatorComponentToolbarMoreButton(page);
await moreButton.click();
const turnButton = page.locator('.turn-into-linked-doc');
assertNotExists(turnButton);
});
});
test.describe('single edgeless element to linked doc', () => {
test('select a shape, turn into a linked doc', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [100, 100], Shape.Square);
await triggerComponentToolbarAction(page, 'createLinkedDoc');
await waitNextFrame(page, 200);
const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block');
assertExists(linkedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
const shapes = await page.evaluate(() => {
const container = document.querySelector('affine-edgeless-root');
return container!.service.crud
.getElementsByType('shape')
.map(s => ({ type: s.type, xywh: s.xywh }));
});
expect(shapes.length).toBe(1);
expect(shapes[0]).toEqual({ type: 'shape', xywh: '[100,100,100,100]' });
});
test('select a connector, turn into a linked doc', async ({ page }) => {
await edgelessCommonSetup(page);
await createConnectorElement(page, [100, 150], [100, 10]);
const connectorPath = await getConnectorPath(page);
await triggerComponentToolbarAction(page, 'createLinkedDoc');
await waitNextFrame(page, 200);
const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block');
assertExists(linkedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
await assertConnectorPath(page, [connectorPath[0], connectorPath[1]], 0);
});
test('select a brush, turn into a linked doc', async ({ page }) => {
await edgelessCommonSetup(page);
const start = { x: 400, y: 400 };
const end = { x: 500, y: 500 };
await addBasicBrushElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'createLinkedDoc');
await waitNextFrame(page, 200);
const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block');
assertExists(linkedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
const brushes = await page.evaluate(() => {
const container = document.querySelector('affine-edgeless-root');
return container!.service.crud
.getElementsByType('brush')
.map(s => ({ type: s.type, xywh: s.xywh }));
});
expect(brushes.length).toBe(1);
});
test('select a group, turn into a linked doc', async ({ page }) => {
await edgelessCommonSetup(page);
await createNote(page, [100, 0]);
await createShapeElement(page, [100, 100], [100, 100], Shape.Square);
await createConnectorElement(page, [100, 150], [100, 10]);
const start = { x: 400, y: 400 };
const end = { x: 500, y: 500 };
await addBasicBrushElement(page, start, end);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await triggerComponentToolbarAction(page, 'createLinkedDoc');
await waitNextFrame(page, 200);
const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block');
assertExists(linkedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
const groups = await page.evaluate(() => {
const container = document.querySelector('affine-edgeless-root');
return container!.service.crud.getElementsByType('group').map(s => ({
type: s.type,
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
children: s.childElements.map((c: any) => c.type || c.flavour),
}));
});
expect(groups.length).toBe(1);
expect(groups[0].children).toContain('affine:note');
expect(groups[0].children).toContain('shape');
expect(groups[0].children).toContain('connector');
expect(groups[0].children).toContain('brush');
});
test('select a frame, turn into a linked doc', async ({ page }) => {
await edgelessCommonSetup(page);
await createNote(page, [100, 0]);
await createShapeElement(page, [100, 100], [100, 100], Shape.Square);
await createConnectorElement(page, [100, 150], [100, 10]);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
const start = { x: 400, y: 400 };
const end = { x: 500, y: 500 };
await addBasicBrushElement(page, start, end);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addFrame');
await triggerComponentToolbarAction(page, 'createLinkedDoc');
await waitNextFrame(page, 200);
const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block');
assertExists(linkedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
const nodes = await page.evaluate(() => {
const container = document.querySelector('affine-edgeless-root');
const elements = container!.service.elements.map(s => s.type);
const blocks = container!.service.blocks.map(b => b.flavour);
blocks.sort();
elements.sort();
return { blocks, elements };
});
expect(nodes).toEqual({
blocks: ['affine:note', 'affine:frame'].sort(),
elements: ['group', 'shape', 'connector', 'brush'].sort(),
});
});
});
test.describe('multiple edgeless elements to linked doc', () => {
test('multi-select note, frame, shape, connector, brush and group, turn it into a linked doc', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createNote(page, [100, 0], 'Hello World');
await page.mouse.click(10, 50);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addGroup');
await createShapeElement(page, [200, 200], [300, 300], Shape.Square);
await createConnectorElement(page, [250, 300], [100, 70]);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'addFrame');
const start = { x: 400, y: 400 };
const end = { x: 500, y: 500 };
await addBasicBrushElement(page, start, end);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'createLinkedDoc');
await waitNextFrame(page, 200);
const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block');
assertExists(linkedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
const nodes = await page.evaluate(() => {
const container = document.querySelector('affine-edgeless-root');
const elements = container!.service.elements.map(s => s.type);
const blocks = container!.service.blocks.map(b => b.flavour);
blocks.sort();
elements.sort();
return { blocks, elements };
});
expect(nodes).toEqual({
blocks: ['affine:frame', 'affine:note'].sort(),
elements: ['shape', 'shape', 'group', 'connector', 'brush'].sort(),
});
});
test('multi-select with embed doc card inside, turn it into a linked doc', async ({
page,
}) => {
await edgelessCommonSetup(page);
const noteId = await createNote(page, [100, 0], 'Hello World');
await page.mouse.click(10, 50);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'turnIntoLinkedDoc');
await createShapeElement(page, [100, 100], [100, 100], Shape.Square);
await createConnectorElement(page, [100, 150], [100, 10]);
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'createLinkedDoc');
await waitNextFrame(page, 200);
const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block');
assertExists(linkedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
const nodes = await page.evaluate(() => {
const container = document.querySelector('affine-edgeless-root');
const elements = container!.service.elements.map(s => s.type);
const blocks = container!.service.blocks.map(b => b.flavour);
return { blocks, elements };
});
expect(nodes.blocks).toHaveLength(1);
expect(nodes.blocks).toContain('affine:embed-synced-doc');
expect(nodes.elements).toHaveLength(2);
expect(nodes.elements).toContain('shape');
expect(nodes.elements).toContain('connector');
});
test('multi-select with mindmap, turn it into a linked doc', async ({
page,
}) => {
await edgelessCommonSetup(page);
await triggerComponentToolbarAction(page, 'addMindmap');
await selectAllByKeyboard(page);
await triggerComponentToolbarAction(page, 'createLinkedDoc');
await waitNextFrame(page, 200);
const linkedSyncedBlock = page.locator('affine-linked-synced-doc-block');
assertExists(linkedSyncedBlock);
await triggerComponentToolbarAction(page, 'openLinkedDoc');
await waitNextFrame(page, 200);
const nodes = await page.evaluate(() => {
const container = document.querySelector('affine-edgeless-root');
const elements = container!.service.elements.map(s => s.type);
const blocks = container!.service.blocks.map(b => b.flavour);
return { blocks, elements };
});
expect(nodes.blocks).toHaveLength(0);
expect(nodes.elements).toHaveLength(5);
expect(nodes.elements).toContain('mindmap');
expect(nodes.elements.filter(el => el === 'shape')).toHaveLength(4);
});
});

View File

@@ -0,0 +1,558 @@
import { expect, type Page } from '@playwright/test';
import { clickView, dblclickView, moveView } from '../utils/actions/click.js';
import {
createBrushElement,
createConnectorElement,
createEdgelessText,
createFrame,
createMindmap,
createNote as _createNote,
createShapeElement,
deleteAll,
dragBetweenViewCoords,
edgelessCommonSetup,
getContainerChildIds,
getSelectedBound,
getSelectedIds,
getTypeById,
setEdgelessTool,
} from '../utils/actions/edgeless.js';
import {
copyByKeyboard,
pasteByKeyboard,
pressArrowDown,
pressBackspace,
pressEscape,
pressForwardDelete,
pressTab,
selectAllByKeyboard,
SHORT_KEY,
type,
undoByKeyboard,
} from '../utils/actions/keyboard.js';
import { waitNextFrame } from '../utils/actions/misc.js';
import {
assertCanvasElementsCount,
assertEdgelessElementBound,
assertEdgelessSelectedModelRect,
assertRichTexts,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.describe('lock', () => {
const getButtons = (page: Page) => {
const elementToolbar = page.locator('edgeless-element-toolbar-widget');
return {
lock: elementToolbar.locator('edgeless-lock-button[data-locked="false"]'),
unlock: elementToolbar.locator(
'edgeless-lock-button[data-locked="true"]'
),
};
};
async function createNote(page: Page, coord1: number[], content?: string) {
await _createNote(page, coord1, content);
await pressEscape(page, 3);
}
test('edgeless element can be locked and unlocked', async ({ page }) => {
await edgelessCommonSetup(page);
const wrapTest = async <F extends (...args: any) => any>(
elementCreateFn: F,
...args: Parameters<F>
) => {
await elementCreateFn(...args);
await waitNextFrame(page);
await pressEscape(page);
await selectAllByKeyboard(page);
const ids = await getSelectedIds(page);
expect(ids).toHaveLength(1);
const type = await getTypeById(page, ids[0]);
const message = `element(${type}) should be able to be (un)locked`;
const { lock, unlock } = getButtons(page);
await expect(lock, message).toBeVisible();
await expect(unlock, message).toBeHidden();
await lock.click();
await expect(lock, message).toBeHidden();
await expect(unlock, message).toBeVisible();
await unlock.click();
await expect(lock, message).toBeVisible();
await expect(unlock, message).toBeHidden();
await deleteAll(page);
await waitNextFrame(page);
};
await wrapTest(createBrushElement, page, [100, 100], [150, 150]);
await wrapTest(createConnectorElement, page, [100, 100], [150, 150]);
await wrapTest(createShapeElement, page, [100, 100], [150, 150]);
await wrapTest(createEdgelessText, page, [100, 100]);
await wrapTest(createMindmap, page, [100, 100]);
await wrapTest(createFrame, page, [100, 100], [150, 150]);
await wrapTest(createNote, page, [100, 100]);
await wrapTest(async () => {
await createShapeElement(page, [100, 100], [150, 150]);
await createShapeElement(page, [150, 150], [200, 200]);
await selectAllByKeyboard(page);
await page.keyboard.press(`${SHORT_KEY}+g`);
});
});
test('locked element should be selectable by clicking or short-cut', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [150, 150]);
await selectAllByKeyboard(page);
await getButtons(page).lock.click();
expect(await getSelectedIds(page)).toHaveLength(1);
await pressEscape(page);
expect(await getSelectedIds(page)).toHaveLength(0);
await selectAllByKeyboard(page);
expect(await getSelectedIds(page)).toHaveLength(1);
await pressEscape(page);
await clickView(page, [125, 125]);
expect(await getSelectedIds(page)).toHaveLength(1);
});
test('locked element should not be selectable by dragging default tool or lasso tool. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [150, 150]);
await selectAllByKeyboard(page);
const { lock, unlock } = getButtons(page);
await lock.click();
await pressEscape(page);
await dragBetweenViewCoords(page, [90, 90], [160, 160]);
expect(await getSelectedIds(page)).toHaveLength(0);
await clickView(page, [125, 125]);
await unlock.click();
await dragBetweenViewCoords(page, [90, 90], [160, 160]);
expect(await getSelectedIds(page)).toHaveLength(1);
});
test('descendant of locked element should not be selectable. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
const shapeId = await createShapeElement(page, [100, 100], [150, 150]);
await createShapeElement(page, [150, 150], [200, 200]);
await selectAllByKeyboard(page);
await page.keyboard.press(`${SHORT_KEY}+g`);
const groupId = (await getSelectedIds(page))[0];
const { lock, unlock } = getButtons(page);
await lock.click();
await pressEscape(page);
await clickView(page, [125, 125]);
expect(await getSelectedIds(page)).toEqual([groupId]);
await clickView(page, [125, 125]);
expect(await getSelectedIds(page)).toEqual([groupId]);
await unlock.click();
await clickView(page, [125, 125]);
expect(await getSelectedIds(page)).toEqual([shapeId]);
await pressEscape(page);
const frameId = await createFrame(page, [50, 50], [250, 250]);
await selectAllByKeyboard(page);
await lock.click();
await pressEscape(page);
await clickView(page, [125, 125]);
expect(await getSelectedIds(page)).toEqual([frameId]);
await unlock.click();
await clickView(page, [125, 125]);
expect(await getSelectedIds(page)).toEqual([shapeId]);
});
test('the selected rect of locked element should contain descendant. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
// frame
await createFrame(page, [0, 0], [100, 100]);
await createShapeElement(page, [100, 100], [150, 150]);
await dragBetweenViewCoords(page, [125, 125], [95, 95]); // add shape to frame, and partial area out of frame
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]); // only frame outline
const { lock, unlock } = getButtons(page);
await lock.click();
await assertEdgelessSelectedModelRect(page, [0, 0, 120, 120]); // frame outline and shape
await pressEscape(page);
await clickView(page, [100, 100]);
await assertEdgelessSelectedModelRect(page, [0, 0, 120, 120]);
await unlock.click();
await assertEdgelessSelectedModelRect(page, [0, 0, 100, 100]);
await pressEscape(page);
await clickView(page, [100, 100]);
await assertEdgelessSelectedModelRect(page, [70, 70, 50, 50]);
await deleteAll(page);
// mindmap
await createMindmap(page, [100, 100]);
const bound = await getSelectedBound(page);
const rootNodePos: [number, number] = [
bound[0] + 10,
bound[1] + 0.5 * bound[3],
];
await clickView(page, rootNodePos);
const rootNodeBound = await getSelectedBound(page);
await lock.click();
await assertEdgelessSelectedModelRect(page, bound);
await clickView(page, rootNodePos);
await assertEdgelessSelectedModelRect(page, bound);
await unlock.click();
await assertEdgelessSelectedModelRect(page, bound);
await clickView(page, rootNodePos);
await assertEdgelessSelectedModelRect(page, rootNodeBound);
});
test('locked element should be copyable, and the copy is unlocked', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [150, 150]);
await selectAllByKeyboard(page);
await getButtons(page).lock.click();
await pressEscape(page);
await clickView(page, [125, 125]);
await copyByKeyboard(page);
await moveView(page, [200, 200]);
await pasteByKeyboard(page);
await clickView(page, [200, 200]);
await expect(getButtons(page).lock).toBeVisible();
});
test('locked element and descendant should not be draggable and moved by arrow key. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
const frame = await createFrame(page, [50, 50], [250, 250]);
const shape1 = await createShapeElement(page, [100, 100], [150, 150]);
const shape2 = await createShapeElement(page, [150, 150], [200, 200]);
await selectAllByKeyboard(page);
await getButtons(page).lock.click();
await pressEscape(page);
await dragBetweenViewCoords(page, [100, 100], [150, 150]);
await assertEdgelessElementBound(page, frame, [50, 50, 200, 200]);
await assertEdgelessElementBound(page, shape1, [100, 100, 50, 50]);
await assertEdgelessElementBound(page, shape2, [150, 150, 50, 50]);
await pressArrowDown(page, 3);
await assertEdgelessElementBound(page, frame, [50, 50, 200, 200]);
await assertEdgelessElementBound(page, shape1, [100, 100, 50, 50]);
await assertEdgelessElementBound(page, shape2, [150, 150, 50, 50]);
await getButtons(page).unlock.click();
await dragBetweenViewCoords(page, [100, 100], [150, 150]);
await assertEdgelessElementBound(page, frame, [100, 100, 200, 200]);
await assertEdgelessElementBound(page, shape1, [150, 150, 50, 50]);
await assertEdgelessElementBound(page, shape2, [200, 200, 50, 50]);
await pressArrowDown(page, 3);
await assertEdgelessElementBound(page, frame, [100, 103, 200, 200]);
await assertEdgelessElementBound(page, shape1, [150, 153, 50, 50]);
await assertEdgelessElementBound(page, shape2, [200, 203, 50, 50]);
});
test('locked element should be moved if parent is moved', async ({
page,
}) => {
await edgelessCommonSetup(page);
const frame = await createFrame(page, [50, 50], [250, 250]);
const shape = await createShapeElement(page, [100, 100], [150, 150]);
await clickView(page, [125, 125]);
await getButtons(page).lock.click();
await selectAllByKeyboard(page);
await dragBetweenViewCoords(page, [100, 100], [150, 150]);
assertEdgelessElementBound(page, frame, [100, 100, 200, 200]);
assertEdgelessElementBound(page, shape, [150, 150, 50, 50]);
});
test('locked element should not be scalable and rotatable. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [150, 150]);
await selectAllByKeyboard(page);
const rect = page.locator('edgeless-selected-rect');
const { lock, unlock } = getButtons(page);
await expect(rect.locator('.resize')).toHaveCount(8);
await expect(rect.locator('.rotate')).toHaveCount(4);
await lock.click();
await expect(rect.locator('.resize')).toHaveCount(0);
await expect(rect.locator('.rotate')).toHaveCount(0);
await unlock.click();
await expect(rect.locator('.resize')).toHaveCount(8);
await expect(rect.locator('.rotate')).toHaveCount(4);
});
test('locked element should not be editable. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
const { lock, unlock } = getButtons(page);
// Shape
{
await createShapeElement(page, [100, 100], [150, 150]);
await selectAllByKeyboard(page);
await lock.click();
await dblclickView(page, [125, 125]);
await expect(page.locator('edgeless-shape-text-editor')).toHaveCount(0);
await unlock.click();
await dblclickView(page, [125, 125]);
await expect(page.locator('edgeless-shape-text-editor')).toHaveCount(1);
await deleteAll(page);
}
// Connector
{
await createConnectorElement(page, [100, 100], [150, 150]);
await selectAllByKeyboard(page);
await lock.click();
await dblclickView(page, [125, 125]);
await expect(page.locator('edgeless-connector-label-editor')).toHaveCount(
0
);
await unlock.click();
await dblclickView(page, [125, 125]);
await expect(page.locator('edgeless-connector-label-editor')).toHaveCount(
1
);
await deleteAll(page);
}
// Mindmap
{
await createMindmap(page, [100, 100]);
const bound = await getSelectedBound(page);
const rootPos: [number, number] = [
bound[0] + 10,
bound[1] + 0.5 * bound[3],
];
await lock.click();
await dblclickView(page, rootPos);
await expect(page.locator('edgeless-shape-text-editor')).toHaveCount(0);
await unlock.click();
await dblclickView(page, rootPos);
await expect(page.locator('edgeless-shape-text-editor')).toHaveCount(1);
await deleteAll(page);
}
// Edgeless Text
{
await createEdgelessText(page, [100, 100], 'text');
await selectAllByKeyboard(page);
await lock.click();
const text = page.locator('affine-edgeless-text');
await text.dblclick();
await type(page, '111');
await expect(text).toHaveText('text');
await unlock.click();
await text.dblclick();
await type(page, '111');
await expect(text).toHaveText('111');
await deleteAll(page);
}
// Note
{
await createNote(page, [100, 100], 'note');
await selectAllByKeyboard(page);
await lock.click();
const note = page.locator('affine-edgeless-note');
await note.dblclick();
await page.keyboard.press('End');
await type(page, '111');
await assertRichTexts(page, ['note']);
await unlock.click();
await note.dblclick();
await page.keyboard.press('End');
await type(page, '111');
await assertRichTexts(page, ['note111']);
await pressEscape(page, 3);
await deleteAll(page);
}
});
test('locked element should not be deletable. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [150, 150]);
await selectAllByKeyboard(page);
const { lock, unlock } = getButtons(page);
await lock.click();
await clickView(page, [125, 125]);
await pressBackspace(page);
await assertCanvasElementsCount(page, 1);
await page.keyboard.press('Delete');
await assertCanvasElementsCount(page, 1);
await pressForwardDelete(page);
await assertCanvasElementsCount(page, 1);
await setEdgelessTool(page, 'eraser');
await dragBetweenViewCoords(page, [90, 90], [160, 160], { steps: 2 });
await assertCanvasElementsCount(page, 1);
await setEdgelessTool(page, 'default');
await selectAllByKeyboard(page);
await unlock.click();
await page.evaluate(() => {
window.doc.captureSync();
});
await pressBackspace(page);
await assertCanvasElementsCount(page, 0);
await undoByKeyboard(page);
await assertCanvasElementsCount(page, 1);
await page.keyboard.press('Delete');
await assertCanvasElementsCount(page, 0);
await undoByKeyboard(page);
await assertCanvasElementsCount(page, 1);
await pressForwardDelete(page);
await assertCanvasElementsCount(page, 0);
await undoByKeyboard(page);
await assertCanvasElementsCount(page, 1);
await setEdgelessTool(page, 'eraser');
await dragBetweenViewCoords(page, [90, 90], [160, 160], { steps: 2 });
await assertCanvasElementsCount(page, 0);
});
test('locked frame should not add new child element. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
const frame = await createFrame(page, [50, 50], [250, 250]);
await selectAllByKeyboard(page);
const frameTitle = page.locator('affine-frame-title');
const { lock, unlock } = getButtons(page);
await lock.click();
const shape = await createShapeElement(page, [100, 100], [150, 150]);
expect(await getContainerChildIds(page, frame)).toHaveLength(0);
await frameTitle.click();
await unlock.click();
expect(await getContainerChildIds(page, frame)).toHaveLength(0);
await clickView(page, [125, 125]);
await dragBetweenViewCoords(page, [125, 125], [130, 130]); // move shape into frame
expect(await getContainerChildIds(page, frame)).toEqual([shape]);
});
test('locked mindmap can not create new node by pressing Tab. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createMindmap(page, [100, 100]);
await selectAllByKeyboard(page);
const bound = await getSelectedBound(page);
const rootPos: [number, number] = [
bound[0] + 10,
bound[1] + 0.5 * bound[3],
];
const nodeEditor = page.locator('edgeless-shape-text-editor');
const { lock, unlock } = getButtons(page);
await lock.click();
await clickView(page, rootPos);
await pressTab(page);
await expect(nodeEditor).toHaveCount(0);
await unlock.click();
await clickView(page, rootPos);
await pressTab(page);
await expect(nodeEditor).toHaveCount(1);
await expect(nodeEditor).toHaveText('New node');
});
test('endpoint of locked connector should not be changeable. unlocking will recover', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createConnectorElement(page, [100, 100], [150, 150]);
const handles = page.locator('edgeless-connector-handle');
await expect(handles).toHaveCount(1);
const { lock, unlock } = getButtons(page);
await lock.click();
await expect(handles).toHaveCount(0);
await unlock.click();
await expect(handles).toHaveCount(1);
});
test('locking multiple elements will create locked group. unlocking a group will release elements', async ({
page,
}) => {
await edgelessCommonSetup(page);
const shape1 = await createShapeElement(page, [100, 100], [150, 150]);
const shape2 = await createShapeElement(page, [150, 150], [200, 200]);
await selectAllByKeyboard(page);
const { lock, unlock } = getButtons(page);
await lock.click();
const group = (await getSelectedIds(page))[0];
expect(group).not.toBeUndefined();
expect(await getTypeById(page, group)).toBe('group');
await unlock.click();
expect(await getSelectedIds(page)).toEqual([shape1, shape2]);
});
test('locking a group should not create a new group', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [150, 150]);
await createShapeElement(page, [150, 150], [200, 200]);
await selectAllByKeyboard(page);
await page.keyboard.press(`${SHORT_KEY}+g`);
const group = (await getSelectedIds(page))[0];
await getButtons(page).lock.click();
expect(await getSelectedIds(page)).toEqual([group]);
});
test('unlocking an element should not unlock its locked descendant', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createFrame(page, [50, 50], [250, 250]);
await createShapeElement(page, [150, 150], [200, 200]);
const { lock, unlock } = getButtons(page);
await clickView(page, [175, 175]);
await lock.click();
await page.locator('affine-frame-title').click();
await lock.click();
await unlock.click();
await clickView(page, [175, 175]);
await expect(lock).toBeHidden();
await expect(unlock).toBeVisible();
});
});

View File

@@ -0,0 +1,371 @@
import { expect } from '@playwright/test';
import { clickView } from '../utils/actions/click.js';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import {
addBasicRectShapeElement,
autoFit,
edgelessCommonSetup,
getSelectedBound,
getSelectedBoundCount,
selectElementInEdgeless,
waitFontsLoaded,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import {
pressBackspace,
pressEnter,
pressTab,
selectAllByKeyboard,
type,
undoByKeyboard,
} from '../utils/actions/keyboard.js';
import { waitNextFrame } from '../utils/actions/misc.js';
import {
assertEdgelessSelectedRect,
assertSelectedBound,
} from '../utils/asserts.js';
import {
addMindmapNodes,
createMindMap,
getMindMapNode,
} from '../utils/mindmap.js';
import { test } from '../utils/playwright.js';
test('elements should be selectable after open mindmap menu', async ({
page,
}) => {
await edgelessCommonSetup(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicRectShapeElement(page, start, end);
await page.locator('.basket-wrapper').click({ position: { x: 0, y: 0 } });
await expect(page.locator('edgeless-mindmap-menu')).toBeVisible();
await page.mouse.click(start.x + 5, start.y + 5);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('undo deletion of mindmap should restore the deleted element', async ({
page,
}) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
await page.keyboard.press('m');
await clickView(page, [0, 0]);
await autoFit(page);
await selectAllByKeyboard(page);
const mindmapBound = await getSelectedBound(page);
await pressBackspace(page);
await selectAllByKeyboard(page);
expect(await getSelectedBoundCount(page)).toBe(0);
await undoByKeyboard(page);
await selectAllByKeyboard(page);
await assertSelectedBound(page, mindmapBound);
});
test('drag mind map node to reorder the node', async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
const mindmapId = await createMindMap(page, [0, 0]);
const { id: nodeId, rect: nodeRect } = await getMindMapNode(
page,
mindmapId,
[0, 0]
);
const { rect: targetRect } = await getMindMapNode(page, mindmapId, [0, 1]);
const { rect: lastRect } = await getMindMapNode(page, mindmapId, [0, 2]);
await selectElementInEdgeless(page, [nodeId]);
await dragBetweenCoords(
page,
{ x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 },
{ x: targetRect.x + targetRect.w / 2, y: targetRect.y + targetRect.h + 40 },
{
steps: 50,
}
);
expect((await getMindMapNode(page, mindmapId, [0, 1])).id).toEqual(nodeId);
await dragBetweenCoords(
page,
{ x: targetRect.x + targetRect.w / 2, y: targetRect.y + targetRect.h / 2 },
{ x: nodeRect.x - 20, y: nodeRect.y - 40 },
{
steps: 50,
}
);
expect((await getMindMapNode(page, mindmapId, [0, 0])).id).toEqual(nodeId);
await dragBetweenCoords(
page,
{ x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 },
{ x: lastRect.x - 20, y: lastRect.y + lastRect.h + 40 },
{
steps: 50,
}
);
expect((await getMindMapNode(page, mindmapId, [0, 2])).id).toEqual(nodeId);
});
test('drag mind map node to make it a child node', async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
const mindmapId = await createMindMap(page, [0, 0]);
{
const { id: nodeId, rect: nodeRect } = await getMindMapNode(
page,
mindmapId,
[0, 0]
);
const { rect: targetRect } = await getMindMapNode(page, mindmapId, [0, 1]);
await selectElementInEdgeless(page, [nodeId]);
await dragBetweenCoords(
page,
{ x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 },
{
x: targetRect.x + targetRect.w / 2,
y: targetRect.y + targetRect.h / 2,
},
{
steps: 50,
}
);
expect((await getMindMapNode(page, mindmapId, [0, 0, 0])).id).toEqual(
nodeId
);
}
{
const { id: childId } = await getMindMapNode(page, mindmapId, [0, 0, 0]);
const { rect: firstRect } = await getMindMapNode(page, mindmapId, [0, 0]);
const { rect: secondRect } = await getMindMapNode(page, mindmapId, [0, 1]);
await dragBetweenCoords(
page,
{ x: firstRect.x + firstRect.w / 2, y: firstRect.y + firstRect.h / 2 },
{
x: secondRect.x + secondRect.w + 10,
y: secondRect.y + secondRect.h / 2,
},
{
steps: 50,
}
);
expect((await getMindMapNode(page, mindmapId, [0, 0, 0, 0])).id).toEqual(
childId
);
}
});
test('cannot drag mind map node to itself or its descendants', async ({
page,
}) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
const mindmapId = await createMindMap(page, [0, 1]);
await addMindmapNodes(page, mindmapId, [0, 1], {
text: 'child node 1',
children: [
{
text: 'child node 2',
},
{
text: 'child node 3',
},
],
});
const { id: node, rect } = await getMindMapNode(page, mindmapId, [0, 1]);
const { id: childNode3, rect: childRect3 } = await getMindMapNode(
page,
mindmapId,
[0, 1, 0, 1]
);
await dragBetweenCoords(
page,
{ x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 },
{ x: childRect3.x + childRect3.w + 10, y: childRect3.y + childRect3.h / 2 },
{
steps: 50,
}
);
expect((await getMindMapNode(page, mindmapId, [0, 1])).id).toEqual(node);
expect((await getMindMapNode(page, mindmapId, [0, 1, 0, 1])).id).toEqual(
childNode3
);
});
test('drag root node should layout in real time', async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
// wait for the font to be loaded
await waitFontsLoaded(page);
const mindmapId = await createMindMap(page, [0, 0]);
const { rect: rootRect } = await getMindMapNode(page, mindmapId, [0]);
const { rect: firstRect } = await getMindMapNode(page, mindmapId, [0, 0]);
const { rect: secondRect } = await getMindMapNode(page, mindmapId, [0, 1]);
const { rect: thirdRect } = await getMindMapNode(page, mindmapId, [0, 2]);
const assertMindMapNodesPosition = async (deltaX: number, deltaY: number) => {
expect((await getMindMapNode(page, mindmapId, [0, 0])).rect).toEqual({
...firstRect,
x: firstRect.x + deltaX,
y: firstRect.y + deltaY,
});
expect((await getMindMapNode(page, mindmapId, [0, 1])).rect).toEqual({
...secondRect,
x: secondRect.x + deltaX,
y: secondRect.y + deltaY,
});
expect((await getMindMapNode(page, mindmapId, [0, 2])).rect).toEqual({
...thirdRect,
x: thirdRect.x + deltaX,
y: thirdRect.y + deltaY,
});
};
await dragBetweenCoords(
page,
{ x: rootRect.x + rootRect.w / 2, y: rootRect.y + rootRect.h / 2 },
{
x: rootRect.x + rootRect.w / 2 + 10,
y: rootRect.y + rootRect.h / 2 + 10,
},
{
steps: 50,
}
);
await assertMindMapNodesPosition(10, 10);
await page.mouse.move(
rootRect.x + rootRect.w / 2 + 10,
rootRect.y + rootRect.h / 2 + 10
);
await page.mouse.down();
await page.mouse.move(
rootRect.x + rootRect.w / 2 + 10 + 4,
rootRect.y + rootRect.h / 2 + 10 + 4
);
await page.mouse.move(
rootRect.x + rootRect.w / 2 + 10 + 44,
rootRect.y + rootRect.h / 2 + 10 + 44,
{ steps: 10 }
);
// assert when dragging is in progress
await waitNextFrame(page, 500);
await assertMindMapNodesPosition(50, 50);
await page.mouse.up();
});
test('drag node out of mind map should detach the node and create a new mind map', async ({
page,
}) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
const mindmapId = await createMindMap(page, [0, 1]);
await addMindmapNodes(page, mindmapId, [0, 1], {
text: 'child node 1',
children: [
{
text: 'child node 2',
},
{
text: 'child node 3',
},
],
});
const { rect } = await getMindMapNode(page, mindmapId, [0, 1]);
await dragBetweenCoords(
page,
{
x: rect.x + rect.w / 2,
y: rect.y + rect.h / 2,
},
{
x: rect.x + rect.w / 2,
y: rect.y + rect.h / 2 + 300,
},
{
steps: 50,
}
);
const { count, mindmap: lastMindmapId } = await page.evaluate(() => {
const edgelessBlock = document.querySelector('affine-edgeless-root');
if (!edgelessBlock) {
throw new Error('edgeless block not found');
}
const mindmaps = edgelessBlock.gfx.gfxElements.filter(
el => 'type' in el && el.type === 'mindmap'
);
return {
count: mindmaps.length,
mindmap: mindmaps[mindmaps.length - 1].id,
};
});
expect(count).toBe(2);
expect((await getMindMapNode(page, lastMindmapId, [0, 0])).text).toBe(
'child node 1'
);
expect((await getMindMapNode(page, lastMindmapId, [0, 0, 0])).text).toBe(
'child node 2'
);
expect((await getMindMapNode(page, lastMindmapId, [0, 0, 1])).text).toBe(
'child node 3'
);
});
test('allow to type content directly when node has been selected', async ({
page,
}) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
const mindmapId = await createMindMap(page, [0, 0]);
const { id: nodeId } = await getMindMapNode(page, mindmapId, [0, 1]);
await clickView(page, [0, 0]);
await selectElementInEdgeless(page, [nodeId]);
await type(page, 'parent node');
await pressEnter(page);
await pressTab(page);
await type(page, 'child node 1');
await pressEnter(page);
await pressEnter(page);
await type(page, 'child node 2');
await pressEnter(page);
expect((await getMindMapNode(page, mindmapId, [0, 1])).text).toBe(
'parent node'
);
expect((await getMindMapNode(page, mindmapId, [0, 1, 0])).text).toBe(
'child node 1'
);
expect((await getMindMapNode(page, mindmapId, [0, 1, 1])).text).toBe(
'child node 2'
);
});

View File

@@ -0,0 +1,155 @@
import { expect } from '@playwright/test';
import {
addNote,
dragHandleFromBlockToBlockBottomById,
enterPlaygroundRoom,
focusRichText,
initEmptyEdgelessState,
initThreeParagraphs,
setEdgelessTool,
switchEditorMode,
type,
waitNextFrame,
} from '../../utils/actions/index.js';
import { assertRectExist, assertRichTexts } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
const CENTER_X = 450;
const CENTER_Y = 450;
test('drag handle should be shown when a note is activated in default mode or hidden in other modes', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
await switchEditorMode(page);
const noteBox = await page.locator('affine-edgeless-note').boundingBox();
if (!noteBox) {
throw new Error('Missing edgeless affine-note');
}
const [x, y] = [noteBox.x + 26, noteBox.y + noteBox.height / 2];
await page.mouse.move(x, y);
await expect(page.locator('.affine-drag-handle-container')).toBeHidden();
await page.mouse.dblclick(x, y);
await waitNextFrame(page);
await page.mouse.move(x, y);
await expect(page.locator('.affine-drag-handle-container')).toBeVisible();
await page.mouse.move(0, 0);
await setEdgelessTool(page, 'shape');
await page.mouse.move(x, y);
await expect(page.locator('.affine-drag-handle-container')).toBeHidden();
await page.mouse.move(0, 0);
await setEdgelessTool(page, 'default');
await page.mouse.move(x, y);
await expect(page.locator('.affine-drag-handle-container')).toBeVisible();
});
test('drag handle can drag note into another note', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
await switchEditorMode(page);
const noteRect = await page
.locator(`[data-block-id="${noteId}"]`)
.boundingBox();
assertRectExist(noteRect);
const secondNoteId = await addNote(page, 'hello world', 100, 100);
await waitNextFrame(page);
const secondNoteRect = await page
.locator(`[data-block-id="${secondNoteId}"]`)
.boundingBox();
assertRectExist(secondNoteRect);
{
const [x, y] = [
noteRect.x + noteRect.width / 2,
noteRect.y + noteRect.height / 2,
];
await page.mouse.click(noteRect.x, noteRect.y + noteRect.height + 100);
await page.mouse.move(x, y);
await page.mouse.click(x, y);
const handlerRect = await page
.locator('.affine-drag-handle-container')
.boundingBox();
assertRectExist(handlerRect);
await page.mouse.move(
handlerRect.x + handlerRect.width / 2,
handlerRect.y + handlerRect.height / 2
);
await page.mouse.down();
const [targetX, targetY] = [
secondNoteRect.x + 10,
secondNoteRect.y + secondNoteRect.height / 2,
];
await page.mouse.move(targetX, targetY);
await page.mouse.up();
await waitNextFrame(page);
}
});
test('drag handle should work inside one note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await switchEditorMode(page);
await page.mouse.dblclick(CENTER_X, CENTER_Y);
await dragHandleFromBlockToBlockBottomById(page, '3', '5');
await waitNextFrame(page);
await expect(page.locator('affine-drag-handle-container')).toBeHidden();
await assertRichTexts(page, ['456', '789', '123']);
});
test.fixme(
'drag handle should work across multiple notes',
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await switchEditorMode(page);
await setEdgelessTool(page, 'note');
await page.mouse.click(200, 200);
await focusRichText(page, 3);
await waitNextFrame(page);
// block id 7
await type(page, '000');
await page.mouse.dblclick(CENTER_X, CENTER_Y - 20);
await dragHandleFromBlockToBlockBottomById(page, '3', '7');
await expect(page.locator('.affine-drag-handle-container')).toBeHidden();
await waitNextFrame(page);
await assertRichTexts(page, ['456', '789', '000', '123']);
// await page.mouse.dblclick(305, 305);
await dragHandleFromBlockToBlockBottomById(page, '3', '4');
await waitNextFrame(page);
await expect(page.locator('.affine-drag-handle-container')).toBeHidden();
await assertRichTexts(page, ['456', '123', '789', '000']);
await expect(page.locator('selected > *')).toHaveCount(0);
}
);

View File

@@ -0,0 +1,79 @@
import {
addNote,
changeNoteDisplayModeWithId,
enterPlaygroundRoom,
initEmptyEdgelessState,
switchEditorMode,
zoomResetByKeyboard,
} from '../../utils/actions/index.js';
import { assertBlockCount } from '../../utils/asserts.js';
import { NoteDisplayMode } from '../../utils/bs-alternative.js';
import { test } from '../../utils/playwright.js';
test('Note added on doc mode should display on both modes by default', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
// there should be 1 note in doc page
await assertBlockCount(page, 'note', 1);
await switchEditorMode(page);
// there should be 1 note in edgeless page as well
await assertBlockCount(page, 'edgeless-note', 1);
});
test('Note added on edgeless mode should display on edgeless only by default', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await addNote(page, 'note2', 100, 100);
// assert add note success, there should be 2 notes in edgeless page
await assertBlockCount(page, 'edgeless-note', 2);
await switchEditorMode(page);
// switch to doc mode, the note added on edgeless mode should not render on doc mode
// there should be only 1 note in doc page
await assertBlockCount(page, 'note', 1);
});
test('Note can be changed to display on doc and edgeless mode', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
const noteId = await addNote(page, 'note2', 100, 200);
await page.mouse.click(200, 150);
// assert add note success, there should be 2 notes in edgeless page
await assertBlockCount(page, 'edgeless-note', 2);
// switch to doc mode
await switchEditorMode(page);
// there should be 1 notes in doc page
await assertBlockCount(page, 'note', 1);
// switch back to edgeless mode
await switchEditorMode(page);
// change note display mode to doc only
await changeNoteDisplayModeWithId(
page,
noteId,
NoteDisplayMode.DocAndEdgeless
);
// there should still be 2 notes in edgeless page
await assertBlockCount(page, 'edgeless-note', 2);
// switch to doc mode
await switchEditorMode(page);
// change successfully, there should be 2 notes in doc page
await assertBlockCount(page, 'note', 2);
});

View File

@@ -0,0 +1,534 @@
import { expect } from '@playwright/test';
import { lightThemeV2 } from '@toeverything/theme/v2';
import {
activeNoteInEdgeless,
addNote,
assertEdgelessTool,
changeEdgelessNoteBackground,
changeNoteDisplayMode,
locatorComponentToolbar,
locatorEdgelessZoomToolButton,
selectNoteInEdgeless,
setEdgelessTool,
switchEditorMode,
triggerComponentToolbarAction,
zoomOutByKeyboard,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
click,
clickBlockById,
dragBetweenCoords,
dragBetweenIndices,
enterPlaygroundRoom,
focusRichText,
focusRichTextEnd,
initEmptyEdgelessState,
initThreeParagraphs,
pressArrowDown,
pressArrowUp,
pressBackspace,
pressEnter,
pressTab,
type,
undoByKeyboard,
waitForInlineEditorStateUpdated,
waitNextFrame,
} from '../../utils/actions/index.js';
import {
assertBlockChildrenIds,
assertBlockCount,
assertEdgelessNonSelectedRect,
assertEdgelessNoteBackground,
assertEdgelessSelectedRect,
assertExists,
assertNoteSequence,
assertNoteXYWH,
assertRichTextInlineRange,
assertRichTexts,
assertTextSelection,
} from '../../utils/asserts.js';
import {
DEFAULT_NOTE_HEIGHT,
DEFAULT_NOTE_WIDTH,
NoteDisplayMode,
} from '../../utils/bs-alternative.js';
import { test } from '../../utils/playwright.js';
const CENTER_X = 450;
const CENTER_Y = 450;
test('can drag selected non-active note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
// selected, non-active
await page.mouse.click(CENTER_X, CENTER_Y);
await dragBetweenCoords(
page,
{ x: CENTER_X, y: CENTER_Y },
{ x: CENTER_X, y: CENTER_Y + 100 }
);
await assertNoteXYWH(page, [0, 100, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
await undoByKeyboard(page);
await waitNextFrame(page);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
});
test('add Note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await addNote(page, 'hello', 300, 300);
await assertEdgelessTool(page, 'default');
await assertRichTexts(page, ['', 'hello']);
await page.mouse.click(300, 200);
await page.mouse.click(350, 320);
await assertEdgelessSelectedRect(page, [
270,
260,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
});
test('add empty Note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'note');
// add note at 300,300
await page.mouse.click(300, 300);
await waitForInlineEditorStateUpdated(page);
// should wait for inline editor update and resizeObserver callback
await waitNextFrame(page);
// assert add note success
await assertBlockCount(page, 'edgeless-note', 2);
// click out of note
await page.mouse.click(250, 200);
// assert empty note is note removed
await page.mouse.move(320, 320);
await assertBlockCount(page, 'edgeless-note', 2);
});
test('always keep at least 1 note block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'default');
// clicking in default mode will try to remove empty note block
await page.mouse.click(0, 0);
const notes = await page.locator('affine-edgeless-note').all();
expect(notes.length).toEqual(1);
});
test('edgeless arrow up/down', async ({ page }) => {
await enterPlaygroundRoom(page);
const { paragraphId, noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 400);
await type(page, 'aaaaa');
await pressEnter(page);
await type(page, 'aaaaa');
await pressEnter(page);
await type(page, 'aaa');
await waitForInlineEditorStateUpdated(page);
// 0 for page, 1 for surface, 2 for note, 3 for paragraph
expect(paragraphId).toBe('3');
await clickBlockById(page, paragraphId);
await assertRichTextInlineRange(page, 0, 5, 0);
await pressArrowDown(page);
await waitNextFrame(page);
await assertRichTextInlineRange(page, 1, 5, 0);
await pressArrowUp(page);
await waitNextFrame(page);
await assertRichTextInlineRange(page, 0, 5, 0);
await pressArrowUp(page);
await waitNextFrame(page);
await assertRichTextInlineRange(page, 0, 0, 0);
});
test('dragging un-selected note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
await switchEditorMode(page);
const noteBox = await page.locator('affine-edgeless-note').boundingBox();
if (!noteBox) {
throw new Error('Missing edgeless affine-note');
}
await page.mouse.click(noteBox.x + 5, noteBox.y + 5);
await assertEdgelessSelectedRect(page, [
noteBox.x,
noteBox.y,
noteBox.width,
noteBox.height,
]);
await dragBetweenCoords(
page,
{ x: noteBox.x + 10, y: noteBox.y + 15 },
{ x: noteBox.x + 10, y: noteBox.y + 35 },
{ steps: 10 }
);
await assertEdgelessSelectedRect(page, [
noteBox.x,
noteBox.y + 20,
noteBox.width,
noteBox.height,
]);
});
test('format quick bar should show up when double-clicking on text', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await switchEditorMode(page);
await page.mouse.dblclick(CENTER_X, CENTER_Y);
await waitNextFrame(page);
await page
.locator('rich-text')
.nth(1)
.dblclick({
position: { x: 10, y: 10 },
delay: 20,
});
await page.waitForTimeout(200);
const formatBar = page.locator('.affine-format-bar-widget');
await expect(formatBar).toBeVisible();
});
test('when editing text in edgeless, should hide component toolbar', async ({
page,
}) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
const toolbar = locatorComponentToolbar(page);
await expect(toolbar).toBeVisible();
await page.mouse.click(0, 0);
await activeNoteInEdgeless(page, noteId);
await expect(toolbar).toBeHidden();
});
test('duplicate note should work correctly', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'duplicate');
await waitNextFrame(page, 200); // wait viewport fit animation
const moreActionsContainer = page.locator('.more-actions-container');
await expect(moreActionsContainer).toBeHidden();
const noteLocator = page.locator('affine-edgeless-note');
await expect(noteLocator).toHaveCount(2);
const [firstNote, secondNote] = await noteLocator.all();
// content should be same
expect(await firstNote.innerText()).toEqual(await secondNote.innerText());
// size should be same
const firstNoteBox = await firstNote.boundingBox();
const secondNoteBox = await secondNote.boundingBox();
expect(firstNoteBox!.width).toBeCloseTo(secondNoteBox!.width);
expect(firstNoteBox!.height).toBeCloseTo(secondNoteBox!.height);
});
test('double click toolbar zoom button, should not add text', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const zoomOutButton = await locatorEdgelessZoomToolButton(
page,
'zoomOut',
false
);
await zoomOutButton.dblclick();
await assertEdgelessNonSelectedRect(page);
});
test('change note color', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await switchEditorMode(page);
await assertEdgelessNoteBackground(
page,
noteId,
lightThemeV2['edgeless/note/white']
);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'changeNoteColor');
await changeEdgelessNoteBackground(page, 'Green');
await assertEdgelessNoteBackground(
page,
noteId,
lightThemeV2['edgeless/note/green']
);
});
test('cursor for active and inactive state', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await pressEnter(page);
await pressEnter(page);
await assertRichTexts(page, ['hello', '', '']);
await switchEditorMode(page);
await page.mouse.click(CENTER_X, CENTER_Y);
await waitNextFrame(page);
await assertTextSelection(page);
await page.mouse.dblclick(CENTER_X, CENTER_Y);
await waitNextFrame(page);
await assertTextSelection(page, {
blockId: '3',
index: 5,
length: 0,
});
});
test('when no visible note block, clicking in page mode will auto add a new note block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await assertBlockCount(page, 'edgeless-note', 1);
// select note
await selectNoteInEdgeless(page, '2');
await assertNoteSequence(page, '1');
await assertBlockCount(page, 'edgeless-note', 1);
// hide note
await triggerComponentToolbarAction(page, 'changeNoteDisplayMode');
await waitNextFrame(page);
await changeNoteDisplayMode(page, NoteDisplayMode.EdgelessOnly);
await switchEditorMode(page);
let note = await page.evaluate(() => {
return document.querySelector('affine-note');
});
expect(note).toBeNull();
await click(page, { x: 200, y: 280 });
note = await page.evaluate(() => {
return document.querySelector('affine-note');
});
expect(note).not.toBeNull();
});
test.fixme(
'Click at empty note should add a paragraph block',
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, '123');
await assertRichTexts(page, ['123']);
await switchEditorMode(page);
// Drag paragraph out of note block
const paragraphBlock = await page
.locator(`[data-block-id="3"]`)
.boundingBox();
assertExists(paragraphBlock);
await page.mouse.dblclick(paragraphBlock.x, paragraphBlock.y);
await waitNextFrame(page);
await page.mouse.move(
paragraphBlock.x + paragraphBlock.width / 2,
paragraphBlock.y + paragraphBlock.height / 2
);
await waitNextFrame(page);
const handle = await page
.locator('.affine-drag-handle-container')
.boundingBox();
assertExists(handle);
await page.mouse.move(
handle.x + handle.width / 2,
handle.y + handle.height / 2,
{ steps: 10 }
);
await page.mouse.down();
await page.mouse.move(100, 200, { steps: 30 });
await page.mouse.up();
// There should be two note blocks and one paragraph block
await assertRichTexts(page, ['123']);
await assertBlockCount(page, 'edgeless-note', 2);
await assertBlockCount(page, 'paragraph', 1);
// Click at empty note block to add a paragraph block
const emptyNote = await page.locator(`[data-block-id="2"]`).boundingBox();
assertExists(emptyNote);
await page.mouse.click(
emptyNote.x + emptyNote.width / 2,
emptyNote.y + emptyNote.height / 2
);
await waitNextFrame(page, 300);
await type(page, '456');
await waitNextFrame(page, 400);
await page.mouse.click(100, 100);
await waitNextFrame(page, 400);
await assertBlockCount(page, 'paragraph', 2);
}
);
test('Should focus at closest text block when note collapse', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
// Make sure there is no rich text content
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertRichTexts(page, ['']);
// Select the note
await zoomOutByKeyboard(page);
const notePortalBox = await page
.locator('affine-edgeless-note')
.boundingBox();
assertExists(notePortalBox);
await page.mouse.click(notePortalBox.x + 10, notePortalBox.y + 10);
await waitNextFrame(page, 200);
const selectedRect = page
.locator('edgeless-selected-rect')
.locator('.affine-edgeless-selected-rect');
await expect(selectedRect).toBeVisible();
// Collapse the note
const selectedBox = await selectedRect.boundingBox();
assertExists(selectedBox);
await page.mouse.move(
selectedBox.x + selectedBox.width / 2,
selectedBox.y + selectedBox.height
);
await page.mouse.down();
await page.mouse.move(
selectedBox.x + selectedBox.width / 2,
selectedBox.y + selectedBox.height + 200,
{ steps: 10 }
);
await page.mouse.up();
await expect(selectedRect).toBeVisible();
// Click at the bottom of note to focus at the closest text block
await page.mouse.click(
selectedBox.x + selectedBox.width / 2,
selectedBox.y + selectedBox.height - 20
);
await waitNextFrame(page, 200);
// Should be enter edit mode and there are no selected rect
await expect(selectedRect).toBeHidden();
// Focus at the closest text block and make sure can type
await type(page, 'hello');
await waitNextFrame(page, 200);
await assertRichTexts(page, ['hello']);
});
test('delete first block in edgeless note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]);
await page.mouse.dblclick(CENTER_X, CENTER_Y);
// first block without children, nothing should happen
await assertRichTexts(page, ['']);
await assertBlockChildrenIds(page, '3', []);
await pressBackspace(page);
await type(page, 'aaa');
await pressEnter(page);
await type(page, 'bbb');
await pressTab(page);
await assertRichTexts(page, ['aaa', 'bbb']);
await assertBlockChildrenIds(page, '3', ['4']);
// first block with children, need to bring children to parent
await focusRichTextEnd(page);
await pressBackspace(page, 3);
await assertRichTexts(page, ['', 'bbb']);
await pressBackspace(page);
await assertRichTexts(page, ['bbb']);
await assertBlockChildrenIds(page, '4', []);
});
test('select text cross blocks in edgeless note', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 400);
await type(page, 'aaa');
await pressEnter(page);
await type(page, 'bbb');
await pressEnter(page);
await type(page, 'ccc');
await assertRichTexts(page, ['aaa', 'bbb', 'ccc']);
await dragBetweenIndices(page, [0, 1], [2, 2]);
await pressBackspace(page);
await assertRichTexts(page, ['ac']);
});

View File

@@ -0,0 +1,223 @@
import { expect } from '@playwright/test';
import {
activeNoteInEdgeless,
enterPlaygroundRoom,
getNoteRect,
initEmptyEdgelessState,
redoByClick,
resizeElementByHandle,
selectNoteInEdgeless,
setEdgelessTool,
switchEditorMode,
triggerComponentToolbarAction,
type,
undoByClick,
waitForInlineEditorStateUpdated,
waitNextFrame,
zoomResetByKeyboard,
} from '../../utils/actions/index.js';
import {
assertBlockCount,
assertEdgelessSelectedRect,
assertNoteRectEqual,
assertRectEqual,
assertRichTexts,
} from '../../utils/asserts.js';
import { NOTE_MIN_HEIGHT, NOTE_MIN_WIDTH } from '../../utils/bs-alternative.js';
import { test } from '../../utils/playwright.js';
test('resize note in edgeless mode', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 400);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
// unselect note
await page.mouse.click(50, 50);
expect(noteId).toBe('2'); // 0 for page, 1 for surface
await selectNoteInEdgeless(page, noteId);
const initRect = await getNoteRect(page, noteId);
await resizeElementByHandle(page, { x: -100, y: 0 }, 'bottom-left');
const draggedRect = await getNoteRect(page, noteId);
assertRectEqual(draggedRect, {
x: initRect.x - 100,
y: initRect.y,
w: initRect.w + 100,
h: initRect.h,
});
await switchEditorMode(page);
await switchEditorMode(page);
const newRect = await getNoteRect(page, noteId);
assertRectEqual(newRect, draggedRect);
});
test('resize note then collapse note', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 400);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
// unselect note
await page.mouse.click(50, 50);
expect(noteId).toBe('2'); // 0 for page, 1 for surface
await selectNoteInEdgeless(page, noteId);
const initRect = await getNoteRect(page, noteId);
await resizeElementByHandle(page, { x: 0, y: 100 }, 'bottom-right');
let noteRect = await getNoteRect(page, noteId);
await expect(page.getByTestId('edgeless-note-collapse-button')).toBeVisible();
assertRectEqual(noteRect, {
x: initRect.x,
y: initRect.y,
w: initRect.w,
h: initRect.h + 100,
});
await page.getByTestId('edgeless-note-collapse-button')!.click();
let domRect = await page.locator('affine-edgeless-note').boundingBox();
expect(domRect!.height).toBeCloseTo(NOTE_MIN_HEIGHT);
await page.getByTestId('edgeless-note-collapse-button')!.click();
domRect = await page.locator('affine-edgeless-note').boundingBox();
expect(domRect!.height).toBeCloseTo(initRect.h + 100);
await selectNoteInEdgeless(page, noteId);
await resizeElementByHandle(page, { x: 0, y: -150 }, 'bottom-right');
noteRect = await getNoteRect(page, noteId);
assertRectEqual(noteRect, {
x: initRect.x,
y: initRect.y,
w: initRect.w,
h: NOTE_MIN_HEIGHT,
});
await switchEditorMode(page);
await switchEditorMode(page);
const newRect = await getNoteRect(page, noteId);
assertRectEqual(newRect, noteRect);
});
test('resize note then auto size and custom size', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 400);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
// unselect note
await page.mouse.click(50, 50);
await selectNoteInEdgeless(page, noteId);
const initRect = await getNoteRect(page, noteId);
await resizeElementByHandle(page, { x: 0, y: 100 }, 'bottom-right');
const draggedRect = await getNoteRect(page, noteId);
assertRectEqual(draggedRect, {
x: initRect.x,
y: initRect.y,
w: initRect.w,
h: initRect.h + 100,
});
await triggerComponentToolbarAction(page, 'autoSize');
await waitNextFrame(page, 200);
const autoSizeRect = await getNoteRect(page, noteId);
assertRectEqual(autoSizeRect, initRect);
await triggerComponentToolbarAction(page, 'autoSize');
await waitNextFrame(page, 200);
await assertNoteRectEqual(page, noteId, draggedRect);
await undoByClick(page);
await page.mouse.click(50, 50);
await waitNextFrame(page, 200);
await assertNoteRectEqual(page, noteId, initRect);
await redoByClick(page);
await waitNextFrame(page, 200);
await assertNoteRectEqual(page, noteId, draggedRect);
});
test('drag to add customized size note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'note');
// add note at 300,300
await page.mouse.move(300, 300);
await page.mouse.down();
await page.mouse.move(900, 600, { steps: 10 });
await page.mouse.up();
// should wait for inline editor update and resizeObserver callback
await waitForInlineEditorStateUpdated(page);
// assert add note success
await assertBlockCount(page, 'edgeless-note', 2);
// click out of note
await page.mouse.click(250, 200);
// click on note to select it
await page.mouse.click(600, 500);
// assert selected note
// note add on edgeless mode will have a offsetX of 30 and offsetY of 40
await assertEdgelessSelectedRect(page, [270, 260, 600, 300]);
});
test('drag to add customized size note: should clamp to min width and min height', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'note');
// add note at 300,300
await page.mouse.move(300, 300);
await page.mouse.down();
await page.mouse.move(400, 360, { steps: 10 });
await page.mouse.up();
await waitNextFrame(page);
await waitNextFrame(page);
// should wait for inline editor update and resizeObserver callback
await waitForInlineEditorStateUpdated(page);
// assert add note success
await assertBlockCount(page, 'edgeless-note', 2);
// click out of note
await page.mouse.click(250, 200);
// click on note to select it
await page.mouse.click(320, 300);
// assert selected note
// note add on edgeless mode will have a offsetX of 30 and offsetY of 40
await assertEdgelessSelectedRect(page, [
270,
260,
NOTE_MIN_WIDTH,
NOTE_MIN_HEIGHT,
]);
});

View File

@@ -0,0 +1,147 @@
import { expect, type Page } from '@playwright/test';
import {
addNote,
locatorScalePanelButton,
selectNoteInEdgeless,
switchEditorMode,
triggerComponentToolbarAction,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
copyByKeyboard,
pasteByKeyboard,
selectAllByKeyboard,
} from '../../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
initEmptyEdgelessState,
waitNextFrame,
} from '../../utils/actions/misc.js';
import { assertRectExist } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
async function setupAndAddNote(page: Page) {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
const noteId = await addNote(page, 'hello world', 100, 200);
await page.mouse.click(0, 0);
return noteId;
}
async function openScalePanel(page: Page, noteId: string) {
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'changeNoteScale');
await waitNextFrame(page);
const scalePanel = page.locator('edgeless-scale-panel');
await expect(scalePanel).toBeVisible();
return scalePanel;
}
async function checkNoteScale(
page: Page,
noteId: string,
expectedScale: number,
expectedType: 'equal' | 'greater' | 'less' = 'equal'
) {
const edgelessNote = page.locator(
`affine-edgeless-note[data-block-id="${noteId}"]`
);
const noteContainer = edgelessNote.getByTestId('edgeless-note-container');
const style = await noteContainer.getAttribute('style');
if (!style) {
throw new Error('Style attribute not found');
}
const scaleMatch = style.match(/transform:\s*scale\(([\d.]+)\)/);
if (!scaleMatch) {
throw new Error('Scale transform not found in style');
}
const actualScale = parseFloat(scaleMatch[1]);
switch (expectedType) {
case 'equal':
expect(actualScale).toBeCloseTo(expectedScale, 2);
break;
case 'greater':
expect(actualScale).toBeGreaterThan(expectedScale);
break;
case 'less':
expect(actualScale).toBeLessThan(expectedScale);
}
}
test.describe('note scale', () => {
test('Note scale can be changed by scale panel button', async ({ page }) => {
const noteId = await setupAndAddNote(page);
await openScalePanel(page, noteId);
const scale150 = locatorScalePanelButton(page, 50);
await scale150.click();
await checkNoteScale(page, noteId, 0.5);
});
test('Note scale can be changed by scale panel input', async ({ page }) => {
const noteId = await setupAndAddNote(page);
const scalePanel = await openScalePanel(page, noteId);
const scaleInput = scalePanel.locator('.scale-input');
await scaleInput.click();
await page.keyboard.type('50');
await page.keyboard.press('Enter');
await checkNoteScale(page, noteId, 0.5);
});
test('Note scale input support copy paste', async ({ page }) => {
const noteId = await setupAndAddNote(page);
const scalePanel = await openScalePanel(page, noteId);
const scaleInput = scalePanel.locator('.scale-input');
await scaleInput.click();
await page.keyboard.type('50');
await selectAllByKeyboard(page);
await copyByKeyboard(page);
await page.mouse.click(0, 0);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'changeNoteScale');
await waitNextFrame(page);
await scaleInput.click();
await pasteByKeyboard(page);
await page.keyboard.press('Enter');
await checkNoteScale(page, noteId, 0.5);
});
test('Note scale can be changed by shift drag', async ({ page }) => {
const noteId = await setupAndAddNote(page);
await selectNoteInEdgeless(page, noteId);
const edgelessNote = page.locator(
`affine-edgeless-note[data-block-id="${noteId}"]`
);
const noteRect = await edgelessNote.boundingBox();
assertRectExist(noteRect);
await page.mouse.move(
noteRect.x + noteRect.width,
noteRect.y + noteRect.height
);
await page.keyboard.down('Shift');
await page.mouse.down();
await page.mouse.move(
noteRect.x + noteRect.width * 2,
noteRect.y + noteRect.height * 2
);
await page.mouse.up();
// expect style scale to be greater than 1
await checkNoteScale(page, noteId, 1, 'greater');
});
});

View File

@@ -0,0 +1,156 @@
import { expect } from '@playwright/test';
import {
enterPlaygroundRoom,
initEmptyEdgelessState,
initSixParagraphs,
initThreeParagraphs,
selectNoteInEdgeless,
switchEditorMode,
triggerComponentToolbarAction,
} from '../../utils/actions/index.js';
import { assertRectExist, assertRichTexts } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test.describe('note slicer', () => {
test('could enable and disenable note slicer', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await initSixParagraphs(page);
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
// note slicer button should not be visible when note slicer setting is disenabled
await expect(page.locator('.note-slicer-button')).toBeHidden();
await expect(page.locator('.note-slicer-dividing-line')).toHaveCount(0);
await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting');
// note slicer button should be visible when note slicer setting is enabled
await expect(page.locator('.note-slicer-button')).toBeVisible();
await expect(page.locator('.note-slicer-dividing-line')).toHaveCount(5);
});
test('note slicer will add new note', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await initSixParagraphs(page);
await switchEditorMode(page);
await expect(page.locator('affine-edgeless-note')).toHaveCount(1);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting');
await expect(page.locator('.note-slicer-button')).toBeVisible();
await page.locator('.note-slicer-button').click();
await expect(page.locator('affine-edgeless-note')).toHaveCount(2);
});
test('note slicer button should appears at right position', async ({
page,
}) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting');
const blocks = await page
.locator(`[data-block-id="${noteId}"] [data-block-id]`)
.all();
expect(blocks.length).toBe(3);
const firstBlockRect = await blocks[0].boundingBox();
assertRectExist(firstBlockRect);
const secondBlockRect = await blocks[1].boundingBox();
assertRectExist(secondBlockRect);
await page.mouse.move(
secondBlockRect.x + 1,
secondBlockRect.y + secondBlockRect.height / 2
);
let slicerButtonRect = await page
.locator('.note-slicer-button')
.boundingBox();
assertRectExist(slicerButtonRect);
let buttonRectMiddle = slicerButtonRect.y + slicerButtonRect.height / 2;
expect(buttonRectMiddle).toBeGreaterThan(
firstBlockRect.y + firstBlockRect.height
);
expect(buttonRectMiddle).toBeGreaterThan(secondBlockRect.y);
const thirdBlockRect = await blocks[2].boundingBox();
assertRectExist(thirdBlockRect);
await page.mouse.move(
thirdBlockRect.x + 1,
thirdBlockRect.y + thirdBlockRect.height / 2
);
slicerButtonRect = await page.locator('.note-slicer-button').boundingBox();
assertRectExist(slicerButtonRect);
buttonRectMiddle = slicerButtonRect.y + slicerButtonRect.height / 2;
expect(buttonRectMiddle).toBeGreaterThan(
secondBlockRect.y + secondBlockRect.height
);
expect(buttonRectMiddle).toBeLessThan(thirdBlockRect.y);
});
test('note slicer button should appears at right position when editor is not located at left top corner', async ({
page,
}) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
await page.evaluate(() => {
const el = document.createElement('div');
const app = document.querySelector('#app') as HTMLElement;
el.style.height = '100px';
el.style.background = 'red';
app!.style.paddingLeft = '80px';
document.body.insertBefore(el, app);
});
const blocks = await page
.locator(`[data-block-id="${noteId}"] [data-block-id]`)
.all();
expect(blocks.length).toBe(3);
const firstBlockRect = await blocks[0].boundingBox();
assertRectExist(firstBlockRect);
const secondBlockRect = await blocks[1].boundingBox();
assertRectExist(secondBlockRect);
await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting');
await page.mouse.move(
secondBlockRect.x + 1,
secondBlockRect.y + secondBlockRect.height / 2
);
const slicerButtonRect = await page
.locator('.note-slicer-button')
.boundingBox();
assertRectExist(slicerButtonRect);
const buttonRectMiddle = slicerButtonRect.y + slicerButtonRect.height / 2;
expect(buttonRectMiddle).toBeGreaterThan(
firstBlockRect.y + firstBlockRect.height
);
expect(buttonRectMiddle).toBeGreaterThan(secondBlockRect.y);
});
});

View File

@@ -0,0 +1,140 @@
import { expect } from '@playwright/test';
import {
activeNoteInEdgeless,
click,
copyByKeyboard,
countBlock,
dragBetweenCoords,
enterPlaygroundRoom,
fillLine,
focusRichText,
getNoteRect,
initEmptyEdgelessState,
initSixParagraphs,
pasteByKeyboard,
redoByClick,
redoByKeyboard,
selectNoteInEdgeless,
switchEditorMode,
triggerComponentToolbarAction,
type,
undoByClick,
undoByKeyboard,
waitNextFrame,
zoomResetByKeyboard,
} from '../../utils/actions/index.js';
import { assertRectEqual } from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test('undo/redo should work correctly after clipping', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await initSixParagraphs(page);
await switchEditorMode(page);
await expect(page.locator('affine-edgeless-note')).toHaveCount(1);
await selectNoteInEdgeless(page, noteId);
await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting');
const button = page.locator('.note-slicer-button');
await button.click();
await expect(page.locator('affine-edgeless-note')).toHaveCount(2);
await undoByKeyboard(page);
await waitNextFrame(page);
await expect(page.locator('affine-edgeless-note')).toHaveCount(1);
await redoByKeyboard(page);
await waitNextFrame(page);
await expect(page.locator('affine-edgeless-note')).toHaveCount(2);
});
test('undo/redo should work correctly after resizing', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await activeNoteInEdgeless(page, noteId);
await waitNextFrame(page, 400);
// current implementation may be a little inefficient
await fillLine(page, true);
await page.mouse.click(0, 0);
await waitNextFrame(page, 400);
await selectNoteInEdgeless(page, noteId);
const initRect = await getNoteRect(page, noteId);
const rightHandle = page.locator('.handle[aria-label="right"] .resize');
const box = await rightHandle.boundingBox();
if (box === null) throw new Error();
await dragBetweenCoords(
page,
{ x: box.x + 5, y: box.y + 5 },
{ x: box.x + 105, y: box.y + 5 }
);
const draggedRect = await getNoteRect(page, noteId);
assertRectEqual(draggedRect, {
x: initRect.x,
y: initRect.y,
w: initRect.w + 100,
h: draggedRect.h, // not assert `h` here
});
expect(draggedRect.h).toBe(initRect.h);
await undoByKeyboard(page);
await waitNextFrame(page);
const undoRect = await getNoteRect(page, noteId);
assertRectEqual(undoRect, initRect);
await redoByKeyboard(page);
await waitNextFrame(page);
const redoRect = await getNoteRect(page, noteId);
assertRectEqual(redoRect, draggedRect);
});
test('continuous undo and redo (note block add operation) should work', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await switchEditorMode(page);
await click(page, { x: 260, y: 450 });
await copyByKeyboard(page);
let count = await countBlock(page, 'affine-edgeless-note');
expect(count).toBe(1);
await page.mouse.move(100, 100);
await pasteByKeyboard(page, false);
await waitNextFrame(page, 1000);
await page.mouse.move(200, 200);
await pasteByKeyboard(page, false);
await waitNextFrame(page, 1000);
await page.mouse.move(300, 300);
await pasteByKeyboard(page, false);
await waitNextFrame(page, 1000);
count = await countBlock(page, 'affine-edgeless-note');
expect(count).toBe(4);
await undoByClick(page);
count = await countBlock(page, 'affine-edgeless-note');
expect(count).toBe(3);
await undoByClick(page);
count = await countBlock(page, 'affine-edgeless-note');
expect(count).toBe(2);
await redoByClick(page);
count = await countBlock(page, 'affine-edgeless-note');
expect(count).toBe(3);
await redoByClick(page);
count = await countBlock(page, 'affine-edgeless-note');
expect(count).toBe(4);
});

View File

@@ -0,0 +1,263 @@
import { expect, type Locator, type Page } from '@playwright/test';
import {
activeNoteInEdgeless,
addNote,
assertEdgelessTool,
locatorEdgelessToolButton,
multiTouchDown,
multiTouchMove,
multiTouchUp,
setEdgelessTool,
switchEditorMode,
} from '../utils/actions/edgeless.js';
import {
addBasicRectShapeElement,
dragBetweenCoords,
enterPlaygroundRoom,
initEmptyEdgelessState,
toggleEditorReadonly,
type,
waitForInlineEditorStateUpdated,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertEdgelessSelectedRect,
assertNotHasClass,
assertRichTexts,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('pan tool basic', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicRectShapeElement(page, start, end);
await setEdgelessTool(page, 'pan');
await dragBetweenCoords(
page,
{
x: start.x + 5,
y: start.y + 5,
},
{
x: start.x + 25,
y: start.y + 25,
}
);
await setEdgelessTool(page, 'default');
await page.mouse.click(start.x + 25, start.y + 25);
await assertEdgelessSelectedRect(page, [120, 120, 100, 100]);
});
test('pan tool shortcut', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await page.keyboard.down('Space');
await assertEdgelessTool(page, 'pan');
await dragBetweenCoords(
page,
{
x: start.x + 5,
y: start.y + 5,
},
{
x: start.x + 25,
y: start.y + 25,
}
);
await page.keyboard.up('Space');
await assertEdgelessSelectedRect(page, [120, 120, 100, 100]);
});
// FIXME(@doouding): Failed on CI
test.skip('pan tool with middle button', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await dragBetweenCoords(
page,
{
x: 400,
y: 400,
},
{
x: 420,
y: 420,
},
{
button: 'middle',
}
);
await assertEdgelessTool(page, 'default');
await assertEdgelessSelectedRect(page, [120, 120, 100, 100]);
});
test('pan tool shortcut should revert to the previous tool on keyup', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await page.mouse.click(100, 100);
await setEdgelessTool(page, 'brush');
{
await page.keyboard.down('Space');
await assertEdgelessTool(page, 'pan');
await page.keyboard.up('Space');
await assertEdgelessTool(page, 'brush');
}
});
test('pan tool shortcut does not affect other tools while using the tool', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
// Test if while drawing shortcut does not switch to pan tool
await setEdgelessTool(page, 'brush');
await dragBetweenCoords(
page,
{ x: 100, y: 110 },
{ x: 200, y: 300 },
{
click: true,
beforeMouseUp: async () => {
await page.keyboard.down('Space');
await assertEdgelessTool(page, 'brush');
},
}
);
await setEdgelessTool(page, 'eraser');
await dragBetweenCoords(
page,
{ x: 100, y: 110 },
{ x: 200, y: 300 },
{
click: true,
beforeMouseUp: async () => {
await page.keyboard.down('Space');
await assertEdgelessTool(page, 'eraser');
},
}
);
// Maybe add other tools too
});
test('pan tool shortcut when user is editing', async ({ page }) => {
await enterPlaygroundRoom(page);
const ids = await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'default');
await activeNoteInEdgeless(page, ids.noteId);
await waitForInlineEditorStateUpdated(page);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
await page.keyboard.down('Space');
const defaultButton = await locatorEdgelessToolButton(page, 'pan', false);
await assertNotHasClass(defaultButton, 'pan');
await waitNextFrame(page);
});
test.describe('pan tool in readonly mode', () => {
async function setupReadonlyEdgeless(page: Page) {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const noteId = await addNote(page, 'hello world', 100, 200);
await page.mouse.click(50, 100);
const edgelessNote = page.locator(
`affine-edgeless-note[data-block-id="${noteId}"]`
);
const originalBoundingBox = await edgelessNote.boundingBox();
expect(originalBoundingBox).not.toBeNull();
const { x: originalX, y: originalY } = originalBoundingBox!;
// Toggle readonly mode
await toggleEditorReadonly(page);
await page.waitForTimeout(100);
return { edgelessNote, originalX, originalY };
}
async function assertPanned(
edgelessNote: Locator,
originalX: number,
originalY: number
) {
const newBoundingBox = await edgelessNote.boundingBox();
expect(newBoundingBox).not.toBeNull();
const { x: newX, y: newY } = newBoundingBox!;
expect(newX).toBeGreaterThan(originalX);
expect(newY).toBeGreaterThan(originalY);
}
test('can be used by keyboard', async ({ page }) => {
const { edgelessNote, originalX, originalY } =
await setupReadonlyEdgeless(page);
await page.keyboard.down('Space');
await assertEdgelessTool(page, 'pan');
// Pan the viewport
await dragBetweenCoords(page, { x: 300, y: 300 }, { x: 400, y: 400 });
await assertPanned(edgelessNote, originalX, originalY);
});
test('can be used by multi-touch', async ({ page }) => {
const { edgelessNote, originalX, originalY } =
await setupReadonlyEdgeless(page);
// Pan the viewport using multi-touch
const from = [
{ x: 300, y: 300 },
{ x: 400, y: 300 },
];
const to = [
{ x: 350, y: 350 },
{ x: 450, y: 350 },
];
await multiTouchDown(page, from);
await multiTouchMove(page, from, to);
await multiTouchUp(page, to);
await assertPanned(edgelessNote, originalX, originalY);
});
});

View File

@@ -0,0 +1,125 @@
import { expect, type Page } from '@playwright/test';
import {
click,
copyByKeyboard,
enterPlaygroundRoom,
focusRichText,
getAllEdgelessNoteIds,
getAllEdgelessTextIds,
getNoteBoundBoxInEdgeless,
initEmptyEdgelessState,
pasteByKeyboard,
pasteTestImage,
pressEnter,
pressEnterWithShortkey,
pressEscape,
selectAllByKeyboard,
setEdgelessTool,
switchEditorMode,
type,
} from '../utils/actions/index.js';
import { test } from '../utils/playwright.js';
test.describe('pasting blocks', () => {
const initContent = async (page: Page) => {
// Text
await type(page, 'hello');
await pressEnter(page);
// Image
await pasteTestImage(page);
await pressEnter(page);
// Text
await type(page, 'world');
await pressEnter(page);
// code
await type(page, '``` ');
await type(page, 'code');
await pressEnterWithShortkey(page);
};
test('pasting a note block', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await focusRichText(page);
await initContent(page);
await switchEditorMode(page);
await click(page, { x: 0, y: 0 });
const box = await getNoteBoundBoxInEdgeless(page, noteId);
await click(page, {
x: box.x + 10,
y: box.y + 10,
});
await copyByKeyboard(page);
await pasteByKeyboard(page);
// not equal to noteId
const noteIds = await getAllEdgelessNoteIds(page);
expect(noteIds.length).toBe(2);
expect(noteIds[0]).toBe(noteId);
const newNoteId = noteIds[1];
const newNote = page.locator(
`affine-edgeless-note[data-block-id="${newNoteId}"]`
);
await expect(newNote).toBeVisible();
const blocks = newNote.locator('[data-block-id]');
await expect(blocks.nth(0)).toContainText('hello');
await expect(blocks.nth(1).locator('.resizable-img')).toBeVisible();
await expect(blocks.nth(2)).toContainText('world');
await expect(blocks.nth(3)).toContainText('code');
});
test('pasting a edgeless block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140, {
delay: 100,
});
await initContent(page);
await pressEscape(page, 3);
await page.mouse.click(130, 140);
await copyByKeyboard(page);
await page.mouse.move(500, 500);
await pasteByKeyboard(page);
const textIds = await getAllEdgelessTextIds(page);
expect(textIds.length).toBe(2);
const newTextId = textIds[1];
const newText = page.locator(
`affine-edgeless-text[data-block-id="${newTextId}"]`
);
await expect(newText).toBeVisible();
const blocks = newText.locator('[data-block-id]');
await expect(blocks.nth(0)).toContainText('hello');
await expect(blocks.nth(1).locator('.resizable-img')).toBeVisible();
await expect(blocks.nth(2)).toContainText('world');
await expect(blocks.nth(3)).toContainText('code');
});
test('pasting a note block from doc mode', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello world');
await selectAllByKeyboard(page);
await copyByKeyboard(page);
await switchEditorMode(page);
await click(page, {
x: 100,
y: 100,
});
await pasteByKeyboard(page);
// not equal to noteId
const noteIds = await getAllEdgelessNoteIds(page);
expect(noteIds.length).toBe(2);
const newNoteId = noteIds[1];
const newNote = page.locator(
`affine-edgeless-note[data-block-id="${newNoteId}"]`
);
await expect(newNote).toBeVisible();
const blocks = newNote.locator('[data-block-id]');
await expect(blocks.nth(0)).toContainText('hello world');
});
});

View File

@@ -0,0 +1,249 @@
import { expect } from '@playwright/test';
import {
assertEdgelessTool,
createFrame,
createNote,
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup,
enterPresentationMode,
locatorPresentationToolbarButton,
setEdgelessTool,
Shape,
toggleFramePanel,
} from '../utils/actions/edgeless.js';
import {
copyByKeyboard,
pasteByKeyboard,
pressEscape,
selectAllBlocksByKeyboard,
} from '../utils/actions/keyboard.js';
import { waitNextFrame } from '../utils/actions/misc.js';
import { test } from '../utils/playwright.js';
test.describe('presentation', () => {
test('should render note when enter presentation mode', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await createNote(page, [300, 100], 'hello');
// Frame shape
await setEdgelessTool(page, 'frame');
await dragBetweenViewCoords(page, [80, 80], [220, 220]);
await waitNextFrame(page, 100);
// Frame note
await setEdgelessTool(page, 'frame');
await dragBetweenViewCoords(page, [240, 0], [800, 200]);
expect(await page.locator('affine-frame').count()).toBe(2);
await enterPresentationMode(page);
await waitNextFrame(page, 100);
const nextButton = locatorPresentationToolbarButton(page, 'next');
await nextButton.click();
const edgelessNote = page.locator('affine-edgeless-note');
await expect(edgelessNote).toBeVisible();
const prevButton = locatorPresentationToolbarButton(page, 'previous');
await prevButton.click();
await expect(edgelessNote).toBeHidden();
await waitNextFrame(page, 300);
await nextButton.click();
await expect(edgelessNote).toBeVisible();
});
test('should exit presentation mode when press escape', async ({ page }) => {
await edgelessCommonSetup(page);
await createNote(page, [300, 100], 'hello');
// Frame note
await setEdgelessTool(page, 'frame');
await dragBetweenViewCoords(page, [240, 0], [800, 200]);
expect(await page.locator('affine-frame').count()).toBe(1);
await enterPresentationMode(page);
await waitNextFrame(page, 300);
await assertEdgelessTool(page, 'frameNavigator');
const navigatorBlackBackground = page.locator(
'.edgeless-navigator-black-background'
);
await expect(navigatorBlackBackground).toBeVisible();
await pressEscape(page);
await waitNextFrame(page, 100);
await assertEdgelessTool(page, 'default');
await expect(navigatorBlackBackground).toBeHidden();
});
test('should be able to adjust order of presentation in toolbar', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createFrame(page, [100, 100], [100, 200]);
await createFrame(page, [200, 100], [300, 200]);
await createFrame(page, [300, 100], [400, 200]);
await createFrame(page, [400, 100], [500, 200]);
await enterPresentationMode(page);
await page.locator('.edgeless-frame-order-button').click();
const frameItems = page.locator(
'edgeless-frame-order-menu .item.draggable'
);
const dragIndicators = page.locator(
'edgeless-frame-order-menu .drag-indicator'
);
await expect(frameItems).toHaveCount(4);
await expect(frameItems.nth(0)).toHaveText('Frame 1');
await expect(frameItems.nth(1)).toHaveText('Frame 2');
await expect(frameItems.nth(2)).toHaveText('Frame 3');
await expect(frameItems.nth(3)).toHaveText('Frame 4');
// 1 2 3 4
await frameItems.nth(2).dragTo(dragIndicators.nth(0));
// 3 1 2 4
await frameItems.nth(3).dragTo(dragIndicators.nth(2));
// 3 1 4 2
await frameItems.nth(1).dragTo(dragIndicators.nth(3));
// 3 4 1 2
await expect(frameItems).toHaveCount(4);
await expect(frameItems.nth(0)).toHaveText('Frame 3');
await expect(frameItems.nth(1)).toHaveText('Frame 4');
await expect(frameItems.nth(2)).toHaveText('Frame 1');
await expect(frameItems.nth(3)).toHaveText('Frame 2');
const currentFrame = page.locator('.edgeless-frame-navigator-title');
const nextButton = locatorPresentationToolbarButton(page, 'next');
await expect(currentFrame).toHaveText('Frame 3');
await nextButton.click();
await expect(currentFrame).toHaveText('Frame 4');
await nextButton.click();
await expect(currentFrame).toHaveText('Frame 1');
await nextButton.click();
await expect(currentFrame).toHaveText('Frame 2');
});
test('should be able to adjust order of presentation in frame panel', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createFrame(page, [100, 100], [100, 200]);
await createFrame(page, [200, 100], [300, 200]);
await createFrame(page, [300, 100], [400, 200]);
await createFrame(page, [400, 100], [500, 200]);
// await enterPresentationMode(page);
await toggleFramePanel(page);
// await page.locator('.edgeless-frame-order-button').click();
const frameCards = page.locator('affine-frame-card .frame-card-body');
const frameTitles = page.locator('affine-frame-card-title .card-title');
await expect(frameTitles).toHaveCount(4);
await expect(frameTitles.nth(0)).toHaveText('Frame 1');
await expect(frameTitles.nth(1)).toHaveText('Frame 2');
await expect(frameTitles.nth(2)).toHaveText('Frame 3');
await expect(frameTitles.nth(3)).toHaveText('Frame 4');
const drag = async (from: number, to: number) => {
const startBBox = await frameCards.nth(from).boundingBox();
expect(startBBox).not.toBeNull();
if (startBBox === null) return;
const endBBox = await frameTitles.nth(to).boundingBox();
expect(endBBox).not.toBeNull();
if (endBBox === null) return;
await page.mouse.move(
startBBox.x + startBBox.width / 2,
startBBox.y + startBBox.height / 2
);
await page.mouse.down();
await page.mouse.move(endBBox.x + endBBox.width / 2, endBBox.y, {
steps: 2,
});
await page.mouse.up();
};
// 1 2 3 4
await drag(2, 0);
// 3 1 2 4
await drag(3, 2);
// 3 1 4 2
await drag(1, 3);
// 3 4 1 2
await expect(frameTitles).toHaveCount(4);
await expect(frameTitles.nth(0)).toHaveText('Frame 3');
await expect(frameTitles.nth(1)).toHaveText('Frame 4');
await expect(frameTitles.nth(2)).toHaveText('Frame 1');
await expect(frameTitles.nth(3)).toHaveText('Frame 2');
await enterPresentationMode(page);
await page.locator('.edgeless-frame-order-button').click();
const frameItems = page.locator(
'edgeless-frame-order-menu .item.draggable'
);
await expect(frameItems).toHaveCount(4);
await expect(frameItems.nth(0)).toHaveText('Frame 3');
await expect(frameItems.nth(1)).toHaveText('Frame 4');
await expect(frameItems.nth(2)).toHaveText('Frame 1');
await expect(frameItems.nth(3)).toHaveText('Frame 2');
const currentFrame = page.locator('.edgeless-frame-navigator-title');
const nextButton = locatorPresentationToolbarButton(page, 'next');
await expect(currentFrame).toHaveText('Frame 3');
await nextButton.click();
await expect(currentFrame).toHaveText('Frame 4');
await nextButton.click();
await expect(currentFrame).toHaveText('Frame 1');
await nextButton.click();
await expect(currentFrame).toHaveText('Frame 2');
});
test('duplicate frames should keep the presentation orders', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createFrame(page, [100, 100], [100, 200]);
await createFrame(page, [200, 100], [300, 200]);
await createFrame(page, [300, 100], [400, 200]);
await createFrame(page, [400, 100], [500, 200]);
await selectAllBlocksByKeyboard(page);
await copyByKeyboard(page);
await pasteByKeyboard(page);
await enterPresentationMode(page);
await page.locator('.edgeless-frame-order-button').click();
const frameItems = page.locator(
'edgeless-frame-order-menu .item.draggable'
);
await expect(frameItems).toHaveCount(8);
await expect(frameItems.nth(0)).toHaveText('Frame 1');
await expect(frameItems.nth(1)).toHaveText('Frame 2');
await expect(frameItems.nth(2)).toHaveText('Frame 3');
await expect(frameItems.nth(3)).toHaveText('Frame 4');
await expect(frameItems.nth(4)).toHaveText('Frame 1');
await expect(frameItems.nth(5)).toHaveText('Frame 2');
await expect(frameItems.nth(6)).toHaveText('Frame 3');
await expect(frameItems.nth(7)).toHaveText('Frame 4');
});
});

View File

@@ -0,0 +1,448 @@
import { expect, type Page } from '@playwright/test';
import {
createShapeElement,
edgelessCommonSetup,
getFirstContainerId,
getSelectedBound,
getSortedIds,
initThreeOverlapFilledShapes,
initThreeOverlapNotes,
Shape,
shiftClickView,
switchEditorMode,
triggerComponentToolbarAction,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import {
captureHistory,
clickView,
enterPlaygroundRoom,
initEmptyEdgelessState,
redoByKeyboard,
undoByKeyboard,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertEdgelessSelectedRect,
assertSelectedBound,
} from '../utils/asserts.js';
import {
DEFAULT_NOTE_HEIGHT,
DEFAULT_NOTE_WIDTH,
} from '../utils/bs-alternative.js';
import { test } from '../utils/playwright.js';
test.describe('reordering', () => {
test.describe('group index', () => {
let sortedIds: string[];
async function init(page: Page) {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [100, 0], [200, 100], Shape.Square);
await createShapeElement(page, [200, 0], [300, 100], Shape.Square);
await createShapeElement(page, [300, 0], [400, 100], Shape.Square);
sortedIds = await getSortedIds(page);
}
test('group', async ({ page }) => {
await init(page);
await clickView(page, [50, 50]);
await shiftClickView(page, [150, 50]);
await triggerComponentToolbarAction(page, 'addGroup');
const groupId = await getFirstContainerId(page);
const currentSortedIds = await getSortedIds(page);
expect(currentSortedIds).toEqual([
...sortedIds.slice(2),
groupId,
...sortedIds.slice(0, 2),
]);
});
test('release from group', async ({ page }) => {
await init(page);
await clickView(page, [50, 50]);
await shiftClickView(page, [150, 50]);
await triggerComponentToolbarAction(page, 'addGroup');
const groupId = await getFirstContainerId(page);
await clickView(page, [50, 50]);
await triggerComponentToolbarAction(page, 'releaseFromGroup');
const currentSortedIds = await getSortedIds(page);
const releasedShapeId = sortedIds[0];
expect(currentSortedIds).toEqual([
...sortedIds.slice(2),
groupId,
sortedIds[1],
releasedShapeId,
]);
});
test('ungroup', async ({ page }) => {
await init(page);
await clickView(page, [50, 50]);
await shiftClickView(page, [150, 50]);
await triggerComponentToolbarAction(page, 'addGroup');
await triggerComponentToolbarAction(page, 'ungroup');
const currentSortedIds = await getSortedIds(page);
const ungroupedIds = [sortedIds[0], sortedIds[1]];
expect(currentSortedIds).toEqual([
...sortedIds.filter(id => !ungroupedIds.includes(id)),
...ungroupedIds,
]);
});
});
test.describe('reordering shapes', () => {
async function init(page: Page) {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await initThreeOverlapFilledShapes(page);
await page.mouse.click(0, 0);
}
test('bring to front', async ({ page }) => {
await init(page);
// should be rect2
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [160, 160, 100, 100]);
// click outside to clear selection
await page.mouse.click(50, 50);
// should be rect1
await page.mouse.click(150, 150);
await assertEdgelessSelectedRect(page, [130, 130, 100, 100]);
// should be rect0
await page.mouse.click(110, 130);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
// bring rect0 to front
await triggerComponentToolbarAction(page, 'bringToFront');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be rect0
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('bring forward', async ({ page }) => {
await init(page);
// should be rect0
await page.mouse.click(120, 120);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
// bring rect0 forward
await triggerComponentToolbarAction(page, 'bringForward');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be rect0
await page.mouse.click(150, 150);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('send backward', async ({ page }) => {
await init(page);
// should be rect2
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [160, 160, 100, 100]);
// bring rect2 backward
await triggerComponentToolbarAction(page, 'sendBackward');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be rect1
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [130, 130, 100, 100]);
});
test('send to back', async ({ page }) => {
await init(page);
// should be rect2
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [160, 160, 100, 100]);
// bring rect2 to back
await triggerComponentToolbarAction(page, 'sendToBack');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be rect1
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [130, 130, 100, 100]);
// send rect1 to back
await triggerComponentToolbarAction(page, 'sendToBack');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be rect0
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('undo and redo', async ({ page }) => {
await init(page);
// should be rect2
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [160, 160, 100, 100]);
// send rect2 to back
await triggerComponentToolbarAction(page, 'sendToBack');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be rect1
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [130, 130, 100, 100]);
// undo
await undoByKeyboard(page);
// clear selection
await page.mouse.click(50, 50);
// should be rect2
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [160, 160, 100, 100]);
// redo
await redoByKeyboard(page);
// clear selection
await page.mouse.click(50, 50);
// should be rect2
await page.mouse.click(180, 180);
await assertEdgelessSelectedRect(page, [130, 130, 100, 100]);
});
});
test.describe('reordering notes', () => {
async function init(page: Page) {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await initThreeOverlapNotes(page);
await waitNextFrame(page);
await page.mouse.click(0, 0);
}
test('bring to front', async ({ page }) => {
await edgelessCommonSetup(page);
await zoomResetByKeyboard(page);
await initThreeOverlapNotes(page, 130, 190);
await waitNextFrame(page);
// click outside to clear selection
await page.mouse.click(50, 100);
// should be note2
await page.mouse.click(180, 200);
const bound = await getSelectedBound(page);
await assertSelectedBound(page, bound);
await clickView(page, [bound[0] - 15, bound[1] + 10]);
bound[0] -= 30;
await assertSelectedBound(page, bound);
await clickView(page, [bound[0] - 15, bound[1] + 10]);
bound[0] -= 30;
await assertSelectedBound(page, bound);
// bring note0 to front
await triggerComponentToolbarAction(page, 'bringToFront');
// clear
await page.mouse.click(100, 50);
// should be note0
await clickView(page, [bound[0] + 40, bound[1] + 10]);
await assertSelectedBound(page, bound);
});
test('bring forward', async ({ page }) => {
await init(page);
// click outside to clear selection
await page.mouse.click(50, 50);
// should be note0
await page.mouse.click(120, 140);
await assertEdgelessSelectedRect(page, [
100,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
// bring note0 forward
await triggerComponentToolbarAction(page, 'bringForward');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be rect0
await page.mouse.click(150, 140);
await assertEdgelessSelectedRect(page, [
100,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
});
test('send backward', async ({ page }) => {
await init(page);
// click outside to clear selection
await page.mouse.click(50, 50);
// should be note2
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
160,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
// bring note2 backward
await triggerComponentToolbarAction(page, 'sendBackward');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be note1
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
130,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
});
test('send to back', async ({ page }) => {
await init(page);
// click outside to clear selection
await page.mouse.click(50, 50);
// should be note2
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
160,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
// bring note2 to back
await triggerComponentToolbarAction(page, 'sendToBack');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be note1
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
130,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
// send note1 to back
await triggerComponentToolbarAction(page, 'sendToBack');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be note0
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
100,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
});
test('undo and redo', async ({ page }) => {
await init(page);
// click outside to clear selection
await page.mouse.click(50, 50);
// should be note2
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
160,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
await captureHistory(page);
// bring note2 to back
await triggerComponentToolbarAction(page, 'sendToBack');
// click outside to clear selection
await page.mouse.click(50, 50);
// should be note1
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
130,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
// undo
await undoByKeyboard(page);
// clear selection
await page.mouse.click(50, 50);
// should be note2
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
160,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
// redo
await redoByKeyboard(page);
// clear selection
await page.mouse.click(50, 50);
// should be note1
await page.mouse.click(180, 140);
await assertEdgelessSelectedRect(page, [
130,
100,
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT,
]);
});
});
});

View File

@@ -0,0 +1,199 @@
import {
switchEditorMode,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import {
addBasicBrushElement,
addBasicRectShapeElement,
dragBetweenCoords,
enterPlaygroundRoom,
initEmptyEdgelessState,
resizeElementByHandle,
} from '../utils/actions/index.js';
import {
assertEdgelessSelectedReactCursor,
assertEdgelessSelectedRect,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.describe('resizing shapes and aspect ratio will be maintained', () => {
test('positive adjustment', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await page.mouse.click(110, 110);
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
await addBasicRectShapeElement(
page,
{ x: 210, y: 110 },
{ x: 310, y: 210 }
);
await page.mouse.click(220, 120);
await assertEdgelessSelectedRect(page, [210, 110, 100, 100]);
await dragBetweenCoords(page, { x: 120, y: 90 }, { x: 220, y: 130 });
await assertEdgelessSelectedRect(page, [98, 98, 212, 112]);
await resizeElementByHandle(page, { x: 50, y: 50 });
await assertEdgelessSelectedRect(page, [148, 124.19, 162, 85.81]);
await page.mouse.move(160, 160);
await assertEdgelessSelectedRect(page, [148, 124.19, 162, 85.81]);
await page.mouse.move(260, 160);
await assertEdgelessSelectedRect(page, [148, 124.19, 162, 85.81]);
});
test('negative adjustment', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await page.mouse.click(110, 110);
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
await addBasicRectShapeElement(
page,
{ x: 210, y: 110 },
{ x: 310, y: 210 }
);
await page.mouse.click(220, 120);
await assertEdgelessSelectedRect(page, [210, 110, 100, 100]);
await dragBetweenCoords(page, { x: 120, y: 90 }, { x: 220, y: 130 });
await assertEdgelessSelectedRect(page, [98, 98, 212, 112]);
await resizeElementByHandle(page, { x: 400, y: 300 }, 'top-left', 30);
await assertEdgelessSelectedRect(page, [310, 210, 356, 188]);
await page.mouse.move(450, 300);
await assertEdgelessSelectedRect(page, [310, 210, 356, 188]);
await page.mouse.move(320, 220);
await assertEdgelessSelectedRect(page, [310, 210, 356, 188]);
});
});
test.describe('cursor style', () => {
test('editor is aligned at the start of viewport', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await addBasicRectShapeElement(
page,
{ x: 200, y: 200 },
{ x: 300, y: 300 }
);
await page.mouse.click(250, 250);
await assertEdgelessSelectedRect(page, [200, 200, 100, 100]);
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top',
cursor: 'ns-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'right',
cursor: 'ew-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom',
cursor: 'ns-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'left',
cursor: 'ew-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top-left',
cursor: 'nwse-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top-right',
cursor: 'nesw-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom-left',
cursor: 'nesw-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom-right',
cursor: 'nwse-resize',
});
});
test('editor is not aligned at the start of viewport', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await page.addStyleTag({
content: 'body { padding: 100px 150px; }',
});
await addBasicRectShapeElement(
page,
{ x: 200, y: 200 },
{ x: 300, y: 300 }
);
await page.mouse.click(250, 250);
await assertEdgelessSelectedRect(page, [200, 200, 100, 100]);
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top',
cursor: 'ns-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'right',
cursor: 'ew-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom',
cursor: 'ns-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'left',
cursor: 'ew-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top-left',
cursor: 'nwse-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top-right',
cursor: 'nesw-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom-left',
cursor: 'nesw-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom-right',
cursor: 'nwse-resize',
});
});
});

View File

@@ -0,0 +1,227 @@
import {
addBasicRectShapeElement,
dragBetweenCoords,
enterPlaygroundRoom,
initEmptyEdgelessState,
resizeElementByHandle,
rotateElementByHandle,
switchEditorMode,
} from '../utils/actions/index.js';
import {
assertEdgelessSelectedReactCursor,
assertEdgelessSelectedRect,
assertEdgelessSelectedRectRotation,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.describe('rotation', () => {
test('angle adjustment by four corners', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await rotateElementByHandle(page, 45, 'top-left');
await assertEdgelessSelectedRectRotation(page, 45);
await rotateElementByHandle(page, 45, 'top-right');
await assertEdgelessSelectedRectRotation(page, 90);
await rotateElementByHandle(page, 45, 'bottom-right');
await assertEdgelessSelectedRectRotation(page, 135);
await rotateElementByHandle(page, 45, 'bottom-left');
await assertEdgelessSelectedRectRotation(page, 180);
});
test('angle snap', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await page.keyboard.down('Shift');
await rotateElementByHandle(page, 5);
await assertEdgelessSelectedRectRotation(page, 0);
await rotateElementByHandle(page, 10);
await assertEdgelessSelectedRectRotation(page, 15);
await rotateElementByHandle(page, 10);
await assertEdgelessSelectedRectRotation(page, 30);
await rotateElementByHandle(page, 10);
await assertEdgelessSelectedRectRotation(page, 45);
await rotateElementByHandle(page, 5);
await assertEdgelessSelectedRectRotation(page, 45);
await page.keyboard.up('Shift');
});
test('single shape', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await rotateElementByHandle(page, 45, 'top-right');
await assertEdgelessSelectedRectRotation(page, 45);
});
test('multiple shapes', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await addBasicRectShapeElement(
page,
{ x: 200, y: 100 },
{ x: 300, y: 200 }
);
await dragBetweenCoords(page, { x: 90, y: 90 }, { x: 310, y: 110 });
await assertEdgelessSelectedRect(page, [100, 100, 200, 100]);
await rotateElementByHandle(page, 90, 'bottom-right');
await assertEdgelessSelectedRectRotation(page, 0);
await assertEdgelessSelectedRect(page, [150, 50, 100, 200]);
});
test('combination with resizing', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await rotateElementByHandle(page, 90, 'bottom-left');
await assertEdgelessSelectedRectRotation(page, 90);
await resizeElementByHandle(page, { x: 10, y: -10 }, 'bottom-right');
await assertEdgelessSelectedRect(page, [110, 100, 90, 90]);
await rotateElementByHandle(page, -90, 'bottom-right');
await assertEdgelessSelectedRectRotation(page, 0);
await resizeElementByHandle(page, { x: 10, y: 10 }, 'bottom-right');
await assertEdgelessSelectedRect(page, [110, 100, 100, 100]);
});
test('combination with resizing for multiple shapes', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await addBasicRectShapeElement(
page,
{ x: 200, y: 100 },
{ x: 300, y: 200 }
);
await dragBetweenCoords(page, { x: 90, y: 90 }, { x: 310, y: 110 });
await assertEdgelessSelectedRect(page, [100, 100, 200, 100]);
await rotateElementByHandle(page, 90, 'bottom-left');
await assertEdgelessSelectedRectRotation(page, 0);
await assertEdgelessSelectedRect(page, [150, 50, 100, 200]);
await resizeElementByHandle(page, { x: -10, y: -20 }, 'bottom-right');
await assertEdgelessSelectedRect(page, [150, 50, 90, 180]);
await rotateElementByHandle(page, -90, 'bottom-right');
await assertEdgelessSelectedRectRotation(page, 0);
await assertEdgelessSelectedRect(page, [105, 95, 180, 90]);
await resizeElementByHandle(page, { x: 20, y: 10 }, 'bottom-right');
await assertEdgelessSelectedRect(page, [105, 95, 200, 100]);
});
});
test.describe('cursor style', () => {
test('update resize cursor direction after rotating', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await rotateElementByHandle(page, 45, 'top-left');
await assertEdgelessSelectedRectRotation(page, 45);
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top',
cursor: 'nesw-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'right',
cursor: 'nwse-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom',
cursor: 'nesw-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'left',
cursor: 'nwse-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top-right',
cursor: 'ew-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'top-left',
cursor: 'ns-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom-right',
cursor: 'ns-resize',
});
await assertEdgelessSelectedReactCursor(page, {
mode: 'resize',
handle: 'bottom-left',
cursor: 'ew-resize',
});
});
});

View File

@@ -0,0 +1,94 @@
import { expect } from '@playwright/test';
import * as actions from '../../utils/actions/edgeless.js';
import {
addBasicConnectorElement,
createConnectorElement,
createShapeElement,
dragBetweenCoords,
enterPlaygroundRoom,
initEmptyEdgelessState,
Shape,
switchEditorMode,
toModelCoord,
waitNextFrame,
} from '../../utils/actions/index.js';
import { test } from '../../utils/playwright.js';
test.describe('select multiple connectors', () => {
test('should show single selection rect', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await addBasicConnectorElement(
page,
{ x: 100, y: 200 },
{ x: 300, y: 200 }
);
await addBasicConnectorElement(
page,
{ x: 100, y: 230 },
{ x: 300, y: 230 }
);
await addBasicConnectorElement(
page,
{ x: 100, y: 260 },
{ x: 300, y: 260 }
);
await dragBetweenCoords(page, { x: 50, y: 50 }, { x: 400, y: 290 });
await waitNextFrame(page);
expect(
await page
.locator('.affine-edgeless-selected-rect')
.locator('.element-handle')
.count()
).toBe(0);
});
test('should disable resize when a connector is already connected', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
const start = await toModelCoord(page, [100, 0]);
const end = await toModelCoord(page, [200, 100]);
await createShapeElement(page, start, end, Shape.Diamond);
const c1 = await toModelCoord(page, [200, 50]);
const c2 = await toModelCoord(page, [450, 50]);
await createConnectorElement(page, c1, c2);
await addBasicConnectorElement(
page,
{ x: 250, y: 200 },
{ x: 450, y: 200 }
);
await addBasicConnectorElement(
page,
{ x: 250, y: 230 },
{ x: 450, y: 230 }
);
await addBasicConnectorElement(
page,
{ x: 250, y: 260 },
{ x: 450, y: 260 }
);
await dragBetweenCoords(page, { x: 500, y: 20 }, { x: 400, y: 290 });
await waitNextFrame(page);
const selectedRectLocalor = page.locator('.affine-edgeless-selected-rect');
expect(await selectedRectLocalor.locator('.element-handle').count()).toBe(
0
);
expect(
await selectedRectLocalor.locator('.handle').locator('.resize').count()
).toBe(0);
});
});

View File

@@ -0,0 +1,265 @@
import { expect } from '@playwright/test';
import * as actions from '../../utils/actions/edgeless.js';
import {
addNote,
changeNoteDisplayModeWithId,
setEdgelessTool,
zoomResetByKeyboard,
} from '../../utils/actions/edgeless.js';
import {
addBasicBrushElement,
addBasicRectShapeElement,
dragBetweenCoords,
enterPlaygroundRoom,
initEmptyEdgelessState,
selectAllByKeyboard,
switchEditorMode,
} from '../../utils/actions/index.js';
import {
assertEdgelessDraggingArea,
assertEdgelessNonSelectedRect,
assertEdgelessSelectedElementHandleCount,
assertEdgelessSelectedRect,
assertVisibleBlockCount,
} from '../../utils/asserts.js';
import { NoteDisplayMode } from '../../utils/bs-alternative.js';
import { test } from '../../utils/playwright.js';
test.describe('translation should constrain to cur axis when dragged with shift key', () => {
test('constrain-x', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await page.mouse.move(110, 110);
await page.mouse.down();
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await page.keyboard.down('Shift');
await page.mouse.move(110, 200); // constrain to y
await page.mouse.move(300, 200); // constrain to x
await assertEdgelessSelectedRect(page, [290, 100, 100, 100]); // y should remain same as constrained to x
});
test('constrain-y', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await page.mouse.move(110, 110);
await page.mouse.down();
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await page.keyboard.down('Shift');
await page.mouse.move(200, 110); // constrain to x
await page.mouse.move(200, 300); // constrain to y
await assertEdgelessSelectedRect(page, [100, 290, 100, 100]); // x should remain same as constrained to y
});
});
test('select multiple shapes and press "Escape" to cancel selection', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await page.mouse.click(110, 110);
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
await addBasicRectShapeElement(page, { x: 210, y: 110 }, { x: 310, y: 210 });
await page.mouse.click(220, 120);
await assertEdgelessSelectedRect(page, [210, 110, 100, 100]);
// Select both shapes
await dragBetweenCoords(page, { x: 90, y: 90 }, { x: 320, y: 220 });
// assert all shapes are selected
await assertEdgelessSelectedRect(page, [98, 98, 212, 112]);
// Press "Escape" to cancel the selection
await page.keyboard.press('Escape');
await assertEdgelessNonSelectedRect(page);
});
test('should move selection drag area when holding spaceBar', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await setEdgelessTool(page, 'default');
// Click to start the initial dragging area
await page.mouse.click(100, 100);
const initialX = 100,
initialY = 100;
const finalX = 300,
finalY = 300;
await dragBetweenCoords(
page,
{ x: initialX, y: initialY },
{ x: finalX, y: finalY },
{
beforeMouseUp: async () => {
await page.keyboard.down('Space');
const dx = 100,
dy = 100;
await page.mouse.move(finalX + dx, finalY + dy);
await assertEdgelessDraggingArea(page, [
initialX + dx,
initialY + dy,
// width and height should be same
finalX - initialX,
finalY - initialY,
]);
await page.keyboard.up('Space');
},
}
);
});
test('selection drag-area start should be same when space is pressed again', async ({
page,
}) => {
//? This test is to check whether there is any flicker or jump when using the space again in the same selection
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
// Make the selection out side the rect and move the selection to the rect
await dragBetweenCoords(
page,
// Make the selection not selecting the rect
{ x: 100, y: 100 },
{ x: 200, y: 200 },
{
beforeMouseUp: async () => {
await page.keyboard.down('Space');
// Move the selection over to the rect
await page.mouse.move(300, 300);
let draggingArea = page.locator('.affine-edgeless-dragging-area');
const firstBound = await draggingArea.boundingBox();
await page.keyboard.up('Space');
await page.mouse.move(400, 400);
await page.keyboard.down('Space');
await page.mouse.move(410, 410);
await page.mouse.move(400, 400);
draggingArea = page.locator('.affine-edgeless-dragging-area');
const newBound = await draggingArea.boundingBox();
expect(firstBound).not.toBe(null);
expect(newBound).not.toBe(null);
const { x: fx, y: fy } = firstBound!;
const { x: nx, y: ny } = newBound!;
expect([fx, fy]).toStrictEqual([nx, ny]);
},
}
);
});
test('should be able to update selection dragging area after releasing space', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await setEdgelessTool(page, 'default');
// Click to start the initial dragging area
await page.mouse.click(100, 100);
const initialX = 100,
initialY = 100;
const finalX = 300,
finalY = 300;
await dragBetweenCoords(
page,
{ x: initialX, y: initialY },
{ x: finalX, y: finalY },
{
beforeMouseUp: async () => {
await page.keyboard.down('Space');
const dx = 100,
dy = 100;
// Move the mouse to simulate dragging with spaceBar held
await page.mouse.move(finalX + dx, finalY + dy);
await page.keyboard.up('Space');
// scale after moving
const dSx = 100;
const dSy = 100;
await page.mouse.move(finalX + dx + dSx, finalY + dy + dSy);
await assertEdgelessDraggingArea(page, [
initialX + dx,
initialY + dy,
// In the second scale it should scale by dS(.)
finalX - initialX + dSx,
finalY - initialY + dSy,
]);
},
}
);
});
test('cmd+a should not select doc only note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
const note2 = await addNote(page, 'note2', 100, 200);
await addNote(page, 'note3', 200, 300);
await page.mouse.click(200, 500);
// assert add note success, there should be 2 notes in edgeless page
await assertVisibleBlockCount(page, 'edgeless-note', 3);
// change note display mode to doc only
await changeNoteDisplayModeWithId(page, note2, NoteDisplayMode.DocOnly);
// there should still be 2 notes in edgeless page
await assertVisibleBlockCount(page, 'edgeless-note', 2);
// cmd+a should not select doc only note
await selectAllByKeyboard(page);
// there should be only 2 notes in selection
await assertEdgelessSelectedElementHandleCount(page, 2);
});

View File

@@ -0,0 +1,466 @@
import { expect } from '@playwright/test';
import * as actions from '../../utils/actions/edgeless.js';
import {
getNoteBoundBoxInEdgeless,
setEdgelessTool,
switchEditorMode,
} from '../../utils/actions/edgeless.js';
import {
addBasicBrushElement,
addBasicRectShapeElement,
click,
clickInCenter,
dragBetweenCoords,
enterPlaygroundRoom,
getBoundingRect,
initEmptyEdgelessState,
initThreeParagraphs,
pressEnter,
waitNextFrame,
} from '../../utils/actions/index.js';
import {
assertBlockCount,
assertEdgelessRemoteSelectedModelRect,
assertEdgelessRemoteSelectedRect,
assertEdgelessSelectedModelRect,
assertEdgelessSelectedRect,
assertSelectionInNote,
} from '../../utils/asserts.js';
import { test } from '../../utils/playwright.js';
test('should update rect of selection when resizing viewport', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await actions.switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await dragBetweenCoords(page, { x: 120, y: 90 }, { x: 220, y: 130 });
const selectedRectClass = '.affine-edgeless-selected-rect';
await actions.zoomResetByKeyboard(page);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await actions.decreaseZoomLevel(page);
await waitNextFrame(page);
await actions.decreaseZoomLevel(page);
await waitNextFrame(page);
const selectedRectInZoom = await getBoundingRect(page, selectedRectClass);
await assertEdgelessSelectedRect(page, [
selectedRectInZoom.x,
selectedRectInZoom.y,
50,
50,
]);
await actions.switchEditorEmbedMode(page);
await waitNextFrame(page);
const selectedRectInEmbed = await getBoundingRect(page, selectedRectClass);
await assertEdgelessSelectedRect(page, [
selectedRectInEmbed.x,
selectedRectInEmbed.y,
50,
50,
]);
await actions.switchEditorEmbedMode(page);
await actions.increaseZoomLevel(page);
await waitNextFrame(page);
await actions.increaseZoomLevel(page);
await waitNextFrame(page);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('should update react of remote selection when resizing viewport', async ({
context,
page: pageA,
}) => {
const room = await enterPlaygroundRoom(pageA);
await initEmptyEdgelessState(pageA);
await actions.switchEditorMode(pageA);
await actions.zoomResetByKeyboard(pageA);
const pageB = await context.newPage();
await enterPlaygroundRoom(pageB, {
room,
noInit: true,
});
await actions.switchEditorMode(pageB);
await actions.zoomResetByKeyboard(pageB);
await actions.createShapeElement(
pageA,
[0, 0],
[100, 100],
actions.Shape.Square
);
const point = await actions.toViewCoord(pageA, [50, 50]);
await click(pageA, { x: point[0], y: point[1] });
await click(pageB, { x: point[0], y: point[1] });
await assertEdgelessSelectedModelRect(pageB, [0, 0, 100, 100]);
await assertEdgelessRemoteSelectedModelRect(pageB, [0, 0, 100, 100]);
// to 50%
await actions.decreaseZoomLevel(pageB);
await waitNextFrame(pageB);
await actions.decreaseZoomLevel(pageB);
await waitNextFrame(pageB);
const selectedRectInZoom = await getBoundingRect(
pageB,
'.affine-edgeless-selected-rect'
);
await assertEdgelessRemoteSelectedRect(pageB, [
selectedRectInZoom.x,
selectedRectInZoom.y,
50,
50,
]);
});
test('select multiple shapes and translate', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await page.mouse.click(110, 110);
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
await addBasicRectShapeElement(page, { x: 210, y: 110 }, { x: 310, y: 210 });
await page.mouse.click(220, 120);
await assertEdgelessSelectedRect(page, [210, 110, 100, 100]);
await dragBetweenCoords(page, { x: 120, y: 90 }, { x: 220, y: 130 });
await assertEdgelessSelectedRect(page, [98, 98, 212, 112]);
await dragBetweenCoords(page, { x: 120, y: 120 }, { x: 150, y: 150 });
await assertEdgelessSelectedRect(page, [125, 128, 212, 112]);
await page.mouse.click(160, 160);
await assertEdgelessSelectedRect(page, [125, 128, 104, 104]);
await page.mouse.click(250, 150);
await assertEdgelessSelectedRect(page, [237, 140, 100, 100]);
});
test('selection box of shape element sync on fast dragging', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await setEdgelessTool(page, 'shape');
await dragBetweenCoords(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await setEdgelessTool(page, 'default');
await dragBetweenCoords(
page,
{ x: 110, y: 110 },
{ x: 660, y: 460 },
{ click: true }
);
await assertEdgelessSelectedRect(page, [650, 446, 100, 100]);
});
test('when the selection is always a note, it should remain in an active state', async ({
page,
}) => {
await enterPlaygroundRoom(page);
const ids = await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
const bound = await getNoteBoundBoxInEdgeless(page, ids.noteId);
await setEdgelessTool(page, 'note');
const newNoteX = bound.x;
const newNoteY = bound.y + bound.height + 100;
// add text
await page.mouse.click(newNoteX, newNoteY);
await waitNextFrame(page);
await page.keyboard.type('hello');
await pressEnter(page);
// should wait for inline editor update and resizeObserver callback
await waitNextFrame(page);
// assert add text success
await assertBlockCount(page, 'edgeless-note', 2);
await clickInCenter(page, bound);
await clickInCenter(page, bound);
await waitNextFrame(page);
await assertSelectionInNote(page, ids.noteId, 'affine-edgeless-note');
});
test('should auto panning when selection rectangle reaches viewport edges', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await addBasicRectShapeElement(page, { x: 200, y: 100 }, { x: 300, y: 200 });
await page.mouse.click(210, 110);
await assertEdgelessSelectedRect(page, [200, 100, 100, 100]);
const selectedRectClass = '.affine-edgeless-selected-rect';
// Panning to the left
await setEdgelessTool(page, 'pan');
await dragBetweenCoords(
page,
{
x: 600,
y: 200,
},
{
x: 200,
y: 200,
}
);
await setEdgelessTool(page, 'default');
await page.mouse.click(210, 110);
let selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(300);
await expect(selectedRect).toBeHidden();
// Click to start selection and hold the mouse to trigger auto panning to the left
await page.mouse.move(210, 110);
await page.mouse.down();
await page.mouse.move(0, 210, { steps: 20 });
await page.waitForTimeout(500);
await page.mouse.up();
// Expect to select the shape element
selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(300);
await expect(selectedRect).toBeVisible();
// Panning to the top
await page.mouse.click(400, 600);
await setEdgelessTool(page, 'pan');
await dragBetweenCoords(
page,
{
x: 400,
y: 600,
},
{
x: 400,
y: 100,
}
);
await setEdgelessTool(page, 'default');
await page.mouse.click(600, 100);
selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(300);
await expect(selectedRect).toBeHidden();
// Click to start selection and hold the mouse to trigger auto panning to the top
await page.mouse.move(600, 100);
await page.mouse.down();
await page.mouse.move(400, 0, { steps: 20 });
await page.waitForTimeout(500);
await page.mouse.up();
// Expect to select the empty note
selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(300);
await expect(selectedRect).toBeVisible();
// Panning to the right
await page.mouse.click(100, 600);
await setEdgelessTool(page, 'pan');
await dragBetweenCoords(
page,
{
x: 20,
y: 600,
},
{
x: 1000,
y: 600,
}
);
await setEdgelessTool(page, 'default');
await page.mouse.click(800, 600);
selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(100);
await expect(selectedRect).toBeHidden();
// Click to start selection and hold the mouse to trigger auto panning to the right
await dragBetweenCoords(
page,
{
x: 800,
y: 600,
},
{
x: 1000,
y: 200,
},
{
beforeMouseUp: async () => {
await page.waitForTimeout(600);
},
}
);
// Expect to select the empty note
selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(300);
await expect(selectedRect).toBeVisible();
// Panning to the bottom
await page.mouse.click(400, 100);
await setEdgelessTool(page, 'pan');
await dragBetweenCoords(
page,
{
x: 400,
y: 100,
},
{
x: 400,
y: 850,
},
{
click: true,
}
);
await setEdgelessTool(page, 'default');
await waitNextFrame(page, 500);
await page.mouse.click(400, 400);
selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(100);
await expect(selectedRect).toBeHidden();
// Click to start selection and hold the mouse to trigger auto panning to the right
await dragBetweenCoords(
page,
{
x: 800,
y: 300,
},
{
x: 820,
y: 1150,
},
{
click: true,
beforeMouseUp: async () => {
await page.waitForTimeout(500);
},
}
);
// Expect to select the empty note
selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(300);
await expect(selectedRect).toBeVisible();
});
test('should also update dragging area when viewport changes', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
// Panning to the top
await page.mouse.click(400, 600);
await setEdgelessTool(page, 'pan');
await dragBetweenCoords(
page,
{
x: 400,
y: 600,
},
{
x: 400,
y: 100,
}
);
await setEdgelessTool(page, 'default');
await page.mouse.click(200, 300);
const selectedRectClass = '.affine-edgeless-selected-rect';
let selectedRect = page.locator(selectedRectClass);
await expect(selectedRect).toBeHidden();
// set up initial dragging area
await page.mouse.move(200, 300);
await page.mouse.down();
await page.mouse.move(600, 200, { steps: 20 });
await page.waitForTimeout(300);
// wheel the viewport to the top
await page.mouse.wheel(0, -300);
await page.waitForTimeout(300);
await page.mouse.up();
// Expect to select the empty note
selectedRect = page.locator(selectedRectClass);
await page.waitForTimeout(300);
await expect(selectedRect).toBeVisible();
await page.waitForTimeout(300);
});
test('should select shapes while moving selection', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await actions.zoomResetByKeyboard(page);
await addBasicRectShapeElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
// Make the selection out side the rect and move the selection to the rect
await dragBetweenCoords(
page,
// Make the selection not selecting the rect
{ x: 70, y: 70 },
{ x: 90, y: 90 },
{
beforeMouseUp: async () => {
await page.keyboard.down('Space');
// Move the selection over to the rect
await page.mouse.move(120, 120);
await page.keyboard.up('Space');
},
}
);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await addBasicBrushElement(page, { x: 210, y: 100 }, { x: 310, y: 300 });
await page.mouse.click(211, 101);
// Make a wide selection and move it to select both of the shapes
await dragBetweenCoords(
page,
// Make the selection above the spaces
{ x: 70, y: 70 },
{ x: 400, y: 90 },
{
beforeMouseUp: async () => {
await page.keyboard.down('Space');
// Move the selection over both of the shapes
await page.mouse.move(400, 120);
await page.keyboard.up('Space');
},
}
);
await assertEdgelessSelectedRect(page, [100, 98, 212, 204]);
});

View File

@@ -0,0 +1,739 @@
import { expect, type Page } from '@playwright/test';
import { lightThemeV2 } from '@toeverything/theme/v2';
import {
assertEdgelessTool,
changeShapeFillColor,
changeShapeFillColorToTransparent,
changeShapeStrokeColor,
changeShapeStrokeStyle,
changeShapeStrokeWidth,
changeShapeStyle,
clickComponentToolbarMoreMenuButton,
getEdgelessSelectedRect,
locatorComponentToolbar,
locatorEdgelessToolButton,
locatorShapeStrokeStyleButton,
openComponentToolbarMoreMenu,
pickColorAtPoints,
resizeElementByHandle,
setEdgelessTool,
switchEditorMode,
triggerComponentToolbarAction,
zoomResetByKeyboard,
} from '../utils/actions/edgeless.js';
import {
addBasicBrushElement,
addBasicRectShapeElement,
copyByKeyboard,
dragBetweenCoords,
enterPlaygroundRoom,
focusRichText,
initEmptyEdgelessState,
pasteByKeyboard,
pressEscape,
type,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertEdgelessCanvasText,
assertEdgelessColorSameWithHexColor,
assertEdgelessNonSelectedRect,
assertEdgelessSelectedRect,
assertExists,
assertRichTexts,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.describe('add shape', () => {
test('without holding shift key', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicRectShapeElement(page, start0, end0);
await assertEdgelessTool(page, 'default');
await assertEdgelessSelectedRect(page, [100, 100, 50, 100]);
const start1 = { x: 100, y: 100 };
const end1 = { x: 200, y: 150 };
await addBasicRectShapeElement(page, start1, end1);
await assertEdgelessTool(page, 'default');
await assertEdgelessSelectedRect(page, [100, 100, 100, 50]);
});
test('with holding shift key', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await page.keyboard.down('Shift');
const start0 = { x: 100, y: 100 };
const end0 = { x: 150, y: 200 };
await addBasicRectShapeElement(page, start0, end0);
await page.keyboard.up('Shift');
await assertEdgelessTool(page, 'default');
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await page.keyboard.down('Shift');
const start1 = { x: 100, y: 100 };
const end1 = { x: 200, y: 150 };
await addBasicRectShapeElement(page, start1, end1);
await assertEdgelessTool(page, 'default');
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('with holding space bar', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 200, y: 200 };
await setEdgelessTool(page, 'shape');
await dragBetweenCoords(page, start0, end0, {
steps: 50,
beforeMouseUp: async () => {
// move the shape
await page.keyboard.down('Space');
await page.mouse.move(300, 300);
await page.keyboard.up('Space');
await page.mouse.move(500, 600);
},
});
await assertEdgelessSelectedRect(page, [200, 200, 300, 400]);
});
test('with holding space bar + shift', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start0 = { x: 100, y: 100 };
const end0 = { x: 200, y: 200 };
await setEdgelessTool(page, 'shape');
await page.keyboard.down('Shift');
await dragBetweenCoords(page, start0, end0, {
steps: 50,
beforeMouseUp: async () => {
// move the shape
await page.keyboard.down('Space');
await page.mouse.move(300, 300);
await page.keyboard.up('Space');
await page.mouse.move(500, 600);
},
});
await assertEdgelessSelectedRect(page, [200, 200, 400, 400]);
});
});
test('delete shape by component-toolbar', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicBrushElement(page, start, end);
await page.mouse.click(110, 110);
await openComponentToolbarMoreMenu(page);
await clickComponentToolbarMoreMenuButton(page, 'delete');
await assertEdgelessNonSelectedRect(page);
});
//FIXME: need a way to test hand-drawn-like style
test.skip('change shape fill color', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const rect = {
start: { x: 100, y: 100 },
end: { x: 200, y: 200 },
};
await addBasicRectShapeElement(page, rect.start, rect.end);
await page.mouse.click(rect.start.x + 5, rect.start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
await changeShapeFillColor(page, 'MediumGrey');
await page.waitForTimeout(50);
const [picked] = await pickColorAtPoints(page, [
[rect.start.x + 20, rect.start.y + 20],
]);
await assertEdgelessColorSameWithHexColor(
page,
lightThemeV2['edgeless/palette/medium/greyMedium'],
picked
);
});
test('change shape stroke color', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const rect = {
start: { x: 100, y: 100 },
end: { x: 200, y: 200 },
};
await addBasicRectShapeElement(page, rect.start, rect.end);
await page.mouse.click(rect.start.x + 5, rect.start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
await changeShapeStrokeColor(page, 'HeavyYellow');
await page.waitForTimeout(50);
const [picked] = await pickColorAtPoints(page, [
[rect.start.x + 1, rect.start.y + 1],
]);
await assertEdgelessColorSameWithHexColor(
page,
lightThemeV2['edgeless/palette/heavy/yellow'],
picked
);
});
test('the tooltip of shape tool button should be hidden when the shape menu is shown', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const shapeTool = await locatorEdgelessToolButton(page, 'shape');
const shapeToolBox = await shapeTool.boundingBox();
const tooltip = page.locator('.affine-tooltip');
assertExists(shapeToolBox);
await page.mouse.move(shapeToolBox.x + 2, shapeToolBox.y + 2);
await expect(tooltip).toBeVisible();
await page.mouse.click(shapeToolBox.x + 2, shapeToolBox.y + 2);
await expect(tooltip).toBeHidden();
await page.mouse.click(shapeToolBox.x + 2, shapeToolBox.y + 2);
await expect(tooltip).toBeVisible();
});
test('delete shape block by keyboard', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'shape');
await dragBetweenCoords(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await setEdgelessTool(page, 'default');
const startPoint = await page.evaluate(() => {
const hitbox = document.querySelector('[data-block-id="3"]');
if (!hitbox) {
throw new Error('hitbox is null');
}
const rect = hitbox.getBoundingClientRect();
if (rect == null) {
throw new Error('rect is null');
}
return {
x: rect.x,
y: rect.y,
};
});
await page.mouse.click(startPoint.x + 2, startPoint.y + 2);
await waitNextFrame(page);
await page.keyboard.press('Backspace');
const exist = await page.evaluate(() => {
return document.querySelector('[data-block-id="3"]') != null;
});
expect(exist).toBe(false);
});
test('edgeless toolbar shape menu shows up and close normally', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const toolbarLocator = page.locator('.edgeless-toolbar-container');
await expect(toolbarLocator).toBeVisible();
const shapeTool = await locatorEdgelessToolButton(page, 'shape');
const shapeToolBox = await shapeTool.boundingBox();
assertExists(shapeToolBox);
await page.mouse.click(shapeToolBox.x + 2, shapeToolBox.y + 2);
const shapeMenu = page.locator('edgeless-shape-menu');
await expect(shapeMenu).toBeVisible();
await page.waitForTimeout(500);
await page.mouse.click(shapeToolBox.x + 2, shapeToolBox.y + 2);
await page.waitForTimeout(500);
await expect(shapeMenu).toBeHidden();
});
test('hovering on shape should not have effect on underlying block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await assertRichTexts(page, ['hello']);
await switchEditorMode(page);
const block = page.locator('affine-edgeless-note');
const blockBox = await block.boundingBox();
if (blockBox === null) throw new Error('Unexpected box value: box is null');
const { x, y } = blockBox;
await setEdgelessTool(page, 'shape');
await dragBetweenCoords(page, { x, y }, { x: x + 100, y: y + 100 });
await setEdgelessTool(page, 'default');
await page.mouse.click(x + 10, y + 10);
await assertEdgelessSelectedRect(page, [x, y, 100, 100]);
});
test('shape element should not move when the selected state is inactive', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'shape');
await dragBetweenCoords(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await setEdgelessTool(page, 'default');
await dragBetweenCoords(
page,
{ x: 50, y: 50 },
{ x: 110, y: 110 },
{ steps: 2 }
);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('change shape stroke width', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 150 };
const end = { x: 200, y: 250 };
await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
await changeShapeStrokeColor(page, 'MediumMagenta');
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
await changeShapeStrokeWidth(page);
await page.mouse.click(start.x + 5, start.y + 5);
await assertEdgelessSelectedRect(page, [100, 150, 100, 100]);
await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
});
test('change shape stroke style', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 150 };
const end = { x: 200, y: 250 };
await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
await changeShapeStrokeColor(page, 'MediumBlue');
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
await changeShapeStrokeStyle(page, 'dash');
await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'changeShapeStrokeStyles');
const activeButton = locatorShapeStrokeStyleButton(page, 'dash');
const className = await activeButton.evaluate(ele => ele.className);
expect(className.includes(' active')).toBeTruthy();
const pickedColor = await pickColorAtPoints(page, [[start.x + 20, start.y]]);
expect(pickedColor[0]).toBe('#000000');
});
test('click to add shape', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'shape');
await waitNextFrame(page, 500);
await page.mouse.move(400, 400);
await page.mouse.move(200, 200);
await page.mouse.click(200, 200, { button: 'left', delay: 300 });
await assertEdgelessTool(page, 'default');
await assertEdgelessSelectedRect(page, [200, 200, 100, 100]);
});
test('dbclick to add text in shape', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'shape');
await waitNextFrame(page, 500);
await page.mouse.click(200, 150);
await waitNextFrame(page);
await page.mouse.dblclick(250, 200);
await waitNextFrame(page);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
await assertEdgelessTool(page, 'default');
// test select, copy, paste
const select = async () => {
await page.mouse.move(245, 205);
await page.mouse.down();
await page.mouse.move(245, 205);
await page.mouse.down();
await page.mouse.move(262, 205, {
steps: 10,
});
await page.mouse.up();
};
await select();
// h|ell|o
await waitNextFrame(page);
await copyByKeyboard(page);
await waitNextFrame(page);
// FIXME(@Flrande): this is a workaround, we should keep selection
await select();
await waitNextFrame(page);
await type(page, 'ddd', 50);
await waitNextFrame(page);
await assertEdgelessCanvasText(page, 'hdddo');
await pasteByKeyboard(page);
await assertEdgelessCanvasText(page, 'hdddello');
});
test('should show selected rect after exiting editing by pressing Escape', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'shape');
await waitNextFrame(page, 500);
await dragBetweenCoords(page, { x: 100, y: 100 }, { x: 200, y: 200 });
await waitNextFrame(page);
await page.mouse.dblclick(150, 150);
await waitNextFrame(page);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
await pressEscape(page);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('auto wrap text in shape', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'shape');
await waitNextFrame(page, 500);
await page.mouse.click(200, 150);
await waitNextFrame(page);
await page.mouse.dblclick(250, 200);
await waitNextFrame(page);
await type(page, 'aaaa\nbbbb\n');
await assertEdgelessCanvasText(page, 'aaaa\nbbbb\n');
await assertEdgelessTool(page, 'default');
// blur to finish typing
await page.mouse.click(150, 150);
// select shape
await page.mouse.click(200, 150);
// the height of shape should be increased because of \n
let selectedRect = await getEdgelessSelectedRect(page);
let lastWidth = selectedRect.width;
let lastHeight = selectedRect.height;
await page.mouse.dblclick(250, 200);
await waitNextFrame(page);
// type long text
await type(page, '\ncccccccc');
await assertEdgelessCanvasText(page, 'aaaa\nbbbb\ncccccccc');
// blur to finish typing
await page.mouse.click(150, 150);
// select shape
await page.mouse.click(200, 150);
// the height of shape should be increased because of long text
// cccccccc -- wrap --> cccccc\ncc
selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).toBe(lastWidth);
expect(selectedRect.height).toBeGreaterThan(lastHeight);
lastWidth = selectedRect.width;
lastHeight = selectedRect.height;
// try to decrease height
await resizeElementByHandle(page, { x: 0, y: -50 }, 'bottom-right');
// you can't decrease height because of min height to fit text
selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).toBe(lastWidth);
expect(selectedRect.height).toBeGreaterThanOrEqual(lastHeight);
lastWidth = selectedRect.width;
lastHeight = selectedRect.height;
// increase width to make text not wrap
await resizeElementByHandle(page, { x: 50, y: -10 }, 'bottom-right');
// the height of shape should be decreased because of long text not wrap
selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).toBeGreaterThan(lastWidth);
expect(selectedRect.height).toBeLessThan(lastHeight);
// try to decrease width
await resizeElementByHandle(page, { x: -140, y: 0 }, 'bottom-right');
// you can't decrease width after text can't wrap (each line just has 1 char)
await assertEdgelessSelectedRect(page, [200, 150, 52, 404]);
});
test('change shape style', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 150 };
const end = { x: 200, y: 250 };
await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStyle');
await changeShapeStyle(page, 'general');
await waitNextFrame(page);
await page.mouse.click(start.x + 5, start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeStrokeColor');
const color = 'LightPurple';
await changeShapeStrokeColor(page, color);
await page.waitForTimeout(50);
const [picked] = await pickColorAtPoints(page, [[start.x + 1, start.y + 1]]);
await assertEdgelessColorSameWithHexColor(
page,
lightThemeV2['edgeless/palette/light/purpleLight'],
picked
);
});
test('shape adds text by button', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'shape');
await waitNextFrame(page, 500);
await page.mouse.click(200, 150);
await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'addText');
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
});
test('should reset shape text when text is empty', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'shape');
await waitNextFrame(page, 500);
await page.mouse.click(200, 150);
await waitNextFrame(page);
await triggerComponentToolbarAction(page, 'addText');
await type(page, ' a ');
await assertEdgelessCanvasText(page, ' a ');
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.click(200, 150);
const addTextBtn = locatorComponentToolbar(page).getByRole('button', {
name: 'Add text',
});
await expect(addTextBtn).toBeHidden();
await page.mouse.dblclick(250, 200);
await assertEdgelessCanvasText(page, 'a');
await page.keyboard.press('Backspace');
await assertEdgelessCanvasText(page, '');
await page.mouse.click(0, 0);
await waitNextFrame(page);
await page.mouse.click(200, 150);
await expect(addTextBtn).toBeVisible();
});
test.describe('shape hit test', () => {
async function addTransparentRect(
page: Page,
start: { x: number; y: number },
end: { x: number; y: number }
) {
const rect = {
start,
end,
};
await addBasicRectShapeElement(page, rect.start, rect.end);
await page.mouse.click(rect.start.x + 5, rect.start.y + 5);
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
await changeShapeFillColorToTransparent(page);
await page.waitForTimeout(50);
}
test.beforeEach(async ({ page }) => {
await enterPlaygroundRoom(page);
await page.evaluate(() => {
window.doc
.get(window.$blocksuite.blocks.FeatureFlagService)
.setFlag('enable_edgeless_text', false);
});
await initEmptyEdgelessState(page);
await switchEditorMode(page);
});
const rect = {
start: { x: 100, y: 100 },
end: { x: 200, y: 200 },
};
test('can select hollow shape by clicking center area', async ({ page }) => {
await addTransparentRect(page, rect.start, rect.end);
await page.mouse.click(rect.start.x - 20, rect.start.y - 20);
await assertEdgelessNonSelectedRect(page);
await page.mouse.click(rect.start.x + 50, rect.start.y + 50);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
});
test('double click can add text in shape hollow area', async ({ page }) => {
await addTransparentRect(page, rect.start, rect.end);
await page.mouse.click(rect.start.x - 20, rect.start.y - 20);
await assertEdgelessNonSelectedRect(page);
await assertEdgelessTool(page, 'default');
await page.mouse.dblclick(rect.start.x + 20, rect.start.y + 20);
await waitNextFrame(page);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
});
// FIXME(@flrande): This is broken by recent changes
// In Playwright, we can't add text in shape hollow area
test.fixme(
'using text tool to add text in shape hollow area',
async ({ page }) => {
await addTransparentRect(page, rect.start, rect.end);
await page.mouse.click(rect.start.x - 20, rect.start.y - 20);
await assertEdgelessNonSelectedRect(page);
await assertEdgelessTool(page, 'default');
await setEdgelessTool(page, 'text');
await page.mouse.click(rect.start.x + 50, rect.start.y + 50);
await waitNextFrame(page);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
}
);
test('should enter edit mode when double-clicking a text area in a shape with a transparent background', async ({
page,
}) => {
await addTransparentRect(page, rect.start, rect.end);
await page.mouse.click(rect.start.x - 20, rect.start.y - 20);
await assertEdgelessNonSelectedRect(page);
await assertEdgelessTool(page, 'default');
await page.mouse.dblclick(rect.start.x + 50, rect.start.y + 50);
await waitNextFrame(page);
await type(page, 'hello');
await pressEscape(page);
await waitNextFrame(page);
const textAlignBtn = locatorComponentToolbar(page).getByRole('button', {
name: 'Alignment',
});
await textAlignBtn.click();
await page
.locator('edgeless-align-panel')
.getByRole('button', { name: 'Left' })
.click();
// creates an edgeless-text
await page.mouse.dblclick(rect.start.x + 80, rect.start.y + 20);
await waitNextFrame(page);
await page.locator('edgeless-text-editor').isVisible();
await pressEscape(page);
await waitNextFrame(page);
// enters edit mode
await page.mouse.dblclick(rect.start.x + 20, rect.start.y + 50);
await page.locator('edgeless-shape-text-editor').isVisible();
await type(page, ' world');
await assertEdgelessCanvasText(page, 'hello world');
});
});

View File

@@ -0,0 +1,366 @@
import { expect } from '@playwright/test';
import {
addBasicRectShapeElement,
assertEdgelessShapeType,
createShapeElement,
edgelessCommonSetup,
getEdgelessSelectedRect,
getZoomLevel,
locatorEdgelessToolButton,
setEdgelessTool,
type ShapeName,
switchEditorMode,
zoomFitByKeyboard,
zoomInByKeyboard,
zoomOutByKeyboard,
zoomResetByKeyboard,
zoomToSelection,
} from '../utils/actions/edgeless.js';
import {
clickView,
dragBetweenCoords,
enterPlaygroundRoom,
focusRichText,
initEmptyEdgelessState,
pressBackspace,
pressEscape,
pressForwardDelete,
selectAllByKeyboard,
selectNoteInEdgeless,
type,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertBlockCount,
assertEdgelessNonSelectedRect,
assertEdgelessSelectedModelRect,
assertEdgelessSelectedRect,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('shortcut', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await page.mouse.click(100, 100);
// text is removed temporarily
// await page.keyboard.press('t');
// const textButton = await locatorEdgelessToolButton(page, 'text');
// await expect(textButton).toHaveAttribute('active', '');
await page.keyboard.press('s');
const shapeButton = await locatorEdgelessToolButton(page, 'shape');
await expect(shapeButton).toHaveAttribute('active', '');
await page.keyboard.press('p');
const penButton = await locatorEdgelessToolButton(page, 'brush');
await expect(penButton).toHaveAttribute('active', '');
await page.keyboard.press('h');
const panButton = await locatorEdgelessToolButton(page, 'pan');
await expect(panButton).toHaveAttribute('active', '');
await page.keyboard.press('c');
const connectorButton = await locatorEdgelessToolButton(page, 'connector');
await expect(connectorButton).toHaveAttribute('active', '');
// await page.keyboard.press('l');
// const lassoButton = await locatorEdgelessToolButton(page, 'lasso');
// await expect(lassoButton).toHaveAttribute('active', '');
});
test.skip('toggle lasso tool modes', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await page.mouse.click(100, 100);
const lassoButton = await locatorEdgelessToolButton(page, 'lasso', false);
const isLassoMode = async (type: 'freehand' | 'polygonal') => {
const classes = (await lassoButton.getAttribute('class'))?.split(' ') ?? [];
return classes.includes(type);
};
await page.keyboard.press('Shift+l');
expect(await isLassoMode('freehand')).toBe(true);
await page.keyboard.press('Shift+l');
expect(await isLassoMode('polygonal')).toBe(true);
await page.keyboard.press('Shift+l');
expect(await isLassoMode('freehand')).toBe(true);
});
test('toggle shapes shortcut', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await page.mouse.click(100, 100);
await setEdgelessTool(page, 'shape');
const shapesInOrder = [
'ellipse',
'diamond',
'triangle',
'roundedRect',
'rect',
'ellipse',
'diamond',
'triangle',
'roundedRect',
] as ShapeName[];
for (const shape of shapesInOrder) {
await page.keyboard.press('Shift+s');
await assertEdgelessShapeType(page, shape);
}
});
test('should not switch shapes in editing', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await setEdgelessTool(page, 'shape');
await waitNextFrame(page);
await assertEdgelessShapeType(page, 'rect');
await page.mouse.click(200, 150);
await waitNextFrame(page);
await page.mouse.dblclick(250, 200);
await waitNextFrame(page);
await type(page, 'hello');
await page.keyboard.press('Shift+s');
await pressEscape(page);
await waitNextFrame(page, 200);
await setEdgelessTool(page, 'shape');
await assertEdgelessShapeType(page, 'rect');
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(250, 200);
await waitNextFrame(page);
await page.keyboard.press('Shift+S');
await pressEscape(page);
await waitNextFrame(page);
await waitNextFrame(page, 200);
await setEdgelessTool(page, 'shape');
await assertEdgelessShapeType(page, 'rect');
});
test('pressing the ESC key will return to the default state', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
const start = { x: 100, y: 100 };
const end = { x: 200, y: 200 };
await addBasicRectShapeElement(page, start, end);
await page.mouse.click(start.x + 5, start.y + 5);
await assertEdgelessSelectedRect(page, [100, 100, 100, 100]);
await pressEscape(page);
await assertEdgelessNonSelectedRect(page);
});
test.describe('zooming', () => {
test('zoom fit to screen', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomResetByKeyboard(page);
const start = { x: 0, y: 0 };
const end = { x: 900, y: 200 };
await addBasicRectShapeElement(page, start, end);
await zoomFitByKeyboard(page);
const zoom = await getZoomLevel(page);
expect(zoom).not.toBe(100);
});
test('zoom out', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await clickView(page, [0, 0]);
await zoomResetByKeyboard(page);
await zoomOutByKeyboard(page);
let zoom = await getZoomLevel(page);
expect(zoom).toBe(75);
await zoomOutByKeyboard(page);
zoom = await getZoomLevel(page);
expect(zoom).toBe(50);
});
test('zoom reset', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await clickView(page, [0, 0]);
await zoomResetByKeyboard(page);
let zoom = await getZoomLevel(page);
expect(zoom).toBe(100);
await zoomOutByKeyboard(page);
zoom = await getZoomLevel(page);
expect(zoom).toBe(75);
await zoomResetByKeyboard(page);
zoom = await getZoomLevel(page);
expect(zoom).toBe(100);
});
test('zoom in', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await clickView(page, [0, 0]);
await zoomResetByKeyboard(page);
await zoomInByKeyboard(page);
let zoom = await getZoomLevel(page);
expect(zoom).toBe(125);
await zoomInByKeyboard(page);
zoom = await getZoomLevel(page);
expect(zoom).toBe(150);
});
test('zoom to selection', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomToSelection(page);
const start = { x: 0, y: 0 };
const end = { x: 900, y: 200 };
await addBasicRectShapeElement(page, start, end);
await page.keyboard.down('Space');
await dragBetweenCoords(
page,
{
x: 200,
y: 200,
},
{
x: 200 - 50,
y: 200 - 50,
}
);
await page.keyboard.up('Space');
await zoomFitByKeyboard(page);
const shapeContained = await page.evaluate(() => {
const edgelessBlock = document.querySelector('affine-edgeless-root');
if (!edgelessBlock) {
throw new Error('edgeless block not found');
}
const gfx = edgelessBlock.gfx;
const element = gfx.selection.selectedElements[0];
return gfx.viewport.viewportBounds.contains(element.elementBound);
});
expect(shapeContained).toBe(true);
});
});
test('cmd + A should select all elements by default', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100]);
await createShapeElement(page, [100, 0], [200, 100]);
await selectAllByKeyboard(page);
await assertEdgelessSelectedModelRect(page, [0, 0, 200, 100]);
});
test('cmd + A should not fire inside active note', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
// second click become active
await selectNoteInEdgeless(page, noteId);
await selectAllByKeyboard(page);
// should not have selected rect
let error = null;
try {
await getEdgelessSelectedRect(page);
} catch (e) {
error = e;
}
expect(error).not.toBeNull();
});
test.describe('delete', () => {
test('do not delete element when active', async ({ page }) => {
await enterPlaygroundRoom(page);
const { noteId } = await initEmptyEdgelessState(page);
await focusRichText(page);
await type(page, 'hello');
await switchEditorMode(page);
await selectNoteInEdgeless(page, noteId);
const box1 = await getEdgelessSelectedRect(page);
await page.mouse.click(box1.x + 10, box1.y + 10);
await pressBackspace(page);
await assertBlockCount(page, 'edgeless-note', 1);
await pressForwardDelete(page);
await assertBlockCount(page, 'edgeless-note', 1);
});
});
test.describe('Arrow Keys should move selection', () => {
test('with shift increment by 10px', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
await page.keyboard.down('Shift');
for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowLeft');
for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowDown');
await assertEdgelessSelectedRect(page, [0, 200, 100, 100]);
});
test('without shift increment by 1px', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await addBasicRectShapeElement(
page,
{ x: 100, y: 100 },
{ x: 200, y: 200 }
);
for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowRight');
for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowUp');
await assertEdgelessSelectedRect(page, [110, 90, 100, 100]);
});
});

View File

@@ -0,0 +1,45 @@
import { undoByClick } from '../utils/actions/click.js';
import {
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup,
Shape,
} from '../utils/actions/edgeless.js';
import { waitNextFrame } from '../utils/actions/misc.js';
import { assertSelectedBound } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.describe('snap', () => {
test('snap', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [300, 0], [300 + 100, 100], Shape.Square);
await assertSelectedBound(page, [300, 0, 100, 100]);
await dragBetweenViewCoords(page, [300 + 5, 50], [300 + 5, 50 + 5]);
await assertSelectedBound(page, [300, 5, 100, 100]);
await undoByClick(page);
await dragBetweenViewCoords(page, [300 + 5, 50], [300 + 5, 50 + 3]);
await assertSelectedBound(page, [300, 0, 100, 100]);
});
test('snapDistribute', async ({ page }) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [0, 0], [100, 100], Shape.Square);
await createShapeElement(page, [300, 0], [300 + 100, 100], Shape.Square);
await createShapeElement(page, [144, 0], [144 + 100, 100], Shape.Square);
await assertSelectedBound(page, [144, 0, 100, 100]);
await dragBetweenViewCoords(
page,
[144 + 100 - 9, 100 - 9],
[144 + 100 - 9 + 3, 100 - 9]
);
await assertSelectedBound(page, [150, 0, 100, 100]);
await waitNextFrame(page);
});
});

View File

@@ -0,0 +1,317 @@
import { expect, type Page } from '@playwright/test';
import {
assertEdgelessTool,
enterPlaygroundRoom,
getEdgelessSelectedRect,
initEmptyEdgelessState,
pressArrowLeft,
pressEnter,
setEdgelessTool,
SHORT_KEY,
switchEditorMode,
type,
waitForInlineEditorStateUpdated,
waitNextFrame,
zoomResetByKeyboard,
} from '../utils/actions/index.js';
import { getLinkedDocPopover } from '../utils/actions/linked-doc.js';
import { assertEdgelessCanvasText } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
async function assertTextFont(page: Page, font: string) {
const fontButton = page.getByRole('button', {
name: /^Font$/,
});
const fontPanel = page.locator('edgeless-font-family-panel');
const isFontPanelShow = await fontPanel.isVisible();
if (!isFontPanelShow) {
if (!(await fontButton.isVisible()))
throw new Error('edgeless change text toolbar is not visible');
await fontButton.click();
}
const button = fontPanel.locator(`[data-font="${font}"]`);
await expect(button.locator('.active-mode-color[active]')).toBeVisible();
}
test.describe('edgeless canvas text', () => {
test.beforeEach(async ({ page }) => {
await enterPlaygroundRoom(page);
await page.evaluate(() => {
window.doc
.get(window.$blocksuite.blocks.FeatureFlagService)
.setFlag('enable_edgeless_text', false);
});
await initEmptyEdgelessState(page);
await switchEditorMode(page);
});
test('add text element in default mode', async ({ page }) => {
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140);
await waitForInlineEditorStateUpdated(page);
await waitNextFrame(page);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
await assertEdgelessTool(page, 'default');
await page.mouse.click(120, 140);
expect(await page.locator('edgeless-text-editor').count()).toBe(0);
await page.mouse.dblclick(145, 155);
await waitNextFrame(page);
await page.locator('edgeless-text-editor').waitFor({
state: 'attached',
});
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hhelloello');
await pressArrowLeft(page, 5);
await type(page, 'ddd\n');
await assertEdgelessCanvasText(page, 'hddd\nhelloello');
});
test('should not trigger linked doc popover in canvas text', async ({
page,
}) => {
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140);
await waitForInlineEditorStateUpdated(page);
await waitNextFrame(page);
await type(page, '@');
const { linkedDocPopover } = getLinkedDocPopover(page);
await expect(linkedDocPopover).not.toBeVisible();
await pressEnter(page);
await assertEdgelessCanvasText(page, '@\n');
});
// it's also a little flaky
test('add text element in text mode', async ({ page }) => {
await page.mouse.dblclick(130, 140);
await waitNextFrame(page);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
await assertEdgelessTool(page, 'default');
await page.mouse.click(120, 140);
expect(await page.locator('edgeless-text-editor').count()).toBe(0);
await page.mouse.dblclick(145, 145);
await page.locator('edgeless-text-editor').waitFor({
state: 'attached',
});
await type(page, 'hello');
await page.waitForTimeout(100);
await assertEdgelessCanvasText(page, 'hhelloello');
await page.mouse.click(145, 155);
await type(page, 'ddd\n');
await assertEdgelessCanvasText(page, 'hddd\nhelloello');
});
test('copy and paste', async ({ page }) => {
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140);
await waitNextFrame(page);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hello');
await assertEdgelessTool(page, 'default');
await page.mouse.move(145, 155);
await page.mouse.down();
await page.mouse.move(170, 155, {
steps: 10,
});
await page.mouse.up();
// h|ell|o
await waitNextFrame(page, 200);
await page.keyboard.press(`${SHORT_KEY}+c`);
await waitNextFrame(page, 200);
await type(page, 'ddd', 100);
await waitNextFrame(page, 200);
await assertEdgelessCanvasText(page, 'hdddo');
await page.keyboard.press(`${SHORT_KEY}+v`);
await assertEdgelessCanvasText(page, 'hdddello');
});
test('normalize text element rect after change its font', async ({
page,
}) => {
await page.mouse.dblclick(200, 200);
await waitNextFrame(page);
await type(page, 'aaa\nbbbbbbbb\n\ncc');
await assertEdgelessCanvasText(page, 'aaa\nbbbbbbbb\n\ncc');
await assertEdgelessTool(page, 'default');
await page.mouse.click(10, 100);
await page.mouse.click(220, 210);
await waitNextFrame(page);
let { width: lastWidth, height: lastHeight } =
await getEdgelessSelectedRect(page);
const fontButton = page.getByRole('button', { name: /^Font$/ });
await fontButton.click();
// Default is Inter
await assertTextFont(page, 'Inter');
const kalamTextFont = page.getByText('Kalam');
await kalamTextFont.click();
await waitNextFrame(page);
let selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).not.toEqual(lastWidth);
expect(selectedRect.height).not.toEqual(lastHeight);
lastWidth = selectedRect.width;
lastHeight = selectedRect.height;
await fontButton.click();
await assertTextFont(page, 'Kalam');
const InterTextFont = page.getByText('Inter');
await InterTextFont.click();
await waitNextFrame(page);
selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).not.toEqual(lastWidth);
expect(selectedRect.height).not.toEqual(lastHeight);
});
test('auto wrap text by dragging left and right edge', async ({ page }) => {
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140);
await waitForInlineEditorStateUpdated(page);
await waitNextFrame(page);
await type(page, 'hellohello');
await assertEdgelessCanvasText(page, 'hellohello');
await assertEdgelessTool(page, 'default');
// quit edit mode
await page.mouse.click(120, 140);
// select text element
await page.mouse.click(150, 140);
await waitNextFrame(page);
// should exit selected rect and record last width and height, then compare them
let selectedRect = await getEdgelessSelectedRect(page);
let lastWidth = selectedRect.width;
let lastHeight = selectedRect.height;
// move cursor to the right edge and drag it to resize the width of text element
await page.mouse.move(130 + lastWidth, 160);
await page.mouse.down();
await page.mouse.move(130 + lastWidth / 2, 160, {
steps: 10,
});
await page.mouse.up();
// the text should be wrapped, so check the width and height of text element
selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).toBeLessThan(lastWidth);
expect(selectedRect.height).toBeGreaterThan(lastHeight);
await page.mouse.dblclick(140, 160);
await waitForInlineEditorStateUpdated(page);
await waitNextFrame(page);
await assertEdgelessCanvasText(page, 'hellohello');
// quit edit mode
await page.mouse.click(120, 140);
// select text element
await page.mouse.click(150, 140);
await waitNextFrame(page);
// check selected rect and record the last width and height
selectedRect = await getEdgelessSelectedRect(page);
lastWidth = selectedRect.width;
lastHeight = selectedRect.height;
// move cursor to the left edge and drag it to resize the width of text element
await page.mouse.move(130, 160);
await page.mouse.down();
await page.mouse.move(60, 160, {
steps: 10,
});
await page.mouse.up();
// the text should be unwrapped, check the width and height of text element
selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).toBeGreaterThan(lastWidth);
expect(selectedRect.height).toBeLessThan(lastHeight);
await page.mouse.dblclick(100, 160);
await waitForInlineEditorStateUpdated(page);
await waitNextFrame(page);
await assertEdgelessCanvasText(page, 'hellohello');
});
test('text element should have maxWidth after adjusting width by dragging left or right edge', async ({
page,
}) => {
await zoomResetByKeyboard(page);
await setEdgelessTool(page, 'default');
await page.mouse.dblclick(130, 140);
await waitForInlineEditorStateUpdated(page);
await waitNextFrame(page);
await type(page, 'hellohello');
await assertEdgelessCanvasText(page, 'hellohello');
await assertEdgelessTool(page, 'default');
// quit edit mode
await page.mouse.click(120, 140);
// select text element
await page.mouse.click(150, 140);
await waitNextFrame(page);
let selectedRect = await getEdgelessSelectedRect(page);
let lastWidth = selectedRect.width;
let lastHeight = selectedRect.height;
// move cursor to the right edge and drag it to resize the width of text element
await page.mouse.move(130 + lastWidth, 160);
await page.mouse.down();
await page.mouse.move(130 + lastWidth / 2, 160, {
steps: 10,
});
await page.mouse.up();
// the text should be wrapped, so check the width and height of text element
selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).toBeLessThan(lastWidth);
expect(selectedRect.height).toBeGreaterThan(lastHeight);
lastWidth = selectedRect.width;
lastHeight = selectedRect.height;
// enter edit mode
await waitNextFrame(page);
await page.mouse.dblclick(140, 180);
await waitForInlineEditorStateUpdated(page);
await waitNextFrame(page);
await type(page, 'hello');
await assertEdgelessCanvasText(page, 'hellohellohello');
// quit edit mode
await page.mouse.click(120, 140);
// select text element
await page.mouse.click(150, 140);
await waitNextFrame(page);
// after input, the width of the text element should be the same as before, but the height should be changed
selectedRect = await getEdgelessSelectedRect(page);
expect(selectedRect.width).toBeCloseTo(Math.round(lastWidth));
expect(selectedRect.height).toBeGreaterThan(lastHeight);
});
});

View File

@@ -0,0 +1,261 @@
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import { assertExists } from '@blocksuite/global/utils';
import { expect, type Page } from '@playwright/test';
import { switchEditorMode } from './utils/actions/edgeless.js';
import { getLinkedDocPopover } from './utils/actions/linked-doc.js';
import {
enterPlaygroundRoom,
focusRichText,
initEmptyEdgelessState,
initEmptyParagraphState,
waitNextFrame,
} from './utils/actions/misc.js';
import { test } from './utils/playwright.js';
test.describe('Embed synced doc', () => {
test.beforeEach(async ({ page }) => {
await enterPlaygroundRoom(page);
});
async function createAndConvertToEmbedSyncedDoc(page: Page) {
const { createLinkedDoc } = getLinkedDocPopover(page);
const linkedDoc = await createLinkedDoc('page1');
const lickedDocBox = await linkedDoc.boundingBox();
assertExists(lickedDocBox);
await page.mouse.move(
lickedDocBox.x + lickedDocBox.width / 2,
lickedDocBox.y + lickedDocBox.height / 2
);
await waitNextFrame(page, 200);
const referencePopup = page.locator('.affine-reference-popover-container');
await expect(referencePopup).toBeVisible();
const switchButton = page.getByRole('button', { name: 'Switch view' });
await switchButton.click();
const embedSyncedDocBtn = page.getByRole('button', { name: 'Embed view' });
await expect(embedSyncedDocBtn).toBeVisible();
await embedSyncedDocBtn.click();
await waitNextFrame(page, 200);
const embedSyncedBlock = page.locator('affine-embed-synced-doc-block');
expect(await embedSyncedBlock.count()).toBe(1);
}
test('can change linked doc to embed synced doc', async ({ page }) => {
await initEmptyParagraphState(page);
await focusRichText(page);
await createAndConvertToEmbedSyncedDoc(page);
});
test('can change embed synced doc to card view', async ({ page }) => {
await initEmptyParagraphState(page);
await focusRichText(page);
await createAndConvertToEmbedSyncedDoc(page);
const syncedDoc = page.locator(`affine-embed-synced-doc-block`);
const syncedDocBox = await syncedDoc.boundingBox();
assertExists(syncedDocBox);
await page.mouse.click(
syncedDocBox.x + syncedDocBox.width / 2,
syncedDocBox.y + syncedDocBox.height / 2
);
await waitNextFrame(page, 200);
const toolbar = page.locator('.embed-card-toolbar');
await expect(toolbar).toBeVisible();
const switchBtn = toolbar.getByRole('button', { name: 'Switch view' });
await expect(switchBtn).toBeVisible();
await switchBtn.click();
await waitNextFrame(page, 200);
const cardBtn = toolbar.getByRole('button', { name: 'Card view' });
await cardBtn.click();
await waitNextFrame(page, 200);
const embedSyncedBlock = page.locator('affine-embed-linked-doc-block');
expect(await embedSyncedBlock.count()).toBe(1);
});
test.fixme(
'drag embed synced doc to whiteboard should fit in height',
async ({ page }) => {
await initEmptyEdgelessState(page);
await focusRichText(page);
await createAndConvertToEmbedSyncedDoc(page);
// Focus on the embed synced doc
const embedSyncedBlock = page.locator('affine-embed-synced-doc-block');
let embedSyncedBox = await embedSyncedBlock.boundingBox();
assertExists(embedSyncedBox);
await page.mouse.click(
embedSyncedBox.x + embedSyncedBox.width / 2,
embedSyncedBox.y + embedSyncedBox.height / 2
);
// Switch to edgeless mode
await switchEditorMode(page);
await waitNextFrame(page, 200);
// Double click on note to enter edit status
const noteBlock = page.locator('affine-edgeless-note');
const noteBlockBox = await noteBlock.boundingBox();
assertExists(noteBlockBox);
await page.mouse.dblclick(noteBlockBox.x + 10, noteBlockBox.y + 10);
await waitNextFrame(page, 200);
// Drag the embed synced doc to whiteboard
embedSyncedBox = await embedSyncedBlock.boundingBox();
assertExists(embedSyncedBox);
const height = embedSyncedBox.height;
await page.mouse.move(embedSyncedBox.x - 10, embedSyncedBox.y - 100);
await page.mouse.move(embedSyncedBox.x - 10, embedSyncedBox.y + 10);
await waitNextFrame(page);
await page.mouse.down();
await page.mouse.move(100, 200, { steps: 30 });
await page.mouse.up();
// Check the height of the embed synced doc portal, it should be the same as the embed synced doc in note
const EmbedSyncedDocBlock = page.locator(
'affine-embed-edgeless-synced-doc-block'
);
const EmbedSyncedDocBlockBox = await EmbedSyncedDocBlock.boundingBox();
const border = 1;
assertExists(EmbedSyncedDocBlockBox);
expect(EmbedSyncedDocBlockBox.height).toBeCloseTo(height + 2 * border, 1);
}
);
test('nested embed synced doc should be rendered as card when depth >=1', async ({
page,
}) => {
await page.evaluate(() => {
const { doc, collection } = window;
const rootId = doc.addBlock('affine:page', {
title: new window.$blocksuite.store.Text(),
});
const noteId = doc.addBlock('affine:note', {}, rootId);
doc.addBlock('affine:paragraph', {}, noteId);
const doc2 = collection.createDoc({ id: 'doc2' });
doc2.load();
const rootId2 = doc2.addBlock('affine:page', {
title: new window.$blocksuite.store.Text('Doc 2'),
});
const noteId2 = doc2.addBlock('affine:note', {}, rootId2);
doc2.addBlock(
'affine:paragraph',
{
text: new window.$blocksuite.store.Text('Hello from Doc 2'),
},
noteId2
);
const doc3 = collection.createDoc({ id: 'doc3' });
doc3.load();
const rootId3 = doc3.addBlock('affine:page', {
title: new window.$blocksuite.store.Text('Doc 3'),
});
const noteId3 = doc3.addBlock('affine:note', {}, rootId3);
doc3.addBlock(
'affine:paragraph',
{
text: new window.$blocksuite.store.Text('Hello from Doc 3'),
},
noteId3
);
doc2.addBlock(
'affine:embed-synced-doc',
{
pageId: 'doc3',
},
noteId2
);
doc.addBlock(
'affine:embed-synced-doc',
{
pageId: 'doc2',
},
noteId
);
});
expect(await page.locator('affine-embed-synced-doc-block').count()).toBe(2);
expect(await page.locator('affine-paragraph').count()).toBe(2);
expect(await page.locator('affine-embed-synced-doc-card').count()).toBe(1);
expect(await page.locator('editor-host').count()).toBe(2);
});
test.describe('synced doc should be readonly', () => {
test('synced doc should be readonly', async ({ page }) => {
await initEmptyParagraphState(page);
await focusRichText(page);
await createAndConvertToEmbedSyncedDoc(page);
const locator = page.locator('affine-embed-synced-doc-block');
await locator.click();
const toolbar = page.locator('editor-toolbar');
const openMenu = toolbar.getByRole('button', { name: 'Open' });
await openMenu.click();
const button = toolbar.getByRole('button', { name: 'Open this doc' });
await button.click();
await page.evaluate(async () => {
const { collection } = window;
const getDocCollection = () => {
for (const [id, doc] of collection.docs.entries()) {
if (id === 'doc:home') {
continue;
}
return doc;
}
return null;
};
const doc2Collection = getDocCollection();
const doc2 = doc2Collection!.getStore();
const [noteBlock] = doc2!.getBlocksByFlavour('affine:note');
const noteId = noteBlock.id;
const databaseId = doc2.addBlock(
'affine:database',
{
title: new window.$blocksuite.store.Text('Database 1'),
},
noteId
);
const model = doc2.getBlockById(databaseId) as DatabaseBlockModel;
const datasource =
new window.$blocksuite.blocks.DatabaseBlockDataSource(model);
datasource.viewManager.viewAdd('table');
});
// go back to previous doc
await page.evaluate(() => {
const { collection, editor } = window;
editor.doc = collection.getDoc('doc:home')!;
});
const databaseFirstCell = page.locator(
'.affine-database-column-header.database-row'
);
await databaseFirstCell.click({ force: true });
const selectedCount = await page
.locator('.affine-embed-synced-doc-container.selected')
.count();
expect(selectedCount).toBe(1);
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,356 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import {
addBasicShapeElement,
addNote,
createNote,
createShapeElement,
dragBetweenViewCoords,
edgelessCommonSetup,
enterPresentationMode,
getZoomLevel,
setEdgelessTool,
Shape,
switchEditorMode,
toggleFramePanel,
} from '../utils/actions/edgeless.js';
import { waitNextFrame } from '../utils/actions/index.js';
import {
assertEdgelessNonSelectedRect,
assertEdgelessSelectedRect,
assertZoomLevel,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
async function dragFrameCard(
page: Page,
fromCard: Locator,
toCard: Locator,
direction: 'up' | 'down' = 'down'
) {
const fromRect = await fromCard.boundingBox();
const toRect = await toCard.boundingBox();
// drag to the center of the toCard
const center = { x: toRect!.width / 2, y: toRect!.height / 2 };
const offset = direction === 'up' ? { x: 0, y: -20 } : { x: 0, y: 20 };
await page.mouse.move(fromRect!.x + center.x, fromRect!.y + center.y);
await page.mouse.down();
await page.mouse.move(
toRect!.x + center.x + offset.x,
toRect!.y + center.y + offset.y,
{ steps: 10 }
);
await page.mouse.up();
}
test.describe('frame panel', () => {
test('should display empty placeholder when no frames', async ({ page }) => {
await edgelessCommonSetup(page);
await toggleFramePanel(page);
const frameCards = page.locator('affine-frame-card');
expect(await frameCards.count()).toBe(0);
const placeholder = page.locator('.no-frame-placeholder');
expect(await placeholder.isVisible()).toBeTruthy();
});
test('should display frame cards when there are frames', async ({ page }) => {
await edgelessCommonSetup(page);
await toggleFramePanel(page);
await addBasicShapeElement(
page,
{ x: 300, y: 300 },
{ x: 350, y: 350 },
Shape.Square
);
await addNote(page, 'hello', 150, 500);
await page.mouse.click(0, 0);
await setEdgelessTool(page, 'frame');
await dragBetweenCoords(page, { x: 250, y: 250 }, { x: 360, y: 360 });
await setEdgelessTool(page, 'frame');
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 });
const frames = page.locator('affine-frame');
expect(await frames.count()).toBe(2);
const frameCards = page.locator('affine-frame-card');
expect(await frameCards.count()).toBe(2);
});
test('should render edgeless note correctly in frame preview', async ({
page,
}) => {
await edgelessCommonSetup(page);
await toggleFramePanel(page);
await addNote(page, 'hello', 150, 500);
await page.mouse.click(0, 0);
await setEdgelessTool(page, 'frame');
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 });
await waitNextFrame(page, 100);
const frames = page.locator('affine-frame');
expect(await frames.count()).toBe(1);
const frameCards = page.locator('affine-frame-card');
expect(await frameCards.count()).toBe(1);
const edgelessNote = page.locator('affine-frame-card affine-edgeless-note');
expect(await edgelessNote.count()).toBe(1);
});
test('should update panel when frames change', async ({ page }) => {
await edgelessCommonSetup(page);
await toggleFramePanel(page);
const frameCards = page.locator('affine-frame-card');
expect(await frameCards.count()).toBe(0);
await addNote(page, 'hello', 150, 500);
await page.mouse.click(0, 0);
await setEdgelessTool(page, 'frame');
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 });
await setEdgelessTool(page, 'frame');
await dragBetweenCoords(page, { x: 50, y: 300 }, { x: 120, y: 400 });
await waitNextFrame(page);
const frames = page.locator('affine-frame');
expect(await frames.count()).toBe(2);
expect(await frameCards.count()).toBe(2);
await page.mouse.click(50, 300);
await page.keyboard.press('Delete');
await waitNextFrame(page);
expect(await frames.count()).toBe(1);
expect(await frameCards.count()).toBe(1);
});
test.describe('frame panel behavior after mode switch', () => {
async function setupFrameTest(page: Page) {
await edgelessCommonSetup(page);
await toggleFramePanel(page);
await addNote(page, 'hello', 150, 500);
await page.mouse.click(0, 0);
await waitNextFrame(page, 100);
await setEdgelessTool(page, 'frame');
await dragBetweenCoords(
page,
{ x: 100, y: 440 },
{ x: 640, y: 600 },
{ steps: 10 }
);
await waitNextFrame(page, 100);
const edgelessNote = page.locator(
'affine-frame-card affine-edgeless-note'
);
expect(await edgelessNote.count()).toBe(1);
return edgelessNote;
}
test('should render edgeless note correctly after mode switch', async ({
page,
}) => {
const edgelessNote = await setupFrameTest(page);
const initialNoteRect = await edgelessNote.boundingBox();
expect(initialNoteRect).not.toBeNull();
const {
width: noteWidth,
height: noteHeight,
x: noteX,
y: noteY,
} = initialNoteRect!;
const checkNoteRect = async () => {
expect(await edgelessNote.count()).toBe(1);
const newNoteRect = await edgelessNote.boundingBox();
expect(newNoteRect).not.toBeNull();
expect(newNoteRect!.width).toBe(noteWidth);
expect(newNoteRect!.height).toBe(noteHeight);
expect(newNoteRect!.x).toBe(noteX);
expect(newNoteRect!.y).toBe(noteY);
};
await switchEditorMode(page);
await checkNoteRect();
await switchEditorMode(page);
await checkNoteRect();
});
test('should update frame preview when note is moved', async ({ page }) => {
const edgelessNote = await setupFrameTest(page);
const initialNoteRect = await edgelessNote.boundingBox();
expect(initialNoteRect).not.toBeNull();
await switchEditorMode(page);
await switchEditorMode(page);
async function moveNoteAndCheck(
start: { x: number; y: number },
end: { x: number; y: number },
comparison: 'greaterThan' | 'lessThan'
) {
await page.mouse.move(start.x, start.y);
await page.mouse.down();
await page.mouse.move(end.x, end.y);
await page.mouse.up();
await waitNextFrame(page);
const newNoteRect = await edgelessNote.boundingBox();
expect(newNoteRect).not.toBeNull();
if (comparison === 'greaterThan') {
expect(newNoteRect!.x).toBeGreaterThan(initialNoteRect!.x);
expect(newNoteRect!.y).toBeGreaterThan(initialNoteRect!.y);
} else {
expect(newNoteRect!.x).toBeLessThan(initialNoteRect!.x);
expect(newNoteRect!.y).toBeLessThan(initialNoteRect!.y);
}
}
// Move the note to the right
await moveNoteAndCheck(
{ x: 150, y: 500 },
{ x: 200, y: 550 },
'greaterThan'
);
// Move the note back to the left
await moveNoteAndCheck(
{ x: 200, y: 550 },
{ x: 100, y: 450 },
'lessThan'
);
// Move the note diagonally
await moveNoteAndCheck(
{ x: 100, y: 450 },
{ x: 250, y: 600 },
'greaterThan'
);
});
});
test.describe('select and de-select frame', () => {
async function setupFrameTest(page: Page) {
await edgelessCommonSetup(page);
await toggleFramePanel(page);
await addNote(page, 'hello', 150, 500);
await page.mouse.click(0, 0);
await setEdgelessTool(page, 'frame');
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 640, y: 600 });
await waitNextFrame(page);
const frames = page.locator('affine-frame');
const frameCards = page.locator('affine-frame-card');
expect(await frames.count()).toBe(1);
expect(await frameCards.count()).toBe(1);
return { frames, frameCards };
}
test('by click on frame card', async ({ page }) => {
const { frameCards } = await setupFrameTest(page);
// click on the first frame card
await frameCards.nth(0).click();
await assertEdgelessSelectedRect(page, [100, 440, 540, 160]);
await frameCards.nth(0).click();
await assertEdgelessNonSelectedRect(page);
});
test('by click on blank area', async ({ page }) => {
const { frameCards } = await setupFrameTest(page);
// click on the first frame card
await frameCards.nth(0).click();
await assertEdgelessSelectedRect(page, [100, 440, 540, 160]);
const framePanel = page.locator('.frame-panel-container');
const panelRect = await framePanel.boundingBox();
expect(panelRect).not.toBeNull();
const { x, y, width, height } = panelRect!;
await page.mouse.click(x + width / 2, y + height / 2);
await assertEdgelessNonSelectedRect(page);
});
});
test('should fit the viewport to the frame when double click frame card', async ({
page,
}) => {
await edgelessCommonSetup(page);
await toggleFramePanel(page);
await assertZoomLevel(page, 100);
await addNote(page, 'hello', 150, 500);
await page.mouse.click(0, 0);
await setEdgelessTool(page, 'frame');
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 });
await waitNextFrame(page);
const frameCards = page.locator('affine-frame-card');
await frameCards.nth(0).dblclick();
const zoomLevel = await getZoomLevel(page);
expect(zoomLevel).toBeGreaterThan(100);
});
test('should reorder frames when drag and drop frame card', async ({
page,
}) => {
await edgelessCommonSetup(page);
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
await createNote(page, [300, 100], 'hello');
// Frame shape
await setEdgelessTool(page, 'frame');
await dragBetweenViewCoords(page, [80, 80], [220, 220]);
await waitNextFrame(page, 100);
// Frame note
await setEdgelessTool(page, 'frame');
await dragBetweenViewCoords(page, [240, 0], [800, 200]);
expect(await page.locator('affine-frame').count()).toBe(2);
await toggleFramePanel(page);
const frameCards = page.locator('affine-frame-card');
expect(await frameCards.count()).toBe(2);
// Drag the first frame card to the second
await dragFrameCard(page, frameCards.nth(0), frameCards.nth(1));
await enterPresentationMode(page);
await waitNextFrame(page, 100);
// Check if frame contains note now is the first
const edgelessNote = page.locator(
'affine-edgeless-root affine-edgeless-note'
);
await expect(edgelessNote).toBeVisible();
});
});

View File

@@ -0,0 +1,93 @@
import { expect } from '@playwright/test';
import {
dragBetweenIndices,
enterPlaygroundRoom,
focusRichText,
getPageSnapshot,
initEmptyCodeBlockState,
initEmptyParagraphState,
initThreeParagraphs,
resetHistory,
type,
undoByClick,
} from '../utils/actions/index.js';
import { assertRichTexts } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('should bracket complete works', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '([{');
// type without selection should not trigger bracket complete
await assertRichTexts(page, ['([{']);
await dragBetweenIndices(page, [0, 1], [0, 2]);
await type(page, '(');
await assertRichTexts(page, ['(([){']);
await type(page, ')');
// Should not trigger bracket complete when type right bracket
await assertRichTexts(page, ['(()){']);
});
test('bracket complete should not work when selecting mutiple lines', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
// 1(23 45)6 789
await dragBetweenIndices(page, [0, 1], [1, 2]);
await type(page, '(');
await assertRichTexts(page, ['1(6', '789']);
});
test('should bracket complete with backtick works', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello world');
await dragBetweenIndices(page, [0, 2], [0, 5]);
await resetHistory(page);
await type(page, '`');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
await undoByClick(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_undo.json`
);
});
test('auto delete bracket right', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, '(');
await assertRichTexts(page, ['()']);
await type(page, '(');
await assertRichTexts(page, ['(())']);
await page.keyboard.press('Backspace');
await assertRichTexts(page, ['()']);
await page.keyboard.press('Backspace');
await assertRichTexts(page, ['']);
});
test('skip redundant right bracket', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
await type(page, '(');
await assertRichTexts(page, ['()']);
await type(page, ')');
await assertRichTexts(page, ['()']);
await type(page, ')');
await assertRichTexts(page, ['())']);
});

View File

@@ -0,0 +1,474 @@
import { expect } from '@playwright/test';
import {
dragBetweenIndices,
enterPlaygroundRoom,
focusRichText,
getPageSnapshot,
initEmptyParagraphState,
initThreeParagraphs,
inlineCode,
MODIFIER_KEY,
pressArrowDown,
pressArrowLeft,
pressArrowRight,
pressArrowUp,
pressEnter,
pressForwardDelete,
pressShiftTab,
pressTab,
readClipboardText,
redoByClick,
redoByKeyboard,
resetHistory,
setInlineRangeInSelectedRichText,
SHIFT_KEY,
SHORT_KEY,
strikethrough,
type,
undoByClick,
undoByKeyboard,
updateBlockType,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertBlockChildrenIds,
assertRichTextInlineRange,
assertRichTextModelType,
assertRichTexts,
assertTextFormat,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('rich-text hotkey scope on single press', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await pressEnter(page);
await type(page, 'world');
await assertRichTexts(page, ['hello', 'world']);
await dragBetweenIndices(page, [0, 0], [1, 5]);
await page.keyboard.press('Backspace');
await assertRichTexts(page, ['']);
});
test('single line rich-text inline code hotkey', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await dragBetweenIndices(page, [0, 0], [0, 5]);
await inlineCode(page);
await assertTextFormat(page, 0, 5, { code: true });
// undo
await undoByKeyboard(page);
await assertTextFormat(page, 0, 5, {});
// redo
await redoByKeyboard(page);
await waitNextFrame(page);
await assertTextFormat(page, 0, 5, { code: true });
// the format should be removed after trigger the hotkey again
await inlineCode(page);
await assertTextFormat(page, 0, 5, {});
});
test('type character jump out code node', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'Hello');
await setInlineRangeInSelectedRichText(page, 0, 5);
await inlineCode(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
await focusRichText(page);
await page.keyboard.press(`${SHORT_KEY}+ArrowRight`);
await type(page, 'block suite');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
});
test('single line rich-text strikethrough hotkey', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await dragBetweenIndices(page, [0, 0], [0, 5]);
await strikethrough(page);
await assertTextFormat(page, 0, 5, { strike: true });
await undoByClick(page);
await assertTextFormat(page, 0, 5, {});
await redoByClick(page);
await assertTextFormat(page, 0, 5, { strike: true });
await waitNextFrame(page);
// the format should be removed after trigger the hotkey again
await strikethrough(page);
await assertTextFormat(page, 0, 5, {});
});
test('use formatted cursor with hotkey', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa');
// format italic
await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 });
await type(page, 'bbb');
// format bold
await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 });
await type(page, 'ccc');
// unformat italic
await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 });
await type(page, 'ddd');
// unformat bold
await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 });
await type(page, 'eee');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
// format bold
await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 });
await type(page, 'fff');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_bold.json`
);
await pressArrowLeft(page);
await pressArrowRight(page);
await type(page, 'ggg');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_bold_ggg.json`
);
await setInlineRangeInSelectedRichText(page, 3, 0);
await waitNextFrame(page);
await type(page, 'hhh');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_bold_hhh.json`
);
});
test('use formatted cursor with hotkey at empty line', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
// format bold
await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 });
await type(page, 'aaa');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_bold.json`
);
});
test('should single line format hotkey work', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await dragBetweenIndices(page, [0, 1], [0, 4]);
// bold
await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 });
// italic
await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 });
// underline
await page.keyboard.press(`${SHORT_KEY}+u`, { delay: 50 });
// strikethrough
await page.keyboard.press(`${SHORT_KEY}+Shift+s`, { delay: 50 });
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
// bold
await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 });
// italic
await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 });
// underline
await page.keyboard.press(`${SHORT_KEY}+u`, { delay: 50 });
// strikethrough
await page.keyboard.press(`${SHORT_KEY}+Shift+s`, { delay: 50 });
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});
test('should hotkey work in paragraph', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, 'hello');
// XXX wait for group to be updated
await page.waitForTimeout(10);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+1`);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+6`);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_press_6.json`
);
await page.waitForTimeout(50);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+8`);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_press_8.json`
);
await page.waitForTimeout(50);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+9`);
await waitNextFrame(page, 200);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_press_9.json`
);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+0`);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_press_0.json`
);
await page.waitForTimeout(50);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+d`);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_press_d.json`
);
});
test('format list to h1', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await updateBlockType(page, 'affine:list', 'bulleted');
await type(page, 'aa');
await focusRichText(page, 0);
await updateBlockType(page, 'affine:paragraph', 'h1');
await assertRichTextModelType(page, 'h1');
await undoByClick(page);
await assertRichTextModelType(page, 'bulleted');
await redoByClick(page);
await assertRichTextModelType(page, 'h1');
});
test('should cut work single line', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await resetHistory(page);
await dragBetweenIndices(page, [0, 1], [0, 4]);
// cut
await page.keyboard.press(`${SHORT_KEY}+x`);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await undoByKeyboard(page);
const text = await readClipboardText(page);
expect(text).toBe('ell');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_undo.json`
);
});
test('should ctrl+enter create new block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '123');
await pressArrowLeft(page, 2);
await pressEnter(page);
await waitNextFrame(page);
await assertRichTexts(page, ['1', '23']);
await page.keyboard.press(`${SHORT_KEY}+Enter`);
await assertRichTexts(page, ['1', '23', '']);
});
test('should left/right key navigator works', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await focusRichText(page, 0);
await assertRichTextInlineRange(page, 0, 3);
await page.keyboard.press(`${SHORT_KEY}+ArrowLeft`, { delay: 50 });
await assertRichTextInlineRange(page, 0, 0);
await pressArrowLeft(page);
await assertRichTextInlineRange(page, 0, 0);
await page.keyboard.press(`${SHORT_KEY}+ArrowRight`, { delay: 50 });
await assertRichTextInlineRange(page, 0, 3);
await pressArrowRight(page);
await assertRichTextInlineRange(page, 1, 0);
await pressArrowLeft(page);
await assertRichTextInlineRange(page, 0, 3);
await pressArrowRight(page, 4);
await assertRichTextInlineRange(page, 1, 3);
await pressArrowRight(page);
await assertRichTextInlineRange(page, 2, 0);
await pressArrowLeft(page);
await assertRichTextInlineRange(page, 1, 3);
});
test('should up/down key navigator works', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await focusRichText(page, 0);
await assertRichTextInlineRange(page, 0, 3);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 1, 3);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 2, 3);
await page.keyboard.press(`${SHORT_KEY}+ArrowLeft`, { delay: 50 });
await assertRichTextInlineRange(page, 2, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 1, 0);
await pressArrowRight(page);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 0, 1);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 1, 1);
});
test('should support ctrl/cmd+shift+l convert to linked doc', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await dragBetweenIndices(
page,
[2, 3],
[0, 0],
{ x: 20, y: 20 },
{ x: 0, y: 0 }
);
await waitNextFrame(page);
await page.keyboard.press(`${SHORT_KEY}+${SHIFT_KEY}+l`);
const linkedDocCard = page.locator('affine-embed-linked-doc-block');
await expect(linkedDocCard).toBeVisible();
const title = page.locator('.affine-embed-linked-doc-content-title-text');
expect(await title.innerText()).toBe('Untitled');
const noteContent = page.locator('.affine-embed-linked-doc-content-note');
expect(await noteContent.innerText()).toBe('123');
});
test('should forwardDelete works when delete single character', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, 'hello');
await pressArrowLeft(page, 5);
await pressForwardDelete(page);
await assertRichTexts(page, ['ello']);
});
test.describe('keyboard operation to move block up or down', () => {
test('common paragraph', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await pressEnter(page);
await type(page, 'world');
await pressEnter(page);
await type(page, 'foo');
await pressEnter(page);
await type(page, 'bar');
await assertRichTexts(page, ['hello', 'world', 'foo', 'bar']);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowUp`);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowUp`);
await assertRichTexts(page, ['hello', 'bar', 'world', 'foo']);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowDown`);
await assertRichTexts(page, ['hello', 'world', 'bar', 'foo']);
});
test('with indent', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await pressEnter(page);
await pressTab(page);
await waitNextFrame(page);
await type(page, 'world');
await pressEnter(page);
await pressShiftTab(page);
await waitNextFrame(page);
await type(page, 'foo');
await assertRichTexts(page, ['hello', 'world', 'foo']);
await assertBlockChildrenIds(page, '2', ['3']);
await pressArrowUp(page, 2);
await waitNextFrame(page);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowDown`);
await waitNextFrame(page);
await assertRichTexts(page, ['foo', 'hello', 'world']);
await assertBlockChildrenIds(page, '1', ['4', '2']);
await assertBlockChildrenIds(page, '2', ['3']);
});
test('keep cursor', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await pressEnter(page);
await type(page, 'world');
await pressEnter(page);
await type(page, 'foo');
await assertRichTexts(page, ['hello', 'world', 'foo']);
await assertRichTextInlineRange(page, 2, 3);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowUp`);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowUp`);
await assertRichTextInlineRange(page, 0, 3);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowDown`);
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+ArrowDown`);
await assertRichTextInlineRange(page, 2, 3);
});
});
test('Enter key should as expected after setting heading by shortkey', async ({
page,
}, testInfo) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/4987',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await page.keyboard.press(`${SHORT_KEY}+${MODIFIER_KEY}+1`);
await pressEnter(page);
await type(page, 'world');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});

View File

@@ -0,0 +1,176 @@
import { expect } from '@playwright/test';
import {
dragBetweenIndices,
enterPlaygroundRoom,
focusRichText,
getPageSnapshot,
initEmptyParagraphState,
initThreeParagraphs,
inlineCode,
pressArrowLeft,
pressArrowUp,
pressEnter,
pressForwardDelete,
pressShiftEnter,
readClipboardText,
redoByClick,
resetHistory,
setInlineRangeInSelectedRichText,
SHORT_KEY,
type,
undoByClick,
undoByKeyboard,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertBlockSelections,
assertRichTextInlineRange,
assertRichTexts,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('should multiple line format hotkey work', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
// 0 1 2
// 1|23 456 78|9
await dragBetweenIndices(page, [0, 1], [2, 2]);
// bold
await page.keyboard.press(`${SHORT_KEY}+b`);
// italic
await page.keyboard.press(`${SHORT_KEY}+i`);
// underline
await page.keyboard.press(`${SHORT_KEY}+u`);
// strikethrough
await page.keyboard.press(`${SHORT_KEY}+Shift+S`);
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
// bold
await page.keyboard.press(`${SHORT_KEY}+b`, { delay: 50 });
// italic
await page.keyboard.press(`${SHORT_KEY}+i`, { delay: 50 });
// underline
await page.keyboard.press(`${SHORT_KEY}+u`, { delay: 50 });
// strikethrough
await page.keyboard.press(`${SHORT_KEY}+Shift+s`, { delay: 50 });
await waitNextFrame(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});
test('multi line rich-text inline code hotkey', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
// 0 1 2
// 1|23 456 78|9
await dragBetweenIndices(page, [0, 1], [2, 2]);
await inlineCode(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await undoByClick(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_undo.json`
);
await redoByClick(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_redo.json`
);
});
test('should cut work multiple line', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await resetHistory(page);
// 0 1 2
// 1|23 456 78|9
await dragBetweenIndices(page, [0, 1], [2, 2]);
// cut
await page.keyboard.press(`${SHORT_KEY}+x`);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await undoByKeyboard(page);
const text = await readClipboardText(page);
expect(text).toBe(`23 456 78`);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_undo.json`
);
});
test('arrow up and down behavior on multiline text blocks when previous is non-text', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await pressEnter(page);
await pressArrowUp(page);
await type(page, '--- ');
await pressEnter(page);
await focusRichText(page);
await type(page, '124');
await pressShiftEnter(page);
await type(page, '1234');
await pressArrowUp(page);
await waitNextFrame(page, 100);
await assertRichTextInlineRange(page, 0, 3);
await pressArrowUp(page);
await assertBlockSelections(page, ['4']);
});
test('should forwardDelete works when delete multi characters', async ({
page,
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/3122',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, 'hello');
await pressArrowLeft(page, 5);
await setInlineRangeInSelectedRichText(page, 1, 3);
await pressForwardDelete(page);
await assertRichTexts(page, ['ho']);
});
test('should drag multiple block and input text works', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2982',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await dragBetweenIndices(page, [0, 1], [2, 1]);
await type(page, 'ab');
await assertRichTexts(page, ['1ab89']);
await undoByKeyboard(page);
await assertRichTexts(page, ['123', '456', '789']);
});

View File

@@ -0,0 +1,43 @@
import {
cutByKeyboard,
dragOverTitle,
enterPlaygroundRoom,
focusRichText,
focusTitle,
initEmptyParagraphState,
pasteByKeyboard,
pressEnter,
type,
} from '../utils/actions/index.js';
import { assertRichTexts, assertTitle } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('should cut in title works', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusTitle(page);
await type(page, 'hello');
await assertTitle(page, 'hello');
await dragOverTitle(page);
await cutByKeyboard(page);
await assertTitle(page, '');
await focusRichText(page);
await pasteByKeyboard(page);
await assertRichTexts(page, ['hello']);
});
test('enter in title should move cursor in new paragraph block', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusTitle(page);
await type(page, 'hello');
await assertTitle(page, 'hello');
await pressEnter(page);
await type(page, 'world');
await assertRichTexts(page, ['world', '']);
});

View File

@@ -0,0 +1,154 @@
import '../utils/declare-test-window.js';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import {
activeEmbed,
copyByKeyboard,
dragEmbedResizeByTopLeft,
dragEmbedResizeByTopRight,
enterPlaygroundRoom,
initImageState,
moveToImage,
pasteByKeyboard,
pressArrowLeft,
pressEnter,
redoByClick,
redoByKeyboard,
type,
undoByKeyboard,
waitNextFrame,
} from '../utils/actions/index.js';
import {
assertImageOption,
assertImageSize,
assertRichDragButton,
assertRichImage,
assertRichTextInlineRange,
assertRichTexts,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
async function focusCaption(page: Page) {
await page.click(
'.affine-image-toolbar-container .image-toolbar-button.caption'
);
}
test('can drag resize image by left menu', async ({ page }) => {
await enterPlaygroundRoom(page);
await initImageState(page);
await assertRichImage(page, 1);
await activeEmbed(page);
await assertRichDragButton(page);
await assertImageSize(page, { width: 752, height: 564 });
await dragEmbedResizeByTopLeft(page);
await waitNextFrame(page);
await assertImageSize(page, { width: 358, height: 268 });
await undoByKeyboard(page);
await waitNextFrame(page);
await assertImageSize(page, { width: 752, height: 564 });
await redoByKeyboard(page);
await waitNextFrame(page);
await assertImageSize(page, { width: 358, height: 268 });
});
test('can drag resize image by right menu', async ({ page }) => {
await enterPlaygroundRoom(page);
await initImageState(page);
await assertRichImage(page, 1);
await activeEmbed(page);
await assertRichDragButton(page);
await assertImageSize(page, { width: 752, height: 564 });
await dragEmbedResizeByTopRight(page);
await assertImageSize(page, { width: 338, height: 253 });
await undoByKeyboard(page);
await assertImageSize(page, { width: 752, height: 564 });
await redoByKeyboard(page);
await assertImageSize(page, { width: 338, height: 253 });
});
test('can click and delete image', async ({ page }) => {
await enterPlaygroundRoom(page);
await initImageState(page);
await assertRichImage(page, 1);
await activeEmbed(page);
await page.keyboard.press('Backspace');
await assertRichImage(page, 0);
await undoByKeyboard(page);
await assertRichImage(page, 1);
await redoByClick(page);
await assertRichImage(page, 0);
});
test('can click and copy image', async ({ page }) => {
await enterPlaygroundRoom(page);
await initImageState(page);
await assertRichImage(page, 1);
await activeEmbed(page);
await copyByKeyboard(page);
await pressEnter(page);
await waitNextFrame(page);
await pasteByKeyboard(page);
await waitNextFrame(page, 200);
await assertRichImage(page, 2);
});
test('enter shortcut on focusing embed block and its caption', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initImageState(page);
await assertRichImage(page, 1);
await moveToImage(page);
await assertImageOption(page);
const caption = page.locator('affine-image block-caption-editor textarea');
await focusCaption(page);
await type(page, '123');
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2495',
});
// blur
await page.mouse.click(0, 500);
await caption.click({ position: { x: 0, y: 0 } });
await type(page, 'abc');
await expect(caption).toHaveValue('abc123');
});
test('should support the enter key of image caption', async ({ page }) => {
await enterPlaygroundRoom(page);
await initImageState(page);
await assertRichImage(page, 1);
await moveToImage(page);
await assertImageOption(page);
const caption = page.locator('affine-image block-caption-editor textarea');
await focusCaption(page);
await type(page, 'abc123');
await pressArrowLeft(page, 3);
await pressEnter(page);
await expect(caption).toHaveValue('abc');
await assertRichTexts(page, ['123']);
await assertRichTextInlineRange(page, 0, 0, 0);
});

View File

@@ -0,0 +1,79 @@
import { expect } from '@playwright/test';
import {
activeEmbed,
enterPlaygroundRoom,
initImageState,
pressArrowDown,
pressArrowUp,
pressBackspace,
pressEnter,
type,
} from '../utils/actions/index.js';
import {
assertBlockCount,
assertBlockSelections,
assertRichImage,
assertRichTextInlineRange,
assertRichTexts,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test.beforeEach(async ({ page }) => {
await enterPlaygroundRoom(page);
await initImageState(page, true);
await assertRichImage(page, 1);
});
test('press enter will create new block when click and select image', async ({
page,
}) => {
await activeEmbed(page);
await pressEnter(page);
await type(page, 'aa');
await assertRichTexts(page, ['', 'aa']);
});
test('press backspace after image block can select image block', async ({
page,
}) => {
await activeEmbed(page);
await pressEnter(page);
await assertRichTextInlineRange(page, 1, 0);
await assertBlockCount(page, 'paragraph', 2);
await pressBackspace(page);
await assertBlockSelections(page, ['3']);
await assertBlockCount(page, 'paragraph', 1);
});
test('press enter when image is selected should move next paragraph and should placeholder', async ({
page,
}) => {
await activeEmbed(page);
await pressEnter(page);
const placeholder = page.locator('.affine-paragraph-placeholder.visible');
await expect(placeholder).toBeVisible();
});
test('press arrow up when image is selected should move to previous paragraph', async ({
page,
}) => {
await activeEmbed(page);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 0, 0);
await type(page, 'aa');
await assertRichTexts(page, ['aa']);
});
test('press arrow down when image is selected should move to previous paragraph', async ({
page,
}) => {
await activeEmbed(page);
await pressEnter(page);
await type(page, 'aa');
await activeEmbed(page);
await pressArrowDown(page);
await type(page, 'bb');
await assertRichTexts(page, ['', 'bbaa']);
});

View File

@@ -0,0 +1,168 @@
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import {
enterPlaygroundRoom,
expectConsoleMessage,
} from '../utils/actions/index.js';
import { test } from '../utils/playwright.js';
const mockImageId = '_e2e_test_image_id_';
async function initMockImage(page: Page) {
await page.evaluate(() => {
const { doc } = window;
doc.captureSync();
const rootId = doc.addBlock('affine:page');
const noteId = doc.addBlock('affine:note', {}, rootId);
doc.addBlock(
'affine:image',
{
sourceId: '_e2e_test_image_id_',
width: 200,
height: 180,
},
noteId
);
doc.captureSync();
});
}
test('image loading but failed', async ({ page }) => {
expectConsoleMessage(
page,
'Error: Failed to fetch blob _e2e_test_image_id_',
'warning'
);
expectConsoleMessage(
page,
'Failed to load resource: the server responded with a status of 404 (Not Found)'
);
expectConsoleMessage(
page,
'Error: Image blob is missing!, retrying',
'warning'
);
const room = await enterPlaygroundRoom(page, { blobSource: ['mock'] });
const timeout = 2000;
// block image data request, force wait 100ms for loading test,
// always return 404
await page.route(
`**/api/collection/${room}/blob/${mockImageId}`,
async route => {
await page.waitForTimeout(timeout);
// broken image
return route.fulfill({
status: 404,
});
}
);
await initMockImage(page);
const loadingContent = await page
.locator(
'.affine-image-fallback-card .affine-image-fallback-card-title-text'
)
.innerText();
expect(loadingContent).toBe('Loading image...');
await page.waitForTimeout(3 * timeout);
await expect(
page.locator(
'.affine-image-fallback-card .affine-image-fallback-card-title-text'
)
).toContainText('Image loading failed.');
});
test('image loading but success', async ({ page }) => {
expectConsoleMessage(
page,
'Error: Failed to fetch blob _e2e_test_image_id_',
'warning'
);
expectConsoleMessage(
page,
'Failed to load resource: the server responded with a status of 404 (Not Found)'
);
expectConsoleMessage(
page,
'Error: Image blob is missing!, retrying',
'warning'
);
const room = await enterPlaygroundRoom(page, { blobSource: ['mock'] });
const imageBuffer = await readFile(
fileURLToPath(new URL('../fixtures/smile.png', import.meta.url))
);
const timeout = 2000;
let count = 0;
// block image data request, force wait 100ms for loading test,
// always return 404
await page.route(
`**/api/collection/${room}/blob/${mockImageId}`,
async route => {
await page.waitForTimeout(timeout);
count++;
if (count === 3) {
return route.fulfill({
status: 200,
body: imageBuffer,
});
}
// broken image
return route.fulfill({
status: 404,
});
}
);
await initMockImage(page);
const loadingContent = await page
.locator(
'.affine-image-fallback-card .affine-image-fallback-card-title-text'
)
.innerText();
expect(loadingContent).toBe('Loading image...');
await page.waitForTimeout(3 * timeout);
const img = page.locator('.affine-image-container img');
await expect(img).toBeVisible();
const src = await img.getAttribute('src');
expect(src).toBeDefined();
});
test('image loaded successfully', async ({ page }) => {
const room = await enterPlaygroundRoom(page, { blobSource: ['mock'] });
const imageBuffer = await readFile(
fileURLToPath(new URL('../fixtures/smile.png', import.meta.url))
);
await page.route(
`**/api/collection/${room}/blob/${mockImageId}`,
async route => {
return route.fulfill({
status: 200,
body: imageBuffer,
});
}
);
await initMockImage(page);
await page.waitForTimeout(1000);
const img = page.locator('.affine-image-container img');
await expect(img).toBeVisible();
const src = await img.getAttribute('src');
expect(src).toBeDefined();
});

View File

@@ -0,0 +1,90 @@
import { expect } from '@playwright/test';
import {
activeEmbed,
dragBetweenCoords,
enterPlaygroundRoom,
initImageState,
insertThreeLevelLists,
pressEnter,
scrollToTop,
} from '../utils/actions/index.js';
import { assertRichImage } from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
// FIXME(@fundon): This behavior is not meeting the design spec
test.skip('popup menu should follow position of image when scrolling', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initImageState(page);
await activeEmbed(page);
await pressEnter(page);
await insertThreeLevelLists(page, 0);
await pressEnter(page);
await insertThreeLevelLists(page, 3);
await pressEnter(page);
await insertThreeLevelLists(page, 6);
await pressEnter(page);
await insertThreeLevelLists(page, 9);
await pressEnter(page);
await insertThreeLevelLists(page, 12);
await scrollToTop(page);
const rect = await page.locator('.affine-image-container img').boundingBox();
if (!rect) throw new Error('image not found');
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2);
await page.waitForTimeout(150);
const menu = page.locator('.affine-image-toolbar-container');
await expect(menu).toBeVisible();
await page.evaluate(
([rect]) => {
const viewport = document.querySelector('.affine-page-viewport');
if (!viewport) {
throw new Error();
}
// const distance = viewport.scrollHeight - viewport.clientHeight;
viewport.scrollTo(0, (rect.height + rect.y) / 2);
},
[rect]
);
await page.waitForTimeout(150);
const image = page.locator('.affine-image-container img');
const imageRect = await image.boundingBox();
const menuRect = await menu.boundingBox();
if (!imageRect) throw new Error('image not found');
if (!menuRect) throw new Error('menu not found');
expect(imageRect.y).toBeCloseTo((rect.y - rect.height) / 2, 172);
expect(menuRect.y).toBeCloseTo(65, -0.325);
});
test('select image should not show format bar', async ({ page }) => {
await enterPlaygroundRoom(page);
await initImageState(page);
await assertRichImage(page, 1);
const image = page.locator('affine-image');
const rect = await image.boundingBox();
if (!rect) {
throw new Error('image not found');
}
await dragBetweenCoords(
page,
{ x: rect.x - 20, y: rect.y + 20 },
{ x: rect.x + 20, y: rect.y + 40 }
);
const rects = page.locator('affine-block-selection').locator('visible=true');
await expect(rects).toHaveCount(1);
const formatQuickBar = page.locator(`.format-quick-bar`);
await expect(formatQuickBar).not.toBeVisible();
await page.mouse.wheel(0, rect.y + rect.height);
await expect(formatQuickBar).not.toBeVisible();
await page.mouse.click(0, 0);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
import { expect } from '@playwright/test';
import {
enterPlaygroundRoom,
focusRichText,
getPageSnapshot,
initEmptyParagraphState,
type,
} from '../utils/actions/index.js';
import { test } from '../utils/playwright.js';
test('add latex block using slash menu', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await type(page, '/eq\naaa');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});
test('add latex block using markdown shortcut with space', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await type(page, '$$$$ aaa');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});
test('add latex block using markdown shortcut with enter', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await type(page, '$$$$\naaa');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});

View File

@@ -0,0 +1,337 @@
import { ZERO_WIDTH_SPACE } from '@blocksuite/inline';
import { expect } from '@playwright/test';
import {
cutByKeyboard,
pasteByKeyboard,
pressArrowLeft,
pressArrowRight,
pressArrowUp,
pressBackspace,
pressBackspaceWithShortKey,
pressEnter,
pressShiftEnter,
redoByKeyboard,
selectAllByKeyboard,
type,
undoByKeyboard,
} from '../utils/actions/keyboard.js';
import {
enterPlaygroundRoom,
focusRichText,
initEmptyParagraphState,
} from '../utils/actions/misc.js';
import {
assertRichTextInlineDeltas,
assertRichTextInlineRange,
} from '../utils/asserts.js';
import { test } from '../utils/playwright.js';
test('add inline latex at the start of line', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const latexEditorLine = page.locator('latex-editor-menu v-line div');
const latexElement = page.locator(
'affine-paragraph rich-text affine-latex-node'
);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
expect(await latexElement.isVisible()).not.toBeTruthy();
await type(page, '$$ ');
expect(await latexEditorLine.isVisible()).toBeTruthy();
expect(await latexElement.isVisible()).toBeTruthy();
expect(await latexElement.locator('.placeholder').innerText()).toBe(
'Equation'
);
await type(page, 'E=mc^2');
expect(await latexEditorLine.innerText()).toBe('E=mc^2');
expect(await latexElement.locator('.katex').innerHTML()).toBe(
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math>'
);
await pressEnter(page);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
expect(await latexElement.locator('.katex').innerHTML()).toBe(
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math>'
);
});
test('add inline latex in the middle of text', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const latexEditorLine = page.locator('latex-editor-menu v-line div');
const latexElement = page.locator(
'affine-paragraph rich-text affine-latex-node'
);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
expect(await latexElement.isVisible()).not.toBeTruthy();
await type(page, 'aaaa');
await pressArrowLeft(page, 2);
await type(page, '$$ ');
expect(await latexEditorLine.isVisible()).toBeTruthy();
expect(await latexElement.isVisible()).toBeTruthy();
expect(await latexElement.locator('.placeholder').innerText()).toBe(
'Equation'
);
await type(page, 'E=mc^2');
expect(await latexEditorLine.innerText()).toBe('E=mc^2');
expect(await latexElement.locator('.katex').innerHTML()).toBe(
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math>'
);
await pressEnter(page);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
expect(await latexElement.locator('.katex').innerHTML()).toBe(
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math>'
);
});
test('update inline latex by clicking the node', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const latexEditorLine = page.locator('latex-editor-menu v-line div');
const latexElement = page.locator(
'affine-paragraph rich-text affine-latex-node'
);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
await type(page, '$$ ');
expect(await latexEditorLine.isVisible()).toBeTruthy();
await type(page, 'E=mc^2');
await pressEnter(page);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
await latexElement.click();
expect(await latexEditorLine.isVisible()).toBeTruthy();
await pressBackspace(page, 6);
await type(page, String.raw`\def\arraystretch{1.5}`);
await pressShiftEnter(page);
await type(page, String.raw`\begin{array}{c:c:c}`);
await pressShiftEnter(page);
await type(page, String.raw`a & b & c \\ \\ hline`);
await pressShiftEnter(page);
await type(page, String.raw`d & e & f \\`);
await pressShiftEnter(page);
await type(page, String.raw`\hdashline`);
await pressShiftEnter(page);
await type(page, String.raw`g & h & i`);
await pressShiftEnter(page);
await type(page, String.raw`\end{array}`);
expect(await latexElement.locator('.katex').innerHTML()).toBe(
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.66em" columnalign="center center center" columnlines="dashed dashed" columnspacing="1em" rowlines="none none dashed"><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mi>a</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi>b</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi>c</mi></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mi>h</mi><mi>l</mi><mi>i</mi><mi>n</mi><mi>e</mi><mi>d</mi></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi>e</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi>f</mi></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mi>g</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi>h</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi>i</mi></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\\def\\arraystretch{1.5}\n\\begin{array}{c:c:c}\na &amp; b &amp; c \\\\ \\\\ hline\nd &amp; e &amp; f \\\\\n\\hdashline\ng &amp; h &amp; i\n\\end{array}</annotation></semantics></math>'
);
// click outside to hide the editor
await page.click('affine-editor-container');
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
});
test('latex editor', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const latexEditorLine = page.locator('latex-editor-menu v-line div');
const latexElement = page.locator(
'affine-paragraph rich-text affine-latex-node'
);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
await type(page, '$$ ');
expect(await latexEditorLine.isVisible()).toBeTruthy();
// test cursor movement works as expected
// https://github.com/toeverything/blocksuite/pull/8368
await type(page, 'ababababababababababababababababababababababababab');
expect(await latexEditorLine.innerText()).toBe(
'ababababababababababababababababababababababababab'
);
// click outside to hide the editor
expect(await latexEditorLine.isVisible()).toBeTruthy();
await page.mouse.click(130, 130);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
await latexElement.click();
expect(await latexEditorLine.isVisible()).toBeTruthy();
expect(await latexEditorLine.innerText()).toBe(
'ababababababababababababababababababababababababab'
);
await pressBackspaceWithShortKey(page, 2);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE);
await undoByKeyboard(page);
expect(await latexEditorLine.innerText()).toBe(
'ababababababababababababababababababababababababab'
);
await redoByKeyboard(page);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE);
await undoByKeyboard(page);
expect(await latexEditorLine.innerText()).toBe(
'ababababababababababababababababababababababababab'
);
// undo-redo
await pressArrowLeft(page, 5);
await page.keyboard.down('Shift');
await pressArrowUp(page);
await pressArrowRight(page);
await page.keyboard.up('Shift');
/**
* abababababababababab|ababab
* abababababababababa|babab
*/
await cutByKeyboard(page);
expect(await latexEditorLine.innerText()).toBe('ababababababababababababab');
/**
* abababababababababab|babab
*/
await pressArrowRight(page, 2);
/**
* ababababababababababba|bab
*/
await pasteByKeyboard(page);
expect(await latexEditorLine.innerText()).toBe(
'ababababababababababababababababababababababababab'
);
await selectAllByKeyboard(page);
await pressBackspace(page);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE);
// highlight
await type(
page,
String.raw`a+\left(\vcenter{\hbox{$\frac{\frac a b}c$}}\right)`
);
expect(
(await latexEditorLine.locator('latex-editor-unit').innerHTML()).replace(
/lit\$\d+\$/g,
'lit$test$'
)
).toBe(
'\x3C!----><span>\x3C!--?lit$test$-->\x3C!----><v-text style="color:#000000;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->a+</span></v-text>\x3C!---->\x3C!----><v-text style="color:#795E26;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->\\left</span></v-text>\x3C!---->\x3C!----><v-text style="color:#000000;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->(</span></v-text>\x3C!---->\x3C!----><v-text style="color:#795E26;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->\\vcenter</span></v-text>\x3C!---->\x3C!----><v-text style="color:#000000;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->{</span></v-text>\x3C!---->\x3C!----><v-text style="color:#795E26;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->\\hbox</span></v-text>\x3C!---->\x3C!----><v-text style="color:#000000;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->{</span></v-text>\x3C!---->\x3C!----><v-text style="color:#267F99;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->$</span></v-text>\x3C!---->\x3C!----><v-text style="color:#267F99;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->\\frac{\\frac a b}c</span></v-text>\x3C!---->\x3C!----><v-text style="color:#267F99;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->$</span></v-text>\x3C!---->\x3C!----><v-text style="color:#000000;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->}}</span></v-text>\x3C!---->\x3C!----><v-text style="color:#795E26;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->\\right</span></v-text>\x3C!---->\x3C!----><v-text style="color:#000000;">\x3C!----><span data-v-text="true" style="word-break:break-word;text-wrap:wrap;white-space-collapse:break-spaces;">\x3C!--?lit$test$-->)</span></v-text>\x3C!----></span>'
);
});
test('add inline latex using slash menu', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const latexEditorLine = page.locator('latex-editor-menu v-line div');
const latexElement = page.locator(
'affine-paragraph rich-text affine-latex-node'
);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
expect(await latexElement.isVisible()).not.toBeTruthy();
await type(page, '/ieq\n');
expect(await latexEditorLine.isVisible()).toBeTruthy();
expect(await latexElement.isVisible()).toBeTruthy();
expect(await latexElement.locator('.placeholder').innerText()).toBe(
'Equation'
);
await type(page, 'E=mc^2');
expect(await latexEditorLine.innerText()).toBe('E=mc^2');
expect(await latexElement.locator('.katex').innerHTML()).toBe(
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math>'
);
await pressEnter(page);
expect(await latexEditorLine.isVisible()).not.toBeTruthy();
expect(await latexElement.locator('.katex').innerHTML()).toBe(
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math>'
);
});
test('add inline latex using markdown shortcut', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
// toggle by space or enter
await type(page, 'aa$$bb$$ cc$$dd$$\n');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: ' ',
attributes: {
latex: 'bb',
},
},
{
insert: 'cc',
},
{
insert: ' ',
attributes: {
latex: 'dd',
},
},
]);
await pressArrowUp(page);
await pressArrowRight(page, 3);
await pressBackspace(page);
await assertRichTextInlineDeltas(page, [
{
insert: 'aacc',
},
{
insert: ' ',
attributes: {
latex: 'dd',
},
},
]);
});
test('undo-redo when add inline latex using markdown shortcut', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aa$$bb$$ ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: ' ',
attributes: {
latex: 'bb',
},
},
]);
await assertRichTextInlineRange(page, 0, 3, 0);
await undoByKeyboard(page);
await assertRichTextInlineDeltas(page, [
{
insert: 'aa$$bb$$ ',
},
]);
await assertRichTextInlineRange(page, 0, 9, 0);
await redoByKeyboard(page);
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: ' ',
attributes: {
latex: 'bb',
},
},
]);
await assertRichTextInlineRange(page, 0, 3, 0);
});

View File

@@ -0,0 +1,421 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import {
cutByKeyboard,
dragBetweenIndices,
enterPlaygroundRoom,
focusRichText,
focusRichTextEnd,
getPageSnapshot,
initEmptyParagraphState,
pasteByKeyboard,
pressEnter,
pressShiftEnter,
pressTab,
selectAllByKeyboard,
setSelection,
SHORT_KEY,
switchReadonly,
type,
waitNextFrame,
} from './utils/actions/index.js';
import { assertKeyboardWorkInInput } from './utils/asserts.js';
import { test } from './utils/playwright.js';
const pressCreateLinkShortCut = async (page: Page) => {
await page.keyboard.press(`${SHORT_KEY}+k`);
};
test('basic link', async ({ page }, testInfo) => {
const linkText = 'linkText';
const link = 'http://example.com';
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, linkText);
// Create link
await dragBetweenIndices(page, [0, 0], [0, 8]);
await pressCreateLinkShortCut(page);
await page.mouse.move(0, 0);
const createLinkPopoverLocator = page.locator('.affine-link-popover.create');
await expect(createLinkPopoverLocator).toBeVisible();
const linkPopoverInput = page.locator('.affine-link-popover-input');
await expect(linkPopoverInput).toBeVisible();
await type(page, link);
await pressEnter(page);
await expect(createLinkPopoverLocator).not.toBeVisible();
const linkLocator = page.locator('affine-link a');
await expect(linkLocator).toHaveAttribute('href', link);
// clear text selection
await page.keyboard.press('ArrowLeft');
const viewLinkPopoverLocator = page.locator('.affine-link-popover.view');
// Hover link
await expect(viewLinkPopoverLocator).not.toBeVisible();
await linkLocator.hover();
// wait for popover delay open
await page.waitForTimeout(200);
await expect(viewLinkPopoverLocator).toBeVisible();
// Edit link
const text2 = 'link2';
const link2 = 'https://github.com';
const editLinkBtn = viewLinkPopoverLocator.getByTestId('edit');
await editLinkBtn.click();
const editLinkPopoverLocator = page.locator('.affine-link-edit-popover');
await expect(editLinkPopoverLocator).toBeVisible();
// workaround to make tab key work as expected
await editLinkPopoverLocator.click({
position: { x: 5, y: 5 },
});
await page.keyboard.press('Tab');
await type(page, text2);
await page.keyboard.press('Tab');
await type(page, link2);
await page.keyboard.press('Tab');
await pressEnter(page);
const link2Locator = page.locator('affine-link a');
await expect(link2Locator).toHaveAttribute('href', link2);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
test('add link when dragging from empty line', async ({ page }) => {
const linkText = 'linkText\n\n';
const link = 'http://example.com';
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, linkText);
// Create link
await dragBetweenIndices(page, [2, 0], [0, 0], {
x: 1,
y: 2,
});
await pressCreateLinkShortCut(page);
await page.mouse.move(0, 0);
const createLinkPopoverLocator = page.locator('.affine-link-popover.create');
await expect(createLinkPopoverLocator).toBeVisible();
const linkPopoverInput = page.locator('.affine-link-popover-input');
await expect(linkPopoverInput).toBeVisible();
await type(page, link);
await pressEnter(page);
await expect(createLinkPopoverLocator).not.toBeVisible();
const linkLocator = page.locator('affine-link a');
await expect(linkLocator).toHaveAttribute('href', link);
});
async function createLinkBlock(page: Page, str: string, link: string) {
const id = await page.evaluate(
([str, link]) => {
const { doc } = window;
const rootId = doc.addBlock('affine:page', {
title: new window.$blocksuite.store.Text('title'),
});
const noteId = doc.addBlock('affine:note', {}, rootId);
const text = new window.$blocksuite.store.Text([
{ insert: 'Hello' },
{ insert: str, attributes: { link } },
]);
const id = doc.addBlock(
'affine:paragraph',
{ type: 'text', text: text },
noteId
);
return id;
},
[str, link]
);
return id;
}
test('type character in link should not jump out link node', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await createLinkBlock(page, 'link text', 'http://example.com');
await focusRichText(page, 0);
await page.keyboard.press('ArrowLeft');
await type(page, 'IN_LINK');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
test('type character after link should not extend the link attributes', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await createLinkBlock(page, 'link text', 'http://example.com');
await focusRichText(page, 0);
await type(page, 'AFTER_LINK');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
test('readonly mode should not trigger link popup', async ({ page }) => {
await enterPlaygroundRoom(page);
const linkText = 'linkText';
await createLinkBlock(page, 'linkText', 'http://example.com');
await focusRichText(page, 0);
const linkLocator = page.locator(`text="${linkText}"`);
// Hover link
const linkPopoverLocator = page.locator('.affine-link-popover');
await linkLocator.hover();
await expect(linkPopoverLocator).toBeVisible();
await switchReadonly(page);
await page.mouse.move(0, 0);
// XXX Wait for readonly delay
await page.waitForTimeout(300);
await linkLocator.hover();
await expect(linkPopoverLocator).not.toBeVisible();
// ---
// press hotkey should not trigger create link popup
await dragBetweenIndices(page, [0, 0], [0, 3]);
await pressCreateLinkShortCut(page);
await expect(linkPopoverLocator).not.toBeVisible();
const linkPopoverInput = page.locator('.affine-link-popover-input');
await expect(linkPopoverInput).not.toBeVisible();
});
test('should mock selection not stored', async ({ page }, testInfo) => {
const linkText = 'linkText';
const link = 'http://example.com';
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, linkText);
// Create link
await dragBetweenIndices(page, [0, 0], [0, 8]);
await pressCreateLinkShortCut(page);
const mockSelectNode = page.locator('.mock-selection');
await expect(mockSelectNode).toHaveCount(1);
await expect(mockSelectNode).toBeVisible();
// the mock select node should not be stored in the Y doc
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
await type(page, link);
await pressEnter(page);
// the mock select node should be removed after link created
await expect(mockSelectNode).not.toBeVisible();
await expect(mockSelectNode).toHaveCount(0);
});
test('should keyboard work in link popover', async ({ page }) => {
await enterPlaygroundRoom(page);
const linkText = 'linkText';
await createLinkBlock(page, linkText, 'http://example.com');
await dragBetweenIndices(page, [0, 0], [0, 8]);
await pressCreateLinkShortCut(page);
const linkPopoverInput = page.locator('.affine-link-popover-input');
await assertKeyboardWorkInInput(page, linkPopoverInput);
await page.mouse.click(500, 500);
const linkLocator = page.locator(`text="${linkText}"`);
const linkPopover = page.locator('.affine-link-popover');
await linkLocator.hover();
await waitNextFrame(page, 200);
await expect(linkLocator).toBeVisible();
// Hover link
await linkLocator.hover();
// wait for popover delay open
await page.waitForTimeout(200);
await expect(linkPopover).toBeVisible();
const editLinkBtn = linkPopover.getByTestId('edit');
await editLinkBtn.click();
const editLinkPopover = page.locator('.affine-link-edit-popover');
await expect(editLinkPopover).toBeVisible();
const editTextInput = editLinkPopover.locator(
'.affine-edit-area.text .affine-edit-input'
);
await assertKeyboardWorkInInput(page, editTextInput);
const editLinkInput = editLinkPopover.locator(
'.affine-edit-area.link .affine-edit-input'
);
await assertKeyboardWorkInInput(page, editLinkInput);
});
test('link bar should not be appear when the range is collapsed', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa');
await pressCreateLinkShortCut(page);
const linkPopoverLocator = page.locator('.affine-link-popover');
await expect(linkPopoverLocator).not.toBeVisible();
await dragBetweenIndices(page, [0, 0], [0, 3]);
await pressCreateLinkShortCut(page);
await expect(linkPopoverLocator).toBeVisible();
await focusRichText(page); // click to cancel the link popover
await focusRichTextEnd(page);
await pressShiftEnter(page);
await waitNextFrame(page);
await type(page, 'bbb');
await dragBetweenIndices(page, [0, 1], [0, 5]);
await pressCreateLinkShortCut(page);
await expect(linkPopoverLocator).toBeVisible();
await focusRichTextEnd(page);
await pressEnter(page);
// create auto line-break in span element
await type(page, 'd'.repeat(67));
await page.mouse.click(1, 1);
await waitNextFrame(page);
await dragBetweenIndices(page, [1, 1], [1, 66]);
await pressCreateLinkShortCut(page);
await expect(linkPopoverLocator).toBeVisible();
});
test('create link with paste', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa');
await dragBetweenIndices(page, [0, 0], [0, 3]);
await pressCreateLinkShortCut(page);
const createLinkPopoverLocator = page.locator('.affine-link-popover.create');
const confirmBtn = createLinkPopoverLocator.locator('.affine-confirm-button');
await expect(createLinkPopoverLocator).toBeVisible();
await expect(confirmBtn).toHaveAttribute('disabled');
await type(page, 'affine.pro');
await expect(confirmBtn).not.toHaveAttribute('disabled');
await selectAllByKeyboard(page);
await cutByKeyboard(page);
// press enter should not trigger confirm
await pressEnter(page);
await expect(createLinkPopoverLocator).toBeVisible();
await expect(confirmBtn).toHaveAttribute('disabled');
await pasteByKeyboard(page, false);
await expect(confirmBtn).not.toHaveAttribute('disabled');
await pressEnter(page);
await expect(createLinkPopoverLocator).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
test('convert link to card', async ({ page }, testInfo) => {
const linkText = 'alinkTexta';
const link = 'http://example.com';
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa');
await pressEnter(page);
await type(page, linkText);
// Create link
await setSelection(page, 3, 1, 3, 9);
await pressCreateLinkShortCut(page);
await waitNextFrame(page);
const linkPopoverLocator = page.locator('.affine-link-popover');
await expect(linkPopoverLocator).toBeVisible();
const linkPopoverInput = page.locator('.affine-link-popover-input');
await expect(linkPopoverInput).toBeVisible();
await type(page, link);
await pressEnter(page);
await expect(linkPopoverLocator).not.toBeVisible();
await focusRichText(page, 1);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
const linkLocator = page.locator('affine-link a');
await linkLocator.hover();
await waitNextFrame(page);
await expect(linkPopoverLocator).toBeVisible();
await page.getByRole('button', { name: 'Switch view' }).click();
const linkToCardBtn = page.getByTestId('link-to-card');
const linkToEmbedBtn = page.getByTestId('link-to-embed');
await expect(linkToCardBtn).toBeVisible();
await expect(linkToEmbedBtn).not.toBeVisible();
await page.mouse.move(0, 0);
await waitNextFrame(page);
await expect(linkPopoverLocator).not.toBeVisible();
await focusRichText(page, 1);
await pressTab(page);
await linkLocator.hover();
await waitNextFrame(page);
await expect(linkPopoverLocator).toBeVisible();
await page.getByRole('button', { name: 'Switch view' }).click();
await expect(linkToCardBtn).toBeVisible();
await expect(linkToEmbedBtn).not.toBeVisible();
});
test('convert link to embed', async ({ page }, testInfo) => {
const linkText = 'alinkTexta';
const link = 'https://www.youtube.com/watch?v=U6s2pdxebSo';
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa');
await pressEnter(page);
await type(page, linkText);
// Create link
await setSelection(page, 3, 1, 3, 9);
await pressCreateLinkShortCut(page);
await waitNextFrame(page);
const linkPopoverLocator = page.locator('.affine-link-popover');
await expect(linkPopoverLocator).toBeVisible();
const linkPopoverInput = page.locator('.affine-link-popover-input');
await expect(linkPopoverInput).toBeVisible();
await type(page, link);
await pressEnter(page);
await expect(linkPopoverLocator).not.toBeVisible();
await focusRichText(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
const linkLocator = page.locator('affine-link a');
await linkLocator.hover();
await waitNextFrame(page);
await expect(linkPopoverLocator).toBeVisible();
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,842 @@
import { expect, type Locator } from '@playwright/test';
import {
dragBetweenIndices,
enterPlaygroundRoom,
enterPlaygroundWithList,
focusRichText,
getPageSnapshot,
initEmptyEdgelessState,
initEmptyParagraphState,
initThreeLists,
pressArrowLeft,
pressArrowUp,
pressBackspace,
pressBackspaceWithShortKey,
pressEnter,
pressShiftEnter,
pressShiftTab,
pressSpace,
pressTab,
redoByClick,
switchEditorMode,
switchReadonly,
type,
undoByClick,
undoByKeyboard,
updateBlockType,
waitNextFrame,
} from './utils/actions/index.js';
import {
assertBlockChildrenFlavours,
assertBlockChildrenIds,
assertBlockCount,
assertBlockType,
assertListPrefix,
assertRichTextInlineRange,
assertRichTexts,
assertTextContent,
} from './utils/asserts.js';
import { test } from './utils/playwright.js';
import { getFormatBar } from './utils/query.js';
async function isToggleIconVisible(toggleIcon: Locator) {
const connected = await toggleIcon.isVisible();
if (!connected) return false;
const element = await toggleIcon.elementHandle();
if (!element) return false;
const opacity = await element.evaluate(node => {
// https://stackoverflow.com/questions/11365296/how-do-i-get-the-opacity-of-an-element-using-javascript
return window.getComputedStyle(node).getPropertyValue('opacity');
});
if (!opacity || typeof opacity !== 'string') {
throw new Error('opacity is not a string');
}
const isVisible = opacity !== '0';
return isVisible;
}
test('add new bulleted list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await updateBlockType(page, 'affine:list', 'bulleted');
await focusRichText(page, 0);
await type(page, 'aa');
await pressEnter(page);
await type(page, 'aa');
await pressEnter(page);
await assertRichTexts(page, ['aa', 'aa', '']);
await assertBlockCount(page, 'list', 3);
});
test('add new todo list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await updateBlockType(page, 'affine:list', 'todo');
await focusRichText(page, 0);
await type(page, 'aa');
await assertRichTexts(page, ['aa']);
const checkBox = page.locator('.affine-list-block__prefix');
await expect(page.locator('.affine-list--checked')).toHaveCount(0);
await checkBox.click();
await expect(page.locator('.affine-list--checked')).toHaveCount(1);
await checkBox.click();
await expect(page.locator('.affine-list--checked')).toHaveCount(0);
});
test('add new toggle list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await updateBlockType(page, 'affine:list', 'toggle');
await focusRichText(page, 0);
await type(page, 'top');
await pressTab(page);
await pressEnter(page);
await type(page, 'kid 1');
await pressEnter(page);
await assertRichTexts(page, ['top', 'kid 1', '']);
await assertBlockCount(page, 'list', 3);
});
test('convert nested paragraph to list', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa\nbbb');
await pressTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await dragBetweenIndices(page, [0, 1], [1, 2]);
const { openParagraphMenu, bulletedBtn } = getFormatBar(page);
await openParagraphMenu();
await bulletedBtn.click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test('convert to numbered list block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0); // created 0, 1, 2
await updateBlockType(page, 'affine:list', 'bulleted'); // replaced 2 to 3
await waitNextFrame(page);
await updateBlockType(page, 'affine:list', 'numbered');
await focusRichText(page, 0);
const listSelector = '.affine-list-rich-text-wrapper';
const bulletIconSelector = `${listSelector} > div`;
await assertTextContent(page, bulletIconSelector, /1\./);
await undoByClick(page);
// const numberIconSelector = `${listSelector} > svg`;
// await expect(page.locator(numberIconSelector)).toHaveCount(1);
await redoByClick(page);
await focusRichText(page, 0);
await type(page, 'aa');
await pressEnter(page); // created 4
await assertBlockType(page, '4', 'numbered');
await type(page, 'aa');
await pressEnter(page); // created 5
await assertBlockType(page, '5', 'numbered');
await page.keyboard.press('Tab');
await assertBlockType(page, '5', 'numbered');
});
test('indent list block', async ({ page }) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
await focusRichText(page, 1);
await type(page, 'hello');
await assertRichTexts(page, ['', 'hello', '']);
await page.keyboard.press('Tab'); // 0(1(2(3)4))
await assertRichTexts(page, ['', 'hello', '']);
await assertBlockChildrenIds(page, '1', ['2', '4']);
await assertBlockChildrenIds(page, '2', ['3']);
await undoByKeyboard(page); // 0(1(2,3,4))
await assertBlockChildrenIds(page, '1', ['2', '3', '4']);
});
test('unindent list block', async ({ page }) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
await focusRichText(page, 1);
await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3)4))
await assertBlockChildrenIds(page, '1', ['2', '4']);
await assertBlockChildrenIds(page, '2', ['3']);
await pressShiftTab(page); // 0(1(2,3,4))
await assertBlockChildrenIds(page, '1', ['2', '3', '4']);
await pressShiftTab(page);
await assertBlockChildrenIds(page, '1', ['2', '3', '4']);
});
test('remove all indent for a list block', async ({ page }) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
await focusRichText(page, 1);
await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3)4))
await focusRichText(page, 2);
await page.keyboard.press('Tab', { delay: 50 });
await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3(4))))
await assertBlockChildrenIds(page, '3', ['4']);
await pressBackspaceWithShortKey(page); // 0(1(2(3)4))
await assertBlockChildrenIds(page, '1', ['2', '4']);
await assertBlockChildrenIds(page, '2', ['3']);
});
test('insert new list block by enter', async ({ page }) => {
await enterPlaygroundWithList(page);
await assertRichTexts(page, ['', '', '']);
await focusRichText(page, 1);
await type(page, 'hello');
await assertRichTexts(page, ['', 'hello', '']);
await pressEnter(page);
await type(page, 'world');
await assertRichTexts(page, ['', 'hello', 'world', '']);
await assertBlockChildrenFlavours(page, '1', [
'affine:list',
'affine:list',
'affine:list',
'affine:list',
]);
});
test('delete at start of list block', async ({ page }) => {
await enterPlaygroundWithList(page);
await focusRichText(page, 1);
await page.keyboard.press('Backspace');
await assertBlockChildrenFlavours(page, '1', [
'affine:list',
'affine:paragraph',
'affine:list',
]);
await waitNextFrame(page, 200);
await assertRichTextInlineRange(page, 1, 0, 0);
await undoByClick(page);
await assertBlockChildrenFlavours(page, '1', [
'affine:list',
'affine:list',
'affine:list',
]);
await waitNextFrame(page);
//FIXME: it just failed in playwright
// await assertSelection(page, 1, 0, 0);
});
test('nested list blocks', async ({ page }, testInfo) => {
await enterPlaygroundWithList(page);
await focusRichText(page, 0);
await type(page, '123');
await focusRichText(page, 1);
await pressTab(page);
await type(page, '456');
await focusRichText(page, 2);
await pressTab(page);
await pressTab(page);
await type(page, '789');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await focusRichText(page, 1);
await pressShiftTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});
test('update numbered list block prefix', async ({ page }) => {
await enterPlaygroundWithList(page, ['', '', ''], 'numbered'); // 0(1(2,3,4))
await focusRichText(page, 1);
await type(page, 'lunatic');
await assertRichTexts(page, ['', 'lunatic', '']);
await assertListPrefix(page, ['1', '2', '3']);
await page.keyboard.press('Tab');
await assertListPrefix(page, ['1', 'a', '2']);
await page.keyboard.press('Shift+Tab');
await assertListPrefix(page, ['1', '2', '3']);
await waitNextFrame(page, 200);
await page.keyboard.press('Enter');
await assertListPrefix(page, ['1', '2', '3', '4']);
await waitNextFrame(page, 200);
await type(page, 'concorde');
await assertRichTexts(page, ['', 'lunatic', 'concorde', '']);
await page.keyboard.press('Tab');
await assertListPrefix(page, ['1', '2', 'a', '3']);
});
test('basic indent and unindent', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'text1');
await pressEnter(page);
await type(page, 'text2');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await page.keyboard.press('Tab');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after_tab.json`
);
await page.waitForTimeout(100);
await pressShiftTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after_shift_tab.json`
);
});
test('should indent todo block preserve todo status', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'text1');
await pressEnter(page);
await type(page, '[x]');
await pressSpace(page);
await type(page, 'todo item');
await pressTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await pressShiftTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test('enter list block with empty text', async ({ page }, testInfo) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
/**
* -
* -
* -
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await focusRichText(page, 1);
await pressTab(page);
await focusRichText(page, 2);
await pressTab(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
await pressEnter(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
await pressEnter(page);
/**
* -
* -
* |
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_3.json`
);
await undoByClick(page);
await undoByClick(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
/**
* -
* -|
* -
*/
await focusRichText(page, 1);
await waitNextFrame(page);
await pressEnter(page);
await waitNextFrame(page);
/**
* -
* -|
* -
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_4.json`
);
await undoByClick(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
/**
* -|
* -
* -
*/
await focusRichText(page, 0);
await waitNextFrame(page);
await pressEnter(page);
await waitNextFrame(page);
/**
* |
* -
* -
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_5.json`
);
await undoByClick(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
});
test('enter list block with non-empty text', async ({ page }) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
await focusRichText(page, 0);
await type(page, 'aa');
await focusRichText(page, 1);
await type(page, 'bb');
await pressTab(page);
await focusRichText(page, 2);
await type(page, 'cc');
await pressTab(page);
await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4)))
await focusRichText(page, 1);
await pressEnter(page);
await assertBlockChildrenIds(page, '2', ['3', '5', '4']);
await undoByClick(page);
await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4)))
await focusRichText(page, 0);
await pressEnter(page);
await assertBlockChildrenIds(page, '2', ['6', '3', '4']); // 0(1(2,(6,3,4)))
await waitNextFrame(page);
await undoByClick(page);
await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4)))
});
test.describe('indent correctly when deleting list item', () => {
test('delete the child item in the middle position', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, '- a');
await pressEnter(page);
await pressTab(page);
await type(page, 'b');
await pressEnter(page);
await type(page, 'c');
await pressEnter(page);
await type(page, 'd');
await pressArrowUp(page);
await pressArrowLeft(page);
await pressBackspace(page);
await pressBackspace(page);
await assertBlockChildrenIds(page, '3', ['4', '6']);
await assertRichTexts(page, ['a', 'bc', 'd']);
await assertRichTextInlineRange(page, 1, 1);
});
test('merge two lists', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, '- a');
await pressEnter(page);
await pressTab(page);
await type(page, 'b');
await pressEnter(page);
await pressTab(page);
await type(page, 'c');
await pressEnter(page);
await pressBackspace(page, 3);
await assertRichTexts(page, ['a', 'b', 'c', '']);
await waitNextFrame(page);
await pressEnter(page);
await type(page, '- d');
await pressEnter(page);
await pressTab(page);
await type(page, 'e');
await pressEnter(page);
await pressTab(page);
await type(page, 'f');
await pressArrowUp(page, 3);
await pressBackspace(page, 2);
await waitNextFrame(page, 200);
await assertRichTexts(page, ['a', 'b', '', 'd', 'e', 'f']);
await assertBlockChildrenIds(page, '1', ['3', '9']);
await assertBlockChildrenIds(page, '3', ['4']);
await assertBlockChildrenIds(page, '4', ['5']);
await assertBlockChildrenIds(page, '10', ['11']);
});
});
test('delete list item with nested children items', async ({ page }) => {
await enterPlaygroundWithList(page);
await focusRichText(page, 0);
await type(page, '1');
await focusRichText(page, 1);
await pressTab(page);
await type(page, '2');
await focusRichText(page, 2);
await pressTab(page);
await pressTab(page);
await type(page, '3');
await pressEnter(page);
await type(page, '4');
await focusRichText(page, 1);
await pressArrowLeft(page);
// 1
// |2
// 3
// 4
await pressBackspace(page);
await waitNextFrame(page);
// 1
// |2 (transformed to paragraph)
// 3
// 4
await pressBackspace(page);
await waitNextFrame(page);
// 1
// |2
// 3
// 4
await pressBackspace(page);
await waitNextFrame(page);
// 1|2
// 3
// 4
await assertRichTextInlineRange(page, 0, 1);
await assertRichTexts(page, ['12', '3', '4']);
await assertBlockChildrenIds(page, '1', ['2', '4', '5']);
});
test('add number prefix to a todo item should not forcefully change it into numbered list, vice versa', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, '1. numberList');
await assertListPrefix(page, ['1']);
await focusRichText(page, 0, { clickPosition: { x: 0, y: 0 } });
await type(page, '[] ');
await assertListPrefix(page, ['1']);
await pressBackspace(page, 14);
await type(page, '[] todoList');
await assertListPrefix(page, ['']);
await focusRichText(page, 0, { clickPosition: { x: 0, y: 0 } });
await type(page, '1. ');
await assertListPrefix(page, ['']);
});
test('should not convert to a list when pressing space at the second line', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa');
await pressShiftEnter(page);
await type(page, '-');
await pressSpace(page);
await type(page, 'bbb');
await assertRichTexts(page, ['aaa\n- bbb']);
});
test.describe('toggle list', () => {
test('click toggle icon should collapsed list', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
const toggleIcon = page.locator('.toggle-icon');
const prefixes = page.locator('.affine-list-block__prefix');
const listChildren = page
.locator('[data-block-id="4"] .affine-block-children-container')
.nth(0);
const parentPrefix = prefixes.nth(1);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await parentPrefix.hover();
await waitNextFrame(page);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await expect(listChildren).toBeVisible();
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_toggle.json`
);
// Collapsed toggle icon should be show always
await page.mouse.move(0, 0);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await expect(listChildren).not.toBeVisible();
await toggleIcon.click();
await expect(listChildren).toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await page.mouse.move(0, 0);
await waitNextFrame(page, 200);
expect(await isToggleIconVisible(toggleIcon)).toBe(false);
});
test('indent item should expand toggle', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
await focusRichText(page, 2);
await pressEnter(page);
await pressEnter(page);
await type(page, '012');
const toggleIcon = page.locator('.toggle-icon');
const listChildren = page
.locator('[data-block-id="4"] .affine-block-children-container')
.nth(0);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await expect(listChildren).toBeVisible();
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_toggle.json`
);
await focusRichText(page, 3);
await pressTab(page);
await waitNextFrame(page, 200);
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});
test('toggle icon should be show when hover', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
const toggleIcon = page.locator('.toggle-icon');
const prefixes = page.locator('.affine-list-block__prefix');
const parentPrefix = prefixes.nth(1);
expect(await isToggleIconVisible(toggleIcon)).toBe(false);
await parentPrefix.hover();
await waitNextFrame(page, 200);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await page.mouse.move(0, 0);
await waitNextFrame(page, 300);
expect(await isToggleIconVisible(toggleIcon)).toBe(false);
});
});
test.describe('readonly', () => {
test('can expand toggle in readonly mode', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
const toggleIcon = page.locator('.toggle-icon');
const prefixes = page.locator('.affine-list-block__prefix');
const listChildren = page
.locator('[data-block-id="4"] .affine-block-children-container')
.nth(0);
const parentPrefix = prefixes.nth(1);
await parentPrefix.hover();
await waitNextFrame(page, 200);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await expect(listChildren).toBeVisible();
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_before_readonly.json`
);
await waitNextFrame(page, 200);
await switchReadonly(page);
await waitNextFrame(page, 200);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await expect(listChildren).not.toBeVisible();
await toggleIcon.click();
await expect(listChildren).toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_before_readonly.json`
);
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_before_readonly.json`
);
});
test('can not modify todo list in readonly mode', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const checkBox = page.locator('.affine-list-block__prefix');
{
await type(page, '[] todo');
await switchReadonly(page);
await expect(page.locator('.affine-list--checked')).toHaveCount(0);
await checkBox.click();
await expect(page.locator('.affine-list--checked')).toHaveCount(0);
}
{
await switchReadonly(page, false);
await checkBox.click();
await switchReadonly(page);
await expect(page.locator('.affine-list--checked')).toHaveCount(1);
await checkBox.click();
await expect(page.locator('.affine-list--checked')).toHaveCount(1);
}
});
test('should render collapsed list correctly', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
// await switchEditorMode(page);
await initThreeLists(page);
const toggleIcon = page.locator('.toggle-icon');
const listChildren = page
.locator('[data-block-id="5"] .affine-block-children-container')
.nth(0);
await expect(listChildren).toBeVisible();
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
await switchReadonly(page);
// trick for render a readonly doc from scratch
await switchEditorMode(page);
await switchEditorMode(page);
await expect(listChildren).not.toBeVisible();
});
});

View File

@@ -0,0 +1,565 @@
import {
enterPlaygroundRoom,
focusRichText,
getCursorBlockIdAndHeight,
initEmptyParagraphState,
pressArrowLeft,
pressBackspace,
pressEnter,
pressSpace,
redoByKeyboard,
resetHistory,
type,
undoByClick,
undoByKeyboard,
waitNextFrame,
} from './utils/actions/index.js';
import {
assertBlockType,
assertRichTextInlineDeltas,
assertRichTextInlineRange,
assertRichTexts,
assertText,
} from './utils/asserts.js';
import { test } from './utils/playwright.js';
test('markdown shortcut', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await resetHistory(page);
let id: string | null = null;
await waitNextFrame(page);
await type(page, '[] ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'todo');
await undoByClick(page);
await assertText(page, '[] ');
await undoByClick(page);
//FIXME: it just failed in playwright
await focusRichText(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '[ ] ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'todo');
await undoByClick(page);
await assertText(page, '[ ] ');
await undoByClick(page);
//FIXME: it just failed in playwright
await focusRichText(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '[x] ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'todo');
await undoByClick(page);
await assertText(page, '[x] ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '* ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'bulleted');
await undoByClick(page);
await assertText(page, '* ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '- ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'bulleted');
await undoByClick(page);
await assertText(page, '- ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '1. ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'numbered');
await undoByClick(page);
await assertText(page, '1. ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '20. ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'numbered');
await undoByClick(page);
await assertText(page, '20. ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '# ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'h1');
await undoByClick(page);
await assertText(page, '# ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '## ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'h2');
await undoByClick(page);
await assertText(page, '## ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '### ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'h3');
await undoByClick(page);
await assertText(page, '### ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '#### ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'h4');
await undoByClick(page);
await assertText(page, '#### ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '##### ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'h5');
await undoByClick(page);
await assertText(page, '##### ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '###### ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'h6');
await undoByClick(page);
await assertText(page, '###### ');
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '> ');
await waitNextFrame(page);
[id] = await getCursorBlockIdAndHeight(page);
await assertBlockType(page, id, 'quote');
await undoByClick(page);
await assertText(page, '> ');
await undoByClick(page);
await assertRichTexts(page, ['']);
// testing various horizontal dividers
await waitNextFrame(page);
await type(page, '--- ');
await undoByClick(page);
await assertRichTexts(page, ['--- ']);
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '*** ');
await undoByClick(page);
await assertRichTexts(page, ['*** ']);
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '___ ');
await undoByClick(page);
await assertRichTexts(page, ['___ ']);
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '------ ');
await undoByClick(page);
await assertRichTexts(page, ['------ ']);
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '****** ');
await undoByClick(page);
await assertRichTexts(page, ['****** ']);
await undoByClick(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '______ ');
await undoByClick(page);
await assertRichTexts(page, ['______ ']);
await undoByClick(page);
await assertRichTexts(page, ['']);
});
test.describe('markdown inline-text', () => {
test.beforeEach(async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await resetHistory(page);
});
test('bolditalic', async ({ page }) => {
await type(page, 'aa***bb*** ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bb',
attributes: {
bold: true,
italic: true,
},
},
]);
await undoByKeyboard(page);
await assertRichTextInlineRange(page, 0, 11);
await assertRichTextInlineDeltas(page, [
{
insert: 'aa***bb*** ',
},
]);
await redoByKeyboard(page);
await type(page, 'cc');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bbcc',
attributes: {
bold: true,
italic: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '***test *** ');
await assertRichTexts(page, ['***test *** ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
// *** + space will be converted to divider, so needn't test this case here
// await waitNextFrame(page);
// await type(page, '*** test*** ');
// await assertRichTexts(page, ['*** test*** ']);
// await undoByKeyboard(page);
// await assertRichTexts(page, ['']);
});
test('bold', async ({ page }) => {
await type(page, 'aa**bb** ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bb',
attributes: {
bold: true,
},
},
]);
await undoByKeyboard(page);
await assertRichTextInlineRange(page, 0, 9);
await assertRichTextInlineDeltas(page, [
{
insert: 'aa**bb** ',
},
]);
await redoByKeyboard(page);
await type(page, 'cc');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bbcc',
attributes: {
bold: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '**test ** ');
await assertRichTexts(page, ['**test ** ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '** test** ');
await assertRichTexts(page, ['** test** ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
});
test('italic', async ({ page }) => {
await type(page, 'aa*bb* ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bb',
attributes: {
italic: true,
},
},
]);
await undoByKeyboard(page);
await assertRichTextInlineRange(page, 0, 7);
await assertRichTextInlineDeltas(page, [
{
insert: 'aa*bb* ',
},
]);
await redoByKeyboard(page);
await type(page, 'cc');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bbcc',
attributes: {
italic: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '*test * ');
await assertRichTexts(page, ['*test * ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
// * + space will be converted to bulleted list, so needn't test this case here
// await waitNextFrame(page);
// await type(page, '* test* ');
// await assertRichTexts(page, ['* test* ']);
// await undoByKeyboard(page);
// await assertRichTexts(page, ['']);
});
test('strike', async ({ page }) => {
await type(page, 'aa~~bb~~ ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bb',
attributes: {
strike: true,
},
},
]);
await undoByKeyboard(page);
await assertRichTextInlineRange(page, 0, 9);
await assertRichTextInlineDeltas(page, [
{
insert: 'aa~~bb~~ ',
},
]);
await redoByKeyboard(page);
await type(page, 'cc');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bbcc',
attributes: {
strike: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '~~test ~~ ');
await assertRichTexts(page, ['~~test ~~ ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '~~ test~~ ');
await assertRichTexts(page, ['~~ test~~ ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
});
test('underline', async ({ page }) => {
await type(page, 'aa~bb~ ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bb',
attributes: {
underline: true,
},
},
]);
await undoByKeyboard(page);
await assertRichTextInlineRange(page, 0, 7);
await assertRichTextInlineDeltas(page, [
{
insert: 'aa~bb~ ',
},
]);
await redoByKeyboard(page);
await type(page, 'cc');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bbcc',
attributes: {
underline: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '~test ~ ');
await assertRichTexts(page, ['~test ~ ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '~ test~ ');
await assertRichTexts(page, ['~ test~ ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
});
test('code', async ({ page }) => {
await type(page, 'aa`bb` ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bb',
attributes: {
code: true,
},
},
]);
await undoByKeyboard(page);
await assertRichTextInlineRange(page, 0, 7);
await assertRichTextInlineDeltas(page, [
{
insert: 'aa`bb` ',
},
]);
await redoByKeyboard(page);
await type(page, 'cc');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'bb',
attributes: {
code: true,
},
},
{
insert: 'cc',
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '`test ` ');
await assertRichTexts(page, ['`test ` ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await waitNextFrame(page);
await type(page, '` test` ');
await assertRichTexts(page, ['` test` ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
});
});
test('inline code should work when pressing Enter followed by Backspace twice', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '`test`');
await pressSpace(page);
await waitNextFrame(page);
await pressArrowLeft(page);
await waitNextFrame(page);
await pressEnter(page);
await waitNextFrame(page);
await pressBackspace(page);
await waitNextFrame(page);
await pressEnter(page);
await waitNextFrame(page);
await pressBackspace(page);
await assertRichTexts(page, ['test']);
});

View File

@@ -0,0 +1,43 @@
import { expect } from '@playwright/test';
import {
switchMultipleEditorsMode,
toggleMultipleEditors,
} from '../utils/actions/edgeless.js';
import {
enterPlaygroundRoom,
initEmptyEdgelessState,
initThreeParagraphs,
waitNextFrame,
} from '../utils/actions/misc.js';
import { test } from '../utils/playwright.js';
test('the shift pressing status should effect all editors', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await initThreeParagraphs(page);
await toggleMultipleEditors(page);
await switchMultipleEditorsMode(page);
await waitNextFrame(page, 5000);
const getShiftPressedStatus = async () => {
return page.evaluate(() => {
const edgelessBlocks = document.querySelectorAll('affine-edgeless-root');
return Array.from(edgelessBlocks).map(edgelessRoot => {
return edgelessRoot.gfx.keyboard.shiftKey$.peek();
});
});
};
await page.keyboard.down('Shift');
const pressed = await getShiftPressedStatus();
expect(pressed).toEqual([true, true]);
await page.keyboard.up('Shift');
const released = await getShiftPressedStatus();
expect(released).toEqual([false, false]);
});

View File

@@ -0,0 +1,33 @@
import { expect } from '@playwright/test';
import { dragBetweenCoords } from '../utils/actions/drag.js';
import { toggleMultipleEditors } from '../utils/actions/edgeless.js';
import {
enterPlaygroundRoom,
initEmptyParagraphState,
initThreeParagraphs,
} from '../utils/actions/misc.js';
import { getRichTextBoundingBox } from '../utils/actions/selection.js';
import { test } from '../utils/playwright.js';
test('should only show one format bar when multiple editors are toggled', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeParagraphs(page);
await toggleMultipleEditors(page);
// Select some text
const box123 = await getRichTextBoundingBox(page, '2');
const above123 = { x: box123.left + 10, y: box123.top + 2 };
const box789 = await getRichTextBoundingBox(page, '4');
const bottomRight789 = { x: box789.right - 10, y: box789.bottom - 2 };
await dragBetweenCoords(page, above123, bottomRight789, { steps: 10 });
// should only show one format bar
const formatBar = page.locator('.affine-format-bar-widget');
await expect(formatBar).toHaveCount(1);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,102 @@
import {
enterPlaygroundRoom,
focusRichText,
initEmptyParagraphState,
pressArrowDown,
pressArrowRight,
pressArrowUp,
pressEnter,
type,
} from './utils/actions/index.js';
import {
assertRichTextInlineRange,
assertTextContain,
} from './utils/asserts.js';
import { test } from './utils/playwright.js';
test('prohibit creating divider within quote', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/995',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, '>');
await page.keyboard.press('Space', { delay: 50 });
await focusRichText(page);
await type(page, '123');
await pressEnter(page);
await type(page, '---');
await page.keyboard.press('Space', { delay: 50 });
await assertTextContain(page, '---');
});
test('quote arrow up/down', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/2834',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaaaa');
await pressEnter(page);
await type(page, 'aaaaa');
await pressEnter(page);
await type(page, 'aaa');
await pressEnter(page);
await type(page, '> aaaaaaaaa');
await pressEnter(page);
await type(page, 'aaa');
await pressEnter(page);
await type(page, 'aaaaaaaaa');
await pressEnter(page);
await pressEnter(page);
await type(page, 'aaaaa');
await pressEnter(page);
await type(page, 'aaaaa');
await pressEnter(page);
await type(page, 'aaa');
await assertRichTextInlineRange(page, 6, 3, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 5, 3, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 4, 3, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 3, 15, 0);
await pressArrowRight(page, 8);
await assertRichTextInlineRange(page, 3, 23, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 3, 13, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 3, 9, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 2, 3, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 1, 5, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 0, 5, 0);
await pressArrowUp(page);
await assertRichTextInlineRange(page, 0, 0, 0);
await pressArrowRight(page, 4);
await assertRichTextInlineRange(page, 0, 4, 0);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 1, 4, 0);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 2, 3, 0);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 3, 2, 0);
await pressArrowRight(page, 8);
await assertRichTextInlineRange(page, 3, 10, 0);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 3, 14, 0);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 4, 2, 0);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 5, 2, 0);
await pressArrowDown(page);
await assertRichTextInlineRange(page, 6, 2, 0);
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,942 @@
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,
} 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,
}, testInfo) => {
await initEmptyParagraphState(page);
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();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
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,
}, testInfo) => {
await initEmptyParagraphState(page);
const slashMenu = page.locator(`.slash-menu`);
await focusRichText(page);
await type(page, '/');
await expect(slashMenu).toBeVisible();
await pressBackspace(page);
await expect(slashMenu).toBeHidden();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
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,
}, testInfo) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/toeverything/blocksuite/issues/1126',
});
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'hello');
await pressShiftEnter(page);
await waitNextFrame(page);
await type(page, '/copy');
await pressEnter(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
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 }, testInfo) => {
await enterPlaygroundRoom(page);
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();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
test('should style empty line works', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
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');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}.json`
);
});
});
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 titleColumn = page.locator('.affine-database-column').nth(0);
expect(await titleColumn.innerText()).toBe('Title');
const defaultRows = page.locator('.affine-database-block-row');
expect(await defaultRows.count()).toBe(3);
});
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.blockStd.WidgetViewIdentifier(
'affine:page|affine-slash-menu-widget'
),
// @ts-ignore
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 {
override config = {
...SlashMenuWidget.DEFAULT_CONFIG,
items: [
{ groupName: 'Custom Menu' },
{
name: 'Custom Menu Item',
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
icon: '' as any,
action: () => {
// do nothing
},
},
{
name: 'Custom Menu Item',
// oxlint-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.blockStd.WidgetViewIdentifier(
'affine:page|affine-slash-menu-widget'
),
// @ts-ignore
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']);
});

View File

@@ -0,0 +1,22 @@
import type { Page } from '@playwright/test';
import { waitNextFrame } from './misc.js';
export async function updateBlockType(
page: Page,
flavour: string,
type?: string
) {
await page.evaluate(
([flavour, type]) => {
window.host.std.command.exec(window.$blocksuite.blocks.updateBlockType, {
flavour,
props: {
type,
},
});
},
[flavour, type] as [string, string?]
);
await waitNextFrame(page, 400);
}

View File

@@ -0,0 +1,129 @@
import type { IPoint } from '@blocksuite/global/utils';
import type { Store } from '@blocksuite/store';
import type { Page } from '@playwright/test';
import { toViewCoord } from './edgeless.js';
import { waitNextFrame } from './misc.js';
export function getDebugMenu(page: Page) {
const debugMenu = page.locator('starter-debug-menu');
return {
debugMenu,
undoBtn: debugMenu.locator('sl-tooltip[content="Undo"]'),
redoBtn: debugMenu.locator('sl-tooltip[content="Redo"]'),
blockTypeButton: debugMenu.getByRole('button', { name: 'Block Type' }),
testOperationsButton: debugMenu.getByRole('button', {
name: 'Test Operations',
}),
pagesBtn: debugMenu.getByTestId('docs-button'),
};
}
export async function moveView(page: Page, point: [number, number]) {
const [x, y] = await toViewCoord(page, point);
await page.mouse.move(x, y);
}
export async function click(page: Page, point: IPoint) {
await page.mouse.click(point.x, point.y);
}
export async function clickView(page: Page, point: [number, number]) {
const [x, y] = await toViewCoord(page, point);
await page.mouse.click(x, y);
}
export async function dblclickView(page: Page, point: [number, number]) {
const [x, y] = await toViewCoord(page, point);
await page.mouse.dblclick(x, y);
}
export async function undoByClick(page: Page) {
await getDebugMenu(page).undoBtn.click();
}
export async function redoByClick(page: Page) {
await getDebugMenu(page).redoBtn.click();
}
export async function clickBlockById(page: Page, id: string) {
await page.click(`[data-block-id="${id}"]`);
}
export async function doubleClickBlockById(page: Page, id: string) {
await page.dblclick(`[data-block-id="${id}"]`);
}
export async function disconnectByClick(page: Page) {
await clickTestOperationsMenuItem(page, 'Disconnect');
}
export async function connectByClick(page: Page) {
await clickTestOperationsMenuItem(page, 'Connect');
}
export async function addNoteByClick(page: Page) {
await clickTestOperationsMenuItem(page, 'Add Note');
}
export async function addNewPage(page: Page) {
const { pagesBtn } = getDebugMenu(page);
if (!(await page.locator('docs-panel').isVisible())) {
await pagesBtn.click();
}
await page.locator('.new-doc-button').click();
const docMetas = await page.evaluate(() => {
const { collection } = window;
return collection.meta.docMetas;
});
if (!docMetas.length) throw new Error('Add new doc failed');
return docMetas[docMetas.length - 1];
}
export async function switchToPage(page: Page, docId?: string) {
await page.evaluate(docId => {
const { collection, editor } = window;
if (!docId) {
const docMetas = collection.meta.docMetas;
if (!docMetas.length) return;
docId = docMetas[0].id;
}
const doc = collection.getDoc(docId);
if (!doc) return;
editor.doc = doc;
}, docId);
}
export async function clickTestOperationsMenuItem(page: Page, name: string) {
const menuButton = getDebugMenu(page).testOperationsButton;
await menuButton.click();
await waitNextFrame(page); // wait for animation ended
const menuItem = page.getByRole('menuitem', { name });
await menuItem.click();
await menuItem.waitFor({ state: 'hidden' }); // wait for animation ended
}
export async function switchReadonly(page: Page, value = true) {
await page.evaluate(_value => {
const defaultPage = document.querySelector(
'affine-page-root,affine-edgeless-root'
) as HTMLElement & {
doc: Store;
};
const doc = defaultPage.doc;
doc.readonly = _value;
}, value);
}
export async function activeEmbed(page: Page) {
await page.click('.resizable-img');
}
export async function toggleDarkMode(page: Page) {
await page.click('sl-tooltip[content="Toggle Dark Mode"] sl-button');
}

View File

@@ -0,0 +1,271 @@
import type { Page } from '@playwright/test';
import { assertImageOption } from '../asserts.js';
import { getIndexCoordinate, waitNextFrame } from './misc.js';
export async function dragBetweenCoords(
page: Page,
from: { x: number; y: number },
to: { x: number; y: number },
options?: {
beforeMouseUp?: () => Promise<void>;
steps?: number;
click?: boolean;
button?: 'left' | 'right' | 'middle';
}
) {
const steps = options?.steps ?? 20;
const button: 'left' | 'right' | 'middle' = options?.button ?? 'left';
const { x: x1, y: y1 } = from;
const { x: x2, y: y2 } = to;
options?.click && (await page.mouse.click(x1, y1));
await page.mouse.move(x1, y1);
await page.mouse.down({ button });
await page.mouse.move(x2, y2, { steps });
await options?.beforeMouseUp?.();
await page.mouse.up({ button });
}
export async function dragBetweenIndices(
page: Page,
[startRichTextIndex, startVIndex]: [number, number],
[endRichTextIndex, endVIndex]: [number, number],
startCoordOffSet: { x: number; y: number } = { x: 0, y: 0 },
endCoordOffSet: { x: number; y: number } = { x: 0, y: 0 },
options?: {
beforeMouseUp?: () => Promise<void>;
steps?: number;
click?: boolean;
}
) {
const finalOptions = {
steps: 50,
...options,
};
const startCoord = await getIndexCoordinate(
page,
[startRichTextIndex, startVIndex],
startCoordOffSet
);
const endCoord = await getIndexCoordinate(
page,
[endRichTextIndex, endVIndex],
endCoordOffSet
);
await dragBetweenCoords(page, startCoord, endCoord, finalOptions);
}
export async function dragOverTitle(page: Page) {
const { from, to } = await page.evaluate(() => {
const titleInput = document.querySelector(
'doc-title rich-text'
) as HTMLTextAreaElement;
const titleBound = titleInput.getBoundingClientRect();
return {
from: { x: titleBound.left + 1, y: titleBound.top + 1 },
to: { x: titleBound.right - 1, y: titleBound.bottom - 1 },
};
});
await dragBetweenCoords(page, from, to, {
steps: 5,
});
}
export async function dragEmbedResizeByTopRight(page: Page) {
const { from, to } = await page.evaluate(() => {
const bottomRightButton = document.querySelector(
'.top-right'
) as HTMLInputElement;
const bottomRightButtonBound = bottomRightButton.getBoundingClientRect();
const y = bottomRightButtonBound.top;
return {
from: { x: bottomRightButtonBound.left + 5, y: y + 5 },
to: { x: bottomRightButtonBound.left + 5 - 200, y },
};
});
await dragBetweenCoords(page, from, to, {
steps: 10,
});
}
export async function dragEmbedResizeByTopLeft(page: Page) {
const { from, to } = await page.evaluate(() => {
const bottomRightButton = document.querySelector(
'.top-left'
) as HTMLInputElement;
const bottomRightButtonBound = bottomRightButton.getBoundingClientRect();
const y = bottomRightButtonBound.top;
return {
from: { x: bottomRightButtonBound.left + 5, y: y + 5 },
to: { x: bottomRightButtonBound.left + 5 + 200, y },
};
});
await dragBetweenCoords(page, from, to, {
steps: 10,
});
}
export async function dragHandleFromBlockToBlockBottomById(
page: Page,
sourceId: string,
targetId: string,
bottom = true,
offset?: number,
beforeMouseUp?: () => Promise<void>
) {
const sourceBlock = await page
.locator(`[data-block-id="${sourceId}"]`)
.boundingBox();
const targetBlock = await page
.locator(`[data-block-id="${targetId}"]`)
.boundingBox();
if (!sourceBlock || !targetBlock) {
throw new Error();
}
await page.mouse.move(
sourceBlock.x + sourceBlock.width / 2,
sourceBlock.y + sourceBlock.height / 2
);
await waitNextFrame(page);
const dragHandleContainer = page.locator('.affine-drag-handle-container');
await dragHandleContainer.hover();
const handle = await dragHandleContainer.boundingBox();
if (!handle) {
throw new Error();
}
await page.mouse.move(
handle.x + handle.width / 2,
handle.y + handle.height / 2,
{ steps: 10 }
);
await page.mouse.down();
await page.mouse.move(
targetBlock.x,
targetBlock.y + (bottom ? targetBlock.height - 1 : 1),
{
steps: 50,
}
);
if (offset) {
await page.mouse.move(
targetBlock.x + offset,
targetBlock.y + (bottom ? targetBlock.height - 1 : 1),
{
steps: 50,
}
);
}
if (beforeMouseUp) {
await beforeMouseUp();
}
await page.mouse.up();
}
export async function dragBlockToPoint(
page: Page,
sourceId: string,
point: { x: number; y: number }
) {
const sourceBlock = await page
.locator(`[data-block-id="${sourceId}"]`)
.boundingBox();
if (!sourceBlock) {
throw new Error();
}
await page.mouse.move(
sourceBlock.x + sourceBlock.width / 2,
sourceBlock.y + sourceBlock.height / 2
);
const handle = await page
.locator('.affine-drag-handle-container')
.boundingBox();
if (!handle) {
throw new Error();
}
await page.mouse.move(
handle.x + handle.width / 2,
handle.y + handle.height / 2
);
await page.mouse.down();
await page.mouse.move(point.x, point.y, {
steps: 50,
});
await page.mouse.up();
}
export async function moveToImage(page: Page) {
const { x, y } = await page.evaluate(() => {
const bottomRightButton = document.querySelector('img') as HTMLElement;
const imageClient = bottomRightButton.getBoundingClientRect();
const y = imageClient.top;
return {
x: imageClient.left + 30,
y: y + 30,
};
});
await page.mouse.move(x, y);
}
export async function popImageMoreMenu(page: Page) {
await moveToImage(page);
await assertImageOption(page);
const moreButton = page.locator('.image-toolbar-button.more');
await moreButton.click();
const menu = page.locator('.image-more-popup-menu');
const turnIntoCardButton = page.locator('editor-menu-action', {
hasText: 'Turn into card view',
});
const copyButton = page.locator('editor-menu-action', {
hasText: 'Copy',
});
const duplicateButton = page.locator('editor-menu-action', {
hasText: 'Duplicate',
});
const deleteButton = page.locator('editor-menu-action', {
hasText: 'Delete',
});
return {
menu,
copyButton,
turnIntoCardButton,
duplicateButton,
deleteButton,
};
}
export async function clickBlockDragHandle(page: Page, blockId: string) {
const blockBox = await page
.locator(`[data-block-id="${blockId}"]`)
.boundingBox();
if (!blockBox) {
throw new Error();
}
await page.mouse.move(
blockBox.x + blockBox.width / 2,
blockBox.y + blockBox.height / 2
);
const handleBox = await page
.locator('.affine-drag-handle-container')
.boundingBox();
if (!handleBox) {
throw new Error();
}
await page.mouse.click(
handleBox.x + handleBox.width / 2,
handleBox.y + handleBox.height / 2
);
}

Some files were not shown because too many files have changed in this diff Show More