@specific.dev/spectest 0.39.0 → 0.41.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/components/supabase.d.ts +87 -27
- package/dist/components/supabase.js +352 -69
- package/dist/daemon.d.ts +38 -0
- package/dist/daemon.js +388 -941
- 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 +3 -3
- package/dist/ingress.d.ts +1 -1
- 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 +2040 -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 +5489 -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 +2725 -0
- package/src/ingress.ts +305 -0
- package/src/inspect.ts +739 -0
- package/src/locator.ts +716 -0
- package/src/mobile.ts +133 -0
- package/src/record-secrets.ts +41 -0
- package/src/recorder.ts +846 -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/src/mobile.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Mobile app handle for tests — drives an Expo/React-Native-Web app rendered
|
|
2
|
+
// in a phone-emulated headless Chromium and recorded as an rrweb session that
|
|
3
|
+
// the dashboard replays inside a phone bezel.
|
|
4
|
+
//
|
|
5
|
+
// `ctx.mobile(ctx.svc.app)` opens one of these already pointed at the app. The
|
|
6
|
+
// surface is the SAME Playwright-native one as `ctx.browser()` (the shared
|
|
7
|
+
// `Browser` session + `Locator`s from browser.ts/locator.ts) plus two mobile
|
|
8
|
+
// extensions: a `touchscreen` and `swipe`. There is no mobile-specific locator
|
|
9
|
+
// type — `getByTestId`/`getByRole`/`getByText`/… return the same `Locator`,
|
|
10
|
+
// and on a mobile session a locator's `tap()` dispatches a real CDP touch with
|
|
11
|
+
// the press dwell RN Pressables need (the `mobileStrategy` in locator.ts;
|
|
12
|
+
// desktop `tap()` throws and steers you to `click()`).
|
|
13
|
+
//
|
|
14
|
+
// This file is now just the `MobileApp` handle (`ctx.mobile` accepts it) and
|
|
15
|
+
// the `Mobile` public view of the shared backend. React Native Web renders
|
|
16
|
+
// `testID="x"` to `data-testid="x"` and `accessibilityLabel` to `aria-label`,
|
|
17
|
+
// so `getByTestId`/`getByLabel` map straight onto Playwright's built-ins.
|
|
18
|
+
|
|
19
|
+
import { acquirePersistentMobileBackend, openMobileBackend } from "./browser.js";
|
|
20
|
+
import type {
|
|
21
|
+
Browser,
|
|
22
|
+
BrowserSessionRecorder,
|
|
23
|
+
SafeAreaInsets,
|
|
24
|
+
Touchscreen,
|
|
25
|
+
} from "./browser.js";
|
|
26
|
+
|
|
27
|
+
/** Branded handle a mobile-app component (e.g. `expo()`) exposes on
|
|
28
|
+
* `ctx.svc.<name>`. The brand is a `Symbol.for` key so `JSON.stringify`
|
|
29
|
+
* drops it (wire-invisible) while `ctx.mobile(...)` can still type-check
|
|
30
|
+
* against it. */
|
|
31
|
+
export const MOBILE_APP: unique symbol = Symbol.for("spectest.mobileApp");
|
|
32
|
+
|
|
33
|
+
/** What `ctx.mobile(...)` accepts — produced by a mobile-app component's
|
|
34
|
+
* `helpers` factory. Carries the in-VM URL the session auto-navigates to. */
|
|
35
|
+
export interface MobileApp {
|
|
36
|
+
readonly [MOBILE_APP]: true;
|
|
37
|
+
/** Resolved in-VM URL of the app's web build (e.g. `http://app.internal:8081`). */
|
|
38
|
+
readonly url: string;
|
|
39
|
+
/** Optional script installed before the session's first navigation, so it
|
|
40
|
+
* runs ahead of the app bundle on the very first document (no relaunch) —
|
|
41
|
+
* the place to plant reduced-motion / `Notification` shims once for every
|
|
42
|
+
* `ctx.mobile(app)` call. Set via `expo({ initScript })`. */
|
|
43
|
+
readonly initScript?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** True if `x` is a {@link MobileApp} handle. */
|
|
47
|
+
export function isMobileApp(x: unknown): x is MobileApp {
|
|
48
|
+
return (
|
|
49
|
+
typeof x === "object" &&
|
|
50
|
+
x !== null &&
|
|
51
|
+
(x as Record<symbol, unknown>)[MOBILE_APP] === true &&
|
|
52
|
+
typeof (x as { url?: unknown }).url === "string"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build a {@link MobileApp} handle from a resolved URL (and an optional
|
|
57
|
+
* init script installed before the session's first navigation). */
|
|
58
|
+
export function mobileApp(url: string, initScript?: string): MobileApp {
|
|
59
|
+
return { [MOBILE_APP]: true, url, initScript };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
63
|
+
// Mobile session
|
|
64
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A phone-emulated app session. The full {@link Browser} surface (locators,
|
|
68
|
+
* `keyboard`/`mouse`, `evaluate`, …) plus a `touchscreen` and `swipe`. Opened
|
|
69
|
+
* via `ctx.mobile(app)` already on the app.
|
|
70
|
+
*
|
|
71
|
+
* The device is fixed (latest iPhone: viewport, DPR, mobile UA, touch). A
|
|
72
|
+
* locator's `tap()` on this session uses a real CDP touch with the RN press
|
|
73
|
+
* dwell; `getByTestId` targets `testID`, `getByLabel` targets
|
|
74
|
+
* `accessibilityLabel`.
|
|
75
|
+
*/
|
|
76
|
+
export interface Mobile extends Browser {
|
|
77
|
+
/** Coordinate touch taps (mobile escape hatch — prefer locator `tap()`). */
|
|
78
|
+
readonly touchscreen: Touchscreen;
|
|
79
|
+
/** Swipe the screen in a direction (a touch drag from the center). */
|
|
80
|
+
swipe(direction: "up" | "down" | "left" | "right", opts?: { distance?: number }): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Open an EPHEMERAL phone-emulated session pointed at `url` (`close()`
|
|
85
|
+
* destroys it). Library callers only — the daemon's `ctx.mobile(app)` goes
|
|
86
|
+
* through {@link openPersistentMobile} so sessions survive across tests.
|
|
87
|
+
*/
|
|
88
|
+
export async function openMobile(opts: {
|
|
89
|
+
url?: string;
|
|
90
|
+
recorder: BrowserSessionRecorder | null;
|
|
91
|
+
}): Promise<Mobile> {
|
|
92
|
+
const backend = await openMobileBackend({
|
|
93
|
+
frame: "mobile",
|
|
94
|
+
url: opts.url,
|
|
95
|
+
recorder: opts.recorder,
|
|
96
|
+
});
|
|
97
|
+
// MobileBackend is a superset of Mobile (adds the low-level primitives).
|
|
98
|
+
return backend;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Acquire the persistent phone-emulated session for an app (one per app URL,
|
|
103
|
+
* created on first use). The daemon calls this from `ctx.mobile(app)` with a
|
|
104
|
+
* per-test rrweb recorder; the resulting record carries `frame: "mobile"` so
|
|
105
|
+
* the dashboard renders a phone bezel. `detach` is the test-end hook;
|
|
106
|
+
* `mobile.close()` destroys the session for real.
|
|
107
|
+
*/
|
|
108
|
+
export async function openPersistentMobile(opts: {
|
|
109
|
+
url: string;
|
|
110
|
+
recorder: BrowserSessionRecorder | null;
|
|
111
|
+
/** Installed before the fresh session's first navigation (ignored on an
|
|
112
|
+
* attached session, which already carries it on the forked holder). */
|
|
113
|
+
initScript?: string;
|
|
114
|
+
}): Promise<{
|
|
115
|
+
mobile: Mobile;
|
|
116
|
+
attached: boolean;
|
|
117
|
+
detach(): Promise<void>;
|
|
118
|
+
/** Safe-area insets emulated on the view (`null` when the CDP override
|
|
119
|
+
* is unavailable) — the daemon stamps them onto the session record. */
|
|
120
|
+
safeAreaInsets: SafeAreaInsets | null;
|
|
121
|
+
}> {
|
|
122
|
+
const { browser, attached, detach } = await acquirePersistentMobileBackend(
|
|
123
|
+
opts.url,
|
|
124
|
+
opts.recorder,
|
|
125
|
+
opts.initScript,
|
|
126
|
+
);
|
|
127
|
+
return {
|
|
128
|
+
mobile: browser,
|
|
129
|
+
attached,
|
|
130
|
+
detach,
|
|
131
|
+
safeAreaInsets: browser.safeAreaInsets,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Eval-scoped secret channel for record-mode fakes (see
|
|
2
|
+
// `components/replayFake.ts`).
|
|
3
|
+
//
|
|
4
|
+
// The control plane resolves a platform secret (e.g. `STRIPE_API_KEY`)
|
|
5
|
+
// server-side and pushes it into the daemon on the `/eval` request body
|
|
6
|
+
// only — never on the `run_tests` path, never into the project tarball,
|
|
7
|
+
// the config hash, or any cassette. The daemon stashes the resolved
|
|
8
|
+
// values here for exactly the duration of one eval (set before the
|
|
9
|
+
// snippet runs, cleared in the eval's `finally`) and the record-mode fake
|
|
10
|
+
// forwarder reads them via {@link getRecordSecret}.
|
|
11
|
+
//
|
|
12
|
+
// Eval-scoped is load-bearing: daemon memory survives snapshot/fork, so a
|
|
13
|
+
// long-lived secrets map could ride a warm/post-test snapshot into a
|
|
14
|
+
// forkable state a hermetic `spectest test` could observe. Clearing per
|
|
15
|
+
// eval means a secret never persists into anything a replay run can reach.
|
|
16
|
+
// This module is shared (one instance per daemon process) so the daemon
|
|
17
|
+
// writes and the component reads the same map.
|
|
18
|
+
|
|
19
|
+
const SECRETS = new Map<string, string>();
|
|
20
|
+
|
|
21
|
+
/** Replace the eval-scoped secret set. Called by the daemon at the start
|
|
22
|
+
* of each `/eval` with the values the control plane resolved. */
|
|
23
|
+
export function setRecordSecrets(secrets: Record<string, string> | undefined): void {
|
|
24
|
+
SECRETS.clear();
|
|
25
|
+
if (!secrets) return;
|
|
26
|
+
for (const [ref, value] of Object.entries(secrets)) {
|
|
27
|
+
if (typeof value === "string") SECRETS.set(ref, value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Drop all secrets. Called from the eval's `finally` so nothing survives
|
|
32
|
+
* past the eval that supplied them. */
|
|
33
|
+
export function clearRecordSecrets(): void {
|
|
34
|
+
SECRETS.clear();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Resolve a secret by its platform `ref`. `undefined` if the control
|
|
38
|
+
* plane didn't supply it (ref not configured, or not on the eval path). */
|
|
39
|
+
export function getRecordSecret(ref: string): string | undefined {
|
|
40
|
+
return SECRETS.get(ref);
|
|
41
|
+
}
|