diff --git a/BUILD-NOTES.md b/BUILD-NOTES.md index 8cb3c8e..0b517cd 100644 --- a/BUILD-NOTES.md +++ b/BUILD-NOTES.md @@ -653,3 +653,107 @@ source/thumbnail hashes and first-five live preservation. The integration remain retain the original design approval provenance and distinguish fresh final-source checks from reused unchanged-source evidence. No runtime, schema, billing, sandbox, launch, infrastructure or dependency change is included. + +## Accepted decision-oriented templates — Batch02 integration (Astra) + +This separate four-template adoption is based on the deployed PR41 merge +`1b48644469a8a392263482cd31ceb8db8ac7ab2a`. The first eight redesigned HTML sources +and their thumbnails remain byte-for-byte unchanged. Seven additional accepted +technical/editorial designs are still queued; this PR does not finish the section. + +- **Meeting recap:** six owned action rows lead beside the complete decision minutes, + followed by the explicit deferral, open questions and meeting handover. +- **Decision brief:** a compact recommendation above the complete five-criterion + option bench, then alternatives, explicit asks and the deadline. +- **Spec:** scope pairing and ruled native requirement records, keeping every ID, + priority, acceptance clause and blocking question relationship. +- **Research Brief:** the answer qualified by confidence and method, an evidence and + provenance register, grouped sources, answer-changing conditions and unknowns. + +Catalog descriptions reflect these compositions. The unchanged canonical thumbnail +pipeline uses 1600px source viewports for Meeting recap and Decision brief (all six +owned actions / all five comparison criteria visible), and 1440px for Spec and +Research Brief (scope/requirements and answer/confidence/evidence visible). The +actual gallery cards were reviewed at 1440px and 390px. No global capture hack, +manual PNG edit, other thumbnail change, slug/category/slot or API contract change. +The catalog stays at nineteen HTML examples plus the Markdown One-pager. + +### Exact accepted-source mapping + +| Template | Accepted SHA-256 | Canonical SHA-256 | +| --- | --- | --- | +| meeting-recap | `e8ff4f5203c169c60bd559eea0c8998bd4a629a5904e58c3d4915d2a4d1c8b28` | `be10b99a09fa0e589619bf5f2b07f3027722e0e9f8ad5c5e9e01c50416fee495` | +| decision-brief | `18b8f4f8bf9eddc649e17de08bd932257f5da7163654a69cc1e3eded1b26eda6` | `77da9c704f717c821792d30f2276742f277d3a13f57977a43fa1040e6cc9d68f` | +| spec | `1cc6c161b5e158ea6692f5b4219fa1f9e1077127e6272a2953a2a2df8e601451` | same, byte-for-byte | +| research-brief | `2ba837d4421fffb84c551813e3462e7bc67c25c041e139f02677ec4175316945` | same, byte-for-byte | + +The only source differences are one approved `noRedundantRoles`-only compatibility +comment at the beginning of Meeting recap and Decision brief. No CSS, body, title, +tag, ID, scope, role, headers reference, text, data, code, link or SVG change. Both +already have native scoped headers; there is no cell-tag correction in this batch. +The accepted previews and the earlier public evidence remain immutable. + +These two responsive tables retain explicit table roles because their mobile CSS +uses block/grid display and visually clips column headings. [MDN's table display +accessibility warning](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/display#tables) +provides the compatibility rationale. This adds only these two named files to the +local exception list; it is not permission for other files, rules or global config. + +| Explicit role | Meeting recap | Decision brief | +| --- | ---: | ---: | +| table | 1 | 1 | +| rowgroup | 2 | 2 | +| row | 7 | 6 | +| columnheader | 4 | 4 | +| rowheader | 0 | 5 | +| cell | 24 | 15 | + +The existing scoped unit test now bounds all four approved exception inventories. +Its two new cases fail against the accepted pre-comment files and pass with the +exact approved candidates. Existing browser tests are extended, not duplicated: +the new cases traverse the real app sandbox and verify native `headers` IDs, scope, +correct table/row/column membership, accessible names and phone overflow. Disposable +DOM red controls must reject dangling IDs and existing-but-wrong row/column headers, +then recover when original references are restored. No production data is involved. + +Eight accepted/candidate pairs at 1440/768/390/320 have equal PNG bytes and ARIA +snapshots; their candidate hashes equal the final canonical hashes. Eight real +local app sandbox measurements record table hierarchy and header associations with +actual child-frame widths. Evidence is Chromium-only: not a Safari/Firefox or +screen-reader navigation certification. Case-insensitive accessible-name comparison +accounts only for visual CSS uppercase versus authored ARIA snapshot case; IDs, +scopes, relationships and original names remain recorded without normalization. + +### Validation, provenance and warning disposition + +Independent content parity retains all 298 text nodes, 159 numeric occurrences, +70 keyed groups and the link/code/ordinal relationships. Final source identity is +proved by the exact allowed transform, not by screenshots alone. The earlier +16 main plus 24 actual/prior breakpoint captures, 58 detailed semantic width checks, +12 fixtures, no-JS/growth checks and actual gallery images remain valid evidence of +unchanged rendered source, explicitly labelled as reused where appropriate. Final +checks, build, complete unit/browser suites and final-head CI are reported separately. + +Accepted-source lint: **71 errors → 0** through only the two approved comments. +Warnings: **31 → 37**, with all 31 baseline warnings unchanged. Meeting recap adds +five specificity diagnostics (`caption b`, two `.open-section`, two `.opens`) and +one mobile `.num` spacing `!important` warning. The caption and facts selectors +address different elements; scoped meeting-grid rules intentionally outrank generic +section/list fallbacks. The `!important` priority appears unnecessary in the current +stylesheet, but no rendering/accessibility defect was observed. Accepted CSS is +preserved rather than rewritten merely to quiet diagnostics; no warnings suppressed. + +[Final integrated review and source-bound evidence](https://anacreon.ai/downloads/aa-template-revamp-1788686289312/integrated-batch02/final/index.html) +links to the immutable earlier approval/candidate evidence and separates fresh tests +from unchanged-source reuse. This PR is **not merged or deployed** pending internal +integrated review. No runtime, dependencies, schema, billing, sandbox, launch, +credentials or infrastructure change is included. + +A separate **test-fixture-only** correction was necessary for reliable full-suite +validation: the existing returning-reader analytics test used real time plus one +hour while promising the same UTC day. It fails on unchanged main after 23:00 UTC, +when the production privacy salt correctly rotates. This one test now fixes +`Date.now()` at noon using Vitest's automatically restored spy. Its assertions, +tomorrow/rotation coverage and all production analytics code remain unchanged. +The unchanged-base failure and corrected fourteen-test pass are recorded in the +review; no runtime clock, customer data, analytics behavior or financial change. diff --git a/public/assets/template-thumbs/decision-brief.png b/public/assets/template-thumbs/decision-brief.png index b93ffbc..fa492db 100644 Binary files a/public/assets/template-thumbs/decision-brief.png and b/public/assets/template-thumbs/decision-brief.png differ diff --git a/public/assets/template-thumbs/meeting-recap.png b/public/assets/template-thumbs/meeting-recap.png index 8f85dfd..081b850 100644 Binary files a/public/assets/template-thumbs/meeting-recap.png and b/public/assets/template-thumbs/meeting-recap.png differ diff --git a/public/assets/template-thumbs/research-brief.png b/public/assets/template-thumbs/research-brief.png index 842d803..b4332de 100644 Binary files a/public/assets/template-thumbs/research-brief.png and b/public/assets/template-thumbs/research-brief.png differ diff --git a/public/assets/template-thumbs/spec.png b/public/assets/template-thumbs/spec.png index 2a30099..886f594 100644 Binary files a/public/assets/template-thumbs/spec.png and b/public/assets/template-thumbs/spec.png differ diff --git a/templates/decision-brief.html b/templates/decision-brief.html index 6cb97e2..e70ea48 100644 --- a/templates/decision-brief.html +++ b/templates/decision-brief.html @@ -1,132 +1,19 @@ + - - + + Decision brief — how we bill for overage from January - -
+
Decision brief @@ -147,56 +34,56 @@ tbody tr:first-child th,tbody tr:first-child td{border-top:0}
-
-

The options, on the same criteria

+
- - - - - - - +
CriterionA · Hard cap with upgrade promptB · Metered overageRecommendedC · Soft cap with 7-day grace
+ + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + +

The options, on the same criteria

+
CriterionA · Hard cap with upgrade promptB · Metered overageRecommendedC · Soft cap with 7-day grace
Modelled revenue, FY27+$180k+$310k+$240k
Modelled revenue, FY27+$180k+$310k+$240k
Support load+40 tickets / month+12 tickets / month+25 tickets / month
Support load+40 tickets / month+12 tickets / month+25 tickets / month
Time to ship3 weeks5 weeks4 weeks
Time to ship3 weeks5 weeks4 weeks
Churn riskHighStops work mid-taskLowNothing is interruptedMediumThe grace period ends
Churn riskHighStops work mid-taskLowNothing is interruptedMediumThe grace period ends
Time to revert1 week2 weeks3 weeks
Time to revert1 week2 weeks3 weeks
-

Revenue is modelled on the June–August usage distribution and carries ±18% at the 80% interval. Support estimates scale from the 2025 seat-cap launch, our closest comparable.

+

Revenue is modelled on the June–August usage distribution and carries ±18% at the 80% interval. Support estimates scale from the 2025 seat-cap launch, our closest comparable.

-
+

Why not the others

  • @@ -210,7 +97,7 @@ tbody tr:first-child th,tbody tr:first-child td{border-top:0}
-
+

What we are asking for

    @@ -223,6 +110,6 @@ tbody tr:first-child th,tbody tr:first-child td{border-top:0}
Prepared by Ines Arriaga, Pricing. Modelling reviewed by Sam Oyelaran, Data. Sources: usage export 1 June – 31 August 2026; 2025 seat-cap retrospective.
- + diff --git a/templates/manifest.ts b/templates/manifest.ts index 3f62c61..2bec6a3 100644 --- a/templates/manifest.ts +++ b/templates/manifest.ts @@ -24,21 +24,23 @@ export const starterTemplateManifest = [ "slug": "meeting-recap", "category": "meetings", "name": "Meeting recap", - "description": "What was decided, who owes what by when, and what is still open. A numbered decision ledger over an owned action table with per-item status.", + "description": "A working meeting handover: six owned actions lead beside complete decision minutes, with compact outcome counts, explicit deferrals and open questions that keep their owners and deadlines.", "type": "html", "content_file": "meeting-recap.html", "thumbnail": "/assets/template-thumbs/meeting-recap.png", - "slots": [] + "slots": [], + "thumbnail_viewport": 1600 }, { "slug": "decision-brief", "category": "decisions", "name": "Decision brief", - "description": "Two to four options scored on identical criteria, a recommendation up front, an honest account of why the alternatives lose, and the ask with a dated deadline.", + "description": "An evidence-first option bench: a compact recommendation above the full five-criterion comparison, the selected option marked in words, then rejected alternatives, explicit asks and the decision deadline.", "type": "html", "content_file": "decision-brief.html", "thumbnail": "/assets/template-thumbs/decision-brief.png", - "slots": [] + "slots": [], + "thumbnail_viewport": 1600 }, { "slug": "one-pager", @@ -81,11 +83,12 @@ export const starterTemplateManifest = [ "slug": "spec", "category": "decisions", "name": "Spec", - "description": "A PRD that opens on scope itself — what the release does, beside what it deliberately does not — then numbered requirements each carrying a priority and an acceptance line, and the open questions still blocking the build.", + "description": "A technical requirements folio: in-and-out scope, ruled ID and priority records pairing each requirement with its acceptance clause, then blocking questions with their owners and timings.", "type": "html", "content_file": "spec.html", "thumbnail": "/assets/template-thumbs/spec.png", - "slots": [] + "slots": [], + "thumbnail_viewport": 1440 }, { "slug": "case-study", @@ -123,11 +126,12 @@ export const starterTemplateManifest = [ "slug": "research-brief", "category": "research", "name": "Research Brief", - "description": "A question answered up front, then the evidence that earns it: numbered findings each carrying its own citation and a primary/secondary tag, a confidence rating with its reasoning, a source list, and an honest section on what is still unknown.", + "description": "An evidence register: the question and answer qualified immediately by confidence and method, five claims beside their provenance, a grouped source register, and the conditions and unknowns that could change the answer.", "type": "html", "content_file": "research-brief.html", "thumbnail": "/assets/template-thumbs/research-brief.png", - "slots": [] + "slots": [], + "thumbnail_viewport": 1440 }, { "slug": "metrics-dashboard", diff --git a/templates/meeting-recap.html b/templates/meeting-recap.html index 5ad3a62..94d719d 100644 --- a/templates/meeting-recap.html +++ b/templates/meeting-recap.html @@ -1,178 +1,19 @@ + - - + + Q4 platform priorities — meeting recap - -
+
Meeting recap @@ -193,7 +34,62 @@ td.due{white-space:nowrap;font-variant-numeric:tabular-nums}
  • 1deferred to 11 Sep
  • -
    +
    +

    Who owes what

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Action items — 6 assigned, 2 closed. Dates are end of day.
    OwnerActionDueStatus
    Priya RamanWrite the tracing rollout plan: one request id across four log systems.11 SepIn progress
    Marcus FeldPost the go/no-go once the error budget number is in.8 SepNot started
    Dana WhitfieldSend Northwind the SAML timeline and the assertions it covers.5 SepDone
    Tomas LindqvistWrite down the conditions under which scoped SAML gets replaced.18 SepNot started
    Nadia OseiAdd ingest ownership to the 11 September agenda.5 SepDone
    Sofia KaurCost the read-replica work for a Q1 re-proposal.25 SepNot started
    +
    +
    + +

    What we decided

    1. @@ -224,62 +120,7 @@ td.due{white-space:nowrap;font-variant-numeric:tabular-nums}

      Deferred. Who owns ingest was raised twice and settled neither time. Moved to the architecture review on 11 September.

    -
    -

    Who owes what

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Action items — 6 assigned, 2 closed. Dates are end of day.
    OwnerActionDueStatus
    Priya RamanWrite the tracing rollout plan: one request id across four log systems.11 SepIn progress
    Marcus FeldPost the go/no-go once the error budget number is in.8 SepNot started
    Dana WhitfieldSend Northwind the SAML timeline and the assertions it covers.5 SepDone
    Tomas LindqvistWrite down the conditions under which scoped SAML gets replaced.18 SepNot started
    Nadia OseiAdd ingest ownership to the 11 September agenda.5 SepDone
    Sofia KaurCost the read-replica work for a Q1 re-proposal.25 SepNot started
    -
    -
    - -
    +

    Still open

    • @@ -293,12 +134,12 @@ td.due{white-space:nowrap;font-variant-numeric:tabular-nums}
    -
    +

    PresentNadia Osei (chair), Priya Raman, Marcus Feld, Dana Whitfield, Tomas Lindqvist, Sofia Kaur, Ben Achterberg

    ApologiesGrace Lim — written comments on decision 03, read into the record

    NextArchitecture review, 11 September, 15:00 UTC

    -
    + diff --git a/templates/research-brief.html b/templates/research-brief.html index 386fe0b..eba4fae 100644 --- a/templates/research-brief.html +++ b/templates/research-brief.html @@ -1,166 +1,20 @@ - - + + Do teams keep paying for AI meeting notes after month three? - -
    +
    -
    +
    Research brief

    Do teams keep paying for AI meeting notes after month three?

    @@ -181,96 +35,91 @@ ul,ol{margin:0;padding:0;list-style:none} predicting renewal once it clears roughly 90%.

    -
    -
    +

    Evidence

    1. 01
      -

      Month-six retention clusters in the low twenties

      +

      Month-six retention clusters in the low twenties

      Across six vendors that disclose cohort curves, median month-six retention is 24%; best 34%, worst 18%. Month three is where the curve bends, not month one.

      - SecondaryBracken Research, State of Meeting Intelligence 2026, June 2026 · n=6 +
      SecondaryBracken Research, State of Meeting Intelligence 2026, June 2026 · n=6 vendor cohorts · bracken-research.com/smi-2026
    2. 02
      -

      A connected destination is the single strongest correlate

      +

      A connected destination is the single strongest correlate

      Workspaces that connected a CRM, tracker or wiki within fourteen days retained at 71% by month six, against 22% for those that never did — a gap that holds after controlling for seat count and plan.

      - PrimaryInternal cohort analysis, +
      PrimaryInternal cohort analysis, 4,182 workspaces activated Jan–Mar 2026 · queried 11 Sep 2026
    3. 03
      -

      Summary quality is table stakes, not a differentiator

      +

      Summary quality is table stakes, not a differentiator

      Buyers rank accuracy in the top three reasons to buy and outside the top five reasons to renew. Every product in the set was "good enough" on transcription.

      - PrimaryBuyer interviews, 22 +
      PrimaryBuyer interviews, 22 accounts, 20–120 seats, Jul–Aug 2026 · transcripts on file
    4. 04
      -

      Churn happens at the renewal date, not during the trial

      +

      Churn happens at the renewal date, not during the trial

      Trial-to-paid is stable at 38%, but 81% of lost revenue leaves at the first annual renewal — which is why quarterly dashboards read this market as healthier than it is.

      - PrimaryInternal billing extract, +
      PrimaryInternal billing extract, FY2025–26 · plus two vendor disclosures reporting the same shape
    5. 05
      -

      Price is not the stated reason — dormant seats are

      +

      Price is not the stated reason — dormant seats are

      Three of twenty-two churned accounts named price. Fourteen named seats they paid for and never used — the same complaint under another name.

      - PrimaryExit interviews, 22 accounts +
      PrimaryExit interviews, 22 accounts · coded independently by two analysts, agreement 0.84
    - - -
    -
    +

    Sources · 9

      -
    1. 01
      Bracken Research, State of Meeting Intelligence 2026Jun 2026 · secondary
    2. -
    3. 02
      Internal cohort analysis, 4,182 workspacesSep 2026 · primary
    4. -
    5. 03
      Buyer interviews, 22 accounts, 20–120 seatsJul–Aug 2026 · primary
    6. -
    7. 04–05
      Internal billing extract FY2025–26; exit interviews, 22 accounts, - double-codedAug–Sep 2026 · primary
    8. -
    9. 06–09
      Four vendor investor updates and pricing pages, read 8–10 Sep2026 · secondary
    10. +
    11. 01
      Bracken Research, State of Meeting Intelligence 2026Jun 2026 · secondary
    12. +
    13. 02
      Internal cohort analysis, 4,182 workspacesSep 2026 · primary
    14. +
    15. 03
      Buyer interviews, 22 accounts, 20–120 seatsJul–Aug 2026 · primary
    16. +
    17. 04–05
      Internal billing extract FY2025–26; exit interviews, 22 accounts, + double-codedAug–Sep 2026 · primary
    18. +
    19. 06–09
      Four vendor investor updates and pricing pages, read 8–10 Sep2026 · secondary
    -
    -

    Method

    -

    Desk review of published cohort curves, then two internal datasets and twenty-two - interviews against our own base. Retention is measured on revenue, not logos.

    -
    - - - -
    +

    What would change this answer

      @@ -297,6 +146,6 @@ too wide to quote. were read 8–10 September; internal figures were queried on 11 September and will drift. Transcripts and the cohort query are on file with Research. -
    + diff --git a/templates/spec.html b/templates/spec.html index 5761209..bbe9cd4 100644 --- a/templates/spec.html +++ b/templates/spec.html @@ -1,111 +1,18 @@ - - + + Spec — saved views for the ticket queue (1.9) - -
    +
    Product spec · 1.9 @@ -147,66 +54,66 @@ h2.sec+*{margin-top:.75rem}

    Requirements — 6, three of them must-have

    1. - R-01 -

      An agent can save the current filter state as a named view.

      +
      R-01 Must +

      An agent can save the current filter state as a named view.

      Accepts when — a view reproduces an identical result set on reload, sort included.

    2. - R-02 -

      Views list by last used: five shown, the rest behind "All views".

      +
      R-02 Must +

      Views list by last used: five shown, the rest behind "All views".

      Accepts when — an agent with twelve views sees their five most recent without opening a menu.

    3. - R-03 -

      A view can be shared with a team and is read-only to everyone but its owner.

      +
      R-03 Must +

      A view can be shared with a team and is read-only to everyone but its owner.

      Accepts when — a non-owner can duplicate a shared view and edit the copy, but not save over it.

    4. - R-04 -

      Every view has a stable URL that anyone with queue access can open.

      +
      R-04 Should +

      Every view has a stable URL that anyone with queue access can open.

      Accepts when — the URL in a fresh session lands on the same queue, or a clear "no access" state.

    5. - R-05 -

      Deleting a shared view warns with the number of people who used it recently.

      +
      R-05 Should +

      Deleting a shared view warns with the number of people who used it recently.

      Accepts when — the confirmation names the 30-day count and requires a second action.

    6. - R-06 -

      Up to three views can be pinned to the sidebar.

      +
      R-06 Could +

      Up to three views can be pinned to the sidebar.

      Accepts when — pinning a fourth asks which pin to replace rather than failing silently.

    -
    +

    Open questions — 3, all blocking the build start

    • -

      What happens to a shared view when its owner leaves?

      +

      What happens to a shared view when its owner leaves?

      Transfer to the team, or archive. Archiving is safer and will annoy people; transfer needs an owner-election rule we do not have.

      -

      Needs: Mira Halvorsen with Legal — before the build starts, not before beta.

      +

      Needs: Mira Halvorsen with Legal — before the build starts, not before beta.

    • -

      Do saved views count against the 200-request filter rate limit?

      +

      Do saved views count against the 200-request filter rate limit?

      Five pinned views refreshing on queue open is five requests where there is one. At 400 agents that is a different traffic shape.

      -

      Needs: Platform — by 12 September.

      +

      Needs: Platform — by 12 September.

    • -

      Does "ordered by last used" leak activity between teammates?

      +

      Does "ordered by last used" leak activity between teammates?

      A view's position tells you when a colleague last worked the queue. Fine internally, probably; not obviously fine for regulated accounts.

      -

      Needs: Security review — before beta.

      +

      Needs: Security review — before beta.

    Draft 3, updated 4 September 2026 by Mira Halvorsen. The 90-second baseline comes from queue instrumentation added in 1.7, over 4,100 sessions in August. Comments close 11 September; anything unresolved becomes an open question above, not a silent assumption.
    - + diff --git a/tests/e2e/template-table-semantics.spec.ts b/tests/e2e/template-table-semantics.spec.ts index 1acd302..e518da4 100644 --- a/tests/e2e/template-table-semantics.spec.ts +++ b/tests/e2e/template-table-semantics.spec.ts @@ -1,6 +1,7 @@ -import { expect, test } from '@playwright/test'; +import { expect, type Locator, test } from '@playwright/test'; const sandboxOrigin = process.env.E2E_CLOUD_SANDBOX_ORIGIN; +const cloudOrigin = process.env.E2E_CLOUD_BASE_URL; for (const [slug, columns, expectedRows] of [ ['metrics-dashboard', ['Channel', 'Sessions', 'Signups', 'Conv.', 'vs W33'], 6], @@ -54,3 +55,94 @@ for (const [slug, columns, expectedRows] of [ ); }); } + +// ID references must resolve to native scoped headers in this table and the correct row/column. +// Seeing a rowheader role somewhere in an AX snapshot is not sufficient evidence of association. +const inspectHeaderReferences = (table: Locator, expectedReferences: number) => + table.evaluate((element, expected) => { + if (!(element instanceof HTMLTableElement)) throw new Error('Expected native table'); + return [...element.querySelectorAll('tbody td')].map((cell) => { + if (!(cell instanceof HTMLTableCellElement)) throw new Error('Expected native cell'); + const ids = cell.headers.trim().split(/\s+/); + const scopes = ids.map((id) => document.getElementById(id)?.getAttribute('scope')); + const valid = + ids.length === expected && + new Set(ids).size === ids.length && + scopes.filter((scope) => scope === 'col').length === 1 && + scopes.filter((scope) => scope === 'row').length === expected - 1 && + ids.every((id) => { + const header = document.getElementById(id); + if (!(header instanceof HTMLTableCellElement) || header.tagName !== 'TH') return false; + if (header.closest('table') !== element) return false; + if (header.scope === 'row') return header.parentElement === cell.parentElement; + return ( + header.scope === 'col' && + header.closest('thead') === element.tHead && + header.cellIndex === cell.cellIndex + ); + }); + return { ids, valid }; + }); + }, expectedReferences); + +for (const [slug, expectedRows, expectedCells, expectedReferences] of [ + ['meeting-recap', 6, 24, 1], + ['decision-brief', 5, 15, 2], +] as const) { + test(`${slug}: native header IDs and AX names survive the real responsive sandbox`, async ({ + page, + }) => { + await page.goto(`${cloudOrigin}/templates/${slug}`); + const iframe = page.locator('iframe'); + await expect(iframe).toHaveAttribute('sandbox', 'allow-scripts'); + await expect(iframe).toHaveAttribute('src', `${sandboxOrigin}/templates/${slug}/frame`); + const frame = page.frameLocator('iframe'); + const table = frame.getByRole('table'); + await expect(table).toHaveCount(1); + await expect(table.getByRole('row')).toHaveCount(expectedRows + 1); + await expect(table.getByRole('columnheader')).toHaveCount(4); + await expect(table.getByRole('rowheader')).toHaveCount(slug === 'decision-brief' ? 5 : 0); + await expect(table.getByRole('cell')).toHaveCount(expectedCells); + for (const header of await table.locator('th').all()) { + const id = await header.getAttribute('id'); + expect(id).toBeTruthy(); + // CSS uppercasing differs from the ARIA snapshot's authored case; words must still agree. + const name = (await header.innerText()).trim().replace(/\s+/g, ' '); + await expect(header).toHaveAccessibleName( + new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') + ); + } + const associations = await inspectHeaderReferences(table, expectedReferences); + expect(associations).toHaveLength(expectedCells); + expect(associations.every((association) => association.valid)).toBe(true); + + // Red controls modify only this disposable browser DOM, never the source or customer data. + // The same assertion must detect both dangling IDs and existing-but-wrong column/row IDs. + const firstCell = table.locator('tbody td').first(); + const original = await firstCell.getAttribute('headers'); + if (!original) throw new Error('Missing native header references'); + await firstCell.evaluate((cell) => cell.setAttribute('headers', 'missing-header')); + expect((await inspectHeaderReferences(table, expectedReferences)).every((a) => a.valid)).toBe( + false + ); + await firstCell.evaluate((cell, ids) => cell.setAttribute('headers', ids), original); + const wrongHeader = + slug === 'meeting-recap' + ? await table.locator('thead th').nth(1).getAttribute('id') + : await table.locator('tbody th').nth(1).getAttribute('id'); + if (!wrongHeader) throw new Error('Missing red-control header'); + const wrongIds = original.replace(/^\S+/, wrongHeader); + expect(wrongIds).not.toBe(original); + await firstCell.evaluate((cell, ids) => cell.setAttribute('headers', ids), wrongIds); + expect((await inspectHeaderReferences(table, expectedReferences)).every((a) => a.valid)).toBe( + false + ); + await firstCell.evaluate((cell, ids) => cell.setAttribute('headers', ids), original); + expect((await inspectHeaderReferences(table, expectedReferences)).every((a) => a.valid)).toBe( + true + ); + expect( + await frame.locator('html').evaluate((element) => element.scrollWidth <= innerWidth) + ).toBe(true); + }); +} diff --git a/tests/integration/viewer/view-capture.test.ts b/tests/integration/viewer/view-capture.test.ts index 18845d5..c266186 100644 --- a/tests/integration/viewer/view-capture.test.ts +++ b/tests/integration/viewer/view-capture.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { createViewerTestContext, publishSharedArtifact, @@ -258,6 +258,9 @@ describe('who the reader is, without knowing who they are', () => { }); it('counts a returning reader as another view but the same visitor', async () => { + // This scenario promises the same UTC day. Real time after 23:00 plus one hour is tomorrow, + // when the privacy salt correctly rotates; anchor the fixture without changing production time. + vi.spyOn(Date, 'now').mockReturnValue(Date.UTC(2026, 8, 6, 12)); const ctx = await createViewerTestContext({ trustProxy: 1 }); try { diff --git a/tests/unit/template-table-semantics.test.ts b/tests/unit/template-table-semantics.test.ts index bc4b0d6..61ce0a9 100644 --- a/tests/unit/template-table-semantics.test.ts +++ b/tests/unit/template-table-semantics.test.ts @@ -4,9 +4,9 @@ import { describe, expect, it } from 'vitest'; const source = (slug: string) => readFileSync(new URL(`../../templates/${slug}.html`, import.meta.url), 'utf8'); -// These two responsive tables intentionally retain explicit roles when CSS changes their display. +// These four responsive tables intentionally retain explicit roles when CSS changes their display. // Bound the exception to the known table inventory, rather than silencing future unrelated roles. -describe('responsive operational table semantics', () => { +describe('responsive template table semantics', () => { it('uses four native scoped Postmortem row headers, not interactive roles on td', () => { const html = source('postmortem'); expect( @@ -17,12 +17,19 @@ describe('responsive operational table semantics', () => { expect(html).not.toMatch(/]*role="rowheader"/); }); - for (const [slug, inventory] of [ + for (const [slug, inventory, imageRoles] of [ [ 'metrics-dashboard', { table: 1, rowgroup: 3, row: 7, columnheader: 5, rowheader: 6, cell: 24 }, + 1, + ], + ['postmortem', { table: 1, rowgroup: 2, row: 5, columnheader: 5, rowheader: 4, cell: 16 }, 1], + ['meeting-recap', { table: 1, rowgroup: 2, row: 7, columnheader: 4, cell: 24 }, 0], + [ + 'decision-brief', + { table: 1, rowgroup: 2, row: 6, columnheader: 4, rowheader: 5, cell: 15 }, + 0, ], - ['postmortem', { table: 1, rowgroup: 2, row: 5, columnheader: 5, rowheader: 4, cell: 16 }], ] as const) { it(`${slug} limits its compatibility exception to the documented role inventory`, () => { const html = source(slug); @@ -39,8 +46,8 @@ describe('responsive operational table semantics', () => { } expect(counts).toEqual(inventory); expect(html.match(/role="[^"]+"/g)).toHaveLength( - Object.values(inventory).reduce((sum, count) => sum + count, 0) + 1 - ); // One unchanged named SVG image, outside the table. + Object.values(inventory).reduce((sum, count) => sum + count, 0) + imageRoles + ); // The two operational examples also have one unchanged named SVG image. }); } });