132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
import { expect, test } from "@playwright/test"
|
|
import { apiPage, createFlow, 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 createFlow(page, {
|
|
name: flowName,
|
|
title: "Searchable flow",
|
|
nodes: [
|
|
{
|
|
id: "findme",
|
|
type: "python",
|
|
title: "Find me",
|
|
params: { tuning_gain: 7, api_token: "s3cr3t-token" },
|
|
},
|
|
],
|
|
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()
|
|
})
|
|
|
|
test("finds a setting with its value, and never a credential", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/")
|
|
await page.getByRole("button", { name: "Search", exact: true }).click()
|
|
const input = page.getByTestId("global-search-input")
|
|
|
|
await input.fill("tuning_gain")
|
|
const gain = page.getByRole("option", { name: /findme\.tuning_gain/ })
|
|
await expect(gain).toContainText(`${flowName} · python · 7`)
|
|
|
|
// The token is masked on the server, so it never reaches the page.
|
|
await input.fill("api_token")
|
|
await expect(
|
|
page.getByRole("option", { name: /findme\.api_token/ }),
|
|
).toContainText("••••")
|
|
await expect(page.getByRole("dialog")).not.toContainText("s3cr3t-token")
|
|
|
|
await input.fill("tuning_gain")
|
|
await gain.click()
|
|
await expect(page).toHaveURL(new RegExp(`/flows/${flowName}\\?node=findme$`))
|
|
})
|