Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
Two things the first pass got wrong. The Changes and "Given up on" lists had no cap, so they ran as long as the audit trail did — and worse, a truncating flex row still offers its whole unwrapped line as a min-content contribution, so the card sized itself to the longest entry and took the page sideways with it. Both now use the same capped, scrolling box the runs and failures lists already had, which fixes the length and the width together. Running downwards, a rank of eight nodes was two thousand pixels across because a node box is landscape: siblings cost a rank four times as much across as they do down it. A rank wider than two now wraps onto the ranks below, settling over a few passes since wrapping one rank pushes what was under it up into the room that freed. The value chip on an edge no longer reserves its width there either — that width is spent across the screen rather than along the flow, and the rank gap it would widen is already wider than the chip is tall. The same flow that laid out 2040x216 is now 803x722; a chain and a diamond are unchanged, and so is every desktop layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDSXaRhvqHYNevgDGmNAto
201 lines
6.6 KiB
TypeScript
201 lines
6.6 KiB
TypeScript
import { expect, type Page, test } from "@playwright/test"
|
|
import { api, apiPage, deleteAll } from "./utils/api"
|
|
|
|
/**
|
|
* Every screen at a phone's width, checking the one thing that is easy to
|
|
* break and hard to notice: a page that scrolls sideways.
|
|
*
|
|
* The rules this enforces are in the root `DESIGN-GUIDELINES.md` → Responsive.
|
|
* A wide table, chart or dock is allowed to scroll inside its own box; none of
|
|
* them may widen the page around it.
|
|
*/
|
|
|
|
const flowName = `test_mobile_${Date.now().toString(36)}`
|
|
const dashboardName = `${flowName}_panel`
|
|
|
|
test.describe.configure({ mode: "serial" })
|
|
|
|
/**
|
|
* Does anything on this page stick out past the viewport?
|
|
*
|
|
* Two signals, because either can hide the other. A page that scrolls
|
|
* sideways fails `scrollWidth`; one that merely *contains* something too wide
|
|
* makes the browser widen the layout viewport and zoom the whole page out
|
|
* instead, which shows up only as `innerWidth` no longer being the device's.
|
|
*/
|
|
async function fits(page: Page): Promise<{ inner: number; scroll: number }> {
|
|
return page.evaluate(() => ({
|
|
inner: window.innerWidth,
|
|
scroll: document.documentElement.scrollWidth,
|
|
}))
|
|
}
|
|
|
|
async function expectFits(page: Page, where: string) {
|
|
const width = page.viewportSize()?.width ?? 0
|
|
const { inner, scroll } = await fits(page)
|
|
expect(inner, `${where} is too wide for the viewport`).toBe(width)
|
|
expect(scroll, `${where} scrolls sideways`).toBeLessThanOrEqual(inner)
|
|
}
|
|
|
|
test.beforeAll(async ({ browser }) => {
|
|
const page = await apiPage(browser)
|
|
|
|
// A chain and a four-way fan: enough of a graph to lay out, and wider than
|
|
// a phone can take abreast, so the wrapping is exercised.
|
|
await api(page, `/flows/${flowName}`, {
|
|
method: "PUT",
|
|
data: {
|
|
name: flowName,
|
|
title: "Mobile",
|
|
nodes: [
|
|
{
|
|
id: "source",
|
|
type: "python",
|
|
provides: [{ name: "reading", dtype: "float" }],
|
|
},
|
|
{
|
|
id: "scale",
|
|
type: "python",
|
|
requires: [{ name: "reading", dtype: "float" }],
|
|
provides: [{ name: "scaled", dtype: "float" }],
|
|
},
|
|
...["one", "two", "three", "four"].map((id) => ({
|
|
id,
|
|
type: "python",
|
|
requires: [{ name: "scaled", dtype: "float" }],
|
|
})),
|
|
],
|
|
version: 1,
|
|
},
|
|
})
|
|
const saved = await (await api(page, `/flows/${flowName}`)).json()
|
|
await api(page, `/flows/${flowName}/publish`, {
|
|
method: "POST",
|
|
data: { version: saved.definition.version },
|
|
})
|
|
|
|
await api(page, `/dashboards/${dashboardName}`, { method: "POST" })
|
|
const dashboard = await (
|
|
await api(page, `/dashboards/${dashboardName}`)
|
|
).json()
|
|
dashboard.pages[0].sections[0].widgets = [
|
|
{
|
|
id: "top",
|
|
type: "stat",
|
|
title: "Top",
|
|
layout: { lg: { x: 0, y: 0, w: 3, h: 2 } },
|
|
config: { message: `${flowName}.reading` },
|
|
},
|
|
{
|
|
id: "beside",
|
|
type: "stat",
|
|
title: "Beside",
|
|
layout: { lg: { x: 3, y: 0, w: 3, h: 2 } },
|
|
config: { message: `${flowName}.scaled` },
|
|
},
|
|
]
|
|
const draft = await (
|
|
await api(page, `/dashboards/${dashboardName}`, {
|
|
method: "PUT",
|
|
data: dashboard,
|
|
})
|
|
).json()
|
|
await api(page, `/dashboards/${dashboardName}/publish`, {
|
|
method: "POST",
|
|
data: { version: draft.version },
|
|
})
|
|
await page.close()
|
|
})
|
|
|
|
test.afterAll(async ({ browser }) => {
|
|
await deleteAll(browser, [
|
|
`/dashboards/${dashboardName}`,
|
|
`/flows/${flowName}`,
|
|
])
|
|
})
|
|
|
|
test("home fits the viewport", async ({ page }) => {
|
|
await page.goto("/")
|
|
await page
|
|
.getByText(/Flow activity/i)
|
|
.first()
|
|
.waitFor({ timeout: 15000 })
|
|
await expectFits(page, "home")
|
|
})
|
|
|
|
test("the overviews fit the viewport", async ({ page }) => {
|
|
for (const path of ["/flows", "/dashboards"]) {
|
|
await page.goto(path)
|
|
await page.waitForLoadState("networkidle")
|
|
await expectFits(page, path)
|
|
}
|
|
})
|
|
|
|
/** Where React Flow put a node in the graph, out of its wrapper transform. */
|
|
async function nodeAt(page: Page, id: string) {
|
|
const style = await page
|
|
.locator(`.react-flow__node[data-id="${id}"]`)
|
|
.evaluate((el) => (el as HTMLElement).style.transform)
|
|
const [x, y] = [...style.matchAll(/-?[\d.]+/g)].map((m) => Number(m[0]))
|
|
return { x, y }
|
|
}
|
|
|
|
test("the flow editor fits, and its dock is reachable", async ({ page }) => {
|
|
await page.goto(`/flows/${flowName}`)
|
|
await page.waitForSelector(".react-flow__node")
|
|
await expectFits(page, "the flow editor")
|
|
|
|
// A phone has height to spare and no width, so the graph runs downwards:
|
|
// what a node feeds sits below it, not beside it.
|
|
const source = await nodeAt(page, "source")
|
|
const scale = await nodeAt(page, "scale")
|
|
expect(scale.y, "the graph does not run top to bottom").toBeGreaterThan(
|
|
source.y,
|
|
)
|
|
expect(Math.abs(scale.x - source.x)).toBeLessThan(200)
|
|
|
|
// …and it grows downwards rather than sideways: the four consumers of one
|
|
// message wrap onto rows instead of standing eight hundred pixels abreast.
|
|
const rows = new Map<number, number>()
|
|
for (const id of ["source", "scale", "one", "two", "three", "four"]) {
|
|
const { y } = await nodeAt(page, id)
|
|
rows.set(y, (rows.get(y) ?? 0) + 1)
|
|
}
|
|
expect(
|
|
Math.max(...rows.values()),
|
|
"a row is wider than a phone",
|
|
).toBeLessThanOrEqual(2)
|
|
|
|
// The dock used to overflow a phone, which put the buttons at its ends
|
|
// outside the shell's `overflow-hidden` and made them unclickable.
|
|
for (const id of ["add-node", "run-flow", "edit-flow", "publish-flow"]) {
|
|
const box = await page.getByTestId(id).boundingBox()
|
|
expect(box, `${id} is not on screen`).not.toBeNull()
|
|
const width = page.viewportSize()?.width ?? 0
|
|
expect(box!.x, `${id} starts off the left edge`).toBeGreaterThanOrEqual(0)
|
|
expect(
|
|
box!.x + box!.width,
|
|
`${id} runs off the right edge`,
|
|
).toBeLessThanOrEqual(width)
|
|
}
|
|
})
|
|
|
|
test("a dashboard stacks instead of shrinking", async ({ page }) => {
|
|
await page.goto(`/dashboards/${dashboardName}`)
|
|
await page.waitForSelector("[data-testid=widget-frame]", { timeout: 15000 })
|
|
await expectFits(page, "the dashboard editor")
|
|
|
|
// Side by side on a panel, one under the other here.
|
|
const first = await page.getByTestId("widget-frame").first().boundingBox()
|
|
const second = await page.getByTestId("widget-frame").nth(1).boundingBox()
|
|
expect(first).not.toBeNull()
|
|
expect(second).not.toBeNull()
|
|
expect(second!.y).toBeGreaterThanOrEqual(first!.y + first!.height - 1)
|
|
})
|
|
|
|
test("the panel view fits the viewport", async ({ page }) => {
|
|
await page.goto(`/view/${dashboardName}`)
|
|
await page.waitForSelector("[data-testid=widget-frame]", { timeout: 15000 })
|
|
await expectFits(page, "the panel view")
|
|
})
|