Two real findings from adversarial review of the previous commit (the v4 UI port's route change), both fixed: - app/tests-layout/*.spec.js (20 files, 133 tests): every one of these navigated to /funpack/ and asserted on composer-specific DOM (.cx-workspace*, window.FunPack) that no longer exists there now that the ported v4 frontend is served at that URL instead. Left as-is they would silently time out on every run. Each file now carries an explicit test.skip(true, "...") with a comment explaining why and pointing at the plan's pixel-exactness step, which is what will eventually rewrite these against the new DOM -- declared and tracked, not quietly broken. - core/tests/test_registry.py's fixture-based tests never exercised the new category field through the real announcement_of()/scan() path -- only test_schema.py's direct validate() calls did, which bypass registry.py's FIELDS name-mapping entirely. A typo there (e.g. "Category" instead of "CATEGORY") would have passed every existing test. The "good module" fixture gained CATEGORY = "guidance" and test_the_good_module_loads now asserts on it, closing the gap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
208 lines
9.0 KiB
JavaScript
208 lines
9.0 KiB
JavaScript
// A floating window must always be reachable.
|
|
//
|
|
// Position is remembered per window id, so the dangerous case is not "where did
|
|
// it open" but "where did it open GIVEN a screen it no longer fits". jsdom
|
|
// cannot see this at all: getBoundingClientRect is zeros there, so a window
|
|
// entirely off the right edge measures the same as one in the middle.
|
|
|
|
import { test, expect } from "@playwright/test";
|
|
|
|
// Skipped: / now serves the ported v4 frontend (app/legacy/), not composer --
|
|
// these specs assert on .cx-workspace*/window.FunPack, which no longer exist
|
|
// on the served page. Pending rewrite against the v4-derived DOM as part of
|
|
// the v4-UI-onto-v5-backend port's pixel-exactness verification step (see
|
|
// the plan file). Not deleted: the assertions are still the right SHAPE of
|
|
// check, just against selectors this page no longer has.
|
|
test.skip(true, "composer retired in favor of the v4 UI port -- rewrite against app/legacy's DOM");
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
});
|
|
|
|
async function openWindow(page, id) {
|
|
return page.evaluate(async (id) => {
|
|
const { composer } = await import("/funpack/app/composer/composer.js");
|
|
const handle = composer.floating.window({
|
|
id, title: "Attack", body: composer.text.md({ text: "content" }),
|
|
});
|
|
const box = handle.node.getBoundingClientRect();
|
|
return { x: box.x, y: box.y, w: box.width, h: box.height,
|
|
vw: innerWidth, vh: innerHeight };
|
|
}, id);
|
|
}
|
|
|
|
test("a window remembered off a wider screen still opens reachable", async ({ page }) => {
|
|
// Remember a position far to the right, as dragging on a wide screen would.
|
|
await page.evaluate(() => localStorage.setItem(
|
|
"cx.win.attack", JSON.stringify({ x: 1400, y: 40, width: 420, height: 320 })));
|
|
|
|
await page.setViewportSize({ width: 800, height: 600 });
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
|
|
const box = await openWindow(page, "attack");
|
|
|
|
// Something grabbable has to be on screen -- a titlebar with no pixels in the
|
|
// viewport cannot be dragged back, and the position is remembered, so the
|
|
// window would be lost until localStorage was cleared by hand.
|
|
expect(box.x, "the whole window is past the right edge").toBeLessThan(box.vw);
|
|
expect(box.x + box.w, "the whole window is past the left edge").toBeGreaterThan(0);
|
|
expect(box.y).toBeGreaterThanOrEqual(0);
|
|
expect(box.y, "the titlebar is below the bottom edge").toBeLessThan(box.vh);
|
|
|
|
// And enough of it to actually hit with a pointer.
|
|
const visible = Math.min(box.x + box.w, box.vw) - Math.max(box.x, 0);
|
|
expect(visible, "less than a grabbable strip is on screen").toBeGreaterThanOrEqual(60);
|
|
});
|
|
|
|
test("a window open while the viewport shrinks stays reachable", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1400, height: 900 });
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
await openWindow(page, "shrink");
|
|
|
|
await page.setViewportSize({ width: 600, height: 500 });
|
|
const box = await page.evaluate(() => {
|
|
const node = document.querySelector(".cx-window");
|
|
const r = node.getBoundingClientRect();
|
|
return { x: r.x, y: r.y, w: r.width, vw: innerWidth };
|
|
});
|
|
|
|
expect(box.x).toBeLessThan(box.vw);
|
|
expect(box.x + box.w).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("a window cannot be dragged somewhere it cannot be dragged back from", async ({ page }) => {
|
|
await page.setViewportSize({ width: 900, height: 700 });
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
await openWindow(page, "dragged");
|
|
|
|
const bar = page.locator(".cx-window-bar").first();
|
|
await bar.hover();
|
|
await page.mouse.down();
|
|
await page.mouse.move(5000, 5000, { steps: 5 }); // far past both edges
|
|
await page.mouse.up();
|
|
|
|
const box = await page.evaluate(() => {
|
|
const r = document.querySelector(".cx-window").getBoundingClientRect();
|
|
return { x: r.x, y: r.y, w: r.width, vw: innerWidth, vh: innerHeight };
|
|
});
|
|
expect(box.x).toBeLessThan(box.vw);
|
|
expect(box.y).toBeLessThan(box.vh);
|
|
expect(box.x + box.w).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("a window cannot be resized until its own controls leave the screen", async ({ page }) => {
|
|
// The other half of "a window must stay reachable". Position was clamped and
|
|
// size was not, so a corner drag grew the window past every edge and carried
|
|
// its close and maximise buttons off with it -- 4937px wide in a 900px
|
|
// viewport, with Escape the only way back.
|
|
await page.setViewportSize({ width: 900, height: 700 });
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
await openWindow(page, "resized");
|
|
|
|
const grip = page.locator(".cx-window-grip").first();
|
|
await grip.hover();
|
|
await page.mouse.down();
|
|
await page.mouse.move(5000, 5000, { steps: 5 });
|
|
await page.mouse.up();
|
|
|
|
const closeButton = page.locator('.cx-window [aria-label="Close"]').first();
|
|
await expect(closeButton, "the close button is off screen").toBeInViewport();
|
|
|
|
const box = await page.evaluate(() => {
|
|
const r = document.querySelector(".cx-window").getBoundingClientRect();
|
|
return { w: r.width, h: r.height, vw: innerWidth, vh: innerHeight };
|
|
});
|
|
expect(box.w, "the window is wider than the viewport").toBeLessThanOrEqual(box.vw);
|
|
expect(box.h, "the window is taller than the viewport").toBeLessThanOrEqual(box.vh);
|
|
});
|
|
|
|
test("a window cannot be resized into nothing", async ({ page }) => {
|
|
await page.setViewportSize({ width: 900, height: 700 });
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
await openWindow(page, "shrunk");
|
|
|
|
const grip = page.locator(".cx-window-grip").first();
|
|
await grip.hover();
|
|
await page.mouse.down();
|
|
await page.mouse.move(0, 0, { steps: 5 });
|
|
await page.mouse.up();
|
|
|
|
const box = await page.evaluate(() => {
|
|
const r = document.querySelector(".cx-window").getBoundingClientRect();
|
|
return { w: r.width, h: r.height };
|
|
});
|
|
expect(box.w, "the window collapsed to nothing").toBeGreaterThanOrEqual(100);
|
|
expect(box.h).toBeGreaterThanOrEqual(100);
|
|
});
|
|
|
|
// A window that FITS the screen must be placed whole on it. The size clamp and
|
|
// the position clamp used to be measured against two different budgets -- the
|
|
// viewport, and the viewport minus a grabbable strip -- which do not add up, so
|
|
// a wide window came back with its own controls past the right edge while every
|
|
// assertion about "a strip is reachable" still passed.
|
|
|
|
async function openSizedWindow(page, id, size) {
|
|
return page.evaluate(async ({ id, size }) => {
|
|
const { composer } = await import("/funpack/app/composer/composer.js");
|
|
composer.floating.window({
|
|
id, title: "Attack", body: composer.text.md({ text: "content" }), ...size,
|
|
});
|
|
}, { id, size });
|
|
}
|
|
|
|
test("a wide window remembered off a wider screen opens with its controls reachable", async ({ page }) => {
|
|
await page.goto("/funpack/");
|
|
await page.evaluate(() => localStorage.setItem(
|
|
"cx.win.wide", JSON.stringify({ x: 1000, y: 40, width: 900, height: 320 })));
|
|
|
|
await page.setViewportSize({ width: 800, height: 600 });
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
await openSizedWindow(page, "wide", {});
|
|
|
|
await expect(page.locator('.cx-window [aria-label="Close"]').first(),
|
|
"the close button is off screen").toBeInViewport();
|
|
await expect(page.locator(".cx-window-grip").first(),
|
|
"the resize grip is off screen").toBeInViewport();
|
|
});
|
|
|
|
test("a wide window stays whole when the viewport shrinks under it", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1400, height: 900 });
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
await openSizedWindow(page, "wide-shrink", { x: 600, y: 40, width: 700, height: 300 });
|
|
|
|
await page.setViewportSize({ width: 800, height: 600 });
|
|
await expect(page.locator('.cx-window [aria-label="Close"]').first(),
|
|
"the close button is off screen").toBeInViewport();
|
|
});
|
|
|
|
test("dragging may still leave part of a window off the edge", async ({ page }) => {
|
|
// The looseness is deliberate and is what makes the rule above safe to
|
|
// tighten: putting a window half off the screen is an ordinary thing to do,
|
|
// and it must not snap back while the pointer is still holding it.
|
|
await page.setViewportSize({ width: 900, height: 700 });
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
await openSizedWindow(page, "loose", { x: 100, y: 100, width: 420, height: 320 });
|
|
|
|
const bar = page.locator(".cx-window-bar").first();
|
|
await bar.hover();
|
|
await page.mouse.down();
|
|
await page.mouse.move(5000, 300, { steps: 5 });
|
|
await page.mouse.up();
|
|
|
|
const box = await page.evaluate(() => {
|
|
const r = document.querySelector(".cx-window").getBoundingClientRect();
|
|
return { x: r.x, w: r.width, vw: innerWidth };
|
|
});
|
|
expect(box.x + box.w, "dragging snapped the window fully back on screen")
|
|
.toBeGreaterThan(box.vw);
|
|
});
|