@specific.dev/spectest 0.35.1 → 0.36.1
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 +55 -15
- package/dist/components/postgres.d.ts +24 -0
- package/dist/components/postgres.js +5 -0
- package/dist/index.d.ts +11 -2
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -305,15 +305,29 @@ const RRWEB_BOOTSTRAP = `
|
|
|
305
305
|
// src points at a same-origin URL (e.g. next/font's
|
|
306
306
|
// /_next/static/media/*.woff2) is unreachable at replay time and the
|
|
307
307
|
// 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
|
-
//
|
|
308
|
+
// help (FontFace-API fonts only), so inline the bytes ourselves: fetch
|
|
309
|
+
// each url() (same-origin -> allowed, and the page already trusts the
|
|
310
|
+
// in-VM CA), rewrite the rule's src to a data:
|
|
311
311
|
// URI, then take a fresh full snapshot so the now-self-contained CSS
|
|
312
312
|
// is captured. Plain string ops, no regex — this whole script ships
|
|
313
313
|
// inside a TS template literal where regex backslashes get mangled.
|
|
314
|
+
//
|
|
315
|
+
// Do NOT start this scan from document.fonts.ready. This script runs at
|
|
316
|
+
// document-start. At that moment the font set is idle, so Chrome settles
|
|
317
|
+
// that first ready promise immediately, before the parser has registered
|
|
318
|
+
// one stylesheet. The scan then saw 0 @font-face rules, and a one-shot
|
|
319
|
+
// latch stopped it from looking again, so no font was ever inlined.
|
|
320
|
+
// (Found on run_03j0mxtvp6x0p16d9y83e: every next/font face fell back.
|
|
321
|
+
// The diagnostics were invisible too, because rrweb's console plugin
|
|
322
|
+
// only patches console at the load event.)
|
|
323
|
+
//
|
|
324
|
+
// Two triggers instead: the load event, by which all render-blocking CSS
|
|
325
|
+
// is parsed, and each \`loadingdone\` on document.fonts, which catches CSS
|
|
326
|
+
// added later by an HMR update or a client-side route change. The scan
|
|
327
|
+
// skips a rule it already inlined, so repeated triggers are cheap.
|
|
314
328
|
(function () {
|
|
315
|
-
if (window.
|
|
316
|
-
window.
|
|
329
|
+
if (window.__spectestFontInliner) return;
|
|
330
|
+
window.__spectestFontInliner = true;
|
|
317
331
|
// Diagnostics ride the console-record plugin, so they land in the
|
|
318
332
|
// recording (and state.db) — our only debugging window into the
|
|
319
333
|
// headless page. Prefix is grep-able; keep them terse.
|
|
@@ -370,7 +384,12 @@ const RRWEB_BOOTSTRAP = `
|
|
|
370
384
|
}
|
|
371
385
|
}
|
|
372
386
|
}
|
|
373
|
-
|
|
387
|
+
// Every @font-face cssText this run already rewrote. A rule stays out
|
|
388
|
+
// of later scans, so an HMR update that adds one stylesheet costs one
|
|
389
|
+
// fetch, not a re-inline of the whole page.
|
|
390
|
+
var inlined = {};
|
|
391
|
+
var scans = 0, busy = false, rescan = false;
|
|
392
|
+
function collect() {
|
|
374
393
|
// Collect each @font-face's *cssText* (reliable for any CSSRule —
|
|
375
394
|
// unlike .style.setProperty('src',…), which silently no-ops on
|
|
376
395
|
// CSSFontFaceRule in Chrome) plus the http url()s inside it.
|
|
@@ -384,13 +403,26 @@ const RRWEB_BOOTSTRAP = `
|
|
|
384
403
|
eachFontFace(rules, function (rule) {
|
|
385
404
|
faceCount++;
|
|
386
405
|
var cssText = rule.cssText || "";
|
|
406
|
+
if (inlined[cssText]) return;
|
|
387
407
|
var urls = urlsIn(cssText, base);
|
|
388
408
|
if (urls.length) faces.push({ cssText: cssText, urls: urls });
|
|
389
409
|
});
|
|
390
410
|
}
|
|
391
|
-
|
|
392
|
-
|
|
411
|
+
// Log the first scan (so a page with no web fonts still says so) and
|
|
412
|
+
// every scan with work to do. A quiet re-trigger stays silent.
|
|
413
|
+
if (!scans++ || faces.length) {
|
|
414
|
+
log("sheets=" + sheets.length + " readable=" + readable +
|
|
415
|
+
" fontFaces=" + faceCount + " new=" + faces.length);
|
|
416
|
+
}
|
|
417
|
+
return faces;
|
|
418
|
+
}
|
|
419
|
+
function run() {
|
|
420
|
+
// One pass at a time. A trigger during a pass queues exactly one
|
|
421
|
+
// more, which then sees the marks the pass just wrote.
|
|
422
|
+
if (busy) { rescan = true; return; }
|
|
423
|
+
var faces = collect();
|
|
393
424
|
if (!faces.length) return;
|
|
425
|
+
busy = true;
|
|
394
426
|
var jobs = faces.map(function (face) {
|
|
395
427
|
return Promise.all(face.urls.map(function (u) {
|
|
396
428
|
return fetchDataUri(u.abs)
|
|
@@ -403,7 +435,9 @@ const RRWEB_BOOTSTRAP = `
|
|
|
403
435
|
text = text.split(pairs[k].token).join(pairs[k].dataUri);
|
|
404
436
|
changed = true;
|
|
405
437
|
}
|
|
406
|
-
|
|
438
|
+
if (!changed) return null;
|
|
439
|
+
inlined[face.cssText] = true;
|
|
440
|
+
return text;
|
|
407
441
|
});
|
|
408
442
|
});
|
|
409
443
|
Promise.all(jobs).then(function (texts) {
|
|
@@ -426,15 +460,21 @@ const RRWEB_BOOTSTRAP = `
|
|
|
426
460
|
log("took full snapshot");
|
|
427
461
|
}
|
|
428
462
|
} catch (e) { log("snapshot FAIL " + (e && e.message)); }
|
|
463
|
+
}).catch(function (e) {
|
|
464
|
+
log("run FAIL " + (e && e.message));
|
|
465
|
+
}).then(function () {
|
|
466
|
+
busy = false;
|
|
467
|
+
if (rescan) { rescan = false; setTimeout(run, 0); }
|
|
429
468
|
});
|
|
430
469
|
}
|
|
470
|
+
function schedule() { setTimeout(run, 0); }
|
|
431
471
|
try {
|
|
432
|
-
if (document.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
472
|
+
if (document.readyState === "complete") schedule();
|
|
473
|
+
else window.addEventListener("load", schedule);
|
|
474
|
+
// Fires each time a batch of fonts finishes loading, including fonts
|
|
475
|
+
// pulled in by CSS that arrives after the load event.
|
|
476
|
+
if (document.fonts && document.fonts.addEventListener) {
|
|
477
|
+
document.fonts.addEventListener("loadingdone", schedule);
|
|
438
478
|
}
|
|
439
479
|
} catch (e) { log("init FAIL " + (e && e.message)); }
|
|
440
480
|
})();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServiceSetupContext } from "../index.js";
|
|
1
2
|
import { type SqlClient } from "../sql.js";
|
|
2
3
|
export interface PostgresOptions {
|
|
3
4
|
/** Image tag for the official `postgres` image. Default `"18-alpine"`.
|
|
@@ -32,6 +33,28 @@ export interface PostgresOptions {
|
|
|
32
33
|
args?: string[];
|
|
33
34
|
/** Extra environment variables forwarded to the container. */
|
|
34
35
|
env?: Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* One-shot hook that runs after the ready probe passes, before any
|
|
38
|
+
* dependent service starts and before any test. This is where schema
|
|
39
|
+
* and seed data go: the daemon captures the result into the
|
|
40
|
+
* warm-template snapshot, so it never runs again on a warm start or a
|
|
41
|
+
* fork.
|
|
42
|
+
*
|
|
43
|
+
* The argument is the full test context plus this service's `name` and
|
|
44
|
+
* its {@link PostgresHelpers}, so the instrumented client is already
|
|
45
|
+
* built:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* db: postgres({
|
|
49
|
+
* database: "todos", user: "todos", password: "todos",
|
|
50
|
+
* setup: async ({ helpers }) => {
|
|
51
|
+
* await helpers.client`CREATE TABLE todos (
|
|
52
|
+
* id SERIAL PRIMARY KEY, text TEXT NOT NULL)`;
|
|
53
|
+
* },
|
|
54
|
+
* }),
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
setup?: (args: ServiceSetupContext<PostgresHelpers>) => void | Promise<void>;
|
|
35
58
|
}
|
|
36
59
|
/** Helpers a `postgres(...)` service exposes on `ctx.svc.<name>`. */
|
|
37
60
|
export interface PostgresHelpers {
|
|
@@ -67,6 +90,7 @@ export interface PostgresHelpers {
|
|
|
67
90
|
* ```
|
|
68
91
|
*/
|
|
69
92
|
export declare function postgres(opts: PostgresOptions): {
|
|
93
|
+
setup?: ((args: ServiceSetupContext<PostgresHelpers>) => void | Promise<void>) | undefined;
|
|
70
94
|
env: {
|
|
71
95
|
POSTGRES_DB: string;
|
|
72
96
|
POSTGRES_USER: string;
|
|
@@ -54,5 +54,10 @@ export function postgres(opts) {
|
|
|
54
54
|
`@${name}:${port}/${encodeURIComponent(opts.database)}`;
|
|
55
55
|
return { client: new SQL(url, { label: name }) };
|
|
56
56
|
},
|
|
57
|
+
// The user's hook, forwarded verbatim — postgres() declares none of
|
|
58
|
+
// its own. If it ever gains one, CHAIN the user's after it (the way
|
|
59
|
+
// `serviceGroup` chains a group hook after the primary part's) rather
|
|
60
|
+
// than letting either one win.
|
|
61
|
+
...(opts.setup ? { setup: opts.setup } : {}),
|
|
57
62
|
};
|
|
58
63
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -455,8 +455,17 @@ export interface ServiceDefinition<H extends Record<string, any> = Record<string
|
|
|
455
455
|
}
|
|
456
456
|
/** A services map — what users pass to `environment.services`. Entries
|
|
457
457
|
* can be plain `ServiceConfig` literals or `ServiceDefinition`s that
|
|
458
|
-
* carry a `helpers` factory (e.g. what `postgres(...)` returns).
|
|
459
|
-
|
|
458
|
+
* carry a `helpers` factory (e.g. what `postgres(...)` returns).
|
|
459
|
+
*
|
|
460
|
+
* The entry type is `ServiceDefinition`, not `ServiceConfig`: at runtime
|
|
461
|
+
* the map really does hold `helpers`/`setup` (the daemon reads them off
|
|
462
|
+
* each entry), and both are optional, so every plain `ServiceConfig`
|
|
463
|
+
* still satisfies it. Narrowing this to `ServiceConfig` — the wire type,
|
|
464
|
+
* which by definition carries no functions — made the natural way to
|
|
465
|
+
* hoist a services map out of `index.ts` (`… satisfies ServicesMap`) fail
|
|
466
|
+
* the typecheck on any service declaring a hook, while the same literal
|
|
467
|
+
* written inline passed. */
|
|
468
|
+
export type ServicesMap = Record<string, ServiceDefinition<any>>;
|
|
460
469
|
/** Awaited return type of a service's `helpers` factory, or `never` if
|
|
461
470
|
* the service doesn't ship one. */
|
|
462
471
|
type HelpersOf<D> = D extends {
|