@specific.dev/spectest 0.128.0 → 0.129.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/README.md +54 -0
- package/dist/browser.d.ts +12 -5
- package/dist/browser.js +13 -13
- package/dist/index.d.ts +2 -2
- package/dist/recorder.d.ts +29 -2
- package/dist/terminal.d.ts +31 -17
- package/dist/terminal.js +261 -105
- package/package.json +1 -1
- package/src/browser-navigation.test.ts +36 -0
- package/src/browser.ts +26 -18
- package/src/index.ts +2 -2
- package/src/recorder.ts +22 -2
- package/src/terminal.test.ts +324 -0
- package/src/terminal.ts +269 -119
package/README.md
CHANGED
|
@@ -24,6 +24,60 @@ work; do not replace readiness with sleeps or arbitrary `.first()` selections.
|
|
|
24
24
|
SDK 0.123.0 uses native Playwright assertions while retaining Spectest's API and
|
|
25
25
|
recording. Ambiguous single-element selectors fail immediately; make them unique.
|
|
26
26
|
|
|
27
|
+
For terminal readiness, use `terminal.waitFor` directly. It retries the condition
|
|
28
|
+
and throws on timeout, so neither an outer `ctx.wait` nor an extra assertion is
|
|
29
|
+
needed to check that text appears:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
await terminal.waitFor("Rack is initialized", /Current rack status:\s*Initialized/, {
|
|
33
|
+
timeoutMs: 35 * 60_000,
|
|
34
|
+
intervalMs: 5_000,
|
|
35
|
+
failOn: [
|
|
36
|
+
{ description: "Setup was rejected", matcher: "Start Rack Setup Failed" },
|
|
37
|
+
{ description: "Initialization failed", matcher: "Initialization Failed" },
|
|
38
|
+
{ description: "Initialization panicked", matcher: "Initialization Panicked" },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
await terminal.press("Ctrl+C"); // Separate cleanup after the condition succeeds.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
SDK 0.129.0 records the string/regex matcher (or identifies a custom predicate),
|
|
45
|
+
timeout, and start/end positions for this wait. The dashboard shows its condition
|
|
46
|
+
and outcome and replays up to the result. Later inputs belong to their own steps;
|
|
47
|
+
the full terminal session can be opened separately. `ctx.wait` remains useful
|
|
48
|
+
when a retry must repeat actions, rather than just observe terminal state.
|
|
49
|
+
|
|
50
|
+
Terminal waits record matched, timed-out, failure-condition, predicate-error,
|
|
51
|
+
process-exit and cancelled outcomes. `failOn` checks the rendered screen before
|
|
52
|
+
checking success; string and regex conditions are recorded explicitly. For a
|
|
53
|
+
custom predicate, provide `condition: "Expected sled has a valid bootstrap IPv6 address"`
|
|
54
|
+
to explain the actual check separately from its step title. Async predicates are
|
|
55
|
+
awaited, and their rejection is recorded. A final matching screen succeeds even
|
|
56
|
+
if the process has exited; otherwise exit fails the wait after final output is
|
|
57
|
+
drained. No `Promise.race` with `terminal.exited` is needed.
|
|
58
|
+
|
|
59
|
+
Pass `signal: controller.signal` to cancel a wait without closing its terminal.
|
|
60
|
+
Cancellation stops SDK polling; it cannot forcibly stop work started inside a
|
|
61
|
+
custom predicate. `terminal.close({ timeoutMs: 10_000 })` cancels pending waits,
|
|
62
|
+
sends EOF, escalates to a kill, and bounds cleanup including output drain. The
|
|
63
|
+
default close deadline is 10 seconds; repeated calls share the same result or
|
|
64
|
+
failure. `terminal.exited` records the exit when observed after completion, not
|
|
65
|
+
when its promise is first read. A stalled output drain fails after 5 seconds
|
|
66
|
+
instead of hanging indefinitely. Test code should preserve its original failure
|
|
67
|
+
if diagnostic or cleanup operations also fail.
|
|
68
|
+
|
|
69
|
+
Browser navigation accepts public Playwright-style options, without a backend
|
|
70
|
+
cast or `pageOp` call:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await browser.goto(consoleUrl, { timeout: 120_000, waitUntil: "domcontentloaded" });
|
|
74
|
+
await browser.reload({ timeout: 120_000 });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
These options also apply to `goBack` and `goForward`. Navigation deadlines do not
|
|
78
|
+
cancel a surrounding `ctx.poll`, and retrying a navigation after a visible error
|
|
79
|
+
is a recovery action that should remain visible in the test results.
|
|
80
|
+
|
|
27
81
|
Pub/Sub integrations are project-owned as of SDK 0.98.0. The former
|
|
28
82
|
`pubsub`/`pubsubClient` exports and `/components/gcp` entry point are removed.
|
|
29
83
|
Use a local service definition or `serviceGroup()` wrapper; the repository’s
|
package/dist/browser.d.ts
CHANGED
|
@@ -124,6 +124,11 @@ export interface Mouse {
|
|
|
124
124
|
/** Wheel-scroll by a pixel delta. */
|
|
125
125
|
wheel(dx: number, dy: number): Promise<void>;
|
|
126
126
|
}
|
|
127
|
+
export interface NavigationOptions {
|
|
128
|
+
/** Playwright navigation timeout in milliseconds; 0 disables the deadline. */
|
|
129
|
+
timeout?: number;
|
|
130
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
|
|
131
|
+
}
|
|
127
132
|
/**
|
|
128
133
|
* Headless browser session — Playwright `Page`-shaped. Select elements with
|
|
129
134
|
* the `getBy*`/`locator` roots (returning a {@link Locator}) and act on them;
|
|
@@ -136,14 +141,14 @@ export interface Browser {
|
|
|
136
141
|
url(): string;
|
|
137
142
|
/** Current page `<title>` (async, like Playwright's `page.title()`). */
|
|
138
143
|
title(): Promise<string>;
|
|
139
|
-
/** Navigate to a URL;
|
|
140
|
-
goto(url: string): Promise<void>;
|
|
144
|
+
/** Navigate to a URL; waits for the main frame load unless waitUntil is supplied. */
|
|
145
|
+
goto(url: string, options?: NavigationOptions): Promise<void>;
|
|
141
146
|
/** Navigate back in session history. */
|
|
142
|
-
goBack(): Promise<void>;
|
|
147
|
+
goBack(options?: NavigationOptions): Promise<void>;
|
|
143
148
|
/** Navigate forward in session history. */
|
|
144
|
-
goForward(): Promise<void>;
|
|
149
|
+
goForward(options?: NavigationOptions): Promise<void>;
|
|
145
150
|
/** Reload the current page. */
|
|
146
|
-
reload(): Promise<void>;
|
|
151
|
+
reload(options?: NavigationOptions): Promise<void>;
|
|
147
152
|
readonly keyboard: Keyboard;
|
|
148
153
|
readonly mouse: Mouse;
|
|
149
154
|
/** Root CSS query. */
|
|
@@ -363,6 +368,8 @@ export declare function acquirePersistentDesktop(endpoint: string, windowIndex:
|
|
|
363
368
|
desktopChrome?: DesktopChrome;
|
|
364
369
|
}>;
|
|
365
370
|
export interface RecordableFields {
|
|
371
|
+
timeout: number;
|
|
372
|
+
waitUntil: NonNullable<NavigationOptions["waitUntil"]>;
|
|
366
373
|
target: RecordedBrowserLocatorPart[];
|
|
367
374
|
destinationTarget: RecordedBrowserLocatorPart[];
|
|
368
375
|
url: string;
|
package/dist/browser.js
CHANGED
|
@@ -1491,11 +1491,11 @@ function buildBackend(holder, recorder, buildOpts) {
|
|
|
1491
1491
|
keyboard,
|
|
1492
1492
|
mouse,
|
|
1493
1493
|
touchscreen,
|
|
1494
|
-
goto(url) {
|
|
1494
|
+
goto(url, options) {
|
|
1495
1495
|
recorder?.noteNavigation?.(url);
|
|
1496
|
-
return instrumented("goto", { url }, async () => {
|
|
1496
|
+
return instrumented("goto", { url, ...options }, async () => {
|
|
1497
1497
|
try {
|
|
1498
|
-
await holder.page.goto(url, { waitUntil: "load" });
|
|
1498
|
+
await holder.page.goto(url, { waitUntil: "load", ...options });
|
|
1499
1499
|
}
|
|
1500
1500
|
catch (err) {
|
|
1501
1501
|
// Restored-renderer DNS bug (see `rebuildView`): only when the
|
|
@@ -1508,23 +1508,23 @@ function buildBackend(holder, recorder, buildOpts) {
|
|
|
1508
1508
|
if (!(await daemonResolves(url)))
|
|
1509
1509
|
throw err;
|
|
1510
1510
|
await rebuildView(holder);
|
|
1511
|
-
await holder.page.goto(url, { waitUntil: "load" });
|
|
1511
|
+
await holder.page.goto(url, { waitUntil: "load", ...options });
|
|
1512
1512
|
}
|
|
1513
1513
|
});
|
|
1514
1514
|
},
|
|
1515
|
-
goBack() {
|
|
1516
|
-
return instrumented("goBack", {}, async () => {
|
|
1517
|
-
await holder.page.goBack();
|
|
1515
|
+
goBack(options) {
|
|
1516
|
+
return instrumented("goBack", { ...options }, async () => {
|
|
1517
|
+
await holder.page.goBack(options);
|
|
1518
1518
|
});
|
|
1519
1519
|
},
|
|
1520
|
-
goForward() {
|
|
1521
|
-
return instrumented("goForward", {}, async () => {
|
|
1522
|
-
await holder.page.goForward();
|
|
1520
|
+
goForward(options) {
|
|
1521
|
+
return instrumented("goForward", { ...options }, async () => {
|
|
1522
|
+
await holder.page.goForward(options);
|
|
1523
1523
|
});
|
|
1524
1524
|
},
|
|
1525
|
-
reload() {
|
|
1526
|
-
return instrumented("reload", {}, async () => {
|
|
1527
|
-
await holder.page.reload();
|
|
1525
|
+
reload(options) {
|
|
1526
|
+
return instrumented("reload", { ...options }, async () => {
|
|
1527
|
+
await holder.page.reload(options);
|
|
1528
1528
|
});
|
|
1529
1529
|
},
|
|
1530
1530
|
locator: (css) => makeLocator(backend, strategy, { steps: [{ m: "locator", args: [css] }] }),
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { Annotated } from "./annotate.js";
|
|
|
18
18
|
export { SQL, type SqlClient, type SqlOptions } from "./sql.js";
|
|
19
19
|
export { RedisClient, type RedisClientLike, type RedisOptions } from "./redis.js";
|
|
20
20
|
export { S3Client, type S3ClientLike, type S3File, type S3ClientOptions } from "./s3.js";
|
|
21
|
-
export type { Browser, BrowserOptions, Keyboard, Mouse, Touchscreen } from "./browser.js";
|
|
21
|
+
export type { Browser, BrowserOptions, NavigationOptions, Keyboard, Mouse, Touchscreen } from "./browser.js";
|
|
22
22
|
export type { MicrophoneFixture } from "./harness/microphone-fixture.js";
|
|
23
23
|
export type { FakeWebSocket, FakeWebSocketHandlers } from "./fake-websocket.js";
|
|
24
24
|
import type { FakeWebSocketHandlers } from "./fake-websocket.js";
|
|
@@ -34,7 +34,7 @@ export type { Mobile, MobileApp } from "./mobile.js";
|
|
|
34
34
|
import type { Mobile, MobileApp } from "./mobile.js";
|
|
35
35
|
export type { Desktop, DesktopApp, DesktopOptions } from "./desktop.js";
|
|
36
36
|
import type { Desktop, DesktopApp, DesktopOptions } from "./desktop.js";
|
|
37
|
-
export type { Terminal, TerminalOpts, TerminalResult } from "./terminal.js";
|
|
37
|
+
export type { Terminal, TerminalOpts, TerminalResult, TerminalWaitOptions, TerminalFailureCondition, TerminalCloseOptions } from "./terminal.js";
|
|
38
38
|
import type { Terminal, TerminalOpts, TerminalResult } from "./terminal.js";
|
|
39
39
|
export { type CoverageAdapter, type CoverageCaptureContext, type CoverageConfigureInfo, type CoverageSourceOptions, type CoverageSourceContext, type CoverageLoadContext, type ServiceCoverage, } from "./coverage.js";
|
|
40
40
|
import { type ServiceCoverage } from "./coverage.js";
|
package/dist/recorder.d.ts
CHANGED
|
@@ -407,6 +407,9 @@ export interface RecordedBrowserLocatorPart {
|
|
|
407
407
|
}
|
|
408
408
|
export interface BrowserEvent extends BaseEvent {
|
|
409
409
|
kind: "browser";
|
|
410
|
+
/** Explicit navigation options, when supplied by the test. */
|
|
411
|
+
timeout?: number;
|
|
412
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
|
|
410
413
|
action: BrowserAction;
|
|
411
414
|
target?: RecordedBrowserLocatorPart[];
|
|
412
415
|
destinationTarget?: RecordedBrowserLocatorPart[];
|
|
@@ -534,6 +537,18 @@ export interface TerminalEvent extends BaseEvent {
|
|
|
534
537
|
}
|
|
535
538
|
/** Action taken on an open interactive terminal session. */
|
|
536
539
|
export type TerminalStepAction = "send" | "sendLine" | "press" | "waitFor" | "exit" | "close";
|
|
540
|
+
export type TerminalWaitMatcher = {
|
|
541
|
+
kind: "text";
|
|
542
|
+
value: string;
|
|
543
|
+
} | {
|
|
544
|
+
kind: "regex";
|
|
545
|
+
value: string;
|
|
546
|
+
flags: string;
|
|
547
|
+
} | {
|
|
548
|
+
kind: "custom";
|
|
549
|
+
description?: string;
|
|
550
|
+
};
|
|
551
|
+
export type TerminalWaitOutcome = "matched" | "timed_out" | "failure_matched" | "predicate_error" | "process_exited" | "cancelled";
|
|
537
552
|
/**
|
|
538
553
|
* One operation on an open interactive terminal — analogous to
|
|
539
554
|
* `BrowserEvent`. The session's heavy asciicast frames live on the
|
|
@@ -557,9 +572,21 @@ export interface TerminalStepEvent extends BaseEvent {
|
|
|
557
572
|
description?: string;
|
|
558
573
|
/** How many polls `waitFor` made. */
|
|
559
574
|
attempts?: number;
|
|
560
|
-
/** Whether `waitFor` matched
|
|
575
|
+
/** Whether `waitFor` matched; see outcome for failure reasons. */
|
|
561
576
|
matched?: boolean;
|
|
562
|
-
/**
|
|
577
|
+
/** The condition being observed, including an optional custom-predicate description. */
|
|
578
|
+
matcher?: TerminalWaitMatcher;
|
|
579
|
+
outcome?: TerminalWaitOutcome;
|
|
580
|
+
failureMatchers?: {
|
|
581
|
+
description: string;
|
|
582
|
+
matcher: TerminalWaitMatcher;
|
|
583
|
+
}[];
|
|
584
|
+
intervalMs?: number;
|
|
585
|
+
timeoutMs?: number;
|
|
586
|
+
/** Wait boundaries on the session clock, including time with no output. */
|
|
587
|
+
castStartTimeSec?: number;
|
|
588
|
+
castEndTimeSec?: number;
|
|
589
|
+
/** Exit code observed on exit, close, or a failed wait. */
|
|
563
590
|
exitCode?: number;
|
|
564
591
|
/** Rendered screen at the moment of the op (post-action), truncated. */
|
|
565
592
|
screenPreview: string;
|
package/dist/terminal.d.ts
CHANGED
|
@@ -25,6 +25,25 @@ export interface TerminalOpts {
|
|
|
25
25
|
*/
|
|
26
26
|
timeoutMs?: number;
|
|
27
27
|
}
|
|
28
|
+
/** A visible terminal failure that should stop a wait immediately. */
|
|
29
|
+
export interface TerminalFailureCondition {
|
|
30
|
+
description: string;
|
|
31
|
+
matcher: string | RegExp;
|
|
32
|
+
}
|
|
33
|
+
export interface TerminalWaitOptions {
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
intervalMs?: number;
|
|
36
|
+
/** Cancel this observation without closing the terminal. */
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
/** Describe a custom predicate's condition separately from the step title. */
|
|
39
|
+
condition?: string;
|
|
40
|
+
/** Checked against the rendered screen before the success matcher. */
|
|
41
|
+
failOn?: readonly TerminalFailureCondition[];
|
|
42
|
+
}
|
|
43
|
+
export interface TerminalCloseOptions {
|
|
44
|
+
/** Total deadline including graceful exit, kill and pipe drain. Default 10 s. */
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
}
|
|
28
47
|
/** Result of the one-shot `ctx.terminal(...)` convenience. */
|
|
29
48
|
export interface TerminalResult {
|
|
30
49
|
/** Full decoded PTY output (raw bytes, ANSI escapes included). Capped. */
|
|
@@ -50,7 +69,8 @@ export interface Terminal {
|
|
|
50
69
|
/**
|
|
51
70
|
* Resolves when the underlying process ends — by `close()`, by the
|
|
52
71
|
* inner command exiting, or by timeout — to a `TerminalResult` with
|
|
53
|
-
* the full captured `output` and `exitCode`.
|
|
72
|
+
* the full captured `output` and `exitCode`. Rejects if output cannot
|
|
73
|
+
* be drained after exit.
|
|
54
74
|
*
|
|
55
75
|
* The result is provenance-tagged against the `exit` step, so an
|
|
56
76
|
* `expect((await term.exited).output).toContain(...)` /
|
|
@@ -91,29 +111,23 @@ export interface Terminal {
|
|
|
91
111
|
* `description` is a short human label that shows up in the event
|
|
92
112
|
* log so timelines don't fill with anonymous waits.
|
|
93
113
|
*
|
|
94
|
-
* Defaults:
|
|
95
|
-
*
|
|
114
|
+
* Defaults: 10 s total timeout, 100 ms between polls. Throws on timeout,
|
|
115
|
+
* a failure condition, predicate error, cancellation, or process exit before
|
|
116
|
+
* a match. On exit the final drained screen is checked first. Async custom
|
|
117
|
+
* predicates are supported; cancellation stops polling but cannot forcibly
|
|
118
|
+
* cancel work started by the predicate itself.
|
|
96
119
|
*/
|
|
97
|
-
waitFor(description: string, matcher: string, opts?:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}): Promise<Wrapped<string>>;
|
|
101
|
-
waitFor(description: string, matcher: RegExp, opts?: {
|
|
102
|
-
timeoutMs?: number;
|
|
103
|
-
intervalMs?: number;
|
|
104
|
-
}): Promise<Wrapped<RegExpMatchArray>>;
|
|
105
|
-
waitFor<T = unknown>(description: string, matcher: (screen: string, output: string) => T, opts?: {
|
|
106
|
-
timeoutMs?: number;
|
|
107
|
-
intervalMs?: number;
|
|
108
|
-
}): Promise<Wrapped<NonNullable<Awaited<T>>>>;
|
|
120
|
+
waitFor(description: string, matcher: string, opts?: TerminalWaitOptions): Promise<Wrapped<string>>;
|
|
121
|
+
waitFor(description: string, matcher: RegExp, opts?: TerminalWaitOptions): Promise<Wrapped<RegExpMatchArray>>;
|
|
122
|
+
waitFor<T = unknown>(description: string, matcher: (screen: string, output: string) => T, opts?: TerminalWaitOptions): Promise<Wrapped<NonNullable<Awaited<T>>>>;
|
|
109
123
|
/**
|
|
110
124
|
* Tear down. Writes Ctrl-D to stdin to nudge the shell out, then
|
|
111
125
|
* SIGKILLs after a grace period. Idempotent. Resolves to a
|
|
112
126
|
* `TerminalResult` (tagged against the `close` step) so you can assert
|
|
113
127
|
* on the transcript of a session that doesn't self-terminate. Optional
|
|
114
|
-
* — terminals are not auto-closed when a test ends.
|
|
128
|
+
* — terminals are not auto-closed when a test ends. Rejects on cleanup timeout.
|
|
115
129
|
*/
|
|
116
|
-
close(): Promise<Wrapped<TerminalResult>>;
|
|
130
|
+
close(opts?: TerminalCloseOptions): Promise<Wrapped<TerminalResult>>;
|
|
117
131
|
}
|
|
118
132
|
/**
|
|
119
133
|
* The concrete handle `openTerminal` returns. Extends the public
|