@opice/harness 0.4.1 → 0.5.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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/reporter.d.ts +11 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +2 -0
- package/dist/reporter.js.map +1 -1
- package/dist/scenario.d.ts +52 -8
- package/dist/scenario.d.ts.map +1 -1
- package/dist/scenario.js +137 -45
- package/dist/scenario.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/reporter.ts +13 -0
- package/src/scenario.ts +159 -46
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { el, tid, waitFor, wait, evalJs, screenshot } from './element.js';
|
|
|
2
2
|
export { byLabel, byRole, byText } from './accessible.js';
|
|
3
3
|
export { back, currentPath, currentUrl, forward, open, reload } from './navigation.js';
|
|
4
4
|
export { getPage, getContext } from './context.js';
|
|
5
|
-
export { browserTest, invariant, step } from './scenario.js';
|
|
5
|
+
export { browserTest, DEFAULT_WALKTHROUGH_TIMEOUT_MS, invariant, step } from './scenario.js';
|
|
6
6
|
export type { BrowserTestMeta, StepContract } from './scenario.js';
|
|
7
7
|
export { getReporter, setReporter, configureFromEnv } from './reporter.js';
|
|
8
8
|
export type { Reporter, ReporterConfig, StepEvent, ScenarioStart, ScenarioFinish } from './reporter.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAEzD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAEtF,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAElD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAEzD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAEtF,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAElD,OAAO,EAAE,WAAW,EAAE,8BAA8B,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AAC5F,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAElE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAC1E,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEvG,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,YAAY,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAC5G,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC7D,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAI9C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAGzC,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { el, tid, waitFor, wait, evalJs, screenshot } from './element.js';
|
|
|
2
2
|
export { byLabel, byRole, byText } from './accessible.js';
|
|
3
3
|
export { back, currentPath, currentUrl, forward, open, reload } from './navigation.js';
|
|
4
4
|
export { getPage, getContext } from './context.js';
|
|
5
|
-
export { browserTest, invariant, step } from './scenario.js';
|
|
5
|
+
export { browserTest, DEFAULT_WALKTHROUGH_TIMEOUT_MS, invariant, step } from './scenario.js';
|
|
6
6
|
export { getReporter, setReporter, configureFromEnv } from './reporter.js';
|
|
7
7
|
export { parseOpiceDsn } from './dsn.js';
|
|
8
8
|
export { command, call, runCommand, makeCtx, loadUserCommands, findUserCommandsFile, z } from './command.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAEzD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAEtF,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAElD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAEzD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAEtF,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAElD,OAAO,EAAE,WAAW,EAAE,8BAA8B,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AAG5F,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAG1E,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAGxC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAG5G,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAG7D,iFAAiF;AACjF,uEAAuE;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/reporter.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export interface ReporterConfig {
|
|
|
24
24
|
}
|
|
25
25
|
export interface StepEvent {
|
|
26
26
|
scenarioId: string;
|
|
27
|
+
/**
|
|
28
|
+
* 0-based retry attempt that produced this step. The platform shows only the
|
|
29
|
+
* final attempt's steps; earlier attempts are kept for forensics. Defaults
|
|
30
|
+
* to 0 on the platform side when omitted (older clients).
|
|
31
|
+
*/
|
|
32
|
+
attempt?: number;
|
|
27
33
|
/** Authoring order within the scenario, assigned at step() call time. */
|
|
28
34
|
sequence: number;
|
|
29
35
|
/**
|
|
@@ -65,6 +71,11 @@ export interface ScenarioFinish {
|
|
|
65
71
|
scenarioId: string;
|
|
66
72
|
status: 'passed' | 'failed';
|
|
67
73
|
durationMs: number;
|
|
74
|
+
/**
|
|
75
|
+
* Total attempts the scenario took (>= 1). A passed scenario with
|
|
76
|
+
* `attempts > 1` is flaky. Omitted ⇒ the platform defaults it to 1.
|
|
77
|
+
*/
|
|
78
|
+
attempts?: number;
|
|
68
79
|
}
|
|
69
80
|
export interface Reporter {
|
|
70
81
|
startScenario(input: ScenarioStart): Promise<string>;
|
package/dist/reporter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,MAAM,CAAC,EAAE,IAAI,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,SAAS;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,CAAA;IAC/D,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,MAAM,CAAC,EAAE,IAAI,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,SAAS;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,CAAA;IAC/D,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACxB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACpD,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACtB;AAWD,eAAO,MAAM,WAAW,QAAwC,CAAA;AAMhE,MAAM,WAAW,UAAU;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACb;AAqKD,wBAAgB,WAAW,IAAI,QAAQ,CAEtC;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,QAAQ,CA8B/E"}
|
package/dist/reporter.js
CHANGED
|
@@ -93,6 +93,7 @@ class HttpReporter {
|
|
|
93
93
|
? await this.encodeScreenshot(event.screenshotPath)
|
|
94
94
|
: undefined;
|
|
95
95
|
await this.fetch('POST', `/api/v1/runs/${runId}/scenarios/${event.scenarioId}/steps`, {
|
|
96
|
+
attempt: event.attempt,
|
|
96
97
|
sequence: event.sequence,
|
|
97
98
|
kind: event.kind,
|
|
98
99
|
name: event.name,
|
|
@@ -111,6 +112,7 @@ class HttpReporter {
|
|
|
111
112
|
await this.fetch('PATCH', `/api/v1/runs/${runId}/scenarios/${input.scenarioId}`, {
|
|
112
113
|
status: input.status,
|
|
113
114
|
durationMs: input.durationMs,
|
|
115
|
+
attempts: input.attempts,
|
|
114
116
|
});
|
|
115
117
|
}
|
|
116
118
|
async flush() {
|
package/dist/reporter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reporter.js","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,+EAA+E;AAC/E,MAAM,kBAAkB,GAAG,MAAM,CAAA;AACjC,kFAAkF;AAClF,MAAM,eAAe,GAAG,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"reporter.js","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,+EAA+E;AAC/E,MAAM,kBAAkB,GAAG,MAAM,CAAA;AACjC,kFAAkF;AAClF,MAAM,eAAe,GAAG,MAAM,CAAA;AA6E9B,MAAM,YAAY;IACjB,KAAK,CAAC,aAAa,CAAC,KAAoB;QACvC,OAAO,QAAQ,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;IAC1C,CAAC;IACD,KAAK,CAAC,UAAU,CAAC,MAAiB,IAAkB,CAAC;IACrD,KAAK,CAAC,cAAc,CAAC,MAAsB,IAAkB,CAAC;IAC9D,KAAK,CAAC,KAAK,KAAmB,CAAC;CAC/B;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAA;AAEhE,SAAS,WAAW,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG;IACrC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,GAAG,OAAO,CAAC,CAAA;AAC7C,CAAC;AAQD,MAAM,YAAY;IAKY;IAJrB,YAAY,GAA2B,IAAI,CAAA;IAClC,OAAO,GAA0B,IAAI,GAAG,EAAE,CAAA;IACnD,iBAAiB,GAAG,KAAK,CAAA;IAEjC,YAA6B,MAAsB;QAAtB,WAAM,GAAN,MAAM,CAAgB;IAAG,CAAC;IAE/C,KAAK,CAAC,SAAS;QACtB,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACxB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;QACpC,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAA;IACzB,CAAC;IAEO,KAAK,CAAC,QAAQ;QACrB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,cAAc,EAAE;YACzD,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YAC1B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YAC1B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;SAC1B,CAAC,CAAA;QACF,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAW,CAAA;QACzC,iEAAiE;QACjE,yDAAyD;QACzD,IAAI,CAAC;YACJ,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC3C,MAAM,OAAO,GAAe,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAA;YACjG,aAAa,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAA;QAC/D,CAAC;QAAC,MAAM,CAAC;YACR,cAAc;QACf,CAAC;QACD,OAAO,KAAK,CAAA;IACb,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,KAAoB;QACvC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;QACpC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,gBAAgB,KAAK,YAAY,EAAE;YAC5E,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,KAAK,EAAE,KAAK,CAAC,KAAK;SAClB,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,YAAY,CAAW,CAAA;IACxC,CAAC;IAED,UAAU,CAAC,KAAgB;QAC1B,uEAAuE;QACvE,oEAAoE;QACpE,+CAA+C;QAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAA;QAC9C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACnB,OAAO,OAAO,CAAA;IACf,CAAC;IAEO,KAAK,CAAC,kBAAkB,CAAC,KAAgB;QAChD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;QACpC,MAAM,UAAU,GAAG,KAAK,CAAC,cAAc;YACtC,CAAC,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,cAAc,CAAC;YACnD,CAAC,CAAC,SAAS,CAAA;QACZ,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,gBAAgB,KAAK,cAAc,KAAK,CAAC,UAAU,QAAQ,EAAE;YACrF,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,UAAU;SACV,CAAC,CAAA;IACH,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,KAAqB;QACzC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;QACpC,gEAAgE;QAChE,6BAA6B;QAC7B,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,gBAAgB,KAAK,cAAc,KAAK,CAAC,UAAU,EAAE,EAAE;YAChF,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACxB,CAAC,CAAA;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACV,uEAAuE;QACvE,2EAA2E;QAC3E,oEAAoE;QACpE,2EAA2E;QAC3E,sEAAsE;QACtE,kCAAkC;QAClC,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAA;QACnF,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;QACnE,4DAA4D;IAC7D,CAAC;IAEO,KAAK,CAAC,OAAyB;QACtC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACzB,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;IACpD,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,IAAY;QAC1C,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;YACnC,OAAO,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;QAC9B,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,SAAS,CAAA;QACjB,CAAC;IACF,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,MAAc,EAAE,IAAY,EAAE,IAAc;QAC/D,IAAI,QAAkB,CAAA;QACtB,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,EAAE;gBACnD,MAAM;gBACN,OAAO,EAAE;oBACR,eAAe,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;oBAC/C,cAAc,EAAE,kBAAkB;iBAClC;gBACD,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBACrD,gEAAgE;gBAChE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,kBAAkB,CAAC;aAC/C,CAAC,CAAA;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,sEAAsE;YACtE,sEAAsE;YACtE,uEAAuE;YACvE,oDAAoD;YACpD,IAAI,CAAC,eAAe,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;YAC3F,MAAM,GAAG,CAAA;QACV,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAClB,MAAM,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,IAAI,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAA;YACnE,IAAI,CAAC,eAAe,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,EAAE,MAAM,CAAC,CAAA;YACjD,MAAM,IAAI,KAAK,CAAC,kBAAkB,MAAM,IAAI,IAAI,YAAY,MAAM,EAAE,CAAC,CAAA;QACtE,CAAC;QACD,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA4B,CAAA;IAC1D,CAAC;IAED;;;;;OAKG;IACK,eAAe,CAAC,IAAY,EAAE,MAAc;QACnD,IAAI,IAAI,CAAC,iBAAiB;YAAE,OAAM;QAClC,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAA;QAC7B,OAAO,CAAC,KAAK,CACZ,kDAAkD,IAAI,KAAK,MAAM,KAAK;cACpE,mDAAmD;cACnD,0BAA0B;cAC1B,wFAAwF;cACxF,0FAA0F;cAC1F,uFAAuF;cACvF,sFAAsF,CACxF,CAAA;IACF,CAAC;CACD;AAED,IAAI,MAAM,GAAa,IAAI,YAAY,EAAE,CAAA;AAEzC,MAAM,UAAU,WAAW;IAC1B,OAAO,MAAM,CAAA;AACd,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAAkB;IAC7C,MAAM,GAAG,QAAQ,CAAA;AAClB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACpE,8DAA8D;IAC9D,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAA;IAC3C,MAAM,QAAQ,GAAG,GAAG,CAAC,gBAAgB,CAAC,IAAI,GAAG,EAAE,QAAQ,CAAA;IACvD,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,GAAG,EAAE,OAAO,CAAA;IACtD,MAAM,MAAM,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,GAAG,EAAE,MAAM,CAAA;IAClD,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC;QACxC,OAAO,IAAI,YAAY,EAAE,CAAA;IAC1B,CAAC;IACD,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,0EAA0E;IAC1E,0BAA0B;IAC1B,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAA;IACnD,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAA;IAC1D,MAAM,YAAY,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;IAC/E,IAAI,CAAC,YAAY,EAAE,CAAC;QACnB,OAAO,IAAI,YAAY,EAAE,CAAA;IAC1B,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC;QACjC,QAAQ;QACR,SAAS;QACT,MAAM;QACN,MAAM,EAAE,GAAG,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,iBAAiB,CAAC;QACrD,MAAM,EAAE,GAAG,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,YAAY,CAAC;QAChD,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;KAC7B,CAAC,CAAA;IACF,WAAW,CAAC,QAAQ,CAAC,CAAA;IACrB,OAAO,QAAQ,CAAA;AAChB,CAAC;AAED,gCAAgC;AAChC,gBAAgB,EAAE,CAAA"}
|
package/dist/scenario.d.ts
CHANGED
|
@@ -30,19 +30,63 @@ export interface BrowserTestMeta {
|
|
|
30
30
|
seeds?: string[];
|
|
31
31
|
/** Identities / roles the scenario acts as, e.g. `['crmOperator']`. */
|
|
32
32
|
roles?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* One-time scenario setup, run once before the walkthrough (in `beforeAll`) —
|
|
35
|
+
* the place for "establish a precondition the steps assume", e.g. minting
|
|
36
|
+
* auth tokens. Replaces a hand-written `beforeAll` in the body form. Runs
|
|
37
|
+
* before any browser navigation, so it can register cookies/identity the
|
|
38
|
+
* first paint needs.
|
|
39
|
+
*/
|
|
40
|
+
setup?: () => void | Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Per-scenario retry budget (body form only). A flaky scenario that fails
|
|
43
|
+
* then passes within the budget is reported as **passed but flaky** (the
|
|
44
|
+
* dashboard badges it). Each attempt gets a fresh browser + a clean
|
|
45
|
+
* navigation, so a retry can't inherit the failed attempt's page state.
|
|
46
|
+
*
|
|
47
|
+
* Omit to inherit the global default (`opice test --retries=N` / `bun test
|
|
48
|
+
* --retry=N`). Ignored by the legacy registrar form (it can't be retried
|
|
49
|
+
* cleanly — it shares one browser across its `test()` blocks).
|
|
50
|
+
*/
|
|
51
|
+
retries?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Per-scenario timeout (ms) for the walkthrough body. Defaults to
|
|
54
|
+
* {@link DEFAULT_WALKTHROUGH_TIMEOUT_MS}. Body form only.
|
|
55
|
+
*/
|
|
56
|
+
timeout?: number;
|
|
33
57
|
}
|
|
34
58
|
/**
|
|
35
|
-
*
|
|
59
|
+
* Default timeout for a walkthrough body. A real browser walk — first page
|
|
60
|
+
* load, async data, a dev server compiling a chunk on first hit — easily
|
|
61
|
+
* exceeds bun's 5s default; each retrying assertion still bounds itself.
|
|
62
|
+
*/
|
|
63
|
+
export declare const DEFAULT_WALKTHROUGH_TIMEOUT_MS = 60000;
|
|
64
|
+
/**
|
|
65
|
+
* Register a top-level browser test scenario. Two forms, picked automatically:
|
|
66
|
+
*
|
|
67
|
+
* **Body form (preferred)** — pass an **async** function; it IS the walkthrough:
|
|
68
|
+
*
|
|
69
|
+
* browserTest({ name: '…', retries: 2, setup: () => mintTokens() }, async () => {
|
|
70
|
+
* await step('…', async () => { … })
|
|
71
|
+
* })
|
|
72
|
+
*
|
|
73
|
+
* `browserTest` owns the single `test('walkthrough', …)` call, so it honours
|
|
74
|
+
* `meta.retries` (bun `{ retry }`) and `meta.timeout`. Each attempt opens a
|
|
75
|
+
* **fresh** browser context + clean navigation, so a retry never inherits the
|
|
76
|
+
* failed attempt's page state. `meta.setup` runs once before the walkthrough.
|
|
77
|
+
*
|
|
78
|
+
* **Legacy registrar form** — pass a **sync** function that registers its own
|
|
79
|
+
* `beforeAll`/`test`/`describe` blocks (the old multi-test pattern). The browser
|
|
80
|
+
* is launched once in `beforeAll` and shared across those blocks. It can't be
|
|
81
|
+
* retried cleanly (shared state), so `meta.retries` is ignored.
|
|
36
82
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* typically contains nested `describe`/`test` blocks), and tears the browser
|
|
40
|
-
* down in `afterAll`.
|
|
83
|
+
* The two are told apart by whether `fn` is an `AsyncFunction`: a walkthrough
|
|
84
|
+
* body always awaits its steps; a registrar never needs to be async.
|
|
41
85
|
*
|
|
42
|
-
* Metadata is the **first** argument (`{ name, url, hash, feature, seeds,
|
|
43
|
-
*
|
|
86
|
+
* Metadata is the **first** argument (`{ name, url, hash, feature, seeds, roles,
|
|
87
|
+
* setup, retries, timeout }`); `name` is required.
|
|
44
88
|
*/
|
|
45
|
-
export declare function browserTest(meta: BrowserTestMeta, fn: () => void): void;
|
|
89
|
+
export declare function browserTest(meta: BrowserTestMeta, fn: () => void | Promise<void>): void;
|
|
46
90
|
/**
|
|
47
91
|
* The durable contract of a step or invariant, separate from its mechanics.
|
|
48
92
|
*
|
package/dist/scenario.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenario.d.ts","sourceRoot":"","sources":["../src/scenario.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;
|
|
1
|
+
{"version":3,"file":"scenario.d.ts","sourceRoot":"","sources":["../src/scenario.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,QAAS,CAAA;AAwCpD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAwIvF;AAkDD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACb;AA+FD,KAAK,QAAQ,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAE1C,UAAU,MAAM;IACf,uBAAuB;IACvB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,8EAA8E;IAC9E,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,qDAAqD;IACrD,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnE;AAED,UAAU,UAAU;IACnB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpE;;;;;OAKG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACjF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,IAAI,EAAE,MAAM,GAAG,UAa3B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,SAAS,EAAE;IACvB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAWpE,CAAA"}
|
package/dist/scenario.js
CHANGED
|
@@ -16,6 +16,12 @@ function bunTest() {
|
|
|
16
16
|
return require('bun:test');
|
|
17
17
|
}
|
|
18
18
|
const PLAYGROUND_URL = process.env['PLAYGROUND_URL'] ?? 'http://localhost:15180';
|
|
19
|
+
/**
|
|
20
|
+
* Default timeout for a walkthrough body. A real browser walk — first page
|
|
21
|
+
* load, async data, a dev server compiling a chunk on first hit — easily
|
|
22
|
+
* exceeds bun's 5s default; each retrying assertion still bounds itself.
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_WALKTHROUGH_TIMEOUT_MS = 60_000;
|
|
19
25
|
/**
|
|
20
26
|
* Best-effort capture of the `*.test.ts` path that called `browserTest`, by
|
|
21
27
|
* walking the stack for the first `.test.` frame. Reported so a failed
|
|
@@ -49,16 +55,35 @@ let currentScenarioPending = 0;
|
|
|
49
55
|
// fire-and-forget and would otherwise be sequenced by arrival order at the
|
|
50
56
|
// worker, which screenshot-encoding latency can reshuffle.
|
|
51
57
|
let currentScenarioStepSeq = 0;
|
|
58
|
+
// 0-based index of the current attempt. In the body form the walkthrough wrapper
|
|
59
|
+
// bumps it on every (re-)invocation, so steps carry the attempt that produced
|
|
60
|
+
// them and the dashboard shows only the final one. The legacy form never
|
|
61
|
+
// retries, so it stays 0.
|
|
62
|
+
let currentAttempt = 0;
|
|
52
63
|
/**
|
|
53
|
-
* Register a top-level browser test scenario.
|
|
64
|
+
* Register a top-level browser test scenario. Two forms, picked automatically:
|
|
65
|
+
*
|
|
66
|
+
* **Body form (preferred)** — pass an **async** function; it IS the walkthrough:
|
|
67
|
+
*
|
|
68
|
+
* browserTest({ name: '…', retries: 2, setup: () => mintTokens() }, async () => {
|
|
69
|
+
* await step('…', async () => { … })
|
|
70
|
+
* })
|
|
71
|
+
*
|
|
72
|
+
* `browserTest` owns the single `test('walkthrough', …)` call, so it honours
|
|
73
|
+
* `meta.retries` (bun `{ retry }`) and `meta.timeout`. Each attempt opens a
|
|
74
|
+
* **fresh** browser context + clean navigation, so a retry never inherits the
|
|
75
|
+
* failed attempt's page state. `meta.setup` runs once before the walkthrough.
|
|
76
|
+
*
|
|
77
|
+
* **Legacy registrar form** — pass a **sync** function that registers its own
|
|
78
|
+
* `beforeAll`/`test`/`describe` blocks (the old multi-test pattern). The browser
|
|
79
|
+
* is launched once in `beforeAll` and shared across those blocks. It can't be
|
|
80
|
+
* retried cleanly (shared state), so `meta.retries` is ignored.
|
|
54
81
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* typically contains nested `describe`/`test` blocks), and tears the browser
|
|
58
|
-
* down in `afterAll`.
|
|
82
|
+
* The two are told apart by whether `fn` is an `AsyncFunction`: a walkthrough
|
|
83
|
+
* body always awaits its steps; a registrar never needs to be async.
|
|
59
84
|
*
|
|
60
|
-
* Metadata is the **first** argument (`{ name, url, hash, feature, seeds,
|
|
61
|
-
*
|
|
85
|
+
* Metadata is the **first** argument (`{ name, url, hash, feature, seeds, roles,
|
|
86
|
+
* setup, retries, timeout }`); `name` is required.
|
|
62
87
|
*/
|
|
63
88
|
export function browserTest(meta, fn) {
|
|
64
89
|
if (typeof meta === 'string') {
|
|
@@ -71,13 +96,17 @@ export function browserTest(meta, fn) {
|
|
|
71
96
|
}
|
|
72
97
|
const reporter = getReporter();
|
|
73
98
|
const testFile = captureTestFile();
|
|
74
|
-
const { describe, beforeAll, afterAll } = bunTest();
|
|
99
|
+
const { describe, beforeAll, afterAll, test } = bunTest();
|
|
100
|
+
// An async fn is the walkthrough body (browserTest owns its test()); a sync
|
|
101
|
+
// fn is the legacy registrar (it registers its own test()/hooks).
|
|
102
|
+
const isBody = fn.constructor.name === 'AsyncFunction';
|
|
75
103
|
describe(meta.name, () => {
|
|
76
104
|
beforeAll(async () => {
|
|
77
105
|
currentScenarioStart = Date.now();
|
|
78
|
-
currentScenarioFailures = 0;
|
|
79
106
|
currentScenarioPending = 0;
|
|
107
|
+
currentScenarioFailures = 0;
|
|
80
108
|
currentScenarioStepSeq = 0;
|
|
109
|
+
currentAttempt = 0;
|
|
81
110
|
try {
|
|
82
111
|
currentScenarioId = await reporter.startScenario({
|
|
83
112
|
name: meta.name,
|
|
@@ -92,50 +121,32 @@ export function browserTest(meta, fn) {
|
|
|
92
121
|
currentScenarioId = null;
|
|
93
122
|
}
|
|
94
123
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
await
|
|
102
|
-
const base = meta.url ?? PLAYGROUND_URL;
|
|
103
|
-
const url = meta.hash ? `${base}#${meta.hash}` : base;
|
|
104
|
-
// `domcontentloaded`, not the default `load`: an SPA paints after its JS
|
|
105
|
-
// runs and may hold `load` on a slow chunk or long-lived connection, so
|
|
106
|
-
// waiting for `load` flakily times out under CI contention. Readiness is
|
|
107
|
-
// handled by the test's retrying assertions.
|
|
108
|
-
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
124
|
+
// One-time precondition (mint tokens, …), before any navigation.
|
|
125
|
+
if (meta.setup)
|
|
126
|
+
await meta.setup();
|
|
127
|
+
// Body form opens the browser per attempt (in the test wrapper);
|
|
128
|
+
// the legacy registrar shares one browser, launched here once.
|
|
129
|
+
if (!isBody)
|
|
130
|
+
await openScenario(meta);
|
|
109
131
|
}
|
|
110
132
|
catch (e) {
|
|
111
|
-
// Setup failed before any step ran
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
// failed step so the dashboard shows *why*, finish the scenario as
|
|
117
|
-
// failed, then re-throw so the run still fails.
|
|
118
|
-
currentScenarioFailures++;
|
|
133
|
+
// Setup failed before any step ran. bun:test does NOT run afterAll
|
|
134
|
+
// when beforeAll throws, so the scenario started above would otherwise
|
|
135
|
+
// sit on the dashboard as 'running' forever — record a synthetic failed
|
|
136
|
+
// step, finish it as failed here, then re-throw so the run stays red.
|
|
137
|
+
await recordSetupFailure(reporter, e);
|
|
119
138
|
if (currentScenarioId) {
|
|
120
|
-
const error = e instanceof Error ? e.message : String(e);
|
|
121
|
-
const durationMs = Date.now() - currentScenarioStart;
|
|
122
139
|
try {
|
|
123
|
-
await reporter.
|
|
140
|
+
await reporter.finishScenario({
|
|
124
141
|
scenarioId: currentScenarioId,
|
|
125
|
-
sequence: currentScenarioStepSeq++,
|
|
126
|
-
kind: 'step',
|
|
127
|
-
name: 'scenario setup',
|
|
128
142
|
status: 'failed',
|
|
129
|
-
durationMs,
|
|
130
|
-
|
|
143
|
+
durationMs: Date.now() - currentScenarioStart,
|
|
144
|
+
attempts: 1,
|
|
131
145
|
});
|
|
132
|
-
await reporter.finishScenario({ scenarioId: currentScenarioId, status: 'failed', durationMs });
|
|
133
146
|
}
|
|
134
147
|
catch {
|
|
135
|
-
// best-effort
|
|
136
|
-
// original setup error we're about to re-throw.
|
|
148
|
+
// best-effort
|
|
137
149
|
}
|
|
138
|
-
// Null it so afterAll (should it run) doesn't double-finish.
|
|
139
150
|
currentScenarioId = null;
|
|
140
151
|
}
|
|
141
152
|
throw e;
|
|
@@ -170,7 +181,9 @@ export function browserTest(meta, fn) {
|
|
|
170
181
|
const durationMs = Date.now() - currentScenarioStart;
|
|
171
182
|
const status = currentScenarioFailures > 0 ? 'failed' : 'passed';
|
|
172
183
|
try {
|
|
173
|
-
|
|
184
|
+
// attempts = final attempt index + 1. A passed scenario with
|
|
185
|
+
// attempts > 1 failed at least once first → flaky.
|
|
186
|
+
await reporter.finishScenario({ scenarioId: currentScenarioId, status, durationMs, attempts: currentAttempt + 1 });
|
|
174
187
|
}
|
|
175
188
|
catch {
|
|
176
189
|
// best-effort
|
|
@@ -178,9 +191,86 @@ export function browserTest(meta, fn) {
|
|
|
178
191
|
}
|
|
179
192
|
currentScenarioId = null;
|
|
180
193
|
}, 30_000);
|
|
181
|
-
|
|
194
|
+
if (isBody) {
|
|
195
|
+
const body = fn;
|
|
196
|
+
const timeout = meta.timeout ?? DEFAULT_WALKTHROUGH_TIMEOUT_MS;
|
|
197
|
+
// Only set `retry` when a budget is configured — leaving it unset lets
|
|
198
|
+
// bun's global `--retry` default apply; passing `retry: 0` overrides it.
|
|
199
|
+
const testOptions = meta.retries === undefined ? { timeout } : { timeout, retry: meta.retries };
|
|
200
|
+
// bun re-runs the test body for every retry attempt; `attempt` counts
|
|
201
|
+
// those invocations (0-based). Each opens a fresh browser + navigation.
|
|
202
|
+
let attempt = -1;
|
|
203
|
+
test('walkthrough', async () => {
|
|
204
|
+
attempt++;
|
|
205
|
+
currentAttempt = attempt;
|
|
206
|
+
currentScenarioFailures = 0;
|
|
207
|
+
currentScenarioStepSeq = 0;
|
|
208
|
+
currentScenarioPending = 0;
|
|
209
|
+
try {
|
|
210
|
+
await openScenario(meta);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
// Setup failed: record it (afterAll finishes the scenario) and fail
|
|
214
|
+
// the attempt so bun retries or, once spent, leaves the run red.
|
|
215
|
+
await recordSetupFailure(reporter, e);
|
|
216
|
+
throw e;
|
|
217
|
+
}
|
|
218
|
+
await body();
|
|
219
|
+
}, testOptions);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// Legacy registrar: it registers its own test()/hooks; the shared
|
|
223
|
+
// browser was opened in beforeAll above.
|
|
224
|
+
fn();
|
|
225
|
+
}
|
|
182
226
|
});
|
|
183
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Open a fresh isolated browser context + page for `meta` and navigate to its
|
|
230
|
+
* scenario URL. `launchPage()` closes any previous context first, so calling
|
|
231
|
+
* this again (a retry attempt) tears down the failed attempt's page cleanly.
|
|
232
|
+
*/
|
|
233
|
+
async function openScenario(meta) {
|
|
234
|
+
const page = await launchPage();
|
|
235
|
+
// Repo-level context setup (browser-setup.ts) runs before the first
|
|
236
|
+
// navigation, so an addInitScript it registers fires before the app's own
|
|
237
|
+
// scripts on first paint.
|
|
238
|
+
const setup = await loadUserSetup();
|
|
239
|
+
if (setup)
|
|
240
|
+
await setup(getContext());
|
|
241
|
+
const base = meta.url ?? PLAYGROUND_URL;
|
|
242
|
+
const url = meta.hash ? `${base}#${meta.hash}` : base;
|
|
243
|
+
// `domcontentloaded`, not the default `load`: an SPA paints after its JS runs
|
|
244
|
+
// and may hold `load` on a slow chunk or long-lived connection, so waiting for
|
|
245
|
+
// `load` flakily times out under CI contention. Readiness is handled by the
|
|
246
|
+
// test's retrying assertions.
|
|
247
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Record a synthetic failed 'scenario setup' step for the current attempt and
|
|
251
|
+
* count it toward scenario failures. Does NOT finish the scenario (the caller
|
|
252
|
+
* decides whether afterAll will, or whether it must finish inline).
|
|
253
|
+
*/
|
|
254
|
+
async function recordSetupFailure(reporter, e) {
|
|
255
|
+
currentScenarioFailures++;
|
|
256
|
+
if (!currentScenarioId)
|
|
257
|
+
return;
|
|
258
|
+
try {
|
|
259
|
+
await reporter.recordStep({
|
|
260
|
+
scenarioId: currentScenarioId,
|
|
261
|
+
attempt: currentAttempt,
|
|
262
|
+
sequence: currentScenarioStepSeq++,
|
|
263
|
+
kind: 'step',
|
|
264
|
+
name: 'scenario setup',
|
|
265
|
+
status: 'failed',
|
|
266
|
+
durationMs: Date.now() - currentScenarioStart,
|
|
267
|
+
error: e instanceof Error ? e.message : String(e),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// best-effort: reporting the failure must never mask the original error.
|
|
272
|
+
}
|
|
273
|
+
}
|
|
184
274
|
async function runUnit(unit) {
|
|
185
275
|
const reporter = getReporter();
|
|
186
276
|
// Capture order at call time, before the fire-and-forget record below.
|
|
@@ -198,6 +288,7 @@ async function runUnit(unit) {
|
|
|
198
288
|
if (currentScenarioId) {
|
|
199
289
|
void reporter.recordStep({
|
|
200
290
|
scenarioId: currentScenarioId,
|
|
291
|
+
attempt: currentAttempt,
|
|
201
292
|
sequence,
|
|
202
293
|
kind: unit.kind,
|
|
203
294
|
name: unit.name,
|
|
@@ -247,6 +338,7 @@ async function runUnit(unit) {
|
|
|
247
338
|
if (currentScenarioId) {
|
|
248
339
|
void reporter.recordStep({
|
|
249
340
|
scenarioId: currentScenarioId,
|
|
341
|
+
attempt: currentAttempt,
|
|
250
342
|
sequence,
|
|
251
343
|
kind: unit.kind,
|
|
252
344
|
name: unit.name,
|
package/dist/scenario.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenario.js","sourceRoot":"","sources":["../src/scenario.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"scenario.js","sourceRoot":"","sources":["../src/scenario.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C;;;;;;GAMG;AACH,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC9C,SAAS,OAAO;IACf,OAAO,OAAO,CAAC,UAAU,CAA8B,CAAA;AACxD,CAAC;AAED,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,wBAAwB,CAAA;AA4DhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,MAAM,CAAA;AAEpD;;;;GAIG;AACH,SAAS,eAAe;IACvB,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,KAAK,CAAA;IAC/B,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAA;QACzE,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAA;YAC9C,IAAI,CAAC;gBACJ,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,CAAA;gBAC7C,OAAO,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;YACxC,CAAC;YAAC,MAAM,CAAC;gBACR,OAAO,GAAG,CAAA;YACX,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,SAAS,CAAA;AACjB,CAAC;AAED,IAAI,iBAAiB,GAAkB,IAAI,CAAA;AAC3C,IAAI,oBAAoB,GAAW,CAAC,CAAA;AACpC,IAAI,uBAAuB,GAAG,CAAC,CAAA;AAC/B,IAAI,sBAAsB,GAAG,CAAC,CAAA;AAC9B,6EAA6E;AAC7E,mEAAmE;AACnE,2EAA2E;AAC3E,2DAA2D;AAC3D,IAAI,sBAAsB,GAAG,CAAC,CAAA;AAC9B,iFAAiF;AACjF,8EAA8E;AAC9E,yEAAyE;AACzE,0BAA0B;AAC1B,IAAI,cAAc,GAAG,CAAC,CAAA;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,WAAW,CAAC,IAAqB,EAAE,EAA8B;IAChF,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,yEAAyE;QACzE,MAAM,IAAI,KAAK,CACd,yFAAyF;cACvF,sBAAsB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,mCAAmC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAC7G,CAAA;IACF,CAAC;IACD,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,wFAAwF,CAAC,CAAA;IAC1G,CAAC;IACD,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAA;IAClC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAA;IACzD,4EAA4E;IAC5E,kEAAkE;IAClE,MAAM,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,KAAK,eAAe,CAAA;IAEtD,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE;QACxB,SAAS,CAAC,KAAK,IAAI,EAAE;YACpB,oBAAoB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACjC,sBAAsB,GAAG,CAAC,CAAA;YAC1B,uBAAuB,GAAG,CAAC,CAAA;YAC3B,sBAAsB,GAAG,CAAC,CAAA;YAC1B,cAAc,GAAG,CAAC,CAAA;YAClB,IAAI,CAAC;gBACJ,iBAAiB,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC;oBAChD,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,QAAQ;oBACR,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;iBACjB,CAAC,CAAA;YACH,CAAC;YAAC,MAAM,CAAC;gBACR,iBAAiB,GAAG,IAAI,CAAA;YACzB,CAAC;YACD,IAAI,CAAC;gBACJ,iEAAiE;gBACjE,IAAI,IAAI,CAAC,KAAK;oBAAE,MAAM,IAAI,CAAC,KAAK,EAAE,CAAA;gBAClC,iEAAiE;gBACjE,+DAA+D;gBAC/D,IAAI,CAAC,MAAM;oBAAE,MAAM,YAAY,CAAC,IAAI,CAAC,CAAA;YACtC,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACZ,mEAAmE;gBACnE,uEAAuE;gBACvE,wEAAwE;gBACxE,sEAAsE;gBACtE,MAAM,kBAAkB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;gBACrC,IAAI,iBAAiB,EAAE,CAAC;oBACvB,IAAI,CAAC;wBACJ,MAAM,QAAQ,CAAC,cAAc,CAAC;4BAC7B,UAAU,EAAE,iBAAiB;4BAC7B,MAAM,EAAE,QAAQ;4BAChB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,oBAAoB;4BAC7C,QAAQ,EAAE,CAAC;yBACX,CAAC,CAAA;oBACH,CAAC;oBAAC,MAAM,CAAC;wBACR,cAAc;oBACf,CAAC;oBACD,iBAAiB,GAAG,IAAI,CAAA;gBACzB,CAAC;gBACD,MAAM,CAAC,CAAA;YACR,CAAC;QACF,CAAC,EAAE,MAAM,CAAC,CAAA;QAEV,QAAQ,CAAC,KAAK,IAAI,EAAE;YACnB,IAAI,CAAC;gBACJ,MAAM,SAAS,EAAE,CAAA;YAClB,CAAC;YAAC,MAAM,CAAC;gBACR,sBAAsB;YACvB,CAAC;YACD,kEAAkE;YAClE,uEAAuE;YACvE,wEAAwE;YACxE,qBAAqB;YACrB,IAAI,sBAAsB,GAAG,CAAC,EAAE,CAAC;gBAChC,OAAO,CAAC,IAAI,CACX,qBAAqB,IAAI,CAAC,IAAI,SAAS,sBAAsB,qBAAqB;sBAChF,6EAA6E,CAC/E,CAAA;YACF,CAAC;YACD,IAAI,iBAAiB,EAAE,CAAC;gBACvB,8DAA8D;gBAC9D,4DAA4D;gBAC5D,+DAA+D;gBAC/D,uCAAuC;gBACvC,IAAI,CAAC;oBACJ,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAA;gBACvB,CAAC;gBAAC,MAAM,CAAC;oBACR,cAAc;gBACf,CAAC;gBACD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,oBAAoB,CAAA;gBACpD,MAAM,MAAM,GAAG,uBAAuB,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAA;gBAChE,IAAI,CAAC;oBACJ,6DAA6D;oBAC7D,mDAAmD;oBACnD,MAAM,QAAQ,CAAC,cAAc,CAAC,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,GAAG,CAAC,EAAE,CAAC,CAAA;gBACnH,CAAC;gBAAC,MAAM,CAAC;oBACR,cAAc;gBACf,CAAC;YACF,CAAC;YACD,iBAAiB,GAAG,IAAI,CAAA;QACzB,CAAC,EAAE,MAAM,CAAC,CAAA;QAEV,IAAI,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,EAAyB,CAAA;YACtC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,8BAA8B,CAAA;YAC9D,uEAAuE;YACvE,yEAAyE;YACzE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,CAAA;YAC/F,sEAAsE;YACtE,wEAAwE;YACxE,IAAI,OAAO,GAAG,CAAC,CAAC,CAAA;YAChB,IAAI,CAAC,aAAa,EAAE,KAAK,IAAI,EAAE;gBAC9B,OAAO,EAAE,CAAA;gBACT,cAAc,GAAG,OAAO,CAAA;gBACxB,uBAAuB,GAAG,CAAC,CAAA;gBAC3B,sBAAsB,GAAG,CAAC,CAAA;gBAC1B,sBAAsB,GAAG,CAAC,CAAA;gBAC1B,IAAI,CAAC;oBACJ,MAAM,YAAY,CAAC,IAAI,CAAC,CAAA;gBACzB,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACZ,oEAAoE;oBACpE,iEAAiE;oBACjE,MAAM,kBAAkB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;oBACrC,MAAM,CAAC,CAAA;gBACR,CAAC;gBACD,MAAM,IAAI,EAAE,CAAA;YACb,CAAC,EAAE,WAAW,CAAC,CAAA;QAChB,CAAC;aAAM,CAAC;YACP,kEAAkE;YAClE,yCAAyC;YACzC,EAAE,EAAE,CAAA;QACL,CAAC;IACF,CAAC,CAAC,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,YAAY,CAAC,IAAqB;IAChD,MAAM,IAAI,GAAG,MAAM,UAAU,EAAE,CAAA;IAC/B,oEAAoE;IACpE,0EAA0E;IAC1E,0BAA0B;IAC1B,MAAM,KAAK,GAAG,MAAM,aAAa,EAAE,CAAA;IACnC,IAAI,KAAK;QAAE,MAAM,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,IAAI,cAAc,CAAA;IACvC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IACrD,8EAA8E;IAC9E,+EAA+E;IAC/E,4EAA4E;IAC5E,8BAA8B;IAC9B,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAA;AACxD,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,kBAAkB,CAAC,QAAkB,EAAE,CAAU;IAC/D,uBAAuB,EAAE,CAAA;IACzB,IAAI,CAAC,iBAAiB;QAAE,OAAM;IAC9B,IAAI,CAAC;QACJ,MAAM,QAAQ,CAAC,UAAU,CAAC;YACzB,UAAU,EAAE,iBAAiB;YAC7B,OAAO,EAAE,cAAc;YACvB,QAAQ,EAAE,sBAAsB,EAAE;YAClC,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,gBAAgB;YACtB,MAAM,EAAE,QAAQ;YAChB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,oBAAoB;YAC7C,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;SACjD,CAAC,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACR,yEAAyE;IAC1E,CAAC;AACF,CAAC;AAsCD,KAAK,UAAU,OAAO,CAAC,IAAa;IACnC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,uEAAuE;IACvE,MAAM,QAAQ,GAAG,sBAAsB,EAAE,CAAA;IACzC,6EAA6E;IAC7E,kEAAkE;IAClE,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,EAAE,KAAK,SAAS,CAAA;IAEhE,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,6BAA6B;IAC7B,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACd,sBAAsB,EAAE,CAAA;QACxB,IAAI,iBAAiB,EAAE,CAAC;YACvB,KAAK,QAAQ,CAAC,UAAU,CAAC;gBACxB,UAAU,EAAE,iBAAiB;gBAC7B,OAAO,EAAE,cAAc;gBACvB,QAAQ;gBACR,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM,EAAE,SAAS;gBACjB,UAAU,EAAE,CAAC;gBACb,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM;gBAC7B,MAAM,EAAE,IAAI,CAAC,MAAM;aACnB,CAAC,CAAA;QACH,CAAC;QACD,OAAM;IACP,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACxB,IAAI,MAAM,GAAe,QAAQ,CAAA;IACjC,IAAI,KAAyB,CAAA;IAC7B,IAAI,CAAC;QACJ,MAAM,IAAI,CAAC,EAAE,EAAE,CAAA;QACf,gEAAgE;QAChE,oEAAoE;QACpE,wCAAwC;QACxC,IAAI,KAAK;YAAE,MAAM,GAAG,WAAW,CAAA;IAChC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,KAAK,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAClD,IAAI,KAAK,EAAE,CAAC;YACX,sEAAsE;YACtE,uEAAuE;YACvE,mEAAmE;YACnE,iDAAiD;YACjD,MAAM,GAAG,OAAO,CAAA;QACjB,CAAC;aAAM,CAAC;YACP,MAAM,GAAG,QAAQ,CAAA;YACjB,uBAAuB,EAAE,CAAA;YACzB,MAAM,CAAC,CAAA;QACR,CAAC;IACF,CAAC;YAAS,CAAC;QACV,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAA;QACrC,IAAI,cAAkC,CAAA;QACtC,IAAI,CAAC;YACJ,cAAc,GAAG,MAAM,UAAU,EAAE,CAAA;QACpC,CAAC;QAAC,MAAM,CAAC;YACR,6CAA6C;QAC9C,CAAC;QACD,IAAI,iBAAiB,EAAE,CAAC;YACvB,KAAK,QAAQ,CAAC,UAAU,CAAC;gBACxB,UAAU,EAAE,iBAAiB;gBAC7B,OAAO,EAAE,cAAc;gBACvB,QAAQ;gBACR,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM;gBACN,UAAU;gBACV,KAAK;gBACL,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM;gBAC7B,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,cAAc;aACd,CAAC,CAAA;QACH,CAAC;IACF,CAAC;AACF,CAAC;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,MAAM,IAAI,GAAwB,MAAM,CAAC,MAAM,CACrD,CAAC,IAAY,EAAE,IAA6B,EAAE,IAAe,EAAiB,EAAE;IAC/E,IAAI,OAAO,IAAI,KAAK,UAAU,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;IACjD,CAAC;IACD,OAAO,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;AACjE,CAAC,EACD;IACC,KAAK,EAAE,CAAC,IAAY,EAAE,MAAc,EAAE,EAAY,EAAiB,EAAE,CACpE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAC5C,OAAO,EAAE,CAAC,IAAY,EAAE,MAAc,EAAE,QAAuB,EAAiB,EAAE,CACjF,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;CAClD,CACD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,SAAS,GAKlB,MAAM,CAAC,MAAM,CAChB,CAAC,IAAY,EAAE,EAAY,EAAiB,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EACvF;IACC,IAAI,EAAE,CAAC,IAAY,EAAE,IAAa,EAAiB,EAAE,CACpD,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;IAC5E,OAAO,EAAE,CAAC,IAAY,EAAE,MAAc,EAAiB,EAAE,CACxD,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC7C,KAAK,EAAE,CAAC,IAAY,EAAE,MAAc,EAAE,EAAY,EAAiB,EAAE,CACpE,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;CACjD,CACD,CAAA"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { back, currentPath, currentUrl, forward, open, reload } from './navigati
|
|
|
6
6
|
|
|
7
7
|
export { getPage, getContext } from './context.js'
|
|
8
8
|
|
|
9
|
-
export { browserTest, invariant, step } from './scenario.js'
|
|
9
|
+
export { browserTest, DEFAULT_WALKTHROUGH_TIMEOUT_MS, invariant, step } from './scenario.js'
|
|
10
10
|
export type { BrowserTestMeta, StepContract } from './scenario.js'
|
|
11
11
|
|
|
12
12
|
export { getReporter, setReporter, configureFromEnv } from './reporter.js'
|
package/src/reporter.ts
CHANGED
|
@@ -37,6 +37,12 @@ export interface ReporterConfig {
|
|
|
37
37
|
|
|
38
38
|
export interface StepEvent {
|
|
39
39
|
scenarioId: string
|
|
40
|
+
/**
|
|
41
|
+
* 0-based retry attempt that produced this step. The platform shows only the
|
|
42
|
+
* final attempt's steps; earlier attempts are kept for forensics. Defaults
|
|
43
|
+
* to 0 on the platform side when omitted (older clients).
|
|
44
|
+
*/
|
|
45
|
+
attempt?: number
|
|
40
46
|
/** Authoring order within the scenario, assigned at step() call time. */
|
|
41
47
|
sequence: number
|
|
42
48
|
/**
|
|
@@ -80,6 +86,11 @@ export interface ScenarioFinish {
|
|
|
80
86
|
scenarioId: string
|
|
81
87
|
status: 'passed' | 'failed'
|
|
82
88
|
durationMs: number
|
|
89
|
+
/**
|
|
90
|
+
* Total attempts the scenario took (>= 1). A passed scenario with
|
|
91
|
+
* `attempts > 1` is flaky. Omitted ⇒ the platform defaults it to 1.
|
|
92
|
+
*/
|
|
93
|
+
attempts?: number
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
export interface Reporter {
|
|
@@ -171,6 +182,7 @@ class HttpReporter implements Reporter {
|
|
|
171
182
|
? await this.encodeScreenshot(event.screenshotPath)
|
|
172
183
|
: undefined
|
|
173
184
|
await this.fetch('POST', `/api/v1/runs/${runId}/scenarios/${event.scenarioId}/steps`, {
|
|
185
|
+
attempt: event.attempt,
|
|
174
186
|
sequence: event.sequence,
|
|
175
187
|
kind: event.kind,
|
|
176
188
|
name: event.name,
|
|
@@ -190,6 +202,7 @@ class HttpReporter implements Reporter {
|
|
|
190
202
|
await this.fetch('PATCH', `/api/v1/runs/${runId}/scenarios/${input.scenarioId}`, {
|
|
191
203
|
status: input.status,
|
|
192
204
|
durationMs: input.durationMs,
|
|
205
|
+
attempts: input.attempts,
|
|
193
206
|
})
|
|
194
207
|
}
|
|
195
208
|
|
package/src/scenario.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from 'node:module'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { closePage, getContext, launchPage } from './context.js'
|
|
4
4
|
import { screenshot } from './element.js'
|
|
5
|
-
import { getReporter } from './reporter.js'
|
|
5
|
+
import { getReporter, type Reporter } from './reporter.js'
|
|
6
6
|
import { loadUserSetup } from './setup.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -51,8 +51,39 @@ export interface BrowserTestMeta {
|
|
|
51
51
|
seeds?: string[]
|
|
52
52
|
/** Identities / roles the scenario acts as, e.g. `['crmOperator']`. */
|
|
53
53
|
roles?: string[]
|
|
54
|
+
/**
|
|
55
|
+
* One-time scenario setup, run once before the walkthrough (in `beforeAll`) —
|
|
56
|
+
* the place for "establish a precondition the steps assume", e.g. minting
|
|
57
|
+
* auth tokens. Replaces a hand-written `beforeAll` in the body form. Runs
|
|
58
|
+
* before any browser navigation, so it can register cookies/identity the
|
|
59
|
+
* first paint needs.
|
|
60
|
+
*/
|
|
61
|
+
setup?: () => void | Promise<void>
|
|
62
|
+
/**
|
|
63
|
+
* Per-scenario retry budget (body form only). A flaky scenario that fails
|
|
64
|
+
* then passes within the budget is reported as **passed but flaky** (the
|
|
65
|
+
* dashboard badges it). Each attempt gets a fresh browser + a clean
|
|
66
|
+
* navigation, so a retry can't inherit the failed attempt's page state.
|
|
67
|
+
*
|
|
68
|
+
* Omit to inherit the global default (`opice test --retries=N` / `bun test
|
|
69
|
+
* --retry=N`). Ignored by the legacy registrar form (it can't be retried
|
|
70
|
+
* cleanly — it shares one browser across its `test()` blocks).
|
|
71
|
+
*/
|
|
72
|
+
retries?: number
|
|
73
|
+
/**
|
|
74
|
+
* Per-scenario timeout (ms) for the walkthrough body. Defaults to
|
|
75
|
+
* {@link DEFAULT_WALKTHROUGH_TIMEOUT_MS}. Body form only.
|
|
76
|
+
*/
|
|
77
|
+
timeout?: number
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Default timeout for a walkthrough body. A real browser walk — first page
|
|
82
|
+
* load, async data, a dev server compiling a chunk on first hit — easily
|
|
83
|
+
* exceeds bun's 5s default; each retrying assertion still bounds itself.
|
|
84
|
+
*/
|
|
85
|
+
export const DEFAULT_WALKTHROUGH_TIMEOUT_MS = 60_000
|
|
86
|
+
|
|
56
87
|
/**
|
|
57
88
|
* Best-effort capture of the `*.test.ts` path that called `browserTest`, by
|
|
58
89
|
* walking the stack for the first `.test.` frame. Reported so a failed
|
|
@@ -85,19 +116,38 @@ let currentScenarioPending = 0
|
|
|
85
116
|
// fire-and-forget and would otherwise be sequenced by arrival order at the
|
|
86
117
|
// worker, which screenshot-encoding latency can reshuffle.
|
|
87
118
|
let currentScenarioStepSeq = 0
|
|
119
|
+
// 0-based index of the current attempt. In the body form the walkthrough wrapper
|
|
120
|
+
// bumps it on every (re-)invocation, so steps carry the attempt that produced
|
|
121
|
+
// them and the dashboard shows only the final one. The legacy form never
|
|
122
|
+
// retries, so it stays 0.
|
|
123
|
+
let currentAttempt = 0
|
|
88
124
|
|
|
89
125
|
/**
|
|
90
|
-
* Register a top-level browser test scenario.
|
|
126
|
+
* Register a top-level browser test scenario. Two forms, picked automatically:
|
|
127
|
+
*
|
|
128
|
+
* **Body form (preferred)** — pass an **async** function; it IS the walkthrough:
|
|
129
|
+
*
|
|
130
|
+
* browserTest({ name: '…', retries: 2, setup: () => mintTokens() }, async () => {
|
|
131
|
+
* await step('…', async () => { … })
|
|
132
|
+
* })
|
|
133
|
+
*
|
|
134
|
+
* `browserTest` owns the single `test('walkthrough', …)` call, so it honours
|
|
135
|
+
* `meta.retries` (bun `{ retry }`) and `meta.timeout`. Each attempt opens a
|
|
136
|
+
* **fresh** browser context + clean navigation, so a retry never inherits the
|
|
137
|
+
* failed attempt's page state. `meta.setup` runs once before the walkthrough.
|
|
91
138
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
139
|
+
* **Legacy registrar form** — pass a **sync** function that registers its own
|
|
140
|
+
* `beforeAll`/`test`/`describe` blocks (the old multi-test pattern). The browser
|
|
141
|
+
* is launched once in `beforeAll` and shared across those blocks. It can't be
|
|
142
|
+
* retried cleanly (shared state), so `meta.retries` is ignored.
|
|
96
143
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
144
|
+
* The two are told apart by whether `fn` is an `AsyncFunction`: a walkthrough
|
|
145
|
+
* body always awaits its steps; a registrar never needs to be async.
|
|
146
|
+
*
|
|
147
|
+
* Metadata is the **first** argument (`{ name, url, hash, feature, seeds, roles,
|
|
148
|
+
* setup, retries, timeout }`); `name` is required.
|
|
99
149
|
*/
|
|
100
|
-
export function browserTest(meta: BrowserTestMeta, fn: () => void): void {
|
|
150
|
+
export function browserTest(meta: BrowserTestMeta, fn: () => void | Promise<void>): void {
|
|
101
151
|
if (typeof meta === 'string') {
|
|
102
152
|
// Migration aid: the old signature was `browserTest(name, fn, options)`.
|
|
103
153
|
throw new Error(
|
|
@@ -110,14 +160,18 @@ export function browserTest(meta: BrowserTestMeta, fn: () => void): void {
|
|
|
110
160
|
}
|
|
111
161
|
const reporter = getReporter()
|
|
112
162
|
const testFile = captureTestFile()
|
|
113
|
-
const { describe, beforeAll, afterAll } = bunTest()
|
|
163
|
+
const { describe, beforeAll, afterAll, test } = bunTest()
|
|
164
|
+
// An async fn is the walkthrough body (browserTest owns its test()); a sync
|
|
165
|
+
// fn is the legacy registrar (it registers its own test()/hooks).
|
|
166
|
+
const isBody = fn.constructor.name === 'AsyncFunction'
|
|
114
167
|
|
|
115
168
|
describe(meta.name, () => {
|
|
116
169
|
beforeAll(async () => {
|
|
117
170
|
currentScenarioStart = Date.now()
|
|
118
|
-
currentScenarioFailures = 0
|
|
119
171
|
currentScenarioPending = 0
|
|
172
|
+
currentScenarioFailures = 0
|
|
120
173
|
currentScenarioStepSeq = 0
|
|
174
|
+
currentAttempt = 0
|
|
121
175
|
try {
|
|
122
176
|
currentScenarioId = await reporter.startScenario({
|
|
123
177
|
name: meta.name,
|
|
@@ -131,47 +185,28 @@ export function browserTest(meta: BrowserTestMeta, fn: () => void): void {
|
|
|
131
185
|
currentScenarioId = null
|
|
132
186
|
}
|
|
133
187
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
if (setup) await setup(getContext())
|
|
140
|
-
const base = meta.url ?? PLAYGROUND_URL
|
|
141
|
-
const url = meta.hash ? `${base}#${meta.hash}` : base
|
|
142
|
-
// `domcontentloaded`, not the default `load`: an SPA paints after its JS
|
|
143
|
-
// runs and may hold `load` on a slow chunk or long-lived connection, so
|
|
144
|
-
// waiting for `load` flakily times out under CI contention. Readiness is
|
|
145
|
-
// handled by the test's retrying assertions.
|
|
146
|
-
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
|
188
|
+
// One-time precondition (mint tokens, …), before any navigation.
|
|
189
|
+
if (meta.setup) await meta.setup()
|
|
190
|
+
// Body form opens the browser per attempt (in the test wrapper);
|
|
191
|
+
// the legacy registrar shares one browser, launched here once.
|
|
192
|
+
if (!isBody) await openScenario(meta)
|
|
147
193
|
} catch (e) {
|
|
148
|
-
// Setup failed before any step ran
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
// failed step so the dashboard shows *why*, finish the scenario as
|
|
154
|
-
// failed, then re-throw so the run still fails.
|
|
155
|
-
currentScenarioFailures++
|
|
194
|
+
// Setup failed before any step ran. bun:test does NOT run afterAll
|
|
195
|
+
// when beforeAll throws, so the scenario started above would otherwise
|
|
196
|
+
// sit on the dashboard as 'running' forever — record a synthetic failed
|
|
197
|
+
// step, finish it as failed here, then re-throw so the run stays red.
|
|
198
|
+
await recordSetupFailure(reporter, e)
|
|
156
199
|
if (currentScenarioId) {
|
|
157
|
-
const error = e instanceof Error ? e.message : String(e)
|
|
158
|
-
const durationMs = Date.now() - currentScenarioStart
|
|
159
200
|
try {
|
|
160
|
-
await reporter.
|
|
201
|
+
await reporter.finishScenario({
|
|
161
202
|
scenarioId: currentScenarioId,
|
|
162
|
-
sequence: currentScenarioStepSeq++,
|
|
163
|
-
kind: 'step',
|
|
164
|
-
name: 'scenario setup',
|
|
165
203
|
status: 'failed',
|
|
166
|
-
durationMs,
|
|
167
|
-
|
|
204
|
+
durationMs: Date.now() - currentScenarioStart,
|
|
205
|
+
attempts: 1,
|
|
168
206
|
})
|
|
169
|
-
await reporter.finishScenario({ scenarioId: currentScenarioId, status: 'failed', durationMs })
|
|
170
207
|
} catch {
|
|
171
|
-
// best-effort
|
|
172
|
-
// original setup error we're about to re-throw.
|
|
208
|
+
// best-effort
|
|
173
209
|
}
|
|
174
|
-
// Null it so afterAll (should it run) doesn't double-finish.
|
|
175
210
|
currentScenarioId = null
|
|
176
211
|
}
|
|
177
212
|
throw e
|
|
@@ -207,7 +242,9 @@ export function browserTest(meta: BrowserTestMeta, fn: () => void): void {
|
|
|
207
242
|
const durationMs = Date.now() - currentScenarioStart
|
|
208
243
|
const status = currentScenarioFailures > 0 ? 'failed' : 'passed'
|
|
209
244
|
try {
|
|
210
|
-
|
|
245
|
+
// attempts = final attempt index + 1. A passed scenario with
|
|
246
|
+
// attempts > 1 failed at least once first → flaky.
|
|
247
|
+
await reporter.finishScenario({ scenarioId: currentScenarioId, status, durationMs, attempts: currentAttempt + 1 })
|
|
211
248
|
} catch {
|
|
212
249
|
// best-effort
|
|
213
250
|
}
|
|
@@ -215,10 +252,84 @@ export function browserTest(meta: BrowserTestMeta, fn: () => void): void {
|
|
|
215
252
|
currentScenarioId = null
|
|
216
253
|
}, 30_000)
|
|
217
254
|
|
|
218
|
-
|
|
255
|
+
if (isBody) {
|
|
256
|
+
const body = fn as () => Promise<void>
|
|
257
|
+
const timeout = meta.timeout ?? DEFAULT_WALKTHROUGH_TIMEOUT_MS
|
|
258
|
+
// Only set `retry` when a budget is configured — leaving it unset lets
|
|
259
|
+
// bun's global `--retry` default apply; passing `retry: 0` overrides it.
|
|
260
|
+
const testOptions = meta.retries === undefined ? { timeout } : { timeout, retry: meta.retries }
|
|
261
|
+
// bun re-runs the test body for every retry attempt; `attempt` counts
|
|
262
|
+
// those invocations (0-based). Each opens a fresh browser + navigation.
|
|
263
|
+
let attempt = -1
|
|
264
|
+
test('walkthrough', async () => {
|
|
265
|
+
attempt++
|
|
266
|
+
currentAttempt = attempt
|
|
267
|
+
currentScenarioFailures = 0
|
|
268
|
+
currentScenarioStepSeq = 0
|
|
269
|
+
currentScenarioPending = 0
|
|
270
|
+
try {
|
|
271
|
+
await openScenario(meta)
|
|
272
|
+
} catch (e) {
|
|
273
|
+
// Setup failed: record it (afterAll finishes the scenario) and fail
|
|
274
|
+
// the attempt so bun retries or, once spent, leaves the run red.
|
|
275
|
+
await recordSetupFailure(reporter, e)
|
|
276
|
+
throw e
|
|
277
|
+
}
|
|
278
|
+
await body()
|
|
279
|
+
}, testOptions)
|
|
280
|
+
} else {
|
|
281
|
+
// Legacy registrar: it registers its own test()/hooks; the shared
|
|
282
|
+
// browser was opened in beforeAll above.
|
|
283
|
+
fn()
|
|
284
|
+
}
|
|
219
285
|
})
|
|
220
286
|
}
|
|
221
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Open a fresh isolated browser context + page for `meta` and navigate to its
|
|
290
|
+
* scenario URL. `launchPage()` closes any previous context first, so calling
|
|
291
|
+
* this again (a retry attempt) tears down the failed attempt's page cleanly.
|
|
292
|
+
*/
|
|
293
|
+
async function openScenario(meta: BrowserTestMeta): Promise<void> {
|
|
294
|
+
const page = await launchPage()
|
|
295
|
+
// Repo-level context setup (browser-setup.ts) runs before the first
|
|
296
|
+
// navigation, so an addInitScript it registers fires before the app's own
|
|
297
|
+
// scripts on first paint.
|
|
298
|
+
const setup = await loadUserSetup()
|
|
299
|
+
if (setup) await setup(getContext())
|
|
300
|
+
const base = meta.url ?? PLAYGROUND_URL
|
|
301
|
+
const url = meta.hash ? `${base}#${meta.hash}` : base
|
|
302
|
+
// `domcontentloaded`, not the default `load`: an SPA paints after its JS runs
|
|
303
|
+
// and may hold `load` on a slow chunk or long-lived connection, so waiting for
|
|
304
|
+
// `load` flakily times out under CI contention. Readiness is handled by the
|
|
305
|
+
// test's retrying assertions.
|
|
306
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Record a synthetic failed 'scenario setup' step for the current attempt and
|
|
311
|
+
* count it toward scenario failures. Does NOT finish the scenario (the caller
|
|
312
|
+
* decides whether afterAll will, or whether it must finish inline).
|
|
313
|
+
*/
|
|
314
|
+
async function recordSetupFailure(reporter: Reporter, e: unknown): Promise<void> {
|
|
315
|
+
currentScenarioFailures++
|
|
316
|
+
if (!currentScenarioId) return
|
|
317
|
+
try {
|
|
318
|
+
await reporter.recordStep({
|
|
319
|
+
scenarioId: currentScenarioId,
|
|
320
|
+
attempt: currentAttempt,
|
|
321
|
+
sequence: currentScenarioStepSeq++,
|
|
322
|
+
kind: 'step',
|
|
323
|
+
name: 'scenario setup',
|
|
324
|
+
status: 'failed',
|
|
325
|
+
durationMs: Date.now() - currentScenarioStart,
|
|
326
|
+
error: e instanceof Error ? e.message : String(e),
|
|
327
|
+
})
|
|
328
|
+
} catch {
|
|
329
|
+
// best-effort: reporting the failure must never mask the original error.
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
222
333
|
type StepStatus = 'passed' | 'failed' | 'fixme' | 'fixmepass' | 'pending'
|
|
223
334
|
type StepKind = 'step' | 'invariant'
|
|
224
335
|
|
|
@@ -273,6 +384,7 @@ async function runUnit(unit: RunUnit): Promise<void> {
|
|
|
273
384
|
if (currentScenarioId) {
|
|
274
385
|
void reporter.recordStep({
|
|
275
386
|
scenarioId: currentScenarioId,
|
|
387
|
+
attempt: currentAttempt,
|
|
276
388
|
sequence,
|
|
277
389
|
kind: unit.kind,
|
|
278
390
|
name: unit.name,
|
|
@@ -318,6 +430,7 @@ async function runUnit(unit: RunUnit): Promise<void> {
|
|
|
318
430
|
if (currentScenarioId) {
|
|
319
431
|
void reporter.recordStep({
|
|
320
432
|
scenarioId: currentScenarioId,
|
|
433
|
+
attempt: currentAttempt,
|
|
321
434
|
sequence,
|
|
322
435
|
kind: unit.kind,
|
|
323
436
|
name: unit.name,
|