@opice/harness 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -27
- package/dist/accessible.d.ts +28 -0
- package/dist/accessible.d.ts.map +1 -0
- package/dist/accessible.js +31 -0
- package/dist/accessible.js.map +1 -0
- package/dist/command.d.ts +65 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +88 -0
- package/dist/command.js.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +50 -0
- package/dist/context.js.map +1 -0
- package/dist/dsn.d.ts +17 -0
- package/dist/dsn.d.ts.map +1 -0
- package/dist/dsn.js +17 -0
- package/dist/dsn.js.map +1 -0
- package/dist/element.d.ts +50 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +82 -0
- package/dist/element.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation.d.ts +22 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +35 -0
- package/dist/navigation.js.map +1 -0
- package/dist/reporter.d.ts +61 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +210 -0
- package/dist/reporter.js.map +1 -0
- package/dist/scenario.d.ts +30 -0
- package/dist/scenario.d.ts.map +1 -0
- package/dist/scenario.js +162 -0
- package/dist/scenario.js.map +1 -0
- package/package.json +11 -4
- package/src/accessible.ts +38 -0
- package/src/command.ts +134 -0
- package/src/context.ts +55 -0
- package/src/element.ts +56 -81
- package/src/index.ts +16 -1
- package/src/navigation.ts +41 -0
- package/src/reporter.ts +18 -0
- package/src/scenario.ts +38 -22
- package/src/agent-browser.ts +0 -30
package/README.md
CHANGED
|
@@ -1,66 +1,130 @@
|
|
|
1
1
|
# @opice/harness
|
|
2
2
|
|
|
3
|
-
Runtime primitives for [opice](../../README.md) — AI-driven E2E browser tests on
|
|
3
|
+
Runtime primitives for [opice](../../README.md) — AI-driven E2E browser tests on
|
|
4
|
+
top of [Playwright](https://playwright.dev). The browser runs **in-process**
|
|
5
|
+
under `bun test`; there is no CLI or daemon in the test path.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
bun add -D @opice/harness
|
|
11
|
+
bunx playwright install chromium
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
Runs under the Bun test runner.
|
|
12
15
|
|
|
13
16
|
## Usage
|
|
14
17
|
|
|
15
18
|
```ts
|
|
16
|
-
import { test,
|
|
17
|
-
import { browserTest, el,
|
|
19
|
+
import { test, describe } from 'bun:test'
|
|
20
|
+
import { browserTest, el, byRole, byLabel, step, expect } from '@opice/harness'
|
|
18
21
|
|
|
19
22
|
browserTest('DataGrid', () => {
|
|
20
|
-
test('renders
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
test('renders and is interactive', async () => {
|
|
24
|
+
await step('table is visible', async () => {
|
|
25
|
+
await expect(el('datagrid-table')).toBeVisible()
|
|
26
|
+
})
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
el(
|
|
28
|
+
await step('clicking a row highlights it', async () => {
|
|
29
|
+
await el('datagrid-row-0').click()
|
|
30
|
+
await expect(el('datagrid-row-0')).toHaveAttribute('data-highlighted', '')
|
|
28
31
|
})
|
|
29
|
-
|
|
30
|
-
})
|
|
32
|
+
}, 60_000)
|
|
31
33
|
}, { hash: 'datagrid' })
|
|
32
34
|
```
|
|
33
35
|
|
|
36
|
+
The DSL is **async** and returns Playwright `Locator`s, so the full Locator API
|
|
37
|
+
(`.click()`, `.fill()`, `.textContent()`, `.first()`, …) and the web-first
|
|
38
|
+
`expect(locator)` assertions are available. `expect` is re-exported from the
|
|
39
|
+
harness (Playwright's `expect`, which works under `bun:test`).
|
|
40
|
+
|
|
34
41
|
## API
|
|
35
42
|
|
|
36
|
-
###
|
|
43
|
+
### Locators
|
|
44
|
+
|
|
45
|
+
- `el(selector)` — a `Locator`. A bare word is a test-id (`el('foo')` ≡
|
|
46
|
+
`getByTestId('foo')`, matching `data-testid`); anything with CSS-flavoured
|
|
47
|
+
characters is a raw CSS selector (`el('main h1')`).
|
|
48
|
+
- `tid(id)` — build a `[data-testid="..."]` selector string for composing into a
|
|
49
|
+
larger CSS selector: `el(`${tid('row')} button`)`.
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
- `tid(id)` — build a `[data-testid="..."]` selector string for compound selectors.
|
|
51
|
+
### Accessible-name selectors
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
Native Playwright accessibility locators — reliable, real user gestures. Prefer
|
|
54
|
+
these (or `data-testid`) over CSS.
|
|
42
55
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
56
|
+
- `byRole(role, name?)` — by ARIA role, optionally filtered by accessible name.
|
|
57
|
+
- `byLabel(text)` — a form control by its `<label>` / `aria-label`.
|
|
58
|
+
- `byText(text)` — by visible text.
|
|
45
59
|
|
|
46
|
-
|
|
60
|
+
### Assertions
|
|
61
|
+
|
|
62
|
+
- `expect(locator)` — Playwright's web-first, auto-retrying assertions:
|
|
63
|
+
`.toBeVisible()`, `.toHaveText()`, `.toContainText()`, `.toBeEnabled()`,
|
|
64
|
+
`.toHaveAttribute()`, … Prefer these over manual polling. Generic matchers
|
|
65
|
+
(`.toBe`, `.toEqual`) work too.
|
|
66
|
+
|
|
67
|
+
### Navigation
|
|
68
|
+
|
|
69
|
+
- `open(url)`, `reload()`, `back()`, `forward()` — page navigation (each awaits
|
|
70
|
+
the load event).
|
|
71
|
+
- `currentUrl()`, `currentPath()` — read `location.href` / `location.pathname`
|
|
72
|
+
(synchronous).
|
|
47
73
|
|
|
48
74
|
### Waiting
|
|
49
75
|
|
|
50
|
-
- `waitFor(condition, opts?)` — polls
|
|
51
|
-
|
|
76
|
+
- `waitFor(condition, opts?)` — polls a (possibly async) predicate until true;
|
|
77
|
+
throws on timeout (default 10s / 200ms). For predicates that don't map to a
|
|
78
|
+
retrying `expect` assertion.
|
|
79
|
+
- `wait(ms)` — fixed sleep. Avoid when `waitFor` or `expect` works.
|
|
52
80
|
|
|
53
81
|
### Scenarios
|
|
54
82
|
|
|
55
|
-
- `browserTest(name, fn, options?)` — top-level scenario.
|
|
56
|
-
|
|
83
|
+
- `browserTest(name, fn, options?)` — top-level scenario. Launches a fresh
|
|
84
|
+
isolated Playwright browser + context + page in `beforeAll`, navigates to the
|
|
85
|
+
scenario URL, tears down in `afterAll`. Pass `{ hash: 'foo' }` for
|
|
86
|
+
`PLAYGROUND_URL#foo`, or a string shorthand: `browserTest(name, fn, 'foo')`.
|
|
87
|
+
- `step(name, fn)` — reportable async step. `await step('…', async () => {…})`;
|
|
88
|
+
captures duration + screenshot. Reporter is a no-op until the platform is wired.
|
|
89
|
+
|
|
90
|
+
### Custom verbs (user-land)
|
|
91
|
+
|
|
92
|
+
Define a domain verb once in `<repo>/browser-tools.ts` and use it in **both** the
|
|
93
|
+
authoring agent (`opice-browser`) and your tests:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// browser-tools.ts
|
|
97
|
+
import { command, z } from '@opice/harness'
|
|
98
|
+
|
|
99
|
+
export const fullEnum = command('fullEnum',
|
|
100
|
+
z.object({ label: z.string(), option: z.string() }),
|
|
101
|
+
async ({ page }, { label, option }) => {
|
|
102
|
+
await page.getByLabel(label).press('Enter')
|
|
103
|
+
await page.getByRole('button', { name: option }).click()
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// in a test
|
|
109
|
+
import { call } from '@opice/harness'
|
|
110
|
+
import { fullEnum } from '../browser-tools'
|
|
111
|
+
await call(fullEnum, { label: 'Typ', option: 'Faktura' })
|
|
112
|
+
```
|
|
57
113
|
|
|
58
114
|
### Misc
|
|
59
115
|
|
|
60
|
-
- `screenshot(path?)` — saves a PNG, returns the path
|
|
61
|
-
- `evalJs(js)` — `
|
|
116
|
+
- `screenshot(path?)` — saves a PNG, returns the path (default under `/tmp/`).
|
|
117
|
+
- `evalJs(js)` — `page.evaluate` passthrough (returns the real JS value).
|
|
118
|
+
- `getPage()` / `getContext()` — the live Playwright `Page` / `BrowserContext`
|
|
119
|
+
for an escape hatch into the raw API.
|
|
62
120
|
|
|
63
121
|
## Configuration
|
|
64
122
|
|
|
65
123
|
- `PLAYGROUND_URL` — base URL for `browserTest` (default `http://localhost:15180`).
|
|
66
|
-
- `
|
|
124
|
+
- `OPICE_HEADED` (or `PWDEBUG`) — run headed for local debugging (default headless).
|
|
125
|
+
- `OPICE_ENDPOINT`, `OPICE_PROJECT`, `OPICE_API_KEY` — reporter config (or a single `OPICE_DSN`).
|
|
126
|
+
- `OPICE_REPORT` — `auto` (default: report only in CI), `always` (report locally too), or
|
|
127
|
+
`never`. Outside CI, reporting is opt-in so iterating with bare `bun test` doesn't stream
|
|
128
|
+
half-finished runs onto the shared dashboard. CI-detected runs are tagged `ci`, opted-in
|
|
129
|
+
local runs `local`.
|
|
130
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Locator, Page } from 'playwright';
|
|
2
|
+
/** The ARIA role union accepted by Playwright's `getByRole`. */
|
|
3
|
+
type Role = Parameters<Page['getByRole']>[0];
|
|
4
|
+
/**
|
|
5
|
+
* Accessible-name selectors — `byRole` / `byLabel` / `byText`.
|
|
6
|
+
*
|
|
7
|
+
* opice prefers `data-testid` (see `el`), but real apps often can't be
|
|
8
|
+
* annotated — third-party UIs, generated form-field ids, components you don't
|
|
9
|
+
* own. These map straight onto Playwright's accessibility-aware locators, which
|
|
10
|
+
* compute the real ARIA accessible name and fire real user gestures. No
|
|
11
|
+
* in-page resolver, no stamping — the previous engine (agent-browser) was
|
|
12
|
+
* CSS-only and couldn't do this, which is a large part of why opice moved to
|
|
13
|
+
* Playwright.
|
|
14
|
+
*
|
|
15
|
+
* All three return a `Locator`, so the full Locator API and `expect(locator)`
|
|
16
|
+
* assertions apply.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Find an element by ARIA role and (optionally) its accessible name.
|
|
20
|
+
* `name` does a substring, case-insensitive match by default.
|
|
21
|
+
*/
|
|
22
|
+
export declare function byRole(role: Role, name?: string): Locator;
|
|
23
|
+
/** Find a form control by its associated `<label>` (or `aria-label`) text. */
|
|
24
|
+
export declare function byLabel(text: string): Locator;
|
|
25
|
+
/** Find an element by its visible text (substring, case-insensitive). */
|
|
26
|
+
export declare function byText(text: string): Locator;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=accessible.d.ts.map
|
|
@@ -0,0 +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;AAE5C;;;;;;;;;;;;;GAaG;AAEH;;;GAGG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,8EAA8E;AAC9E,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED,yEAAyE;AACzE,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5C"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getPage } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Accessible-name selectors — `byRole` / `byLabel` / `byText`.
|
|
4
|
+
*
|
|
5
|
+
* opice prefers `data-testid` (see `el`), but real apps often can't be
|
|
6
|
+
* annotated — third-party UIs, generated form-field ids, components you don't
|
|
7
|
+
* own. These map straight onto Playwright's accessibility-aware locators, which
|
|
8
|
+
* compute the real ARIA accessible name and fire real user gestures. No
|
|
9
|
+
* in-page resolver, no stamping — the previous engine (agent-browser) was
|
|
10
|
+
* CSS-only and couldn't do this, which is a large part of why opice moved to
|
|
11
|
+
* Playwright.
|
|
12
|
+
*
|
|
13
|
+
* All three return a `Locator`, so the full Locator API and `expect(locator)`
|
|
14
|
+
* assertions apply.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Find an element by ARIA role and (optionally) its accessible name.
|
|
18
|
+
* `name` does a substring, case-insensitive match by default.
|
|
19
|
+
*/
|
|
20
|
+
export function byRole(role, name) {
|
|
21
|
+
return getPage().getByRole(role, name == null ? undefined : { name });
|
|
22
|
+
}
|
|
23
|
+
/** Find a form control by its associated `<label>` (or `aria-label`) text. */
|
|
24
|
+
export function byLabel(text) {
|
|
25
|
+
return getPage().getByLabel(text);
|
|
26
|
+
}
|
|
27
|
+
/** Find an element by its visible text (substring, case-insensitive). */
|
|
28
|
+
export function byText(text) {
|
|
29
|
+
return getPage().getByText(text);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=accessible.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessible.js","sourceRoot":"","sources":["../src/accessible.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAKtC;;;;;;;;;;;;;GAaG;AAEH;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,IAAU,EAAE,IAAa;IAC/C,OAAO,OAAO,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;AACtE,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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Locator, Page } from 'playwright';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* The shared command registry.
|
|
5
|
+
*
|
|
6
|
+
* A command is a named, schema-validated browser verb implemented once over a
|
|
7
|
+
* Playwright page. The same command object is used on both faces:
|
|
8
|
+
*
|
|
9
|
+
* - **authoring** — the `opice-browser` daemon loads it and exposes it to the
|
|
10
|
+
* agent (`opice-browser <name> …`),
|
|
11
|
+
* - **tests** — the harness loads the same module so a test can call the verb
|
|
12
|
+
* directly.
|
|
13
|
+
*
|
|
14
|
+
* Built-in verbs (open/click/fill/byRole/…) ship with the `opice-browser`
|
|
15
|
+
* daemon; user-land verbs live in a repo's `browser-tools.ts` and are picked up
|
|
16
|
+
* by `loadUserCommands`. Both are the *same* `Command` objects — that is the
|
|
17
|
+
* unification that closes the authoring↔test vocabulary gap.
|
|
18
|
+
*/
|
|
19
|
+
/** Page + accessibility-aware helpers handed to every command implementation. */
|
|
20
|
+
export interface CommandCtx {
|
|
21
|
+
page: Page;
|
|
22
|
+
/** Resolve a test-id (bare word) or raw CSS selector to a locator. */
|
|
23
|
+
el(selectorOrTestId: string): Locator;
|
|
24
|
+
byRole(role: Parameters<Page['getByRole']>[0], name?: string): Locator;
|
|
25
|
+
byLabel(text: string): Locator;
|
|
26
|
+
byText(text: string): Locator;
|
|
27
|
+
}
|
|
28
|
+
export interface Command<S extends z.ZodType = z.ZodType> {
|
|
29
|
+
name: string;
|
|
30
|
+
/** One-line description, surfaced in `opice-browser commands`. */
|
|
31
|
+
description?: string;
|
|
32
|
+
params: S;
|
|
33
|
+
run: (ctx: CommandCtx, args: z.infer<S>) => Promise<unknown>;
|
|
34
|
+
}
|
|
35
|
+
/** Define a browser command. See `CommandCtx` for what `ctx` provides. */
|
|
36
|
+
export declare function command<S extends z.ZodType>(name: string, params: S, run: (ctx: CommandCtx, args: z.infer<S>) => Promise<unknown>, description?: string): Command<S>;
|
|
37
|
+
/** Build the command context bound to a specific page. */
|
|
38
|
+
export declare function makeCtx(page: Page): CommandCtx;
|
|
39
|
+
/** Validate args against a command's schema and run it on `page`. */
|
|
40
|
+
export declare function runCommand(page: Page, cmd: Command, rawArgs: unknown): Promise<unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Invoke a command against the active scenario page from inside a test. Pair
|
|
43
|
+
* with a direct import of the verb from `browser-tools.ts` so the args are
|
|
44
|
+
* type-checked against its schema:
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { call } from '@opice/harness'
|
|
48
|
+
* import { fullEnum } from '../browser-tools'
|
|
49
|
+
* await call(fullEnum, { label: 'Typ', option: 'Faktura' })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function call<S extends z.ZodType>(cmd: Command<S>, args: z.infer<S>): Promise<unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Locate a repo's `browser-tools.ts` (or `.js`/`.mjs`), walking up from `from`.
|
|
55
|
+
* Returns the absolute path, or null if none is found before the filesystem
|
|
56
|
+
* root or a `package.json` boundary without one above it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function findUserCommandsFile(from?: string): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Load user-land commands from a repo's `browser-tools.ts`. Returns a map keyed
|
|
61
|
+
* by command name (empty if the file is absent). Throws on a duplicate name.
|
|
62
|
+
*/
|
|
63
|
+
export declare function loadUserCommands(from?: string): Promise<Map<string, Command>>;
|
|
64
|
+
export { z };
|
|
65
|
+
//# sourceMappingURL=command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB;;;;;;;;;;;;;;;GAeG;AAEH,iFAAiF;AACjF,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,IAAI,CAAA;IACV,sEAAsE;IACtE,EAAE,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAA;IACrC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACtE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAC9B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAC7B;AAED,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO;IACvD,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,CAAA;IACT,GAAG,EAAE,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5D;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,EACT,GAAG,EAAE,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5D,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED,0DAA0D;AAC1D,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CAQ9C;AAED,qEAAqE;AACrE,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAG7F;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnG;AAaD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAWhF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAanF;AAED,OAAO,EAAE,CAAC,EAAE,CAAA"}
|
package/dist/command.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { getPage } from './context.js';
|
|
6
|
+
import { locatorOn } from './element.js';
|
|
7
|
+
/** Define a browser command. See `CommandCtx` for what `ctx` provides. */
|
|
8
|
+
export function command(name, params, run, description) {
|
|
9
|
+
return { name, params, run, description };
|
|
10
|
+
}
|
|
11
|
+
/** Build the command context bound to a specific page. */
|
|
12
|
+
export function makeCtx(page) {
|
|
13
|
+
return {
|
|
14
|
+
page,
|
|
15
|
+
el: (sel) => locatorOn(page, sel),
|
|
16
|
+
byRole: (role, name) => page.getByRole(role, name == null ? undefined : { name }),
|
|
17
|
+
byLabel: (text) => page.getByLabel(text),
|
|
18
|
+
byText: (text) => page.getByText(text),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Validate args against a command's schema and run it on `page`. */
|
|
22
|
+
export async function runCommand(page, cmd, rawArgs) {
|
|
23
|
+
const args = cmd.params.parse(rawArgs);
|
|
24
|
+
return cmd.run(makeCtx(page), args);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Invoke a command against the active scenario page from inside a test. Pair
|
|
28
|
+
* with a direct import of the verb from `browser-tools.ts` so the args are
|
|
29
|
+
* type-checked against its schema:
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { call } from '@opice/harness'
|
|
33
|
+
* import { fullEnum } from '../browser-tools'
|
|
34
|
+
* await call(fullEnum, { label: 'Typ', option: 'Faktura' })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export async function call(cmd, args) {
|
|
38
|
+
return runCommand(getPage(), cmd, args);
|
|
39
|
+
}
|
|
40
|
+
/** Duck-type check: is a module export a `Command`? */
|
|
41
|
+
function isCommand(value) {
|
|
42
|
+
return (typeof value === 'object'
|
|
43
|
+
&& value !== null
|
|
44
|
+
&& typeof value.name === 'string'
|
|
45
|
+
&& typeof value.run === 'function'
|
|
46
|
+
&& 'params' in value);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Locate a repo's `browser-tools.ts` (or `.js`/`.mjs`), walking up from `from`.
|
|
50
|
+
* Returns the absolute path, or null if none is found before the filesystem
|
|
51
|
+
* root or a `package.json` boundary without one above it.
|
|
52
|
+
*/
|
|
53
|
+
export function findUserCommandsFile(from = process.cwd()) {
|
|
54
|
+
let dir = path.resolve(from);
|
|
55
|
+
for (;;) {
|
|
56
|
+
for (const name of ['browser-tools.ts', 'browser-tools.js', 'browser-tools.mjs']) {
|
|
57
|
+
const candidate = path.join(dir, name);
|
|
58
|
+
if (existsSync(candidate))
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
const parent = path.dirname(dir);
|
|
62
|
+
if (parent === dir)
|
|
63
|
+
return null;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load user-land commands from a repo's `browser-tools.ts`. Returns a map keyed
|
|
69
|
+
* by command name (empty if the file is absent). Throws on a duplicate name.
|
|
70
|
+
*/
|
|
71
|
+
export async function loadUserCommands(from) {
|
|
72
|
+
const registry = new Map();
|
|
73
|
+
const file = findUserCommandsFile(from);
|
|
74
|
+
if (!file)
|
|
75
|
+
return registry;
|
|
76
|
+
const mod = (await import(pathToFileURL(file).href));
|
|
77
|
+
for (const value of Object.values(mod)) {
|
|
78
|
+
if (!isCommand(value))
|
|
79
|
+
continue;
|
|
80
|
+
if (registry.has(value.name)) {
|
|
81
|
+
throw new Error(`browser-tools.ts: duplicate command name "${value.name}" (${file})`);
|
|
82
|
+
}
|
|
83
|
+
registry.set(value.name, value);
|
|
84
|
+
}
|
|
85
|
+
return registry;
|
|
86
|
+
}
|
|
87
|
+
export { z };
|
|
88
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BrowserContext, type Page } from 'playwright';
|
|
2
|
+
/** The active page, or throw if called outside a `browserTest` scenario. */
|
|
3
|
+
export declare function getPage(): Page;
|
|
4
|
+
/** The active browser context (for cookies/storage, new tabs, etc.). */
|
|
5
|
+
export declare function getContext(): BrowserContext;
|
|
6
|
+
/** Launch a fresh isolated browser + context + page. Called from `beforeAll`. */
|
|
7
|
+
export declare function launchPage(): Promise<Page>;
|
|
8
|
+
/** Close the page, context, and browser. Called from `afterAll`. */
|
|
9
|
+
export declare function closePage(): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,YAAY,CAAA;AAoBnF,4EAA4E;AAC5E,wBAAgB,OAAO,IAAI,IAAI,CAK9B;AAED,wEAAwE;AACxE,wBAAgB,UAAU,IAAI,cAAc,CAK3C;AAED,iFAAiF;AACjF,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAKhD;AAED,oEAAoE;AACpE,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAS/C"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* The live Playwright page for the running scenario. `browserTest` launches a
|
|
4
|
+
* fresh browser + context + page per scenario (`beforeAll`) and tears it down
|
|
5
|
+
* (`afterAll`); the DSL — `el`, `byRole`, navigation — reads the current page
|
|
6
|
+
* from here. This module replaces the old agent-browser CLI session handling:
|
|
7
|
+
* there is no shell-out and no daemon, the browser runs in-process under
|
|
8
|
+
* `bun test`.
|
|
9
|
+
*/
|
|
10
|
+
let browser = null;
|
|
11
|
+
let context = null;
|
|
12
|
+
let page = null;
|
|
13
|
+
/** Headed mode for local debugging (`OPICE_HEADED=1` or Playwright's `PWDEBUG`). */
|
|
14
|
+
function headed() {
|
|
15
|
+
return !!(process.env['OPICE_HEADED'] || process.env['PWDEBUG']);
|
|
16
|
+
}
|
|
17
|
+
/** The active page, or throw if called outside a `browserTest` scenario. */
|
|
18
|
+
export function getPage() {
|
|
19
|
+
if (!page) {
|
|
20
|
+
throw new Error('opice: no active page — call DSL helpers inside a browserTest scenario.');
|
|
21
|
+
}
|
|
22
|
+
return page;
|
|
23
|
+
}
|
|
24
|
+
/** The active browser context (for cookies/storage, new tabs, etc.). */
|
|
25
|
+
export function getContext() {
|
|
26
|
+
if (!context) {
|
|
27
|
+
throw new Error('opice: no active browser context — call inside a browserTest scenario.');
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
/** Launch a fresh isolated browser + context + page. Called from `beforeAll`. */
|
|
32
|
+
export async function launchPage() {
|
|
33
|
+
browser = await chromium.launch({ headless: !headed() });
|
|
34
|
+
context = await browser.newContext();
|
|
35
|
+
page = await context.newPage();
|
|
36
|
+
return page;
|
|
37
|
+
}
|
|
38
|
+
/** Close the page, context, and browser. Called from `afterAll`. */
|
|
39
|
+
export async function closePage() {
|
|
40
|
+
try {
|
|
41
|
+
await context?.close();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await browser?.close();
|
|
45
|
+
page = null;
|
|
46
|
+
context = null;
|
|
47
|
+
browser = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgD,MAAM,YAAY,CAAA;AAEnF;;;;;;;GAOG;AAEH,IAAI,OAAO,GAAmB,IAAI,CAAA;AAClC,IAAI,OAAO,GAA0B,IAAI,CAAA;AACzC,IAAI,IAAI,GAAgB,IAAI,CAAA;AAE5B,oFAAoF;AACpF,SAAS,MAAM;IACd,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;AACjE,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,OAAO;IACtB,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,yEAAyE,CAAC,CAAA;IAC3F,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,wEAAwE,CAAC,CAAA;IAC1F,CAAC;IACD,OAAO,OAAO,CAAA;AACf,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,UAAU;IAC/B,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACxD,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,CAAA;IACpC,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;IAC9B,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,SAAS;IAC9B,IAAI,CAAC;QACJ,MAAM,OAAO,EAAE,KAAK,EAAE,CAAA;IACvB,CAAC;YAAS,CAAC;QACV,MAAM,OAAO,EAAE,KAAK,EAAE,CAAA;QACtB,IAAI,GAAG,IAAI,CAAA;QACX,OAAO,GAAG,IAAI,CAAA;QACd,OAAO,GAAG,IAAI,CAAA;IACf,CAAC;AACF,CAAC"}
|
package/dist/dsn.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An opice DSN packs everything a project needs to report into one string:
|
|
3
|
+
*
|
|
4
|
+
* OPICE_DSN=https://<apiKey>@<host>/<slug>
|
|
5
|
+
*
|
|
6
|
+
* The api key rides in the userinfo, the host is the platform endpoint, and
|
|
7
|
+
* the first path segment is the project slug. It's the single value the
|
|
8
|
+
* dashboard hands you to drop into `.env`; the individual `OPICE_*` vars still
|
|
9
|
+
* win when set, so a DSN is purely a convenience fallback.
|
|
10
|
+
*/
|
|
11
|
+
export interface OpiceDsn {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
project: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function parseOpiceDsn(raw: string | undefined | null): OpiceDsn | null;
|
|
17
|
+
//# sourceMappingURL=dsn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dsn.d.ts","sourceRoot":"","sources":["../src/dsn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,QAAQ;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,QAAQ,GAAG,IAAI,CAY7E"}
|
package/dist/dsn.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function parseOpiceDsn(raw) {
|
|
2
|
+
if (!raw)
|
|
3
|
+
return null;
|
|
4
|
+
let url;
|
|
5
|
+
try {
|
|
6
|
+
url = new URL(raw);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const apiKey = decodeURIComponent(url.username);
|
|
12
|
+
const project = url.pathname.replace(/^\/+/, '').split('/')[0] ?? '';
|
|
13
|
+
if (!apiKey || !project)
|
|
14
|
+
return null;
|
|
15
|
+
return { apiKey, endpoint: `${url.protocol}//${url.host}`, project };
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=dsn.js.map
|
package/dist/dsn.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dsn.js","sourceRoot":"","sources":["../src/dsn.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,aAAa,CAAC,GAA8B;IAC3D,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,IAAI,GAAQ,CAAA;IACZ,IAAI,CAAC;QACJ,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;IACnB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAA;IACZ,CAAC;IACD,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC/C,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACpE,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IACpC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,QAAQ,KAAK,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAA;AACrE,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Locator, Page } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a selector into a `Locator` on an explicit page — the shared core
|
|
4
|
+
* behind `el()` and the command-registry context. Bare identifiers become
|
|
5
|
+
* test-ids (`getByTestId`, matching `data-testid`); anything with CSS-flavoured
|
|
6
|
+
* characters (`[ ] . # : > ` or a space) is a raw CSS selector.
|
|
7
|
+
*/
|
|
8
|
+
export declare function locatorOn(page: Page, selectorOrTestId: string): Locator;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a selector into a Playwright `Locator`.
|
|
11
|
+
*
|
|
12
|
+
* Bare identifiers are auto-wrapped as test-ids (`page.getByTestId`, which
|
|
13
|
+
* matches `data-testid` by default); anything with CSS-flavoured characters
|
|
14
|
+
* (`[ ] . # : > ` or a space) is treated as a raw CSS selector. Heuristic — if
|
|
15
|
+
* you need a plain-tag selector (e.g. `h1`), give it structure (`main h1`).
|
|
16
|
+
*
|
|
17
|
+
* The returned value is a real Playwright `Locator`, so the full Locator API
|
|
18
|
+
* (`.click()`, `.fill()`, `.textContent()`, `.first()`, `.nth()`, …) and the
|
|
19
|
+
* web-first `expect(locator)` assertions are available. All actions auto-wait
|
|
20
|
+
* for actionability and fire real user gestures.
|
|
21
|
+
*/
|
|
22
|
+
export declare function el(selectorOrTestId: string): Locator;
|
|
23
|
+
/**
|
|
24
|
+
* Build a `[data-testid="..."]` selector string, for composing into a larger
|
|
25
|
+
* CSS selector: `el(`${tid('row')} button`)`. For a plain test-id, prefer
|
|
26
|
+
* `el('row')` directly.
|
|
27
|
+
*/
|
|
28
|
+
export declare function tid(testId: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Poll a (possibly async) condition until it returns true or times out.
|
|
31
|
+
*
|
|
32
|
+
* Prefer Playwright's retrying assertions — `await expect(el('x')).toBeVisible()`,
|
|
33
|
+
* `.toHaveText(...)` — which auto-wait and give better failure messages. Keep
|
|
34
|
+
* `waitFor` for arbitrary predicates that don't map to a locator assertion.
|
|
35
|
+
*/
|
|
36
|
+
export declare function waitFor(condition: () => boolean | Promise<boolean>, { timeout, interval, message }?: {
|
|
37
|
+
timeout?: number;
|
|
38
|
+
interval?: number;
|
|
39
|
+
message?: string;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
/** Fixed sleep. Avoid when possible — prefer `waitFor` or retrying assertions. */
|
|
42
|
+
export declare function wait(ms: number): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Evaluate JavaScript in the page and return its result. Thin wrapper over
|
|
45
|
+
* `page.evaluate`; the value is the real JS value (not a JSON string).
|
|
46
|
+
*/
|
|
47
|
+
export declare function evalJs<T = unknown>(js: string): Promise<T>;
|
|
48
|
+
/** Capture a screenshot to `path` (or a temp file) and return the path. */
|
|
49
|
+
export declare function screenshot(path?: string): Promise<string>;
|
|
50
|
+
//# sourceMappingURL=element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAM/C;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAKvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,EAAE,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC5B,SAAS,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EAC3C,EAAE,OAAsB,EAAE,QAAwB,EAAE,OAAO,EAAE,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3H,OAAO,CAAC,IAAI,CAAC,CAef;AAED,kFAAkF;AAClF,wBAAsB,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpD;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAE1D;AAED,2EAA2E;AAC3E,wBAAsB,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI/D"}
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { getPage } from './context.js';
|
|
2
|
+
const POLL_INTERVAL = 200;
|
|
3
|
+
const POLL_TIMEOUT = 10_000;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a selector into a `Locator` on an explicit page — the shared core
|
|
6
|
+
* behind `el()` and the command-registry context. Bare identifiers become
|
|
7
|
+
* test-ids (`getByTestId`, matching `data-testid`); anything with CSS-flavoured
|
|
8
|
+
* characters (`[ ] . # : > ` or a space) is a raw CSS selector.
|
|
9
|
+
*/
|
|
10
|
+
export function locatorOn(page, selectorOrTestId) {
|
|
11
|
+
if (/[\[\].#:> ]/.test(selectorOrTestId)) {
|
|
12
|
+
return page.locator(selectorOrTestId);
|
|
13
|
+
}
|
|
14
|
+
return page.getByTestId(selectorOrTestId);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a selector into a Playwright `Locator`.
|
|
18
|
+
*
|
|
19
|
+
* Bare identifiers are auto-wrapped as test-ids (`page.getByTestId`, which
|
|
20
|
+
* matches `data-testid` by default); anything with CSS-flavoured characters
|
|
21
|
+
* (`[ ] . # : > ` or a space) is treated as a raw CSS selector. Heuristic — if
|
|
22
|
+
* you need a plain-tag selector (e.g. `h1`), give it structure (`main h1`).
|
|
23
|
+
*
|
|
24
|
+
* The returned value is a real Playwright `Locator`, so the full Locator API
|
|
25
|
+
* (`.click()`, `.fill()`, `.textContent()`, `.first()`, `.nth()`, …) and the
|
|
26
|
+
* web-first `expect(locator)` assertions are available. All actions auto-wait
|
|
27
|
+
* for actionability and fire real user gestures.
|
|
28
|
+
*/
|
|
29
|
+
export function el(selectorOrTestId) {
|
|
30
|
+
return locatorOn(getPage(), selectorOrTestId);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build a `[data-testid="..."]` selector string, for composing into a larger
|
|
34
|
+
* CSS selector: `el(`${tid('row')} button`)`. For a plain test-id, prefer
|
|
35
|
+
* `el('row')` directly.
|
|
36
|
+
*/
|
|
37
|
+
export function tid(testId) {
|
|
38
|
+
return `[data-testid="${testId}"]`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Poll a (possibly async) condition until it returns true or times out.
|
|
42
|
+
*
|
|
43
|
+
* Prefer Playwright's retrying assertions — `await expect(el('x')).toBeVisible()`,
|
|
44
|
+
* `.toHaveText(...)` — which auto-wait and give better failure messages. Keep
|
|
45
|
+
* `waitFor` for arbitrary predicates that don't map to a locator assertion.
|
|
46
|
+
*/
|
|
47
|
+
export async function waitFor(condition, { timeout = POLL_TIMEOUT, interval = POLL_INTERVAL, message } = {}) {
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
while (Date.now() - start < timeout) {
|
|
50
|
+
try {
|
|
51
|
+
if (await condition())
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// condition threw — treat as not yet ready
|
|
56
|
+
}
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
58
|
+
}
|
|
59
|
+
if (!(await condition())) {
|
|
60
|
+
const elapsed = Date.now() - start;
|
|
61
|
+
const hint = message ?? condition.toString().slice(0, 120);
|
|
62
|
+
throw new Error(`waitFor timed out after ${elapsed}ms: ${hint}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Fixed sleep. Avoid when possible — prefer `waitFor` or retrying assertions. */
|
|
66
|
+
export async function wait(ms) {
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Evaluate JavaScript in the page and return its result. Thin wrapper over
|
|
71
|
+
* `page.evaluate`; the value is the real JS value (not a JSON string).
|
|
72
|
+
*/
|
|
73
|
+
export function evalJs(js) {
|
|
74
|
+
return getPage().evaluate(js);
|
|
75
|
+
}
|
|
76
|
+
/** Capture a screenshot to `path` (or a temp file) and return the path. */
|
|
77
|
+
export async function screenshot(path) {
|
|
78
|
+
const target = path ?? `/tmp/opice-screenshot-${Date.now()}.png`;
|
|
79
|
+
await getPage().screenshot({ path: target });
|
|
80
|
+
return target;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=element.js.map
|