From 7b14d3c6ecd7763fb03483e3f33e821b2b1c583b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:15:26 +0000 Subject: [PATCH 1/2] test(ui): run the kept Console live specs nightly, delete the three that are stale or duplicated The live specs outside the required Live E2E UI job ran in no CI lane. Each of the eighteen was run on the workspace backend: fifteen pass and are kept, listed once in ui/package.json's test:e2e:live:nightly and run by examples/app-showcase/test/console-live-specs.live.test.ts in the nightly live tier. Deleted: studio-record-page (a record page is now created slotted with no copied regions, by design), detail-related-list (a row's name link now opens the record drawer, by design) and showcase-smoke (every surface and assertion it held is in the showcase package's nightly smoke). Claude-Session: https://claude.ai/code/session_01JQwxbXq8B9svQTRonMmTRf Co-authored-by: Claude --- .../test/console-live-specs.live.test.ts | 244 ++++++++++++++++++ ui/e2e/live/detail-related-list.spec.ts | 64 ----- ui/e2e/live/showcase-smoke.spec.ts | 92 ------- ui/e2e/live/studio-record-page.spec.ts | 55 ---- ui/package.json | 1 + 5 files changed, 245 insertions(+), 211 deletions(-) create mode 100644 examples/app-showcase/test/console-live-specs.live.test.ts delete mode 100644 ui/e2e/live/detail-related-list.spec.ts delete mode 100644 ui/e2e/live/showcase-smoke.spec.ts delete mode 100644 ui/e2e/live/studio-record-page.spec.ts diff --git a/examples/app-showcase/test/console-live-specs.live.test.ts b/examples/app-showcase/test/console-live-specs.live.test.ts new file mode 100644 index 00000000000..3f6163855d5 --- /dev/null +++ b/examples/app-showcase/test/console-live-specs.live.test.ts @@ -0,0 +1,244 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { spawnSync } from 'node:child_process'; +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; + +/** + * The Console's live Playwright specs that the required `Live E2E UI` job does + * not run, run in the NIGHTLY `live` tier. + * + * `ui/e2e/live/` drives the Console against a real backend serving this + * showcase app. Four of its specs are the required `Live E2E UI` job + * (`test:e2e:live:ci` in `ui/package.json`); the rest ran nowhere. The card + * that wired them here (#25914, ruling Q3 B) ran each one, deleted the stale + * ones, and listed the kept ones in `ui/package.json`'s `test:e2e:live:nightly` + * script — that script is the list's one home, and this file reads it by + * running it. Like `showcase-smoke.live.test.ts` beside it, the + * `*.live.test.*` name puts this file in the `live` tier + * (`scripts/nightly-tiers.mjs`): a pull request and the merge queue never + * collect it, and a red night files a card through `test-nightly-tiers.yml`'s + * report job, which names this file. The failure text names each red spec. + * + * The stack is the one the `Live E2E UI` job boots, step for step: + * 1. the workspace backend, `ui/e2e/live/ci/start-backend.sh` (this app, + * booted by this tree's built CLI, on a throwaway database); + * 2. the Console built with `VITE_BASE_PATH=/` into a scratch directory — + * the specs navigate root paths (`/apps/…`), so neither the `/_console` + * copy the backend serves nor the default relative-base build can serve + * them — and served by `vite preview`, proxying `/api` to that backend; + * 3. `pnpm --dir ui run test:e2e:live:nightly` against the two. + * Unlike the smoke beside it, this file runs one build itself (step 2, about + * two minutes): the nightly's shard builds the Console only for `/_console`. + * A failed build fails this test naming the build, never as a spec red. + * + * Prerequisites, which this file does not build: the showcase's dependency + * closure built (`turbo run test` builds it; the backend needs the CLI's + * `dist/`), the `@objectstack/*` packages the Console compiles against (the + * nightly's `pnpm console:build` on this shard builds them), and a Chromium + * that `ui/`'s `@playwright/test` can launch (the same version as this + * package's, which the nightly installs on this shard). A run missing one + * fails every spec at once; read a whole-run red as the environment first. + */ + +// `process.cwd()` is this package's established seed for its own files +// (`test/vitest-console-teardown-race.test.ts` says why `import.meta.url` is +// not available here). The `ui/` tree is two levels up, at the repo root. +const PACKAGE_ROOT = process.cwd(); +const REPO_ROOT = `${PACKAGE_ROOT}/../..`; +const UI_ROOT = `${REPO_ROOT}/ui`; +const CONSOLE_ROOT = `${UI_ROOT}/apps/console`; +const LIVE_CI = `${UI_ROOT}/e2e/live/ci`; +const VITE_BIN = `${CONSOLE_ROOT}/node_modules/.bin/vite`; +const PLAYWRIGHT_BIN = `${UI_ROOT}/node_modules/.bin/playwright`; +const NIGHTLY_SCRIPT = 'test:e2e:live:nightly'; + +/** + * The whole run's wall budget: backend boot (≤ 300 s by start-backend.sh's + * own deadline), the Console build (~2 min on a hosted runner) and the specs + * (~3 min of test time measured locally, one worker). 25 minutes keeps the + * nightly shard's 45-minute limit out of reach, so a hang fails as this test, + * with its report, and not as a killed shard with none. + */ +const RUN_BUDGET_MS = 25 * 60_000; +const BACKEND_BUDGET_MS = 6 * 60_000; +const BUILD_BUDGET_MS = 8 * 60_000; +const SPECS_BUDGET_MS = 10 * 60_000; + +/** A port the OS hands out (a listen on port 0), as `showcase-smoke.live.test.ts` does and for its reason. */ +function freePort(): number { + const probe = spawnSync( + 'node', + ['-e', "const s=require('node:net').createServer();s.listen(0,'127.0.0.1',()=>{process.stdout.write(String(s.address().port));s.close();});"], + { encoding: 'utf8' }, + ); + const port = Number(probe.stdout); + if (probe.status !== 0 || !Number.isInteger(port) || port <= 0) { + throw new Error(`could not get a free port from the OS (exit ${probe.status}): ${probe.stderr ?? ''}`); + } + return port; +} + +/** What a vitest worker adds to `process.env` — see `showcase-smoke.live.test.ts`, which measured it. */ +const VITEST_WORKER_ENV = ['NODE_ENV', 'TEST', 'BASE_URL', 'MODE', 'DEV', 'PROD', 'SSR', 'FORCE_TTY', 'OS_REGISTRY_LOG']; + +interface ReportTest { + readonly status?: string; +} +interface ReportSpec { + readonly title?: string; + readonly file?: string; + readonly tests?: readonly ReportTest[]; +} +interface ReportSuite { + readonly suites?: readonly ReportSuite[]; + readonly specs?: readonly ReportSpec[]; +} +interface PlaywrightReport { + readonly stats?: { readonly expected?: number; readonly unexpected?: number; readonly flaky?: number; readonly skipped?: number }; + readonly suites?: readonly ReportSuite[]; + readonly errors?: readonly { readonly message?: string }[]; +} + +/** `file › title` of every spec whose test Playwright called `unexpected`. */ +function failingSpecs(suites: readonly ReportSuite[] = []): string[] { + const out: string[] = []; + for (const suite of suites) { + for (const spec of suite.specs ?? []) { + if ((spec.tests ?? []).some((t) => t.status === 'unexpected')) out.push(`${spec.file ?? '?'} › ${spec.title ?? '?'}`); + } + out.push(...failingSpecs(suite.suites)); + } + return out; +} + +/** The last lines of a log file, for a failure message that has to stand on its own. */ +function tail(file: string, lines = 40): string { + if (!existsSync(file)) return `(no log at ${file})`; + return readFileSync(file, 'utf8').split('\n').slice(-lines).join('\n'); +} + +describe('Console live specs outside the required job (live tier)', () => { + it( + `every spec in ui's ${NIGHTLY_SCRIPT} passes against the workspace backend`, + () => { + for (const bin of [VITE_BIN, PLAYWRIGHT_BIN]) { + expect(existsSync(bin), `${bin} is missing: install the workspace`).toBe(true); + } + const uiScripts = (JSON.parse(readFileSync(`${UI_ROOT}/package.json`, 'utf8')) as { scripts?: Record }).scripts ?? {}; + expect(uiScripts[NIGHTLY_SCRIPT], `ui/package.json has no "${NIGHTLY_SCRIPT}" script — the kept-spec list's one home`).toBeTruthy(); + + const scratch = mkdtempSync(`${tmpdir()}/console-live-specs-`); + const backendDir = `${scratch}/backend`; + const consoleDir = `${scratch}/console`; + const previewLog = `${scratch}/vite-preview.log`; + const reportFile = `${scratch}/report.json`; + + // The children are not vitest workers: drop what vitest put into this + // worker's environment, so the backend boots as it does from a shell + // (`NODE_ENV=test` disarms the dev-admin seed the specs sign in with). + const env: Record = { ...process.env }; + for (const key of Object.keys(env)) { + if (key.startsWith('VITEST')) delete env[key]; + } + for (const key of VITEST_WORKER_ENV) delete env[key]; + delete env.FORCE_COLOR; + env.NO_COLOR = '1'; + + const apiPort = freePort(); + const appPort = freePort(); + const apiUrl = `http://localhost:${apiPort}`; + const appUrl = `http://localhost:${appPort}`; + const backendEnv = { ...env, LIVE_BACKEND_DIR: backendDir, LIVE_BACKEND_PORT: String(apiPort) }; + let previewPid = ''; + + try { + const backend = spawnSync('bash', [`${LIVE_CI}/start-backend.sh`], { + encoding: 'utf8', + env: backendEnv, + timeout: BACKEND_BUDGET_MS, + stdio: ['ignore', 'inherit', 'inherit'], + }); + expect(backend.status, `start-backend.sh exited ${backend.status}: the workspace backend did not boot.\n${tail(`${backendDir}/backend.log`)}`).toBe(0); + + const build = spawnSync( + 'bash', + ['-c', 'cd "$1" && exec "$2" build --outDir "$3" --emptyOutDir', 'bash', CONSOLE_ROOT, VITE_BIN, consoleDir], + { + encoding: 'utf8', + // The Console's build OOMs at Node's default heap (ci.yml's Build & E2E UI says so). + env: { ...env, VITE_BASE_PATH: '/', NODE_OPTIONS: `${env.NODE_OPTIONS ?? ''} --max-old-space-size=4096`.trim() }, + timeout: BUILD_BUDGET_MS, + stdio: ['ignore', 'inherit', 'inherit'], + }, + ); + expect(build.status, `the Console build (VITE_BASE_PATH=/) exited ${build.status} — read its output above; no spec ran`).toBe(0); + + // In the background: only the `nohup` command is backgrounded, so `$!` + // is the preview server itself (the bin shim ends in `exec node …`), + // which the `finally` below stops by that PID. + const preview = spawnSync( + 'bash', + [ + '-c', + 'cd "$1" || exit 1; DEV_PROXY_TARGET="$2" nohup "$3" preview --outDir "$4" --port "$5" --strictPort > "$6" 2>&1 & echo $!', + 'bash', + CONSOLE_ROOT, + apiUrl, + VITE_BIN, + consoleDir, + String(appPort), + previewLog, + ], + { encoding: 'utf8', env, timeout: 30_000 }, + ); + previewPid = (preview.stdout ?? '').trim(); + expect(preview.status === 0 && /^\d+$/.test(previewPid), `vite preview did not start: ${preview.stderr ?? ''}`).toBe(true); + const ready = spawnSync( + 'bash', + ['-c', 'for _ in $(seq 1 60); do curl -sf -o /dev/null "$1" && exit 0; sleep 1; done; exit 1', 'bash', `${appUrl}/`], + { encoding: 'utf8', env, timeout: 90_000 }, + ); + expect(ready.status, `vite preview did not answer on ${appUrl} within 60 s.\n${tail(previewLog)}`).toBe(0); + + // `list` streams one line per test to this process's stdout — the + // nightly's stall guard reads output to tell a slow suite from a hung + // one; `json` is the verdict read below. + const run = spawnSync('pnpm', ['--dir', UI_ROOT, 'run', NIGHTLY_SCRIPT, '--reporter=list,json'], { + encoding: 'utf8', + env: { ...env, LIVE_API_URL: apiUrl, LIVE_APP_URL: appUrl, PLAYWRIGHT_JSON_OUTPUT_NAME: reportFile }, + timeout: SPECS_BUDGET_MS, + stdio: ['ignore', 'inherit', 'inherit'], + }); + + expect( + existsSync(reportFile), + `playwright exited ${run.status} without writing its JSON report — it never reached the specs. ` + + 'Read its output above: a global setup that could not sign in, or a browser it could not launch.', + ).toBe(true); + const report = JSON.parse(readFileSync(reportFile, 'utf8')) as PlaywrightReport; + const stats = report.stats ?? {}; + const failing = failingSpecs(report.suites); + const ran = (stats.expected ?? 0) + (stats.unexpected ?? 0) + (stats.flaky ?? 0); + const errors = (report.errors ?? []).map((e) => (e.message ?? '').split('\n')[0]); + + expect( + { failing, errors }, + `${failing.length} of ${ran} Console live test(s) failed, ${errors.length} run-level error(s)` + + (errors.length > 0 || (failing.length > 0 && failing.length === ran) + ? ' — the whole run: read it as an environment failure first (backend not serving, no seeded admin, ' + + 'Console build not served, browser not launchable) before filing a product defect' + : ''), + ).toEqual({ failing: [], errors: [] }); + expect(ran, 'playwright reported no test that ran — a run of nothing is not a green run').toBeGreaterThan(0); + expect(run.status, `playwright exited ${run.status} with every test passing — read its output above`).toBe(0); + } finally { + if (previewPid) spawnSync('kill', [previewPid], { encoding: 'utf8' }); + spawnSync('bash', [`${LIVE_CI}/stop-backend.sh`], { encoding: 'utf8', env: backendEnv, timeout: 30_000 }); + rmSync(scratch, { recursive: true, force: true }); + } + }, + RUN_BUDGET_MS, + ); +}); diff --git a/ui/e2e/live/detail-related-list.spec.ts b/ui/e2e/live/detail-related-list.spec.ts deleted file mode 100644 index 65c9e39b40a..00000000000 --- a/ui/e2e/live/detail-related-list.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Live e2e for relationship-derived DETAIL-PAGE related lists — the read-side - * mirror of `inlineEdit`. - * - * `showcase_project.account` is a lookup → showcase_account, so the Account's - * record detail page must auto-render a "Projects" related list with NO page - * config (derived from the relationship by `deriveRelatedLists`). The list's - * title/columns come from `relatedListTitle` / `relatedListColumns` declared on - * the relationship. We open an Account record and assert the Projects related - * list is present. - */ -/** - * The synthesized record page lays itself out by the width of the view it - * occupies (objectstack#26678): the tab strip this spec opens exists only - * below the two-column floor (`RECORD_PAGE_TWO_COLUMN_MIN_WIDTH` in - * `@object-ui/plugin-detail`). The Desktop Chrome viewport leaves the record - * view right at that floor once the console sidebar takes its share, so the - * spec pins a viewport that keeps the page in its tabbed tier. - */ -test.use({ viewport: { width: 1200, height: 800 } }); - -test('Account detail page auto-renders a relationship-derived Projects related list', async ({ page }) => { - // 1) Open the Accounts list and drill into Northwind, a seeded account that - // owns projects (the status assertion below needs rows to read). - await page.goto('/apps/showcase_app/showcase_account'); - - // The data grid renders the primary (name) cell as a link to the record's - // full-page detail route, `/record/`. Grab the id from that route. - const nameLink = page.getByRole('row').getByRole('link', { name: 'Northwind', exact: true }).first(); - await nameLink.waitFor({ state: 'visible', timeout: 15_000 }); - await nameLink.click(); - - await page.waitForURL(/\/showcase_account\/record\/[^/?#]+/, { timeout: 15_000 }); - const recordId = new URL(page.url()).pathname.split('/record/')[1]?.split('/')[0]; - expect(recordId).toBeTruthy(); - - // 2) Open the canonical full-page detail (synthesized record page). - await page.goto(`/apps/showcase_app/showcase_account/record/${recordId}`); - await expect(page).toHaveURL(/\/showcase_account\/record\//, { timeout: 15_000 }); - - // 3) `showcase_project.account` declares `relatedList: 'primary'`, so the - // synthesized page gives that list its own tab, titled by its declared - // `relatedListTitle` ("Projects"); non-primary lists share a "Related" - // tab. Either way the tab only exists because `deriveRelatedLists` found - // the relationship — it's the read-side mirror at work. - // (The tab's name gains a row-count badge once the count loads.) - const projectsTab = page.getByRole('tab', { name: /^Projects/ }); - await projectsTab.waitFor({ state: 'visible', timeout: 15_000 }); - await projectsTab.click(); - - // 4) The relationship-derived related list renders in that tab. - const relatedPanel = page.getByRole('tabpanel', { name: /^Projects/ }); - await expect(relatedPanel.getByText('Projects', { exact: false }).first()).toBeVisible({ timeout: 15_000 }); - // The status column renders as a friendly, capitalized badge label (e.g. - // "Planned"/"Active") — NOT the raw lowercase value ("planned"). This guards - // the cell-renderer resolution for explicitly-supplied related-list columns. - // (Empty override columns like Health are pruned, so we don't assert those.) - await expect( - relatedPanel.getByText(/^(Planned|Active|On Hold|Completed|Cancelled)$/).first(), - ).toBeVisible({ timeout: 15_000 }); - await expect(relatedPanel.getByText(/^(planned|active|on_hold|completed|cancelled)$/)).toHaveCount(0); -}); diff --git a/ui/e2e/live/showcase-smoke.spec.ts b/ui/e2e/live/showcase-smoke.spec.ts deleted file mode 100644 index c2f38450386..00000000000 --- a/ui/e2e/live/showcase-smoke.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Live smoke for the framework `app-showcase` workspace — drives the REAL stack - * (console dev server + ObjectStack backend serving examples/app-showcase) and - * sweeps every navigation surface, asserting the health invariants that manual - * QA otherwise has to eyeball. It guards the *class* of bug found while QA-ing - * the showcase (empty charts, leaked dev placeholders, uncaught render errors): - * - * - no uncaught page exception (blank-page / render crash); - * - no recharts "width(-1) and height(-1)" warning (the dashboard chart-height - * collapse — `auto-rows-min` regression); - * - no leaked "record:… no actions configured" dev placeholder (the - * quick-actions location-filter regression); - * - the surface actually rendered something (main content present). - * - * Prereqs (see playwright.live.config.ts): backend on :3000 (objectstack serve - * --dev from examples/app-showcase) + console dev on :5180. Run: - * pnpm test:e2e:live e2e/live/showcase-smoke.spec.ts - * - * App route base is the showcase package id; override with SHOWCASE_APP. - */ -const APP = process.env.SHOWCASE_APP || 'com.example.showcase'; -const base = (seg: string) => `/apps/${APP}/${seg}`; - -// Every nav surface in the showcase app (workspace / data model / analytics / pages). -const SURFACES: { name: string; path: string; chart?: boolean }[] = [ - { name: 'My Work', path: base('page/showcase_my_work') }, - { name: 'Approvals', path: base('page/showcase_review_queue') }, - { name: 'New Project Wizard', path: base('page/showcase_new_project_wizard') }, - { name: 'Settings (preference)', path: base('showcase_preference') }, - { name: 'Projects', path: base('showcase_project') }, - { name: 'Tasks', path: base('showcase_task') }, - { name: 'Accounts', path: base('showcase_account') }, - { name: 'Invoices', path: base('showcase_invoice') }, - { name: 'Products', path: base('showcase_product') }, - { name: 'Teams', path: base('showcase_team') }, - { name: 'Categories', path: base('showcase_category') }, - { name: 'Field Zoo', path: base('showcase_field_zoo') }, - { name: 'Delivery Operations', path: base('dashboard/showcase_ops_dashboard'), chart: true }, - { name: 'Chart Gallery', path: base('dashboard/showcase_chart_gallery'), chart: true }, - { name: 'Hours by Status', path: base('report/showcase_hours_by_status') }, - { name: 'Status × Priority', path: base('report/showcase_status_priority_matrix') }, - { name: 'Task Overview', path: base('report/showcase_task_overview') }, - { name: 'Component Gallery', path: base('page/showcase_component_gallery') }, - { name: 'Project Workspace', path: base('page/showcase_project_workspace') }, - { name: 'Task Workbench', path: base('page/showcase_task_workbench') }, - { name: 'Task Triage', path: base('page/showcase_task_triage') }, - { name: 'Active Projects', path: base('page/showcase_active_projects') }, - { name: 'All Views', path: base('page/showcase_task_all_views') }, - { name: 'Task Board', path: base('page/showcase_task_board') }, - { name: 'Task Calendar', path: base('page/showcase_task_calendar') }, - { name: 'Task Gallery', path: base('page/showcase_task_gallery') }, - { name: 'Team Schedule', path: base('page/showcase_task_schedule') }, - { name: 'Activity Timeline', path: base('page/showcase_task_timeline') }, - { name: 'Work Map', path: base('page/showcase_task_map') }, -]; - -for (const surface of SURFACES) { - test(`surface renders cleanly: ${surface.name}`, async ({ page }) => { - const pageErrors: string[] = []; - page.on('pageerror', (e) => pageErrors.push(e.message)); - - await page.goto(surface.path, { waitUntil: 'domcontentloaded' }); - // Let data fetch + render settle (charts/grids hydrate after first paint). - await page.locator('main').first().waitFor({ state: 'visible', timeout: 20_000 }); - await page.waitForTimeout(1500); - - // 1) No uncaught render crash. - expect(pageErrors, `uncaught errors on ${surface.name}`).toEqual([]); - - // 2) No leaked dev placeholder (e.g. quick-actions location-filter regression). - await expect( - page.getByText(/no actions configured/i), - `leaked "no actions configured" placeholder on ${surface.name}`, - ).toHaveCount(0); - - // 3) The surface rendered content (not a blank shell). - const mainText = (await page.locator('main').first().innerText().catch(() => '')) || ''; - expect(mainText.trim().length, `${surface.name} rendered no main content`).toBeGreaterThan(0); - - // 4) Charts: no zero-height collapse, and at least one chart drew a sized SVG. - if (surface.chart) { - const svg = page.locator('.recharts-wrapper svg, .recharts-surface').first(); - await svg.waitFor({ state: 'visible', timeout: 20_000 }); - const box = await svg.boundingBox(); - expect(box, `${surface.name}: no chart SVG`).not.toBeNull(); - expect(box!.width, `${surface.name}: chart width`).toBeGreaterThan(0); - expect(box!.height, `${surface.name}: chart height`).toBeGreaterThan(0); - } - }); -} diff --git a/ui/e2e/live/studio-record-page.spec.ts b/ui/e2e/live/studio-record-page.spec.ts deleted file mode 100644 index 3844a60ccd7..00000000000 --- a/ui/e2e/live/studio-record-page.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Live e2e for #1541 (record-page authoring via Studio, ADR-0034). Creating a - * `pageType: 'record'` page bound to an object in Studio's "New page" form - * persists it as a draft AND seeds its `regions` from the object's synthesized - * default detail page (via the `page` resource's `createSeed` hook) — so the - * author starts from the auto-generated layout, not a blank canvas. - * - * (The page is then composed in the PagePreview canvas and published through - * the existing ResourceEditPage draft/publish chrome; render-on-records is - * handled by usePageAssignment over the synthesized default.) - */ -test('Studio: a new record page is created bound to its object and seeded from the default layout', async ({ page }) => { - const puts: any[] = []; - page.on('request', (r) => { - if (r.method() === 'PUT' && /\/meta\/page\//.test(r.url())) { - try { puts.push(r.postDataJSON()); } catch { /* ignore */ } - } - }); - - await page.goto('/apps/showcase_app/metadata/page/new'); - // The create form is generated from the page resource's createSchema. - await page.getByLabel(/^Label/i).first().waitFor({ state: 'visible', timeout: 20_000 }); - - const uniq = Date.now().toString().slice(-6); - // Label slugifies into Name; bind the object. Page type defaults to 'list' - // (the interface page, the most-built type), so a record page is picked. - await page.getByLabel(/^Label/i).first().fill(`Invoice Page ${uniq}`); - await page.getByLabel(/^Page type/i).click(); - await page.getByRole('option', { name: 'record', exact: true }).click(); - await expect(page.getByLabel(/^Page type/i)).toContainText('record'); - // The Object control is a select (`role=combobox`) listing the objects, not a - // text input. Anchored on the whole label: `/^Object/` also matches the app - // bar's "ObjectOS home" link. - await page.getByLabel(/^Object$/i).click(); - await page.getByRole('option', { name: 'showcase_invoice', exact: true }).click(); - await expect(page.getByLabel(/^Object$/i)).toContainText('showcase_invoice'); - - await Promise.all([ - page.waitForRequest((r) => r.method() === 'PUT' && /\/meta\/page\//.test(r.url()), { timeout: 15_000 }).catch(() => null), - page.locator('button[title^="Save"]').first().click(), - ]); - await page.waitForTimeout(800); - - expect(puts.length).toBeGreaterThan(0); - const item = (puts[puts.length - 1]?.item ?? puts[puts.length - 1]) as any; - expect(item.type).toBe('record'); - expect(item.object).toBe('showcase_invoice'); - // Seeded from buildDefaultPageSchema — NOT a blank `regions: []`. - expect(Array.isArray(item.regions)).toBe(true); - const blocks: string[] = item.regions.flatMap((r: any) => (r?.components || []).map((c: any) => c?.type)); - expect(blocks.length).toBeGreaterThan(0); - expect(blocks).toContain('record:highlights'); -}); diff --git a/ui/package.json b/ui/package.json index cf420f97e94..5237e8c0181 100644 --- a/ui/package.json +++ b/ui/package.json @@ -116,6 +116,7 @@ "test:e2e:ui": "playwright test --ui", "test:e2e:live": "playwright test --config=playwright.live.config.ts", "test:e2e:live:ci": "playwright test --config=playwright.live.config.ts screen-flow.spec.ts action-modal.spec.ts master-detail.spec.ts saved-view-filter.spec.ts", + "test:e2e:live:nightly": "playwright test --config=playwright.live.config.ts cascading-options.spec.ts declared-sharing-rules-reach-13091.spec.ts field-conditional-rules.spec.ts form-view-subforms.spec.ts grid-conditional-rules.spec.ts grid-file-upload.spec.ts grid-parent-rules.spec.ts inline-edit-polish-2572.spec.ts list-row-action-cel.spec.ts record-history-display.spec.ts required-when-submit.spec.ts studio-editor.spec.ts studio-object-designer.spec.ts summary-rollup.spec.ts user-filters.spec.ts", "test:e2e:import-harness": "playwright test --config=playwright.import-harness.config.ts", "test:e2e:import-console": "playwright test --config=playwright.import-console.config.ts" }, From 96eec69e7b0f54306b11b354e31b1c824a4916da Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:24:27 +0000 Subject: [PATCH 2/2] test(e2e): repoint ledger lines off the deleted live specs; drop the MSW error excuse The platform checklist cited studio-record-page.spec.ts, detail-related-list.spec.ts and the ui/e2e/live showcase smoke, which this branch deletes. Each citing line is repointed (showcase smoke -> examples/app-showcase/e2e/showcase-smoke.spec.ts) or dropped, with a revision bump and history entry per item. console-rendering.spec.ts no longer excuses mockServiceWorker/MSW console errors: the Console ships no mock service worker, and the idle MSW test was already removed. Claude-Session: https://claude.ai/code/session_01JQwxbXq8B9svQTRonMmTRf Co-authored-by: Claude --- .../areas/platform-core.json | 9 ++--- .../areas/records-forms.json | 9 ++--- .../areas/studio-authoring.json | 33 ++++++++++--------- ui/e2e/console-rendering.spec.ts | 7 ++-- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index d7e5fecf99a..14ae4e7d266 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -483,7 +483,7 @@ "title": "Metadata authoring round-trip: draft → publish on a WRITABLE package; read-only packages and locked types are server-side refused", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -519,7 +519,7 @@ { "clause": "Studio's designer authors through the same pipeline: creating a record page issues PUT /api/v1/meta/page/ bound to its object and seeded from the default layout", "oracle": "network", - "verify": "capture the PUT during Studio create (pinned by objectui e2e/live/studio-record-page.spec.ts, which waits on exactly that request and asserts its body — see automated.ref)", + "verify": "capture the PUT during Studio create (hand-driven: no live spec pins this request)", "evidence": "the captured PUT" }, { @@ -549,7 +549,7 @@ "traps": ["dispatcher-vs-hono-route", "stale-console-bundle"], "automated": { "kind": "api", - "ref": "packages/qa/dogfood/test/package-first-authoring.dogfood.test.ts (scores NO clause of this item: it calls protocol.saveMetaItem in-process on a booted CRM stack — the WRITABLE_PACKAGE_REQUIRED refusal with its writable-base control, then the package lifecycle — and drives no /api/v1/meta door, so it is protocol-layer evidence for acceptance[3] only); objectui: e2e/live/studio-record-page.spec.ts (acceptance[2] ONLY: it creates a record page bound to showcase_invoice and asserts the captured PUT /meta/page body's type 'record', its object, and regions seeded with a record:highlights block), e2e/live/studio-object-designer.spec.ts (scores NO clause of this item: its cases drive the object designer and the Studio package switcher, and no clause here scores either)" + "ref": "packages/qa/dogfood/test/package-first-authoring.dogfood.test.ts (scores NO clause of this item: it calls protocol.saveMetaItem in-process on a booted CRM stack — the WRITABLE_PACKAGE_REQUIRED refusal with its writable-base control, then the package lifecycle — and drives no /api/v1/meta door, so it is protocol-layer evidence for acceptance[3] only); objectui: e2e/live/studio-object-designer.spec.ts (scores NO clause of this item: its cases drive the object designer and the Studio package switcher, and no clause here scores either)" }, "source": [ "packages/runtime/src/route-ledger.ts (PUT /meta/:type/:name, GET /meta/_drafts, GET /meta/:type/:name/published, POST /packages/:id/publish-drafts)", @@ -563,7 +563,8 @@ { "revision": 3, "date": "2026-10-16", "change": "steps[1], [3] and [7] and acceptance[0].verify and [5].verify re-pointed (checklist-accuracy findings of QA run #23939, wave #23878). A plain PUT /meta/view/NAME saves state=active, so the draft never appeared in /meta/_drafts — ?mode=draft is required — and a view's name must be qualified OBJECT.view (#23939 used showcase_task.os_qa_view_RUNID). The Studio half now names the create form (/_console/apps/showcase_app/metadata/page/new) and its 'list' default, which has to be set to 'record' for a record page. Not loosened: every clause still scores the same server reads", "ref": "#24036" }, { "revision": 4, "date": "2026-10-20", "change": "automated.stale.observedIn, partial, why and ownedBy refreshed — the carried note from round 6 of wave anchor #24579 (its report, comment 6280484387), the refresh round 6 made on studio-authoring.record-page-roundtrip in PR #24697. Tier 1 run #24598 re-measured studio-record-page.spec.ts red at the sweep's console pin 4016370af3c9 (0 passed / 1 failed), and the spec is byte-identical at the shipped pin 12dc7b47c340, so the marker first written from #7695 still holds; ownedBy names the family card objectstack-ai/objectui#13104. The why also said studio-object-designer.spec.ts was unaffected; #24598 ran its F1 and F4 red (F2 and F3 passed), and partial now says so. No clause changed", "ref": "#24717" }, { "revision": 5, "date": "2026-10-20", "change": "automated.stale removed, automated.ref scoped per objectui spec, and acceptance[2].verify's stale note rewritten — round 14 of wave anchor #24579 (18.0 sweep), on the objectui pointer 6284006658. objectui#13132 (objectui 0b36ffa) repaired both specs the stale record named, and the console pin moved to 616df6598b42 (PR #24829), which contains 0b36ffa. studio-record-page.spec.ts: blob 181b2562c18d at the old pin 12dc7b47c340, 3844a60ccd79 at both 0b36ffa and 616df6598b42; it now sets Page type to record and picks the object from the Object combobox. studio-object-designer.spec.ts: blob 5967807df335, then 58a7f1cbf024 at both; F1 and F4 repaired. objectui measured its whole live suite at 0b36ffa as 61 passed (11.0m) on the shipped showcase; no live run exists at 616df6598b42, so the verdict there is NOT MEASURED. Re-read at the new pin, the ref keeps only what this record claimed: studio-record-page.spec.ts asserts acceptance[2], the clause the stale record sent to hand-driving, and studio-object-designer.spec.ts, on which no clause here rests, now says so. The dogfood pin is unchanged. No clause changed", "ref": "#24857" }, - { "revision": 6, "date": "2026-10-20", "change": "automated.ref and acceptance[3].verify narrowed — checklist-accuracy finding of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4: acceptance[3]'s verify names the dogfood pin as covering the PUT door), round 15 of wave anchor #24579. Re-read at base b1676441ba, after round 14's ref edit: package-first-authoring.dogfood.test.ts calls protocol.saveMetaItem in-process on a booted CRM stack and never PUT /api/v1/meta. By RUNNER rule 6 (a pin scores only the layer it drives) it scores no clause of this item, and the ref and acceptance[3].verify now say so. The objectui studio-record-page.spec.ts keeps acceptance[2]. No clause changed", "ref": "#24891" } + { "revision": 6, "date": "2026-10-20", "change": "automated.ref and acceptance[3].verify narrowed — checklist-accuracy finding of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4: acceptance[3]'s verify names the dogfood pin as covering the PUT door), round 15 of wave anchor #24579. Re-read at base b1676441ba, after round 14's ref edit: package-first-authoring.dogfood.test.ts calls protocol.saveMetaItem in-process on a booted CRM stack and never PUT /api/v1/meta. By RUNNER rule 6 (a pin scores only the layer it drives) it scores no clause of this item, and the ref and acceptance[3].verify now say so. The objectui studio-record-page.spec.ts keeps acceptance[2]. No clause changed", "ref": "#24891" }, + { "revision": 7, "date": "2026-10-28", "change": "automated.ref and acceptance[2].verify drop ui/e2e/live/studio-record-page.spec.ts, deleted by the triage of the live specs that ran in no CI lane (ruling Q3 B): measured red on the workspace backend at 1e8e5296, alone, twice: a Studio-created record page is saved kind slotted with no copied regions unless the author picks full (fd73f2b3, the page createSeed in ui/packages/app-shell/src/views/metadata-admin/anchors.ts; both outcomes unit-pinned in anchors.page-seed.test.ts beside it), so the spec's seeded-regions assertion reads a superseded design. acceptance[2] is hand-driven again. Its clause still reads 'seeded from the default layout', which now holds only for a page the author creates full; that clause is left for the ledger's owner. No clause changed", "ref": "#25914" } ] }, { diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 1923a96e123..068000b9ca8 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -1537,7 +1537,7 @@ "title": "Related lists derive from the relationship and page on the server — never fetch every child row — and are read-gated on both ends", "since": "v16", "status": "active", - "revision": 9, + "revision": 10, "priority": "P1", "surface": "browser", "personas": [ @@ -1584,7 +1584,7 @@ { "clause": "related lists are DERIVED from the relationship declaration with no hand-built page: the Account detail auto-renders them (Projects is the pinned example)", "oracle": "dom", - "verify": "after screenshot, the related tabs exist on the stock detail page (pinned by objectui e2e/live/detail-related-list.spec.ts)", + "verify": "after screenshot, the related tabs exist on the stock detail page", "evidence": "detail screenshot" }, { @@ -1616,7 +1616,7 @@ ], "automated": { "kind": "e2e", - "ref": "objectui: e2e/live/detail-related-list.spec.ts — acceptance[2] ONLY (the relationship-derived Projects related list on the Account detail page: Northwind's detail renders a Projects tab whose panel shows the projects' status labels). packages/qa/dogfood/test/related-list-server-paging-and-gate.dogfood.test.ts, at the REST data door the related list reads through (GET /api/v1/data/showcase_contact scoped by the Northwind filter) on the showcase as shipped, pins the server halves: acceptance[1] and negative[1] (the filtered read holds the 26 seeded contacts; $top=10 pages at $skip 0, 10 and 20 each answer total 26 and return 10, 10 and 6 rows with no id repeated or skipped), acceptance[4] (each served child re-reads by its own id with the same row) and acceptance[5] with negative[2] (a member staffed contributor is refused the identical query 403 with no rows; a member staffed manager reads it 200 with rows). acceptance[0] and negative[0] (the console's own top/skip on every child request), the pager text, acceptance[3] (the Invoices tab's authored title and columns) and the Contacts tab's absence for the contributor stay live browser legs: they are judged on the console's requests and rendered detail page, which no HTTP pin reaches, and the card's ruling adds no Playwright lane (#26573)" + "ref": "packages/qa/dogfood/test/related-list-server-paging-and-gate.dogfood.test.ts, at the REST data door the related list reads through (GET /api/v1/data/showcase_contact scoped by the Northwind filter) on the showcase as shipped, pins the server halves: acceptance[1] and negative[1] (the filtered read holds the 26 seeded contacts; $top=10 pages at $skip 0, 10 and 20 each answer total 26 and return 10, 10 and 6 rows with no id repeated or skipped), acceptance[4] (each served child re-reads by its own id with the same row) and acceptance[5] with negative[2] (a member staffed contributor is refused the identical query 403 with no rows; a member staffed manager reads it 200 with rows). acceptance[0] and negative[0] (the console's own top/skip on every child request), the pager text, acceptance[2] (the relationship-derived Projects tab on Northwind's detail page and its status labels), acceptance[3] (the Invoices tab's authored title and columns) and the Contacts tab's absence for the contributor stay live browser legs: they are judged on the console's requests and rendered detail page, which no HTTP pin reaches, and the card's ruling adds no Playwright lane (#26573)" }, "source": [ "#3358 §4 (evidence run captured exactly this trace)", @@ -1679,7 +1679,8 @@ "date": "2026-10-27", "change": "automated.ref names the dogfood pin for the server halves of acceptance[1], [4], [5] and negative[1], [2] (#26573 slice 4), and says why acceptance[0], [3], negative[0] and the rendered halves stay live browser legs. No clause changed", "ref": "#26573" - } + }, + { "revision": 10, "date": "2026-10-28", "change": "automated.ref and acceptance[2].verify drop ui/e2e/live/detail-related-list.spec.ts, deleted by the triage of the live specs that ran in no CI lane (ruling Q3 B): measured red on the workspace backend at 1e8e5296, alone, twice: a row's name link now opens the record drawer (the record-open preference, #26681) and the spec waited for the /record/ID route it no longer navigates to. acceptance[2] joins the live browser legs the ref lists; its derivation, own-tab promotion and option-label cells stay unit-pinned in objectui (deriveRelatedLists.test.ts, buildDefaultPageSchema.test.ts, RelatedList.columnIdentityAccessor.test.tsx). The dogfood pin is unchanged. No clause changed", "ref": "#25914" } ] }, { diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index fd040360662..a867074861b 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -289,7 +289,7 @@ "title": "Record-page authoring round-trip: created bound to its object, seeded from the default layout, block-edited, published, rendered", "since": "v16", "status": "active", - "revision": 9, + "revision": 10, "priority": "P1", "surface": "mixed", "personas": ["admin (authors)", "end user (opens the record). On stock no non-admin persona reads showcase_contact (auditor.demo is refused 'You don't have access to Contact records', QA run #24604), so the viewer is the admin unless a grant is provisioned — e.g. a member bound to the `manager` position, whose showcase_manager set reads showcase_contact (examples/app-showcase/src/security/permission-sets.ts); record which"], @@ -314,7 +314,7 @@ { "clause": "the created page persists bound to its object with PRE-SEEDED regions — record:highlights present, regions non-empty", "oracle": "network", - "verify": "the captured PUT body has type:'record', object:'showcase_contact', Array.isArray(regions) with blocks including record:highlights (pinned carrier-neutrally by objectui e2e/live/studio-record-page.spec.ts, which binds showcase_invoice — see automated.ref)", + "verify": "the captured PUT body has type:'record', object:'showcase_contact', Array.isArray(regions) with blocks including record:highlights", "evidence": "the captured PUT payload" }, { @@ -342,10 +342,9 @@ "traps": ["stale-console-bundle", "hydration-race", "automation-input"], "automated": { "kind": "e2e", - "ref": "ui/e2e/live/studio-record-page.spec.ts — acceptance[0], carrier-neutrally: it creates a record page bound to showcase_invoice (not this item's showcase_contact) and asserts the captured PUT body's type 'record', its object, and regions seeded with a record:highlights block. + ui/e2e/live/studio-editor.spec.ts — acceptance[1], carrier-neutrally: 'Add block opens a block-type picker with configurable block kinds' opens the live page editor on an environment-authored copy of the shipped showcase_project_workspace (an app page, not this item's showcase_contact record page) and asserts that the dialog offers Card, Section and Record details. The picker offers one static catalogue whatever the page (AddBlockButton in objectui's PageBlockCanvas.tsx reads TYPES_BY_CATEGORY from block-types.ts), so the carrier does not change the answer. Both were read at the console pin cf153eb2e535 (blobs 3844a60ccd79 and 19f9cc131654) and again at objectui b41ed7f68956, QA run #25815's pin, where both are present and assert as described; they now live under ui/e2e/live in this checkout. Neither covers acceptance[2] or [3]" + "ref": "ui/e2e/live/studio-editor.spec.ts — acceptance[1], carrier-neutrally: 'Add block opens a block-type picker with configurable block kinds' opens the live page editor on an environment-authored copy of the shipped showcase_project_workspace (an app page, not this item's showcase_contact record page) and asserts that the dialog offers Card, Section and Record details. The picker offers one static catalogue whatever the page (AddBlockButton in objectui's PageBlockCanvas.tsx reads TYPES_BY_CATEGORY from block-types.ts), so the carrier does not change the answer. It was read at the console pin cf153eb2e535 (blob 19f9cc131654) and again at objectui b41ed7f68956, QA run #25815's pin, where it is present and asserts as described; it now lives under ui/e2e/live in this checkout and runs in Nightly Tiers. It covers none of acceptance[0], [2] or [3]" }, "source": [ - "objectui: e2e/live/studio-record-page.spec.ts (#1541, ADR-0034 — create bound + seeded regions, asserted off the PUT payload)", "objectui: e2e/live/studio-editor.spec.ts (page editor sections + Add-block picker contract)" ], "history": [ @@ -392,7 +391,8 @@ "date": "2026-10-26", "change": "automated.ref names the run's objectui pin beside the console pin it cited (checklist-accuracy finding of QA run #25815, release-18.2.0-tier1 at 6679644a, objectui pin b41ed7f68956; wave anchor #25807): the ref cited cf153eb2e535 for the reading, the run read both specs at b41ed7f68956, present and asserting as described. automated.ref and fixtures.knownGaps[0] re-pointed to ui/. The retired objectui-pin wording is replaced on the way (#26035 left this file to its card): objectui is imported under ui/, so the pins resolve in this checkout and the console builds from this tree with pnpm console:build. The clauses are unchanged", "ref": "#25848" - } + }, + { "revision": 10, "date": "2026-10-28", "change": "automated.ref, acceptance[0].verify and source[0] drop ui/e2e/live/studio-record-page.spec.ts, deleted by the triage of the live specs that ran in no CI lane (ruling Q3 B): measured red on the workspace backend at 1e8e5296, alone, twice: a Studio-created record page is saved kind slotted with no copied regions unless the author picks full (fd73f2b3, the page createSeed in ui/packages/app-shell/src/views/metadata-admin/anchors.ts; both outcomes unit-pinned in anchors.page-seed.test.ts beside it), so the spec's seeded-regions assertion reads a superseded design. acceptance[0] is hand-driven again; its clause still asks for seeded regions, which now holds only for a page the author creates full, and is left for the ledger's owner. studio-editor.spec.ts keeps acceptance[1]. No clause changed", "ref": "#25914" } ] }, { @@ -975,7 +975,7 @@ "title": "Declarative custom pages render their real block COMPOSITION (not just 'no error'): each declared block resolves its renderer and binds its data source, data-bound blocks show seeded rows (API cross-check), and a block that cannot bind degrades with a NAMED error, never a blank region", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai) driving the published showcase app as an end user"], @@ -986,7 +986,7 @@ "a scratch page name (e.g. qa_page_probe) authorable via the draft door PUT /api/v1/meta/page/qa_page_probe?mode=draft — used ONLY for the degradation probe, so no shipped showcase page is mutated" ], "knownGaps": [ - "objectui e2e/live/showcase-smoke.spec.ts sweeps only a SUBSET of these pages (My Work, Project Workspace, Active Projects, Component Gallery are in its SURFACES list; Command Center 大屏 / showcase_command_center is NOT) and only asserts render-clean (no pageerror / no placeholder leak / non-empty
). This item DEEPENS that shallow pin: it asserts the block composition and the data binding, which the smoke does not — so it is NOT marked `automated`.", + "examples/app-showcase/e2e/showcase-smoke.spec.ts (the showcase's browser smoke, run by Nightly Tiers) sweeps these pages, Command Center 大屏 / showcase_command_center included, and only asserts render-clean (no pageerror / no placeholder leak / non-empty
past the boot splash / a laid-out chart). This item DEEPENS that shallow pin: it asserts the block composition and the data binding, which the smoke does not — so it is NOT marked `automated`.", "command-center's KPIs read via the console adapter's aggregate path; where the exact aggregate endpoint differs from a plain GET, cross-check the tile against the same filter applied to the data plane (count of the matching rows), not against a pixel." ] }, @@ -1063,11 +1063,12 @@ "examples/app-showcase/src/ui/pages/command-center.page.ts#chart (object-metric bare KPIs + object-chart bar/donut/area on datasets + object-grid work queue), my-work.page.ts ({current_user_id} object-grid + page:card visible-gate), project-workspace.page.ts (object-master-detail-form), active-projects.page.ts (interfaceConfig list: filterBy/sort/appearance/userFilters/addRecord)", "objectui: packages/components/src/renderers/layout/page.tsx (PageRenderer — region/template dispatch, full-bleed for width:'full' main)", "objectui: packages/react/src/SchemaRenderer.tsx (the OBJUI-001 'Unknown component type' red panel + the SchemaErrorBoundary 'Component failed to render' panel — the NAMED-degradation contract; page./data/user expression scope)", - "objectui: e2e/live/showcase-smoke.spec.ts (the shallow render-clean sweep for my-work/project-workspace/active-projects — this item deepens it with block-composition + data-binding assertions)", + "examples/app-showcase/e2e/showcase-smoke.spec.ts (the nightly render-clean sweep for my-work/project-workspace/active-projects/command-center — this item deepens it with block-composition + data-binding assertions)", "examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts#showcase_task_metrics (showcase_task_metrics / showcase_project_metrics — the chart bindings)" ], "history": [ - { "revision": 1, "date": "2026-08-08", "change": "new item: declarative custom pages render their real block composition and bind data (API cross-check, not pixels), with a scratch-page probe proving a bad binding degrades to a NAMED error not a blank — grounded in the four showcase declarative page sources, the PageComponentType enum, objectui PageRenderer + SchemaRenderer, and deepening the shallow showcase-smoke render-clean pin", "ref": "claude/platform-test-checklist-ocwugl" } + { "revision": 1, "date": "2026-08-08", "change": "new item: declarative custom pages render their real block composition and bind data (API cross-check, not pixels), with a scratch-page probe proving a bad binding degrades to a NAMED error not a blank — grounded in the four showcase declarative page sources, the PageComponentType enum, objectui PageRenderer + SchemaRenderer, and deepening the shallow showcase-smoke render-clean pin", "ref": "claude/platform-test-checklist-ocwugl" }, + { "revision": 2, "date": "2026-10-28", "change": "knownGaps/source re-pointed from ui/e2e/live/showcase-smoke.spec.ts, deleted by the triage of the live specs that ran in no CI lane (ruling Q3 B) as a duplicate: every surface it swept and each of its four assertions are in examples/app-showcase/e2e/showcase-smoke.spec.ts, which Nightly Tiers runs, with a wider surface list and stricter waits. That sweep includes showcase_command_center, which the old note said it did not. Still not marked automated: the sweep is render-clean only. No clause changed", "ref": "#25914" } ] }, { @@ -1075,7 +1076,7 @@ "title": "Page variables + page-level actions are runtime-live: variables initialize empty, an interactive writer updates one and dependent visibleWhen predicates re-evaluate WITHOUT reload, and a page action reads the live variable snapshot to POST resolved values that create a real record", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai) driving the published showcase app as an end user"], @@ -1086,7 +1087,7 @@ "the public web-to-lead endpoint POST /api/v1/forms/contact-us/submit and its showcase_inquiry sink (ADR-0056) reachable on the stock showcase boot — the contact-form submit action posts here" ], "knownGaps": [ - "neither showcase_page_variables nor showcase_contact_form is in objectui e2e/live/showcase-smoke.spec.ts's SURFACES sweep — there is no automated render-clean pin for either, so this item is browser-driven end to end (not marked `automated`)" + "showcase_page_variables and showcase_contact_form are in examples/app-showcase/e2e/showcase-smoke.spec.ts's SURFACES sweep (run by Nightly Tiers), which pins render-clean only; nothing automated drives the page-variable loop or the submit, so this item is browser-driven end to end (not marked `automated`)" ] }, "steps": [ @@ -1158,7 +1159,8 @@ "ADR-0056 (public web-to-lead form → showcase_inquiry), ADR-0089 (visibleWhen canonical predicate)" ], "history": [ - { "revision": 1, "date": "2026-08-08", "change": "new item: the end-to-end page-variable loop (initialize empty → interactive writer updates → dependent visibleWhen re-evaluates live, no reload) and the page-level `api` action resolving {{page.}} from the live snapshot to create a real showcase_inquiry — grounded in PageVariableSchema, the page-variables + contact-form sources, and objectui's PageVariablesProvider/PageVariableActionBridge", "ref": "claude/platform-test-checklist-ocwugl" } + { "revision": 1, "date": "2026-08-08", "change": "new item: the end-to-end page-variable loop (initialize empty → interactive writer updates → dependent visibleWhen re-evaluates live, no reload) and the page-level `api` action resolving {{page.}} from the live snapshot to create a real showcase_inquiry — grounded in PageVariableSchema, the page-variables + contact-form sources, and objectui's PageVariablesProvider/PageVariableActionBridge", "ref": "claude/platform-test-checklist-ocwugl" }, + { "revision": 2, "date": "2026-10-28", "change": "knownGaps/source re-pointed from ui/e2e/live/showcase-smoke.spec.ts, deleted by the triage of the live specs that ran in no CI lane (ruling Q3 B) as a duplicate: every surface it swept and each of its four assertions are in examples/app-showcase/e2e/showcase-smoke.spec.ts, which Nightly Tiers runs, with a wider surface list and stricter waits. That sweep includes both of this item's pages, which the old note said it did not; it pins render-clean only, so the item stays browser-driven. No clause changed", "ref": "#25914" } ] }, { @@ -1166,7 +1168,7 @@ "title": "Source-authored page tiers render by the RIGHT path: kind:'html' source is PARSED-never-executed (static, in-browser sdui-parser compile), kind:'react' source is EXECUTED behind its capability gate (live data + interactivity) — command-center-jsx / capability-map (html) vs crm-workbench (react)", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai) driving the published showcase app as an end user"], @@ -1178,7 +1180,7 @@ ], "knownGaps": [ "NO manual build step is needed to VIEW either tier in the running showcase (contra the task's guess): a kind:'html' page compiles IN-BROWSER via @object-ui/sdui-parser compile(source, manifest) at render, against a whitelist manifest built from the LIVE ComponentRegistry (packages/components/src/renderers/layout/page.tsx); a kind:'react' page is transpiled IN-BROWSER by a lazily-loaded Sucrase @object-ui/react-runtime chunk. The only build-shaped risk is the vendored /_console bundle being stale (stale-console-bundle) — a stale sdui-parser/react-runtime mis-compiles a page that is fine on a fresh build. ⚠ A FAULTING html source never reaches the RENDERED page on a stock boot: the metadata save door compiles an html page's source against the deployment's manifest with the same compiler `os validate` uses (packages/metadata-protocol/src/runtime-authoring-gate.ts#HTML_PAGE_SOURCE_COMPILE), so an active save or a publish of a faulting html page answers 422 INVALID_METADATA carrying the compile diagnostics, and nothing is served. The 'HTML page failed to compile (N)' panel is therefore reachable only in the page designer's DRAFT preview, where a ?mode=draft save of the faulting source is admitted and rendered (QA run #23166)", - "none of showcase_command_center_jsx / showcase_capability_map / showcase_crm_workbench is in objectui e2e/live/showcase-smoke.spec.ts's SURFACES sweep — there is no automated render-clean pin for the html/react tiers, so this item is browser-driven (not marked `automated`)." + "showcase_command_center_jsx, showcase_capability_map and showcase_crm_workbench are in examples/app-showcase/e2e/showcase-smoke.spec.ts's SURFACES sweep (run by Nightly Tiers), which pins render-clean only; no pin drives either tier's compile path or the capability gate, so this item is browser-driven (not marked `automated`)." ] }, "steps": [ @@ -1252,7 +1254,8 @@ { "revision": 1, "date": "2026-08-08", "change": "new item: the html (parse-never-execute, in-browser sdui-parser compile, static) vs react (executed at render, capability-gated, live data + interactivity) source-page tiers — the objectui renderer-path split grounded in PageRenderer's kind dispatch + react-page.tsx's gate, distinguishing command-center-jsx/capability-map (html) from crm-workbench (react); records the honest build-step finding (none needed to view — in-browser compile/transpile) as a knownGap", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-10-13", "change": "acceptance[5] (clause and verify), steps[7] and knownGaps[0] name where an html compile error is observable (assertion defect, minor, #23166 A5; parked on #23121, carried by #23169). The save door compiles an html page's source against the deployment's manifest (runtime-authoring-gate.ts HTML_PAGE_SOURCE_COMPILE), so a faulting html page is refused 422 INVALID_METADATA at save / publish and never publishes, and the compile-error panel is reachable only in the designer's draft preview. QA run #23166 met the 422 and the draft-preview panel. The react half is unchanged", "ref": "#23169" }, { "revision": 3, "date": "2026-10-16", "change": "steps[7](a) names how to reach the draft preview (checklist-accuracy finding 8 of QA run #23942, wave #23878). The compile panel is reachable only for a page staged in a writable package that an app's navigation references, opened at /_console/studio/PKG/interfaces?surface=page:NAME; an env-wide draft has no Studio surface, which is what #23942 met. The clause is unchanged", "ref": "#24036" }, - { "revision": 4, "date": "2026-10-25", "change": "the react-pages tier defaults OFF (ADR-0081 amendment): fixtures.requires names the showcase's committed .env opt-in OS_PAGE_REACT_ENABLED=true, and the both-sides gate's off-state is a boot with OS_PAGE_REACT_ENABLED=false (steps[6], acceptance[4], negative[1], source rows). The clauses are unchanged", "ref": "claude/project-thread-80dotk" } + { "revision": 4, "date": "2026-10-25", "change": "the react-pages tier defaults OFF (ADR-0081 amendment): fixtures.requires names the showcase's committed .env opt-in OS_PAGE_REACT_ENABLED=true, and the both-sides gate's off-state is a boot with OS_PAGE_REACT_ENABLED=false (steps[6], acceptance[4], negative[1], source rows). The clauses are unchanged", "ref": "claude/project-thread-80dotk" }, + { "revision": 5, "date": "2026-10-28", "change": "knownGaps/source re-pointed from ui/e2e/live/showcase-smoke.spec.ts, deleted by the triage of the live specs that ran in no CI lane (ruling Q3 B) as a duplicate: every surface it swept and each of its four assertions are in examples/app-showcase/e2e/showcase-smoke.spec.ts, which Nightly Tiers runs, with a wider surface list and stricter waits. That sweep includes the three html/react pages, which the old note said it did not; it pins render-clean only, so the item stays browser-driven. No clause changed", "ref": "#25914" } ] }, { diff --git a/ui/e2e/console-rendering.spec.ts b/ui/e2e/console-rendering.spec.ts index a810c601add..dc63bb9ef38 100644 --- a/ui/e2e/console-rendering.spec.ts +++ b/ui/e2e/console-rendering.spec.ts @@ -18,14 +18,11 @@ test.describe('Console Rendering', () => { if (msg.type() === 'error') { const text = msg.text(); // Ignore benign errors (e.g. favicon 404, service-worker registration, - // MSW mock messages, and network errors for optional resources) + // and network errors for optional resources). The Console ships no + // mock service worker, so no MSW message is excused here. if ( text.includes('favicon') || text.includes('service-worker') || - text.includes('mockServiceWorker') || - text.includes('MSW') || - text.includes('Mock Service Worker') || - text.includes('[MSW]') || text.includes('Failed to load resource') || text.includes('net::ERR_') || text.includes('Failed to connect to ObjectStack server')