diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 21f48ec5..4f1a51d2 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -41,6 +41,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light', + connectable: false, }, ]; diff --git a/examples/svelte/src/routes/tests/nodes/DragHandleNode.svelte b/examples/svelte/src/routes/tests/nodes/DragHandleNode.svelte new file mode 100644 index 00000000..da5fa4c4 --- /dev/null +++ b/examples/svelte/src/routes/tests/nodes/DragHandleNode.svelte @@ -0,0 +1,26 @@ + + +
+
+
+ + diff --git a/examples/svelte/src/routes/tests/nodes/Flow.svelte b/examples/svelte/src/routes/tests/nodes/Flow.svelte index 6686c907..6f684cbf 100644 --- a/examples/svelte/src/routes/tests/nodes/Flow.svelte +++ b/examples/svelte/src/routes/tests/nodes/Flow.svelte @@ -3,10 +3,16 @@ import { initialNodes, initialEdges } from './nodesAndEdges'; import { SvelteFlow } from '@xyflow/svelte'; + import DragHandleNode from './DragHandleNode.svelte'; + import '@xyflow/svelte/dist/style.css'; const nodes = writable(initialNodes); const edges = writable(initialEdges); + + const nodeTypes = { + DragHandleNode + }; - + diff --git a/examples/svelte/src/routes/tests/nodes/nodesAndEdges.ts b/examples/svelte/src/routes/tests/nodes/nodesAndEdges.ts index 37e2d999..bd382bef 100644 --- a/examples/svelte/src/routes/tests/nodes/nodesAndEdges.ts +++ b/examples/svelte/src/routes/tests/nodes/nodesAndEdges.ts @@ -2,50 +2,61 @@ import type { Edge, Node } from '@xyflow/svelte'; export const initialNodes: Node[] = [ { - id: '1', - data: { label: '1' }, + id: 'Node-1', + data: { label: 'Node-1' }, position: { x: 0, y: 0 }, - class: 'test-class', + type: 'input', + class: 'playwright-test-class-123', style: 'background-color: red;' }, { - id: '2', - data: { label: '2' }, + id: 'Node-2', + type: 'output', + data: { label: 'Node-2' }, position: { x: -100, y: 100 } }, { - id: '3', - data: { label: '3' }, + id: 'Node-3', + data: { label: 'Node-3' }, position: { x: 100, y: 100 } }, { - id: '4', - data: { label: '4' }, - position: { x: 0, y: 200 } + id: 'Node-4', + data: { label: 'Node-4' }, + position: { x: 0, y: 200 }, + type: 'output' + }, + { + id: 'drag-handle', + data: { label: 'Drag Handle' }, + position: { x: 200, y: 0 }, + type: 'DragHandleNode', + dragHandle: '.custom-drag-handle' + }, + { + id: 'notConnectable', + type: 'output', + data: { label: 'notConnectable' }, + position: { x: 0, y: 300 }, + connectable: false }, { id: 'notDraggable', data: { label: 'notDraggable' }, - position: { x: 0, y: 300 }, + position: { x: 0, y: 400 }, draggable: false }, { id: 'notSelectable', data: { label: 'notSelectable' }, - position: { x: 0, y: 400 }, - selectable: false - }, - { - id: 'notConnectable', - data: { label: 'notConnectable' }, position: { x: 0, y: 500 }, - connectable: false + selectable: false }, { id: 'notDeletable', data: { label: 'notDeletable' }, position: { x: 0, y: 600 }, - connectable: false + deletable: false }, { id: 'hidden', @@ -59,15 +70,15 @@ export const initialEdges: Edge[] = [ { id: '1-2', type: 'default', - source: '1', - target: '2', + source: 'Node-1', + target: 'Node-2', label: 'edge' }, { id: '1-3', type: 'default', - source: '1', - target: '3', + source: 'Node-1', + target: 'Node-3', label: 'edge' } ]; diff --git a/tests/playwright/e2e/backspace-delete.spec.ts b/tests/playwright/e2e/backspace-delete.spec.ts deleted file mode 100644 index 0f6d3648..00000000 --- a/tests/playwright/e2e/backspace-delete.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { FRAMEWORK } from './constants'; - -test('Delete Node with backspace', async ({ page }) => { - await page.goto('/'); - await page.locator(`.${FRAMEWORK}-flow__node`).nth(0).click(); - // await page.getByTestId('rf__node-1').click(); - await page.keyboard.press('Backspace'); - - const nodes = await page.locator(`.${FRAMEWORK}-flow__node`).all(); - expect(nodes).toHaveLength(3); - - const edges = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); - expect(edges).toHaveLength(0); -}); diff --git a/tests/playwright/e2e/move-node.spec.ts b/tests/playwright/e2e/move-node.spec.ts deleted file mode 100644 index d9fea594..00000000 --- a/tests/playwright/e2e/move-node.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('Move Node', async ({ page }) => { - await page.goto('/'); - - const locator = page.getByTestId('rf__node-1'); - - const transformBeforeMove = await locator.evaluate((element) => { - return element.style.transform; - }); - - await locator.hover(); - await page.mouse.down(); - await page.mouse.move(500, 500); - await page.mouse.up(); - - const transformAfterMove = await locator.evaluate((element) => { - return element.style.transform; - }); - - expect(transformBeforeMove).not.toMatch(transformAfterMove); -}); diff --git a/tests/playwright/e2e/nodes.spec.ts b/tests/playwright/e2e/nodes.spec.ts index b06de961..03a3bef9 100644 --- a/tests/playwright/e2e/nodes.spec.ts +++ b/tests/playwright/e2e/nodes.spec.ts @@ -9,6 +9,240 @@ test.describe('NODES', () => { }); test.describe('selection', () => { - test('selecting a node', async ({ page }) => {}); + test('selecting a node by click', async ({ page }) => { + const locator = page.locator(`.${FRAMEWORK}-flow__node`).first(); + await locator.click(); + + await expect(locator).toHaveClass(/selected/); + }); + + test('selecting multiple nodes with shift drag', async ({ page }) => { + const nodes = page.locator(`.${FRAMEWORK}-flow__node`); + const firstNode = nodes.first(); + const secondNode = nodes.nth(1); + const thirdNode = nodes.nth(2); + + await expect(firstNode).toBeInViewport(); + await expect(secondNode).toBeInViewport(); + await expect(thirdNode).toBeInViewport(); + + const box = await firstNode.boundingBox(); + + await page.mouse.move(box!.x - 100, box!.y - 10); + await page.keyboard.down('Shift'); + await page.mouse.down(); + await page.mouse.move(box!.x + 200, box!.y + 200); + await page.mouse.up(); + await page.keyboard.up('Shift'); + + await expect(firstNode).toHaveClass(/selected/); + await expect(secondNode).toHaveClass(/selected/); + await expect(thirdNode).toHaveClass(/selected/); + + const selection = page.locator(`.${FRAMEWORK}-flow__selection`); + await expect(selection).toBeInViewport(); + }); + + test('selectable=false prevents selection', async ({ page }) => { + const locator = page.locator(`.${FRAMEWORK}-flow__node`).and(page.locator('[data-id="notSelectable"]')); + await locator.click(); + + await expect(locator).not.toHaveClass(/selected/); + }); + }); + + test.describe('dragging', () => { + test('dragging a node', async ({ page }) => { + const node = page.locator(`.${FRAMEWORK}-flow__node`).first(); + + const transformBeforeMove = await node.evaluate((element) => { + return element.style.transform; + }); + + await node.hover(); + await page.mouse.down(); + await page.mouse.move(500, 500); + await page.mouse.up(); + + const transformAfterMove = await node.evaluate((element) => { + return element.style.transform; + }); + + expect(transformBeforeMove).not.toMatch(transformAfterMove); + }); + + test('draggable=false prevents dragging', async ({ page }) => { + const node = page.locator(`.${FRAMEWORK}-flow__node`).and(page.locator('[data-id="notDraggable"]')); + + const transformBeforeMove = await node.evaluate((element) => { + return element.style.transform; + }); + + await node.hover(); + await page.mouse.down(); + await page.mouse.move(500, 500); + await page.mouse.up(); + + const transformAfterMove = await node.evaluate((element) => { + return element.style.transform; + }); + + expect(transformBeforeMove).toMatch(transformAfterMove); + }); + + test('custom drag handle works', async ({ page }) => { + const node = page.locator(`.${FRAMEWORK}-flow__node`).and(page.locator('[data-id="drag-handle"]')); + const dragHandle = page.locator('.custom-drag-handle'); + + const transformBeforeMove = await node.evaluate((element) => { + return element.style.transform; + }); + + const nodeBox = await node.boundingBox(); + + await page.mouse.move(nodeBox!.x + 10, nodeBox!.y + 10); + await page.mouse.down(); + await page.mouse.move(500, 500); + await page.mouse.up(); + + const transformAfterMove = await node.evaluate((element) => { + return element.style.transform; + }); + + expect(transformBeforeMove).toMatch(transformAfterMove); + + await dragHandle.hover(); + await page.mouse.down(); + await page.mouse.move(500, 500); + await page.mouse.up(); + + const transformAfterDragHandleMove = await node.evaluate((element) => { + return element.style.transform; + }); + + expect(transformBeforeMove).not.toMatch(transformAfterDragHandleMove); + }); + }); + + test.describe('deleting', () => { + test('deleting a node and its edges', async ({ page }) => { + const node = page.locator(`.${FRAMEWORK}-flow__node`).and(page.locator('[data-id="Node-1"]')); + + await node.click(); + await page.keyboard.press('Backspace'); + + await expect(node).not.toBeAttached(); + + const edges = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + expect(edges).toHaveLength(0); + }); + + test('deletable=false prevents deletion', async ({ page }) => { + const node = page.locator(`.${FRAMEWORK}-flow__node`).and(page.locator('[data-id="notDeletable"]')); + + await node.click(); + await page.keyboard.press('Backspace'); + + await expect(node).toBeAttached(); + }); + }); + + test.describe('connecting', () => { + test('connecting two nodes', async ({ page }) => { + const outputSourceHandle = page.locator(`.${FRAMEWORK}-flow__handle`).and(page.locator('[data-nodeid="Node-1"]')); + const inputSourceHandle = page.locator(`.${FRAMEWORK}-flow__handle`).and(page.locator('[data-nodeid="Node-4"]')); + + await expect(outputSourceHandle).toBeInViewport(); + await expect(inputSourceHandle).toBeInViewport(); + + const edgesBefore = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + + await outputSourceHandle.hover(); + await page.mouse.down(); + await inputSourceHandle.hover(); + await page.mouse.up(); + + const edgesAfter = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + expect(edgesAfter).toHaveLength(edgesBefore.length + 1); + }); + + test('connecting two output handles does not work', async ({ page }) => { + const firstOutputHandle = page.locator(`.${FRAMEWORK}-flow__handle`).and(page.locator('[data-nodeid="Node-2"]')); + const secondOutputHandle = page.locator(`.${FRAMEWORK}-flow__handle`).and(page.locator('[data-nodeid="Node-4"]')); + + await expect(firstOutputHandle).toBeInViewport(); + await expect(secondOutputHandle).toBeInViewport(); + + const edgesBefore = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + + await firstOutputHandle.hover(); + await page.mouse.down(); + await secondOutputHandle.hover(); + await page.mouse.up(); + + const edgesAfter = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + expect(edgesAfter).toHaveLength(edgesBefore.length); + }); + + test('connecting two input handles does not work', async ({ page }) => { + const firstInputHandle = page.locator(`.${FRAMEWORK}-flow__handle`).and(page.locator('[data-nodeid="Node-1"]')); + const secondInputHandle = page + .locator(`.${FRAMEWORK}-flow__handle`) + .and(page.locator('[data-nodeid="Node-3"]')) + .and(page.locator('.source')); + + await expect(firstInputHandle).toBeInViewport(); + await expect(secondInputHandle).toBeInViewport(); + + const edgesBefore = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + + await firstInputHandle.hover(); + await page.mouse.down(); + await secondInputHandle.hover(); + await page.mouse.up(); + + const edgesAfter = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + expect(edgesAfter).toHaveLength(edgesBefore.length); + }); + + test('connectable=false prevents connections', async ({ page }) => { + const outputHandle = page.locator(`.${FRAMEWORK}-flow__handle`).and(page.locator('[data-nodeid="Node-1"]')); + const notConnectableHandle = page + .locator(`.${FRAMEWORK}-flow__handle`) + .and(page.locator('[data-nodeid="notConnectable"]')); + + const notConnectableBox = await notConnectableHandle.boundingBox(); + + await expect(outputHandle).toBeInViewport(); + await expect(notConnectableHandle).toBeInViewport(); + + const edgesBefore = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + + await outputHandle.hover(); + await page.mouse.down(); + await page.mouse.move(notConnectableBox!.x + 2, notConnectableBox!.y + 2); + await page.mouse.up(); + + const edgesAfter = await page.locator(`.${FRAMEWORK}-flow__edge`).all(); + expect(edgesAfter).toHaveLength(edgesBefore.length); + }); + }); + + test('hidden=true hides the node', async ({ page }) => { + const node = page.locator(`.${FRAMEWORK}-flow__node`).and(page.locator('[data-id="hidden"]')); + + await expect(node).not.toBeInViewport(); + }); + + test('classes get applied', async ({ page }) => { + const node = page.locator(`.${FRAMEWORK}-flow__node`).and(page.locator('[data-id="Node-1"]')); + + await expect(node).toHaveClass(/playwright-test-class-123/); + }); + + test('styles get applied', async ({ page }) => { + const node = page.locator(`.${FRAMEWORK}-flow__node`).and(page.locator('[data-id="Node-1"]')); + + await expect(node).toHaveCSS('background-color', 'rgb(255, 0, 0)'); }); });