πŸ“¦ EqualifyEverything / equalify-iris

πŸ“„ document-lang.test.ts Β· 390 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390import { test } from "node:test";
import assert from "node:assert/strict";
import { bodyLang, wrapDocument } from "../src/pipeline/assembly.ts";
import { titledAs } from "../src/util/outputNames.ts";
import { runAxe, isKnownLanguage, cldrKnowsLanguage, languageListSource } from "../src/pipeline/lint.ts";

// Issue #163: a document assembled from Korean pages was delivered as `<html lang="en">`.
//
// The defect is one the accessibility gate cannot see and the delivered file does not look
// wrong: `html-has-lang` and `html-lang-valid` both pass, because a confident wrong answer
// is well-formed. What fails is WCAG 3.1.1 β€” the attribute names the page's default human
// language, a screen reader picks its voice and pronunciation rules from it, and a Korean
// document announced as English is read out by an English voice attempting Korean text. The
// reader who most needs the attribute is the one with no way to see that it is wrong.
//
// #163 has two halves, and they are separately breakable, which is why they are separately
// tested. The page half (test/page-prompt.test.ts) asks each page to say what language it is
// in, even when it never CHANGES language. This half reads that back off the joined body,
// and is only ever as good as the fragments: nothing here guesses.
//
// The rule is agreement, not majority or first-wins. `en` is kept unless every top-level
// element in the body carries a `lang` and they all carry the same one β€” because a document
// with two languages in it has no single default to declare (that is what per-element `lang`
// is for), and a body whose pages said nothing has nothing to derive from. Failing to `en`
// costs an English-voiced reading of a document that could have been labelled; failing the
// other way β€” promoting one page's language to the root of a document that is mostly not in
// it β€” costs a wrong reading of everything else and cannot be detected downstream either.
test("the shell's language is derived from the body only where every top-level element agrees", () => {
  for (const [what, body, expected] of [
    ["one page, one language",
      `<section lang="ko"><h1>λ³΄κ³ μ„œ</h1></section>`, "ko"],
    ["several pages that agree",
      `<section lang="ko"><p>κ°€</p></section>\n\n<section lang="ko"><p>λ‚˜</p></section>`, "ko"],
    // Case matters to nobody reading the tag and the agreement is on the language, not on the
    // typing: `KO` and `ko` are the same language, and the first spelling seen is the one
    // declared rather than a normalized one, since either is valid and rewriting a page's own
    // answer buys nothing.
    ["pages that agree but were not typed the same way",
      `<div lang="ko"><p>κ°€</p></div>\n\n<div lang="KO"><p>λ‚˜</p></div>`, "ko"],
    ["a region subtag is carried through as written",
      `<section lang="zh-Hans"><p>ζŠ₯ε‘Š</p></section>`, "zh-Hans"],
    ["an unquoted attribute value, which is legal HTML and what a model sometimes writes",
      `<section lang=ko><p>κ°€</p></section>`, "ko"],
    ["single quotes, likewise",
      `<section lang='pt-BR'><p>RelatΓ³rio</p></section>`, "pt-BR"],
    // The commonest document in the system, and the case that made this list necessary: the
    // page prompt prescribes `<hr role="doc-pagebreak">` between pages, so a multi-page body
    // has a top-level element with no text in it β€” and therefore no language β€” between every
    // pair of fragments. Asking it for a `lang` would veto every multi-page document there is.
    ["the page-break separator between fragments is not asked what language it is",
      `<section lang="ko"><p>κ°€</p></section>\n` +
      `<hr role="doc-pagebreak" aria-label="Page 2" id="page-2">\n` +
      `<section lang="ko"><p>λ‚˜</p></section>`, "ko"],
    // A page whose extraction threw is in the body as a comment (see extraction.ts,
    // `@page-failed`). It bears no content, so it has no language either β€” and a document
    // that lost a page has already said so; it should not also lose its voice.
    ["a failed page's marker comment does not count against agreement",
      `<!-- @page-failed 2: model returned no html -->\n\n<section lang="ko"><p>κ°€</p></section>`, "ko"],
    ["a body of nothing but that marker has nothing to derive from",
      `<!-- @page-failed 1: model returned no html -->`, null],
    ["two languages: no single default to declare",
      `<section lang="ko"><p>κ°€</p></section>\n\n<section lang="fr"><p>Bonjour</p></section>`, null],
    // Same content in two languages on the facing pages of one document is exactly what #130
    // reported, and the per-element `lang` the prompt asks for is the right answer for it. The
    // root is not.
    ["a language and a variant of it are still two languages at the root",
      `<section lang="en-GB"><p>Colour</p></section>\n\n<section lang="en-US"><p>Color</p></section>`, null],
    ["one silent page drops the whole document, and takes its labelled siblings with it",
      `<section lang="ko"><p>κ°€</p></section>\n\n<section><p>λ‚˜</p></section>`, null],
    ["a body that says nothing anywhere",
      `<h1>Report</h1>\n\n<p>Text.</p>`, null],
    ["an empty body",
      ``, null],
    // A near-miss value is treated as no value at all, and this is the one place where being
    // strict is the safe direction: `lang` is checked by axe on the root and nowhere else, so
    // promoting an unrecognizable value there would turn a silent 3.1.1 failure into a loud
    // `html-lang-valid` one β€” a regression bought with a fix.
    ["a language spelled out in words is not a language tag",
      `<section lang="Korean"><p>κ°€</p></section>`, null],
    ["an underscore is not a subtag separator",
      `<section lang="ko_KR"><p>κ°€</p></section>`, null],
    ["a tag the canonicalizer refuses outright",
      `<section lang="ko-x"><p>κ°€</p></section>`, null],
    // #196: the country code written where the language code belongs. Well formed, in no alias
    // table, and therefore repaired by nothing β€” so it reached the root untouched and put a
    // SERIOUS `html-lang-valid` on the one element this project writes, which is the regression
    // the shape check was added to prevent. It is the commonest wrong-but-well-formed `lang` in
    // real HTML and a plausible answer to "use the BCP 47 tag" from a model reading a Chinese page.
    ["a country code is not a language code",
      `<section lang="cn"><p>ζŠ₯ε‘Š</p></section>`, null],
    ["nor is the one for Japan",
      `<section lang="jp"><p>ε ±ε‘Š</p></section>`, null],
    ["nor Czechia, Denmark, Greece, Ukraine, Vietnam or Israel",
      `<section lang="cz"><p>zprΓ‘va</p></section>`, null],
    // Shaped like a tag, in no registry: the same gap from the other end, and the reason the
    // question is now put to a list rather than to a pattern.
    ["three letters that are not a language",
      `<section lang="xxy"><p>text</p></section>`, null],
    ["nor are these",
      `<section lang="zzz"><p>text</p></section>`, null],
    // The trap this cannot close, recorded so nobody mistakes the guard for more than it is:
    // `kr`, `se` and `no` ARE languages (Kanuri, Northern Sami, Norwegian) as well as being the
    // country codes for Korea, Sweden and Norway. A Korean page that writes `lang="kr"` gets a
    // Kanuri root, and nothing here can know that: the value is a language, the linter accepts
    // it, and refusing it would refuse the derivation to every real Kanuri document. What catches
    // it is the review loop rewriting the fragment's own `lang`, or nothing.
    ["a code that is both a country code and a language is a language",
      `<section lang="kr"><p>text</p></section>`, "kr"],
    // The other direction, and the reason the list is axe's own rather than CLDR's display data:
    // the obscure end of ISO 639-3 is in the registry the linter validates against but has no
    // English name in CLDR. A proxy would refuse these, and refusing means the document is
    // announced as English β€” a small language's document losing its voice to save a lookup.
    ["a language with no English name is still a language",
      `<section lang="aaa"><p>text</p></section>`, "aaa"],
    ["ditto",
      `<section lang="ttj"><p>text</p></section>`, "ttj"],
    // Well formed, accepted by axe, and not an answer to "what language is this document in". As a
    // default human language they are the same kind of non-answer as `lang="Korean"`; a screen
    // reader given one falls back to its own default, and `en` is at least a language a voice can
    // be chosen for.
    ["undetermined is not a language",
      `<section lang="und"><p>text</p></section>`, null],
    ["no linguistic content is not a language",
      `<section lang="zxx"><p>β–  β–  β– </p></section>`, null],
    ["multiple languages is the case the unanimity rule already answers",
      `<section lang="mul"><p>κ°€ / a</p></section>`, null],
    ["nor is the private-use range",
      `<section lang="qaa"><p>text</p></section>`, null],
    ["but a real language whose tag merely starts with q",
      `<section lang="qu"><p>Runa simi</p></section>`, "qu"],
    ["an empty attribute",
      `<section lang=""><p>κ°€</p></section>`, null],
    ["whitespace is not a language",
      `<section lang="  "><p>κ°€</p></section>`, null],
    // The attribute has to be `lang` and not merely end in it. `xml:lang` is excluded for the
    // same reason: it is not what `html-has-lang` reads on an HTML document, and a page that
    // wrote only that has not labelled itself where it counts.
    ["a different attribute that happens to end in lang",
      `<section data-lang="ko"><p>κ°€</p></section>`, null],
    ["xml:lang alone is not the attribute the root needs",
      `<section xml:lang="ko"><p>κ°€</p></section>`, null],
    // Only the top level is read. A `lang` on something inside a fragment is a change of
    // language WITHIN a page β€” the case the attribute exists for β€” and says nothing about
    // what the page as a whole is in.
    ["a lang deeper in a fragment does not speak for the page",
      `<section><p lang="ko">κ°€</p><p>English text.</p></section>`, null],
    ["nor does one on a nested element of an otherwise labelled page",
      `<section lang="ko"><p>κ°€</p><blockquote lang="en">Quoted.</blockquote></section>`, "ko"],
    // A `lang` value with a `>` in it would end the tag early for a naive scan and could take
    // the rest of the element with it. It is not a language either way.
    ["a value that tries to close the tag",
      `<section lang="ko><p>hidden</p>"><p>κ°€</p></section>`, null],
    // The attributes are read one at a time rather than searched for ` lang=`, because a search
    // finds the string inside another attribute's value β€” and `title`/`alt`/`aria-label` text
    // comes from the page, so a page whose caption mentions a language tag would set the
    // document's language.
    ["a language tag inside another attribute's value is not this element's language",
      `<section title="see lang=fr note"><p>English.</p></section>`, null],
    ["nor does it override a real one",
      `<section title="the lang=fr column" lang="ko"><p>κ°€</p></section>`, "ko"],
    ["an attribute whose name merely starts with lang",
      `<section langue="ko"><p>κ°€</p></section>`, null],
    // A page that omitted an end tag is not a page that said nothing: an unclosed element
    // swallows every page after it, so the one tag a top-level scan reads for that whole run is
    // the FIRST page's β€” which is one page's answer promoted to the root of a document mostly not
    // in it. Nothing here can tell a run holding one element from a run holding five, so the
    // derivation is refused. Omitted end tags are ordinary model output (`sections.ts` handles
    // implied ends for exactly that reason) and nothing rejects an unbalanced fragment, so this
    // is reachable input rather than a hypothetical.
    ["an unclosed element that swallows the English pages after it",
      `<section lang="ko"><p>κ°€</p>\n\n<section><h1>English report</h1></section>\n<p>More English.</p>`, null],
    ["an unclosed div with English content after it",
      `<div lang="ko"><p>λ‚˜</p>\n\n<h1>Hello</h1>\n<p>English body text.</p>`, null],
    ["and the same shape with nothing after it, since the two cannot be told apart",
      `<section lang="ko"><p>κ°€</p>`, null],
    // Top-level text belongs to no element, so no `lang` covers it. The scan therefore reads a
    // segment only when it BEGINS with a start tag, which is also what refuses a stray end tag.
    ["stray prose before a labelled fragment",
      `Preamble text.\n<section lang="ko"><p>κ°€</p></section>`, null],
    ["stray prose between two labelled fragments",
      `<section lang="ko"><p>κ°€</p></section>\nContinued overleaf.\n<section lang="ko"><p>λ‚˜</p></section>`, null],
    ["stray prose after a labelled fragment",
      `<section lang="ko"><p>κ°€</p></section>\nEnd of document.`, null],
    ["a stray end tag at top level",
      `<section lang="ko"><p>κ°€</p></section>\n</div>\n<section lang="ko"><p>λ‚˜</p></section>`, null],
    // Whitespace between fragments is what `assembleBody` joins with, so it must not count as
    // text that nothing claims.
    ["the blank line assembly joins fragments with",
      `<section lang="ko"><p>κ°€</p></section>\n\n<section lang="ko"><p>λ‚˜</p></section>\n`, "ko"],
    // A tag with a preferred form is delivered IN that form. `kor` is what a page writes when
    // told to "use the BCP 47 tag", and measured against this repo's axe it fails
    // `html-lang-valid` β€” so refusing it (fall back to `en`) and promoting it as written (ship a
    // violation on the root) are both worse than answering with the tag it means.
    ["a deprecated three-letter code becomes the two-letter one it means",
      `<section lang="kor"><p>κ°€</p></section>`, "ko"],
    ["and so does a deprecated two-letter one",
      `<section lang="iw"><p>טקבט</p></section>`, "he"],
    ["a three-letter code with no two-letter equivalent is a language and is kept",
      `<section lang="haw"><p>ʻōlelo</p></section>`, "haw"],
    ["ditto Cherokee, Filipino, Cantonese",
      `<section lang="chr"><p>ᏣᎳᎩ</p></section>`, "chr"],
    // Which makes two spellings of one language an agreement rather than a conflict β€” they are
    // the same language, and a document is not multilingual because two pages chose differently.
    ["two pages naming the same language two ways agree",
      `<section lang="ko"><p>κ°€</p></section>\n\n<section lang="kor"><p>λ‚˜</p></section>`, "ko"],
  ] as [string, string, string | null][]) {
    assert.equal(bodyLang(body), expected, what);
  }
});

// The measurement the strictness above rests on, run rather than asserted from memory: axe
// validates a `lang` against the registry's PREFERRED values, so the three-letter codes that have
// a two-letter equivalent are the ones it refuses. If that ever stops being true, the rule in
// `preferredTag` is either too strict or not strict enough and this is the test that says so.
// `html-lang-valid` is the rule that judges the root and only the root; `valid-lang` judges every
// other element. So the third column is the fragment's own answer being reported where it was
// written β€” a body issue the review loop can correct, which is not something the root should paper
// over β€” while the first two say the root itself is never the violation.
test("every language the shell will declare is one the linter accepts", async () => {
  // #196 is why this list is long rather than illustrative. The guard had been widened three times
  // β€” the shape check, then the alias table, then NOT_AN_ANSWER β€” and each pass closed the
  // instances that had been demonstrated to it without asking the question the linter asks, so the
  // next unexamined class shipped a serious violation on the root. Every row here is one runAxe
  // call against the assembled shell, which is the only thing that can say the property holds.
  for (const [written, root, fragmentFlagged] of [
    ["ko", "ko", false], ["zh-Hans", "zh-Hans", false], ["pt-BR", "pt-BR", false],
    ["haw", "haw", false], ["chr", "chr", false],
    // The interesting rows: written by the page in a form axe refuses, delivered in one it accepts.
    ["kor", "ko", true], ["spa", "es", true], ["eng", "en", true],
    // A deprecated two-letter code axe happens to accept is still delivered in its preferred form.
    ["iw", "he", false],
    // Repairs that change more than the spelling of the primary subtag, and are still repairs the
    // linter accepts on the root.
    ["sh", "sr-Latn", false], ["art-lojban", "jbo", false], ["tl", "fil", false], ["in", "id", false],
    // The #196 class: refused here, so the root is a clean `en` while the fragment keeps the answer
    // it wrote and is reported for it β€” a body issue the review loop can correct, which is where a
    // wrong `lang` should be reported rather than on the element this project writes.
    ["cn", "en", true], ["jp", "en", true], ["cz", "en", true], ["dk", "en", true],
    ["gr", "en", true], ["ua", "en", true], ["vn", "en", true],
    ["xxy", "en", true], ["zzz", "en", true],
    // Accepted, and the rows that say the list is axe's own and not a proxy for it: no English name
    // in CLDR, in the registry axe validates against, clean on the root.
    ["aaa", "aaa", false], ["ttj", "ttj", false], ["lns", "lns", false],
    // A language that is also a country code. Accepted, because it IS a language β€” see the note in
    // the derivation test above for what that costs and why nothing here can do better.
    ["kr", "kr", false], ["se", "se", false],
    // Well formed, in the registry, and refused anyway: not an answer to "what language is this
    // document in". The linter accepts all four, which is exactly why they are refused here.
    ["und", "en", false], ["zxx", "en", false], ["mul", "en", false], ["qaa", "en", false],
    // And the ones that derive nothing, where the root has to be a clean `en` all the same.
    ["Korean", "en", true], ["ko_KR", "en", true],
  ] as [string, string, boolean][]) {
    const html = wrapDocument(`<section lang="${written}"><h1>제λͺ©</h1><p>λ³Έλ¬Έ ν…μŠ€νŠΈ</p></section>`);
    assert.match(html, new RegExp(`<html lang="${root}">`), `lang="${written}" should derive ${root}`);
    const lint = await runAxe(html);
    const ids = (lint.violations ?? []).map((v) => v.id);
    assert.ok(!ids.includes("html-lang-valid"), `lang="${written}" delivered a root axe rejects`);
    assert.ok(!ids.includes("html-has-lang"), `lang="${written}" delivered a root with no language`);
    assert.equal(ids.includes("valid-lang"), fragmentFlagged,
      `lang="${written}": the fragment's own attribute should ${fragmentFlagged ? "" : "not "}be reported`);
  }
});

// Which of the two lists answered "is this a language". The primary one is axe's own β€” the list
// `html-lang-valid` validates against, read out of the same pinned dependency the gate runs β€” and it
// is reached through `axe.utils`, a runtime export axe's .d.ts does not declare and its semver does
// not cover. So an axe bump that drops it downgrades the derivation to CLDR display data, which has
// no name for part of the obscure end of ISO 639-3 and would therefore refuse those documents a root
// language. That is a silent narrowing of who gets a correctly labelled document, and this is the
// row that makes it loud instead.
test("the language list is the linter's own, not the fallback", () => {
  assert.equal(languageListSource, "axe", "axe.utils.validLangs() is unreachable; see the fallback note in lint.ts");
  // Cheap sanity on the list itself, in the two directions that matter: a truncated list would
  // refuse every language and read as the guard working, and a list of everything would accept the
  // #196 class straight back onto the root.
  for (const yes of ["ko", "en", "haw", "aaa", "ttj", "jbo", "sr"]) {
    assert.ok(isKnownLanguage(yes), `${yes} should be a language the linter knows`);
  }
  for (const no of ["cn", "jp", "xxy", "zzz", "korean", ""]) {
    assert.ok(!isKnownLanguage(no), `${no} should not be`);
  }
  // Case-folded, because a canonical primary subtag is lowercase but the value a page wrote is not
  // necessarily (`KO`, which axe accepts and this derivation carries through as written).
  assert.ok(isKnownLanguage("KO"));
});

// And what the day of that downgrade actually looks like, measured rather than reasoned about. The
// fallback is unreachable through `isKnownLanguage` in any environment this suite can build β€” the axe
// list is read at import from a pinned dependency and there is no seam to remove it β€” so it is called
// directly here. Otherwise the branch that answers on the day `axe.utils` disappears would be the one
// branch never run, and "it degrades to CLDR" would be a claim about untested code.
//
// The divergence is the point: CLDR is display data, so it knows a language only if it can NAME one,
// and the far end of ISO 639-3 has no English display name. Those documents keep a correct root today
// and would lose it β€” which is a narrowing worth having written down, not a defect in either list.
test("the CLDR fallback answers, and these are the languages a downgrade would cost", () => {
  for (const yes of ["ko", "KO", "en", "haw", "chr", "fil", "qu", "he"]) {
    assert.ok(cldrKnowsLanguage(yes), `CLDR should name ${yes}`);
  }
  for (const no of ["cn", "jp", "xxy", "zzz", "korean", ""]) {
    assert.ok(!cldrKnowsLanguage(no), `CLDR should not name ${no}`);
  }
  // The measured cost, and the reason axe's list is the primary rather than the other way round:
  // every one of these is in the registry `html-lang-valid` validates against, is accepted today,
  // and would be refused a root label by the fallback.
  for (const lost of ["aaa", "aab", "abt", "aby", "acd", "adz", "ahr", "ajz", "lns", "ttj"]) {
    assert.ok(isKnownLanguage(lost), `${lost} should be accepted today`);
    assert.ok(!cldrKnowsLanguage(lost), `${lost} is documented as a fallback casualty and no longer is`);
  }
  // It diverges in the other direction too β€” CLDR names `kor`, which axe refuses β€” and that costs
  // nothing, because `preferredTag` canonicalizes BEFORE asking: `kor` becomes `ko` and the
  // membership check only ever sees a canonical primary subtag. Pinned so the ordering stays.
  assert.ok(cldrKnowsLanguage("kor"));
  assert.ok(!isKnownLanguage("kor"));
  assert.equal(bodyLang(`<section lang="kor"><p>κ°€</p></section>`), "ko");
  // `und`/`qaa` are refused by CLDR and accepted by axe, and both lists are overruled anyway:
  // `NOT_AN_ANSWER` rejects them before either is consulted, so a downgrade cannot change them.
  for (const nonAnswer of ["und", "qaa"]) {
    assert.equal(bodyLang(`<section lang="${nonAnswer}"><p>text</p></section>`), null);
  }
});

// #163's fix labelled the shell's `<title lang="en">` on a non-English document, and the served
// title is patched by a regex one module away (`titledAs`, used by GET /output). The two met badly:
// a pattern matching only a bare `<title>` no-opped on precisely the documents that had just been
// given a truthful root language, so a Korean document was delivered with the placeholder name
// while its download filename still mirrored the upload β€” WCAG 2.4.2 lost where 3.1.1 was won.
//
// The `lang="en"` goes with the string it vouched for. The shell labels its own placeholder because
// "Accessible document" is English; an uploaded file's name is in whatever language the person who
// named it used, and on a Korean document it is usually Korean β€” so carrying the label over would
// assert English over a Korean title in the one place a reader hears the document's name. Dropping it
// leaves the title inheriting the root, which is the policy the root itself follows: fall back to the
// containing default rather than assert a language nobody can vouch for.
test("the served title mirrors the upload whatever attributes the shell put on it", () => {
  const korean = titledAs(wrapDocument(`<section lang="ko"><h1>λ³΄κ³ μ„œ</h1></section>`), "λ³΄κ³ μ„œ-2026");
  assert.match(korean, /<title>λ³΄κ³ μ„œ-2026<\/title>/);
  assert.match(korean, /<html lang="ko">/);
  // The shell's own placeholder keeps the label, since that is the string it is true of.
  assert.match(wrapDocument(`<section lang="ko"><h1>λ³΄κ³ μ„œ</h1></section>`), /<title lang="en">Accessible/);
  // Any other attribute the shell might carry is kept; only the invalidated claim is dropped.
  assert.match(titledAs(`<title id="t" lang='en' dir="ltr">x</title>`, "y"), /<title id="t" dir="ltr">y<\/title>/);
  // And the drop reads attributes rather than searching for the string, so it cannot edit one
  // attribute's value while leaving the claim it meant to drop in place.
  assert.equal(titledAs(`<title data-note="a lang=fr b" lang="en">x</title>`, "y"),
    `<title data-note="a lang=fr b">y</title>`);
  assert.equal(titledAs(`<title langue="x">a</title>`, "y"), `<title langue="x">y</title>`);
  const english = titledAs(wrapDocument(`<h1>Report</h1>`), "quarterly");
  assert.match(english, /<title>quarterly<\/title>/);
  // The name is user input on its way into markup, and into a replacement string.
  assert.match(titledAs(wrapDocument(`<h1>x</h1>`), `a&b<script>`), /<title>a&amp;b&lt;script><\/title>/);
  assert.match(titledAs(wrapDocument(`<h1>x</h1>`), `$&$1`), /<title>\$&amp;\$1<\/title>/);
  // One title, and only the title: the shell's is the first `<title>` in the document, and a body
  // that contains the string must not be rewritten by it.
  assert.equal((titledAs(wrapDocument(`<h1>x</h1>`), "y").match(/<title/g) ?? []).length, 1);
});

// What the derivation is for. `wrapDocument` is the only place the root attribute is written,
// and the assertion is on the shell rather than on `bodyLang` because the shell is what ships.
test("the delivered shell declares the language the body agreed on, and falls back to English", () => {
  const korean = wrapDocument(`<section lang="ko"><h1>λ³΄κ³ μ„œ</h1></section>`);
  assert.match(korean, /<html lang="ko">/);
  // The one English string in the shell, now sitting inside a root that says `ko`. Without a
  // `lang` of its own it inherits that root and is announced as Korean β€” WCAG 3.1.2, and
  // audible in the tab title and in whatever reads the document's name aloud. It is only
  // labelled where it needs to be: an `en` document does not want `lang="en"` repeated on it.
  assert.match(korean, /<title lang="en">Accessible document<\/title>/);

  const english = wrapDocument(`<h1>Report</h1>`);
  assert.match(english, /<html lang="en">/);
  assert.match(english, /<title>Accessible document<\/title>/);

  // A regional variant of English is still English for the title's purposes; the label would
  // be noise.
  assert.match(wrapDocument(`<section lang="en-GB"><p>Colour</p></section>`), /<html lang="en-GB">/);
  assert.match(wrapDocument(`<section lang="en-GB"><p>Colour</p></section>`), /<title>Accessible/);

  // A body deriving nothing is delivered exactly as it was before #163 β€” the fallback is the
  // old behaviour, not a new one, and every other document in the test suite depends on it.
  assert.match(wrapDocument(`<section lang="ko"><p>κ°€</p></section>\n\n<section><p>λ‚˜</p></section>`),
    /<html lang="en">/);

  // The value reaches an attribute, so it has to be a value that cannot leave one. `bodyLang`
  // refuses anything that is not shaped like a language tag, which is what makes that true;
  // this pins the consequence rather than the mechanism, since it is the consequence a later
  // change to the tag pattern would have to keep.
  assert.match(wrapDocument(`<section lang="ko&quot;><script>alert(1)</script>"><p>κ°€</p></section>`),
    /<html lang="en">/);
});