Skip to content

Commit 9edd6e7

Browse files
test(showcase): the smoke's global setup declines the console's workspace prompt before any spec runs (#29889)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi9paTx1Lu8ENYwwssthfK3h)_ Fixes #29811 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:showcase 浏览器冒烟的 `e2e/global-setup.ts` 在所有用例开跑前,用控制台工作区提示框自己的「Not now」按钮把它关掉一次,只把提示框「已问过」的本机记录带进各用例共用的 storageState;没有改任何用例、断言、超时或产品代码。 - **为什么改**:#28953(`2ac698baf1`)让控制台在组织未设默认货币时弹出一个模态框询问管理员。showcase 的开发种子在主机未设区域/区域无国家时不写货币,于是夜间每个用例都撞上这个模态框:背后页面被标成 `aria-hidden`,按角色查找的三个用例(批量操作勾选框、Money 分组按钮、相关列表表格)找不到元素,夜间 live 层因此变红。 - **风险与回滚**:低。只动测试夹具;「Not now」不向工作区写任何东西,showcase 数据与金额显示保持原样;storageState 只多一条提示记录,不带入侧栏展开等其它状态。回滚即回退本 PR。 - **先看**:`examples/app-showcase/e2e/global-setup.ts`(`declineWorkspacePrompt` 及其返回的记录过滤)。 - **两分钟验证**:`cd examples/app-showcase && CI=1 SMOKE_API_URL=http://localhost:39111 npx playwright test e2e/bulk-capability-gate.spec.ts e2e/detail-shapes.spec.ts`,预期 4 passed。 - **需要你做的**:无,席位落地。 ## What changes `examples/app-showcase/e2e/global-setup.ts` now answers the console's one-time workspace prompt once, before any spec runs: - It reads `GET /api/settings/localization` with the signed-in admin's token and decides whether the console would ask, by the console's own condition per key (a timezone still the default and unlocked, or a currency resolving from the default with no value and unlocked). Nothing to ask ⇒ nothing is opened. - Otherwise it opens the showcase app in a browser launched with the specs' own launch options (so `OS_TEST_CHROMIUM_EXECUTABLE_PATH` holds), waits for `workspace-timezone-prompt`, and presses `workspace-timezone-prompt-decline` ("Not now" / "Keep the default"). Declining writes nothing to the workspace. - It takes ONLY the prompt's per-device record (`os:workspace-timezone-prompt:ORG:USER`) from that context's localStorage and adds it to the storageState it already wrote. The rest of what the console stored while open (the sidebar's wide-open flag, recent items, the active organization) is left out, so every spec still starts from the fresh-context layout it measures from; detail-shapes derives its 780 px window from a context with no stored sidebar state. - A declined prompt that left no record throws, naming the cause, because every spec would meet the prompt again. A prompt the backend says would ask but the console never showed is a warning, as in `ui/e2e/live/global-setup.ts`, which already answers the same prompt for the console's own live suite. No spec, assertion, timeout or product file changes. ### Root cause, per failing test All three reds have one cause: `2ac698baf1` (#28953, card #28872, "the workspace prompt also asks an administrator for a still-unset default currency"), which landed between the last green nightly (`494c7435f8`, run 41394734666) and the red one (`339f63da30`, run 41584589032). `git merge-base --is-ancestor 2ac698b`: not an ancestor of `494c7435f8` (exit 1; control leg on the same shallow checkout: `65a8d4ffb1` = `494c7435f8~3`, deeper than the tested commit, exit 0), an ancestor of `339f63da30` (exit 0). The same commit taught `ui/e2e/live/global-setup.ts` to answer the prompt; the showcase smoke's global setup was not taught. The page snapshot of each failure (local reproduction at `4b71a82b76`) shows `dialog: heading "Set the default currency"` with the console behind it `aria-hidden`: 1. `bulk-capability-gate.spec.ts:38`: `getByRole('checkbox').first()` resolves, then the click waits behind the modal overlay until the 45 s test timeout. 2. `detail-shapes.spec.ts:95`: `getByRole('button', { name: 'Money' })` excludes the `aria-hidden` subtree, so "Money header icon svg" is not found. The icon itself renders. 3. `detail-shapes.spec.ts:161`: `getByRole('table')` inside the details column is excluded for the same reason, so `INV-1002` is not found. The product change is intended, so the specs stay as they are and the harness answers the prompt the way a person would. ## Tests - Reproduced red at base `4b71a82b76`: the two spec files, `--retries=0`, gave 3 failed and 1 passed, with the same three tests and the same errors as the nightly. - Green on head, three consecutive runs of the same two files (`--retries=0`, `CI=1`, a free port each run): 4 passed, 4 passed, 4 passed. After each run the storageState held exactly `auth-session-token` and the prompt record. - `pnpm --filter @objectstack/example-showcase typecheck`: exit 0 (its tsconfig includes `e2e/**/*`). - eslint `--no-inline-config` on the changed file: exit 0. ## Acceptance notes - The other 51 tests in `showcase-smoke.spec.ts` stayed green on the red night with the same modal over every surface they opened. A "renders cleanly" check that a modal over the whole page does not disturb is weaker than its name. Noted here only; no card. - Answering with "Not now" keeps the showcase as seeded. The prompt's confirm arm (writing the browser region's currency) is not exercised by this smoke, as before. - No changeset: `@objectstack/example-showcase` is private and the diff is an e2e fixture. Labelled `skip-changeset`. ## Verification <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 32 commands for the 1 changed path. Each one was run with its exit code captured before any pipe: 31 exit 0, and `pnpm check:dual-build-cjs-loads` exit 3 (PREREQUISITE NOT MET: some packages had no `dist/` in this worktree, so nothing was measured; CI's Lint & Repo Gates runs it). `--ran` reconciliation: 32 derived families accounted for, 31 run, 1 NOT-MEASURED, 0 UNRUN. </details> <details><summary>Full showcase live test</summary> `OS_TEST_TIERS=nightly pnpm --filter @objectstack/example-showcase test` under the verify lock, twice on head `835ac408b2` (main `4b71a82b76`), Console and the showcase's dependency closure built at that tree: - `test/showcase-smoke.live.test.ts` (this card's file): passed both times, Playwright `54 passed`. - `test/console-live-specs.live.test.ts`: failed both times on ONE spec of `ui/e2e/live/`, `record-history-display.spec.ts:56` ("the timeline stream's Changes chip shows display values"). The Owner and Customer Since change lines render as field names with no values, so `Live E2E showcase_manager` is not found. That file passed on the red nightly at `339f63da30`, and it boots its own backend and its own global setup (`ui/e2e/live/global-setup.ts`), which this diff does not touch. So the package command exited 1 (lock VERDICT `command-exit 1`, twice). The cause is not attributed here; it is reported to the seat as a finding outside this card. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aad5ee0 commit 9edd6e7

1 file changed

Lines changed: 101 additions & 2 deletions

File tree

‎examples/app-showcase/e2e/global-setup.ts‎

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { request } from '@playwright/test';
1+
import { chromium, request, type FullConfig, type LaunchOptions } from '@playwright/test';
22
import { mkdirSync, writeFileSync } from 'node:fs';
33
import { dirname } from 'node:path';
44

@@ -19,8 +19,9 @@ const API = process.env.SMOKE_API_URL || 'http://localhost:3000';
1919
const EMAIL = process.env.SMOKE_EMAIL || 'admin@objectos.ai';
2020
const PASSWORD = process.env.SMOKE_PASSWORD || 'admin123';
2121
const STATE_PATH = 'e2e/.auth/state.json';
22+
const APP = process.env.SHOWCASE_APP || 'com.example.showcase';
2223

23-
export default async function globalSetup() {
24+
export default async function globalSetup(config: FullConfig) {
2425
const ctx = await request.newContext();
2526
const res = await ctx.post(`${API}/api/v1/auth/sign-in/email`, {
2627
data: { email: EMAIL, password: PASSWORD },
@@ -37,4 +38,102 @@ export default async function globalSetup() {
3738
};
3839
mkdirSync(dirname(STATE_PATH), { recursive: true });
3940
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
41+
42+
// The specs' own launch options (playwright.config.ts carries
43+
// `OS_TEST_CHROMIUM_EXECUTABLE_PATH` there), so the prompt is answered in the
44+
// browser the specs run in.
45+
const promptRecord = await declineWorkspacePrompt(token, config.projects[0]?.use?.launchOptions);
46+
if (promptRecord.length > 0) {
47+
state.origins[0].localStorage.push(...promptRecord);
48+
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
49+
}
50+
}
51+
52+
/**
53+
* Decline the console's one-time workspace prompt before any spec runs.
54+
*
55+
* The console asks an administrator, in ONE modal dialog on the first app page
56+
* it renders, to set the workspace timezone while it is still the manifest
57+
* default, and — since the prompt's currency arm (objectstack#28872, commit
58+
* 2ac698baf1) — the default currency while `localization.currency` has no
59+
* value. The showcase's dev seed derives the currency from the host's POSIX
60+
* locale and leaves it unset on a host that sets none or names no country, and
61+
* the seeded admin may write settings, so every spec would meet that dialog in
62+
* front of the page it reads: the modal makes the page behind it
63+
* `aria-hidden`, so role queries (`getByRole('checkbox')`, `getByRole('table')`,
64+
* a section's disclosure button) find nothing and a click never lands.
65+
*
66+
* The run answers it ONCE, in a real browser, through the prompt's own decline
67+
* button ("Not now" / "Keep the default"), the same way the console's own live
68+
* suite does (ui/e2e/live/global-setup.ts). Declining writes nothing to the
69+
* workspace — the showcase stays exactly as seeded, so no spec's reading of an
70+
* amount moves — and the prompt keeps its per-device record of having asked in
71+
* this origin's localStorage (`PROMPT_RECORD_PREFIX`:ORG:USER). That record,
72+
* and nothing else the console stored while it was open, is what this returns
73+
* for the storageState every spec reuses: the rest (the sidebar's wide-open
74+
* flag, recent items, the active organization) would change the fresh-context
75+
* layout the specs measure from — detail-shapes derives its 780 px window from
76+
* a context with no stored sidebar state.
77+
*
78+
* Skipped when the backend says the prompt would not ask (the console's own
79+
* condition per key, `workspaceTimezonePromptLogic.ts`). A prompt that was
80+
* expected but never shown is reported, not failed: the specs then show whether
81+
* anything stands in their way. A prompt that was declined but left no record
82+
* throws: every spec would meet it again, and that is the run's one cause.
83+
*/
84+
const PROMPT_RECORD_PREFIX = 'os:workspace-timezone-prompt:';
85+
86+
async function declineWorkspacePrompt(
87+
token: string,
88+
launchOptions: LaunchOptions | undefined,
89+
): Promise<{ name: string; value: string }[]> {
90+
const api = await request.newContext({ extraHTTPHeaders: { Authorization: `Bearer ${token}` } });
91+
let wouldAsk = false;
92+
try {
93+
const res = await api.get(`${API}/api/settings/localization`);
94+
if (res.ok()) {
95+
const body = await res.json();
96+
const values = (body?.data ?? body)?.values;
97+
const timezone = values?.timezone;
98+
const currency = values?.currency;
99+
const timezoneUnset = timezone?.source === 'default' && !timezone?.locked;
100+
const currencyUnset =
101+
currency?.source === 'default' &&
102+
!currency?.locked &&
103+
(currency?.value === null || currency?.value === undefined || currency?.value === '');
104+
wouldAsk = timezoneUnset || currencyUnset;
105+
}
106+
} finally {
107+
await api.dispose();
108+
}
109+
if (!wouldAsk) return [];
110+
111+
const browser = await chromium.launch(launchOptions);
112+
try {
113+
const context = await browser.newContext({ storageState: STATE_PATH, baseURL: API });
114+
const page = await context.newPage();
115+
await page.goto(`/_console/apps/${APP}`, { waitUntil: 'domcontentloaded' });
116+
const prompt = page.getByTestId('workspace-timezone-prompt');
117+
try {
118+
await prompt.waitFor({ state: 'visible', timeout: 60_000 });
119+
} catch {
120+
console.warn('[showcase-smoke] the workspace timezone or currency is unset, but the console showed no prompt');
121+
return [];
122+
}
123+
await page.getByTestId('workspace-timezone-prompt-decline').click();
124+
await prompt.waitFor({ state: 'hidden' });
125+
const stored = await context.storageState();
126+
const record = (stored.origins.find((o) => o.origin === new URL(API).origin)?.localStorage ?? []).filter((e) =>
127+
e.name.startsWith(PROMPT_RECORD_PREFIX),
128+
);
129+
if (record.length === 0) {
130+
throw new Error(
131+
`the workspace prompt was declined but left no ${PROMPT_RECORD_PREFIX}* record in localStorage, ` +
132+
'so every spec would meet it again',
133+
);
134+
}
135+
return record;
136+
} finally {
137+
await browser.close();
138+
}
40139
}

0 commit comments

Comments
 (0)