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/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/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') 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" },