@specific.dev/spectest 0.36.0 → 0.37.0
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/browser.js +81 -15
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -61,11 +61,37 @@ function base64ByteLength(b64) {
|
|
|
61
61
|
// surfaces as an intermittent `net::ERR_NAME_NOT_RESOLVED`. getaddrinfo
|
|
62
62
|
// reads resolv.conf synchronously per lookup, so it has no startup race
|
|
63
63
|
// and always reaches the resolver. See run 878d0054 (dashboard:3000).
|
|
64
|
+
//
|
|
65
|
+
// The two media flags below make `getUserMedia` work at all. The microVM
|
|
66
|
+
// has no camera and no sound card, so without them a page that asks for one
|
|
67
|
+
// fails on `NotFoundError: Requested device not found` — an app that gates
|
|
68
|
+
// its UI on microphone access can't be driven past that point.
|
|
69
|
+
//
|
|
70
|
+
// What the app gets is Chromium's own synthetic devices: the microphone is a
|
|
71
|
+
// generated beep pattern (a live, real MediaStreamTrack — MediaRecorder,
|
|
72
|
+
// WebRTC and AudioContext analysis all work on it), the camera a rolling
|
|
73
|
+
// test pattern at 640x480. There is deliberately NO way to feed a recording
|
|
74
|
+
// in; this is device availability, not audio fixtures.
|
|
75
|
+
//
|
|
76
|
+
// --use-fake-device-for-media-stream: the synthetic camera + microphone.
|
|
77
|
+
// Also what makes `enumerateDevices()` return anything at all.
|
|
78
|
+
// --use-fake-ui-for-media-stream: auto-accept the permission prompt.
|
|
79
|
+
// Headless has no UI to accept it with, so without this `getUserMedia`
|
|
80
|
+
// fails on `NotAllowedError` even when the fake devices are present.
|
|
81
|
+
//
|
|
82
|
+
// `--autoplay-policy=no-user-gesture-required` is deliberately NOT here.
|
|
83
|
+
// WebAudio over the captured stream works without it: holding a live
|
|
84
|
+
// getUserMedia stream is itself one of the conditions under which Chromium
|
|
85
|
+
// lets an AudioContext start, so an app's level meter or VAD reads the fake
|
|
86
|
+
// device fine (measured). Adding it would also un-block ordinary media
|
|
87
|
+
// autoplay, which is a behaviour change in the app under test, not in us.
|
|
64
88
|
const CHROME_ARGV = [
|
|
65
89
|
"--no-sandbox",
|
|
66
90
|
"--disable-dev-shm-usage",
|
|
67
91
|
"--disable-features=AsyncDns,DnsOverHttps",
|
|
68
92
|
"--dns-over-https-mode=off",
|
|
93
|
+
"--use-fake-device-for-media-stream",
|
|
94
|
+
"--use-fake-ui-for-media-stream",
|
|
69
95
|
];
|
|
70
96
|
/** Default touchStart→touchEnd dwell for touch taps — see `rawTap`. */
|
|
71
97
|
const TAP_DWELL_MS = 60;
|
|
@@ -305,15 +331,29 @@ const RRWEB_BOOTSTRAP = `
|
|
|
305
331
|
// src points at a same-origin URL (e.g. next/font's
|
|
306
332
|
// /_next/static/media/*.woff2) is unreachable at replay time and the
|
|
307
333
|
// page silently falls back to a system font. collectFonts above won't
|
|
308
|
-
// help (FontFace-API fonts only), so inline the bytes ourselves:
|
|
309
|
-
//
|
|
310
|
-
//
|
|
334
|
+
// help (FontFace-API fonts only), so inline the bytes ourselves: fetch
|
|
335
|
+
// each url() (same-origin -> allowed, and the page already trusts the
|
|
336
|
+
// in-VM CA), rewrite the rule's src to a data:
|
|
311
337
|
// URI, then take a fresh full snapshot so the now-self-contained CSS
|
|
312
338
|
// is captured. Plain string ops, no regex — this whole script ships
|
|
313
339
|
// inside a TS template literal where regex backslashes get mangled.
|
|
340
|
+
//
|
|
341
|
+
// Do NOT start this scan from document.fonts.ready. This script runs at
|
|
342
|
+
// document-start. At that moment the font set is idle, so Chrome settles
|
|
343
|
+
// that first ready promise immediately, before the parser has registered
|
|
344
|
+
// one stylesheet. The scan then saw 0 @font-face rules, and a one-shot
|
|
345
|
+
// latch stopped it from looking again, so no font was ever inlined.
|
|
346
|
+
// (Found on run_03j0mxtvp6x0p16d9y83e: every next/font face fell back.
|
|
347
|
+
// The diagnostics were invisible too, because rrweb's console plugin
|
|
348
|
+
// only patches console at the load event.)
|
|
349
|
+
//
|
|
350
|
+
// Two triggers instead: the load event, by which all render-blocking CSS
|
|
351
|
+
// is parsed, and each \`loadingdone\` on document.fonts, which catches CSS
|
|
352
|
+
// added later by an HMR update or a client-side route change. The scan
|
|
353
|
+
// skips a rule it already inlined, so repeated triggers are cheap.
|
|
314
354
|
(function () {
|
|
315
|
-
if (window.
|
|
316
|
-
window.
|
|
355
|
+
if (window.__spectestFontInliner) return;
|
|
356
|
+
window.__spectestFontInliner = true;
|
|
317
357
|
// Diagnostics ride the console-record plugin, so they land in the
|
|
318
358
|
// recording (and state.db) — our only debugging window into the
|
|
319
359
|
// headless page. Prefix is grep-able; keep them terse.
|
|
@@ -370,7 +410,12 @@ const RRWEB_BOOTSTRAP = `
|
|
|
370
410
|
}
|
|
371
411
|
}
|
|
372
412
|
}
|
|
373
|
-
|
|
413
|
+
// Every @font-face cssText this run already rewrote. A rule stays out
|
|
414
|
+
// of later scans, so an HMR update that adds one stylesheet costs one
|
|
415
|
+
// fetch, not a re-inline of the whole page.
|
|
416
|
+
var inlined = {};
|
|
417
|
+
var scans = 0, busy = false, rescan = false;
|
|
418
|
+
function collect() {
|
|
374
419
|
// Collect each @font-face's *cssText* (reliable for any CSSRule —
|
|
375
420
|
// unlike .style.setProperty('src',…), which silently no-ops on
|
|
376
421
|
// CSSFontFaceRule in Chrome) plus the http url()s inside it.
|
|
@@ -384,13 +429,26 @@ const RRWEB_BOOTSTRAP = `
|
|
|
384
429
|
eachFontFace(rules, function (rule) {
|
|
385
430
|
faceCount++;
|
|
386
431
|
var cssText = rule.cssText || "";
|
|
432
|
+
if (inlined[cssText]) return;
|
|
387
433
|
var urls = urlsIn(cssText, base);
|
|
388
434
|
if (urls.length) faces.push({ cssText: cssText, urls: urls });
|
|
389
435
|
});
|
|
390
436
|
}
|
|
391
|
-
|
|
392
|
-
|
|
437
|
+
// Log the first scan (so a page with no web fonts still says so) and
|
|
438
|
+
// every scan with work to do. A quiet re-trigger stays silent.
|
|
439
|
+
if (!scans++ || faces.length) {
|
|
440
|
+
log("sheets=" + sheets.length + " readable=" + readable +
|
|
441
|
+
" fontFaces=" + faceCount + " new=" + faces.length);
|
|
442
|
+
}
|
|
443
|
+
return faces;
|
|
444
|
+
}
|
|
445
|
+
function run() {
|
|
446
|
+
// One pass at a time. A trigger during a pass queues exactly one
|
|
447
|
+
// more, which then sees the marks the pass just wrote.
|
|
448
|
+
if (busy) { rescan = true; return; }
|
|
449
|
+
var faces = collect();
|
|
393
450
|
if (!faces.length) return;
|
|
451
|
+
busy = true;
|
|
394
452
|
var jobs = faces.map(function (face) {
|
|
395
453
|
return Promise.all(face.urls.map(function (u) {
|
|
396
454
|
return fetchDataUri(u.abs)
|
|
@@ -403,7 +461,9 @@ const RRWEB_BOOTSTRAP = `
|
|
|
403
461
|
text = text.split(pairs[k].token).join(pairs[k].dataUri);
|
|
404
462
|
changed = true;
|
|
405
463
|
}
|
|
406
|
-
|
|
464
|
+
if (!changed) return null;
|
|
465
|
+
inlined[face.cssText] = true;
|
|
466
|
+
return text;
|
|
407
467
|
});
|
|
408
468
|
});
|
|
409
469
|
Promise.all(jobs).then(function (texts) {
|
|
@@ -426,15 +486,21 @@ const RRWEB_BOOTSTRAP = `
|
|
|
426
486
|
log("took full snapshot");
|
|
427
487
|
}
|
|
428
488
|
} catch (e) { log("snapshot FAIL " + (e && e.message)); }
|
|
489
|
+
}).catch(function (e) {
|
|
490
|
+
log("run FAIL " + (e && e.message));
|
|
491
|
+
}).then(function () {
|
|
492
|
+
busy = false;
|
|
493
|
+
if (rescan) { rescan = false; setTimeout(run, 0); }
|
|
429
494
|
});
|
|
430
495
|
}
|
|
496
|
+
function schedule() { setTimeout(run, 0); }
|
|
431
497
|
try {
|
|
432
|
-
if (document.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
498
|
+
if (document.readyState === "complete") schedule();
|
|
499
|
+
else window.addEventListener("load", schedule);
|
|
500
|
+
// Fires each time a batch of fonts finishes loading, including fonts
|
|
501
|
+
// pulled in by CSS that arrives after the load event.
|
|
502
|
+
if (document.fonts && document.fonts.addEventListener) {
|
|
503
|
+
document.fonts.addEventListener("loadingdone", schedule);
|
|
438
504
|
}
|
|
439
505
|
} catch (e) { log("init FAIL " + (e && e.message)); }
|
|
440
506
|
})();
|