@specific.dev/spectest 0.39.0 → 0.43.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.d.ts +21 -8
- package/dist/browser.js +78 -36
- package/dist/components/supabase.d.ts +87 -27
- package/dist/components/supabase.js +352 -69
- package/dist/daemon.d.ts +38 -0
- package/dist/daemon.js +464 -987
- package/dist/harness/build-context.d.ts +82 -0
- package/dist/harness/build-context.js +113 -0
- package/dist/harness/buildkit-progress.d.ts +37 -0
- package/dist/harness/buildkit-progress.js +66 -0
- package/dist/harness/container-run.d.ts +89 -0
- package/dist/harness/container-run.js +118 -0
- package/dist/harness/file-mounts.d.ts +91 -0
- package/dist/harness/file-mounts.js +119 -0
- package/dist/harness/hostmatch.d.ts +65 -0
- package/dist/harness/hostmatch.js +108 -0
- package/dist/harness/http-proxy.d.ts +62 -0
- package/dist/harness/http-proxy.js +104 -0
- package/dist/harness/ingress-table.d.ts +148 -0
- package/dist/harness/ingress-table.js +129 -0
- package/dist/harness/log-delta.d.ts +54 -0
- package/dist/harness/log-delta.js +83 -0
- package/dist/harness/main.d.ts +47 -0
- package/dist/harness/main.js +164 -0
- package/dist/harness/methods.d.ts +54 -0
- package/dist/harness/methods.js +65 -0
- package/dist/harness/names-registry.d.ts +63 -0
- package/dist/harness/names-registry.js +90 -0
- package/dist/harness/protocol.d.ts +88 -0
- package/dist/harness/protocol.js +96 -0
- package/dist/harness/ready-poll.d.ts +47 -0
- package/dist/harness/ready-poll.js +67 -0
- package/dist/harness/service-graph.d.ts +29 -0
- package/dist/harness/service-graph.js +92 -0
- package/dist/harness/volume-paths.d.ts +70 -0
- package/dist/harness/volume-paths.js +81 -0
- package/dist/index.d.ts +58 -16
- package/dist/ingress.d.ts +1 -1
- package/dist/mobile.d.ts +9 -5
- package/dist/mobile.js +7 -6
- package/dist/recorder.d.ts +10 -0
- package/dist/resolver.js +5 -8
- package/dist/vendor/rrweb-plugin-console-record.umd.js +521 -0
- package/dist/vendor/rrweb-record.min.js +5061 -0
- package/package.json +7 -1
- package/src/aws-sigv4.ts +218 -0
- package/src/browser.ts +2095 -0
- package/src/components/aws.ts +554 -0
- package/src/components/email.ts +398 -0
- package/src/components/expo.ts +167 -0
- package/src/components/index.ts +81 -0
- package/src/components/k3s.ts +2061 -0
- package/src/components/postgres.ts +132 -0
- package/src/components/replayFake.ts +1015 -0
- package/src/components/s3.ts +132 -0
- package/src/components/supabase.ts +1699 -0
- package/src/daemon.ts +5537 -0
- package/src/harness/build-context.test.ts +0 -0
- package/src/harness/build-context.ts +146 -0
- package/src/harness/buildkit-progress.test.ts +98 -0
- package/src/harness/buildkit-progress.ts +74 -0
- package/src/harness/container-run.test.ts +209 -0
- package/src/harness/container-run.ts +158 -0
- package/src/harness/file-mounts.test.ts +185 -0
- package/src/harness/file-mounts.ts +145 -0
- package/src/harness/hostmatch.test.ts +148 -0
- package/src/harness/hostmatch.ts +109 -0
- package/src/harness/http-proxy.test.ts +156 -0
- package/src/harness/http-proxy.ts +119 -0
- package/src/harness/ingress-rebind.test.ts +125 -0
- package/src/harness/ingress-table.test.ts +172 -0
- package/src/harness/ingress-table.ts +186 -0
- package/src/harness/log-delta.test.ts +125 -0
- package/src/harness/log-delta.ts +100 -0
- package/src/harness/main.test.ts +211 -0
- package/src/harness/main.ts +196 -0
- package/src/harness/methods.test.ts +63 -0
- package/src/harness/methods.ts +92 -0
- package/src/harness/names-registry.test.ts +137 -0
- package/src/harness/names-registry.ts +108 -0
- package/src/harness/protocol.test.ts +148 -0
- package/src/harness/protocol.ts +163 -0
- package/src/harness/ready-poll.test.ts +172 -0
- package/src/harness/ready-poll.ts +93 -0
- package/src/harness/service-graph.test.ts +97 -0
- package/src/harness/service-graph.ts +97 -0
- package/src/harness/volume-paths.test.ts +102 -0
- package/src/harness/volume-paths.ts +112 -0
- package/src/ids.ts +89 -0
- package/src/index.ts +2767 -0
- package/src/ingress.ts +305 -0
- package/src/inspect.ts +739 -0
- package/src/locator.ts +716 -0
- package/src/mobile.ts +138 -0
- package/src/record-secrets.ts +41 -0
- package/src/recorder.ts +856 -0
- package/src/redis.ts +202 -0
- package/src/replay-bundle.ts +108 -0
- package/src/resolver.ts +348 -0
- package/src/s3.ts +333 -0
- package/src/sql.ts +243 -0
- package/src/terminal.ts +740 -0
- package/src/url-match.ts +67 -0
- package/src/vendor/rrweb-plugin-console-record.umd.js +521 -0
- package/src/vendor/rrweb-record.min.js +5061 -0
package/dist/index.d.ts
CHANGED
|
@@ -84,7 +84,7 @@ export interface ServiceConfig {
|
|
|
84
84
|
hostnames?: readonly string[];
|
|
85
85
|
/**
|
|
86
86
|
* Expose this service over HTTPS via a TLS-terminating reverse proxy
|
|
87
|
-
* hosted in the
|
|
87
|
+
* hosted in the harness. Each entry maps a fully-qualified
|
|
88
88
|
* hostname to the HTTP port the service listens on inside its
|
|
89
89
|
* container; the daemon binds the hostname on `:443` (SNI-multiplexed,
|
|
90
90
|
* with a leaf cert signed by the in-VM root CA) and on `:80`, and
|
|
@@ -936,20 +936,57 @@ export interface TestContext<P = undefined, S extends ServicesMap = ServicesMap,
|
|
|
936
936
|
/**
|
|
937
937
|
* Open the headless browser. Backed by Chromium-over-CDP inside the VM.
|
|
938
938
|
*
|
|
939
|
-
* There is
|
|
940
|
-
* call returns it, and it stays alive across
|
|
941
|
-
* of the state a test's snapshot captures, so
|
|
942
|
-
* the exact live page its parent left
|
|
943
|
-
* SPA state). Sign in once in a parent
|
|
944
|
-
* signed in. Sibling tests fork from
|
|
945
|
-
* can't see each other's browsing. A
|
|
946
|
-
* gets a fresh browser on first call
|
|
939
|
+
* There is one persistent browser PER NAME, and `ctx.browser()` is the
|
|
940
|
+
* default (unnamed) one: every call returns it, and it stays alive across
|
|
941
|
+
* tests — the browser is part of the state a test's snapshot captures, so
|
|
942
|
+
* a `dependsOn` child resumes the exact live page its parent left
|
|
943
|
+
* (cookies, localStorage, signed-in SPA state). Sign in once in a parent
|
|
944
|
+
* test; every descendant is already signed in. Sibling tests fork from
|
|
945
|
+
* the same parent snapshot, so they can't see each other's browsing. A
|
|
946
|
+
* test with no browser-using ancestor gets a fresh browser on first call
|
|
947
|
+
* (first call's options win).
|
|
948
|
+
*
|
|
949
|
+
* For a second, independent browser — a second user — name it:
|
|
950
|
+
* `ctx.browser("alice")`. See the named overload.
|
|
947
951
|
*
|
|
948
952
|
* `.close()` destroys the shared instance — the next `ctx.browser()`
|
|
949
953
|
* starts fresh. Don't call it for routine cleanup; recording is detached
|
|
950
954
|
* automatically at test end.
|
|
951
955
|
*/
|
|
952
956
|
browser(opts?: BrowserOptions): Promise<Browser>;
|
|
957
|
+
/**
|
|
958
|
+
* Open the persistent browser called `name`, creating it on first use.
|
|
959
|
+
* Each name is its own browser — its own cookies, localStorage and page —
|
|
960
|
+
* so naming them is how one test drives two users.
|
|
961
|
+
*
|
|
962
|
+
* **Only name a browser when the test needs two or more isolated sessions
|
|
963
|
+
* at once.** Otherwise use `ctx.browser()`: a name is a second identity,
|
|
964
|
+
* not a label, and a lone `ctx.browser("main")` buys nothing over the
|
|
965
|
+
* default while adding its name to every step title in the dashboard.
|
|
966
|
+
*
|
|
967
|
+
* ```ts
|
|
968
|
+
* const alice = await ctx.browser("alice", { url: "https://app.test" });
|
|
969
|
+
* const bob = await ctx.browser("bob", { url: "https://app.test" });
|
|
970
|
+
* await alice.getByRole("button", { name: "Share" }).click();
|
|
971
|
+
* await bob.reload();
|
|
972
|
+
* await expect(bob.getByText("Shared with you")).toBeVisible();
|
|
973
|
+
* ```
|
|
974
|
+
*
|
|
975
|
+
* Every rule of the default browser applies per name: the session rides
|
|
976
|
+
* the snapshot, so a `dependsOn` child inherits *each* named browser
|
|
977
|
+
* exactly where its parent left it (sign both users in once, in the
|
|
978
|
+
* parent); repeat calls with the same name in one test return the same
|
|
979
|
+
* handle; the first call for a name wins its options; `.close()` discards
|
|
980
|
+
* only that name. Sessions are named for the roles under test, not per
|
|
981
|
+
* row of data — each one is a live browser captured in every snapshot
|
|
982
|
+
* from here on, and opening too many is an error.
|
|
983
|
+
*
|
|
984
|
+
* The dashboard titles each step with the browser that performed it
|
|
985
|
+
* (`bob: click "Share"`) and labels the replay with the same name, so a
|
|
986
|
+
* two-user timeline stays readable and selecting a step shows that
|
|
987
|
+
* browser's session.
|
|
988
|
+
*/
|
|
989
|
+
browser(name: string, opts?: BrowserOptions): Promise<Browser>;
|
|
953
990
|
/**
|
|
954
991
|
* Open a phone-emulated session for a mobile app and return a {@link Mobile}
|
|
955
992
|
* handle already pointed at it — no `navigate`. Pass the app handle a
|
|
@@ -968,13 +1005,18 @@ export interface TestContext<P = undefined, S extends ServicesMap = ServicesMap,
|
|
|
968
1005
|
* The session emulates the latest iPhone (viewport + DPR + mobile UA +
|
|
969
1006
|
* touch) and the dashboard replays it inside a phone bezel.
|
|
970
1007
|
*
|
|
971
|
-
* Sessions are persistent, one per app
|
|
972
|
-
* session is captured in the test's snapshot, so a `dependsOn`
|
|
973
|
-
* picks up the app exactly where the parent left it (already signed
|
|
974
|
-
* mid-flow) instead of reloading it. `.close()` discards the session;
|
|
1008
|
+
* Sessions are persistent, one per app per `name`: like `ctx.browser()`,
|
|
1009
|
+
* the live session is captured in the test's snapshot, so a `dependsOn`
|
|
1010
|
+
* child picks up the app exactly where the parent left it (already signed
|
|
1011
|
+
* in, mid-flow) instead of reloading it. `.close()` discards the session;
|
|
975
1012
|
* the next `ctx.mobile(app)` opens the app fresh.
|
|
1013
|
+
*
|
|
1014
|
+
* Pass a `name` for a second phone running the same app — two users in
|
|
1015
|
+
* one test: `ctx.mobile(ctx.svc.app, "alice")`. Names are independent
|
|
1016
|
+
* sessions and are inherited per name by `dependsOn` children, exactly
|
|
1017
|
+
* like named desktop browsers.
|
|
976
1018
|
*/
|
|
977
|
-
mobile(app: MobileApp): Promise<Mobile>;
|
|
1019
|
+
mobile(app: MobileApp, name?: string): Promise<Mobile>;
|
|
978
1020
|
/** The test's display name. */
|
|
979
1021
|
readonly testName: string;
|
|
980
1022
|
/**
|
|
@@ -1038,7 +1080,7 @@ export interface TestSuite<S extends ServicesMap = ServicesMap, F extends FakesM
|
|
|
1038
1080
|
tests: TestCase<unknown, S, F>[];
|
|
1039
1081
|
}
|
|
1040
1082
|
/**
|
|
1041
|
-
* A fake server hosted in the
|
|
1083
|
+
* A fake server hosted in the harness. Use this to stand in for
|
|
1042
1084
|
* external HTTP APIs (auth providers, payment gateways, …) that you can't
|
|
1043
1085
|
* call directly from the hermetic test VM.
|
|
1044
1086
|
*
|
|
@@ -1176,7 +1218,7 @@ export interface TypedTest<S extends ServicesMap, F extends FakesMap = FakesMap>
|
|
|
1176
1218
|
export interface Project<S extends ServicesMap = ServicesMap, F extends FakesMap = FakesMap> {
|
|
1177
1219
|
environment: EnvironmentConfig<S>;
|
|
1178
1220
|
/**
|
|
1179
|
-
* Fake servers hosted in the
|
|
1221
|
+
* Fake servers hosted in the harness (see {@link defineFake}).
|
|
1180
1222
|
* Keyed by stable fake name; the key becomes the slot in
|
|
1181
1223
|
* `ctx.fakes.<key>` from test code. Populated from the `fakes` declared
|
|
1182
1224
|
* in `defineEnvironment(...)`.
|
package/dist/ingress.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Project, ServiceConfig } from "./index.js";
|
|
2
|
-
/** Where a DNS name points. `ingress` = the
|
|
2
|
+
/** Where a DNS name points. `ingress` = the harness's listener on the
|
|
3
3
|
* bridge gateway (fakes, TLS-terminated proxies); `service` = a container's
|
|
4
4
|
* live IP on spectest-net (a plain peer alias, no daemon hop). */
|
|
5
5
|
export type DnsTarget = {
|
package/dist/mobile.d.ts
CHANGED
|
@@ -49,14 +49,18 @@ export declare function openMobile(opts: {
|
|
|
49
49
|
recorder: BrowserSessionRecorder | null;
|
|
50
50
|
}): Promise<Mobile>;
|
|
51
51
|
/**
|
|
52
|
-
* Acquire the persistent phone-emulated session for an app (one per app URL
|
|
53
|
-
* created on first use). The daemon calls this from
|
|
54
|
-
* per-test rrweb recorder; the resulting
|
|
55
|
-
* the dashboard renders a phone bezel.
|
|
56
|
-
* `mobile.close()` destroys the session for
|
|
52
|
+
* Acquire the persistent phone-emulated session for an app (one per app URL
|
|
53
|
+
* per `name`, created on first use). The daemon calls this from
|
|
54
|
+
* `ctx.mobile(app, name?)` with a per-test rrweb recorder; the resulting
|
|
55
|
+
* record carries `frame: "mobile"` so the dashboard renders a phone bezel.
|
|
56
|
+
* `detach` is the test-end hook; `mobile.close()` destroys the session for
|
|
57
|
+
* real.
|
|
57
58
|
*/
|
|
58
59
|
export declare function openPersistentMobile(opts: {
|
|
59
60
|
url: string;
|
|
61
|
+
/** Session name — `""` (the default) is the unnamed session every
|
|
62
|
+
* `ctx.mobile(app)` call shares. Two names on one app are two phones. */
|
|
63
|
+
name?: string;
|
|
60
64
|
recorder: BrowserSessionRecorder | null;
|
|
61
65
|
/** Installed before the fresh session's first navigation (ignored on an
|
|
62
66
|
* attached session, which already carries it on the forked holder). */
|
package/dist/mobile.js
CHANGED
|
@@ -48,14 +48,15 @@ export async function openMobile(opts) {
|
|
|
48
48
|
return backend;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Acquire the persistent phone-emulated session for an app (one per app URL
|
|
52
|
-
* created on first use). The daemon calls this from
|
|
53
|
-
* per-test rrweb recorder; the resulting
|
|
54
|
-
* the dashboard renders a phone bezel.
|
|
55
|
-
* `mobile.close()` destroys the session for
|
|
51
|
+
* Acquire the persistent phone-emulated session for an app (one per app URL
|
|
52
|
+
* per `name`, created on first use). The daemon calls this from
|
|
53
|
+
* `ctx.mobile(app, name?)` with a per-test rrweb recorder; the resulting
|
|
54
|
+
* record carries `frame: "mobile"` so the dashboard renders a phone bezel.
|
|
55
|
+
* `detach` is the test-end hook; `mobile.close()` destroys the session for
|
|
56
|
+
* real.
|
|
56
57
|
*/
|
|
57
58
|
export async function openPersistentMobile(opts) {
|
|
58
|
-
const { browser, attached, detach } = await acquirePersistentMobileBackend(opts.url, opts.recorder, opts.initScript);
|
|
59
|
+
const { browser, attached, detach } = await acquirePersistentMobileBackend(opts.url, opts.name ?? "", opts.recorder, opts.initScript);
|
|
59
60
|
return {
|
|
60
61
|
mobile: browser,
|
|
61
62
|
attached,
|
package/dist/recorder.d.ts
CHANGED
|
@@ -379,6 +379,16 @@ export interface BrowserEvent extends BaseEvent {
|
|
|
379
379
|
* with a `BrowserSessionRecorder` attached (the daemon always does).
|
|
380
380
|
*/
|
|
381
381
|
sessionId?: string;
|
|
382
|
+
/**
|
|
383
|
+
* Author-given name of that session — `ctx.browser("alice")` — absent
|
|
384
|
+
* for the default unnamed browser, so a single-browser test's steps
|
|
385
|
+
* carry no name and render exactly as they always have. Present, it is
|
|
386
|
+
* what the step list badges to say which browser acted. Duplicated here
|
|
387
|
+
* rather than resolved from the session record because the CLI's
|
|
388
|
+
* failure detail renders from persisted events alone, with no replay
|
|
389
|
+
* bundle to look the session up in.
|
|
390
|
+
*/
|
|
391
|
+
sessionName?: string;
|
|
382
392
|
/**
|
|
383
393
|
* Wall-clock `Date.now()` captured at the moment this op *finished*.
|
|
384
394
|
* Lives in the same time base as rrweb's `event.timestamp` fields,
|
package/dist/resolver.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { createSocket } from "node:dgram";
|
|
28
28
|
import { request as httpRequest } from "node:http";
|
|
29
29
|
import { readFile, stat } from "node:fs/promises";
|
|
30
|
+
import { lookup } from "./harness/names-registry.js";
|
|
30
31
|
import * as dnsPacket from "dns-packet";
|
|
31
32
|
const NETWORK_NAME = process.env.SPECTEST_NETWORK ?? "spectest-net";
|
|
32
33
|
const DOCKER_SOCKET = process.env.DOCKER_SOCKET ?? "/var/run/docker.sock";
|
|
@@ -37,7 +38,7 @@ const UPSTREAM_PORT = Number(process.env.SPECTEST_UPSTREAM_PORT ?? "53");
|
|
|
37
38
|
const LISTEN_ADDR = process.env.SPECTEST_RESOLVER_ADDR ?? "127.0.0.53";
|
|
38
39
|
const LISTEN_PORT = Number(process.env.SPECTEST_RESOLVER_PORT ?? "53");
|
|
39
40
|
const TTL_SECONDS = Number(process.env.SPECTEST_RESOLVER_TTL ?? "5");
|
|
40
|
-
/** Path the
|
|
41
|
+
/** Path the harness writes when it brings fakes up. */
|
|
41
42
|
const FAKES_REGISTRY_PATH = process.env.SPECTEST_FAKES_REGISTRY ?? "/run/spectest-fakes.json";
|
|
42
43
|
function dockerGet(path) {
|
|
43
44
|
return new Promise((resolve) => {
|
|
@@ -129,13 +130,9 @@ async function lookupFake(name) {
|
|
|
129
130
|
* longest (most specific) matching suffix wins. */
|
|
130
131
|
async function lookupWildcard(name) {
|
|
131
132
|
await refreshRegistry();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
best = w;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return best?.ip ?? null;
|
|
133
|
+
// Same module the harness writes with, so the two halves of this file
|
|
134
|
+
// format cannot drift apart.
|
|
135
|
+
return lookup({ hosts: {}, wildcards: registryCache.wildcards }, name);
|
|
139
136
|
}
|
|
140
137
|
async function forwardUpstream(query) {
|
|
141
138
|
return new Promise((resolve) => {
|