Skip to content

Commit 35fc9b9

Browse files
test(showcase,ui-live): re-point the two nightly live specs to what record page v3 draws (#28009)
Part of #27114 — the two record-page-v3 reds of nightly run 41009107101 (reading 6516358130). The card stays open until a scheduled `Nightly tiers` run is green. Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只改两个夜间 live 测试 spec(`ui/e2e/live/record-history-display.spec.ts`、`examples/app-showcase/e2e/detail-shapes.spec.ts`),让它们按记录页 v3 实际画出的界面去找元素;产品代码零改动。 - 为什么改:夜间 `Nightly tiers` 在 main 上红了两例。一例是 1440 宽下"时间线"开关默认关、事件流折在关闭的「动态」卡片里;另一例是 #27473 把两栏下限改成 760 px 视图后,1200 宽已不再是标签页布局,而且 v3 把主相关列表画成详情页里的表格、不再是独立标签。 - 风险与回滚:只动测试,不影响任何发布包;回滚即 revert 本 PR。风险在于 780 px 这个窗口宽度依赖侧栏图标栏(48 px)和 760 px 下限,日后两者变动时需要重新测量(注释里写了推导)。 - 先看:① `examples/app-showcase/e2e/detail-shapes.spec.ts` 的 Contoso 用例(视口推导注释与新的 v3 断言);② `ui/e2e/live/record-history-display.spec.ts` 第 2 步(先断言开关关闭、卡片折叠,再点开「动态」卡片)。 - 两分钟验证:在构建好的工作区里运行 `cd examples/app-showcase && OS_TEST_TIERS=nightly pnpm exec vitest run`,预期 `Test Files 2 passed (2)`。 - 需要你做的:无,席位落地。 ## What changes Two test files only, each re-pointed at what record page v3 draws, measured under the nightly harness itself. No product change, no retry, no timeout raised, no test renamed, skipped or deleted. **1. `ui/e2e/live/record-history-display.spec.ts` (driven by `test/console-live-specs.live.test.ts`).** H1 holds. At 1440x900 the nightly harness (a built Console on a fresh database) draws the three-column tier. `showcase_account` is not a worked object: it declares no `stageField`, no lifecycle option `kind` and no `enable.files`. So the reader's Timeline switch defaults to off, and the stream is folded into the closed Activity card at the related column's foot. That card does not mount its blocks until it is opened, so the Changes chip never appeared. The failure snapshot reads `group "Panels"` with `button "Timeline"` (not pressed) and `region "Activity"` with a collapsed `button "Activity"`. The spec now opens that card the way a reader does, and asserts each step: - the Timeline segment of the Panels control reads `aria-pressed="false"`; - the card heading's toggle reads `aria-expanded="false"`, is clicked, then reads `"true"`; - the stream region is scoped inside that card, and every later assertion is unchanged. I picked opening the card over pressing the Timeline segment. The card is where the stream sits by default, and opening it changes no stored preference. The toggle is named by its heading, because the open stream has an "Activity" filter chip of its own. The first attempt matched both (a strict-mode violation) and was corrected in the second commit. The first assertion carries the 15 s page-open budget the spec already gave its first element. **2. `examples/app-showcase/e2e/detail-shapes.spec.ts`, Contoso case (driven by `test/showcase-smoke.live.test.ts`).** H2 holds, and there is a second staleness under it. - *Width.* Measured on this harness with a fresh context and no sidebar cookie: from 768 px (`MOBILE_BREAKPOINT`) to 1000 px the sidebar comes up as its 48 px icon rail. The tier flips between 808 px (two columns) and 800 px (tabs), which is window minus 48 against the 760 px view floor. 1024 px and up have the sidebar expanded, and at 1200 the tier is `two-column` with no tablist. Below 768 px the mobile shell gives the view the whole window, and 760 px drew two columns. So the desktop tabbed tier is a window of 768 to 807 px. The case is pinned at 780x800: 732 px of view, 28 px under the floor and 12 px above the mobile breakpoint. An expanded sidebar only narrows the view further. - *Prominence.* Even inside the tabbed tier, v3 (objectstack#27055 Target 8) draws `relatedList: 'primary'` lists, Invoices and Projects, as body tables in the Details tab. They are no longer tabs of their own. The strip at 780 px is `Details 5 / Related 3 / Timeline`. The case now asserts: - Details is selected and a Related tab exists; - there is no Invoices or Projects tab; - the Details panel holds the Invoices table (`INV-1002`) and the Projects table (`Data Platform`), their rows read at open; - clicking Related selects it and self-fetches Contacts (`Carl Fox`). This replaces the old `waitForTimeout(1500)` plus "panel text is non-empty", which an empty state would also pass. The flat-details, money-strip and footer assertions are unchanged. - *Tier choice.* The case's own header names its invariant: "related-heavy → related lists surface as tabs (prominence rule)". So the case stays in the tabbed tier with a derived viewport, and only its prominence assertions move to v3's rendering. Re-pointing it to the column tier would drop the only live-smoke pin on the tab strip. Every other page in this smoke runs at the Desktop Chrome viewport, which is a column tier. The tab strip is also what the drawer and the split pane draw at every width. The header bullet is updated to say this. ## Tests <details><summary>Repro at base d5951c8 (the nightly invocation): both reds</summary> `OS_TEST_TIERS=nightly pnpm exec vitest run --maxWorkers=2` in `examples/app-showcase`, under `os-verify-lock`: - `e2e/detail-shapes.spec.ts:173`: `expect(locator).toBeVisible() failed · Locator: getByRole('tab', { name: /Details/i }) · element(s) not found`. It failed on both attempts, the retry included. `1 failed · 52 passed (3.1m)`. - `e2e/live/record-history-display.spec.ts:92`: `TimeoutError: locator.waitFor: Timeout 15000ms exceeded` on `getByRole('region', { name: /^(Timeline|时间线)$/ }).locator('[data-stream-filter="changes"]')`. `1 failed · 19 passed (1.8m)`. - `Test Files 2 failed (2)` · `VERDICT command-exit 1`. </details> <details><summary>Green at head 6becc88 (same invocation)</summary> - showcase smoke: `53 passed (2.7m)` - Console live specs: `20 passed (1.6m)` - `Test Files 2 passed (2)` · `Tests 2 passed (2)` · `VERDICT command-exit 0` </details> <details><summary>Teeth: revert both fixes on top of the head, watch both cases fail, restore</summary> The script carries a `trap` restore. Both files were restored to base with `git restore --source=d5951c8e37` (tree only). Mutated blobs `024da82966` / `486a7df9e4` equal the base blobs. Fix-marker counts dropped to 0 / 0. - The same invocation then red-lined exactly the two cases: `detail-shapes.spec.ts:156` (`getByRole('tab', { name: /Details/i })` not found; `1 failed · 52 passed`) and `record-history-display.spec.ts:62` (`locator.waitFor: Timeout 15000ms`; `1 failed · 19 passed`). `Test Files 2 failed (2)` · `VERDICT command-exit 1`. - Restore by `git checkout HEAD --`. The restored blobs `d355411757` / `2961a09237` equal the HEAD blobs, `git diff HEAD` is 0 bytes and `git status --porcelain` is empty. </details> <details><summary>Typecheck</summary> `pnpm --filter @objectstack/example-showcase typecheck` and `ui`'s `type-check:e2e` both pass: `VERDICT command-exit 0`. `--listFiles` counts each edited spec once in its program: `e2e/detail-shapes` is in the showcase tsconfig, and `record-history-display` is in `ui/tsconfig.e2e.json`. </details> ## Acceptance notes - The 780 px pin is a derived reading of two current values: the 48 px icon rail and the 760 px floor. If the sidebar's auto-collapse range or `RECORD_PAGE_TWO_COLUMN_MIN_WIDTH` moves, this case is where it surfaces, as a missing tablist. The comment carries the derivation so the next re-pin is a re-measure, not a guess. - `record-history-display` now relies on the reader default (timeline off for a non-worked object). It asserts that default explicitly, so a run against a long-lived local stack whose browser stored the switch as on fails on a named assertion, not on a timeout. - The beforeAll comment in `detail-shapes.spec.ts` still says Contoso has "2 invoices". The seed has three (INV-1002, INV-1003, INV-1005). It is a comment outside this card's surface, left as is. ## Verification <details><summary>Local gates: dispatch-gates --commands derived 37 over the 2 paths; all exit 0</summary> Each gate ran with its exit code captured to disk before any pipe. 35 exited 0 first time. Two were reruns, not reds: - `pnpm check:dual-build-cjs-loads` first exited 3 (`PREREQUISITE NOT MET`: 7 packages outside the touched closure had no `dist/`). After building them it exited 0: `113 published require entry point(s) across 70 package(s) load`. - `pnpm check:query-options-erasure` hit my own 280 s per-gate cap (124). Rerun uncapped, it exited 0: `ratchet holds: 50 unswept non-test site(s) in 15 file(s), none new`. The reconciliation `dispatch-gates --ran` answered `37 derived famil(ies) accounted for — 37 run, 0 NOT-MEASURED (a DERIVED zero — all 37 recorded an exit code and none of them is 3)`, exit 0. `check:nul-bytes` exit 0, and the control-byte self-scan of both files found nothing. The families CI runs are read off this PR's check-runs. `dogfood_battery`: n/a, because there is no `!` in the title and no `(narrowing)` arm. `skip-changeset`: both files are test specs in packages that publish nothing (`ui/e2e/**`, and the private `@objectstack/example-showcase`). </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYmTYfaPbLQD4BJfomW)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d3d70d3 commit 35fc9b9

2 files changed

Lines changed: 78 additions & 28 deletions

File tree

‎examples/app-showcase/e2e/detail-shapes.spec.ts‎

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import { test, expect } from '@playwright/test';
1212
* - ungrouped → flat details body, no section cards
1313
* - stageField:false → NO `record:path` stepper despite a `status` field
1414
* named to bait the heuristic
15-
* - related-heavy → related lists surface as tabs (prominence rule)
15+
* - related-heavy → related lists surface by prominence (ADR-0085), in the
16+
* tabbed tier as record page v3 draws it (objectstack#27055,
17+
* Target 8): a `relatedList: 'primary'` list is a body
18+
* table in the Details tab, the rest sit behind the
19+
* Related tab and self-fetch when it is shown
1620
*
1721
* Runs against the console the backend serves at /_console (baseURL from
1822
* playwright.config.ts; storageState carries the admin session). The
@@ -156,25 +160,51 @@ test('stageField:false: status renders as a plain field, no stepper', async ({ p
156160
test('ungrouped + related-heavy: flat details and related-list tabs on Contoso', async ({ page }) => {
157161
// The synthesized record page lays itself out by the width of the view it
158162
// occupies (objectstack#26678): the tab strip this case opens exists only
159-
// below the two-column floor (`RECORD_PAGE_TWO_COLUMN_MIN_WIDTH` in
160-
// `@object-ui/plugin-detail`). The Desktop Chrome viewport leaves the record
161-
// view right at that floor once the console sidebar takes its share — two
162-
// columns and a "Related" toggle, no tablist — so the case pins a viewport
163-
// that keeps the page in its tabbed tier.
164-
await page.setViewportSize({ width: 1200, height: 800 });
163+
// below the two-column floor, a record view narrower than
164+
// `RECORD_PAGE_TWO_COLUMN_MIN_WIDTH` (760 px, `@object-ui/plugin-detail`).
165+
// The case stays in that tabbed tier on purpose: the column tiers are what
166+
// every other page of this smoke draws at the Desktop Chrome viewport, and
167+
// the tab strip is also what the drawer and split pane draw at any width.
168+
//
169+
// The window is derived, not picked. Measured on this harness (a fresh
170+
// browser context, so no stored sidebar cookie): from a 768 px window
171+
// (`MOBILE_BREAKPOINT`, the desktop shell) to 1000 px the console sidebar
172+
// came up as its 48 px icon rail, and the record view was the window minus
173+
// that rail — two columns at 808 px (760 of view), tabs at 800 px (752).
174+
// So the desktop tabbed tier is a window of 768 to 807 px, and an expanded
175+
// sidebar (256 px) only narrows the view further. 780 sits inside it with
176+
// headroom on both sides: 732 px of view against the 760 floor, 12 px above
177+
// the mobile breakpoint. ⛔ Not 1200 px: since objectstack#27473 moved the
178+
// floor to a 760 px view, 1200 draws columns and no tablist. ⛔ Not below
179+
// 768 px: that is the mobile shell, where the off-canvas sidebar gives the
180+
// view the whole window (760 px measured as two columns).
181+
await page.setViewportSize({ width: 780, height: 800 });
165182
const errors = await openRecord(page, recordUrl('showcase_account', contosoId));
166183

167184
// Heuristic stepper (account.status is a select named "status").
168185
// record:path renders a desktop + a mobile variant — assert the visible one.
169186
await expect(page.locator(PATH_STEPPER).first()).toBeVisible();
170187

171-
// Related lists surface as tabs (ADR-0085 prominence rule).
188+
// Related lists surface by prominence (ADR-0085), as record page v3 draws
189+
// them (objectstack#27055, Target 8): Invoices and Projects are
190+
// `relatedList: 'primary'` (CORE relationships), so each is a body table in
191+
// the Details tab, with its rows read at open; the rest of Contoso's lists
192+
// (Contacts and the Field Zoo lookups) sit behind the Related tab.
172193
const detailsTab = page.getByRole('tab', { name: /Details/i });
194+
const relatedTab = page.getByRole('tab', { name: /^Related/ });
173195
await expect(detailsTab).toBeVisible();
174-
const invoicesTab = page.getByRole('tab', { name: /Invoices/i });
175-
const projectsTab = page.getByRole('tab', { name: /Projects/i });
176-
await expect(invoicesTab).toBeVisible();
177-
await expect(projectsTab).toBeVisible();
196+
await expect(detailsTab).toHaveAttribute('aria-selected', 'true');
197+
await expect(relatedTab).toBeVisible();
198+
await expect(page.getByRole('tab', { name: /Invoices|Projects/i })).toHaveCount(0);
199+
const detailsPanel = page.getByRole('tabpanel', { name: /Details/i });
200+
const invoicesTable = detailsPanel
201+
.getByRole('table')
202+
.filter({ has: page.getByRole('columnheader', { name: 'Invoice Number' }) });
203+
const projectsTable = detailsPanel
204+
.getByRole('table')
205+
.filter({ has: page.getByRole('columnheader', { name: 'Project Name' }) });
206+
await expect(invoicesTable.getByRole('cell', { name: 'INV-1002', exact: true })).toBeVisible();
207+
await expect(projectsTable.getByRole('cell', { name: 'Data Platform', exact: true })).toBeVisible();
178208

179209
// Ungrouped shape: the Details body is flat — no fieldGroup section cards.
180210
// (Contoso declares no fieldGroups; spot-check a body field label.)
@@ -190,13 +220,13 @@ test('ungrouped + related-heavy: flat details and related-list tabs on Contoso',
190220
expect(footerText, 'footer dangles "Created by" without an actor').not.toMatch(/Created by/);
191221
expect(footerText).toMatch(/Created/);
192222

193-
// Related lists self-fetch lazily when their tab is shown.
194-
await invoicesTab.click();
195-
const panel = page.getByRole('tabpanel');
196-
await expect(panel).toBeVisible();
197-
await page.waitForTimeout(1500);
198-
const panelText = (await panel.innerText().catch(() => '')) || '';
199-
expect(panelText.trim().length, 'invoices tab rendered no content').toBeGreaterThan(0);
223+
// The non-primary lists self-fetch lazily when their tab is shown: the
224+
// Related tab's Contacts list reads Contoso's seeded contacts.
225+
await relatedTab.click();
226+
await expect(relatedTab).toHaveAttribute('aria-selected', 'true');
227+
const relatedPanel = page.getByRole('tabpanel', { name: /^Related/ });
228+
await expect(relatedPanel).toBeVisible();
229+
await expect(relatedPanel.getByRole('cell', { name: 'Carl Fox', exact: true })).toBeVisible();
200230

201231
expect(errors, 'uncaught page errors on related-heavy detail').toEqual([]);
202232
});

‎ui/e2e/live/record-history-display.spec.ts‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,7 @@ async function seededUsers(request: APIRequestContext): Promise<{ actor: SysUser
4343
/**
4444
* The synthesized record page lays itself out by the width of the view it
4545
* occupies (objectstack#26678): a column tier at `RECORD_PAGE_TWO_COLUMN_MIN_WIDTH`
46-
* (`@object-ui/plugin-detail`) and wider, the tab strip below it. The stream
47-
* is resident in either column tier — the timeline column, or, while the
48-
* reader's timeline switch is off (the default for showcase_account, which is
49-
* not a worked object), the Activity card at the related column's foot — and
50-
* mounted at open, so the spec needs no tab and no switch.
46+
* (`@object-ui/plugin-detail`) and wider, the tab strip below it.
5147
*
5248
* A 1440 px window is a column tier whatever the console sidebar does: 1184 px
5349
* of record view with the sidebar expanded (objectstack#27241's three-column
@@ -56,6 +52,19 @@ async function seededUsers(request: APIRequestContext): Promise<{ actor: SysUser
5652
* at 1000×800 the sidebar came up as its icon rail and the page drew two
5753
* columns with no tab strip at all, so a spec that waited for a tab there
5854
* timed out.
55+
*
56+
* In a column tier the stream is NOT resident by default for this object. The
57+
* reader's Timeline switch (the header's Panels control) defaults to off for
58+
* an object that is not worked (`recordTimelineDefaultReason`: showcase_account
59+
* declares no `stageField`, no lifecycle option `kind` and no `enable.files`),
60+
* and a switched-off timeline folds into the Activity (动态) card at the related
61+
* column's foot — closed, its blocks not mounted until the reader opens it
62+
* (`FoldedTimelineCard`, objectstack#27052). Measured under the nightly harness
63+
* (a built Console on a fresh database, so no stored switch): at 1440×900 the
64+
* page drew three columns, the Panels control read Timeline unpressed, and the
65+
* Activity card was folded, so a spec that waited for the Changes chip at open
66+
* timed out. The spec opens that card the way a reader does, and asserts each
67+
* step: the switch is off, the card starts closed, the click opens it.
5968
*/
6069
test.use({ viewport: { width: 1440, height: 900 } });
6170

@@ -83,11 +92,22 @@ test('the timeline stream\'s Changes chip shows display values (localized dates,
8392
expect(res.ok()).toBeTruthy();
8493
}
8594

86-
// 2) Open the synthesized record page and select the timeline stream's
87-
// Changes chip — the field changes alone (the audit entries, plus the
88-
// activity stream's own `created` row).
95+
// 2) Open the synthesized record page, open the folded Activity card that
96+
// holds the stream (see the viewport note above), and select the
97+
// stream's Changes chip — the field changes alone (the audit entries,
98+
// plus the activity stream's own `created` row).
8999
await page.goto(`/apps/showcase_app/showcase_account/record/${id}`);
90-
const stream = page.getByRole('region', { name: /^(Timeline|时间线)$/ });
100+
const timelineSwitch = page.locator('[data-panel-toggles] [data-panel-toggle="timeline"]');
101+
// The page-open wait: the same 15 s budget the spec gives its first element.
102+
await expect(timelineSwitch, 'the Timeline switch is off for an object that is not worked').toHaveAttribute('aria-pressed', 'false', { timeout: 15_000 });
103+
const activityCard = page.getByRole('region', { name: /^(Activity|动态)$/ });
104+
// The toggle is the card heading's button: once open, the stream inside the
105+
// card has an "Activity" filter chip of its own.
106+
const activityToggle = activityCard.getByRole('heading', { name: /^(Activity|动态)$/ }).getByRole('button');
107+
await expect(activityToggle, 'the switched-off timeline is folded into a closed Activity card').toHaveAttribute('aria-expanded', 'false');
108+
await activityToggle.click();
109+
await expect(activityToggle).toHaveAttribute('aria-expanded', 'true');
110+
const stream = activityCard.getByRole('region', { name: /^(Timeline|时间线)$/ });
91111
const changesChip = stream.locator('[data-stream-filter="changes"]');
92112
await changesChip.waitFor({ state: 'visible', timeout: 15_000 });
93113
await changesChip.click();

0 commit comments

Comments
 (0)