Files
app/frontend/tests/search.spec.ts
T
stroblme 4c2df681d3 Flow canvas: label and edge popovers follow pan and zoom, the editors' dock is a full-width bar on a phone
- InputInspector hangs off the label's element and EdgeInspector off a point in flow coordinates (ViewportPortal), both re-measured every frame while open and hidden once the anchor leaves the view. A label that leaves the canvas closes its popover.
- Dock: below md a bar across the width, flush with the bottom edge (bg-card, top border, safe-area inset, no shadow); from md the floating pill as before. The flow dock drops the Run caption and the dashboard editor clears the bar with its own padding, so the editing state fits one row from 360px.
- Tests: the search spec checks the popover rides along with a pan; the mobile spec checks both docks are a full-width bar flush with the bottom.
2026-10-28 07:38:04 +01:00

105 lines
3.7 KiB
TypeScript

import { expect, test } from "@playwright/test"
import { api, apiPage, deleteAll } from "./utils/api"
/**
* The global search: the only way to reach a node without knowing its flow.
*
* The load-bearing part is what the flat index turns back into — a node hit is
* a flow address with that node in focus, which no list endpoint could have
* answered.
*/
const flowName = `test_search_${Date.now().toString(36)}`
test.use({ storageState: "playwright/.auth/user.json" })
test.describe.configure({ mode: "serial" })
test.beforeAll(async ({ browser }) => {
const page = await apiPage(browser)
await api(page, `/flows/${flowName}`, {
method: "PUT",
data: {
name: flowName,
title: "Searchable flow",
nodes: [{ id: "findme", type: "python", title: "Find me" }],
inputs: [{ spec: { name: "findme_input", dtype: "float" } }],
},
})
await page.close()
})
test.afterAll(async ({ browser }) => {
await deleteAll(browser, [`/flows/${flowName}`])
})
test("finds a node by name and opens its flow on it", async ({ page }) => {
await page.goto("/")
await page.getByRole("button", { name: "Search", exact: true }).click()
await page.getByTestId("global-search-input").fill("findme")
await page.getByRole("option", { name: /Find me/ }).click()
await expect(page).toHaveURL(new RegExp(`/flows/${flowName}\\?node=findme$`))
})
test("finds a flow input and reads it where its label sits", async ({
page,
}) => {
await page.goto("/")
await page.getByRole("button", { name: "Search", exact: true }).click()
await page.getByTestId("global-search-input").fill("findme_input")
await page.getByRole("option", { name: /findme_input/ }).click()
await expect(page).toHaveURL(
new RegExp(`/flows/${flowName}\\?node=input%3A${flowName}\\.findme_input$`),
)
// What clicking the label shows, and the settings one press on.
await expect(page.getByTestId("input-inspector")).toBeVisible()
await expect(page.getByTestId("flow-panel")).toBeHidden()
await page.getByTestId("edit-input").click()
await expect(page.getByTestId("flow-panel")).toBeVisible()
})
test("the label popover rides along when the canvas is panned", async ({
page,
}) => {
const id = `input:${flowName}.findme_input`
await page.goto(`/flows/${flowName}?node=${encodeURIComponent(id)}`)
const popover = page.getByTestId("input-inspector")
const label = page.locator(`.react-flow__node[data-id="${id}"]`)
await expect(popover).toBeVisible()
// Its own entrance moves it too, which would read as part of the offset.
await expect
.poll(() => popover.evaluate((el) => el.getAnimations().length))
.toBe(0)
// Where the popover hangs off the label, which a pan must not change.
const offset = async () => {
const [from, to] = [await label.boundingBox(), await popover.boundingBox()]
return [Math.round(to!.x - from!.x), Math.round(to!.y - from!.y)]
}
const [before, at] = [await offset(), await label.boundingBox()]
const { width, height } = page.viewportSize() ?? { width: 0, height: 0 }
await page.mouse.move(width * 0.8, height * 0.2)
await page.mouse.down()
await page.mouse.move(width * 0.8 - 90, height * 0.2 + 50, { steps: 6 })
await page.mouse.up()
await expect.poll(async () => (await label.boundingBox())!.x).not.toBe(at!.x)
await expect(popover).toBeVisible()
await expect.poll(offset).toEqual(before)
})
test("opens on the keyboard from anywhere", async ({ page }) => {
await page.goto("/dashboards")
// The binding lives in the sidebar, so wait for it to be there to press at.
await expect(
page.getByRole("button", { name: "Search", exact: true }),
).toBeVisible()
await page.keyboard.press("ControlOrMeta+k")
await expect(page.getByTestId("global-search-input")).toBeFocused()
})