Files
app/frontend/tests/clipboard.spec.ts
T
stroblmeandClaude Opus 5.5 f44d7ebfcf Clipboard and values specs wait out the side panel's animation
The clipboard spec selected text in the previous node's panel while it was
still sliding out after the paste, so the selection collapsed when it
unmounted and the canvas took the copy. The values spec read the panel's box
mid-slide-in, where the translate leaves the width a float off 400.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:12:10 +02:00

98 lines
3.5 KiB
TypeScript

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. The paste
// opened the copy's panel, and the original's is still sliding out in front
// of it: a selection made in that one collapses when it unmounts.
await expect(page.getByTestId("node-panel")).toHaveCount(1)
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)
})