chore: migrate blocksuite test (#9222)

This commit is contained in:
Saul-Mirone
2024-12-20 11:08:21 +00:00
parent f788fdd0a4
commit bfcc53dc1f
325 changed files with 55958 additions and 26 deletions
@@ -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);
});
});