/** * The second stamp says two words, in two places, and one of those places * cannot import the file that defines it. * * soon-stamp.ts builds the ones on the front page's facts. Base.astro's bar * writes the same span by hand, because the blog's pages never run the app. * Nothing errors when they drift: the page still renders, with two different * promises on it, or with a stamp that has lost its styling and is a bare * "Coming soon" sitting in the middle of the masthead. * * The other half of this is that the mark and the words have to stay together * with what they are marking. A fact that stops being a promise should lose * its stamp, and a destination that gets built should lose its own — but a * stamp that goes missing while the thing it marked is still unbuilt looks * exactly like a feature that shipped. * * Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; const web = join(new URL(".", import.meta.url).pathname, ".."); const read = (path) => readFile(join(web, path), "utf8"); const stamp = await read("src/soon-stamp.ts"); const layout = await read("src/layouts/Base.astro"); const home = await read("src/screens/home.ts"); const matches = await read("src/screens/matches.ts"); const pilot = await read("src/screens/pilot.ts"); const css = await read("src/styles.css"); const words = stamp.match(/SOON = "([^"]+)"/)?.[1]; assert.ok(words, "soon-stamp.ts no longer declares SOON as a literal"); test("the bar's hand-written stamp says what soon-stamp.ts says", () => { // Loose about the whitespace around the words: the span is wide enough that // the formatter breaks it across lines, and where it chooses to break is not // what this is about. assert.match( layout, new RegExp(`class="stamp stamp-soon stamp-\\w+"[^>]*>\\s*${words}\\s*<`), `Base.astro does not stamp ${words}`, ); }); test("every place that stamps has a rule to draw it with", () => { // soon-stamp.ts builds its class from the variant, so the names are in the // union rather than in any string a search would find; Base.astro's is // written out. A variant with no rule is a bare word sitting on a card. const variants = new Set([ ...[...stamp.matchAll(/"(fact|mode|tile|seat)"/g)].map((m) => m[1]), ...[...layout.matchAll(/stamp-soon stamp-(\w+)"/g)].map((m) => m[1]), ]); assert.ok(variants.size >= 5, "the variants stopped being found at all"); assert.ok(css.includes(".stamp-soon {"), "styles.css has no .stamp-soon"); for (const variant of variants) { assert.ok( new RegExp(`\\.stamp-${variant}[\\s,{]`).test(css), `styles.css has no .stamp-${variant}`, ); } }); test("the bar's stamp is in the flow, so it cannot cover a label", () => { // The one this file exists to hold. The bar's stamp used to be absolutely // positioned and rotated across its label: it covered LEADERBOARDS and // OBSERVE at every width, and where the bar wrapped the two marks came down // on ABOUT and UPDATES on the row underneath. Four illegible labels, and // nothing failed. Out of the flow is what let it land anywhere, so that is // what is checked rather than the angle it was turned to. const rule = css.slice( css.indexOf(".stamp-nav {"), css.indexOf("}", css.indexOf(".stamp-nav {")), ); assert.notEqual(rule.length, 0, ".stamp-nav lost its rule"); assert.doesNotMatch( rule, /position:\s*(absolute|fixed)/, "the bar's stamp is out of the flow again, and can land on a label", ); assert.doesNotMatch( rule, /translate\(/, "the bar's stamp is being moved off the place the flow put it", ); }); test("the bar's stamp is hidden from a screen reader, and the label is not", () => { // Two marks and one sentence: the stamp is an angle and a colour, and the // parenthesis is what the words actually reach a reader as. A stamp that // stopped being aria-hidden would have the bar read "Hangar coming soon not // ready yet". const span = layout.slice( layout.indexOf('", layout.indexOf("not ready yet")), ); assert.match( span, /class="stamp stamp-soon[^"]*"\s+aria-hidden="true"/, "the bar's stamp is announced as well as the parenthesis after it", ); assert.match(span, /class="soon-label"/, "the bar's label is not in a span"); }); // --- what the front page is still promising ------------------------------- // // Both of these are prose the site says in the present tense about something // it cannot do. They are checked by the sentence rather than by a count, so // building the thing and rewriting the fact is what removes the case, and // quietly dropping the mark off a fact that still reads this way does not. const PROMISES = ["Design your forces", "Earn commendations"]; for (const promise of PROMISES) { test(`the fact that says "${promise}" is stamped`, () => { const at = home.indexOf(promise); assert.notEqual(at, -1, `no fact on the front page says "${promise}"`); // The flag is the last argument of the fact() call, so it is somewhere // between the sentence and whatever starts the next fact — or the end of // the strip, for the last one. const after = home.slice(at); const ends = after.search(/\n {4}fact\(|\n {2}\]\);/); assert.match( ends === -1 ? after : after.slice(0, ends), /\btrue\b/, `the front page states "${promise}" as a fact of the site today`, ); }); } test("a stamped fact is marked for a screen reader too", () => { assert.match(home, /soonStamp\("fact"\)/, "the facts strip lost its stamp"); assert.match( home, /notReadyNote\(\)/, "a stamped fact is marked by the stamp alone, which is nothing to a " + "screen reader", ); }); test("the note the stamps cannot give a screen reader says one thing", () => { // Two screens call notReadyNote() and Base.astro writes the words by hand, // the same split the stamp itself has. Drift here is one part of the site // announcing something different from the rest. const words = stamp.match(/textContent: " \(([^"]+)\)"/)?.[1]; assert.ok(words, "soon-stamp.ts no longer spells the note out"); assert.ok( layout.includes(`(${words})`), `Base.astro does not say "(${words})"`, ); }); // --- the Modes on Play that cannot be started ----------------------------- // // futureModeCard is the one that stamps; modeCard, beside it, is a real button. // Building a Mode with the wrong one of the two is silent either way round: an // unbuilt Mode that looks live, or a working Mode stamped as if it were not. test("a Mode with nothing behind it is stamped, and a live one is not", () => { const future = matches.slice( matches.indexOf("function futureModeCard"), matches.indexOf("export function matchesScreen"), ); assert.match(future, /soonStamp\("mode"\)/, "futureModeCard lost its stamp"); assert.match( future, /notReadyNote\(\)/, "an unbuilt Mode is marked by the stamp alone, which is nothing to a " + "screen reader", ); const live = matches.slice( matches.indexOf("function modeCard"), matches.indexOf("function futureModeCard"), ); assert.doesNotMatch(live, /soonStamp/, "a Mode that works is stamped soon"); }); // --- the figures on the pilot card that nothing reports -------------------- test("a stat with no reader is stamped, and one with a reader is not", () => { assert.match(pilot, /soonStamp\("tile"\)/, "the unread stats lost the stamp"); // The stamp goes on inside `if (!stat.read)`. Outside it, every tile on the // card would carry one, including the two that count something real. const guarded = pilot.slice( pilot.indexOf("if (!stat.read)"), pilot.indexOf("return box;", pilot.indexOf("if (!stat.read)")), ); assert.match(guarded, /soonStamp\("tile"\)/, "the stamp is not guarded"); assert.match(guarded, /notReadyNote\(\)/, "the caption carries no note"); });