From 48ab3b1ffc611a4119ee5334bdbeb26cb5295b14 Mon Sep 17 00:00:00 2001 From: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:22:03 -0400 Subject: [PATCH] test: cover the DirectoryFreshness all-absent guard end to end Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- tests/e2e-coverage-describe-gate.test.mjs | 3 +- tests/e2e-data-fixture-validity.test.mjs | 25 ++++ .../data-unparseable-dates/awards.json | 6 + .../data-unparseable-dates/members.json | 6 + .../data-unparseable-dates/metrics.json | 6 + .../helpers/member-directory-assertions.js | 44 ++++++ ...ber-directory-freshness-all-absent.spec.js | 133 ++++++++++++++++++ ...ember-directory-freshness-unlinked.spec.js | 8 +- ...member-directory-freshness-variant.spec.js | 8 +- 9 files changed, 226 insertions(+), 13 deletions(-) create mode 100644 tests/e2e/fixtures/data-unparseable-dates/awards.json create mode 100644 tests/e2e/fixtures/data-unparseable-dates/members.json create mode 100644 tests/e2e/fixtures/data-unparseable-dates/metrics.json create mode 100644 tests/e2e/helpers/member-directory-assertions.js create mode 100644 tests/e2e/member-directory-freshness-all-absent.spec.js diff --git a/tests/e2e-coverage-describe-gate.test.mjs b/tests/e2e-coverage-describe-gate.test.mjs index a800b9a4..8d70a246 100644 --- a/tests/e2e-coverage-describe-gate.test.mjs +++ b/tests/e2e-coverage-describe-gate.test.mjs @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; import { isCoverageEnabled } from './tools/e2e-coverage.cjs'; -// Twelve end-to-end specs hold cases that only the coverage build can satisfy +// Thirteen end-to-end specs hold cases that only the coverage build can satisfy // -- they navigate a route that exists only under E2E_COVERAGE=1, or they // assert against the fixture overlay that only that build layers in. Each one // gates itself by hand: @@ -54,6 +54,7 @@ const COVERAGE_GATED = [ 'data-fixtures.spec.js', 'data-variants.spec.js', 'group-link-status-variant.spec.js', + 'member-directory-freshness-all-absent.spec.js', 'member-directory-freshness-unlinked.spec.js', 'member-directory-freshness-variant.spec.js', 'metrics-empty-collections-variant.spec.js', diff --git a/tests/e2e-data-fixture-validity.test.mjs b/tests/e2e-data-fixture-validity.test.mjs index 4e11598d..4b2ddda5 100644 --- a/tests/e2e-data-fixture-validity.test.mjs +++ b/tests/e2e-data-fixture-validity.test.mjs @@ -154,6 +154,31 @@ const EXPECTED = new Map([ adds: ['metrics.json: source revisions are required'], }, ], + [ + 'data-unparseable-dates/awards.json', + { + why: "makes verifiedAt unparseable so DirectoryFreshness's awardsDate is null, one of the three dates its all-absent guard needs missing together; the gate requires a parseable verifiedAt", + adds: [ + 'awards.json: verifiedAt must be a parseable date recording the last completeness audit against verifiedAgainst', + ], + }, + ], + [ + 'data-unparseable-dates/members.json', + { + why: "makes sources.landscape.collectedAt unparseable so DirectoryFreshness's landscapeDate is null; validate-members.mjs holds that field to the pinned landscape snapshot", + adds: [ + 'members.json.sources.landscape: landscape provenance must match enduser-landscape.json', + ], + }, + ], + [ + 'data-unparseable-dates/metrics.json', + { + why: "makes generatedAt unparseable so DirectoryFreshness's architecturesDate is null; the gate requires an ISO 8601 generatedAt", + adds: ['metrics.json: generatedAt must be ISO 8601'], + }, + ], [ 'data-variant/awards.json', { diff --git a/tests/e2e/fixtures/data-unparseable-dates/awards.json b/tests/e2e/fixtures/data-unparseable-dates/awards.json new file mode 100644 index 00000000..81968f57 --- /dev/null +++ b/tests/e2e/fixtures/data-unparseable-dates/awards.json @@ -0,0 +1,6 @@ +{ + "description": "Makes the document-level verifiedAt unparseable so DirectoryFreshness's awardsDate is null, the last of the three dates the all-absent guard in src/components/MemberDirectory/DirectoryFreshness.js requires to be absent together. tests/e2e/fixtures/data-variant/awards.json sets this field to null, which formatDate (src/components/shared/formatDate.js) renders as January 1, 1970 and leaves truthy, so the award sentence survives there and the guard is never reached.", + "set": { + "verifiedAt": "not a timestamp" + } +} diff --git a/tests/e2e/fixtures/data-unparseable-dates/members.json b/tests/e2e/fixtures/data-unparseable-dates/members.json new file mode 100644 index 00000000..8b3d2d5f --- /dev/null +++ b/tests/e2e/fixtures/data-unparseable-dates/members.json @@ -0,0 +1,6 @@ +{ + "description": "Makes sources.landscape.collectedAt unparseable so DirectoryFreshness's landscapeDate is null. It is one of the three dates that must all be absent at once before the all-absent guard in src/components/MemberDirectory/DirectoryFreshness.js -- `if (!landscapeDate && !architecturesDate && !awardsDate) return null;` -- returns null and the provenance note is dropped entirely. The value is an unparseable string rather than null because formatDate (src/components/shared/formatDate.js) runs `new Date(isoDate)`: new Date(null) is the Unix epoch, not an Invalid Date, so a null here formats as January 1, 1970 and stays truthy, which is exactly why the data-variant build cannot reach this guard.", + "set": { + "sources.landscape.collectedAt": "not a timestamp" + } +} diff --git a/tests/e2e/fixtures/data-unparseable-dates/metrics.json b/tests/e2e/fixtures/data-unparseable-dates/metrics.json new file mode 100644 index 00000000..e20d9950 --- /dev/null +++ b/tests/e2e/fixtures/data-unparseable-dates/metrics.json @@ -0,0 +1,6 @@ +{ + "description": "Makes the document-level generatedAt unparseable so DirectoryFreshness's architecturesDate is null, the second of the three dates the all-absent guard in src/components/MemberDirectory/DirectoryFreshness.js requires to be absent together. tests/e2e/fixtures/data-variant/metrics.json sets this field to null instead, which formatDate (src/components/shared/formatDate.js) renders as January 1, 1970 rather than dropping -- the variant build's own spec asserts that it stays truthy -- so the variant build renders the architectures sentence and never reaches the guard.", + "set": { + "generatedAt": "not a timestamp" + } +} diff --git a/tests/e2e/helpers/member-directory-assertions.js b/tests/e2e/helpers/member-directory-assertions.js new file mode 100644 index 00000000..e43751e5 --- /dev/null +++ b/tests/e2e/helpers/member-directory-assertions.js @@ -0,0 +1,44 @@ +// The assertion every member-directory freshness spec makes about the page +// *around* the provenance note. +// +// Each of those specs proves something the note does not do on a fixture +// build -- a sentence dropped, a link rendered as text, the note absent +// altogether -- and each of those is an assertion that something is missing. +// An assertion that a page lacks a sentence passes just as well when the +// route 404s, the build lost the page, or the selector stopped matching, so +// each spec pairs it with evidence that the directory itself still rendered. +// +// That evidence was the same twelve lines in all three specs, which is the +// copy `npm run check:duplication` exists to stop: a third copy pushed the +// repository over its clone ceiling. It lives here instead -- a plain module +// in a subdirectory of tests/e2e/, which keeps it out of three listings at +// once. Playwright's default testMatch collects only *.spec.js, so it is not +// run as a test; `registers every file in testDir as a spec` in +// tests/playwright-config.test.mjs lists files directly in tests/e2e and so +// does not demand it be renamed; and the unit coverage file-set in +// tests/tools/coverage-report.mjs enumerates scripts/, src/, tests/tools/ and +// tests/helpers*.mjs, none of which this is, so it carries no coverage floor +// that no unit test could ever satisfy. +// +// `expect` is taken as an argument rather than imported so this module stays +// out of the fixture graph in tests/tools/e2e-coverage.cjs: the specs already +// import the coverage-aware `test`/`expect` pair from there, and a second +// import path for `expect` is how a spec ends up asserting through a +// different instance than the one its fixtures were built from. + +/** The accessible name of the member directory region on /community/members. */ +export const DIRECTORY_REGION = 'End User Community organization directory'; + +/** + * Asserts the member directory rendered its cards on the page as loaded. + * + * @param {import('@playwright/test').Page} page the page under test + * @param {import('@playwright/test').Expect} expect the spec's `expect` + * @returns {Promise} + */ +export async function expectDirectoryIntact(page, expect) { + const section = page.getByRole('region', { name: DIRECTORY_REGION }); + await expect( + section.getByRole('button', { name: /^Open .+ profile$/ }).first(), + ).toBeVisible(); +} diff --git a/tests/e2e/member-directory-freshness-all-absent.spec.js b/tests/e2e/member-directory-freshness-all-absent.spec.js new file mode 100644 index 00000000..612b3ec6 --- /dev/null +++ b/tests/e2e/member-directory-freshness-all-absent.spec.js @@ -0,0 +1,133 @@ +// End-to-end coverage for the member directory provenance note being dropped +// altogether, when none of the three dates it is built from parses. +// +// DirectoryFreshness opens with an all-absent guard +// (src/components/MemberDirectory/DirectoryFreshness.js): +// +// if (!landscapeDate && !architecturesDate && !awardsDate) return null; +// +// Every other build renders the note, so that `return null` is the one arm of +// the component no browser took. The "End-to-end coverage" job recorded it as +// an uncovered region in all three compiled builds while the unit suite scored +// the file at 100% regions -- the gap this build and this spec close. +// +// Why this needs a build of its own. The arm needs all three dates to be +// absent at the same time, and that shape conflicts with every existing build: +// +// * the ordinary coverage build is where the whole note is measured -- +// tests/e2e/member-directory-freshness.spec.js asserts both source +// anchors -- so clearing the dates there would trade that coverage for +// this arm rather than add a case; +// * tests/e2e/fixtures/data-variant/ sets metrics.generatedAt and +// awards.verifiedAt to null, and formatDate renders null as the Unix +// epoch rather than dropping it, so two of the three dates stay truthy +// there. tests/e2e/member-directory-freshness-variant.spec.js asserts +// exactly that, and asserts the sentences they produce, so the shape +// cannot be changed underneath it; +// * tests/e2e/fixtures/data-no-revision/ needs both timestamps parseable, +// because the plain-text `cncf/landscape` and `cncf/architecture` arms it +// exists for are only reached while their sentences render. +// +// That is the bar tests/tools/e2e-data-fixtures.cjs sets for a new directory: +// a shape that provably conflicts with every existing one. +// +// The three overlays use unparseable strings rather than null for the reason +// the variant build demonstrates: `new Date(null)` is the epoch, not an +// Invalid Date, so a null leaves the sentence rendering with an absurd date +// instead of reaching the guard. +// +// The real route is asserted beside this one. On its own, an assertion that a +// page has no note passes just as well when the page failed to build or the +// selector is wrong; the pair is what makes it evidence that the guard fired. +import { test, expect } from '../tools/e2e-coverage.cjs'; +import { loadSiteData } from '../tools/e2e-data-fixtures.cjs'; +import { expectDirectoryIntact } from './helpers/member-directory-assertions.js'; + +const MEMBERS_PATH = '/community/members'; +const ABSENT_BASE = '/e2e-coverage-unparseable-dates'; + +const COVERAGE_ENV = { E2E_COVERAGE: '1' }; +const ABSENT_ENV = { + E2E_COVERAGE: '1', + E2E_COVERAGE_BUILD: 'unparseable-dates', +}; + +// Read through the overlays rather than hard-coded, so an edited fixture fails +// here instead of leaving a test that asserts nothing. Each document is loaded +// with the env of the build whose page it is asserted against. +const members = loadSiteData('members.json', COVERAGE_ENV); +const metrics = loadSiteData('metrics.json', COVERAGE_ENV); +const awards = loadSiteData('awards.json', COVERAGE_ENV); +const absentMembers = loadSiteData('members.json', ABSENT_ENV); +const absentMetrics = loadSiteData('metrics.json', ABSENT_ENV); +const absentAwards = loadSiteData('awards.json', ABSENT_ENV); + +// formatDate()'s own contract: `null` is not unparseable. `new Date(null)` is +// the Unix epoch, so Number.isNaN(getTime()) is false and the date formats +// rather than dropping (src/components/shared/formatDate.js). This is the +// distinction the whole build rests on, so it is asserted rather than assumed. +const formatsToADate = (value) => !Number.isNaN(new Date(value).getTime()); + +const MEMBERSHIP_SENTENCE = /Directory membership data last synced from/; +const ARCHITECTURES_SENTENCE = /Architecture-derived profiles last synced from/; +const AWARDS_SENTENCE = /Award data last verified on/; + +const note = (page) => page.locator('p[class*="freshnessNote"]'); + +// The unparseable-dates 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('member directory freshness with no parseable date', () => { + test('the whole provenance note is dropped rather than rendering empty sentences', async ({ + page, + }) => { + // The shapes the two pages are built from. Stated as assertions rather + // than used to skip: an overlay that stopped clearing a date would leave + // the guard unexercised, which is the silent retirement this pair exists + // to catch. + expect(formatsToADate(members?.sources?.landscape?.collectedAt)).toBe(true); + expect(formatsToADate(metrics?.generatedAt)).toBe(true); + expect(formatsToADate(awards?.verifiedAt)).toBe(true); + // All three must be unparseable together, or the guard's remaining + // operands short-circuit and the note still renders. + expect(formatsToADate(absentMembers?.sources?.landscape?.collectedAt)).toBe( + false, + ); + expect(formatsToADate(absentMetrics?.generatedAt)).toBe(false); + expect(formatsToADate(absentAwards?.verifiedAt)).toBe(false); + + await page.goto(MEMBERS_PATH); + await expect(note(page)).toHaveCount(1); + await expect(note(page)).toContainText(MEMBERSHIP_SENTENCE); + await expect(note(page)).toContainText(ARCHITECTURES_SENTENCE); + await expect(note(page)).toContainText(AWARDS_SENTENCE); + + await page.goto(`${ABSENT_BASE}${MEMBERS_PATH}`); + // The component returned null: not one sentence missing, the element + // itself absent. + await expect(note(page)).toHaveCount(0); + // "Invalid Date" is what formatDate() exists to prevent, and dropping the + // note is what DirectoryFreshness does once formatDate has dropped every + // date. Neither the string nor a stranded sentence may survive. + await expect(page.locator('body')).not.toContainText('Invalid Date'); + for (const sentence of [ + MEMBERSHIP_SENTENCE, + ARCHITECTURES_SENTENCE, + AWARDS_SENTENCE, + ]) { + await expect(page.locator('body')).not.toContainText(sentence); + } + }); + + test('the directory itself is unaffected by the dropped note', async ({ + page, + }) => { + await page.goto(`${ABSENT_BASE}${MEMBERS_PATH}`); + + // A member card list proves the page is intact, so the assertions above + // are about the note and not about a build that lost the route. + await expectDirectoryIntact(page, expect); + }); +}); diff --git a/tests/e2e/member-directory-freshness-unlinked.spec.js b/tests/e2e/member-directory-freshness-unlinked.spec.js index b5061e0b..af797fe6 100644 --- a/tests/e2e/member-directory-freshness-unlinked.spec.js +++ b/tests/e2e/member-directory-freshness-unlinked.spec.js @@ -57,6 +57,7 @@ // switched arms. import { test, expect } from '../tools/e2e-coverage.cjs'; import { loadSiteData } from '../tools/e2e-data-fixtures.cjs'; +import { expectDirectoryIntact } from './helpers/member-directory-assertions.js'; const MEMBERS_PATH = '/community/members'; const NO_REVISION_BASE = '/e2e-coverage-no-revision'; @@ -138,11 +139,6 @@ describeCoverage('member directory freshness without source URLs', () => { // A member card list proves the page is intact, so the assertions above // are about the note and not about a build that lost the route. - const section = page.getByRole('region', { - name: 'End User Community organization directory', - }); - await expect( - section.getByRole('button', { name: /^Open .+ profile$/ }).first(), - ).toBeVisible(); + await expectDirectoryIntact(page, expect); }); }); diff --git a/tests/e2e/member-directory-freshness-variant.spec.js b/tests/e2e/member-directory-freshness-variant.spec.js index 094321e5..29358940 100644 --- a/tests/e2e/member-directory-freshness-variant.spec.js +++ b/tests/e2e/member-directory-freshness-variant.spec.js @@ -32,6 +32,7 @@ // broken; the pair is what makes it evidence that the arm switched. import { test, expect } from '../tools/e2e-coverage.cjs'; import { loadSiteData } from '../tools/e2e-data-fixtures.cjs'; +import { expectDirectoryIntact } from './helpers/member-directory-assertions.js'; const MEMBERS_PATH = '/community/members'; const VARIANT_BASE = '/e2e-coverage-variant'; @@ -131,12 +132,7 @@ describeCoverage( // A member card list proves the page is intact, so the assertions above // are about the note and not about a build that lost the route. - const section = page.getByRole('region', { - name: 'End User Community organization directory', - }); - await expect( - section.getByRole('button', { name: /^Open .+ profile$/ }).first(), - ).toBeVisible(); + await expectDirectoryIntact(page, expect); }); }, );