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)');
});
});