mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
3c7b16b442
* fix(web): keep the project split still across the Home hand-off The optimistic creation frame left `.split` on its stylesheet default (460px) while ProjectView resolved the saved width, or the equal split of the container, only after it mounted — and that first write ran through the 200ms width transition, so the chat column moved once the real view arrived (OPEND-3207). Move the split geometry into `project-split-layout.ts`, have both surfaces resolve their first-paint width through `resolveProjectSplitLayout`, and settle a surface's first measured write without the transition. Red spec: e2e/ui/home-send-split-width.test.ts (red on main, green here). * test(web): read the 4px handle constant from project-split-layout The material spec pinned `SPLIT_RESIZE_HANDLE_WIDTH = 4` in ProjectView's source; the constant now lives in project-split-layout.ts and ProjectView imports it, so assert on the module and on the import. * test(cms): wait for the Test modal impression before switching language The locale case only passed while the impression frame never fired: on a fast machine the next language switch unmounted the modal before its requestAnimationFrame recorded the impression. CI is slow enough for the frame to land, and the switch that follows then closes the modal for good. Waiting for the recorded impression makes that order deterministic. * fix(cms): keep the open Test modal across a same-deployment reload A language switch (or lease renewal) reloads the selected Test deployment. While the new decisions are in flight the runtime publishes a session without decisions, and the modal treated that gap as the presentation ending: it dropped its "open" marker, then the returning decision looked like a new offer of an activity this account had already seen and stayed closed. Key the open presentation by the selected deployment and snapshot. A reload of the same selection continues it; a redeploy, a different account, a dismissal, or the session ending still releases it. * test(e2e): enroll the Home hand-off specs in the ui_p0 entry-settings group home-amr-pending, home-send-single-loading and home-send-split-width were merged (or opened) without being enrolled in uiP0Groups, so no merge lane had ever executed them; only the prerelease full pool would have. Enroll them with the other Home-entry files.
116 lines
5.3 KiB
TypeScript
116 lines
5.3 KiB
TypeScript
import { expect, test } from '@/playwright/suite';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
import { gotoEntryHome } from '@/playwright/amr';
|
|
import { applyStandardMocks, routeSuccessfulRuns, suppressWhatsNew } from '@/playwright/mock-factory';
|
|
import { T } from '@/timeouts';
|
|
|
|
/*
|
|
* OPEND-3207 · the project split must not move after a Home send lands.
|
|
*
|
|
* A Home send opens the optimistic project frame first (OPEND-2617 / 2614)
|
|
* and ProjectView takes over once `POST /api/projects` answers. The two
|
|
* surfaces used to size the chat column from different sources: the pending
|
|
* frame left `.split` on its stylesheet default (460px), while ProjectView
|
|
* resolved the user's saved width, or the equal split of the container when
|
|
* nothing was saved, only after it mounted. The column therefore widened or
|
|
* narrowed once the real view arrived, dragging the composer and the preview's
|
|
* left edge with it — the jump the two QA recordings show in both directions.
|
|
*
|
|
* These cases sample the chat column every animation frame from the first
|
|
* paint of the hand-off frame until the user's turn is on screen, and require
|
|
* one width for the whole stretch. No stopwatch: a 200ms transition and a hard
|
|
* cut fail the same way.
|
|
*/
|
|
|
|
declare global {
|
|
interface Window {
|
|
__odSplitWidthLog?: Array<{ at: number; width: number; pending: boolean; user: boolean }>;
|
|
}
|
|
}
|
|
|
|
const CHAT_PANEL_WIDTH_STORAGE_KEY = 'open-design.project.chatPanelWidth';
|
|
|
|
async function installSplitWidthProbe(page: Page): Promise<void> {
|
|
await page.addInitScript(() => {
|
|
const log: Array<{ at: number; width: number; pending: boolean; user: boolean }> = [];
|
|
window.__odSplitWidthLog = log;
|
|
let last: string | null = null;
|
|
const sample = () => {
|
|
const slot = document.querySelector('.split-chat-slot') as HTMLElement | null;
|
|
if (slot && slot.getClientRects().length > 0) {
|
|
const width = Math.round(slot.getBoundingClientRect().width);
|
|
const pending = document.querySelector('[data-testid="project-creation-pending-view"]') !== null;
|
|
const user = document.querySelector('[data-testid="chat-log"] .msg.user') !== null;
|
|
const key = `${width}|${pending}|${user}`;
|
|
if (key !== last) {
|
|
last = key;
|
|
log.push({ at: performance.now(), width, pending, user });
|
|
}
|
|
}
|
|
requestAnimationFrame(sample);
|
|
};
|
|
requestAnimationFrame(sample);
|
|
});
|
|
}
|
|
|
|
async function sendFromHome(page: Page, prompt: string): Promise<void> {
|
|
const input = page.getByTestId('home-hero-input');
|
|
await expect(input).toBeVisible({ timeout: T.medium });
|
|
await input.fill(prompt);
|
|
const submit = page.getByTestId('home-hero-submit');
|
|
await expect(submit).toBeEnabled();
|
|
await submit.click();
|
|
}
|
|
|
|
/**
|
|
* Every chat-column width sampled from the first hand-off paint through the
|
|
* frame the user turn appears, plus the widths that followed it, so a shift
|
|
* that lands right after the transcript settles is caught too.
|
|
*/
|
|
async function expectStableSplitWidth(page: Page, expectedWidth?: number): Promise<void> {
|
|
await expect(page.locator('[data-testid="chat-log"] .msg.user').first()).toBeVisible({ timeout: T.long });
|
|
await expect(page.locator('[data-testid="project-creation-pending-view"]')).toHaveCount(0, { timeout: T.medium });
|
|
await expect(page.locator('[data-testid="project-creation-pending-chat"]')).toHaveCount(0, { timeout: T.medium });
|
|
// Let any width transition that was still running finish before reading.
|
|
await page.waitForTimeout(400);
|
|
const log = await page.evaluate(() => window.__odSplitWidthLog ?? []);
|
|
const firstPending = log.findIndex((entry) => entry.pending);
|
|
expect(firstPending, 'the hand-off frame was sampled').toBeGreaterThanOrEqual(0);
|
|
const stretch = log.slice(firstPending);
|
|
const readable = stretch
|
|
.map((entry) => `${Math.round(entry.at - stretch[0]!.at)}ms ${entry.width}px${entry.pending ? ' pending' : ''}${entry.user ? ' user' : ''}`)
|
|
.join('\n');
|
|
const widths = Array.from(new Set(stretch.map((entry) => entry.width)));
|
|
expect(widths, `the chat column changed width after the hand-off:\n${readable}`).toHaveLength(1);
|
|
if (expectedWidth !== undefined) {
|
|
expect(widths[0], `the chat column did not hold the saved width:\n${readable}`).toBe(expectedWidth);
|
|
}
|
|
}
|
|
|
|
test.describe.configure({ timeout: T.xlong });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await suppressWhatsNew(page);
|
|
await installSplitWidthProbe(page);
|
|
});
|
|
|
|
test('[P0] a Home send with no saved chat width keeps one column width from the hand-off frame to the first turn', async ({ page }) => {
|
|
await applyStandardMocks(page);
|
|
await routeSuccessfulRuns(page, { runId: 'home-split-width-default' });
|
|
await gotoEntryHome(page);
|
|
await sendFromHome(page, 'Gamified habit app: draft the onboarding flow.');
|
|
await expectStableSplitWidth(page);
|
|
});
|
|
|
|
test('[P0] a Home send with a saved chat width shows that width from the hand-off frame onwards', async ({ page }) => {
|
|
await applyStandardMocks(page);
|
|
await page.addInitScript((key: string) => {
|
|
window.localStorage.setItem(key, '380');
|
|
}, CHAT_PANEL_WIDTH_STORAGE_KEY);
|
|
await routeSuccessfulRuns(page, { runId: 'home-split-width-saved' });
|
|
await gotoEntryHome(page);
|
|
await sendFromHome(page, 'Gamified habit app: draft the streak screen.');
|
|
await expectStableSplitWidth(page, 380);
|
|
});
|