mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-29 00:06:09 +08:00
chore: migrate blocksuite test (#9222)
This commit is contained in:
@@ -0,0 +1,769 @@
|
||||
import { sleep } from '@blocksuite/global/utils';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
import { switchEditorMode } from 'utils/actions/edgeless.js';
|
||||
|
||||
import { dragBlockToPoint, popImageMoreMenu } from './utils/actions/drag.js';
|
||||
import {
|
||||
pressArrowDown,
|
||||
pressArrowUp,
|
||||
pressBackspace,
|
||||
pressEnter,
|
||||
pressEscape,
|
||||
pressShiftTab,
|
||||
pressTab,
|
||||
redoByKeyboard,
|
||||
SHORT_KEY,
|
||||
type,
|
||||
undoByKeyboard,
|
||||
} from './utils/actions/keyboard.js';
|
||||
import {
|
||||
captureHistory,
|
||||
enterPlaygroundRoom,
|
||||
focusRichText,
|
||||
initEmptyEdgelessState,
|
||||
initEmptyParagraphState,
|
||||
resetHistory,
|
||||
waitNextFrame,
|
||||
} from './utils/actions/misc.js';
|
||||
import {
|
||||
assertBlockChildrenIds,
|
||||
assertBlockCount,
|
||||
assertBlockFlavour,
|
||||
assertBlockSelections,
|
||||
assertKeyboardWorkInInput,
|
||||
assertParentBlockFlavour,
|
||||
assertRichImage,
|
||||
assertRichTextInlineRange,
|
||||
assertStoreMatchJSX,
|
||||
} from './utils/asserts.js';
|
||||
import { test } from './utils/playwright.js';
|
||||
|
||||
const FILE_NAME = 'test-card-1.png';
|
||||
const FILE_PATH = `../playground/public/${FILE_NAME}`;
|
||||
const FILE_ID = 'ejImogf-Tb7AuKY-v94uz1zuOJbClqK-tWBxVr_ksGA=';
|
||||
const FILE_SIZE = 45801;
|
||||
|
||||
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 }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { noteId } = 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');
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:attachment
|
||||
prop:embed={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
prop:name="${FILE_NAME}"
|
||||
prop:rotate={0}
|
||||
prop:size={${FILE_SIZE}}
|
||||
prop:sourceId="${FILE_ID}"
|
||||
prop:style="horizontalThin"
|
||||
prop:type="image/png"
|
||||
/>
|
||||
</affine:note>`,
|
||||
noteId
|
||||
);
|
||||
});
|
||||
|
||||
test('should undo/redo works for attachment', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { noteId } = await initEmptyParagraphState(page);
|
||||
|
||||
const { insertAttachment, waitLoading } = getAttachment(page);
|
||||
|
||||
await focusRichText(page);
|
||||
await insertAttachment();
|
||||
|
||||
// Wait for the attachment to be uploaded
|
||||
await waitLoading();
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
` <affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:attachment
|
||||
prop:embed={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
prop:name="${FILE_NAME}"
|
||||
prop:rotate={0}
|
||||
prop:size={${FILE_SIZE}}
|
||||
prop:sourceId="${FILE_ID}"
|
||||
prop:style="horizontalThin"
|
||||
prop:type="image/png"
|
||||
/>
|
||||
</affine:note>`,
|
||||
noteId
|
||||
);
|
||||
|
||||
await undoByKeyboard(page);
|
||||
await waitNextFrame(page);
|
||||
// The loading/error state should not be restored after undo
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="/"
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:note>`,
|
||||
noteId
|
||||
);
|
||||
|
||||
await redoByKeyboard(page);
|
||||
await waitNextFrame(page);
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:attachment
|
||||
prop:embed={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
prop:name="${FILE_NAME}"
|
||||
prop:rotate={0}
|
||||
prop:size={${FILE_SIZE}}
|
||||
prop:sourceId="${FILE_ID}"
|
||||
prop:style="horizontalThin"
|
||||
prop:type="image/png"
|
||||
/>
|
||||
</affine:note>`,
|
||||
noteId
|
||||
);
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { noteId } = 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();
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:image
|
||||
prop:caption=""
|
||||
prop:height={0}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
prop:rotate={0}
|
||||
prop:size={${FILE_SIZE}}
|
||||
prop:sourceId="${FILE_ID}"
|
||||
prop:width={0}
|
||||
/>
|
||||
</affine:note>`,
|
||||
noteId
|
||||
);
|
||||
await turnImageToCard();
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:attachment
|
||||
prop:caption=""
|
||||
prop:embed={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
prop:name="${FILE_NAME}"
|
||||
prop:rotate={0}
|
||||
prop:size={${FILE_SIZE}}
|
||||
prop:sourceId="${FILE_ID}"
|
||||
prop:style="horizontalThin"
|
||||
prop:type="image/png"
|
||||
/>
|
||||
</affine:note>`,
|
||||
noteId
|
||||
);
|
||||
});
|
||||
|
||||
test('should attachment can be deleted', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { noteId } = 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);
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:note>`,
|
||||
noteId
|
||||
);
|
||||
});
|
||||
|
||||
test.fixme(`support dragging attachment block directly`, async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { noteId } = 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');
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
` <affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:attachment
|
||||
prop:embed={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
prop:name="${FILE_NAME}"
|
||||
prop:rotate={0}
|
||||
prop:size={${FILE_SIZE}}
|
||||
prop:sourceId="${FILE_ID}"
|
||||
prop:style="horizontalThin"
|
||||
prop:type="image/png"
|
||||
/>
|
||||
</affine:note>`,
|
||||
noteId
|
||||
);
|
||||
|
||||
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);
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `<affine:page>
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:attachment
|
||||
prop:embed={false}
|
||||
prop:index="a0"
|
||||
prop:name="${FILE_NAME}"
|
||||
prop:rotate={0}
|
||||
prop:size={${FILE_SIZE}}
|
||||
prop:sourceId="${FILE_ID}"
|
||||
prop:style="horizontalThin"
|
||||
prop:type="image/png"
|
||||
/>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="111"
|
||||
prop:type="text"
|
||||
/>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="222"
|
||||
prop:type="text"
|
||||
/>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="333"
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:note>
|
||||
</affine:page>`
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `<affine:page>
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="111"
|
||||
prop:type="text"
|
||||
/>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="222"
|
||||
prop:type="text"
|
||||
/>
|
||||
<affine:attachment
|
||||
prop:embed={false}
|
||||
prop:index="a0"
|
||||
prop:name="${FILE_NAME}"
|
||||
prop:rotate={0}
|
||||
prop:size={${FILE_SIZE}}
|
||||
prop:sourceId="${FILE_ID}"
|
||||
prop:style="horizontalThin"
|
||||
prop:type="image/png"
|
||||
/>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="333"
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:note>
|
||||
</affine:page>`
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
@@ -0,0 +1,590 @@
|
||||
import type { DeltaInsert } from '@inline/types.js';
|
||||
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 doc.Text('hello'),
|
||||
});
|
||||
const note = doc.addBlock('affine:note', {}, rootId);
|
||||
|
||||
const text = new doc.Text('world');
|
||||
doc.addBlock('affine:paragraph', { text }, note);
|
||||
|
||||
const delta = [
|
||||
{ insert: 'foo ' },
|
||||
{ insert: 'bar', attributes: { bold: true } },
|
||||
];
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.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, {
|
||||
flags: {},
|
||||
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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,461 @@
|
||||
import './utils/declare-test-window.js';
|
||||
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
import type { BlockSnapshot } from '@store/index.js';
|
||||
import { ignoreSnapshotId } from 'utils/ignore.js';
|
||||
import { getEmbedCardToolbar } from 'utils/query.js';
|
||||
|
||||
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 { scoped, test } from './utils/playwright.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, 46, 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');
|
||||
});
|
||||
});
|
||||
@@ -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`);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,714 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
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,
|
||||
assertStoreMatchJSX,
|
||||
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);
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:page>
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:database
|
||||
prop:columns="Array [2]"
|
||||
prop:title="Database 1"
|
||||
prop:views="Array [1]"
|
||||
>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:database>
|
||||
<affine:database
|
||||
prop:columns="Array [2]"
|
||||
prop:title="Database 1"
|
||||
prop:views="Array [1]"
|
||||
>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:database>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:note>
|
||||
</affine:page>`
|
||||
);
|
||||
|
||||
await undoByKeyboard(page);
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:page>
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-white"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:database
|
||||
prop:columns="Array [2]"
|
||||
prop:title="Database 1"
|
||||
prop:views="Array [1]"
|
||||
>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:database>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:note>
|
||||
</affine:page>`
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
const color = '--affine-note-background-grey';
|
||||
await changeEdgelessNoteBackground(page, color);
|
||||
await assertEdgelessNoteBackground(page, ids.noteId, color);
|
||||
|
||||
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, color);
|
||||
}
|
||||
});
|
||||
|
||||
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']);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
@@ -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`
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,668 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { dragBetweenIndices } from 'utils/actions/drag.js';
|
||||
import { getFormatBar } from 'utils/query.js';
|
||||
|
||||
import { updateBlockType } from '../utils/actions/block.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,
|
||||
assertStoreMatchJSX,
|
||||
assertTitle,
|
||||
} from '../utils/asserts.js';
|
||||
import { test } from '../utils/playwright.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 }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { codeBlockId } = 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');
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption=""
|
||||
prop:language="rust"
|
||||
prop:wrap={false}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
await undoByKeyboard(page);
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption=""
|
||||
prop:language={null}
|
||||
prop:wrap={false}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
|
||||
// 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 }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { codeBlockId } = await initEmptyCodeBlockState(page);
|
||||
|
||||
const codeBlockController = getCodeBlock(page);
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption=""
|
||||
prop:language={null}
|
||||
prop:wrap={false}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
|
||||
await codeBlockController.codeBlock.hover();
|
||||
await (await codeBlockController.openMore()).wrapButton.click();
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption=""
|
||||
prop:language={null}
|
||||
prop:wrap={true}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
|
||||
await codeBlockController.codeBlock.hover();
|
||||
await (await codeBlockController.openMore()).cancelWrapButton.click();
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption=""
|
||||
prop:language={null}
|
||||
prop:wrap={false}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
});
|
||||
|
||||
test('add caption works', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { codeBlockId } = 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);
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption="BlockSuite"
|
||||
prop:language={null}
|
||||
prop:wrap={false}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
});
|
||||
|
||||
test('undo code block wrap can work', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { codeBlockId } = await initEmptyCodeBlockState(page);
|
||||
await focusRichText(page);
|
||||
|
||||
const codeBlockController = getCodeBlock(page);
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption=""
|
||||
prop:language={null}
|
||||
prop:wrap={false}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
|
||||
await codeBlockController.codeBlock.hover();
|
||||
await (await codeBlockController.openMore()).wrapButton.click();
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption=""
|
||||
prop:language={null}
|
||||
prop:wrap={true}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
|
||||
await focusRichText(page);
|
||||
await undoByKeyboard(page);
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
/*xml*/ `
|
||||
<affine:code
|
||||
prop:caption=""
|
||||
prop:language={null}
|
||||
prop:wrap={false}
|
||||
/>`,
|
||||
codeBlockId
|
||||
);
|
||||
});
|
||||
|
||||
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`
|
||||
);
|
||||
});
|
||||
@@ -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;']);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
import type {
|
||||
RichTextCell,
|
||||
RichTextCellEditing,
|
||||
} from '@blocks/database-block/properties/rich-text/cell-renderer.js';
|
||||
import { press } from '@inline/__tests__/utils.js';
|
||||
import { ZERO_WIDTH_SPACE } from '@inline/consts.js';
|
||||
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 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);
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { press } from '@inline/__tests__/utils.js';
|
||||
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, 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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { press } from '@inline/__tests__/utils.js';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
initDatabaseDynamicRowWithData,
|
||||
initEmptyDatabaseState,
|
||||
} from '../utils/actions/misc.js';
|
||||
import { test } from '../utils/playwright.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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,768 @@
|
||||
import { BLOCK_CHILDREN_CONTAINER_PADDING_LEFT } from '@blocks/_common/consts.js';
|
||||
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 { 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-drag-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-drag-indicator')).toBeHidden();
|
||||
await assertRichTexts(page, ['789', '123', '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-drag-indicator')).toBeHidden();
|
||||
await assertRichTexts(page, ['1', '2', '22', '23', '21', '3']);
|
||||
|
||||
// FIXME(DND)
|
||||
// await dragHandleFromBlockToBlockBottomById(page, '3', '8');
|
||||
// await expect(page.locator('.affine-drag-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-drag-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-drag-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-drag-indicator')).toBeHidden();
|
||||
|
||||
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
||||
`${testInfo.title}_drag_3_9.json`
|
||||
);
|
||||
|
||||
// FIXME(DND)
|
||||
// await dragHandleFromBlockToBlockBottomById(
|
||||
// page,
|
||||
// '4',
|
||||
// '3',
|
||||
// true,
|
||||
// -(1 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
|
||||
// );
|
||||
// await expect(page.locator('.affine-drag-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-drag-indicator')).toBeHidden();
|
||||
//
|
||||
// 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-drag-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-drag-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 doc.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 doc.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);
|
||||
});
|
||||
@@ -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, 225, 225]);
|
||||
|
||||
// arrange
|
||||
await triggerComponentToolbarAction(page, 'autoArrange');
|
||||
await waitNextFrame(page, 200);
|
||||
await assertEdgelessSelectedModelRect(page, [0, 0, 170, 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, 225, 225]);
|
||||
|
||||
// arrange
|
||||
await triggerComponentToolbarAction(page, 'autoResize');
|
||||
await waitNextFrame(page, 200);
|
||||
await assertEdgelessSelectedModelRect(page, [0, 0, 604.6, 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, 1421.5, 420]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
import { DEFAULT_NOTE_BACKGROUND_COLOR } from '@blocksuite/affine-model';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
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, {
|
||||
flags: {
|
||||
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');
|
||||
const lineColor = '--affine-palette-line-red';
|
||||
await changeShapeStrokeColor(page, lineColor);
|
||||
await triggerComponentToolbarAction(page, 'changeShapeFillColor');
|
||||
const color = '--affine-palette-shape-green';
|
||||
await changeShapeFillColor(page, color);
|
||||
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,
|
||||
DEFAULT_NOTE_BACKGROUND_COLOR
|
||||
);
|
||||
|
||||
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, lineColor);
|
||||
|
||||
// select note block
|
||||
await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
||||
await waitNextFrame(page);
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeNoteColor');
|
||||
const noteColor = '--affine-note-background-red';
|
||||
await changeEdgelessNoteBackground(page, noteColor);
|
||||
|
||||
// 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, noteColor);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
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 { 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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,358 @@
|
||||
import {
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
} from '@blocksuite/affine-model';
|
||||
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,
|
||||
toggleEditorReadonly,
|
||||
ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH,
|
||||
zoomByMouseWheel,
|
||||
zoomResetByKeyboard,
|
||||
} from '../utils/actions/edgeless.js';
|
||||
import {
|
||||
addBasicBrushElement,
|
||||
addBasicRectShapeElement,
|
||||
captureHistory,
|
||||
clickView,
|
||||
enterPlaygroundRoom,
|
||||
focusRichText,
|
||||
initEmptyEdgelessState,
|
||||
redoByClick,
|
||||
type,
|
||||
undoByClick,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/index.js';
|
||||
import {
|
||||
assertEdgelessNonSelectedRect,
|
||||
assertEdgelessSelectedModelRect,
|
||||
assertEdgelessSelectedRect,
|
||||
assertNoteXYWH,
|
||||
assertRichTextInlineRange,
|
||||
assertRichTexts,
|
||||
assertSelectedBound,
|
||||
assertZoomLevel,
|
||||
} from '../utils/asserts.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 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 toggleEditorReadonly(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 toggleEditorReadonly(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();
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
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');
|
||||
const color = '--affine-palette-line-blue';
|
||||
await selectBrushColor(page, color);
|
||||
|
||||
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]]);
|
||||
|
||||
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');
|
||||
const color = '--affine-palette-line-blue';
|
||||
await selectBrushColor(page, color);
|
||||
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]]);
|
||||
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');
|
||||
const color = '--affine-palette-line-blue';
|
||||
await selectBrushColor(page, color);
|
||||
|
||||
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],
|
||||
]);
|
||||
|
||||
await assertEdgelessColorSameWithHexColor(page, color, topEdge);
|
||||
await assertEdgelessColorSameWithHexColor(page, color, bottomEdge);
|
||||
assertSameColor(nearTopEdge, '#4f90ff');
|
||||
assertSameColor(nearBottomEdge, '#4f90ff');
|
||||
});
|
||||
|
||||
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]);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,368 @@
|
||||
import { parseStringToRgba } from '@blocks/root-block/edgeless/components/color-picker/utils.js';
|
||||
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 { 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');
|
||||
}
|
||||
|
||||
function getCurrentColor(locator: Locator) {
|
||||
return locator.evaluate(ele =>
|
||||
getComputedStyle(ele).getPropertyValue('background-color')
|
||||
);
|
||||
}
|
||||
|
||||
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).toHaveCSS('background-color', 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 });
|
||||
});
|
||||
});
|
||||
@@ -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])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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, '--affine-palette-line-teal');
|
||||
|
||||
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, '--affine-palette-line-teal');
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,596 @@
|
||||
import type { EdgelessTextBlockComponent } from '@blocks/edgeless-text-block/edgeless-text-block.js';
|
||||
import { Bound } from '@blocksuite/global/utils';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
import {
|
||||
autoFit,
|
||||
captureHistory,
|
||||
cutByKeyboard,
|
||||
dragBetweenIndices,
|
||||
enterPlaygroundRoom,
|
||||
getEdgelessSelectedRect,
|
||||
getPageSnapshot,
|
||||
initEmptyEdgelessState,
|
||||
pasteByKeyboard,
|
||||
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, {
|
||||
flags: {
|
||||
enable_edgeless_text: true,
|
||||
},
|
||||
});
|
||||
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, 50, 26));
|
||||
|
||||
await type(page, 'aaaaaaaaaa');
|
||||
await waitNextFrame(page, 1000);
|
||||
// just width changed
|
||||
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 83, 26));
|
||||
|
||||
await type(page, '\nbbb');
|
||||
// width not changed, height changed
|
||||
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 83, 50));
|
||||
await type(page, '\ncccccccccccccccc');
|
||||
|
||||
// width and height changed
|
||||
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 131, 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,
|
||||
});
|
||||
await type(page, 'dddddddddddddddddddd');
|
||||
// width not changed, height changed
|
||||
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 131, 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 model = await page.evaluate(() => {
|
||||
const block = window.host.view.getBlock(
|
||||
'4'
|
||||
) as EdgelessTextBlockComponent;
|
||||
return block.model;
|
||||
});
|
||||
const bound = Bound.deserialize(model.xywh);
|
||||
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);
|
||||
|
||||
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');
|
||||
await assertRichTextInlineDeltas(
|
||||
page,
|
||||
[
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
latex: 'bbbccc',
|
||||
},
|
||||
},
|
||||
],
|
||||
1
|
||||
);
|
||||
|
||||
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 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, {
|
||||
flags: {
|
||||
enable_edgeless_text: true,
|
||||
},
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const { doc } = window;
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new doc.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);
|
||||
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`
|
||||
);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,414 @@
|
||||
import {
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
} from '@blocksuite/affine-model';
|
||||
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 { 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']);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (curController as any)['_isSelecting'];
|
||||
});
|
||||
|
||||
expect(isSelecting).toBe(false);
|
||||
});
|
||||
@@ -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
|
||||
.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
|
||||
.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.getElementsByType('group').map(s => ({
|
||||
type: s.type,
|
||||
// eslint-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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,559 @@
|
||||
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);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
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,
|
||||
zoomResetByKeyboard,
|
||||
} from 'utils/actions/edgeless.js';
|
||||
import {
|
||||
pressBackspace,
|
||||
selectAllByKeyboard,
|
||||
undoByKeyboard,
|
||||
} from 'utils/actions/keyboard.js';
|
||||
import { waitNextFrame } from 'utils/actions/misc.js';
|
||||
import {
|
||||
assertEdgelessSelectedRect,
|
||||
assertSelectedBound,
|
||||
} from 'utils/asserts.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);
|
||||
|
||||
await page.keyboard.press('m');
|
||||
await clickView(page, [0, 0]);
|
||||
await autoFit(page);
|
||||
|
||||
const { mindmapId, nodeId, nodeRect } = await page.evaluate(() => {
|
||||
const edgelessBlock = document.querySelector('affine-edgeless-root');
|
||||
if (!edgelessBlock) {
|
||||
throw new Error('edgeless block not found');
|
||||
}
|
||||
const mindmap = edgelessBlock.gfx.gfxElements.filter(
|
||||
el => 'type' in el && el.type === 'mindmap'
|
||||
)[0] as MindmapElementModel;
|
||||
const node = mindmap.tree.children[0].element;
|
||||
const rect = edgelessBlock.gfx.viewport.toViewBound(node.elementBound);
|
||||
|
||||
edgelessBlock.gfx.selection.set({ elements: [node.id] });
|
||||
|
||||
return {
|
||||
mindmapId: mindmap.id,
|
||||
nodeId: node.id,
|
||||
nodeRect: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
w: rect.w,
|
||||
h: rect.h,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await waitNextFrame(page, 100);
|
||||
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 },
|
||||
{ x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 + 120 },
|
||||
{
|
||||
steps: 50,
|
||||
}
|
||||
);
|
||||
|
||||
const secondNodeId = await page.evaluate(
|
||||
({ mindmapId }) => {
|
||||
const edgelessBlock = document.querySelector('affine-edgeless-root');
|
||||
if (!edgelessBlock) {
|
||||
throw new Error('edgeless block not found');
|
||||
}
|
||||
const mindmap = edgelessBlock.gfx.getElementById(
|
||||
mindmapId
|
||||
) as MindmapElementModel;
|
||||
|
||||
return mindmap.tree.children[1].id;
|
||||
},
|
||||
{ mindmapId, nodeId }
|
||||
);
|
||||
|
||||
expect(secondNodeId).toEqual(nodeId);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
|
||||
import {
|
||||
addNote,
|
||||
changeNoteDisplayModeWithId,
|
||||
enterPlaygroundRoom,
|
||||
initEmptyEdgelessState,
|
||||
switchEditorMode,
|
||||
zoomResetByKeyboard,
|
||||
} from '../../utils/actions/index.js';
|
||||
import { assertBlockCount } from '../../utils/asserts.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);
|
||||
});
|
||||
@@ -0,0 +1,531 @@
|
||||
import {
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
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 { 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,
|
||||
'--affine-note-background-white'
|
||||
);
|
||||
|
||||
await selectNoteInEdgeless(page, noteId);
|
||||
await triggerComponentToolbarAction(page, 'changeNoteColor');
|
||||
const color = '--affine-note-background-green';
|
||||
await changeEdgelessNoteBackground(page, color);
|
||||
await assertEdgelessNoteBackground(page, noteId, color);
|
||||
});
|
||||
|
||||
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 assertTextSelection(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']);
|
||||
});
|
||||
@@ -0,0 +1,254 @@
|
||||
import { NOTE_MIN_HEIGHT, NOTE_MIN_WIDTH } from '@blocksuite/affine-model';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
activeNoteInEdgeless,
|
||||
dragBetweenCoords,
|
||||
enterPlaygroundRoom,
|
||||
getNoteRect,
|
||||
initEmptyEdgelessState,
|
||||
redoByClick,
|
||||
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 { 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);
|
||||
const leftHandle = page.locator('.handle[aria-label="left"] .resize');
|
||||
const box = await leftHandle.boundingBox();
|
||||
if (box === null) throw new Error();
|
||||
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: box.x + 5, y: box.y + 5 },
|
||||
{ x: box.x - 95, y: box.y + 5 }
|
||||
);
|
||||
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);
|
||||
const leftHandle = page.locator('.handle[aria-label="left"] .resize');
|
||||
let box = await leftHandle.boundingBox();
|
||||
if (box === null) throw new Error();
|
||||
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: box.x + 50, y: box.y + box.height },
|
||||
{ x: box.x + 50, y: box.y + box.height + 100 }
|
||||
);
|
||||
let noteRect = await getNoteRect(page, noteId);
|
||||
await expect(page.locator('.edgeless-note-collapse-button')).toBeVisible();
|
||||
assertRectEqual(noteRect, {
|
||||
x: initRect.x,
|
||||
y: initRect.y,
|
||||
w: initRect.w,
|
||||
h: initRect.h + 100,
|
||||
});
|
||||
|
||||
await page.locator('.edgeless-note-collapse-button')!.click();
|
||||
let domRect = await page.locator('affine-edgeless-note').boundingBox();
|
||||
expect(domRect!.height).toBeCloseTo(NOTE_MIN_HEIGHT);
|
||||
|
||||
await page.locator('.edgeless-note-collapse-button')!.click();
|
||||
domRect = await page.locator('affine-edgeless-note').boundingBox();
|
||||
expect(domRect!.height).toBeCloseTo(initRect.h + 100);
|
||||
|
||||
await selectNoteInEdgeless(page, noteId);
|
||||
box = await leftHandle.boundingBox();
|
||||
if (box === null) throw new Error();
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: box.x + 50, y: box.y + box.height },
|
||||
{ x: box.x + 50, y: box.y + box.height - 150 }
|
||||
);
|
||||
noteRect = await getNoteRect(page, noteId);
|
||||
await expect(
|
||||
page.locator('.edgeless-note-collapse-button')
|
||||
).not.toBeVisible();
|
||||
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);
|
||||
const bottomRightResize = page.locator(
|
||||
'.handle[aria-label="bottom-right"] .resize'
|
||||
);
|
||||
const box = await bottomRightResize.boundingBox();
|
||||
if (box === null) throw new Error();
|
||||
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: box.x + 5, y: box.y + 5 },
|
||||
{ x: box.x + 5, y: box.y + 105 }
|
||||
);
|
||||
|
||||
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,
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
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.locator('.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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
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);
|
||||
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, {
|
||||
flags: {
|
||||
enable_edgeless_text: true,
|
||||
},
|
||||
});
|
||||
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 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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,448 @@
|
||||
import {
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
} from '@blocksuite/affine-model';
|
||||
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 { 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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
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 { 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);
|
||||
});
|
||||
@@ -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]);
|
||||
});
|
||||
@@ -0,0 +1,727 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
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');
|
||||
const color = '--affine-palette-shape-teal';
|
||||
await changeShapeFillColor(page, color);
|
||||
await page.waitForTimeout(50);
|
||||
const [picked] = await pickColorAtPoints(page, [
|
||||
[rect.start.x + 20, rect.start.y + 20],
|
||||
]);
|
||||
|
||||
await assertEdgelessColorSameWithHexColor(page, color, 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');
|
||||
const color = '--affine-palette-line-teal';
|
||||
await changeShapeStrokeColor(page, color);
|
||||
await page.waitForTimeout(50);
|
||||
const [picked] = await pickColorAtPoints(page, [
|
||||
[rect.start.x + 1, rect.start.y + 1],
|
||||
]);
|
||||
|
||||
await assertEdgelessColorSameWithHexColor(page, color, 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, '--affine-palette-line-teal');
|
||||
|
||||
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, '--affine-palette-line-teal');
|
||||
|
||||
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 = '--affine-palette-line-teal';
|
||||
await changeShapeStrokeColor(page, color);
|
||||
await page.waitForTimeout(50);
|
||||
const [picked] = await pickColorAtPoints(page, [[start.x + 1, start.y + 1]]);
|
||||
|
||||
await assertEdgelessColorSameWithHexColor(page, color, 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, {
|
||||
flags: {
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,324 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
addBasicRectShapeElement,
|
||||
assertEdgelessShapeType,
|
||||
createShapeElement,
|
||||
edgelessCommonSetup,
|
||||
getEdgelessSelectedRect,
|
||||
getZoomLevel,
|
||||
locatorEdgelessToolButton,
|
||||
setEdgelessTool,
|
||||
type ShapeName,
|
||||
switchEditorMode,
|
||||
zoomFitByKeyboard,
|
||||
zoomInByKeyboard,
|
||||
zoomOutByKeyboard,
|
||||
zoomResetByKeyboard,
|
||||
} from '../utils/actions/edgeless.js';
|
||||
import {
|
||||
clickView,
|
||||
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 page.keyboard.press('Escape');
|
||||
await waitNextFrame(page);
|
||||
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 page.keyboard.press('Escape');
|
||||
await waitNextFrame(page);
|
||||
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('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]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,316 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
import { getLinkedDocPopover } from 'utils/actions/linked-doc.js';
|
||||
|
||||
import {
|
||||
assertEdgelessTool,
|
||||
enterPlaygroundRoom,
|
||||
getEdgelessSelectedRect,
|
||||
initEmptyEdgelessState,
|
||||
pressArrowLeft,
|
||||
pressEnter,
|
||||
setEdgelessTool,
|
||||
SHORT_KEY,
|
||||
switchEditorMode,
|
||||
type,
|
||||
waitForInlineEditorStateUpdated,
|
||||
waitNextFrame,
|
||||
zoomResetByKeyboard,
|
||||
} from '../utils/actions/index.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, {
|
||||
flags: {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
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 doc.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 doc.Text('Doc 2'),
|
||||
});
|
||||
|
||||
const noteId2 = doc2.addBlock('affine:note', {}, rootId2);
|
||||
doc2.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.Text('Hello from Doc 2'),
|
||||
},
|
||||
noteId2
|
||||
);
|
||||
|
||||
const doc3 = collection.createDoc({ id: 'doc3' });
|
||||
doc3.load();
|
||||
const rootId3 = doc3.addBlock('affine:page', {
|
||||
title: new doc.Text('Doc 3'),
|
||||
});
|
||||
|
||||
const noteId3 = doc3.addBlock('affine:note', {}, rootId3);
|
||||
doc3.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new doc.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!.getDoc();
|
||||
const [noteBlock] = doc2!.getBlocksByFlavour('affine:note');
|
||||
const noteId = noteBlock.id;
|
||||
|
||||
const databaseId = doc2.addBlock(
|
||||
'affine:database',
|
||||
{
|
||||
title: new doc2.Text('Database 1'),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
const model = doc2.getBlockById(databaseId) as DatabaseBlockModel;
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
const databaseBlock = document.querySelector('affine-database');
|
||||
const databaseService = databaseBlock?.service;
|
||||
if (databaseService) {
|
||||
databaseService.databaseViewInitEmpty(
|
||||
model,
|
||||
databaseService.viewPresets.tableViewMeta.type
|
||||
);
|
||||
databaseService.applyColumnUpdate(model);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 indicatorCount = await page
|
||||
.locator('affine-drag-indicator')
|
||||
.count();
|
||||
expect(indicatorCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 96 B |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,356 @@
|
||||
import { expect, type Locator, type Page } from '@playwright/test';
|
||||
import { dragBetweenCoords } from 'utils/actions/drag.js';
|
||||
import {
|
||||
assertEdgelessNonSelectedRect,
|
||||
assertEdgelessSelectedRect,
|
||||
assertZoomLevel,
|
||||
} from 'utils/asserts.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 { 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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,361 @@
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { expect, type Locator, type Page } from '@playwright/test';
|
||||
import {
|
||||
addNote,
|
||||
changeNoteDisplayModeWithId,
|
||||
switchEditorMode,
|
||||
triggerComponentToolbarAction,
|
||||
zoomResetByKeyboard,
|
||||
} from 'utils/actions/edgeless.js';
|
||||
import { pressBackspace, pressEnter, type } from 'utils/actions/keyboard.js';
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
focusRichTextEnd,
|
||||
focusTitle,
|
||||
getEditorHostLocator,
|
||||
initEmptyEdgelessState,
|
||||
initEmptyParagraphState,
|
||||
waitNextFrame,
|
||||
} from 'utils/actions/misc.js';
|
||||
import { assertRichTexts } from 'utils/asserts.js';
|
||||
|
||||
import { test } from '../../utils/playwright.js';
|
||||
import {
|
||||
createHeadingsWithGap,
|
||||
getVerticalCenterFromLocator,
|
||||
} from './utils.js';
|
||||
|
||||
test.describe('toc-panel', () => {
|
||||
async function toggleTocPanel(page: Page) {
|
||||
await page.click('sl-button:text("Test Operations")');
|
||||
await page.click('sl-menu-item:text("Toggle Outline Panel")');
|
||||
await waitNextFrame(page);
|
||||
const panel = page.locator('affine-outline-panel');
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
function getHeading(panel: Locator, level: number) {
|
||||
return panel.locator(`affine-outline-panel-body .h${level} > span`);
|
||||
}
|
||||
|
||||
function getTitle(panel: Locator) {
|
||||
return panel.locator(`affine-outline-panel-body .title`);
|
||||
}
|
||||
|
||||
async function toggleNoteSorting(page: Page) {
|
||||
const enableSortingButton = page.locator(
|
||||
'.outline-panel-header-container .note-sorting-button'
|
||||
);
|
||||
await enableSortingButton.click();
|
||||
}
|
||||
|
||||
async function dragNoteCard(page: Page, fromCard: Locator, toCard: Locator) {
|
||||
const fromRect = await fromCard.boundingBox();
|
||||
const toRect = await toCard.boundingBox();
|
||||
|
||||
await page.mouse.move(fromRect!.x + 10, fromRect!.y + 10);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
test('should display placeholder when no headings', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const panel = await toggleTocPanel(page);
|
||||
|
||||
const noHeadingPlaceholder = panel.locator('.note-placeholder');
|
||||
|
||||
await focusTitle(page);
|
||||
await type(page, 'Title');
|
||||
await focusRichTextEnd(page);
|
||||
await type(page, 'Hello World');
|
||||
|
||||
await expect(noHeadingPlaceholder).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not display empty when there are only empty headings', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const panel = await toggleTocPanel(page);
|
||||
|
||||
await focusTitle(page);
|
||||
await type(page, 'Title');
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
// heading 1 to 6
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await type(page, `${'#'.repeat(i)} `);
|
||||
await pressEnter(page);
|
||||
await expect(getHeading(panel, i)).toBeHidden();
|
||||
}
|
||||
|
||||
// Title also should be hidden
|
||||
await expect(getTitle(panel)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should display title and headings when there are non-empty headings in editor', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const panel = await toggleTocPanel(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
// heading 1 to 6
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await type(page, `${'#'.repeat(i)} `);
|
||||
await type(page, `Heading ${i}`);
|
||||
await pressEnter(page);
|
||||
|
||||
const heading = getHeading(panel, i);
|
||||
await expect(heading).toBeVisible();
|
||||
await expect(heading).toContainText(`Heading ${i}`);
|
||||
}
|
||||
|
||||
const title = getTitle(panel);
|
||||
await expect(title).toBeHidden();
|
||||
await focusTitle(page);
|
||||
await type(page, 'Title');
|
||||
await expect(title).toHaveText('Title');
|
||||
|
||||
// heading 1 to 6
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
const heading = getHeading(panel, i);
|
||||
await expect(heading).toBeVisible();
|
||||
await expect(heading).toContainText(`Heading ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('should update headings', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const panel = await toggleTocPanel(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
const h1 = getHeading(panel, 1);
|
||||
|
||||
await type(page, '# Heading 1');
|
||||
await expect(h1).toContainText('Heading 1');
|
||||
|
||||
await pressBackspace(page, 'Heading 1'.length);
|
||||
await expect(h1).toBeHidden();
|
||||
await type(page, 'Hello World');
|
||||
await expect(h1).toContainText('Hello World');
|
||||
|
||||
const title = getTitle(panel);
|
||||
|
||||
await focusTitle(page);
|
||||
await type(page, 'Title');
|
||||
await expect(title).toContainText('Title');
|
||||
|
||||
await pressBackspace(page, 2);
|
||||
await expect(title).toContainText('Tit');
|
||||
});
|
||||
|
||||
test('should add padding to sub-headings', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const panel = await toggleTocPanel(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
await type(page, '# Heading 1');
|
||||
await pressEnter(page);
|
||||
|
||||
await type(page, '## Heading 2');
|
||||
await pressEnter(page);
|
||||
|
||||
const h1 = getHeading(panel, 1);
|
||||
const h2 = getHeading(panel, 2);
|
||||
|
||||
const h1Rect = await h1.boundingBox();
|
||||
const h2Rect = await h2.boundingBox();
|
||||
|
||||
expect(h1Rect).not.toBeNull();
|
||||
expect(h2Rect).not.toBeNull();
|
||||
|
||||
expect(h1Rect!.x).toBeLessThan(h2Rect!.x);
|
||||
});
|
||||
|
||||
test('should highlight heading when scroll to area before viewport center', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const editor = getEditorHostLocator(page);
|
||||
const panel = await toggleTocPanel(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
const headings = await createHeadingsWithGap(page);
|
||||
await editor.locator('.inline-editor').first().scrollIntoViewIfNeeded();
|
||||
|
||||
const viewportCenter = await getVerticalCenterFromLocator(
|
||||
page.locator('body')
|
||||
);
|
||||
|
||||
const activeHeadingContainer = panel.locator(
|
||||
'affine-outline-panel-body .active'
|
||||
);
|
||||
|
||||
for (let i = 0; i < headings.length; i++) {
|
||||
const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]);
|
||||
await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 50);
|
||||
await waitNextFrame(page);
|
||||
await expect(activeHeadingContainer).toContainText(`Heading ${i + 1}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('should scroll to heading and highlight heading when click item in outline panel', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const panel = await toggleTocPanel(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
const headings = await createHeadingsWithGap(page);
|
||||
const activeHeadingContainer = panel.locator(
|
||||
'affine-outline-panel-body .active'
|
||||
);
|
||||
|
||||
const headingsInPanel = Array.from({ length: 6 }, (_, i) =>
|
||||
getHeading(panel, i + 1)
|
||||
);
|
||||
|
||||
await headingsInPanel[2].click();
|
||||
await expect(headings[2]).toBeVisible();
|
||||
await expect(activeHeadingContainer).toContainText('Heading 3');
|
||||
});
|
||||
|
||||
test('should scroll to title when click title in outline panel', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const panel = await toggleTocPanel(page);
|
||||
|
||||
await focusTitle(page);
|
||||
await type(page, 'Title');
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
await createHeadingsWithGap(page);
|
||||
|
||||
const title = page.locator('doc-title');
|
||||
const titleInPanel = getTitle(panel);
|
||||
|
||||
await expect(title).not.toBeInViewport();
|
||||
await titleInPanel.click();
|
||||
await waitNextFrame(page, 50);
|
||||
await expect(title).toBeVisible();
|
||||
});
|
||||
|
||||
test('should update notes when change note display mode from note toolbar', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
await switchEditorMode(page);
|
||||
await zoomResetByKeyboard(page);
|
||||
const noteId = await addNote(page, 'hello', 300, 300);
|
||||
await page.mouse.click(100, 100);
|
||||
|
||||
await toggleTocPanel(page);
|
||||
await toggleNoteSorting(page);
|
||||
const docVisibleCard = page.locator(
|
||||
'.card-container[data-invisible="false"]'
|
||||
);
|
||||
const docInvisibleCard = page.locator(
|
||||
'.card-container[data-invisible="true"]'
|
||||
);
|
||||
|
||||
await expect(docVisibleCard).toHaveCount(1);
|
||||
await expect(docInvisibleCard).toHaveCount(1);
|
||||
|
||||
await changeNoteDisplayModeWithId(
|
||||
page,
|
||||
noteId,
|
||||
NoteDisplayMode.DocAndEdgeless
|
||||
);
|
||||
|
||||
await expect(docVisibleCard).toHaveCount(2);
|
||||
await expect(docInvisibleCard).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should reorder notes when drag and drop note in outline panel', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
await switchEditorMode(page);
|
||||
await zoomResetByKeyboard(page);
|
||||
const note1 = await addNote(page, 'hello', 300, 300);
|
||||
const note2 = await addNote(page, 'world', 300, 500);
|
||||
await page.mouse.click(100, 100);
|
||||
|
||||
await changeNoteDisplayModeWithId(
|
||||
page,
|
||||
note1,
|
||||
NoteDisplayMode.DocAndEdgeless
|
||||
);
|
||||
await changeNoteDisplayModeWithId(
|
||||
page,
|
||||
note2,
|
||||
NoteDisplayMode.DocAndEdgeless
|
||||
);
|
||||
|
||||
await toggleTocPanel(page);
|
||||
await toggleNoteSorting(page);
|
||||
const docVisibleCard = page.locator(
|
||||
'.card-container[data-invisible="false"]'
|
||||
);
|
||||
|
||||
await expect(docVisibleCard).toHaveCount(3);
|
||||
await assertRichTexts(page, ['', 'hello', 'world']);
|
||||
|
||||
const noteCard3 = docVisibleCard.nth(2);
|
||||
const noteCard1 = docVisibleCard.nth(0);
|
||||
|
||||
await dragNoteCard(page, noteCard3, noteCard1);
|
||||
|
||||
await waitNextFrame(page);
|
||||
await assertRichTexts(page, ['world', '', 'hello']);
|
||||
});
|
||||
|
||||
test('should update notes after slicing note', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
await switchEditorMode(page);
|
||||
await zoomResetByKeyboard(page);
|
||||
const note1 = await addNote(page, 'hello', 100, 300);
|
||||
await pressEnter(page);
|
||||
await type(page, 'world');
|
||||
await page.mouse.click(100, 100);
|
||||
|
||||
await changeNoteDisplayModeWithId(
|
||||
page,
|
||||
note1,
|
||||
NoteDisplayMode.DocAndEdgeless
|
||||
);
|
||||
|
||||
await toggleTocPanel(page);
|
||||
await toggleNoteSorting(page);
|
||||
const docVisibleCard = page.locator(
|
||||
'.card-container[data-invisible="false"]'
|
||||
);
|
||||
|
||||
await expect(docVisibleCard).toHaveCount(2);
|
||||
|
||||
await triggerComponentToolbarAction(page, 'changeNoteSlicerSetting');
|
||||
await expect(page.locator('.note-slicer-button')).toBeVisible();
|
||||
await page.locator('.note-slicer-button').click();
|
||||
|
||||
await expect(docVisibleCard).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
import { noop } from '@blocksuite/global/utils';
|
||||
import type { OutlineViewer } from '@blocksuite/presets';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
import { addNote, switchEditorMode } from 'utils/actions/edgeless.js';
|
||||
import { pressEnter, type } from 'utils/actions/keyboard.js';
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
focusRichTextEnd,
|
||||
focusTitle,
|
||||
getEditorLocator,
|
||||
initEmptyEdgelessState,
|
||||
initEmptyParagraphState,
|
||||
waitNextFrame,
|
||||
} from 'utils/actions/misc.js';
|
||||
|
||||
import { test } from '../../utils/playwright.js';
|
||||
import {
|
||||
createHeadingsWithGap,
|
||||
getVerticalCenterFromLocator,
|
||||
} from './utils.js';
|
||||
|
||||
test.describe('toc-viewer', () => {
|
||||
async function toggleTocViewer(page: Page) {
|
||||
await page.click('sl-button:text("Test Operations")');
|
||||
await page.click('sl-menu-item:text("Enable Outline Viewer")');
|
||||
await waitNextFrame(page);
|
||||
const viewer = page.locator('affine-outline-viewer');
|
||||
return viewer;
|
||||
}
|
||||
|
||||
function getIndicators(page: Page) {
|
||||
return page.locator('affine-outline-viewer .outline-viewer-indicator');
|
||||
}
|
||||
|
||||
test('should display highlight indicators when non-empty headings exists', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
await toggleTocViewer(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
const indicators = getIndicators(page);
|
||||
|
||||
// heading 1 to 6
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await type(page, `${'#'.repeat(i)} `);
|
||||
await type(page, `Heading ${i}`);
|
||||
await pressEnter(page);
|
||||
|
||||
await expect(indicators.nth(i - 1)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should be hidden when only empty headings exists', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
await toggleTocViewer(page);
|
||||
|
||||
await focusTitle(page);
|
||||
await type(page, 'Title');
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
const indicators = getIndicators(page);
|
||||
|
||||
// heading 1 to 6
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await type(page, `${'#'.repeat(i)} `);
|
||||
await pressEnter(page);
|
||||
await expect(indicators).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('should display outline content when hovering over indicators', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
await toggleTocViewer(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
await type(page, '# Heading 1');
|
||||
await pressEnter(page);
|
||||
|
||||
const indicator = getIndicators(page).first();
|
||||
await indicator.hover({ force: true });
|
||||
|
||||
const items = page.locator('.outline-viewer-item');
|
||||
await expect(items).toHaveCount(2);
|
||||
await expect(items.nth(0)).toContainText(['Table of Contents']);
|
||||
await expect(items.nth(1)).toContainText(['Heading 1']);
|
||||
});
|
||||
|
||||
test('should highlight indicator when scrolling', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
await toggleTocViewer(page);
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
const editor = getEditorLocator(page);
|
||||
const indicators = getIndicators(page);
|
||||
const headings = await createHeadingsWithGap(page);
|
||||
await editor.locator('.inline-editor').first().scrollIntoViewIfNeeded();
|
||||
|
||||
const viewportCenter = await getVerticalCenterFromLocator(
|
||||
page.locator('body')
|
||||
);
|
||||
for (let i = 0; i < headings.length; i++) {
|
||||
const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]);
|
||||
await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 50);
|
||||
await expect(indicators.nth(i)).toHaveClass(/active/);
|
||||
}
|
||||
});
|
||||
|
||||
test('should highlight indicator when click item in outline panel', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const viewer = await toggleTocViewer(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
const headings = await createHeadingsWithGap(page);
|
||||
|
||||
const indicators = getIndicators(page);
|
||||
await indicators.first().hover({ force: true });
|
||||
|
||||
const headingsInPanel = Array.from({ length: 6 }, (_, i) =>
|
||||
viewer.locator(`.h${i + 1} > span`)
|
||||
);
|
||||
|
||||
await headingsInPanel[2].click();
|
||||
await expect(headings[2]).toBeVisible();
|
||||
await expect(indicators.nth(2)).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test('should hide in edgeless mode', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
await toggleTocViewer(page);
|
||||
|
||||
const indicators = getIndicators(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
await type(page, '# Heading 1');
|
||||
await pressEnter(page);
|
||||
|
||||
await expect(indicators).toHaveCount(1);
|
||||
|
||||
await switchEditorMode(page);
|
||||
|
||||
await expect(indicators).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should hide edgeless-only note headings', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
const viewer = await toggleTocViewer(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
|
||||
await type(page, '# Heading 1');
|
||||
await pressEnter(page);
|
||||
|
||||
await type(page, '## Heading 2');
|
||||
await pressEnter(page);
|
||||
|
||||
await switchEditorMode(page);
|
||||
|
||||
await addNote(page, '# Edgeless', 300, 300);
|
||||
|
||||
await switchEditorMode(page);
|
||||
|
||||
const indicators = getIndicators(page);
|
||||
await expect(indicators).toHaveCount(2);
|
||||
|
||||
await indicators.first().hover({ force: true });
|
||||
|
||||
await expect(viewer).toBeVisible();
|
||||
const hiddenTitle = viewer.locator('.hidden-title');
|
||||
await expect(hiddenTitle).toBeHidden();
|
||||
});
|
||||
|
||||
test('outline panel toggle button', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyParagraphState(page);
|
||||
const viewer = await toggleTocViewer(page);
|
||||
|
||||
await focusRichTextEnd(page);
|
||||
await createHeadingsWithGap(page);
|
||||
|
||||
const toggleButton = viewer.locator(
|
||||
'[data-testid="toggle-outline-panel-button"]'
|
||||
);
|
||||
await expect(toggleButton).toHaveCount(0);
|
||||
await viewer.evaluate((el: OutlineViewer) => {
|
||||
el.toggleOutlinePanel = () => {
|
||||
noop();
|
||||
};
|
||||
});
|
||||
|
||||
await waitNextFrame(page);
|
||||
await expect(toggleButton).toHaveCount(1);
|
||||
await expect(toggleButton).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { expect, type Locator, type Page } from '@playwright/test';
|
||||
import { pressEnter, type } from 'utils/actions/keyboard.js';
|
||||
import { getEditorHostLocator } from 'utils/actions/misc.js';
|
||||
|
||||
export async function getVerticalCenterFromLocator(locator: Locator) {
|
||||
const rect = await locator.boundingBox();
|
||||
return rect!.y + rect!.height / 2;
|
||||
}
|
||||
|
||||
export async function createHeadingsWithGap(page: Page) {
|
||||
// heading 1 to 6
|
||||
const editor = getEditorHostLocator(page);
|
||||
|
||||
const headings: Locator[] = [];
|
||||
await pressEnter(page, 10);
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await type(page, `${'#'.repeat(i)} `);
|
||||
await type(page, `Heading ${i}`);
|
||||
const heading = editor.locator(`.h${i}`);
|
||||
await expect(heading).toBeVisible();
|
||||
headings.push(heading);
|
||||
await pressEnter(page, 10);
|
||||
}
|
||||
await pressEnter(page, 10);
|
||||
|
||||
return headings;
|
||||
}
|
||||
@@ -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, ['())']);
|
||||
});
|
||||
@@ -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`
|
||||
);
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
@@ -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', '']);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
@@ -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`
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
import { ZERO_WIDTH_SPACE } from '@inline/consts.js';
|
||||
import { expect } from '@playwright/test';
|
||||
import {
|
||||
assertRichTextInlineDeltas,
|
||||
assertRichTextInlineRange,
|
||||
} from 'utils/asserts.js';
|
||||
|
||||
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 { 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 & b & c \\\\ \\\\ hline\nd & e & f \\\\\n\\hdashline\ng & h & 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('ababababababababababbabab');
|
||||
/**
|
||||
* abababababababababab|babab
|
||||
*/
|
||||
await pressArrowRight(page, 2);
|
||||
/**
|
||||
* ababababababababababba|bab
|
||||
*/
|
||||
await pasteByKeyboard(page);
|
||||
expect(await latexEditorLine.innerText()).toBe(
|
||||
'ababababababababababbaabababababababababababababab'
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,543 @@
|
||||
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,
|
||||
assertStoreMatchJSX,
|
||||
} 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 doc.Text('title'),
|
||||
});
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
|
||||
const text = new doc.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,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const id = await createLinkBlock(page, 'link text', 'http://example.com');
|
||||
await focusRichText(page, 0);
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await type(page, 'IN_LINK');
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text={
|
||||
<>
|
||||
<text
|
||||
insert="Hello"
|
||||
/>
|
||||
<text
|
||||
insert="link texIN_LINKt"
|
||||
link="http://example.com"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
prop:type="text"
|
||||
/>`,
|
||||
id
|
||||
);
|
||||
});
|
||||
|
||||
test('type character after link should not extend the link attributes', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const id = await createLinkBlock(page, 'link text', 'http://example.com');
|
||||
await focusRichText(page, 0);
|
||||
await type(page, 'AFTER_LINK');
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text={
|
||||
<>
|
||||
<text
|
||||
insert="Hello"
|
||||
/>
|
||||
<text
|
||||
insert="link text"
|
||||
link="http://example.com"
|
||||
/>
|
||||
<text
|
||||
insert="AFTER_LINK"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
prop:type="text"
|
||||
/>`,
|
||||
id
|
||||
);
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
const linkText = 'linkText';
|
||||
const link = 'http://example.com';
|
||||
await enterPlaygroundRoom(page);
|
||||
const { paragraphId } = 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
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="linkText"
|
||||
prop:type="text"
|
||||
/>`,
|
||||
paragraphId
|
||||
);
|
||||
|
||||
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 }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const { paragraphId } = 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();
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text={
|
||||
<>
|
||||
<text
|
||||
insert="aaa"
|
||||
link="http://affine.pro"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
prop:type="text"
|
||||
/>`,
|
||||
paragraphId
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
//TODO: wait for embed block completed
|
||||
test.skip('convert link to embed', async ({ page }) => {
|
||||
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);
|
||||
|
||||
await assertStoreMatchJSX(
|
||||
page,
|
||||
`
|
||||
<affine:page>
|
||||
<affine:note
|
||||
prop:background="--affine-note-background-blue"
|
||||
prop:displayMode="both"
|
||||
prop:edgeless={
|
||||
Object {
|
||||
"style": Object {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box",
|
||||
},
|
||||
}
|
||||
}
|
||||
prop:hidden={false}
|
||||
prop:index="a0"
|
||||
prop:lockedBySelf={false}
|
||||
>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text="aaa"
|
||||
prop:type="text"
|
||||
/>
|
||||
<affine:paragraph
|
||||
prop:collapsed={false}
|
||||
prop:text={
|
||||
<>
|
||||
<text
|
||||
insert="a"
|
||||
/>
|
||||
<text
|
||||
insert="linkText"
|
||||
link="${link}"
|
||||
/>
|
||||
<text
|
||||
insert="a"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
prop:type="text"
|
||||
/>
|
||||
</affine:note>
|
||||
</affine:page>`
|
||||
);
|
||||
|
||||
const linkToCardBtn = page.getByTestId('link-to-card');
|
||||
const linkToEmbedBtn = page.getByTestId('link-to-embed');
|
||||
const linkLocator = page.locator('affine-link a');
|
||||
|
||||
await linkLocator.hover();
|
||||
await waitNextFrame(page);
|
||||
await expect(linkPopoverLocator).toBeVisible();
|
||||
await expect(linkToCardBtn).toBeVisible();
|
||||
await expect(linkToEmbedBtn).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 expect(linkToCardBtn).not.toBeVisible();
|
||||
await expect(linkToEmbedBtn).not.toBeVisible();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,842 @@
|
||||
import { expect, type Locator } from '@playwright/test';
|
||||
import { getFormatBar } from 'utils/query.js';
|
||||
|
||||
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';
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,535 @@
|
||||
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, ['']);
|
||||
|
||||
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']);
|
||||
});
|
||||
@@ -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]);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@blocksuite/legacy-e2e",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "yarn playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/block-std": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/presets": "workspace:*",
|
||||
"@playwright/test": "=1.49.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/toeverything/blocksuite.git"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
import process from 'node:process';
|
||||
|
||||
import type { PlaywrightWorkerOptions } from '@playwright/test';
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: '.',
|
||||
timeout: process.env.CI ? 40000 : 999999,
|
||||
fullyParallel: true,
|
||||
snapshotDir: 'snapshots',
|
||||
snapshotPathTemplate: 'snapshots/{testFilePath}/{arg}{ext}',
|
||||
webServer: {
|
||||
command: process.env.CI
|
||||
? 'yarn workspace @blocksuite/playground run preview'
|
||||
: 'yarn workspace @blocksuite/playground run dev',
|
||||
port: process.env.CI ? 4173 : 5173,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
env: {
|
||||
COVERAGE: process.env.COVERAGE ?? '',
|
||||
},
|
||||
},
|
||||
use: {
|
||||
browserName:
|
||||
(process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ??
|
||||
'chromium',
|
||||
viewport: { width: 960, height: 900 },
|
||||
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
|
||||
// You can open traces locally(`npx playwright show-trace trace.zip`)
|
||||
// or in your browser on [Playwright Trace Viewer](https://trace.playwright.dev/).
|
||||
trace: 'on-first-retry',
|
||||
// Record video only when retrying a test for the first time.
|
||||
video: 'on-first-retry',
|
||||
// Timeout for each action
|
||||
actionTimeout: 5_000,
|
||||
permissions:
|
||||
process.env.BROWSER && process.env.BROWSER !== 'chromium'
|
||||
? []
|
||||
: ['clipboard-read', 'clipboard-write'],
|
||||
},
|
||||
workers: '80%',
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
// 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'
|
||||
// default 'list' when running locally
|
||||
// See https://playwright.dev/docs/test-reporters#github-actions-annotations
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
});
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user