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
8 changes: 6 additions & 2 deletions tests/e2e/fixtures/data-variants/metrics.json
Original file line number Diff line number Diff line change
@@ -1,7 +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'.",
"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.",
"set": {
"generatedAt": null
"generatedAt": null,
"referenceArchitectureLifecycle.cards": null,
"referenceArchitectureLifecycle.omitted": null,
"series": null,
"breakdowns": null
},
"append": {
"referenceArchitectureLifecycle.trends.submissions.values": [
Expand Down
165 changes: 165 additions & 0 deletions tests/e2e/metrics-empty-collections-variant.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// End-to-end coverage for the four 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
// document-level collections through a fallback that supplies an empty one:
//
// line 65 `(lifecycle?.cards || []).map(...)` — the lifecycle cards;
// 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.
//
// 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
// 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.
//
// 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
// 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
// coordinates, and the two builds' generated code does not always map back to
// the same ones. Where it does, the regions fold (the variant's cleared
// `generatedAt` already folds ReferenceArchitectures' two arms); where it does
// not, the union keeps the zero. That is the region-attribution problem
// tracked in #1066 and #1079, not something this fixture can reach.
import { test, expect } from '../tools/e2e-coverage.cjs';
import { loadSiteData } from '../tools/e2e-data-fixtures.cjs';

const METRICS_PATH = '/metrics';
const VARIANT_BASE = '/e2e-coverage-variant';
const LIFECYCLE = 'section[aria-labelledby="lifecycle-title"]';
const CARD = 'div[class*="lifecycleCard"]';
const DISCLOSURE_SUMMARY = 'What is not yet measurable';

// The variant site is only built by the coverage run; outside it the base path
// does not exist and the ordinary page still carries every collection.
const describeCoverage =
process.env.E2E_COVERAGE === '1' ? test.describe : test.describe.skip;

const COVERAGE_ENV = { E2E_COVERAGE: '1' };
const VARIANT_ENV = { E2E_COVERAGE: '1', E2E_COVERAGE_VARIANT: '1' };

// Read both documents through the overlay rather than hard-coding labels or
// counts, so an edited overlay fails here instead of leaving a test that
// asserts nothing.
const coverageData = loadSiteData('metrics.json', COVERAGE_ENV);
const variantData = loadSiteData('metrics.json', VARIANT_ENV);

const chartSection = (page, id) =>
page.locator(`section[aria-labelledby="${id}-title"]`);

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
// collections are non-empty, so no fallback is reachable here.
const lifecycle = coverageData.referenceArchitectureLifecycle;
expect(lifecycle.cards.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);

await page.goto(METRICS_PATH);

const section = page.locator(LIFECYCLE);
await expect(section).toBeVisible();
await expect(section.locator(CARD)).toHaveCount(lifecycle.cards.length);
for (const card of lifecycle.cards) {
await expect(section).toContainText(card.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
// list from the empty fallback, which is all this file is about.
const disclosure = page.locator(`${LIFECYCLE} details`).first();
await expect(disclosure.getByText(DISCLOSURE_SUMMARY)).toBeVisible();
await expect(disclosure.locator('p')).toHaveCount(lifecycle.omitted.length);

for (const id of Object.keys(coverageData.series)) {
await expect(chartSection(page, id)).toHaveCount(1);
}
for (const id of Object.keys(coverageData.breakdowns)) {
await expect(chartSection(page, id)).toHaveCount(1);
}
});

test('the variant page renders each collection as empty', async ({
page,
}) => {
const lifecycle = variantData.referenceArchitectureLifecycle;
expect(lifecycle.cards).toBeNull();
expect(lifecycle.omitted).toBeNull();
expect(variantData.series).toBeNull();
expect(variantData.breakdowns).toBeNull();

await page.goto(`${VARIANT_BASE}${METRICS_PATH}`);

// The page still renders the parts that do not depend on the cleared
// collections. Without this the rest of the test would pass just as well
// against a page that failed to render at all.
await expect(page.getByText(/Last updated/).first()).toBeVisible();
const section = page.locator(LIFECYCLE);
await expect(section).toBeVisible();
await expect(
section.getByRole('heading', {
name: 'Reference architecture lifecycle',
}),
).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);
}

// Counted rather than matched on text: the card labels are prefixes of the
// trend labels this overlay keeps ("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);

// The <details> element itself is static markup outside the mapped
// fallback, so it survives; what the fallback removes is every item in it.
const disclosure = page.locator(`${LIFECYCLE} details`).first();
await expect(disclosure.getByText(DISCLOSURE_SUMMARY)).toBeVisible();
await expect(disclosure.locator('p')).toHaveCount(0);

for (const id of Object.keys(coverageData.series)) {
await expect(chartSection(page, id)).toHaveCount(0);
}
for (const id of Object.keys(coverageData.breakdowns)) {
await expect(chartSection(page, id)).toHaveCount(0);
}
});
});
Loading