Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions tests/e2e-data-fixtures.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,73 @@ test('an overlay appends to an existing array', () => {
]);
});

// `add` is the one operation whose leaf must be absent rather than present:
// it exists so an overlay can introduce a record under a keyed collection,
// which `set`, `append` and `setWhere` all refuse to do.
test('an overlay adds a key the real data does not carry', () => {
const patched = applyOverlay(
{ lifecycle: { trends: { real: { values: [] } } } },
overlay({ add: { 'lifecycle.trends.fixture': { values: [1] } } }),
'fixture',
);
assert.deepEqual(patched.lifecycle.trends, {
real: { values: [] },
fixture: { values: [1] },
});
});

// The fail-loud guarantee, pointing the other way from `set`'s: a regenerated
// data file that grows the same key must break the build rather than have the
// record it grew silently replaced by the fixture.
test('adding a key the real data already carries is rejected', () => {
assert.throws(
() =>
applyOverlay(
{ lifecycle: { trends: { fixture: { values: [] } } } },
overlay({ add: { 'lifecycle.trends.fixture': { values: [1] } } }),
'fixture',
),
/cannot add "lifecycle\.trends\.fixture"; the real data already carries it/,
);
});

test('adding through a missing parent is rejected', () => {
assert.throws(
() =>
applyOverlay(
{ lifecycle: {} },
overlay({ add: { 'lifecycle.trends.fixture': {} } }),
'fixture',
),
/"lifecycle\.trends" is missing from the real data/,
);
});

test('an added value is cloned rather than shared with the overlay', () => {
const value = { values: [] };
const patched = applyOverlay(
{ trends: {} },
overlay({ add: { 'trends.fixture': value } }),
'fixture',
);
value.values.push('mutated after the fact');
assert.deepEqual(patched.trends.fixture, { values: [] });
});

// `set` runs before `add`, and `add` before `append`, so one overlay can add
// an array and then push onto it.
test('an overlay can append to the array it added', () => {
const patched = applyOverlay(
{ trends: {} },
overlay({
add: { 'trends.fixture': { values: [] } },
append: { 'trends.fixture.values': [{ date: '2026-01' }] },
}),
'fixture',
);
assert.deepEqual(patched.trends.fixture.values, [{ date: '2026-01' }]);
});

test('the real document is not mutated', () => {
const data = { groups: [{ slug: 'real' }] };
applyOverlay(data, overlay({ append: { groups: [{ slug: 'added' }] } }), 'f');
Expand Down Expand Up @@ -434,6 +501,25 @@ test('every committed overlay still applies to its data file', () => {
(data) => data.some((entry) => entry.industries.length > 0),
'no catalog entry carries an industry, so the populated arm stopped rendering',
],
[
'metrics.json',
(data) =>
Object.values(data.referenceArchitectureLifecycle.trends).filter(
(trend) => trend.values.length === 1,
).length === 1,
'the ordinary coverage build must carry exactly one single-point trend, or Sparkline\u2019s centring arm has nothing to render',
],
[
'metrics.json',
(data) =>
Object.values(data.referenceArchitectureLifecycle.trends).some(
(trend) => trend.values.length === 0,
) &&
Object.values(data.referenceArchitectureLifecycle.trends).some(
(trend) => trend.values.length > 1,
),
'the added trend displaced a real one instead of rendering beside it, so the arms it was added next to stopped rendering',
],
];
for (const [name, holds, why] of cases) {
const data = loadSiteData(name, { E2E_COVERAGE: '1' });
Expand Down
11 changes: 2 additions & 9 deletions tests/e2e/fixtures/data-variants/metrics.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
{
"description": "Clears the document-level generatedAt so ReferenceArchitectures renders the ': null' arm of its syncDate ternary (src/components/ReferenceArchitectures/index.js line 23) and the '' arm beside it at line 33. Four other components read generatedAt, so clearing it moves five pages at once -- which is exactly why it belongs to the variant build rather than to the ordinary coverage build the real pages are measured from. The appended point gives the 'submissions' trend exactly one value, so MetricsDashboard's Sparkline takes the 'values.length === 1 ? 50' arm of its x-coordinate ternary (src/components/MetricsDashboard/index.js line 28). That arm has no reachable shape in the checked-in data: 'submissions' carries zero values and renders 'No trend data yet' instead, 'publications' carries five and takes the arm beside it, and those are the only two trends. Appending the point in the ordinary coverage build would not add a case -- it would trade the empty-values arm for this one, since 'submissions' is the only trend that reaches it. The variant build keeps both: the real /metrics page still renders 'No trend data yet'. The same overlay clears four collections MetricsDashboard reads through an empty fallback: referenceArchitectureLifecycle.cards and referenceArchitectureLifecycle.omitted reach the `|| []` arms at src/components/MetricsDashboard/index.js lines 65 and 85, and series and breakdowns reach the `|| {}` arms at lines 124 and 174. All four are document-level collections on the one file the /metrics page reads, so emptying them in the ordinary coverage build would trade every rendered card, disclosure item, line chart and bar chart for the fallback rather than add a case; they belong to the variant build beside the real one. They are ordinary shapes rather than defensive dead code -- metrics.json is regenerated by `npm run collect:metrics`, and a collector run that cannot reach a source emits the document without that collection. referenceArchitectureLifecycle.trends is deliberately left alone: the append below pushes onto trends.submissions.values, `set` is applied before `append`, and clearing trends would make that append fail the build. The `|| {}` arm at line 76 therefore stays out of reach of this overlay.",
"description": "Clears the document-level generatedAt so ReferenceArchitectures renders the ': null' arm of its syncDate ternary (src/components/ReferenceArchitectures/index.js line 23) and the '' arm beside it at line 33. Four other components read generatedAt, so clearing it moves five pages at once -- which is exactly why it belongs to the variant build rather than to the ordinary coverage build the real pages are measured from. The same overlay clears the five collections MetricsDashboard reads through an empty fallback: referenceArchitectureLifecycle.cards and referenceArchitectureLifecycle.omitted reach the `|| []` arms at src/components/MetricsDashboard/index.js lines 65 and 85, referenceArchitectureLifecycle.trends reaches the `|| {}` arm at line 76, and series and breakdowns reach the `|| {}` arms at lines 124 and 174. All five are document-level collections on the one file the /metrics page reads, so emptying them in the ordinary coverage build would trade every rendered card, trend, disclosure item, line chart and bar chart for the fallback rather than add a case; they belong to the variant build beside the real one. They are ordinary shapes rather than defensive dead code -- metrics.json is regenerated by `npm run collect:metrics`, and a collector run that cannot reach a source emits the document without that collection. trends is cleared here rather than left alone because the single-point sparkline arm it used to conflict with now lives in tests/e2e/fixtures/data/metrics.json, which `add`s a trend beside the real two in the ordinary build instead of appending a point onto the only trend that also reaches the no-values arm.",
"set": {
"generatedAt": null,
"referenceArchitectureLifecycle.cards": null,
"referenceArchitectureLifecycle.omitted": null,
"referenceArchitectureLifecycle.trends": null,
"series": null,
"breakdowns": null
},
"append": {
"referenceArchitectureLifecycle.trends.submissions.values": [
{
"date": "2026-01",
"value": 1
}
]
}
}
14 changes: 14 additions & 0 deletions tests/e2e/fixtures/data/metrics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"description": "Adds one lifecycle trend carrying exactly one value so Sparkline takes the `values.length === 1 ? 50` arm of its x-coordinate ternary (src/components/MetricsDashboard/index.js line 28), which centres a lone point because the expression beside it divides by `values.length - 1`. The checked-in data defines exactly two trends: `submissions` carries zero values and returns \"No trend data yet\" before the ternary is reached, and `publications` carries five and takes the arm beside it, so no shipped shape reaches it. A trend holding one point is an ordinary shape -- `submissions` becomes one the first month a single submission is recorded. This is `add` rather than `append` onto `trends.submissions.values` on purpose: `submissions` is the only trend that reaches the no-values arm, so giving it a point would trade that arm for this one rather than add a case, which is why this arm previously needed the variant build. Adding a trend beside it is additive in the sense tests/e2e/data-fixtures.spec.js means -- both real trends keep rendering exactly as they did -- so all three Sparkline arms render on the one real /metrics page in one build, and the variant overlay is freed to clear `referenceArchitectureLifecycle.trends` outright for the `|| {}` fallback at line 76. The key must be absent from the real data or the build fails, so a collector run that grows a trend of this name breaks here instead of having it silently overwritten.",
"add": {
"referenceArchitectureLifecycle.trends.coverageFixture": {
"label": "Coverage fixture trend with a single point",
"values": [
{
"date": "2026-01",
"value": 1
}
]
}
}
}
66 changes: 40 additions & 26 deletions tests/e2e/metrics-empty-collections-variant.spec.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
// End-to-end coverage for the four empty-collection fallbacks in
// End-to-end coverage for the five empty-collection fallbacks in
// src/components/MetricsDashboard/index.js, which need a build of their own.
//
// The component reads data/metrics.json at module scope and maps over four
// The component reads data/metrics.json at module scope and maps over five
// document-level collections through a fallback that supplies an empty one:
//
// line 65 `(lifecycle?.cards || []).map(...)` — the lifecycle cards;
// line 76 `Object.entries(lifecycle?.trends || {})` — the trend grid;
// line 85 `(lifecycle?.omitted || []).map(...)` — the "what is not yet
// measurable" disclosure items;
// line 124 `Object.entries(metricsData.series || {})` — the line charts;
// line 174 `Object.entries(metricsData.breakdowns || {})` — the bar charts.
//
// None of the four fallbacks is reachable against the data the site ships: the
// checked-in document always carries four cards, three omitted indicators, two
// series and two breakdowns. They are ordinary shapes rather than defensive
// dead code — metrics.json is regenerated by `npm run collect:metrics`, and a
// collector run that cannot reach one of its sources emits the document
// without that collection.
// None of the five fallbacks is reachable against the data the site ships: the
// checked-in document always carries four cards, two trends, three omitted
// indicators, two series and two breakdowns. They are ordinary shapes rather
// than defensive dead code — metrics.json is regenerated by `npm run
// collect:metrics`, and a collector run that cannot reach one of its sources
// emits the document without that collection.
//
// This is not a case tests/e2e/data-fixtures.spec.js can take. That file
// covers the branches an *additive* overlay reaches, where an extra record
// renders beside the real ones and nothing already covered stops rendering.
// These four are the opposite: emptying the collections in the ordinary
// coverage build would trade every rendered card, disclosure item, line chart
// and bar chart for the fallback, so the arms beside them would go uncovered
// instead. They belong to the variant build for the same reason
// These five are the opposite: emptying the collections in the ordinary
// coverage build would trade every rendered card, trend, disclosure item, line
// chart and bar chart for the fallback, so the arms beside them would go
// uncovered instead. They belong to the variant build for the same reason
// AwardsTimeline's provenance paragraph does — see the preamble of
// tests/e2e/data-variants.spec.js for the mechanism. `npm run
// build:e2e:coverage` compiles a second site under /e2e-coverage-variant/ with
// tests/e2e/fixtures/data-variants/** layered on, one `docusaurus serve`
// offers both, and the report unions what each build reached.
//
// The fifth collection, `referenceArchitectureLifecycle.trends` at line 76, is
// deliberately not cleared. The same overlay appends a point to
// `trends.submissions.values` for tests/e2e/metrics-sparkline-variant.spec.js,
// `set` is applied before `append` in tests/tools/e2e-data-fixtures.cjs, and
// clearing trends would make that append fail the build. Reaching line 76
// needs a second variant build, not a second entry in this overlay.
// `referenceArchitectureLifecycle.trends` at line 76 used to be the one
// collection this overlay could not clear. The same overlay appended a point
// to `trends.submissions.values` for the single-point sparkline arm, `set` is
// applied before `append` in tests/tools/e2e-data-fixtures.cjs, and clearing
// trends would have made that append fail the build. That conflict is gone:
// the single-point trend is now `add`ed beside the real two in the ordinary
// build (tests/e2e/fixtures/data/metrics.json, asserted by
// tests/e2e/metrics-sparkline.spec.js), so nothing in the variant build needs
// trends to survive and line 76 is cleared here with the rest.
//
// The real route is asserted alongside the variant one. On its own, an
// assertion that the variant page renders no charts passes just as well when
// the page failed to build or the route is wrong; pairing it with the real
// route is what makes the pair evidence that the arms *switched* rather than
// that one page happens to look a certain way.
//
// What this does NOT promise: that the four regions leave the e2e report's
// What this does NOT promise: that the five regions leave the e2e report's
// uncovered list. The browser demonstrably takes the fallbacks — the
// assertions below only pass if it did — but
// tests/tools/e2e-coverage-report.mjs keys a region on its exact original
Expand Down Expand Up @@ -80,10 +84,11 @@ const chartSection = (page, id) =>

describeCoverage('a metrics document missing its collections', () => {
test('the real page has every collection to render', async ({ page }) => {
// The premise of the whole file: against the data the site ships, all four
// The premise of the whole file: against the data the site ships, all five
// collections are non-empty, so no fallback is reachable here.
const lifecycle = coverageData.referenceArchitectureLifecycle;
expect(lifecycle.cards.length).toBeGreaterThan(0);
expect(Object.keys(lifecycle.trends).length).toBeGreaterThan(0);
expect(lifecycle.omitted.length).toBeGreaterThan(0);
expect(Object.keys(coverageData.series).length).toBeGreaterThan(0);
expect(Object.keys(coverageData.breakdowns).length).toBeGreaterThan(0);
Expand All @@ -97,6 +102,10 @@ describeCoverage('a metrics document missing its collections', () => {
await expect(section).toContainText(card.label);
}

for (const trend of Object.values(lifecycle.trends)) {
await expect(section).toContainText(trend.label);
}

// The items are counted rather than matched on text: the disclosure is
// collapsed by default, and tests/e2e/metrics-dashboard.spec.js already
// owns the open/closed behaviour. A count still distinguishes the mapped
Expand All @@ -118,6 +127,7 @@ describeCoverage('a metrics document missing its collections', () => {
}) => {
const lifecycle = variantData.referenceArchitectureLifecycle;
expect(lifecycle.cards).toBeNull();
expect(lifecycle.trends).toBeNull();
expect(lifecycle.omitted).toBeNull();
expect(variantData.series).toBeNull();
expect(variantData.breakdowns).toBeNull();
Expand All @@ -136,15 +146,19 @@ describeCoverage('a metrics document missing its collections', () => {
}),
).toBeVisible();

// `trends` is the one lifecycle collection this overlay leaves alone, so
// the section keeps rendering sparklines while the cards beside them are
// gone — the empty arms are the cleared collections, not the whole panel.
for (const trend of Object.values(lifecycle.trends)) {
await expect(section).toContainText(trend.label);
// `trends` is cleared too, so the grid renders nothing: no sparkline, no
// "No trend data yet" placeholder either, since that arm lives inside
// Sparkline and Sparkline is never reached. The labels are checked against
// the *real* document because the variant one no longer carries them.
const realLifecycle = coverageData.referenceArchitectureLifecycle;
for (const trend of Object.values(realLifecycle.trends)) {
await expect(section).not.toContainText(trend.label);
}
await expect(section.getByRole('img')).toHaveCount(0);
await expect(section.getByText('No trend data yet')).toHaveCount(0);

// Counted rather than matched on text: the card labels are prefixes of the
// trend labels this overlay keeps ("Open submissions" against "Open
// trend labels the real document carries ("Open submissions" against "Open
// submissions by month"), so a per-label absence assertion would be
// satisfied by the wrong element.
await expect(section.locator(CARD)).toHaveCount(0);
Expand Down
Loading
Loading