@specific.dev/spectest 0.31.0 → 0.32.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 +5 -3
- package/dist/components/index.d.ts +1 -1
- package/dist/components/k3s.d.ts +175 -12
- package/dist/components/k3s.js +403 -62
- package/dist/daemon.js +182 -109
- package/dist/index.d.ts +251 -217
- package/dist/recorder.d.ts +13 -2
- package/dist/recorder.js +13 -0
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -32,7 +32,7 @@ import { readFileSync } from "node:fs";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { generateId } from "./ids.js";
|
|
35
|
-
import { recordBrowser, reserveEvent, truncateUtf8 } from "./recorder.js";
|
|
35
|
+
import { recordBrowser, reserveBackdated, reserveEvent, truncateUtf8 } from "./recorder.js";
|
|
36
36
|
import { wrap } from "./inspect.js";
|
|
37
37
|
import { describeUrlPattern, matchesUrl } from "./url-match.js";
|
|
38
38
|
import { attachBrowserProbe, DEFAULT_ACTION_TIMEOUT_MS, desktopStrategy, makeLocator, mobileStrategy, } from "./locator.js";
|
|
@@ -1407,7 +1407,9 @@ function buildBackend(holder, recorder, buildOpts) {
|
|
|
1407
1407
|
// No page work — the matcher already read the value via silentRead. We
|
|
1408
1408
|
// only mint the timeline anchor: the seq the assertion nests under, plus
|
|
1409
1409
|
// `sessionTimestamp` (post-settle wall clock) so the dashboard seeks the
|
|
1410
|
-
// replay to the frame the assertion observed.
|
|
1410
|
+
// replay to the frame the assertion observed. The offset is backdated to
|
|
1411
|
+
// where the matcher started polling, since that's what `tOffsetMs` means
|
|
1412
|
+
// everywhere else (the ops that reserve up front stamp their start).
|
|
1411
1413
|
const endT = Date.now();
|
|
1412
1414
|
const seq = recordBrowser({
|
|
1413
1415
|
action,
|
|
@@ -1417,7 +1419,7 @@ function buildBackend(holder, recorder, buildOpts) {
|
|
|
1417
1419
|
: {}),
|
|
1418
1420
|
durationMs: waitedMs,
|
|
1419
1421
|
...(error ? { error } : {}),
|
|
1420
|
-
});
|
|
1422
|
+
}, reserveBackdated(waitedMs));
|
|
1421
1423
|
// Drain the rrweb the page buffered while the matcher waited into this
|
|
1422
1424
|
// step's chunk, so `settledTarget` has bounds to seek into.
|
|
1423
1425
|
await drain(action);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { postgres, type PostgresOptions, type PostgresHelpers, } from "./postgres.js";
|
|
2
2
|
export { s3, type S3Options, type S3Helpers, } from "./s3.js";
|
|
3
3
|
export { SQL, type SqlClient, type SqlOptions } from "../sql.js";
|
|
4
|
-
export { k3s, type K3sOptions, type K3sHelpers, type K3sClient, } from "./k3s.js";
|
|
4
|
+
export { k3s, type K3sOptions, type K3sHelpers, type K3sClient, type TaggedObjectApi, type KubernetesObjectRef, type ApplyOptions, type RolloutTarget, type WaitOptions, type LogsOptions, type KubernetesObject, type KubernetesListObject, } from "./k3s.js";
|
|
5
5
|
export { expo, type ExpoOptions, type ExpoHelpers, } from "./expo.js";
|
|
6
6
|
export { supabase, type SupabaseOptions, type SupabaseHelpers, type SupabaseStack, } from "./supabase.js";
|
|
7
7
|
export { email, type EmailOptions, type EmailHelpers, type EmailMessage, type EmailSummary, type EmailMatch, type EmailAttachment, } from "./email.js";
|
package/dist/components/k3s.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AppsV1Api, CoreV1Api, KubeConfig,
|
|
1
|
+
import { AppsV1Api, BatchV1Api, CoreV1Api, KubeConfig, PatchStrategy, type KubernetesListObject, type KubernetesObject, type V1DeleteOptions, type V1Job, type V1Status } from "@kubernetes/client-node";
|
|
2
2
|
import type { ServiceHelpersContext } from "../index.js";
|
|
3
3
|
import type { Wrapped } from "../inspect.js";
|
|
4
|
+
export type { KubernetesObject, KubernetesListObject, } from "@kubernetes/client-node";
|
|
4
5
|
export interface K3sOptions {
|
|
5
6
|
/** Image tag for the official `rancher/k3s` image. Default `"v1.30.6-k3s1"`. */
|
|
6
7
|
version?: string;
|
|
@@ -109,6 +110,50 @@ export interface K3sOptions {
|
|
|
109
110
|
type Tagged<T> = {
|
|
110
111
|
[K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => Promise<Wrapped<R>> : T[K];
|
|
111
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* `{ apiVersion, kind, metadata: { name } }` — enough to address an
|
|
115
|
+
* existing object. Redeclared here because `@kubernetes/client-node`
|
|
116
|
+
* keeps its equivalent (`KubernetesObjectHeader`) module-private.
|
|
117
|
+
*/
|
|
118
|
+
export type KubernetesObjectRef<T extends KubernetesObject = KubernetesObject> = Pick<T, "apiVersion" | "kind"> & {
|
|
119
|
+
metadata: {
|
|
120
|
+
name: string;
|
|
121
|
+
namespace?: string;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* The dynamic object API, hand-declared rather than derived through
|
|
126
|
+
* {@link Tagged}.
|
|
127
|
+
*
|
|
128
|
+
* `Tagged<T>` infers each method from its *erased* signature, and a mapped
|
|
129
|
+
* type can't reintroduce a type parameter (TypeScript has no higher-kinded
|
|
130
|
+
* types). For `CoreV1Api`/`AppsV1Api` that's invisible — their methods
|
|
131
|
+
* return concrete types — but `KubernetesObjectApi.read<T>`/`list<T>` are
|
|
132
|
+
* generic, so mapping them collapsed `read<MyCustomResource>(…)` to
|
|
133
|
+
* `Promise<Wrapped<KubernetesObject>>`: `Expected 0 type arguments, but got
|
|
134
|
+
* 1`, then `Property 'spec' does not exist on type 'KubernetesObject'` —
|
|
135
|
+
* precisely on the custom resources this API exists to reach. Worse, the
|
|
136
|
+
* degraded value made `expect(...)` resolve to the *locator* overload, so
|
|
137
|
+
* the reported error was a baffling `Property 'toBe' does not exist on type
|
|
138
|
+
* 'LocatorAssertion'` three lines further down.
|
|
139
|
+
*
|
|
140
|
+
* So the generic methods are written out. Arguments match the library's
|
|
141
|
+
* (positional, as it declares them); only the resolved value becomes
|
|
142
|
+
* {@link Wrapped}. See {@link K3sHelpers.list} for an options-object
|
|
143
|
+
* wrapper over the positional `list`.
|
|
144
|
+
*/
|
|
145
|
+
export interface TaggedObjectApi {
|
|
146
|
+
create<T extends KubernetesObject>(spec: T, pretty?: string, dryRun?: string, fieldManager?: string): Promise<Wrapped<T>>;
|
|
147
|
+
patch<T extends KubernetesObject>(spec: T, pretty?: string, dryRun?: string, fieldManager?: string, force?: boolean, patchStrategy?: PatchStrategy): Promise<Wrapped<T>>;
|
|
148
|
+
/** Read one object. **Throws** on 404 — to assert something is *gone*,
|
|
149
|
+
* prefer {@link K3sHelpers.list} with a `metadata.name` field selector
|
|
150
|
+
* and assert the returned `items` are empty (a throw carries no value
|
|
151
|
+
* to assert on, and no provenance link). */
|
|
152
|
+
read<T extends KubernetesObject>(spec: KubernetesObjectRef<T>, pretty?: string, exact?: boolean, exportt?: boolean): Promise<Wrapped<T>>;
|
|
153
|
+
replace<T extends KubernetesObject>(spec: T, pretty?: string, dryRun?: string, fieldManager?: string): Promise<Wrapped<T>>;
|
|
154
|
+
delete(spec: KubernetesObject, pretty?: string, dryRun?: string, gracePeriodSeconds?: number, orphanDependents?: boolean, propagationPolicy?: string, body?: V1DeleteOptions): Promise<Wrapped<V1Status>>;
|
|
155
|
+
list<T extends KubernetesObject>(apiVersion: string, kind: string, namespace?: string, pretty?: string, exact?: boolean, exportt?: boolean, fieldSelector?: string, labelSelector?: string, limit?: number, continueToken?: string): Promise<Wrapped<KubernetesListObject<T>>>;
|
|
156
|
+
}
|
|
112
157
|
/**
|
|
113
158
|
* Pre-instantiated `@kubernetes/client-node` API clients sharing the
|
|
114
159
|
* same recording HTTP transport. Every method call lands on the test
|
|
@@ -118,9 +163,55 @@ type Tagged<T> = {
|
|
|
118
163
|
export interface K3sClient {
|
|
119
164
|
core: Tagged<CoreV1Api>;
|
|
120
165
|
apps: Tagged<AppsV1Api>;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
166
|
+
batch: Tagged<BatchV1Api>;
|
|
167
|
+
/** Generic object API — `create()`, `read()`, `patch()`, `delete()`,
|
|
168
|
+
* `list()` against any Kubernetes resource, custom resources included.
|
|
169
|
+
* Generic in the resource type: `objects.read<MyCr>({ … })` resolves to
|
|
170
|
+
* `Wrapped<MyCr>`. */
|
|
171
|
+
objects: TaggedObjectApi;
|
|
172
|
+
}
|
|
173
|
+
/** Options for {@link K3sHelpers.apply}. */
|
|
174
|
+
export interface ApplyOptions {
|
|
175
|
+
/** Namespace for documents that don't declare one (cluster-scoped kinds
|
|
176
|
+
* are unaffected). Defaults to `default`, like `kubectl`. */
|
|
177
|
+
namespace?: string;
|
|
178
|
+
/** Server-side-apply field manager. Default `"spectest"`. Keep it stable
|
|
179
|
+
* across runs — that's what makes re-applying an *update* rather than a
|
|
180
|
+
* conflict. */
|
|
181
|
+
fieldManager?: string;
|
|
182
|
+
/** Take ownership of fields another manager owns, i.e. `kubectl apply
|
|
183
|
+
* --force-conflicts`. Default `true`: a test environment has one owner
|
|
184
|
+
* and a conflict is noise. */
|
|
185
|
+
force?: boolean;
|
|
186
|
+
/** Total budget for converging the whole manifest set, including retries
|
|
187
|
+
* of documents whose CRD/webhook isn't up yet. Default 120 s. */
|
|
188
|
+
timeoutMs?: number;
|
|
189
|
+
}
|
|
190
|
+
/** A workload to wait on: `"web"`, `"deployment/web"`,
|
|
191
|
+
* `"statefulset/db"`, `"daemonset/agent"`, or an object with
|
|
192
|
+
* `kind`/`metadata` (e.g. an element of {@link K3sHelpers.apply}'s
|
|
193
|
+
* result). Bare names are Deployments. */
|
|
194
|
+
export type RolloutTarget = string | KubernetesObject | KubernetesObjectRef;
|
|
195
|
+
/** Options for the wait helpers. */
|
|
196
|
+
export interface WaitOptions {
|
|
197
|
+
/** Namespace of the object. Default `default`, or the object's own. */
|
|
198
|
+
namespace?: string;
|
|
199
|
+
/** Give up after this long. Default 120 s. */
|
|
200
|
+
timeoutMs?: number;
|
|
201
|
+
/** Poll interval. Default 500 ms. */
|
|
202
|
+
intervalMs?: number;
|
|
203
|
+
}
|
|
204
|
+
/** Which pods to read logs from — see {@link K3sHelpers.logs}. */
|
|
205
|
+
export interface LogsOptions {
|
|
206
|
+
/** A single pod by name. */
|
|
207
|
+
pod?: string;
|
|
208
|
+
/** All pods matching a label selector (`"app=web"`). */
|
|
209
|
+
selector?: string;
|
|
210
|
+
namespace?: string;
|
|
211
|
+
/** Container within the pod (required only for multi-container pods). */
|
|
212
|
+
container?: string;
|
|
213
|
+
/** Tail this many lines per pod. Default 200. */
|
|
214
|
+
tailLines?: number;
|
|
124
215
|
}
|
|
125
216
|
/** Helpers a `k3s(...)` service exposes on `ctx.svc.<name>`. */
|
|
126
217
|
export interface K3sHelpers {
|
|
@@ -136,13 +227,86 @@ export interface K3sHelpers {
|
|
|
136
227
|
/** Pre-built API clients. */
|
|
137
228
|
client: K3sClient;
|
|
138
229
|
/**
|
|
139
|
-
* Apply a (multi-document) YAML manifest
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
230
|
+
* Apply a (multi-document) YAML manifest with **server-side apply**
|
|
231
|
+
* semantics — the API equivalent of `kubectl apply --server-side
|
|
232
|
+
* --force-conflicts`. Each document is `PATCH`ed with
|
|
233
|
+
* `application/apply-patch+yaml`, so applying is create-or-update:
|
|
234
|
+
* re-applying an edited manifest converges instead of failing
|
|
235
|
+
* `AlreadyExists`, which is what makes it usable for real manifest sets
|
|
236
|
+
* (and for a `dependsOn` child re-applying over its parent's state).
|
|
237
|
+
*
|
|
238
|
+
* Documents that can't land *yet* are retried until `opts.timeoutMs`:
|
|
239
|
+
* a CR whose CRD is in the same manifest, or a resource an admission
|
|
240
|
+
* webhook rejects while its own pod is still coming up, both resolve on
|
|
241
|
+
* a later round instead of failing the run. A document that keeps
|
|
242
|
+
* failing throws with the API server's own message.
|
|
243
|
+
*
|
|
244
|
+
* Returns the API server's response objects in input order — each
|
|
245
|
+
* element inspect-wrapped (the array container itself is plain), so
|
|
246
|
+
* `expect(applied[0]!.metadata.uid)` links to its apply call.
|
|
247
|
+
*
|
|
248
|
+
* Nothing here shells out to `kubectl`; the cluster is driven over its
|
|
249
|
+
* API from the daemon.
|
|
250
|
+
*/
|
|
251
|
+
apply(manifest: string, opts?: ApplyOptions): Promise<Wrapped<KubernetesObject>[]>;
|
|
252
|
+
/**
|
|
253
|
+
* Wait until a Deployment / StatefulSet / DaemonSet has actually rolled
|
|
254
|
+
* out: the controller has observed the current generation, every replica
|
|
255
|
+
* is updated, and the desired number are Ready (the checks `kubectl
|
|
256
|
+
* rollout status` makes). Throws on timeout with the last-seen status
|
|
257
|
+
* plus pod phases, container reasons and recent Warning events from the
|
|
258
|
+
* namespace, so a stuck image pull or CrashLoop is readable from the
|
|
259
|
+
* failure alone.
|
|
260
|
+
*
|
|
261
|
+
* ```ts
|
|
262
|
+
* await ctx.svc.k8s.apply(manifest);
|
|
263
|
+
* await ctx.svc.k8s.waitForRollout("web");
|
|
264
|
+
* await ctx.svc.k8s.waitForRollout("statefulset/db", { namespace: "data" });
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
waitForRollout(target: RolloutTarget, opts?: WaitOptions): Promise<void>;
|
|
268
|
+
/**
|
|
269
|
+
* Wait for a Job to finish. Resolves with the completed Job (Complete
|
|
270
|
+
* condition, or `succeeded` at the requested completion count) and
|
|
271
|
+
* throws if it fails — with the failed pods' logs attached, which is
|
|
272
|
+
* the thing you actually want when a migration or seed Job dies.
|
|
273
|
+
*
|
|
274
|
+
* ```ts
|
|
275
|
+
* await ctx.svc.k8s.apply(migrateJob);
|
|
276
|
+
* const job = await ctx.svc.k8s.waitForJob("migrate");
|
|
277
|
+
* expect(job.status?.succeeded).toBe(1);
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
waitForJob(name: string, opts?: WaitOptions): Promise<Wrapped<V1Job>>;
|
|
281
|
+
/**
|
|
282
|
+
* Read pod logs — one pod by `pod`, or every pod matching `selector`
|
|
283
|
+
* (concatenated, each preceded by a `==> <pod> <==` header). The
|
|
284
|
+
* diagnostic companion to the wait helpers.
|
|
285
|
+
*/
|
|
286
|
+
logs(opts: LogsOptions): Promise<Wrapped<string>>;
|
|
287
|
+
/**
|
|
288
|
+
* List objects of any kind by options object rather than the library's
|
|
289
|
+
* ten positional parameters (`list(apiVersion, kind, namespace, pretty,
|
|
290
|
+
* exact, exportt, fieldSelector, …)`). Generic in the resource type, so
|
|
291
|
+
* custom resources keep their shape:
|
|
292
|
+
*
|
|
293
|
+
* ```ts
|
|
294
|
+
* // "the Branch is gone" — without a 404 throw to catch
|
|
295
|
+
* const left = await ctx.svc.k8s.list<Branch>({
|
|
296
|
+
* apiVersion: "xata.io/v1", kind: "Branch",
|
|
297
|
+
* fieldSelector: `metadata.name=${name}`,
|
|
298
|
+
* });
|
|
299
|
+
* expect(left.items).toHaveLength(0);
|
|
300
|
+
* ```
|
|
144
301
|
*/
|
|
145
|
-
|
|
302
|
+
list<T extends KubernetesObject = KubernetesObject>(opts: {
|
|
303
|
+
apiVersion: string;
|
|
304
|
+
kind: string;
|
|
305
|
+
namespace?: string;
|
|
306
|
+
fieldSelector?: string;
|
|
307
|
+
labelSelector?: string;
|
|
308
|
+
limit?: number;
|
|
309
|
+
}): Promise<Wrapped<KubernetesListObject<T>>>;
|
|
146
310
|
}
|
|
147
311
|
export declare function k3s(opts?: K3sOptions): {
|
|
148
312
|
readyCheck: {
|
|
@@ -154,7 +318,7 @@ export declare function k3s(opts?: K3sOptions): {
|
|
|
154
318
|
name: string;
|
|
155
319
|
helpers: K3sHelpers;
|
|
156
320
|
}) => Promise<void>;
|
|
157
|
-
helpers: ({ name, exec }: ServiceHelpersContext) => Promise<K3sHelpers>;
|
|
321
|
+
helpers: ({ name, exec, poll }: ServiceHelpersContext) => Promise<K3sHelpers>;
|
|
158
322
|
files?: {
|
|
159
323
|
path: string;
|
|
160
324
|
content: string;
|
|
@@ -169,4 +333,3 @@ export declare function k3s(opts?: K3sOptions): {
|
|
|
169
333
|
cgroupns: string;
|
|
170
334
|
ports: number[];
|
|
171
335
|
};
|
|
172
|
-
export {};
|