import { expect, test } from "@playwright/test" import { deleteAll } from "./utils/api" const flowName = `test_clip_${Date.now().toString(36)}` const KEY = "fluksio.nodeClipboard" declare global { interface Window { /** Whether the app took the last ⌘C/⌘V away from the browser. */ prevented: boolean | null } } test.use({ storageState: "playwright/.auth/user.json", permissions: ["clipboard-read", "clipboard-write"], }) test.afterAll(async ({ browser }) => { await deleteAll(browser, [`/flows/${flowName}`]) }) test("the canvas clipboard yields to the native one", async ({ page }) => { await page.goto("/flows") await page.getByTestId("new-flow").click() await page.getByTestId("new-flow-name").fill(flowName) await page.getByTestId("create-flow").click() await page.waitForURL(`/flows/${flowName}`) await page.getByTestId("add-node").click() await page .getByRole("option", { name: /function/i }) .first() .click() await expect(page.locator(".react-flow__node")).toHaveCount(1) await page.keyboard.press("Escape") // Records whether the app took the chord away from the browser. await page.evaluate((key) => { localStorage.removeItem(key) window.prevented = null window.addEventListener("keydown", (event) => { if (["c", "v"].includes(event.key.toLowerCase())) { window.prevented = event.defaultPrevented } }) }, KEY) const prevented = () => page.evaluate(() => window.prevented) // A node selected and no text selection: the canvas clipboard acts. await page.locator(".react-flow__node").first().click() await page.keyboard.press("ControlOrMeta+c") await expect .poll(() => page.evaluate((key) => localStorage.getItem(key), KEY)) .not.toBeNull() expect(await prevented()).toBe(true) // And it still pastes. await page.keyboard.press("ControlOrMeta+v") await expect(page.locator(".react-flow__node")).toHaveCount(2) expect(await prevented()).toBe(true) // Seeded before the selection is made: writing the clipboard drops it. await page.evaluate(() => navigator.clipboard.writeText("nothing copied")) // A text selection wins, node selection or not. React Flow's own CSS puts // `user-select: none` on every node, so the selectable text on this route is // the floating chrome over the canvas — here the open node panel. const selected = await page.evaluate(() => { const node = document.querySelector( '[data-testid="node-panel"]', ) as HTMLElement const range = document.createRange() range.selectNodeContents(node) const selection = document.getSelection() as Selection selection.removeAllRanges() selection.addRange(range) return selection.toString().trim() }) expect(selected).not.toBe("") await page.keyboard.press("ControlOrMeta+c") expect(await prevented()).toBe(false) const copied = await page.evaluate(() => navigator.clipboard.readText()) expect(copied.trim()).not.toBe("nothing copied") expect(selected).toContain(copied.trim().split("\n")[0]) // Nothing stored: paste falls through to the browser too. await page.evaluate((key) => { localStorage.removeItem(key) document.getSelection()?.removeAllRanges() }, KEY) await page.keyboard.press("ControlOrMeta+v") expect(await prevented()).toBe(false) })