@opice/harness 0.3.0 → 0.4.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/README.md +2 -2
- package/dist/accessible.d.ts +9 -2
- package/dist/accessible.d.ts.map +1 -1
- package/dist/accessible.js +9 -3
- package/dist/accessible.js.map +1 -1
- package/dist/index.d.ts +2 -2
- 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 +20 -4
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +5 -1
- package/dist/reporter.js.map +1 -1
- package/dist/scenario.d.ts +114 -12
- package/dist/scenario.d.ts.map +1 -1
- package/dist/scenario.js +162 -34
- package/dist/scenario.js.map +1 -1
- package/package.json +1 -1
- package/src/accessible.ts +11 -3
- package/src/index.ts +2 -2
- package/src/reporter.ts +25 -5
- package/src/scenario.ts +262 -40
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Runs under the Bun test runner.
|
|
|
19
19
|
import { test, describe } from 'bun:test'
|
|
20
20
|
import { browserTest, el, byRole, byLabel, step, expect } from '@opice/harness'
|
|
21
21
|
|
|
22
|
-
browserTest('DataGrid', () => {
|
|
22
|
+
browserTest({ name: 'DataGrid', hash: 'datagrid' }, () => {
|
|
23
23
|
test('renders and is interactive', async () => {
|
|
24
24
|
await step('table is visible', async () => {
|
|
25
25
|
await expect(el('datagrid-table')).toBeVisible()
|
|
@@ -30,7 +30,7 @@ browserTest('DataGrid', () => {
|
|
|
30
30
|
await expect(el('datagrid-row-0')).toHaveAttribute('data-highlighted', '')
|
|
31
31
|
})
|
|
32
32
|
}, 60_000)
|
|
33
|
-
}
|
|
33
|
+
})
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
The DSL is **async** and returns Playwright `Locator`s, so the full Locator API
|
package/dist/accessible.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Locator, Page } from 'playwright';
|
|
2
2
|
/** The ARIA role union accepted by Playwright's `getByRole`. */
|
|
3
3
|
type Role = Parameters<Page['getByRole']>[0];
|
|
4
|
+
/** Playwright's `getByRole` options minus `name` (which is passed positionally). */
|
|
5
|
+
type RoleOptions = Omit<NonNullable<Parameters<Page['getByRole']>[1]>, 'name'>;
|
|
4
6
|
/**
|
|
5
7
|
* Accessible-name selectors — `byRole` / `byLabel` / `byText`.
|
|
6
8
|
*
|
|
@@ -17,9 +19,14 @@ type Role = Parameters<Page['getByRole']>[0];
|
|
|
17
19
|
*/
|
|
18
20
|
/**
|
|
19
21
|
* Find an element by ARIA role and (optionally) its accessible name.
|
|
20
|
-
* `name` does a substring, case-insensitive match
|
|
22
|
+
* `name` does a substring, case-insensitive match when a string; pass a
|
|
23
|
+
* `RegExp` to match the accessible name by pattern (e.g. a generated id).
|
|
24
|
+
*
|
|
25
|
+
* `options` forwards the rest of Playwright's `getByRole` filters — most useful
|
|
26
|
+
* is `level` to pin a heading (`byRole('heading', 'Title', { level: 2 })`), plus
|
|
27
|
+
* `exact`, `checked`, `pressed`, `expanded`, `disabled`, etc.
|
|
21
28
|
*/
|
|
22
|
-
export declare function byRole(role: Role, name?: string): Locator;
|
|
29
|
+
export declare function byRole(role: Role, name?: string | RegExp, options?: RoleOptions): Locator;
|
|
23
30
|
/** Find a form control by its associated `<label>` (or `aria-label`) text. */
|
|
24
31
|
export declare function byLabel(text: string): Locator;
|
|
25
32
|
/** Find an element by its visible text (substring, case-insensitive). */
|
package/dist/accessible.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessible.d.ts","sourceRoot":"","sources":["../src/accessible.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAG/C,gEAAgE;AAChE,KAAK,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"accessible.d.ts","sourceRoot":"","sources":["../src/accessible.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAG/C,gEAAgE;AAChE,KAAK,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5C,oFAAoF;AACpF,KAAK,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;AAE9E;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAGzF;AAED,8EAA8E;AAC9E,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED,yEAAyE;AACzE,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5C"}
|
package/dist/accessible.js
CHANGED
|
@@ -15,10 +15,16 @@ import { getPage } from './context.js';
|
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
17
|
* Find an element by ARIA role and (optionally) its accessible name.
|
|
18
|
-
* `name` does a substring, case-insensitive match
|
|
18
|
+
* `name` does a substring, case-insensitive match when a string; pass a
|
|
19
|
+
* `RegExp` to match the accessible name by pattern (e.g. a generated id).
|
|
20
|
+
*
|
|
21
|
+
* `options` forwards the rest of Playwright's `getByRole` filters — most useful
|
|
22
|
+
* is `level` to pin a heading (`byRole('heading', 'Title', { level: 2 })`), plus
|
|
23
|
+
* `exact`, `checked`, `pressed`, `expanded`, `disabled`, etc.
|
|
19
24
|
*/
|
|
20
|
-
export function byRole(role, name) {
|
|
21
|
-
|
|
25
|
+
export function byRole(role, name, options) {
|
|
26
|
+
const opts = { ...options, ...(name == null ? {} : { name }) };
|
|
27
|
+
return getPage().getByRole(role, Object.keys(opts).length > 0 ? opts : undefined);
|
|
22
28
|
}
|
|
23
29
|
/** Find a form control by its associated `<label>` (or `aria-label`) text. */
|
|
24
30
|
export function byLabel(text) {
|
package/dist/accessible.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessible.js","sourceRoot":"","sources":["../src/accessible.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"accessible.js","sourceRoot":"","sources":["../src/accessible.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAOtC;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,MAAM,CAAC,IAAU,EAAE,IAAsB,EAAE,OAAqB;IAC/E,MAAM,IAAI,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;IAC9D,OAAO,OAAO,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAClF,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,OAAO,CAAC,IAAY;IACnC,OAAO,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;AAClC,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,MAAM,CAAC,IAAY;IAClC,OAAO,OAAO,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;AACjC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ 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, step } from './scenario.js';
|
|
6
|
-
export type {
|
|
5
|
+
export { browserTest, invariant, step } from './scenario.js';
|
|
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';
|
|
9
9
|
export { parseOpiceDsn } from './dsn.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,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,SAAS,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AAC5D,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, step } from './scenario.js';
|
|
5
|
+
export { browserTest, 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,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,SAAS,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AAG5D,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
|
@@ -26,16 +26,27 @@ export interface StepEvent {
|
|
|
26
26
|
scenarioId: string;
|
|
27
27
|
/** Authoring order within the scenario, assigned at step() call time. */
|
|
28
28
|
sequence: number;
|
|
29
|
+
/**
|
|
30
|
+
* 'step' (a procedural step) or 'invariant' (a scenario-level acceptance).
|
|
31
|
+
* The platform may render invariants distinctly; older workers ignore it.
|
|
32
|
+
*/
|
|
33
|
+
kind?: 'step' | 'invariant';
|
|
29
34
|
name: string;
|
|
30
35
|
/**
|
|
31
36
|
* 'fixme' (a step.fixme that failed, as expected) and 'fixmepass' (a
|
|
32
37
|
* step.fixme that unexpectedly passed) are tolerated warnings — neither
|
|
33
|
-
* fails the scenario.
|
|
38
|
+
* fails the scenario. 'pending' is a phase-1 stub that never ran (no body
|
|
39
|
+
* yet); a scenario carrying one reads as 'incomplete'.
|
|
34
40
|
*/
|
|
35
|
-
status: 'passed' | 'failed' | 'fixme' | 'fixmepass';
|
|
41
|
+
status: 'passed' | 'failed' | 'fixme' | 'fixmepass' | 'pending';
|
|
36
42
|
durationMs: number;
|
|
37
43
|
error?: string;
|
|
38
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Durable rationale carried from the unit's contract (phase-1 `intent`) —
|
|
46
|
+
* why it exists / what it proves. Surfaced on the dashboard.
|
|
47
|
+
*/
|
|
48
|
+
intent?: string;
|
|
49
|
+
/** Mandatory note from .fixme — why the failure is tolerated. */
|
|
39
50
|
reason?: string;
|
|
40
51
|
screenshotPath?: string;
|
|
41
52
|
}
|
|
@@ -43,7 +54,12 @@ export interface ScenarioStart {
|
|
|
43
54
|
name: string;
|
|
44
55
|
hash?: string;
|
|
45
56
|
testFile?: string;
|
|
46
|
-
|
|
57
|
+
/** Requirement / feature id this scenario covers (grouping). */
|
|
58
|
+
feature?: string;
|
|
59
|
+
/** Seeds required for the scenario (machine-checkable preconditions). */
|
|
60
|
+
seeds?: string[];
|
|
61
|
+
/** Identities / roles the scenario acts as. */
|
|
62
|
+
roles?: string[];
|
|
47
63
|
}
|
|
48
64
|
export interface ScenarioFinish {
|
|
49
65
|
scenarioId: 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,IAAI,EAAE,MAAM,CAAA;IACZ
|
|
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;CAClB;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;AAmKD,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
|
@@ -73,7 +73,9 @@ class HttpReporter {
|
|
|
73
73
|
name: input.name,
|
|
74
74
|
hash: input.hash,
|
|
75
75
|
testFile: input.testFile,
|
|
76
|
-
|
|
76
|
+
feature: input.feature,
|
|
77
|
+
seeds: input.seeds,
|
|
78
|
+
roles: input.roles,
|
|
77
79
|
});
|
|
78
80
|
return response['scenarioId'];
|
|
79
81
|
}
|
|
@@ -92,10 +94,12 @@ class HttpReporter {
|
|
|
92
94
|
: undefined;
|
|
93
95
|
await this.fetch('POST', `/api/v1/runs/${runId}/scenarios/${event.scenarioId}/steps`, {
|
|
94
96
|
sequence: event.sequence,
|
|
97
|
+
kind: event.kind,
|
|
95
98
|
name: event.name,
|
|
96
99
|
status: event.status,
|
|
97
100
|
durationMs: event.durationMs,
|
|
98
101
|
error: event.error,
|
|
102
|
+
intent: event.intent,
|
|
99
103
|
reason: event.reason,
|
|
100
104
|
screenshot,
|
|
101
105
|
});
|
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;AAkE9B,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,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;SAC5B,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
|
@@ -1,24 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Scenario metadata — the **first** argument to `browserTest`.
|
|
3
|
+
*
|
|
4
|
+
* This is the durable, machine-relevant context an opice scenario carries
|
|
5
|
+
* independent of its concrete steps: where it runs, what it presupposes, what
|
|
6
|
+
* requirement it covers. It is written in **phase 1** (planning, `opice-plan`)
|
|
7
|
+
* and preserved through **phase 2** (authoring, `opice-author`) — the scenario
|
|
8
|
+
* file IS the spec, so this metadata never lives in a separate `.md` that can
|
|
9
|
+
* drift from the test.
|
|
10
|
+
*
|
|
11
|
+
* The rule of thumb for what belongs here vs. a code comment: *does anything
|
|
12
|
+
* other than a human read it?* Seeds (a precondition a runner could verify),
|
|
13
|
+
* the feature id (grouping on the dashboard), the acting roles — yes, so they
|
|
14
|
+
* are first-class fields. Background rationale that only a human reads stays a
|
|
15
|
+
* comment next to the relevant step.
|
|
16
|
+
*/
|
|
17
|
+
export interface BrowserTestMeta {
|
|
18
|
+
/** Scenario name — becomes the `describe()` title. Required. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Override base URL (defaults to the `PLAYGROUND_URL` env var). */
|
|
5
21
|
url?: string;
|
|
22
|
+
/** Hash fragment appended to the base URL (e.g. `'datagrid'`). */
|
|
23
|
+
hash?: string;
|
|
24
|
+
/** Feature / requirement id this scenario covers (e.g. `'F-SML-03a'`). */
|
|
25
|
+
feature?: string;
|
|
6
26
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* If omitted, defaults to the test file path with `.test.ts` → `.scenario.md`.
|
|
27
|
+
* Seeds that must be loaded for this scenario to run — machine-checkable
|
|
28
|
+
* preconditions, not prose. e.g. `['initial-data', 'crm-master-data']`.
|
|
10
29
|
*/
|
|
11
|
-
|
|
30
|
+
seeds?: string[];
|
|
31
|
+
/** Identities / roles the scenario acts as, e.g. `['crmOperator']`. */
|
|
32
|
+
roles?: string[];
|
|
12
33
|
}
|
|
13
34
|
/**
|
|
14
35
|
* Register a top-level browser test scenario.
|
|
15
36
|
*
|
|
16
|
-
* Each `browserTest(
|
|
37
|
+
* Each `browserTest(meta, fn)` launches its own isolated Playwright browser +
|
|
17
38
|
* context + page, navigates to the playground URL, runs the given `fn` (which
|
|
18
39
|
* typically contains nested `describe`/`test` blocks), and tears the browser
|
|
19
40
|
* down in `afterAll`.
|
|
41
|
+
*
|
|
42
|
+
* Metadata is the **first** argument (`{ name, url, hash, feature, seeds,
|
|
43
|
+
* roles }`); `name` is required.
|
|
20
44
|
*/
|
|
21
|
-
export declare function browserTest(
|
|
45
|
+
export declare function browserTest(meta: BrowserTestMeta, fn: () => void): void;
|
|
46
|
+
/**
|
|
47
|
+
* The durable contract of a step or invariant, separate from its mechanics.
|
|
48
|
+
*
|
|
49
|
+
* `intent` is written in **phase 1** and survives **verbatim** into the
|
|
50
|
+
* authored test — it's the "why this exists / what it proves", the independent
|
|
51
|
+
* statement of intent that the concrete body is checked against. `hint` is
|
|
52
|
+
* phase-1 scaffolding *for the authoring agent* ("what to actually do here");
|
|
53
|
+
* it is consumed when the step is authored and dropped once a body exists.
|
|
54
|
+
*/
|
|
55
|
+
export interface StepContract {
|
|
56
|
+
/** Durable rationale: why this step/invariant exists, what it proves. */
|
|
57
|
+
intent?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Phase-1 instruction to the authoring agent — what to do on the page here.
|
|
60
|
+
* Ephemeral: drop it once the body is written.
|
|
61
|
+
*/
|
|
62
|
+
hint?: string;
|
|
63
|
+
}
|
|
64
|
+
type StepBody = () => void | Promise<void>;
|
|
65
|
+
interface StepFn {
|
|
66
|
+
/** Executable step. */
|
|
67
|
+
(name: string, fn: StepBody): Promise<void>;
|
|
68
|
+
/** Phase-1 stub: a step with a contract but no body yet (status: pending). */
|
|
69
|
+
(name: string, contract: StepContract): Promise<void>;
|
|
70
|
+
/** Authored step that keeps its durable contract. */
|
|
71
|
+
(name: string, contract: StepContract, fn: StepBody): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
interface StepExtras {
|
|
74
|
+
fixme: (name: string, reason: string, fn: StepBody) => Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* A **blocked** pending stub: the step can't be authored yet because the app
|
|
77
|
+
* feature it covers isn't implemented. Reports as 'pending' with the reason
|
|
78
|
+
* attached (the dashboard shows it as blocked, distinct from a plain stub
|
|
79
|
+
* that's merely awaiting a test). `reason` is mandatory — say what's missing.
|
|
80
|
+
*/
|
|
81
|
+
blocked: (name: string, reason: string, contract?: StepContract) => Promise<void>;
|
|
82
|
+
}
|
|
22
83
|
/**
|
|
23
84
|
* A reportable step inside a scenario. Captures duration + screenshot on
|
|
24
85
|
* finish, forwards to the active reporter (no-op unless configured via env).
|
|
@@ -26,6 +87,14 @@ export declare function browserTest(name: string, fn: () => void, options?: Brow
|
|
|
26
87
|
* The body may be sync or async; `step` always returns a promise, so call it
|
|
27
88
|
* with `await step('…', async () => { … })`.
|
|
28
89
|
*
|
|
90
|
+
* Three forms:
|
|
91
|
+
* - `step(name, fn)` — executable step (the common case).
|
|
92
|
+
* - `step(name, { intent, hint })` — a **pending** phase-1 stub: declares the
|
|
93
|
+
* step's intent and what to do, but has no body yet. It does not run and
|
|
94
|
+
* reports as `pending`; `opice-author` fills in the body.
|
|
95
|
+
* - `step(name, { intent }, fn)` — an authored step that keeps its durable
|
|
96
|
+
* `intent` (preserved verbatim from phase 1) alongside the body.
|
|
97
|
+
*
|
|
29
98
|
* `step.fixme(name, reason, fn)` marks a **known, tolerated failure**: the body
|
|
30
99
|
* still runs, but a failure inside it does NOT fail the scenario or the CI run —
|
|
31
100
|
* it's reported as an amber warning instead. The `reason` is mandatory (use it
|
|
@@ -33,8 +102,41 @@ export declare function browserTest(name: string, fn: () => void, options?: Brow
|
|
|
33
102
|
* step unexpectedly *passes*, it's flagged too ('fixmepass') so a stale marker
|
|
34
103
|
* doesn't linger. Unlike Playwright's `test.fixme()`, which **skips** the test,
|
|
35
104
|
* `step.fixme` **runs** it — the mandatory reason is there to keep them apart.
|
|
105
|
+
*
|
|
106
|
+
* `step.blocked(name, reason, contract?)` is a pending stub that **can't be
|
|
107
|
+
* authored yet** because the app feature doesn't exist — distinct from a plain
|
|
108
|
+
* `step(name, contract)` stub that's simply awaiting a test. Both report as
|
|
109
|
+
* 'pending' (scenario reads 'incomplete'); the blocked one carries its reason.
|
|
110
|
+
*/
|
|
111
|
+
export declare const step: StepFn & StepExtras;
|
|
112
|
+
/**
|
|
113
|
+
* A scenario-level **invariant** — an acceptance property the scenario
|
|
114
|
+
* enforces, independent of the procedural steps. This is the durable "what
|
|
115
|
+
* must be true" that used to live in a scenario's prose Notes; expressing it as
|
|
116
|
+
* a call keeps it in the one source of truth (the test) instead of a separate
|
|
117
|
+
* `.md` that drifts.
|
|
118
|
+
*
|
|
119
|
+
* A failing `invariant` fails the scenario like any hard assertion — it's an
|
|
120
|
+
* acceptance, not a nicety.
|
|
121
|
+
*
|
|
122
|
+
* - `invariant(name, fn)` — enforced now.
|
|
123
|
+
* - `invariant.todo(name, hint?)` — phase-1 stub: states the acceptance but
|
|
124
|
+
* isn't wired yet (status: pending). `opice-author` promotes it to an
|
|
125
|
+
* enforced `invariant(...)` (or an `invariant.fixme(...)` if it can't hold
|
|
126
|
+
* yet) once it knows how to check it.
|
|
127
|
+
* - `invariant.blocked(name, reason)` — a pending acceptance that can't be
|
|
128
|
+
* wired yet because the feature it guards isn't implemented (vs `.todo`,
|
|
129
|
+
* which is merely awaiting authoring). Reports 'pending' with the reason.
|
|
130
|
+
* - `invariant.fixme(name, reason, fn)` — a known-unenforceable acceptance,
|
|
131
|
+
* tolerated like `step.fixme` (e.g. a security property deferred to a
|
|
132
|
+
* ticket). The body runs and is expected to fail; the failure is reported as
|
|
133
|
+
* an amber warning and neither fails the scenario nor the run.
|
|
36
134
|
*/
|
|
37
|
-
export declare const
|
|
38
|
-
|
|
135
|
+
export declare const invariant: {
|
|
136
|
+
(name: string, fn: StepBody): Promise<void>;
|
|
137
|
+
todo: (name: string, hint?: string) => Promise<void>;
|
|
138
|
+
blocked: (name: string, reason: string) => Promise<void>;
|
|
139
|
+
fixme: (name: string, reason: string, fn: StepBody) => Promise<void>;
|
|
39
140
|
};
|
|
141
|
+
export {};
|
|
40
142
|
//# sourceMappingURL=scenario.d.ts.map
|
package/dist/scenario.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenario.d.ts","sourceRoot":"","sources":["../src/scenario.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,
|
|
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;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAwHvE;AAKD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACb;AA6FD,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
|
@@ -40,14 +40,10 @@ function captureTestFile() {
|
|
|
40
40
|
}
|
|
41
41
|
return undefined;
|
|
42
42
|
}
|
|
43
|
-
function defaultScenarioFile(testFile) {
|
|
44
|
-
if (!testFile)
|
|
45
|
-
return undefined;
|
|
46
|
-
return testFile.replace(/\.test\.[tj]sx?$/, '.scenario.md');
|
|
47
|
-
}
|
|
48
43
|
let currentScenarioId = null;
|
|
49
44
|
let currentScenarioStart = 0;
|
|
50
45
|
let currentScenarioFailures = 0;
|
|
46
|
+
let currentScenarioPending = 0;
|
|
51
47
|
// Monotonic per-scenario step counter. Assigned synchronously at each step()
|
|
52
48
|
// call so order reflects authoring order — step records are POSTed
|
|
53
49
|
// fire-and-forget and would otherwise be sequenced by arrival order at the
|
|
@@ -56,42 +52,94 @@ let currentScenarioStepSeq = 0;
|
|
|
56
52
|
/**
|
|
57
53
|
* Register a top-level browser test scenario.
|
|
58
54
|
*
|
|
59
|
-
* Each `browserTest(
|
|
55
|
+
* Each `browserTest(meta, fn)` launches its own isolated Playwright browser +
|
|
60
56
|
* context + page, navigates to the playground URL, runs the given `fn` (which
|
|
61
57
|
* typically contains nested `describe`/`test` blocks), and tears the browser
|
|
62
58
|
* down in `afterAll`.
|
|
59
|
+
*
|
|
60
|
+
* Metadata is the **first** argument (`{ name, url, hash, feature, seeds,
|
|
61
|
+
* roles }`); `name` is required.
|
|
63
62
|
*/
|
|
64
|
-
export function browserTest(
|
|
65
|
-
|
|
63
|
+
export function browserTest(meta, fn) {
|
|
64
|
+
if (typeof meta === 'string') {
|
|
65
|
+
// Migration aid: the old signature was `browserTest(name, fn, options)`.
|
|
66
|
+
throw new Error('opice: browserTest now takes metadata first — browserTest({ name, url, hash, … }, fn). '
|
|
67
|
+
+ `Got a string name (${JSON.stringify(meta)}); wrap it: browserTest({ name: ${JSON.stringify(meta)} }, fn).`);
|
|
68
|
+
}
|
|
69
|
+
if (!meta?.name) {
|
|
70
|
+
throw new Error('opice: browserTest requires a `name` in its metadata — browserTest({ name: "…" }, fn).');
|
|
71
|
+
}
|
|
66
72
|
const reporter = getReporter();
|
|
67
73
|
const testFile = captureTestFile();
|
|
68
|
-
const scenarioFile = opts.scenarioFile ?? defaultScenarioFile(testFile);
|
|
69
74
|
const { describe, beforeAll, afterAll } = bunTest();
|
|
70
|
-
describe(name, () => {
|
|
75
|
+
describe(meta.name, () => {
|
|
71
76
|
beforeAll(async () => {
|
|
72
77
|
currentScenarioStart = Date.now();
|
|
73
78
|
currentScenarioFailures = 0;
|
|
79
|
+
currentScenarioPending = 0;
|
|
74
80
|
currentScenarioStepSeq = 0;
|
|
75
81
|
try {
|
|
76
|
-
currentScenarioId = await reporter.startScenario({
|
|
82
|
+
currentScenarioId = await reporter.startScenario({
|
|
83
|
+
name: meta.name,
|
|
84
|
+
hash: meta.hash,
|
|
85
|
+
testFile,
|
|
86
|
+
feature: meta.feature,
|
|
87
|
+
seeds: meta.seeds,
|
|
88
|
+
roles: meta.roles,
|
|
89
|
+
});
|
|
77
90
|
}
|
|
78
91
|
catch {
|
|
79
92
|
currentScenarioId = null;
|
|
80
93
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
try {
|
|
95
|
+
const page = await launchPage();
|
|
96
|
+
// Repo-level context setup (browser-setup.ts) runs before the first
|
|
97
|
+
// navigation, so an addInitScript it registers fires before the app's
|
|
98
|
+
// own scripts on first paint.
|
|
99
|
+
const setup = await loadUserSetup();
|
|
100
|
+
if (setup)
|
|
101
|
+
await setup(getContext());
|
|
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' });
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
// Setup failed before any step ran (e.g. a wrong playground URL whose
|
|
112
|
+
// goto is refused). bun:test does NOT run afterAll when beforeAll
|
|
113
|
+
// throws, so the scenario we already started above would otherwise
|
|
114
|
+
// sit on the dashboard as 'running' forever (see reporter.ts) —
|
|
115
|
+
// invisible as a failure even though CI is red. Record a synthetic
|
|
116
|
+
// failed step so the dashboard shows *why*, finish the scenario as
|
|
117
|
+
// failed, then re-throw so the run still fails.
|
|
118
|
+
currentScenarioFailures++;
|
|
119
|
+
if (currentScenarioId) {
|
|
120
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
121
|
+
const durationMs = Date.now() - currentScenarioStart;
|
|
122
|
+
try {
|
|
123
|
+
await reporter.recordStep({
|
|
124
|
+
scenarioId: currentScenarioId,
|
|
125
|
+
sequence: currentScenarioStepSeq++,
|
|
126
|
+
kind: 'step',
|
|
127
|
+
name: 'scenario setup',
|
|
128
|
+
status: 'failed',
|
|
129
|
+
durationMs,
|
|
130
|
+
error,
|
|
131
|
+
});
|
|
132
|
+
await reporter.finishScenario({ scenarioId: currentScenarioId, status: 'failed', durationMs });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// best-effort: reporting the failure must never mask the
|
|
136
|
+
// original setup error we're about to re-throw.
|
|
137
|
+
}
|
|
138
|
+
// Null it so afterAll (should it run) doesn't double-finish.
|
|
139
|
+
currentScenarioId = null;
|
|
140
|
+
}
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
95
143
|
}, 30_000);
|
|
96
144
|
afterAll(async () => {
|
|
97
145
|
try {
|
|
@@ -100,6 +148,14 @@ export function browserTest(name, fn, options = {}) {
|
|
|
100
148
|
catch {
|
|
101
149
|
// ignore close errors
|
|
102
150
|
}
|
|
151
|
+
// A scenario still carrying unfilled (pending) steps is a phase-1
|
|
152
|
+
// skeleton that was run before authoring. It's not a failure, but it's
|
|
153
|
+
// not done either — make it loud so a half-authored test isn't mistaken
|
|
154
|
+
// for a passing one.
|
|
155
|
+
if (currentScenarioPending > 0) {
|
|
156
|
+
console.warn(`[opice] scenario "${meta.name}" has ${currentScenarioPending} pending step(s) — `
|
|
157
|
+
+ 'authored skeleton, not yet filled in by opice-author. The body did NOT run.');
|
|
158
|
+
}
|
|
103
159
|
if (currentScenarioId) {
|
|
104
160
|
// Drain pending step records (incl. their screenshot uploads)
|
|
105
161
|
// before marking the scenario done. step() fires recordStep
|
|
@@ -125,17 +181,40 @@ export function browserTest(name, fn, options = {}) {
|
|
|
125
181
|
fn();
|
|
126
182
|
});
|
|
127
183
|
}
|
|
128
|
-
async function
|
|
184
|
+
async function runUnit(unit) {
|
|
129
185
|
const reporter = getReporter();
|
|
130
186
|
// Capture order at call time, before the fire-and-forget record below.
|
|
131
187
|
const sequence = currentScenarioStepSeq++;
|
|
188
|
+
// A reason *with* a body is a .fixme (tolerated failure). A reason *without*
|
|
189
|
+
// a body is .blocked (a pending stub that can't be authored yet).
|
|
190
|
+
const fixme = unit.reason !== undefined && unit.fn !== undefined;
|
|
191
|
+
// Phase-1 stub: no body to run. Report it as 'pending' (so the dashboard
|
|
192
|
+
// shows the skeleton — a scenario carrying one reads as 'incomplete') and
|
|
193
|
+
// count it so afterAll can warn. A `reason` here marks it 'blocked' (the
|
|
194
|
+
// feature isn't built); no reason is a plain todo awaiting authoring. No
|
|
195
|
+
// screenshot, zero duration.
|
|
196
|
+
if (!unit.fn) {
|
|
197
|
+
currentScenarioPending++;
|
|
198
|
+
if (currentScenarioId) {
|
|
199
|
+
void reporter.recordStep({
|
|
200
|
+
scenarioId: currentScenarioId,
|
|
201
|
+
sequence,
|
|
202
|
+
kind: unit.kind,
|
|
203
|
+
name: unit.name,
|
|
204
|
+
status: 'pending',
|
|
205
|
+
durationMs: 0,
|
|
206
|
+
intent: unit.contract?.intent,
|
|
207
|
+
reason: unit.reason,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
132
212
|
const start = Date.now();
|
|
133
|
-
const fixme = fixmeReason !== undefined;
|
|
134
213
|
let status = 'passed';
|
|
135
214
|
let error;
|
|
136
215
|
try {
|
|
137
|
-
await fn();
|
|
138
|
-
// A fixme
|
|
216
|
+
await unit.fn();
|
|
217
|
+
// A fixme unit that *passes* is a stale marker: surface it as a
|
|
139
218
|
// 'fixmepass' warning so the author knows they can drop the marker,
|
|
140
219
|
// rather than letting it pass silently.
|
|
141
220
|
if (fixme)
|
|
@@ -146,7 +225,7 @@ async function runStep(name, fn, fixmeReason) {
|
|
|
146
225
|
if (fixme) {
|
|
147
226
|
// Known / tolerated failure. Record it as 'fixme', but DON'T count it
|
|
148
227
|
// toward scenario failures and DON'T re-throw — that's the whole point
|
|
149
|
-
// of
|
|
228
|
+
// of .fixme: the scenario (and the CI run) stay green, the failure
|
|
150
229
|
// surfaces as an amber warning on the dashboard.
|
|
151
230
|
status = 'fixme';
|
|
152
231
|
}
|
|
@@ -169,11 +248,13 @@ async function runStep(name, fn, fixmeReason) {
|
|
|
169
248
|
void reporter.recordStep({
|
|
170
249
|
scenarioId: currentScenarioId,
|
|
171
250
|
sequence,
|
|
172
|
-
|
|
251
|
+
kind: unit.kind,
|
|
252
|
+
name: unit.name,
|
|
173
253
|
status,
|
|
174
254
|
durationMs,
|
|
175
255
|
error,
|
|
176
|
-
|
|
256
|
+
intent: unit.contract?.intent,
|
|
257
|
+
reason: unit.reason,
|
|
177
258
|
screenshotPath,
|
|
178
259
|
});
|
|
179
260
|
}
|
|
@@ -186,6 +267,14 @@ async function runStep(name, fn, fixmeReason) {
|
|
|
186
267
|
* The body may be sync or async; `step` always returns a promise, so call it
|
|
187
268
|
* with `await step('…', async () => { … })`.
|
|
188
269
|
*
|
|
270
|
+
* Three forms:
|
|
271
|
+
* - `step(name, fn)` — executable step (the common case).
|
|
272
|
+
* - `step(name, { intent, hint })` — a **pending** phase-1 stub: declares the
|
|
273
|
+
* step's intent and what to do, but has no body yet. It does not run and
|
|
274
|
+
* reports as `pending`; `opice-author` fills in the body.
|
|
275
|
+
* - `step(name, { intent }, fn)` — an authored step that keeps its durable
|
|
276
|
+
* `intent` (preserved verbatim from phase 1) alongside the body.
|
|
277
|
+
*
|
|
189
278
|
* `step.fixme(name, reason, fn)` marks a **known, tolerated failure**: the body
|
|
190
279
|
* still runs, but a failure inside it does NOT fail the scenario or the CI run —
|
|
191
280
|
* it's reported as an amber warning instead. The `reason` is mandatory (use it
|
|
@@ -193,8 +282,47 @@ async function runStep(name, fn, fixmeReason) {
|
|
|
193
282
|
* step unexpectedly *passes*, it's flagged too ('fixmepass') so a stale marker
|
|
194
283
|
* doesn't linger. Unlike Playwright's `test.fixme()`, which **skips** the test,
|
|
195
284
|
* `step.fixme` **runs** it — the mandatory reason is there to keep them apart.
|
|
285
|
+
*
|
|
286
|
+
* `step.blocked(name, reason, contract?)` is a pending stub that **can't be
|
|
287
|
+
* authored yet** because the app feature doesn't exist — distinct from a plain
|
|
288
|
+
* `step(name, contract)` stub that's simply awaiting a test. Both report as
|
|
289
|
+
* 'pending' (scenario reads 'incomplete'); the blocked one carries its reason.
|
|
290
|
+
*/
|
|
291
|
+
export const step = Object.assign((name, arg2, arg3) => {
|
|
292
|
+
if (typeof arg2 === 'function') {
|
|
293
|
+
return runUnit({ kind: 'step', name, fn: arg2 });
|
|
294
|
+
}
|
|
295
|
+
return runUnit({ kind: 'step', name, contract: arg2, fn: arg3 });
|
|
296
|
+
}, {
|
|
297
|
+
fixme: (name, reason, fn) => runUnit({ kind: 'step', name, reason, fn }),
|
|
298
|
+
blocked: (name, reason, contract) => runUnit({ kind: 'step', name, contract, reason }),
|
|
299
|
+
});
|
|
300
|
+
/**
|
|
301
|
+
* A scenario-level **invariant** — an acceptance property the scenario
|
|
302
|
+
* enforces, independent of the procedural steps. This is the durable "what
|
|
303
|
+
* must be true" that used to live in a scenario's prose Notes; expressing it as
|
|
304
|
+
* a call keeps it in the one source of truth (the test) instead of a separate
|
|
305
|
+
* `.md` that drifts.
|
|
306
|
+
*
|
|
307
|
+
* A failing `invariant` fails the scenario like any hard assertion — it's an
|
|
308
|
+
* acceptance, not a nicety.
|
|
309
|
+
*
|
|
310
|
+
* - `invariant(name, fn)` — enforced now.
|
|
311
|
+
* - `invariant.todo(name, hint?)` — phase-1 stub: states the acceptance but
|
|
312
|
+
* isn't wired yet (status: pending). `opice-author` promotes it to an
|
|
313
|
+
* enforced `invariant(...)` (or an `invariant.fixme(...)` if it can't hold
|
|
314
|
+
* yet) once it knows how to check it.
|
|
315
|
+
* - `invariant.blocked(name, reason)` — a pending acceptance that can't be
|
|
316
|
+
* wired yet because the feature it guards isn't implemented (vs `.todo`,
|
|
317
|
+
* which is merely awaiting authoring). Reports 'pending' with the reason.
|
|
318
|
+
* - `invariant.fixme(name, reason, fn)` — a known-unenforceable acceptance,
|
|
319
|
+
* tolerated like `step.fixme` (e.g. a security property deferred to a
|
|
320
|
+
* ticket). The body runs and is expected to fail; the failure is reported as
|
|
321
|
+
* an amber warning and neither fails the scenario nor the run.
|
|
196
322
|
*/
|
|
197
|
-
export const
|
|
198
|
-
|
|
323
|
+
export const invariant = Object.assign((name, fn) => runUnit({ kind: 'invariant', name, fn }), {
|
|
324
|
+
todo: (name, hint) => runUnit({ kind: 'invariant', name, contract: hint ? { hint } : undefined }),
|
|
325
|
+
blocked: (name, reason) => runUnit({ kind: 'invariant', name, reason }),
|
|
326
|
+
fixme: (name, reason, fn) => runUnit({ kind: 'invariant', name, reason, fn }),
|
|
199
327
|
});
|
|
200
328
|
//# sourceMappingURL=scenario.js.map
|
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,EAAE,MAAM,eAAe,CAAA;AAC3C,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;
|
|
1
|
+
{"version":3,"file":"scenario.js","sourceRoot":"","sources":["../src/scenario.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/accessible.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { getPage } from './context.js'
|
|
|
3
3
|
|
|
4
4
|
/** The ARIA role union accepted by Playwright's `getByRole`. */
|
|
5
5
|
type Role = Parameters<Page['getByRole']>[0]
|
|
6
|
+
/** Playwright's `getByRole` options minus `name` (which is passed positionally). */
|
|
7
|
+
type RoleOptions = Omit<NonNullable<Parameters<Page['getByRole']>[1]>, 'name'>
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Accessible-name selectors — `byRole` / `byLabel` / `byText`.
|
|
@@ -21,10 +23,16 @@ type Role = Parameters<Page['getByRole']>[0]
|
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Find an element by ARIA role and (optionally) its accessible name.
|
|
24
|
-
* `name` does a substring, case-insensitive match
|
|
26
|
+
* `name` does a substring, case-insensitive match when a string; pass a
|
|
27
|
+
* `RegExp` to match the accessible name by pattern (e.g. a generated id).
|
|
28
|
+
*
|
|
29
|
+
* `options` forwards the rest of Playwright's `getByRole` filters — most useful
|
|
30
|
+
* is `level` to pin a heading (`byRole('heading', 'Title', { level: 2 })`), plus
|
|
31
|
+
* `exact`, `checked`, `pressed`, `expanded`, `disabled`, etc.
|
|
25
32
|
*/
|
|
26
|
-
export function byRole(role: Role, name?: string): Locator {
|
|
27
|
-
|
|
33
|
+
export function byRole(role: Role, name?: string | RegExp, options?: RoleOptions): Locator {
|
|
34
|
+
const opts = { ...options, ...(name == null ? {} : { name }) }
|
|
35
|
+
return getPage().getByRole(role, Object.keys(opts).length > 0 ? opts : undefined)
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/** Find a form control by its associated `<label>` (or `aria-label`) text. */
|
package/src/index.ts
CHANGED
|
@@ -6,8 +6,8 @@ export { back, currentPath, currentUrl, forward, open, reload } from './navigati
|
|
|
6
6
|
|
|
7
7
|
export { getPage, getContext } from './context.js'
|
|
8
8
|
|
|
9
|
-
export { browserTest, step } from './scenario.js'
|
|
10
|
-
export type {
|
|
9
|
+
export { browserTest, invariant, step } from './scenario.js'
|
|
10
|
+
export type { BrowserTestMeta, StepContract } from './scenario.js'
|
|
11
11
|
|
|
12
12
|
export { getReporter, setReporter, configureFromEnv } from './reporter.js'
|
|
13
13
|
export type { Reporter, ReporterConfig, StepEvent, ScenarioStart, ScenarioFinish } from './reporter.js'
|
package/src/reporter.ts
CHANGED
|
@@ -39,16 +39,27 @@ export interface StepEvent {
|
|
|
39
39
|
scenarioId: string
|
|
40
40
|
/** Authoring order within the scenario, assigned at step() call time. */
|
|
41
41
|
sequence: number
|
|
42
|
+
/**
|
|
43
|
+
* 'step' (a procedural step) or 'invariant' (a scenario-level acceptance).
|
|
44
|
+
* The platform may render invariants distinctly; older workers ignore it.
|
|
45
|
+
*/
|
|
46
|
+
kind?: 'step' | 'invariant'
|
|
42
47
|
name: string
|
|
43
48
|
/**
|
|
44
49
|
* 'fixme' (a step.fixme that failed, as expected) and 'fixmepass' (a
|
|
45
50
|
* step.fixme that unexpectedly passed) are tolerated warnings — neither
|
|
46
|
-
* fails the scenario.
|
|
51
|
+
* fails the scenario. 'pending' is a phase-1 stub that never ran (no body
|
|
52
|
+
* yet); a scenario carrying one reads as 'incomplete'.
|
|
47
53
|
*/
|
|
48
|
-
status: 'passed' | 'failed' | 'fixme' | 'fixmepass'
|
|
54
|
+
status: 'passed' | 'failed' | 'fixme' | 'fixmepass' | 'pending'
|
|
49
55
|
durationMs: number
|
|
50
56
|
error?: string
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Durable rationale carried from the unit's contract (phase-1 `intent`) —
|
|
59
|
+
* why it exists / what it proves. Surfaced on the dashboard.
|
|
60
|
+
*/
|
|
61
|
+
intent?: string
|
|
62
|
+
/** Mandatory note from .fixme — why the failure is tolerated. */
|
|
52
63
|
reason?: string
|
|
53
64
|
screenshotPath?: string
|
|
54
65
|
}
|
|
@@ -57,7 +68,12 @@ export interface ScenarioStart {
|
|
|
57
68
|
name: string
|
|
58
69
|
hash?: string
|
|
59
70
|
testFile?: string
|
|
60
|
-
|
|
71
|
+
/** Requirement / feature id this scenario covers (grouping). */
|
|
72
|
+
feature?: string
|
|
73
|
+
/** Seeds required for the scenario (machine-checkable preconditions). */
|
|
74
|
+
seeds?: string[]
|
|
75
|
+
/** Identities / roles the scenario acts as. */
|
|
76
|
+
roles?: string[]
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
export interface ScenarioFinish {
|
|
@@ -133,7 +149,9 @@ class HttpReporter implements Reporter {
|
|
|
133
149
|
name: input.name,
|
|
134
150
|
hash: input.hash,
|
|
135
151
|
testFile: input.testFile,
|
|
136
|
-
|
|
152
|
+
feature: input.feature,
|
|
153
|
+
seeds: input.seeds,
|
|
154
|
+
roles: input.roles,
|
|
137
155
|
})
|
|
138
156
|
return response['scenarioId'] as string
|
|
139
157
|
}
|
|
@@ -154,10 +172,12 @@ class HttpReporter implements Reporter {
|
|
|
154
172
|
: undefined
|
|
155
173
|
await this.fetch('POST', `/api/v1/runs/${runId}/scenarios/${event.scenarioId}/steps`, {
|
|
156
174
|
sequence: event.sequence,
|
|
175
|
+
kind: event.kind,
|
|
157
176
|
name: event.name,
|
|
158
177
|
status: event.status,
|
|
159
178
|
durationMs: event.durationMs,
|
|
160
179
|
error: event.error,
|
|
180
|
+
intent: event.intent,
|
|
161
181
|
reason: event.reason,
|
|
162
182
|
screenshot,
|
|
163
183
|
})
|
package/src/scenario.ts
CHANGED
|
@@ -19,17 +19,38 @@ function bunTest(): typeof import('bun:test') {
|
|
|
19
19
|
|
|
20
20
|
const PLAYGROUND_URL = process.env['PLAYGROUND_URL'] ?? 'http://localhost:15180'
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Scenario metadata — the **first** argument to `browserTest`.
|
|
24
|
+
*
|
|
25
|
+
* This is the durable, machine-relevant context an opice scenario carries
|
|
26
|
+
* independent of its concrete steps: where it runs, what it presupposes, what
|
|
27
|
+
* requirement it covers. It is written in **phase 1** (planning, `opice-plan`)
|
|
28
|
+
* and preserved through **phase 2** (authoring, `opice-author`) — the scenario
|
|
29
|
+
* file IS the spec, so this metadata never lives in a separate `.md` that can
|
|
30
|
+
* drift from the test.
|
|
31
|
+
*
|
|
32
|
+
* The rule of thumb for what belongs here vs. a code comment: *does anything
|
|
33
|
+
* other than a human read it?* Seeds (a precondition a runner could verify),
|
|
34
|
+
* the feature id (grouping on the dashboard), the acting roles — yes, so they
|
|
35
|
+
* are first-class fields. Background rationale that only a human reads stays a
|
|
36
|
+
* comment next to the relevant step.
|
|
37
|
+
*/
|
|
38
|
+
export interface BrowserTestMeta {
|
|
39
|
+
/** Scenario name — becomes the `describe()` title. Required. */
|
|
40
|
+
name: string
|
|
41
|
+
/** Override base URL (defaults to the `PLAYGROUND_URL` env var). */
|
|
26
42
|
url?: string
|
|
43
|
+
/** Hash fragment appended to the base URL (e.g. `'datagrid'`). */
|
|
44
|
+
hash?: string
|
|
45
|
+
/** Feature / requirement id this scenario covers (e.g. `'F-SML-03a'`). */
|
|
46
|
+
feature?: string
|
|
27
47
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* If omitted, defaults to the test file path with `.test.ts` → `.scenario.md`.
|
|
48
|
+
* Seeds that must be loaded for this scenario to run — machine-checkable
|
|
49
|
+
* preconditions, not prose. e.g. `['initial-data', 'crm-master-data']`.
|
|
31
50
|
*/
|
|
32
|
-
|
|
51
|
+
seeds?: string[]
|
|
52
|
+
/** Identities / roles the scenario acts as, e.g. `['crmOperator']`. */
|
|
53
|
+
roles?: string[]
|
|
33
54
|
}
|
|
34
55
|
|
|
35
56
|
/**
|
|
@@ -55,14 +76,10 @@ function captureTestFile(): string | undefined {
|
|
|
55
76
|
return undefined
|
|
56
77
|
}
|
|
57
78
|
|
|
58
|
-
function defaultScenarioFile(testFile: string | undefined): string | undefined {
|
|
59
|
-
if (!testFile) return undefined
|
|
60
|
-
return testFile.replace(/\.test\.[tj]sx?$/, '.scenario.md')
|
|
61
|
-
}
|
|
62
|
-
|
|
63
79
|
let currentScenarioId: string | null = null
|
|
64
80
|
let currentScenarioStart: number = 0
|
|
65
81
|
let currentScenarioFailures = 0
|
|
82
|
+
let currentScenarioPending = 0
|
|
66
83
|
// Monotonic per-scenario step counter. Assigned synchronously at each step()
|
|
67
84
|
// call so order reflects authoring order — step records are POSTed
|
|
68
85
|
// fire-and-forget and would otherwise be sequenced by arrival order at the
|
|
@@ -72,41 +89,93 @@ let currentScenarioStepSeq = 0
|
|
|
72
89
|
/**
|
|
73
90
|
* Register a top-level browser test scenario.
|
|
74
91
|
*
|
|
75
|
-
* Each `browserTest(
|
|
92
|
+
* Each `browserTest(meta, fn)` launches its own isolated Playwright browser +
|
|
76
93
|
* context + page, navigates to the playground URL, runs the given `fn` (which
|
|
77
94
|
* typically contains nested `describe`/`test` blocks), and tears the browser
|
|
78
95
|
* down in `afterAll`.
|
|
96
|
+
*
|
|
97
|
+
* Metadata is the **first** argument (`{ name, url, hash, feature, seeds,
|
|
98
|
+
* roles }`); `name` is required.
|
|
79
99
|
*/
|
|
80
|
-
export function browserTest(
|
|
81
|
-
|
|
100
|
+
export function browserTest(meta: BrowserTestMeta, fn: () => void): void {
|
|
101
|
+
if (typeof meta === 'string') {
|
|
102
|
+
// Migration aid: the old signature was `browserTest(name, fn, options)`.
|
|
103
|
+
throw new Error(
|
|
104
|
+
'opice: browserTest now takes metadata first — browserTest({ name, url, hash, … }, fn). '
|
|
105
|
+
+ `Got a string name (${JSON.stringify(meta)}); wrap it: browserTest({ name: ${JSON.stringify(meta)} }, fn).`,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
if (!meta?.name) {
|
|
109
|
+
throw new Error('opice: browserTest requires a `name` in its metadata — browserTest({ name: "…" }, fn).')
|
|
110
|
+
}
|
|
82
111
|
const reporter = getReporter()
|
|
83
112
|
const testFile = captureTestFile()
|
|
84
|
-
const scenarioFile = opts.scenarioFile ?? defaultScenarioFile(testFile)
|
|
85
113
|
const { describe, beforeAll, afterAll } = bunTest()
|
|
86
114
|
|
|
87
|
-
describe(name, () => {
|
|
115
|
+
describe(meta.name, () => {
|
|
88
116
|
beforeAll(async () => {
|
|
89
117
|
currentScenarioStart = Date.now()
|
|
90
118
|
currentScenarioFailures = 0
|
|
119
|
+
currentScenarioPending = 0
|
|
91
120
|
currentScenarioStepSeq = 0
|
|
92
121
|
try {
|
|
93
|
-
currentScenarioId = await reporter.startScenario({
|
|
122
|
+
currentScenarioId = await reporter.startScenario({
|
|
123
|
+
name: meta.name,
|
|
124
|
+
hash: meta.hash,
|
|
125
|
+
testFile,
|
|
126
|
+
feature: meta.feature,
|
|
127
|
+
seeds: meta.seeds,
|
|
128
|
+
roles: meta.roles,
|
|
129
|
+
})
|
|
94
130
|
} catch {
|
|
95
131
|
currentScenarioId = null
|
|
96
132
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
try {
|
|
134
|
+
const page = await launchPage()
|
|
135
|
+
// Repo-level context setup (browser-setup.ts) runs before the first
|
|
136
|
+
// navigation, so an addInitScript it registers fires before the app's
|
|
137
|
+
// own scripts on first paint.
|
|
138
|
+
const setup = await loadUserSetup()
|
|
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
|
|
106
143
|
// runs and may hold `load` on a slow chunk or long-lived connection, so
|
|
107
144
|
// waiting for `load` flakily times out under CI contention. Readiness is
|
|
108
145
|
// handled by the test's retrying assertions.
|
|
109
146
|
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// Setup failed before any step ran (e.g. a wrong playground URL whose
|
|
149
|
+
// goto is refused). bun:test does NOT run afterAll when beforeAll
|
|
150
|
+
// throws, so the scenario we already started above would otherwise
|
|
151
|
+
// sit on the dashboard as 'running' forever (see reporter.ts) —
|
|
152
|
+
// invisible as a failure even though CI is red. Record a synthetic
|
|
153
|
+
// failed step so the dashboard shows *why*, finish the scenario as
|
|
154
|
+
// failed, then re-throw so the run still fails.
|
|
155
|
+
currentScenarioFailures++
|
|
156
|
+
if (currentScenarioId) {
|
|
157
|
+
const error = e instanceof Error ? e.message : String(e)
|
|
158
|
+
const durationMs = Date.now() - currentScenarioStart
|
|
159
|
+
try {
|
|
160
|
+
await reporter.recordStep({
|
|
161
|
+
scenarioId: currentScenarioId,
|
|
162
|
+
sequence: currentScenarioStepSeq++,
|
|
163
|
+
kind: 'step',
|
|
164
|
+
name: 'scenario setup',
|
|
165
|
+
status: 'failed',
|
|
166
|
+
durationMs,
|
|
167
|
+
error,
|
|
168
|
+
})
|
|
169
|
+
await reporter.finishScenario({ scenarioId: currentScenarioId, status: 'failed', durationMs })
|
|
170
|
+
} catch {
|
|
171
|
+
// best-effort: reporting the failure must never mask the
|
|
172
|
+
// original setup error we're about to re-throw.
|
|
173
|
+
}
|
|
174
|
+
// Null it so afterAll (should it run) doesn't double-finish.
|
|
175
|
+
currentScenarioId = null
|
|
176
|
+
}
|
|
177
|
+
throw e
|
|
178
|
+
}
|
|
110
179
|
}, 30_000)
|
|
111
180
|
|
|
112
181
|
afterAll(async () => {
|
|
@@ -115,6 +184,16 @@ export function browserTest(name: string, fn: () => void, options: BrowserTestOp
|
|
|
115
184
|
} catch {
|
|
116
185
|
// ignore close errors
|
|
117
186
|
}
|
|
187
|
+
// A scenario still carrying unfilled (pending) steps is a phase-1
|
|
188
|
+
// skeleton that was run before authoring. It's not a failure, but it's
|
|
189
|
+
// not done either — make it loud so a half-authored test isn't mistaken
|
|
190
|
+
// for a passing one.
|
|
191
|
+
if (currentScenarioPending > 0) {
|
|
192
|
+
console.warn(
|
|
193
|
+
`[opice] scenario "${meta.name}" has ${currentScenarioPending} pending step(s) — `
|
|
194
|
+
+ 'authored skeleton, not yet filled in by opice-author. The body did NOT run.',
|
|
195
|
+
)
|
|
196
|
+
}
|
|
118
197
|
if (currentScenarioId) {
|
|
119
198
|
// Drain pending step records (incl. their screenshot uploads)
|
|
120
199
|
// before marking the scenario done. step() fires recordStep
|
|
@@ -140,19 +219,78 @@ export function browserTest(name: string, fn: () => void, options: BrowserTestOp
|
|
|
140
219
|
})
|
|
141
220
|
}
|
|
142
221
|
|
|
143
|
-
type StepStatus = 'passed' | 'failed' | 'fixme' | 'fixmepass'
|
|
222
|
+
type StepStatus = 'passed' | 'failed' | 'fixme' | 'fixmepass' | 'pending'
|
|
223
|
+
type StepKind = 'step' | 'invariant'
|
|
144
224
|
|
|
145
|
-
|
|
225
|
+
/**
|
|
226
|
+
* The durable contract of a step or invariant, separate from its mechanics.
|
|
227
|
+
*
|
|
228
|
+
* `intent` is written in **phase 1** and survives **verbatim** into the
|
|
229
|
+
* authored test — it's the "why this exists / what it proves", the independent
|
|
230
|
+
* statement of intent that the concrete body is checked against. `hint` is
|
|
231
|
+
* phase-1 scaffolding *for the authoring agent* ("what to actually do here");
|
|
232
|
+
* it is consumed when the step is authored and dropped once a body exists.
|
|
233
|
+
*/
|
|
234
|
+
export interface StepContract {
|
|
235
|
+
/** Durable rationale: why this step/invariant exists, what it proves. */
|
|
236
|
+
intent?: string
|
|
237
|
+
/**
|
|
238
|
+
* Phase-1 instruction to the authoring agent — what to do on the page here.
|
|
239
|
+
* Ephemeral: drop it once the body is written.
|
|
240
|
+
*/
|
|
241
|
+
hint?: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface RunUnit {
|
|
245
|
+
kind: StepKind
|
|
246
|
+
name: string
|
|
247
|
+
contract?: StepContract
|
|
248
|
+
/** Present once authored. Absent ⇒ a pending (phase-1) stub. */
|
|
249
|
+
fn?: () => void | Promise<void>
|
|
250
|
+
/**
|
|
251
|
+
* A human note. With a body (`.fixme`): why a tolerated failure is allowed.
|
|
252
|
+
* Without a body (`.blocked`): why the stub can't be authored yet (the app
|
|
253
|
+
* feature isn't implemented). A plain pending stub has no reason.
|
|
254
|
+
*/
|
|
255
|
+
reason?: string
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function runUnit(unit: RunUnit): Promise<void> {
|
|
146
259
|
const reporter = getReporter()
|
|
147
260
|
// Capture order at call time, before the fire-and-forget record below.
|
|
148
261
|
const sequence = currentScenarioStepSeq++
|
|
262
|
+
// A reason *with* a body is a .fixme (tolerated failure). A reason *without*
|
|
263
|
+
// a body is .blocked (a pending stub that can't be authored yet).
|
|
264
|
+
const fixme = unit.reason !== undefined && unit.fn !== undefined
|
|
265
|
+
|
|
266
|
+
// Phase-1 stub: no body to run. Report it as 'pending' (so the dashboard
|
|
267
|
+
// shows the skeleton — a scenario carrying one reads as 'incomplete') and
|
|
268
|
+
// count it so afterAll can warn. A `reason` here marks it 'blocked' (the
|
|
269
|
+
// feature isn't built); no reason is a plain todo awaiting authoring. No
|
|
270
|
+
// screenshot, zero duration.
|
|
271
|
+
if (!unit.fn) {
|
|
272
|
+
currentScenarioPending++
|
|
273
|
+
if (currentScenarioId) {
|
|
274
|
+
void reporter.recordStep({
|
|
275
|
+
scenarioId: currentScenarioId,
|
|
276
|
+
sequence,
|
|
277
|
+
kind: unit.kind,
|
|
278
|
+
name: unit.name,
|
|
279
|
+
status: 'pending',
|
|
280
|
+
durationMs: 0,
|
|
281
|
+
intent: unit.contract?.intent,
|
|
282
|
+
reason: unit.reason,
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
149
288
|
const start = Date.now()
|
|
150
|
-
const fixme = fixmeReason !== undefined
|
|
151
289
|
let status: StepStatus = 'passed'
|
|
152
290
|
let error: string | undefined
|
|
153
291
|
try {
|
|
154
|
-
await fn()
|
|
155
|
-
// A fixme
|
|
292
|
+
await unit.fn()
|
|
293
|
+
// A fixme unit that *passes* is a stale marker: surface it as a
|
|
156
294
|
// 'fixmepass' warning so the author knows they can drop the marker,
|
|
157
295
|
// rather than letting it pass silently.
|
|
158
296
|
if (fixme) status = 'fixmepass'
|
|
@@ -161,7 +299,7 @@ async function runStep(name: string, fn: () => void | Promise<void>, fixmeReason
|
|
|
161
299
|
if (fixme) {
|
|
162
300
|
// Known / tolerated failure. Record it as 'fixme', but DON'T count it
|
|
163
301
|
// toward scenario failures and DON'T re-throw — that's the whole point
|
|
164
|
-
// of
|
|
302
|
+
// of .fixme: the scenario (and the CI run) stay green, the failure
|
|
165
303
|
// surfaces as an amber warning on the dashboard.
|
|
166
304
|
status = 'fixme'
|
|
167
305
|
} else {
|
|
@@ -181,17 +319,41 @@ async function runStep(name: string, fn: () => void | Promise<void>, fixmeReason
|
|
|
181
319
|
void reporter.recordStep({
|
|
182
320
|
scenarioId: currentScenarioId,
|
|
183
321
|
sequence,
|
|
184
|
-
|
|
322
|
+
kind: unit.kind,
|
|
323
|
+
name: unit.name,
|
|
185
324
|
status,
|
|
186
325
|
durationMs,
|
|
187
326
|
error,
|
|
188
|
-
|
|
327
|
+
intent: unit.contract?.intent,
|
|
328
|
+
reason: unit.reason,
|
|
189
329
|
screenshotPath,
|
|
190
330
|
})
|
|
191
331
|
}
|
|
192
332
|
}
|
|
193
333
|
}
|
|
194
334
|
|
|
335
|
+
type StepBody = () => void | Promise<void>
|
|
336
|
+
|
|
337
|
+
interface StepFn {
|
|
338
|
+
/** Executable step. */
|
|
339
|
+
(name: string, fn: StepBody): Promise<void>
|
|
340
|
+
/** Phase-1 stub: a step with a contract but no body yet (status: pending). */
|
|
341
|
+
(name: string, contract: StepContract): Promise<void>
|
|
342
|
+
/** Authored step that keeps its durable contract. */
|
|
343
|
+
(name: string, contract: StepContract, fn: StepBody): Promise<void>
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
interface StepExtras {
|
|
347
|
+
fixme: (name: string, reason: string, fn: StepBody) => Promise<void>
|
|
348
|
+
/**
|
|
349
|
+
* A **blocked** pending stub: the step can't be authored yet because the app
|
|
350
|
+
* feature it covers isn't implemented. Reports as 'pending' with the reason
|
|
351
|
+
* attached (the dashboard shows it as blocked, distinct from a plain stub
|
|
352
|
+
* that's merely awaiting a test). `reason` is mandatory — say what's missing.
|
|
353
|
+
*/
|
|
354
|
+
blocked: (name: string, reason: string, contract?: StepContract) => Promise<void>
|
|
355
|
+
}
|
|
356
|
+
|
|
195
357
|
/**
|
|
196
358
|
* A reportable step inside a scenario. Captures duration + screenshot on
|
|
197
359
|
* finish, forwards to the active reporter (no-op unless configured via env).
|
|
@@ -199,6 +361,14 @@ async function runStep(name: string, fn: () => void | Promise<void>, fixmeReason
|
|
|
199
361
|
* The body may be sync or async; `step` always returns a promise, so call it
|
|
200
362
|
* with `await step('…', async () => { … })`.
|
|
201
363
|
*
|
|
364
|
+
* Three forms:
|
|
365
|
+
* - `step(name, fn)` — executable step (the common case).
|
|
366
|
+
* - `step(name, { intent, hint })` — a **pending** phase-1 stub: declares the
|
|
367
|
+
* step's intent and what to do, but has no body yet. It does not run and
|
|
368
|
+
* reports as `pending`; `opice-author` fills in the body.
|
|
369
|
+
* - `step(name, { intent }, fn)` — an authored step that keeps its durable
|
|
370
|
+
* `intent` (preserved verbatim from phase 1) alongside the body.
|
|
371
|
+
*
|
|
202
372
|
* `step.fixme(name, reason, fn)` marks a **known, tolerated failure**: the body
|
|
203
373
|
* still runs, but a failure inside it does NOT fail the scenario or the CI run —
|
|
204
374
|
* it's reported as an amber warning instead. The `reason` is mandatory (use it
|
|
@@ -206,11 +376,63 @@ async function runStep(name: string, fn: () => void | Promise<void>, fixmeReason
|
|
|
206
376
|
* step unexpectedly *passes*, it's flagged too ('fixmepass') so a stale marker
|
|
207
377
|
* doesn't linger. Unlike Playwright's `test.fixme()`, which **skips** the test,
|
|
208
378
|
* `step.fixme` **runs** it — the mandatory reason is there to keep them apart.
|
|
379
|
+
*
|
|
380
|
+
* `step.blocked(name, reason, contract?)` is a pending stub that **can't be
|
|
381
|
+
* authored yet** because the app feature doesn't exist — distinct from a plain
|
|
382
|
+
* `step(name, contract)` stub that's simply awaiting a test. Both report as
|
|
383
|
+
* 'pending' (scenario reads 'incomplete'); the blocked one carries its reason.
|
|
384
|
+
*/
|
|
385
|
+
export const step: StepFn & StepExtras = Object.assign(
|
|
386
|
+
(name: string, arg2: StepBody | StepContract, arg3?: StepBody): Promise<void> => {
|
|
387
|
+
if (typeof arg2 === 'function') {
|
|
388
|
+
return runUnit({ kind: 'step', name, fn: arg2 })
|
|
389
|
+
}
|
|
390
|
+
return runUnit({ kind: 'step', name, contract: arg2, fn: arg3 })
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
fixme: (name: string, reason: string, fn: StepBody): Promise<void> =>
|
|
394
|
+
runUnit({ kind: 'step', name, reason, fn }),
|
|
395
|
+
blocked: (name: string, reason: string, contract?: StepContract): Promise<void> =>
|
|
396
|
+
runUnit({ kind: 'step', name, contract, reason }),
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* A scenario-level **invariant** — an acceptance property the scenario
|
|
402
|
+
* enforces, independent of the procedural steps. This is the durable "what
|
|
403
|
+
* must be true" that used to live in a scenario's prose Notes; expressing it as
|
|
404
|
+
* a call keeps it in the one source of truth (the test) instead of a separate
|
|
405
|
+
* `.md` that drifts.
|
|
406
|
+
*
|
|
407
|
+
* A failing `invariant` fails the scenario like any hard assertion — it's an
|
|
408
|
+
* acceptance, not a nicety.
|
|
409
|
+
*
|
|
410
|
+
* - `invariant(name, fn)` — enforced now.
|
|
411
|
+
* - `invariant.todo(name, hint?)` — phase-1 stub: states the acceptance but
|
|
412
|
+
* isn't wired yet (status: pending). `opice-author` promotes it to an
|
|
413
|
+
* enforced `invariant(...)` (or an `invariant.fixme(...)` if it can't hold
|
|
414
|
+
* yet) once it knows how to check it.
|
|
415
|
+
* - `invariant.blocked(name, reason)` — a pending acceptance that can't be
|
|
416
|
+
* wired yet because the feature it guards isn't implemented (vs `.todo`,
|
|
417
|
+
* which is merely awaiting authoring). Reports 'pending' with the reason.
|
|
418
|
+
* - `invariant.fixme(name, reason, fn)` — a known-unenforceable acceptance,
|
|
419
|
+
* tolerated like `step.fixme` (e.g. a security property deferred to a
|
|
420
|
+
* ticket). The body runs and is expected to fail; the failure is reported as
|
|
421
|
+
* an amber warning and neither fails the scenario nor the run.
|
|
209
422
|
*/
|
|
210
|
-
export const
|
|
211
|
-
(name: string, fn:
|
|
423
|
+
export const invariant: {
|
|
424
|
+
(name: string, fn: StepBody): Promise<void>
|
|
425
|
+
todo: (name: string, hint?: string) => Promise<void>
|
|
426
|
+
blocked: (name: string, reason: string) => Promise<void>
|
|
427
|
+
fixme: (name: string, reason: string, fn: StepBody) => Promise<void>
|
|
428
|
+
} = Object.assign(
|
|
429
|
+
(name: string, fn: StepBody): Promise<void> => runUnit({ kind: 'invariant', name, fn }),
|
|
212
430
|
{
|
|
213
|
-
|
|
214
|
-
|
|
431
|
+
todo: (name: string, hint?: string): Promise<void> =>
|
|
432
|
+
runUnit({ kind: 'invariant', name, contract: hint ? { hint } : undefined }),
|
|
433
|
+
blocked: (name: string, reason: string): Promise<void> =>
|
|
434
|
+
runUnit({ kind: 'invariant', name, reason }),
|
|
435
|
+
fixme: (name: string, reason: string, fn: StepBody): Promise<void> =>
|
|
436
|
+
runUnit({ kind: 'invariant', name, reason, fn }),
|
|
215
437
|
},
|
|
216
438
|
)
|