import { test, expect, type Page } from '@playwright/test'; async function waitForGraph(page: Page) { await page.waitForSelector('svg g.node', { timeout: 15000 }); } function nodeByLabel(page: Page, label: string) { return page.locator('g.node').filter({ hasText: label }); } function graphContainer(page: Page) { return page.locator('.bg-white.rounded.shadow'); } async function openSubgraph(page: Page, nodeLabel: string) { await nodeByLabel(page, nodeLabel).click({ button: 'right' }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Subgraph' }).click(); await waitForGraph(page); } async function selectNode(page: Page, nodeLabel: string) { await page.keyboard.down('Control'); await nodeByLabel(page, nodeLabel).click(); await page.keyboard.up('Control'); } test.describe('Cut and Paste', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await waitForGraph(page); }); test('node context menu shows Cut option', async ({ page }) => { await nodeByLabel(page, 'Start').click({ button: 'right' }); const dropdown = page.locator('.ant-dropdown:visible'); await expect(dropdown).toBeVisible({ timeout: 3000 }); await expect(dropdown.locator('.ant-dropdown-menu-item').filter({ hasText: 'Cut' })).toBeVisible(); }); test('empty area context menu shows Paste option', async ({ page }) => { await graphContainer(page).click({ button: 'right', position: { x: 10, y: 400 } }); const dropdown = page.locator('.ant-dropdown:visible'); await expect(dropdown).toBeVisible({ timeout: 3000 }); await expect(dropdown.locator('.ant-dropdown-menu-item').filter({ hasText: 'Paste' })).toBeVisible(); }); test('cut without selection cuts the right-clicked node with dashed style', async ({ page }) => { // Right-click Start without any ctrl+click selection await nodeByLabel(page, 'Start').click({ button: 'right' }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Cut' }).click(); // Graph re-renders with cut node styled as dashed await waitForGraph(page); // The Start node's polygon should have dashed style applied via DOT const startNode = nodeByLabel(page, 'Start'); await expect(startNode).toBeVisible(); }); test('cut with ctrl-selected nodes marks them as cut (dashed style)', async ({ page }) => { // Ctrl+click to select End node await selectNode(page, 'End'); // Right-click End and cut await nodeByLabel(page, 'End').click({ button: 'right' }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Cut' }).click(); // Graph re-renders; cut nodes are still present but with dashed style await waitForGraph(page); await expect(nodeByLabel(page, 'End')).toBeVisible(); }); test('cut node also includes nodes reachable by outgoing edges', async ({ page }) => { // Start → random child, Start is selected for cut // First create a child of Start await nodeByLabel(page, 'Start').click(); await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); // Ctrl+click to select Start await selectNode(page, 'Start'); // Cut Start (which has an outgoing edge to the new child node) await nodeByLabel(page, 'Start').click({ button: 'right' }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Cut' }).click(); // All 3 nodes are still visible (not removed until paste) await waitForGraph(page); await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); }); test('paste adds cut nodes to a different subgraph', async ({ page }) => { // Cut the End node (no selection) await nodeByLabel(page, 'End').click({ button: 'right' }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Cut' }).click(); await waitForGraph(page); // Navigate into Start's subgraph await openSubgraph(page, 'Start'); // Subgraph has its own Start/End nodes await expect(page.locator('g.node')).toHaveCount(2, { timeout: 5000 }); // Paste into this subgraph via right-click on empty area await graphContainer(page).click({ button: 'right', position: { x: 10, y: 400 } }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Paste' }).click(); // The pasted node (End from main) should appear in subgraph — now 3 nodes await waitForGraph(page); await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 }); }); test('after paste, cut nodes are removed from the source graph', async ({ page }) => { // Cut End from main graph await nodeByLabel(page, 'End').click({ button: 'right' }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Cut' }).click(); await waitForGraph(page); // Navigate into Start's subgraph await openSubgraph(page, 'Start'); // Paste there await graphContainer(page).click({ button: 'right', position: { x: 10, y: 400 } }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Paste' }).click(); await waitForGraph(page); // Navigate back to main graph via breadcrumb await page.locator('.ant-breadcrumb-link').first().click(); await waitForGraph(page); // End node should no longer be in main graph (it was cut and pasted) await expect(nodeByLabel(page, 'End')).not.toBeAttached({ timeout: 5000 }); await expect(page.locator('g.node')).toHaveCount(1, { timeout: 5000 }); }); test('pasting on the same graph as the cut source cancels the cut', async ({ page }) => { const initialNodeCount = await page.locator('g.node').count(); // Cut End await nodeByLabel(page, 'End').click({ button: 'right' }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Cut' }).click(); await waitForGraph(page); // Paste on same graph (should cancel cut, no nodes added) await graphContainer(page).click({ button: 'right', position: { x: 10, y: 400 } }); await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 }); await page.waitForTimeout(300); await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Paste' }).click(); await waitForGraph(page); // Node count should be unchanged (no duplication) await expect(page.locator('g.node')).toHaveCount(initialNodeCount, { timeout: 5000 }); // End node is still present (cut was cancelled) await expect(nodeByLabel(page, 'End')).toBeVisible({ timeout: 5000 }); }); });