@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.
Files changed (100) hide show
  1. package/dist/components/supabase.d.ts +87 -27
  2. package/dist/components/supabase.js +352 -69
  3. package/dist/daemon.d.ts +38 -0
  4. package/dist/daemon.js +388 -941
  5. package/dist/harness/build-context.d.ts +82 -0
  6. package/dist/harness/build-context.js +113 -0
  7. package/dist/harness/buildkit-progress.d.ts +37 -0
  8. package/dist/harness/buildkit-progress.js +66 -0
  9. package/dist/harness/container-run.d.ts +89 -0
  10. package/dist/harness/container-run.js +118 -0
  11. package/dist/harness/file-mounts.d.ts +91 -0
  12. package/dist/harness/file-mounts.js +119 -0
  13. package/dist/harness/hostmatch.d.ts +65 -0
  14. package/dist/harness/hostmatch.js +108 -0
  15. package/dist/harness/http-proxy.d.ts +62 -0
  16. package/dist/harness/http-proxy.js +104 -0
  17. package/dist/harness/ingress-table.d.ts +148 -0
  18. package/dist/harness/ingress-table.js +129 -0
  19. package/dist/harness/log-delta.d.ts +54 -0
  20. package/dist/harness/log-delta.js +83 -0
  21. package/dist/harness/main.d.ts +47 -0
  22. package/dist/harness/main.js +164 -0
  23. package/dist/harness/methods.d.ts +54 -0
  24. package/dist/harness/methods.js +65 -0
  25. package/dist/harness/names-registry.d.ts +63 -0
  26. package/dist/harness/names-registry.js +90 -0
  27. package/dist/harness/protocol.d.ts +88 -0
  28. package/dist/harness/protocol.js +96 -0
  29. package/dist/harness/ready-poll.d.ts +47 -0
  30. package/dist/harness/ready-poll.js +67 -0
  31. package/dist/harness/service-graph.d.ts +29 -0
  32. package/dist/harness/service-graph.js +92 -0
  33. package/dist/harness/volume-paths.d.ts +70 -0
  34. package/dist/harness/volume-paths.js +81 -0
  35. package/dist/index.d.ts +3 -3
  36. package/dist/ingress.d.ts +1 -1
  37. package/dist/resolver.js +5 -8
  38. package/dist/vendor/rrweb-plugin-console-record.umd.js +521 -0
  39. package/dist/vendor/rrweb-record.min.js +5061 -0
  40. package/package.json +7 -1
  41. package/src/aws-sigv4.ts +218 -0
  42. package/src/browser.ts +2040 -0
  43. package/src/components/aws.ts +554 -0
  44. package/src/components/email.ts +398 -0
  45. package/src/components/expo.ts +167 -0
  46. package/src/components/index.ts +81 -0
  47. package/src/components/k3s.ts +2061 -0
  48. package/src/components/postgres.ts +132 -0
  49. package/src/components/replayFake.ts +1015 -0
  50. package/src/components/s3.ts +132 -0
  51. package/src/components/supabase.ts +1699 -0
  52. package/src/daemon.ts +5489 -0
  53. package/src/harness/build-context.test.ts +0 -0
  54. package/src/harness/build-context.ts +146 -0
  55. package/src/harness/buildkit-progress.test.ts +98 -0
  56. package/src/harness/buildkit-progress.ts +74 -0
  57. package/src/harness/container-run.test.ts +209 -0
  58. package/src/harness/container-run.ts +158 -0
  59. package/src/harness/file-mounts.test.ts +185 -0
  60. package/src/harness/file-mounts.ts +145 -0
  61. package/src/harness/hostmatch.test.ts +148 -0
  62. package/src/harness/hostmatch.ts +109 -0
  63. package/src/harness/http-proxy.test.ts +156 -0
  64. package/src/harness/http-proxy.ts +119 -0
  65. package/src/harness/ingress-rebind.test.ts +125 -0
  66. package/src/harness/ingress-table.test.ts +172 -0
  67. package/src/harness/ingress-table.ts +186 -0
  68. package/src/harness/log-delta.test.ts +125 -0
  69. package/src/harness/log-delta.ts +100 -0
  70. package/src/harness/main.test.ts +211 -0
  71. package/src/harness/main.ts +196 -0
  72. package/src/harness/methods.test.ts +63 -0
  73. package/src/harness/methods.ts +92 -0
  74. package/src/harness/names-registry.test.ts +137 -0
  75. package/src/harness/names-registry.ts +108 -0
  76. package/src/harness/protocol.test.ts +148 -0
  77. package/src/harness/protocol.ts +163 -0
  78. package/src/harness/ready-poll.test.ts +172 -0
  79. package/src/harness/ready-poll.ts +93 -0
  80. package/src/harness/service-graph.test.ts +97 -0
  81. package/src/harness/service-graph.ts +97 -0
  82. package/src/harness/volume-paths.test.ts +102 -0
  83. package/src/harness/volume-paths.ts +112 -0
  84. package/src/ids.ts +89 -0
  85. package/src/index.ts +2725 -0
  86. package/src/ingress.ts +305 -0
  87. package/src/inspect.ts +739 -0
  88. package/src/locator.ts +716 -0
  89. package/src/mobile.ts +133 -0
  90. package/src/record-secrets.ts +41 -0
  91. package/src/recorder.ts +846 -0
  92. package/src/redis.ts +202 -0
  93. package/src/replay-bundle.ts +108 -0
  94. package/src/resolver.ts +348 -0
  95. package/src/s3.ts +333 -0
  96. package/src/sql.ts +243 -0
  97. package/src/terminal.ts +740 -0
  98. package/src/url-match.ts +67 -0
  99. package/src/vendor/rrweb-plugin-console-record.umd.js +521 -0
  100. 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
+ }