mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(web): keep signed-out updater in top-right cluster (#7482)
* fix(web): keep signed-out updater in top-right cluster * fix(web): gate updater cluster on rendered control Keep the updater host mounted for status updates while exposing the top-right cluster only when the updater renders a control. Preserve the signed-out and identity-loading project-route no-cluster contract.\n\nGenerated-By: looper 0.12.0 (runner=fixer, agent=codex)
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { expect, test } from '@/playwright/suite';
|
||||
import { applyStandardMocks } from '@/playwright/mock-factory';
|
||||
import {
|
||||
applyStandardMocks,
|
||||
routeSignedOutVelaStatus,
|
||||
} from '@/playwright/mock-factory';
|
||||
import { mockAmrPersonalWorkspace } from '@/playwright/amr';
|
||||
import { ensureRailOpen } from '@/playwright/rail';
|
||||
import { T } from '@/timeouts';
|
||||
|
||||
const RECENT_PROJECTS = Array.from({ length: 6 }, (_, i) => ({
|
||||
@@ -14,9 +16,10 @@ const RECENT_PROJECTS = Array.from({ length: 6 }, (_, i) => ({
|
||||
}));
|
||||
|
||||
// Regression boundary: the desktop update-ready prompt and the home composer's
|
||||
// model picker can be open at the same time. The updater now lives in the nav
|
||||
// rail, but it must still paint above the raised composer card and its popover
|
||||
// wherever those independently positioned surfaces overlap.
|
||||
// model picker can be open at the same time. The updater lives in the shared
|
||||
// top-right cluster for both signed-in and signed-out shells. Signed-in keeps
|
||||
// the prompt within the viewport; signed-out stays clear of the raised composer
|
||||
// card and its popover in a compact window.
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await applyStandardMocks(page);
|
||||
@@ -125,27 +128,21 @@ for (const direction of ['ltr', 'rtl'] as const) {
|
||||
});
|
||||
}
|
||||
|
||||
test('[P1] update ready prompt paints above the composer and its agent picker', async ({ page }) => {
|
||||
test.fail(
|
||||
true,
|
||||
'The rail-hosted updater prompt currently paints behind the raised Home composer in compact windows.',
|
||||
);
|
||||
// In the current rail host the prompt grows upward from the footer. A compact
|
||||
// desktop window puts it across the centered composer and model popover.
|
||||
test('[P1] signed-out update prompt stays clear of the composer and its agent picker', async ({ page }) => {
|
||||
await routeSignedOutVelaStatus(page);
|
||||
await page.setViewportSize({ width: 700, height: 600 });
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||
await page.getByText('Loading OpenDesign…').waitFor({ state: 'hidden', timeout: T.long });
|
||||
await expect(page.getByTestId('home-hero')).toBeVisible();
|
||||
|
||||
// The updater host moved into the nav rail footer with the entry topbar's
|
||||
// removal (#5517); the collapsed rail is inert, so expand it first. Open the
|
||||
// control with the keyboard because this stacking test intentionally models
|
||||
// the signed-out shell, whose Cloud sign-in card overlaps the footer pointer
|
||||
// target. Updater pointer actionability is covered by its component tests.
|
||||
await ensureRailOpen(page);
|
||||
const updaterButton = page.getByTestId('entry-nav-updater');
|
||||
await updaterButton.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
// Signed-out has no account capsule, but the updater keeps the same
|
||||
// top-right cluster home and remains directly actionable.
|
||||
await expect(page.getByTestId('entry-nav-account')).toHaveCount(0);
|
||||
const updaterButton = page
|
||||
.locator('.entry-top-right-cluster')
|
||||
.getByTestId('entry-nav-updater');
|
||||
await expect(updaterButton).toBeVisible();
|
||||
await updaterButton.click();
|
||||
const popup = page.getByTestId('updater-popup');
|
||||
await expect(popup).toBeVisible();
|
||||
|
||||
@@ -159,81 +156,34 @@ test('[P1] update ready prompt paints above the composer and its agent picker',
|
||||
await expect(page.getByTestId('inline-model-switcher-popover')).toBeVisible();
|
||||
await expect(popup).toBeVisible();
|
||||
|
||||
// Require real overlap now that both surfaces are present so the stacking
|
||||
// assertion cannot pass on separated geometry.
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(() => {
|
||||
const popupEl = document.querySelector('[data-testid="updater-popup"]');
|
||||
const card = document.querySelector('.home-hero__input-card');
|
||||
const popover = document.querySelector('[data-testid="inline-model-switcher-popover"]');
|
||||
if (popupEl == null || card == null || popover == null) return Number.NaN;
|
||||
const popupRect = popupEl.getBoundingClientRect();
|
||||
return Math.max(
|
||||
...[card, popover].map((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const width = Math.min(popupRect.right, rect.right) - Math.max(popupRect.left, rect.left);
|
||||
const height = Math.min(popupRect.bottom, rect.bottom) - Math.max(popupRect.top, rect.top);
|
||||
return width > 0 && height > 0 ? width * height : 0;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
await expect(page.getByTestId('inline-model-switcher-popover')).toBeVisible();
|
||||
await expect(popup).toBeVisible();
|
||||
|
||||
// The prompt is a dialog: it must be the topmost element wherever the
|
||||
// raised composer card or its agent-picker popover overlaps it. With the
|
||||
// stacking bug, `elementFromPoint` resolves to composer/picker content
|
||||
// instead of the prompt.
|
||||
const probe = await page.evaluate(() => {
|
||||
// Moving the signed-out updater from the rail footer to the top-right cluster
|
||||
// removes the old collision altogether. Keep the geometry assertion after
|
||||
// both surfaces open so a future repositioning cannot silently put the
|
||||
// prompt back across the composer or its popover.
|
||||
const overlapAreas = await page.evaluate(() => {
|
||||
const popupEl = document.querySelector('[data-testid="updater-popup"]');
|
||||
const overlays = [
|
||||
document.querySelector('.home-hero__input-card'),
|
||||
document.querySelector('[data-testid="inline-model-switcher-popover"]'),
|
||||
];
|
||||
if (popupEl == null || overlays.some((el) => el == null)) {
|
||||
return { ready: false, overlapArea: 0, samples: [] };
|
||||
return null;
|
||||
}
|
||||
const p = popupEl.getBoundingClientRect();
|
||||
let overlapArea = 0;
|
||||
const samples: { x: number; y: number; insidePopup: boolean; hit: string }[] = [];
|
||||
for (const overlay of overlays) {
|
||||
return overlays.map((overlay) => {
|
||||
const r = (overlay as Element).getBoundingClientRect();
|
||||
const left = Math.max(p.left, r.left);
|
||||
const right = Math.min(p.right, r.right);
|
||||
const top = Math.max(p.top, r.top);
|
||||
const bottom = Math.min(p.bottom, r.bottom);
|
||||
if (right - left < 4 || bottom - top < 4) continue;
|
||||
overlapArea += (right - left) * (bottom - top);
|
||||
for (const fx of [0.25, 0.5, 0.75]) {
|
||||
for (const fy of [0.25, 0.5, 0.75]) {
|
||||
const x = Math.round(left + (right - left) * fx);
|
||||
const y = Math.round(top + (bottom - top) * fy);
|
||||
const hit = document.elementFromPoint(x, y);
|
||||
samples.push({
|
||||
x,
|
||||
y,
|
||||
insidePopup: hit?.closest('[data-testid="updater-popup"]') != null,
|
||||
hit:
|
||||
hit instanceof HTMLElement
|
||||
? hit.className.toString().slice(0, 60) || hit.tagName
|
||||
: (hit?.tagName ?? 'null'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ready: true, overlapArea, samples };
|
||||
const width = Math.min(p.right, r.right) - Math.max(p.left, r.left);
|
||||
const height = Math.min(p.bottom, r.bottom) - Math.max(p.top, r.top);
|
||||
return width > 0 && height > 0 ? width * height : 0;
|
||||
});
|
||||
});
|
||||
|
||||
expect(probe.ready, 'popup, composer card, and agent picker must all be present').toBe(true);
|
||||
// Geometry precondition: the composer surfaces actually reach under the
|
||||
// prompt — otherwise this test would pass without exercising the stack.
|
||||
expect(probe.overlapArea, 'composer must overlap the prompt area').toBeGreaterThan(0);
|
||||
const leaks = probe.samples.filter((sample) => !sample.insidePopup);
|
||||
expect(
|
||||
leaks,
|
||||
`Composer content paints over the update prompt at: ${JSON.stringify(leaks)}`,
|
||||
).toEqual([]);
|
||||
overlapAreas,
|
||||
'popup, composer card, and agent picker must all be measurable',
|
||||
).not.toBeNull();
|
||||
expect(overlapAreas, 'signed-out updater prompt must stay clear of composer surfaces').toEqual([0, 0]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user