From a090606860ebc6d9bd61d14c47060def201df69b Mon Sep 17 00:00:00 2001 From: Hongtao Lye Date: Fri, 20 Dec 2024 00:10:01 +0800 Subject: [PATCH] test: add test --- .../model/src/elements/mindmap/mindmap.ts | 11 + .../edgeless/edgeless-root-block.ts | 4 + tests/edgeless/mindmap.spec.ts | 322 +++++++++++++++--- tests/utils/actions/edgeless.ts | 27 ++ tests/utils/mindmap.ts | 130 +++++++ 5 files changed, 455 insertions(+), 39 deletions(-) create mode 100644 tests/utils/mindmap.ts diff --git a/packages/affine/model/src/elements/mindmap/mindmap.ts b/packages/affine/model/src/elements/mindmap/mindmap.ts index 0c13b7bad541..25c80623e6b4 100644 --- a/packages/affine/model/src/elements/mindmap/mindmap.ts +++ b/packages/affine/model/src/elements/mindmap/mindmap.ts @@ -626,6 +626,17 @@ export class MindmapElementModel extends GfxGroupLikeElementModel { await edgelessCommonSetup(page); await zoomResetByKeyboard(page); - await page.keyboard.press('m'); - await clickView(page, [0, 0]); - await autoFit(page); + const mindmapId = await createMindMap(page, [0, 0]); + const { id: nodeId, rect: nodeRect } = await getMindMapNode( + page, + mindmapId, + [0, 0] + ); + const { rect: targetRect } = await getMindMapNode(page, mindmapId, [0, 1]); + const { rect: lastRect } = await getMindMapNode(page, mindmapId, [0, 2]); - const { mindmapId, nodeId, nodeRect } = await page.evaluate(() => { - const edgelessBlock = document.querySelector('affine-edgeless-root'); - if (!edgelessBlock) { - throw new Error('edgeless block not found'); + await selectElementInEdgeless(page, [nodeId]); + await dragBetweenCoords( + page, + { x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 }, + { x: targetRect.x + targetRect.w / 2, y: targetRect.y + targetRect.h + 40 }, + { + steps: 50, } - 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); + ); + expect((await getMindMapNode(page, mindmapId, [0, 1])).id).toEqual(nodeId); - edgelessBlock.gfx.selection.set({ elements: [node.id] }); + await dragBetweenCoords( + page, + { x: targetRect.x + targetRect.w / 2, y: targetRect.y + targetRect.h / 2 }, + { x: nodeRect.x - 20, y: nodeRect.y - 40 }, + { + steps: 50, + } + ); + expect((await getMindMapNode(page, mindmapId, [0, 0])).id).toEqual(nodeId); - return { - mindmapId: mindmap.id, - nodeId: node.id, - nodeRect: { - x: rect.x, - y: rect.y, - w: rect.w, - h: rect.h, + await dragBetweenCoords( + page, + { x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 }, + { x: lastRect.x - 20, y: lastRect.y + lastRect.h + 40 }, + { + steps: 50, + } + ); + expect((await getMindMapNode(page, mindmapId, [0, 2])).id).toEqual(nodeId); +}); + +test('drag mind map node to make it a child node', async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); + + const mindmapId = await createMindMap(page, [0, 0]); + + { + const { id: nodeId, rect: nodeRect } = await getMindMapNode( + page, + mindmapId, + [0, 0] + ); + const { rect: targetRect } = await getMindMapNode(page, mindmapId, [0, 1]); + + await selectElementInEdgeless(page, [nodeId]); + await dragBetweenCoords( + page, + { x: nodeRect.x + nodeRect.w / 2, y: nodeRect.y + nodeRect.h / 2 }, + { + x: targetRect.x + targetRect.w / 2, + y: targetRect.y + targetRect.h / 2, }, - }; + { + steps: 50, + } + ); + expect((await getMindMapNode(page, mindmapId, [0, 0, 0])).id).toEqual( + nodeId + ); + } + + { + const { id: childId } = await getMindMapNode(page, mindmapId, [0, 0, 0]); + const { rect: firstRect } = await getMindMapNode(page, mindmapId, [0, 0]); + const { rect: secondRect } = await getMindMapNode(page, mindmapId, [0, 1]); + + await dragBetweenCoords( + page, + { x: firstRect.x + firstRect.w / 2, y: firstRect.y + firstRect.h / 2 }, + { + x: secondRect.x + secondRect.w + 10, + y: secondRect.y + secondRect.h / 2, + }, + { + steps: 50, + } + ); + expect((await getMindMapNode(page, mindmapId, [0, 0, 0, 0])).id).toEqual( + childId + ); + } +}); + +test('cannot drag mind map node to itself or its descendants', async ({ + page, +}) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); + + const mindmapId = await createMindMap(page, [0, 1]); + await addMindmapNodes(page, mindmapId, [0, 1], { + text: 'child node 1', + children: [ + { + text: 'child node 2', + }, + { + text: 'child node 3', + }, + ], }); - await waitNextFrame(page, 100); + const { id: node, rect } = await getMindMapNode(page, mindmapId, [0, 1]); + const { id: childNode3, rect: childRect3 } = await getMindMapNode( + page, + mindmapId, + [0, 1, 0, 1] + ); + await dragBetweenCoords( + page, + { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 }, + { x: childRect3.x + childRect3.w + 10, y: childRect3.y + childRect3.h / 2 }, + { + steps: 50, + } + ); + + expect((await getMindMapNode(page, mindmapId, [0, 1])).id).toEqual(node); + expect((await getMindMapNode(page, mindmapId, [0, 1, 0, 1])).id).toEqual( + childNode3 + ); +}); + +test('drag root node should layout in real time', async ({ page }) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); + + // wait for the font to be loaded + await waitFontsLoaded(page); + + const mindmapId = await createMindMap(page, [0, 0]); + const { rect: rootRect } = await getMindMapNode(page, mindmapId, [0]); + const { rect: firstRect } = await getMindMapNode(page, mindmapId, [0, 0]); + const { rect: secondRect } = await getMindMapNode(page, mindmapId, [0, 1]); + const { rect: thirdRect } = await getMindMapNode(page, mindmapId, [0, 2]); + + const assertMindMapNodesPosition = async (deltaX: number, deltaY: number) => { + await expect((await getMindMapNode(page, mindmapId, [0, 0])).rect).toEqual({ + ...firstRect, + x: firstRect.x + deltaX, + y: firstRect.y + deltaY, + }); + await expect((await getMindMapNode(page, mindmapId, [0, 1])).rect).toEqual({ + ...secondRect, + x: secondRect.x + deltaX, + y: secondRect.y + deltaY, + }); + await expect((await getMindMapNode(page, mindmapId, [0, 2])).rect).toEqual({ + ...thirdRect, + x: thirdRect.x + deltaX, + y: thirdRect.y + deltaY, + }); + }; 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 }, + { x: rootRect.x + rootRect.w / 2, y: rootRect.y + rootRect.h / 2 }, + { + x: rootRect.x + rootRect.w / 2 + 10, + y: rootRect.y + rootRect.h / 2 + 10, + }, { steps: 50, } ); - 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; + await assertMindMapNodesPosition(10, 10); + + await page.mouse.move( + rootRect.x + rootRect.w / 2 + 10, + rootRect.y + rootRect.h / 2 + 10 + ); + await page.mouse.down(); + await page.mouse.move( + rootRect.x + rootRect.w / 2 + 10 + 4, + rootRect.y + rootRect.h / 2 + 10 + 4 + ); + await page.mouse.move( + rootRect.x + rootRect.w / 2 + 10 + 44, + rootRect.y + rootRect.h / 2 + 10 + 44, + { steps: 10 } + ); - return mindmap.tree.children[1].id; + // assert when dragging is in progress + await waitNextFrame(page, 500); + await assertMindMapNodesPosition(50, 50); + + await page.mouse.up(); +}); + +test('drag node out of mind map should detach the node and create a new mind map', async ({ + page, +}) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); + + const mindmapId = await createMindMap(page, [0, 1]); + await addMindmapNodes(page, mindmapId, [0, 1], { + text: 'child node 1', + children: [ + { + text: 'child node 2', + }, + { + text: 'child node 3', + }, + ], + }); + + const { rect } = await getMindMapNode(page, mindmapId, [0, 1]); + await dragBetweenCoords( + page, + { + x: rect.x + rect.w / 2, + y: rect.y + rect.h / 2, }, - { mindmapId, nodeId } + { + x: rect.x + rect.w / 2, + y: rect.y + rect.h / 2 + 300, + }, + { + steps: 50, + } ); - expect(secondNodeId).toEqual(nodeId); + const { count, mindmap: lastMindmapId } = await page.evaluate(() => { + const edgelessBlock = document.querySelector('affine-edgeless-root'); + if (!edgelessBlock) { + throw new Error('edgeless block not found'); + } + const mindmaps = edgelessBlock.gfx.gfxElements.filter( + el => 'type' in el && el.type === 'mindmap' + ); + + return { + count: mindmaps.length, + mindmap: mindmaps[mindmaps.length - 1].id, + }; + }); + + expect(count).toBe(2); + expect((await getMindMapNode(page, lastMindmapId, [0, 0])).text).toBe( + 'child node 1' + ); + expect((await getMindMapNode(page, lastMindmapId, [0, 0, 0])).text).toBe( + 'child node 2' + ); + expect((await getMindMapNode(page, lastMindmapId, [0, 0, 1])).text).toBe( + 'child node 3' + ); +}); + +test('allow to type content directly when node has been selected', async ({ + page, +}) => { + await edgelessCommonSetup(page); + await zoomResetByKeyboard(page); + + const mindmapId = await createMindMap(page, [0, 0]); + const { id: nodeId } = await getMindMapNode(page, mindmapId, [0, 1]); + + await clickView(page, [0, 0]); + await selectElementInEdgeless(page, [nodeId]); + await type(page, 'parent node'); + await pressEnter(page); + await pressTab(page); + await type(page, 'child node 1'); + await pressEnter(page); + await pressEnter(page); + await type(page, 'child node 2'); + await pressEnter(page); + + await expect((await getMindMapNode(page, mindmapId, [0, 1])).text).toBe( + 'parent node' + ); + await expect((await getMindMapNode(page, mindmapId, [0, 1, 0])).text).toBe( + 'child node 1' + ); + await expect((await getMindMapNode(page, mindmapId, [0, 1, 1])).text).toBe( + 'child node 2' + ); }); diff --git a/tests/utils/actions/edgeless.ts b/tests/utils/actions/edgeless.ts index 61b2b07d8212..b36ff57b3383 100644 --- a/tests/utils/actions/edgeless.ts +++ b/tests/utils/actions/edgeless.ts @@ -1912,3 +1912,30 @@ export function toIdCountMap(ids: string[]) { export function getFrameTitle(page: Page, frame: string) { return page.locator(`affine-frame-title[data-id="${frame}"]`); } + +export async function selectElementInEdgeless(page: Page, elements: string[]) { + await page.evaluate( + ({ elements }) => { + const edgelessBlock = document.querySelector('affine-edgeless-root'); + if (!edgelessBlock) { + throw new Error('edgeless block not found'); + } + + edgelessBlock.gfx.selection.set({ + elements, + }); + }, + { elements } + ); +} + +export async function waitFontsLoaded(page: Page) { + await page.evaluate(() => { + const edgelessBlock = document.querySelector('affine-edgeless-root'); + if (!edgelessBlock) { + throw new Error('edgeless block not found'); + } + + return edgelessBlock.fontLoader?.ready; + }); +} diff --git a/tests/utils/mindmap.ts b/tests/utils/mindmap.ts new file mode 100644 index 000000000000..7633cad09f70 --- /dev/null +++ b/tests/utils/mindmap.ts @@ -0,0 +1,130 @@ +import type { + MindmapElementModel, + MindmapNode, + ShapeElementModel, +} from '@blocksuite/affine-model'; +import type { Page } from '@playwright/test'; + +import { clickView } from './actions/click.js'; + +export async function createMindMap(page: Page, coords: [number, number]) { + await page.keyboard.press('m'); + await clickView(page, coords); + + const id = await page.evaluate(() => { + const edgelessBlock = document.querySelector('affine-edgeless-root'); + if (!edgelessBlock) { + throw new Error('edgeless block not found'); + } + const mindmaps = edgelessBlock.gfx.gfxElements.filter( + el => 'type' in el && el.type === 'mindmap' + ); + + return mindmaps[mindmaps.length - 1].id; + }); + + return id; +} + +export async function getMindMapNode( + page: Page, + mindmapId: string, + pathOrId: number[] | string +) { + return page.evaluate( + ({ mindmapId, pathOrId }) => { + const edgelessBlock = document.querySelector('affine-edgeless-root'); + if (!edgelessBlock) { + throw new Error('edgeless block not found'); + } + + const mindmap = edgelessBlock.gfx.getElementById( + mindmapId + ) as MindmapElementModel; + if (!mindmap) { + throw new Error(`Mindmap not found: ${mindmapId}`); + } + + const node = Array.isArray(pathOrId) + ? mindmap.getNodeByPath(pathOrId) + : mindmap.getNode(pathOrId); + if (!node) { + throw new Error(`Mindmap node not found at: ${pathOrId}`); + } + + const rect = edgelessBlock.gfx.viewport.toViewBound( + node.element.elementBound + ); + + return { + path: mindmap.getPath(node), + id: node.id, + text: (node.element as ShapeElementModel).text?.toString() ?? '', + rect: { + x: rect.x, + y: rect.y, + w: rect.w, + h: rect.h, + }, + }; + }, + { + mindmapId, + pathOrId, + } + ); +} + +type NewNodeInfo = { + text: string; + children?: NewNodeInfo[]; +}; + +export async function addMindmapNodes( + page: Page, + mindmapId: string, + path: number[], + newNode: NewNodeInfo +) { + return page.evaluate( + ({ mindmapId, path, newNode }) => { + const edgelessBlock = document.querySelector('affine-edgeless-root'); + if (!edgelessBlock) { + throw new Error('edgeless block not found'); + } + + const mindmap = edgelessBlock.gfx.getElementById( + mindmapId + ) as MindmapElementModel; + if (!mindmap) { + throw new Error(`Mindmap not found: ${mindmapId}`); + } + + const parent = mindmap.getNodeByPath(path); + if (!parent) { + throw new Error(`Mindmap node not found at: ${path}`); + } + + const addNode = ( + mindmap: MindmapElementModel, + node: NewNodeInfo, + parent: MindmapNode + ) => { + const newNodeId = mindmap.addNode(parent, undefined, undefined, { + text: node.text, + }); + + if (node.children) { + node.children.forEach(child => { + addNode(mindmap, child, mindmap.getNode(newNodeId)!); + }); + } + + return newNodeId; + }; + + return addNode(mindmap, newNode, parent); + }, + { mindmapId, path, newNode } + ); +}