Pair displays at the platform's own /display/setup, with or without modules
CI / verify (pull_request) Successful in 1m12s
CI / infrastructure-smoke (pull_request) Successful in 45s
CI / package-release (pull_request) Successful in 50s
CI / migration-smoke (pull_request) Successful in 2m34s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m52s
CI / verify (pull_request) Successful in 1m12s
CI / infrastructure-smoke (pull_request) Successful in 45s
CI / package-release (pull_request) Successful in 50s
CI / migration-smoke (pull_request) Successful in 2m34s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m52s
Displays are platform-owned, but a screen could only start pairing on a
module's display page: every pairing request named a module, so an
installation with no module that uses displays could not set one up.
- /display/setup is the platform's own display page, served whenever
the deployment has device support, whatever is installed. It pairs a
platform display with no module. Once paired, it opens the module it
is set to show; with none set, it shows its name and waits, rechecking
so it stays alive and notices revocation. "setup" is now a reserved
module id.
- Migration core:0009_platform_display_pairing lets a pairing request
name no module. It relaxes module_id and permission_id and adds a
both-or-neither check. It is additive and destroys nothing.
- POST /api/v1/device-pairing/requests accepts {} for a platform
display. Approval accepts an optional moduleId, so the administrator
can choose an installed display module and the device gets that
module's declared permission. A module display's own request can't be
redirected.
- GET /api/v1/device-pairing/self lets a paired device describe itself:
its name and the module displays it may show. It renews the device's
cookie.
- GET /api/v1/devices/setup is replaced by
GET /api/v1/devices/display-modules: the modules a display can be set
to show.
- Settings -> Displays always offers the /display/setup address, Copy
setup link and Add display. Add display has an optional "Show on this
screen" choice, and each paired display says what it shows.
- /display/<module-id> is unchanged, so released Signage 0.1.5 works as
before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
906c015ca8
commit
300d9eab51
@@ -600,33 +600,112 @@ check(
|
||||
),
|
||||
"the sidebar is navigation only, and arriving at Displays focuses no field",
|
||||
);
|
||||
// ADR 0065: how to set up a new screen, in one place — the platform display page of every module
|
||||
// that uses displays, on the API origin where the display host lives, with Add display right under
|
||||
// it; or a plain note, and no Add display, when no installed module uses displays.
|
||||
// ADR 0065: how to set up a new screen, in one place — the platform's own /display/setup on the API
|
||||
// origin, whatever is installed, with Add display right under it.
|
||||
check(
|
||||
await waitFor(
|
||||
`${selector("display-setup")} && !${selector("display-setup-failed")} && (${selector("display-setup-none")} || (${selector("display-setup")}.contains(${selector("add-display")}) && [...document.querySelectorAll('[data-testid="display-setup-url"]')].every((element) => element.textContent.startsWith(${JSON.stringify(`${apiOrigin}/display/`)}))))`,
|
||||
`${selector("display-setup")}?.contains(${selector("add-display")}) && ${selector("display-setup-url")}?.textContent === ${JSON.stringify(`${apiOrigin}/display/setup`)}`,
|
||||
"the display setup steps",
|
||||
),
|
||||
"Settings → Displays explains how to set up a new screen, from the platform's own display page",
|
||||
"Settings → Displays sets up a new screen at the platform's own /display/setup, with Add display",
|
||||
);
|
||||
const displayModuleInstalled = !(await evaluate(`Boolean(${selector("display-setup-none")})`));
|
||||
if (displayModuleInstalled) {
|
||||
await evaluate(`${selector("add-display")}.click()`);
|
||||
check(
|
||||
await waitFor(
|
||||
`${selector("add-display-dialog")} && document.activeElement === ${selector("device-pairing-code-input")}`,
|
||||
"the Add display dialog with its code field focused",
|
||||
),
|
||||
"Add display opens a dialog that focuses the pairing code only once asked",
|
||||
);
|
||||
await evaluate(`${selector("add-display-cancel")}.click()`);
|
||||
await waitFor(`!${selector("add-display-dialog")}`, "the Add display dialog closing");
|
||||
} else {
|
||||
check(
|
||||
!(await evaluate(`Boolean(${selector("add-display")})`)),
|
||||
"with no module that uses displays, Settings → Displays offers no Add display that could only fail",
|
||||
);
|
||||
await evaluate(`${selector("add-display")}.click()`);
|
||||
check(
|
||||
await waitFor(
|
||||
`${selector("add-display-dialog")} && document.activeElement === ${selector("device-pairing-code-input")}`,
|
||||
"the Add display dialog with its code field focused",
|
||||
),
|
||||
"Add display opens a dialog that focuses the pairing code only once asked",
|
||||
);
|
||||
await evaluate(`${selector("add-display-cancel")}.click()`);
|
||||
await waitFor(`!${selector("add-display-dialog")}`, "the Add display dialog closing");
|
||||
|
||||
// ADR 0065, end to end: a new screen — its own browser context, holding no session, as a real
|
||||
// screen would — opens the platform's /display/setup, shows a code, and is added through the real
|
||||
// Settings form. It needs no module: chosen, it opens that module's display by itself; not chosen,
|
||||
// it shows its name and waits.
|
||||
{
|
||||
const { browserContextId } = await cdp.send("Target.createBrowserContext", {});
|
||||
const kiosk = await cdp.send("Target.createTarget", { browserContextId, url: "about:blank" });
|
||||
const { sessionId: kioskSession } = await cdp.send("Target.attachToTarget", {
|
||||
flatten: true,
|
||||
targetId: kiosk.targetId,
|
||||
});
|
||||
const kioskEvaluate = async (expression) => {
|
||||
const result = await cdp.send(
|
||||
"Runtime.evaluate",
|
||||
{ awaitPromise: true, expression, returnByValue: true },
|
||||
kioskSession,
|
||||
);
|
||||
return result.exceptionDetails ? undefined : result.result.value;
|
||||
};
|
||||
const kioskWaitFor = async (expression) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (await kioskEvaluate(`Boolean(${expression})`).catch(() => false)) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
await cdp.send("Page.navigate", { url: `${apiOrigin}/display/setup` }, kioskSession);
|
||||
const shown = (await kioskWaitFor(
|
||||
`/^[0-9]{6}$/.test(${selector("display-pairing-code")}?.textContent ?? "")`,
|
||||
))
|
||||
? await kioskEvaluate(`${selector("display-pairing-code")}.textContent`)
|
||||
: undefined;
|
||||
check(shown !== undefined, "a new screen at /display/setup shows a six-digit pairing code");
|
||||
if (shown !== undefined) {
|
||||
await evaluate(`${selector("add-display")}.click()`);
|
||||
await waitFor(selector("device-pairing-code-input"), "the Add display code field");
|
||||
await evaluate(`(() => {
|
||||
const input = ${selector("device-pairing-code-input")};
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(input, ${JSON.stringify(shown)});
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.form.requestSubmit();
|
||||
})()`);
|
||||
check(
|
||||
await waitFor(
|
||||
`${selector("device-pairing-found")}?.textContent.includes("A new screen is waiting to be paired")`,
|
||||
"the platform screen found by its code",
|
||||
),
|
||||
"Add display finds the screen from /display/setup as a new platform display, with no module",
|
||||
);
|
||||
const chosen = await evaluate(`(() => {
|
||||
const name = ${selector("device-pairing-name")};
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(name, "Smoke platform display");
|
||||
name.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
const shows = ${selector("device-pairing-shows")};
|
||||
const option = shows ? [...shows.options].find((candidate) => candidate.value !== "") : undefined;
|
||||
if (option) {
|
||||
Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value").set.call(shows, option.value);
|
||||
shows.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
return option ? option.value : "";
|
||||
})()`);
|
||||
await waitFor(`!${selector("device-pairing-approve")}?.disabled`, "Pair display enabled");
|
||||
await evaluate(`${selector("device-pairing-approve")}.click()`);
|
||||
check(
|
||||
await waitFor(
|
||||
`!${selector("add-display-dialog")} && [...document.querySelectorAll('[data-testid^="device-"]')].some((card) => card.textContent.includes("Smoke platform display"))`,
|
||||
"the new platform display in the list",
|
||||
),
|
||||
"the paired platform display appears under Paired displays without a reload",
|
||||
);
|
||||
check(
|
||||
await kioskWaitFor(
|
||||
chosen === ""
|
||||
? `${selector("display-setup-waiting-headline")}?.textContent === "Smoke platform display"`
|
||||
: `location.pathname === ${JSON.stringify(`/display/${chosen}`)}`,
|
||||
),
|
||||
chosen === ""
|
||||
? "with no module to show, the paired screen shows its name and waits"
|
||||
: "the paired screen opens the module display it was set to show, by itself",
|
||||
);
|
||||
}
|
||||
await cdp.send("Target.closeTarget", { targetId: kiosk.targetId });
|
||||
await cdp.send("Target.disposeBrowserContext", { browserContextId });
|
||||
}
|
||||
await navigate(`${dashboardOrigin}/settings/discover`);
|
||||
check(
|
||||
@@ -698,20 +777,18 @@ check(
|
||||
"choosing a page from the drawer releases the page lock",
|
||||
);
|
||||
// ADR 0065: on a phone the pairing code is typed at 16px or more, so iOS Safari does not zoom.
|
||||
if (displayModuleInstalled) {
|
||||
await navigate(`${dashboardOrigin}/settings/displays`);
|
||||
await waitFor(selector("add-display"), "Settings → Displays on a phone");
|
||||
await evaluate(`${selector("add-display")}.click()`);
|
||||
check(
|
||||
await waitFor(
|
||||
`${selector("device-pairing-code-input")} && parseFloat(getComputedStyle(${selector("device-pairing-code-input")}).fontSize) >= 16`,
|
||||
"the pairing code field's size on a phone",
|
||||
),
|
||||
"the pairing code field is at least 16px on a phone, so iOS does not zoom into it",
|
||||
);
|
||||
await evaluate(`${selector("add-display-cancel")}.click()`);
|
||||
await waitFor(`!${selector("add-display-dialog")}`, "the Add display dialog closing on a phone");
|
||||
}
|
||||
await navigate(`${dashboardOrigin}/settings/displays`);
|
||||
await waitFor(selector("add-display"), "Settings → Displays on a phone");
|
||||
await evaluate(`${selector("add-display")}.click()`);
|
||||
check(
|
||||
await waitFor(
|
||||
`${selector("device-pairing-code-input")} && parseFloat(getComputedStyle(${selector("device-pairing-code-input")}).fontSize) >= 16`,
|
||||
"the pairing code field's size on a phone",
|
||||
),
|
||||
"the pairing code field is at least 16px on a phone, so iOS does not zoom into it",
|
||||
);
|
||||
await evaluate(`${selector("add-display-cancel")}.click()`);
|
||||
await waitFor(`!${selector("add-display-dialog")}`, "the Add display dialog closing on a phone");
|
||||
await send("Emulation.setDeviceMetricsOverride", {
|
||||
deviceScaleFactor: 1,
|
||||
height: 1080,
|
||||
|
||||
Reference in New Issue
Block a user