diff --git a/tests/e2e-coverage-describe-gate.test.mjs b/tests/e2e-coverage-describe-gate.test.mjs index 9774f11e..ea06ab18 100644 --- a/tests/e2e-coverage-describe-gate.test.mjs +++ b/tests/e2e-coverage-describe-gate.test.mjs @@ -54,6 +54,7 @@ const COVERAGE_GATED = [ 'data-fixtures.spec.js', 'data-variants.spec.js', 'focus-trap-empty-dialog.spec.js', + 'group-link-status-absent-groups.spec.js', 'group-link-status-variant.spec.js', 'member-directory-freshness-all-absent.spec.js', 'member-directory-freshness-unlinked.spec.js', diff --git a/tests/e2e-data-fixture-validity.test.mjs b/tests/e2e-data-fixture-validity.test.mjs index 4b2ddda5..1f9dec98 100644 --- a/tests/e2e-data-fixture-validity.test.mjs +++ b/tests/e2e-data-fixture-validity.test.mjs @@ -138,6 +138,13 @@ const EXPECTED = new Map([ adds: [], }, ], + [ + 'data-no-revision/community-groups.json', + { + why: "clears the groups collection to reach GroupLinkStatus's `groupsData?.groups || []` fallback; the gate requires a non-empty collection, which is exactly the document an interrupted link-check run leaves behind", + adds: ['community-groups.json: groups must be a non-empty array'], + }, + ], [ 'data-no-revision/members.json', { diff --git a/tests/e2e/fixtures/data-no-revision/community-groups.json b/tests/e2e/fixtures/data-no-revision/community-groups.json new file mode 100644 index 00000000..6dd82d7a --- /dev/null +++ b/tests/e2e/fixtures/data-no-revision/community-groups.json @@ -0,0 +1,6 @@ +{ + "description": "Clears the groups collection so GroupLinkStatus takes the `groupsData?.groups || []` fallback in src/components/GroupLinkStatus/index.js instead of filtering a real array. That arm is the component's guard against a partial data/community-groups.json -- `npm run check:community-group-links` writes the file in one pass, so an interrupted or failed run can leave the document without its collection -- and it is the only arm of the component no build reaches: the ordinary coverage build and tests/e2e/fixtures/data-variant/community-groups.json both carry groups, and a cleared collection cannot go in the variant build, which appends a second drifted group to reach the plural stale.length arms. It is set to null rather than [] because an empty array is truthy and would take the left operand, leaving the fallback unreached. It rides this build because nothing the no-revision build already covers reads community-groups.json, so it costs no additional Docusaurus compile (see the preamble of tests/tools/e2e-data-fixtures.cjs).", + "set": { + "groups": null + } +} diff --git a/tests/e2e/group-link-status-absent-groups.spec.js b/tests/e2e/group-link-status-absent-groups.spec.js new file mode 100644 index 00000000..a9c1fdb5 --- /dev/null +++ b/tests/e2e/group-link-status-absent-groups.spec.js @@ -0,0 +1,92 @@ +// End-to-end coverage for the absent-collection fallback in +// src/components/GroupLinkStatus/index.js: +// +// const stale = (groupsData?.groups || []).filter(...) +// +// The `|| []` operand is the component's guard against a +// data/community-groups.json that carries no collection at all. +// `npm run check:community-group-links` rewrites the document in one pass, so +// a run that is interrupted, or that fails after truncating the file, can +// leave the Community page a document without `groups`. The guard is what +// makes that render the page without the drift warning rather than throw on +// `undefined.filter` and take the whole route down. +// +// Why this is the one arm of the component no existing build reaches. +// tests/e2e/group-link-status-variant.spec.js covers the plural drift arms +// from the variant build, which *appends* a second drifted group, and the +// ordinary coverage build appends one; both therefore carry `groups`, and the +// left operand wins in each. Clearing the collection cannot go in either: +// the variant build needs two drifted groups to render the plural grammar, +// and the ordinary build is where the singular arm and the drift warning are +// measured from. The shape rides tests/e2e/fixtures/data-no-revision/ because +// nothing that build already covers reads community-groups.json, so it costs +// no additional Docusaurus compile (see the preamble of +// tests/tools/e2e-data-fixtures.cjs). +// +// The collection is cleared to null rather than to an empty array because an +// empty array is truthy: `[] || []` takes the left operand and leaves the +// fallback unreached. +// +// The real route is asserted beside the no-revision one. An assertion that a +// page carries no drift warning passes just as well when the route 404s or +// the build lost the page, so the pair is what makes it evidence that the +// fallback supplied the empty collection rather than that nothing rendered. +import { test, expect } from '../tools/e2e-coverage.cjs'; +import { loadSiteData } from '../tools/e2e-data-fixtures.cjs'; + +const COMMUNITY_PATH = '/community'; +const NO_REVISION_BASE = '/e2e-coverage-no-revision'; + +const COVERAGE_ENV = { E2E_COVERAGE: '1' }; +const NO_REVISION_ENV = { + E2E_COVERAGE: '1', + E2E_COVERAGE_BUILD: 'no-revision', +}; + +// Read through the overlays rather than hard-coded, so an edited fixture fails +// here instead of leaving a test that asserts nothing. +const groups = loadSiteData('community-groups.json', COVERAGE_ENV); +const absentGroups = loadSiteData('community-groups.json', NO_REVISION_ENV); + +const isStale = (group) => group.archived || group.reachable === false; +const UPDATED_LINE = /Upstream group status last updated on/; +const DRIFT_WARNING = /archived or unreachable upstream/; + +// The no-revision site is only built by the coverage run; outside it the base +// path does not exist. +const describeCoverage = + process.env.E2E_COVERAGE === '1' ? test.describe : test.describe.skip; + +describeCoverage('a community-groups document carrying no collection', () => { + test('the drift warning is dropped rather than throwing on the missing collection', async ({ + page, + }) => { + // The shapes the two pages are built from. Stated as assertions rather + // than used to skip: an overlay that stopped clearing the collection + // would leave the fallback unexercised, which is the silent retirement + // this pair exists to catch. + expect(Array.isArray(groups.groups)).toBe(true); + expect(groups.groups.filter(isStale).length).toBeGreaterThan(0); + expect(absentGroups.groups).toBe(null); + // The last-updated line is the evidence that the component rendered at + // all on the no-revision page, so it has to survive the cleared + // collection: generatedAt is left alone by this overlay. + expect(absentGroups.generatedAt).toEqual(groups.generatedAt); + + await page.goto(COMMUNITY_PATH); + await expect(page.getByText(DRIFT_WARNING)).toBeVisible(); + + await page.goto(`${NO_REVISION_BASE}${COMMUNITY_PATH}`); + // The route built and served: the page's own heading is prose around the + // component, so it renders whether or not the corpus has anything to say. + await expect( + page.getByRole('heading', { name: 'Community', level: 1 }), + ).toBeVisible(); + // The component itself still rendered -- this is an empty collection, not + // a page that lost the component or a build that lost the route. + await expect(page.getByText(UPDATED_LINE)).toBeVisible(); + // ...and with nothing to report it says nothing, rather than reporting an + // empty list of drifted groups. + await expect(page.getByText(DRIFT_WARNING)).toHaveCount(0); + }); +});