@gusnips/vite 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/head.d.ts.map +1 -1
- package/dist/head.js +42 -11
- package/dist/head.js.map +1 -1
- package/package.json +1 -1
- package/src/head.test.ts +39 -0
- package/src/head.ts +45 -11
package/dist/head.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAiBA,4FAA4F;AAC5F,eAAO,MAAM,UAAU,4BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAiBA,4FAA4F;AAC5F,eAAO,MAAM,UAAU,4BAA0B,CAAC;AAmDlD,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB;yBACqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;2DACuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;gDAC4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAClC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAmGjE;AAED,MAAM,WAAW,cAAc;IAC7B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;uDACmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAWD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,cAAmB,GAC1B,IAAI,CA2CN"}
|
package/dist/head.js
CHANGED
|
@@ -30,7 +30,23 @@ function setMeta(html, selector, value) {
|
|
|
30
30
|
return html.replace(re, `$1${escapeAttr(value)}$2`);
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* The same, for a
|
|
33
|
+
* The same regex, for a key a template may spell with EITHER attribute.
|
|
34
|
+
*
|
|
35
|
+
* The `twitter:` family is written both ways in the wild and both work: X's own documentation
|
|
36
|
+
* says `name=`, the Open Graph spec says `property=`, and two adopters' templates carry
|
|
37
|
+
* `name="twitter:card"` in the same head as `property="twitter:title"`. Matching one spelling
|
|
38
|
+
* silently skips the other — and these tags go through `setMetaIfPresent`, where "skipped" is
|
|
39
|
+
* not an error but the intended behaviour for a template that does not have them.
|
|
40
|
+
*
|
|
41
|
+
* The fourth adopter spells the whole family `name=`. Baked with the narrow regex, every one of
|
|
42
|
+
* its 45 prerendered files — eleven pages in three languages — would have kept the template's
|
|
43
|
+
* card title and description, which is the front door's, and nothing in a browser shows it.
|
|
44
|
+
* That is the exact failure this file exists to prevent, arriving through the package rather
|
|
45
|
+
* than despite it.
|
|
46
|
+
*/
|
|
47
|
+
const eitherRe = (key) => new RegExp(`(<meta[^>]*(?:name|property)="${escapeRegex(key)}"[^>]*content=")[^"]*(")`);
|
|
48
|
+
/**
|
|
49
|
+
* Replace the `content` of a tag a template is allowed not to carry.
|
|
34
50
|
*
|
|
35
51
|
* Only the `twitter:` tags, `og:image:alt` and `name="title"` get this. X reads the `og:` tags when
|
|
36
52
|
* the `twitter:` ones are absent, no crawler reads `name="title"` at all, and most templates carry
|
|
@@ -38,9 +54,10 @@ function setMeta(html, selector, value) {
|
|
|
38
54
|
* that never had them. A template that DOES carry one and gets a stale value is still a bug, which
|
|
39
55
|
* is why they are written rather than ignored. Everything a crawler actually depends on goes
|
|
40
56
|
* through `setMeta`.
|
|
57
|
+
*
|
|
58
|
+
* Takes the regex rather than the selector, so each caller picks how strict the spelling is.
|
|
41
59
|
*/
|
|
42
|
-
function setMetaIfPresent(html,
|
|
43
|
-
const re = metaRe(selector);
|
|
60
|
+
function setMetaIfPresent(html, re, value) {
|
|
44
61
|
return re.test(html) ? html.replace(re, `$1${escapeAttr(value)}$2`) : html;
|
|
45
62
|
}
|
|
46
63
|
/**
|
|
@@ -71,16 +88,16 @@ export function bakeHead(template, tags) {
|
|
|
71
88
|
html = setMeta(html, 'name="description"', tags.description);
|
|
72
89
|
html = setMeta(html, 'property="og:title"', shareTitle);
|
|
73
90
|
html = setMeta(html, 'property="og:description"', shareDescription);
|
|
74
|
-
html = setMetaIfPresent(html, 'name="title"', tags.title);
|
|
75
|
-
html = setMetaIfPresent(html,
|
|
76
|
-
html = setMetaIfPresent(html,
|
|
91
|
+
html = setMetaIfPresent(html, metaRe('name="title"'), tags.title);
|
|
92
|
+
html = setMetaIfPresent(html, eitherRe("twitter:title"), shareTitle);
|
|
93
|
+
html = setMetaIfPresent(html, eitherRe("twitter:description"), shareDescription);
|
|
77
94
|
if (tags.image !== undefined) {
|
|
78
95
|
html = setMeta(html, 'property="og:image"', tags.image);
|
|
79
|
-
html = setMetaIfPresent(html,
|
|
96
|
+
html = setMetaIfPresent(html, eitherRe("twitter:image"), tags.image);
|
|
80
97
|
// A card's description goes with the card. Left alone, every page's own card was described
|
|
81
98
|
// with the front page's title — the stale value this function exists to prevent, on the one
|
|
82
99
|
// tag written for somebody who cannot see the picture.
|
|
83
|
-
html = setMetaIfPresent(html,
|
|
100
|
+
html = setMetaIfPresent(html, eitherRe("og:image:alt"), shareTitle);
|
|
84
101
|
}
|
|
85
102
|
if (tags.canonical === null) {
|
|
86
103
|
// Strip rather than blank: an empty canonical is a claim about "" and an empty og:url is a
|
|
@@ -88,11 +105,11 @@ export function bakeHead(template, tags) {
|
|
|
88
105
|
// claim about this address, and a dead link that still unfurls the brand card is fine.
|
|
89
106
|
html = html
|
|
90
107
|
.replace(/\s*<link rel="canonical"[^>]*>/, "")
|
|
91
|
-
.replace(/\s*<meta
|
|
108
|
+
.replace(/\s*<meta[^>]*(?:name|property)="(?:og|twitter):url"[^>]*>/g, "");
|
|
92
109
|
}
|
|
93
110
|
else {
|
|
94
111
|
html = setMeta(html, 'property="og:url"', tags.canonical);
|
|
95
|
-
html = setMetaIfPresent(html,
|
|
112
|
+
html = setMetaIfPresent(html, eitherRe("twitter:url"), tags.canonical);
|
|
96
113
|
// Demanded, like every meta tag a crawler depends on. `String.replace` with no match is a
|
|
97
114
|
// silent no-op, so a template that never had a canonical would get one on no page at all
|
|
98
115
|
// and say nothing about it — the exact failure `setMeta` exists to turn into a red build.
|
|
@@ -144,6 +161,14 @@ export function bakeHead(template, tags) {
|
|
|
144
161
|
}
|
|
145
162
|
return html;
|
|
146
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Past this much growth the file has a page in it, whatever else it also contains.
|
|
166
|
+
*
|
|
167
|
+
* It is what lets the loading-screen search below look at a whole slice rather than one tag: a
|
|
168
|
+
* real page that opens with a live region ("Saved", a connection banner) is far over this, and a
|
|
169
|
+
* splash never is. The donor's was 1,174 bytes.
|
|
170
|
+
*/
|
|
171
|
+
const SPLASH_MAX_GROWTH = 2000;
|
|
147
172
|
/**
|
|
148
173
|
* What every written file must be true of before the build is allowed to pass.
|
|
149
174
|
*
|
|
@@ -195,7 +220,13 @@ export function assertRendered(file, html, template, checks = {}) {
|
|
|
195
220
|
.trimStart();
|
|
196
221
|
if (!/^<[a-z]/.test(head))
|
|
197
222
|
fail("has no element inside its root");
|
|
198
|
-
|
|
223
|
+
// Anywhere in that bounded slice, not only its first tag, and only while the file is small
|
|
224
|
+
// enough to BE a splash. Anchored at the root's first element this missed the fourth adopter's
|
|
225
|
+
// loading screen, which centres its live region inside a `<main>` — one element deeper than the
|
|
226
|
+
// donor's, and an ordinary-looking wrapper to a test that reads one tag. That adopter's own
|
|
227
|
+
// guard missed it from the other side: it failed a file containing a spinner class its splash
|
|
228
|
+
// does not use. Two checks written for one regression, neither of which could ever fire.
|
|
229
|
+
if (grew < SPLASH_MAX_GROWTH && head.includes('role="status"'))
|
|
199
230
|
fail("rendered the loading screen, not the page — something suspended and never resolved");
|
|
200
231
|
}
|
|
201
232
|
//# sourceMappingURL=head.js.map
|
package/dist/head.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,yFAAyF;AACzF,8FAA8F;AAC9F,yDAAyD;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,4FAA4F;AAC5F,MAAM,CAAC,MAAM,UAAU,GAAG,uBAAuB,CAAC;AAElD,mGAAmG;AACnG,MAAM,MAAM,GAAG,CAAC,QAAgB,EAAU,EAAE,CAC1C,IAAI,MAAM,CAAC,cAAc,WAAW,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC;AAE3E;;;;GAIG;AACH,SAAS,OAAO,CAAC,IAAY,EAAE,QAAgB,EAAE,KAAa;IAC5D,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,QAAQ,2BAA2B,CAAC,CAAC;IAC7F,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtD,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/head.test.ts
CHANGED
|
@@ -48,6 +48,14 @@ const LEAN_TEMPLATE = `<!doctype html>
|
|
|
48
48
|
</body>
|
|
49
49
|
</html>`;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A fourth adopter's spelling of the same tags. X's own documentation writes them `name=`, the
|
|
53
|
+
* Open Graph spec writes `property=`, and crawlers read both — so a template may use either, and
|
|
54
|
+
* two templates in this fleet use both in one head. Matching only `property=` turned the other
|
|
55
|
+
* spelling into a silent skip rather than a build error.
|
|
56
|
+
*/
|
|
57
|
+
const NAME_TWITTER_TEMPLATE = TEMPLATE.replace(/property="twitter:/g, 'name="twitter:');
|
|
58
|
+
|
|
51
59
|
const BASE = { title: "T", description: "D", canonical: "https://acme.com/pricing" };
|
|
52
60
|
|
|
53
61
|
describe("bakeHead", () => {
|
|
@@ -96,6 +104,22 @@ describe("bakeHead", () => {
|
|
|
96
104
|
expect(html).not.toContain('name="title"');
|
|
97
105
|
});
|
|
98
106
|
|
|
107
|
+
// …and the trap right beside that exception. A template that DOES carry these, spelled the
|
|
108
|
+
// way X documents them, must not read as one that omits them: skipping is correct for an
|
|
109
|
+
// absent tag and silent data loss for a present one. An adopter on this spelling would have
|
|
110
|
+
// shipped every page wearing the front door's card.
|
|
111
|
+
it("writes the share tags whichever attribute the template spells them with", () => {
|
|
112
|
+
const html = bakeHead(NAME_TWITTER_TEMPLATE, { ...BASE, ogTitle: "share" });
|
|
113
|
+
expect(html).toContain('<meta name="twitter:title" content="share" />');
|
|
114
|
+
expect(html).toContain('<meta name="twitter:description" content="D" />');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("strips a share URL spelled with either attribute", () => {
|
|
118
|
+
const html = bakeHead(NAME_TWITTER_TEMPLATE, { ...BASE, canonical: null });
|
|
119
|
+
expect(html).not.toContain("twitter:url");
|
|
120
|
+
expect(html).not.toContain('content=""');
|
|
121
|
+
});
|
|
122
|
+
|
|
99
123
|
it("strips the canonical and the share URL rather than blanking them", () => {
|
|
100
124
|
const html = bakeHead(TEMPLATE, { ...BASE, canonical: null, noindex: true });
|
|
101
125
|
expect(html).not.toContain('rel="canonical"');
|
|
@@ -255,6 +279,21 @@ describe("assertRendered", () => {
|
|
|
255
279
|
expect(() => assertRendered("pricing.html", page(spinner), shell)).toThrow(/loading screen/);
|
|
256
280
|
});
|
|
257
281
|
|
|
282
|
+
// The donor's splash IS the root's first element. A fourth adopter's centres the live region
|
|
283
|
+
// inside a `<main>`, so a check anchored at the first tag saw an ordinary wrapper and passed
|
|
284
|
+
// the file — while that repo's own guard looked for a spinner class its splash does not use.
|
|
285
|
+
it("catches a loading screen nested inside a wrapper element", () => {
|
|
286
|
+
const nested = `<div id="root"><main class="grid place-items-center"><p role="status">Loading${"x".repeat(700)}</p></main></div>`;
|
|
287
|
+
expect(() => assertRendered("pricing.html", page(nested), shell)).toThrow(/loading screen/);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// What makes the wider search safe: a page with this much in it has a page in it, whatever a
|
|
291
|
+
// live region at the top of it says. A real banner ("Saved", a connection notice) is ordinary.
|
|
292
|
+
it("does not flag a full page that opens with a live region", () => {
|
|
293
|
+
const banner = `<div id="root"><div role="status">Saved</div><main>${"x".repeat(2500)}</main></div>`;
|
|
294
|
+
expect(() => assertRendered("pricing.html", page(banner), shell)).not.toThrow();
|
|
295
|
+
});
|
|
296
|
+
|
|
258
297
|
// The reason the check slices rather than matching in one pattern: a regex that skips
|
|
259
298
|
// Suspense markers can backtrack across the whole document and find a `role="status"` 40 KB
|
|
260
299
|
// down in a perfectly good page.
|
package/src/head.ts
CHANGED
|
@@ -34,7 +34,25 @@ function setMeta(html: string, selector: string, value: string): string {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* The same, for a
|
|
37
|
+
* The same regex, for a key a template may spell with EITHER attribute.
|
|
38
|
+
*
|
|
39
|
+
* The `twitter:` family is written both ways in the wild and both work: X's own documentation
|
|
40
|
+
* says `name=`, the Open Graph spec says `property=`, and two adopters' templates carry
|
|
41
|
+
* `name="twitter:card"` in the same head as `property="twitter:title"`. Matching one spelling
|
|
42
|
+
* silently skips the other — and these tags go through `setMetaIfPresent`, where "skipped" is
|
|
43
|
+
* not an error but the intended behaviour for a template that does not have them.
|
|
44
|
+
*
|
|
45
|
+
* The fourth adopter spells the whole family `name=`. Baked with the narrow regex, every one of
|
|
46
|
+
* its 45 prerendered files — eleven pages in three languages — would have kept the template's
|
|
47
|
+
* card title and description, which is the front door's, and nothing in a browser shows it.
|
|
48
|
+
* That is the exact failure this file exists to prevent, arriving through the package rather
|
|
49
|
+
* than despite it.
|
|
50
|
+
*/
|
|
51
|
+
const eitherRe = (key: string): RegExp =>
|
|
52
|
+
new RegExp(`(<meta[^>]*(?:name|property)="${escapeRegex(key)}"[^>]*content=")[^"]*(")`);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Replace the `content` of a tag a template is allowed not to carry.
|
|
38
56
|
*
|
|
39
57
|
* Only the `twitter:` tags, `og:image:alt` and `name="title"` get this. X reads the `og:` tags when
|
|
40
58
|
* the `twitter:` ones are absent, no crawler reads `name="title"` at all, and most templates carry
|
|
@@ -42,9 +60,10 @@ function setMeta(html: string, selector: string, value: string): string {
|
|
|
42
60
|
* that never had them. A template that DOES carry one and gets a stale value is still a bug, which
|
|
43
61
|
* is why they are written rather than ignored. Everything a crawler actually depends on goes
|
|
44
62
|
* through `setMeta`.
|
|
63
|
+
*
|
|
64
|
+
* Takes the regex rather than the selector, so each caller picks how strict the spelling is.
|
|
45
65
|
*/
|
|
46
|
-
function setMetaIfPresent(html: string,
|
|
47
|
-
const re = metaRe(selector);
|
|
66
|
+
function setMetaIfPresent(html: string, re: RegExp, value: string): string {
|
|
48
67
|
return re.test(html) ? html.replace(re, `$1${escapeAttr(value)}$2`) : html;
|
|
49
68
|
}
|
|
50
69
|
|
|
@@ -142,17 +161,17 @@ export function bakeHead(template: string, tags: HeadTags): string {
|
|
|
142
161
|
html = setMeta(html, 'name="description"', tags.description);
|
|
143
162
|
html = setMeta(html, 'property="og:title"', shareTitle);
|
|
144
163
|
html = setMeta(html, 'property="og:description"', shareDescription);
|
|
145
|
-
html = setMetaIfPresent(html, 'name="title"', tags.title);
|
|
146
|
-
html = setMetaIfPresent(html,
|
|
147
|
-
html = setMetaIfPresent(html,
|
|
164
|
+
html = setMetaIfPresent(html, metaRe('name="title"'), tags.title);
|
|
165
|
+
html = setMetaIfPresent(html, eitherRe("twitter:title"), shareTitle);
|
|
166
|
+
html = setMetaIfPresent(html, eitherRe("twitter:description"), shareDescription);
|
|
148
167
|
|
|
149
168
|
if (tags.image !== undefined) {
|
|
150
169
|
html = setMeta(html, 'property="og:image"', tags.image);
|
|
151
|
-
html = setMetaIfPresent(html,
|
|
170
|
+
html = setMetaIfPresent(html, eitherRe("twitter:image"), tags.image);
|
|
152
171
|
// A card's description goes with the card. Left alone, every page's own card was described
|
|
153
172
|
// with the front page's title — the stale value this function exists to prevent, on the one
|
|
154
173
|
// tag written for somebody who cannot see the picture.
|
|
155
|
-
html = setMetaIfPresent(html,
|
|
174
|
+
html = setMetaIfPresent(html, eitherRe("og:image:alt"), shareTitle);
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
if (tags.canonical === null) {
|
|
@@ -161,10 +180,10 @@ export function bakeHead(template: string, tags: HeadTags): string {
|
|
|
161
180
|
// claim about this address, and a dead link that still unfurls the brand card is fine.
|
|
162
181
|
html = html
|
|
163
182
|
.replace(/\s*<link rel="canonical"[^>]*>/, "")
|
|
164
|
-
.replace(/\s*<meta
|
|
183
|
+
.replace(/\s*<meta[^>]*(?:name|property)="(?:og|twitter):url"[^>]*>/g, "");
|
|
165
184
|
} else {
|
|
166
185
|
html = setMeta(html, 'property="og:url"', tags.canonical);
|
|
167
|
-
html = setMetaIfPresent(html,
|
|
186
|
+
html = setMetaIfPresent(html, eitherRe("twitter:url"), tags.canonical);
|
|
168
187
|
// Demanded, like every meta tag a crawler depends on. `String.replace` with no match is a
|
|
169
188
|
// silent no-op, so a template that never had a canonical would get one on no page at all
|
|
170
189
|
// and say nothing about it — the exact failure `setMeta` exists to turn into a red build.
|
|
@@ -243,6 +262,15 @@ export interface RenderedChecks {
|
|
|
243
262
|
minGrowth?: number;
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Past this much growth the file has a page in it, whatever else it also contains.
|
|
267
|
+
*
|
|
268
|
+
* It is what lets the loading-screen search below look at a whole slice rather than one tag: a
|
|
269
|
+
* real page that opens with a live region ("Saved", a connection banner) is far over this, and a
|
|
270
|
+
* splash never is. The donor's was 1,174 bytes.
|
|
271
|
+
*/
|
|
272
|
+
const SPLASH_MAX_GROWTH = 2000;
|
|
273
|
+
|
|
246
274
|
/**
|
|
247
275
|
* What every written file must be true of before the build is allowed to pass.
|
|
248
276
|
*
|
|
@@ -298,6 +326,12 @@ export function assertRendered(
|
|
|
298
326
|
.trimStart();
|
|
299
327
|
|
|
300
328
|
if (!/^<[a-z]/.test(head)) fail("has no element inside its root");
|
|
301
|
-
|
|
329
|
+
// Anywhere in that bounded slice, not only its first tag, and only while the file is small
|
|
330
|
+
// enough to BE a splash. Anchored at the root's first element this missed the fourth adopter's
|
|
331
|
+
// loading screen, which centres its live region inside a `<main>` — one element deeper than the
|
|
332
|
+
// donor's, and an ordinary-looking wrapper to a test that reads one tag. That adopter's own
|
|
333
|
+
// guard missed it from the other side: it failed a file containing a spinner class its splash
|
|
334
|
+
// does not use. Two checks written for one regression, neither of which could ever fire.
|
|
335
|
+
if (grew < SPLASH_MAX_GROWTH && head.includes('role="status"'))
|
|
302
336
|
fail("rendered the loading screen, not the page — something suspended and never resolved");
|
|
303
337
|
}
|