Guard locked through the desktop grid too

`mobile.spec.ts` covers the stacked editor, which draws through
`DashboardView` on both sides. Desktop `?edit=true` swaps in `GridLayout`
directly and never mounts `LockedProvider`, so the branch the stacked fix
was matched to had no guard of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYM38KSb4V4v2T71eifnZv
This commit is contained in:
2026-08-30 17:32:12 +02:00
co-authored by Claude Opus 5
parent e4428efb8d
commit 5818e5122a
+51
View File
@@ -16,6 +16,9 @@ const flowName = `test_widgets_${Date.now().toString(36)}`
const dashboardName = `${flowName}_panel`
/** A panel of its own: a bar of three rows needs a tile to itself. */
const stackName = `${flowName}_stack`
/** A read-only dashboard, on its own so the lock guard below has a lamp the
* interaction test above hasn't already flipped. */
const lockedName = `${flowName}_locked`
/** A message of the flow under test, qualified the way the engine names it. */
const w = (name: string) => `${flowName}.${name}`
@@ -230,6 +233,30 @@ test.beforeAll(async ({ browser }) => {
method: "POST",
data: { version: stacked.version },
})
const locked = await (
await api(page, `/dashboards/${lockedName}`, { method: "POST" })
).json()
locked.settings = { ...(locked.settings ?? {}), locked: { value: true } }
locked.widgets = [
{
id: "lamp",
type: "switch",
title: "Lamp",
layout: { lg: { x: 0, y: 0, w: 3, h: 2 } },
config: { target: w("lamp"), dtype: "bool", style: "button" },
},
]
const lockedDraft = await (
await api(page, `/dashboards/${lockedName}`, {
method: "PUT",
data: locked,
})
).json()
await api(page, `/dashboards/${lockedName}/publish`, {
method: "POST",
data: { version: lockedDraft.version },
})
await page.close()
})
@@ -237,6 +264,7 @@ test.afterAll(async ({ browser }) => {
await deleteAll(browser, [
`/dashboards/${dashboardName}`,
`/dashboards/${stackName}`,
`/dashboards/${lockedName}`,
`/flows/${flowName}`,
])
})
@@ -469,6 +497,29 @@ test("a control reads back what it published", async ({ page }) => {
await expect(lamp).toHaveText("Off")
})
/**
* A locked dashboard, read and arranged on the desktop grid.
*
* mobile.spec.ts guards this same setting through the stacked editor, which a
* phone's width routes through `DashboardView` on both the read and the
* arrange side. Desktop takes a second path once editing starts — `?edit=true`
* swaps in `<GridLayout>` directly, which never mounts `LockedProvider` at
* all — so this is the same guard through the branch that path never reaches.
*/
test("a locked dashboard locks when read, not while arranged", async ({
page,
}) => {
const lamp = page.getByRole("button", { name: "Lamp" })
await page.goto(`/dashboards/${lockedName}`)
await lamp.waitFor({ timeout: 15000 })
await expect(lamp, "a locked dashboard being read").toBeDisabled()
await page.goto(`/dashboards/${lockedName}?edit=true`)
await lamp.waitFor({ timeout: 15000 })
await expect(lamp, "a locked dashboard being arranged").toBeEnabled()
})
test("an unbound widget says so and takes nothing down", async ({ page }) => {
const crashes: string[] = []
page.on("pageerror", (error) => crashes.push(error.message))