Skip to content
Closed
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
3 changes: 2 additions & 1 deletion tests/e2e-coverage-describe-gate.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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',
Expand Down
25 changes: 25 additions & 0 deletions tests/e2e-data-fixture-validity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
6 changes: 6 additions & 0 deletions tests/e2e/fixtures/data-unparseable-dates/awards.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
6 changes: 6 additions & 0 deletions tests/e2e/fixtures/data-unparseable-dates/members.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
6 changes: 6 additions & 0 deletions tests/e2e/fixtures/data-unparseable-dates/metrics.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
44 changes: 44 additions & 0 deletions tests/e2e/helpers/member-directory-assertions.js
Original file line number Diff line number Diff line change
@@ -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<void>}
*/
export async function expectDirectoryIntact(page, expect) {
const section = page.getByRole('region', { name: DIRECTORY_REGION });
await expect(
section.getByRole('button', { name: /^Open .+ profile$/ }).first(),
).toBeVisible();
}
133 changes: 133 additions & 0 deletions tests/e2e/member-directory-freshness-all-absent.spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
8 changes: 2 additions & 6 deletions tests/e2e/member-directory-freshness-unlinked.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
});
8 changes: 2 additions & 6 deletions tests/e2e/member-directory-freshness-variant.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
},
);
Loading