@speqkit/plugin-playwright 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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/driver.d.ts +38 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +29 -0
- package/dist/driver.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +241 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/src/driver.ts +61 -0
- package/src/index.ts +290 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 speqkit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @speqkit/plugin-playwright
|
|
2
|
+
|
|
3
|
+
Browser steps, scoped `browser` / `context` / `page` resources, and screenshots
|
|
4
|
+
attached as artifacts.
|
|
5
|
+
|
|
6
|
+
```yaml
|
|
7
|
+
# .speq/speq.yaml
|
|
8
|
+
plugins: [yaml, http, cli, playwright]
|
|
9
|
+
|
|
10
|
+
playwright:
|
|
11
|
+
browser: chromium # chromium | firefox | webkit
|
|
12
|
+
headless: true
|
|
13
|
+
baseUrl: https://example.com
|
|
14
|
+
viewport: { width: 1280, height: 720 }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
name: the browser reaches the page
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- id: home
|
|
22
|
+
type: browser.open
|
|
23
|
+
url: /
|
|
24
|
+
|
|
25
|
+
- id: shot
|
|
26
|
+
type: browser.screenshot
|
|
27
|
+
name: home.png
|
|
28
|
+
fullPage: true
|
|
29
|
+
|
|
30
|
+
assert:
|
|
31
|
+
- type: title_is
|
|
32
|
+
expected: Example Domain
|
|
33
|
+
- type: visible
|
|
34
|
+
selector: h1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Playwright itself is an **optional peer dependency**, imported the moment a
|
|
38
|
+
browser is first needed. A repository that only runs HTTP smoke checks never
|
|
39
|
+
downloads a browser because this package exists.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add -D playwright && pnpm exec playwright install chromium
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Steps and assertions
|
|
46
|
+
|
|
47
|
+
| Step | Input | Result |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `browser.open` | `url`, `waitUntil?` | `url`, `title`, `status` |
|
|
50
|
+
| `browser.click` | `selector`, `timeout?` | `selector`, `url` |
|
|
51
|
+
| `browser.fill` | `selector`, `value` | `selector`, `value` |
|
|
52
|
+
| `browser.press` | `selector`, `key` | `selector`, `key` |
|
|
53
|
+
| `browser.wait_for` | `selector`, `state?` | `selector` |
|
|
54
|
+
| `browser.text` | `selector` | `text` — readable later as `${id.text}` |
|
|
55
|
+
| `browser.screenshot` | `name?`, `fullPage?` | attaches a PNG |
|
|
56
|
+
|
|
57
|
+
Assertions: `visible`, `text_contains`, `title_is`, `url_contains`.
|
|
58
|
+
|
|
59
|
+
## Resources
|
|
60
|
+
|
|
61
|
+
| Name | Scope | Lifetime |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `browser` | `run` | one process for the entire run |
|
|
64
|
+
| `browser.context` | `test` | fresh cookies and storage per test |
|
|
65
|
+
| `page` | `test` | closed when the test ends, pass or fail |
|
|
66
|
+
|
|
67
|
+
Nothing here manages that lifetime. The plugin *declares* it and the kernel
|
|
68
|
+
acquires lazily, caches per scope and tears down in reverse order — which is
|
|
69
|
+
also why a suite that crashes does not leave browsers running.
|
|
70
|
+
|
|
71
|
+
## Why this plugin exists
|
|
72
|
+
|
|
73
|
+
It is the second half of the architecture gate. `@speqkit/plugin-loop` proved
|
|
74
|
+
control flow can live outside the kernel; this one exercises the two parts of
|
|
75
|
+
the spine the loop never touched.
|
|
76
|
+
|
|
77
|
+
**Scoped resources — passed, no kernel change.** Three scopes, a resource
|
|
78
|
+
depending on a resource, config reaching `setup`, reverse teardown, and an
|
|
79
|
+
assertion reaching for `page` without a step in front of it. All of it was
|
|
80
|
+
already expressible through the published API.
|
|
81
|
+
|
|
82
|
+
**Binary artifacts — the kernel had to keep a promise it had already made.**
|
|
83
|
+
`ExecContext.attach` was on the contract from day one, but the body went
|
|
84
|
+
nowhere: the event carried a byte count and the bytes were dropped. There was
|
|
85
|
+
nothing to fix in the plugin. What changed is that `path` was added to the
|
|
86
|
+
`artifact.attached` event and the kernel now writes the file — an added field,
|
|
87
|
+
a minor, and `attach(name, body, contentType)` is byte-for-byte the same call.
|
|
88
|
+
|
|
89
|
+
**Suite scope was declared and never opened.** `ResourceScope` published three
|
|
90
|
+
scopes and the runner opened two, so a `suite` resource failed with "scope is
|
|
91
|
+
not open here". Fixed in the runner; no API change.
|
|
92
|
+
|
|
93
|
+
## What this plugin still cannot do
|
|
94
|
+
|
|
95
|
+
**Screenshot on failure.** The obvious feature, and it is not implementable
|
|
96
|
+
from here. A `test:after` hook receives a `HookPayload` with no status on it
|
|
97
|
+
and no way to attach anything, so the plugin cannot see that a test failed or
|
|
98
|
+
hand back the picture proving it. That is a genuine gap in the spine, and the
|
|
99
|
+
fix is a design decision rather than a workaround: either `HookPayload` carries
|
|
100
|
+
the outcome and an attach, or failure capture becomes a step-level concern with
|
|
101
|
+
a `try/catch` step type. It is recorded here instead of being smuggled in.
|
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright's surface, described structurally rather than imported.
|
|
3
|
+
*
|
|
4
|
+
* The plugin must type-check and install in a repository that has no browsers
|
|
5
|
+
* anywhere near it — a Go service that only runs HTTP smoke checks should not
|
|
6
|
+
* pull down 300MB because this package exists in the registry. So `playwright`
|
|
7
|
+
* is an optional peer dependency, loaded at the moment a browser is first
|
|
8
|
+
* needed, and the types below are the only coupling at build time.
|
|
9
|
+
*/
|
|
10
|
+
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
|
11
|
+
export interface Page {
|
|
12
|
+
goto(url: string, options?: Record<string, unknown>): Promise<{
|
|
13
|
+
status(): number;
|
|
14
|
+
} | null>;
|
|
15
|
+
title(): Promise<string>;
|
|
16
|
+
url(): string;
|
|
17
|
+
click(selector: string, options?: Record<string, unknown>): Promise<void>;
|
|
18
|
+
fill(selector: string, value: string, options?: Record<string, unknown>): Promise<void>;
|
|
19
|
+
press(selector: string, key: string, options?: Record<string, unknown>): Promise<void>;
|
|
20
|
+
textContent(selector: string, options?: Record<string, unknown>): Promise<string | null>;
|
|
21
|
+
waitForSelector(selector: string, options?: Record<string, unknown>): Promise<unknown>;
|
|
22
|
+
isVisible(selector: string, options?: Record<string, unknown>): Promise<boolean>;
|
|
23
|
+
screenshot(options?: Record<string, unknown>): Promise<Uint8Array>;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface BrowserContext {
|
|
27
|
+
newPage(): Promise<Page>;
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface Browser {
|
|
31
|
+
newContext(options?: Record<string, unknown>): Promise<BrowserContext>;
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface BrowserType {
|
|
35
|
+
launch(options?: Record<string, unknown>): Promise<Browser>;
|
|
36
|
+
}
|
|
37
|
+
export declare function loadBrowserType(name: BrowserName): Promise<BrowserType>;
|
|
38
|
+
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE3D,MAAM,WAAW,IAAI;IACnB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAAE,MAAM,IAAI,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAC1F,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IACxB,GAAG,IAAI,MAAM,CAAA;IACb,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtF,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACxF,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACtF,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAChF,UAAU,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IAClE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IACtE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5D;AAMD,wBAAsB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAgB7E"}
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright's surface, described structurally rather than imported.
|
|
3
|
+
*
|
|
4
|
+
* The plugin must type-check and install in a repository that has no browsers
|
|
5
|
+
* anywhere near it — a Go service that only runs HTTP smoke checks should not
|
|
6
|
+
* pull down 300MB because this package exists in the registry. So `playwright`
|
|
7
|
+
* is an optional peer dependency, loaded at the moment a browser is first
|
|
8
|
+
* needed, and the types below are the only coupling at build time.
|
|
9
|
+
*/
|
|
10
|
+
const INSTALL_HINT = "@speqkit/plugin-playwright needs the 'playwright' package, which it does not bundle.\n" +
|
|
11
|
+
' pnpm add -D playwright && pnpm exec playwright install chromium';
|
|
12
|
+
export async function loadBrowserType(name) {
|
|
13
|
+
// An indirect specifier on purpose: the module is resolved at run time by
|
|
14
|
+
// the host project, not at build time by this package.
|
|
15
|
+
const specifier = 'playwright';
|
|
16
|
+
let mod;
|
|
17
|
+
try {
|
|
18
|
+
mod = (await import(specifier));
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
throw new Error(`${INSTALL_HINT}\n (import failed: ${err instanceof Error ? err.message : String(err)})`);
|
|
22
|
+
}
|
|
23
|
+
const type = mod[name];
|
|
24
|
+
if (!type?.launch) {
|
|
25
|
+
throw new Error(`playwright exposes no browser named '${name}'; expected chromium, firefox or webkit`);
|
|
26
|
+
}
|
|
27
|
+
return type;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.js","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,MAAM,YAAY,GAChB,wFAAwF;IACxF,mEAAmE,CAAA;AAErE,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAiB;IACrD,0EAA0E;IAC1E,uDAAuD;IACvD,MAAM,SAAS,GAAG,YAAY,CAAA;IAC9B,IAAI,GAA4B,CAAA;IAChC,IAAI,CAAC;QACH,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,CAA4B,CAAA;IAC5D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,GAAG,YAAY,uBAAuB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5G,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAA4B,CAAA;IACjD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,yCAAyC,CAAC,CAAA;IACxG,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The second half of the architecture gate.
|
|
3
|
+
*
|
|
4
|
+
* `@speqkit/plugin-loop` proved control flow can live outside the kernel. This
|
|
5
|
+
* one exercises the two parts of the spine it never touched:
|
|
6
|
+
*
|
|
7
|
+
* - scoped resources — a browser that outlives the run, a page that dies
|
|
8
|
+
* with the test, torn down in reverse order whatever happens;
|
|
9
|
+
* - binary artifacts — a PNG handed to `ctx.attach` and reachable from any
|
|
10
|
+
* reporter, including ones that do not exist yet.
|
|
11
|
+
*
|
|
12
|
+
* Same rule as the loop plugin: written against the published API only. What
|
|
13
|
+
* it could not do without a kernel change is recorded in the README rather
|
|
14
|
+
* than worked around here.
|
|
15
|
+
*/
|
|
16
|
+
declare const _default: import("@speqkit/plugin-api").PluginSpec;
|
|
17
|
+
export default _default;
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;GAcG;;AACH,wBA2PE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { definePlugin } from '@speqkit/plugin-api';
|
|
2
|
+
import { loadBrowserType } from './driver.js';
|
|
3
|
+
/**
|
|
4
|
+
* The second half of the architecture gate.
|
|
5
|
+
*
|
|
6
|
+
* `@speqkit/plugin-loop` proved control flow can live outside the kernel. This
|
|
7
|
+
* one exercises the two parts of the spine it never touched:
|
|
8
|
+
*
|
|
9
|
+
* - scoped resources — a browser that outlives the run, a page that dies
|
|
10
|
+
* with the test, torn down in reverse order whatever happens;
|
|
11
|
+
* - binary artifacts — a PNG handed to `ctx.attach` and reachable from any
|
|
12
|
+
* reporter, including ones that do not exist yet.
|
|
13
|
+
*
|
|
14
|
+
* Same rule as the loop plugin: written against the published API only. What
|
|
15
|
+
* it could not do without a kernel change is recorded in the README rather
|
|
16
|
+
* than worked around here.
|
|
17
|
+
*/
|
|
18
|
+
export default definePlugin({
|
|
19
|
+
name: '@speqkit/plugin-playwright',
|
|
20
|
+
configSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
browser: { type: 'string' },
|
|
24
|
+
headless: { type: 'boolean' },
|
|
25
|
+
slowMo: { type: 'number' },
|
|
26
|
+
baseUrl: { type: 'string' },
|
|
27
|
+
viewport: { type: 'object' },
|
|
28
|
+
timeoutMs: { type: 'number' }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
setup(ctx) {
|
|
32
|
+
/* ---------------------------------------------------------------- */
|
|
33
|
+
/* Resources. Three scopes, three lifetimes, declared not managed. */
|
|
34
|
+
/* ---------------------------------------------------------------- */
|
|
35
|
+
// One browser process for the whole run. Launching it per test is the
|
|
36
|
+
// single most common reason a UI suite takes twenty minutes.
|
|
37
|
+
ctx.defineResource('browser', {
|
|
38
|
+
scope: 'run',
|
|
39
|
+
async setup(res) {
|
|
40
|
+
const config = res.config();
|
|
41
|
+
const type = await loadBrowserType(config.browser ?? 'chromium');
|
|
42
|
+
return type.launch({
|
|
43
|
+
headless: config.headless ?? true,
|
|
44
|
+
...(config.slowMo ? { slowMo: config.slowMo } : {})
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
teardown: (browser) => browser.close()
|
|
48
|
+
});
|
|
49
|
+
// A fresh context per test is what isolation actually means here:
|
|
50
|
+
// cookies, storage and permissions start empty every time, and nothing
|
|
51
|
+
// a test does can leak into the next one.
|
|
52
|
+
ctx.defineResource('browser.context', {
|
|
53
|
+
scope: 'test',
|
|
54
|
+
async setup(res) {
|
|
55
|
+
const config = res.config();
|
|
56
|
+
const browser = await res.resource('browser');
|
|
57
|
+
return browser.newContext({
|
|
58
|
+
...(config.baseUrl ? { baseURL: config.baseUrl } : {}),
|
|
59
|
+
...(config.viewport ? { viewport: config.viewport } : {})
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
teardown: (context) => context.close()
|
|
63
|
+
});
|
|
64
|
+
ctx.defineResource('page', {
|
|
65
|
+
scope: 'test',
|
|
66
|
+
async setup(res) {
|
|
67
|
+
const context = await res.resource('browser.context');
|
|
68
|
+
return context.newPage();
|
|
69
|
+
},
|
|
70
|
+
teardown: (page) => page.close()
|
|
71
|
+
});
|
|
72
|
+
/* ---------------------------------------------------------------- */
|
|
73
|
+
/* Steps */
|
|
74
|
+
/* ---------------------------------------------------------------- */
|
|
75
|
+
const selectorSchema = {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: { selector: { type: 'string' }, timeout: { type: 'number' } },
|
|
78
|
+
required: ['selector'],
|
|
79
|
+
additionalProperties: false
|
|
80
|
+
};
|
|
81
|
+
ctx.defineStepType('browser.open', {
|
|
82
|
+
schema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: { url: { type: 'string' }, waitUntil: { type: 'string' } },
|
|
85
|
+
required: ['url'],
|
|
86
|
+
additionalProperties: false
|
|
87
|
+
},
|
|
88
|
+
async execute(exec, input) {
|
|
89
|
+
const page = await exec.resource('page');
|
|
90
|
+
const response = await page.goto(String(input.url), {
|
|
91
|
+
...(input.waitUntil ? { waitUntil: input.waitUntil } : {})
|
|
92
|
+
});
|
|
93
|
+
return { url: page.url(), title: await page.title(), status: response?.status() ?? null };
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
ctx.defineStepType('browser.click', {
|
|
97
|
+
schema: selectorSchema,
|
|
98
|
+
async execute(exec, input) {
|
|
99
|
+
const page = await exec.resource('page');
|
|
100
|
+
await page.click(String(input.selector), timeoutOf(exec.config(), input));
|
|
101
|
+
return { selector: input.selector, url: page.url() };
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
ctx.defineStepType('browser.fill', {
|
|
105
|
+
schema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: { selector: { type: 'string' }, value: {}, timeout: { type: 'number' } },
|
|
108
|
+
required: ['selector', 'value'],
|
|
109
|
+
additionalProperties: false
|
|
110
|
+
},
|
|
111
|
+
async execute(exec, input) {
|
|
112
|
+
const page = await exec.resource('page');
|
|
113
|
+
await page.fill(String(input.selector), String(input.value), timeoutOf(exec.config(), input));
|
|
114
|
+
return { selector: input.selector, value: input.value };
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
ctx.defineStepType('browser.press', {
|
|
118
|
+
schema: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: { selector: { type: 'string' }, key: { type: 'string' }, timeout: { type: 'number' } },
|
|
121
|
+
required: ['selector', 'key'],
|
|
122
|
+
additionalProperties: false
|
|
123
|
+
},
|
|
124
|
+
async execute(exec, input) {
|
|
125
|
+
const page = await exec.resource('page');
|
|
126
|
+
await page.press(String(input.selector), String(input.key), timeoutOf(exec.config(), input));
|
|
127
|
+
return { selector: input.selector, key: input.key };
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
ctx.defineStepType('browser.wait_for', {
|
|
131
|
+
schema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: { selector: { type: 'string' }, state: { type: 'string' }, timeout: { type: 'number' } },
|
|
134
|
+
required: ['selector'],
|
|
135
|
+
additionalProperties: false
|
|
136
|
+
},
|
|
137
|
+
async execute(exec, input) {
|
|
138
|
+
const page = await exec.resource('page');
|
|
139
|
+
await page.waitForSelector(String(input.selector), {
|
|
140
|
+
...timeoutOf(exec.config(), input),
|
|
141
|
+
...(input.state ? { state: input.state } : {})
|
|
142
|
+
});
|
|
143
|
+
return { selector: input.selector };
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// Reads the page into the variable namespace, so `${title.text}` is
|
|
147
|
+
// available to every later step exactly like an HTTP body would be.
|
|
148
|
+
ctx.defineStepType('browser.text', {
|
|
149
|
+
schema: selectorSchema,
|
|
150
|
+
async execute(exec, input) {
|
|
151
|
+
const page = await exec.resource('page');
|
|
152
|
+
const text = await page.textContent(String(input.selector), timeoutOf(exec.config(), input));
|
|
153
|
+
return { selector: input.selector, text: text ?? '' };
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// The artifact half of the gate: bytes go to `attach` and the kernel is
|
|
157
|
+
// responsible for them from there. This plugin never learns where they
|
|
158
|
+
// are written, and no reporter has to be taught about screenshots.
|
|
159
|
+
ctx.defineStepType('browser.screenshot', {
|
|
160
|
+
schema: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: { name: { type: 'string' }, selector: { type: 'string' }, fullPage: { type: 'boolean' } },
|
|
163
|
+
additionalProperties: false
|
|
164
|
+
},
|
|
165
|
+
async execute(exec, input) {
|
|
166
|
+
const page = await exec.resource('page');
|
|
167
|
+
const name = String(input.name ?? 'screenshot.png');
|
|
168
|
+
const bytes = await page.screenshot({ fullPage: input.fullPage === true });
|
|
169
|
+
exec.attach(name.endsWith('.png') ? name : `${name}.png`, bytes, 'image/png');
|
|
170
|
+
return { name, bytes: bytes.byteLength, url: page.url() };
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
/* ---------------------------------------------------------------- */
|
|
174
|
+
/* Assertions */
|
|
175
|
+
/* ---------------------------------------------------------------- */
|
|
176
|
+
// An assertion may reach for a resource too, so `visible` does not need
|
|
177
|
+
// a step in front of it just to have something to look at.
|
|
178
|
+
ctx.defineAssertion('visible', {
|
|
179
|
+
schema: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: { selector: { type: 'string' } },
|
|
182
|
+
required: ['selector'],
|
|
183
|
+
additionalProperties: false
|
|
184
|
+
},
|
|
185
|
+
async evaluate(assert, input) {
|
|
186
|
+
const page = await assert.resource('page');
|
|
187
|
+
const selector = String(input.selector);
|
|
188
|
+
const visible = await page.isVisible(selector);
|
|
189
|
+
return outcome(visible, `${selector} is not visible`, `${selector} is visible`, true, visible);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
ctx.defineAssertion('text_contains', {
|
|
193
|
+
schema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: { expected: { type: 'string' } },
|
|
196
|
+
required: ['expected'],
|
|
197
|
+
additionalProperties: false
|
|
198
|
+
},
|
|
199
|
+
evaluate(assert, input) {
|
|
200
|
+
const actual = String(assert.last?.text ?? '');
|
|
201
|
+
const needle = String(input.expected);
|
|
202
|
+
return outcome(actual.includes(needle), `expected text to contain ${JSON.stringify(needle)}, got ${JSON.stringify(actual.slice(0, 200))}`, `text contains ${JSON.stringify(needle)}`, needle, actual);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
ctx.defineAssertion('title_is', {
|
|
206
|
+
schema: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: { expected: { type: 'string' } },
|
|
209
|
+
required: ['expected'],
|
|
210
|
+
additionalProperties: false
|
|
211
|
+
},
|
|
212
|
+
async evaluate(assert, input) {
|
|
213
|
+
const page = await assert.resource('page');
|
|
214
|
+
const actual = await page.title();
|
|
215
|
+
return outcome(actual === input.expected, `expected title ${JSON.stringify(input.expected)}, got ${JSON.stringify(actual)}`, `title is ${JSON.stringify(actual)}`, input.expected, actual);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
ctx.defineAssertion('url_contains', {
|
|
219
|
+
schema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: { expected: { type: 'string' } },
|
|
222
|
+
required: ['expected'],
|
|
223
|
+
additionalProperties: false
|
|
224
|
+
},
|
|
225
|
+
async evaluate(assert, input) {
|
|
226
|
+
const page = await assert.resource('page');
|
|
227
|
+
const actual = page.url();
|
|
228
|
+
const needle = String(input.expected);
|
|
229
|
+
return outcome(actual.includes(needle), `expected url to contain ${JSON.stringify(needle)}, got ${JSON.stringify(actual)}`, `url is ${actual}`, needle, actual);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
function timeoutOf(config, input) {
|
|
235
|
+
const timeout = typeof input.timeout === 'number' ? input.timeout : config.timeoutMs;
|
|
236
|
+
return timeout === undefined ? {} : { timeout };
|
|
237
|
+
}
|
|
238
|
+
function outcome(passed, whenFailed, whenPassed, expected, actual) {
|
|
239
|
+
return { passed, message: passed ? whenPassed : whenFailed, expected, actual };
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@speqkit/plugin-playwright",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser steps, scoped browser/context/page resources and screenshot artifacts.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Stepan Kaziatko",
|
|
7
|
+
"homepage": "https://github.com/speqkit/speqkit#readme",
|
|
8
|
+
"bugs": "https://github.com/speqkit/speqkit/issues",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/speqkit/speqkit.git",
|
|
12
|
+
"directory": "packages/plugin-playwright"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"speqkit-plugin"
|
|
23
|
+
],
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"playwright": ">=1.40",
|
|
26
|
+
"@speqkit/plugin-api": "^0.4.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"playwright": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@speqkit/plugin-api": "0.4.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.0.0"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/driver.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright's surface, described structurally rather than imported.
|
|
3
|
+
*
|
|
4
|
+
* The plugin must type-check and install in a repository that has no browsers
|
|
5
|
+
* anywhere near it — a Go service that only runs HTTP smoke checks should not
|
|
6
|
+
* pull down 300MB because this package exists in the registry. So `playwright`
|
|
7
|
+
* is an optional peer dependency, loaded at the moment a browser is first
|
|
8
|
+
* needed, and the types below are the only coupling at build time.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type BrowserName = 'chromium' | 'firefox' | 'webkit'
|
|
12
|
+
|
|
13
|
+
export interface Page {
|
|
14
|
+
goto(url: string, options?: Record<string, unknown>): Promise<{ status(): number } | null>
|
|
15
|
+
title(): Promise<string>
|
|
16
|
+
url(): string
|
|
17
|
+
click(selector: string, options?: Record<string, unknown>): Promise<void>
|
|
18
|
+
fill(selector: string, value: string, options?: Record<string, unknown>): Promise<void>
|
|
19
|
+
press(selector: string, key: string, options?: Record<string, unknown>): Promise<void>
|
|
20
|
+
textContent(selector: string, options?: Record<string, unknown>): Promise<string | null>
|
|
21
|
+
waitForSelector(selector: string, options?: Record<string, unknown>): Promise<unknown>
|
|
22
|
+
isVisible(selector: string, options?: Record<string, unknown>): Promise<boolean>
|
|
23
|
+
screenshot(options?: Record<string, unknown>): Promise<Uint8Array>
|
|
24
|
+
close(): Promise<void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BrowserContext {
|
|
28
|
+
newPage(): Promise<Page>
|
|
29
|
+
close(): Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Browser {
|
|
33
|
+
newContext(options?: Record<string, unknown>): Promise<BrowserContext>
|
|
34
|
+
close(): Promise<void>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BrowserType {
|
|
38
|
+
launch(options?: Record<string, unknown>): Promise<Browser>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const INSTALL_HINT =
|
|
42
|
+
"@speqkit/plugin-playwright needs the 'playwright' package, which it does not bundle.\n" +
|
|
43
|
+
' pnpm add -D playwright && pnpm exec playwright install chromium'
|
|
44
|
+
|
|
45
|
+
export async function loadBrowserType(name: BrowserName): Promise<BrowserType> {
|
|
46
|
+
// An indirect specifier on purpose: the module is resolved at run time by
|
|
47
|
+
// the host project, not at build time by this package.
|
|
48
|
+
const specifier = 'playwright'
|
|
49
|
+
let mod: Record<string, unknown>
|
|
50
|
+
try {
|
|
51
|
+
mod = (await import(specifier)) as Record<string, unknown>
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw new Error(`${INSTALL_HINT}\n (import failed: ${err instanceof Error ? err.message : String(err)})`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const type = mod[name] as BrowserType | undefined
|
|
57
|
+
if (!type?.launch) {
|
|
58
|
+
throw new Error(`playwright exposes no browser named '${name}'; expected chromium, firefox or webkit`)
|
|
59
|
+
}
|
|
60
|
+
return type
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { definePlugin, type AssertOutcome, type InputSchema } from '@speqkit/plugin-api'
|
|
2
|
+
import { loadBrowserType, type Browser, type BrowserContext, type BrowserName, type Page } from './driver.js'
|
|
3
|
+
|
|
4
|
+
interface PlaywrightConfig {
|
|
5
|
+
browser?: BrowserName
|
|
6
|
+
headless?: boolean
|
|
7
|
+
slowMo?: number
|
|
8
|
+
baseUrl?: string
|
|
9
|
+
viewport?: { width: number; height: number }
|
|
10
|
+
timeoutMs?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The second half of the architecture gate.
|
|
15
|
+
*
|
|
16
|
+
* `@speqkit/plugin-loop` proved control flow can live outside the kernel. This
|
|
17
|
+
* one exercises the two parts of the spine it never touched:
|
|
18
|
+
*
|
|
19
|
+
* - scoped resources — a browser that outlives the run, a page that dies
|
|
20
|
+
* with the test, torn down in reverse order whatever happens;
|
|
21
|
+
* - binary artifacts — a PNG handed to `ctx.attach` and reachable from any
|
|
22
|
+
* reporter, including ones that do not exist yet.
|
|
23
|
+
*
|
|
24
|
+
* Same rule as the loop plugin: written against the published API only. What
|
|
25
|
+
* it could not do without a kernel change is recorded in the README rather
|
|
26
|
+
* than worked around here.
|
|
27
|
+
*/
|
|
28
|
+
export default definePlugin({
|
|
29
|
+
name: '@speqkit/plugin-playwright',
|
|
30
|
+
configSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
browser: { type: 'string' },
|
|
34
|
+
headless: { type: 'boolean' },
|
|
35
|
+
slowMo: { type: 'number' },
|
|
36
|
+
baseUrl: { type: 'string' },
|
|
37
|
+
viewport: { type: 'object' },
|
|
38
|
+
timeoutMs: { type: 'number' }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
setup(ctx) {
|
|
43
|
+
/* ---------------------------------------------------------------- */
|
|
44
|
+
/* Resources. Three scopes, three lifetimes, declared not managed. */
|
|
45
|
+
/* ---------------------------------------------------------------- */
|
|
46
|
+
|
|
47
|
+
// One browser process for the whole run. Launching it per test is the
|
|
48
|
+
// single most common reason a UI suite takes twenty minutes.
|
|
49
|
+
ctx.defineResource<Browser>('browser', {
|
|
50
|
+
scope: 'run',
|
|
51
|
+
async setup(res) {
|
|
52
|
+
const config = res.config<PlaywrightConfig>()
|
|
53
|
+
const type = await loadBrowserType(config.browser ?? 'chromium')
|
|
54
|
+
return type.launch({
|
|
55
|
+
headless: config.headless ?? true,
|
|
56
|
+
...(config.slowMo ? { slowMo: config.slowMo } : {})
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
teardown: (browser) => browser.close()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// A fresh context per test is what isolation actually means here:
|
|
63
|
+
// cookies, storage and permissions start empty every time, and nothing
|
|
64
|
+
// a test does can leak into the next one.
|
|
65
|
+
ctx.defineResource<BrowserContext>('browser.context', {
|
|
66
|
+
scope: 'test',
|
|
67
|
+
async setup(res) {
|
|
68
|
+
const config = res.config<PlaywrightConfig>()
|
|
69
|
+
const browser = await res.resource<Browser>('browser')
|
|
70
|
+
return browser.newContext({
|
|
71
|
+
...(config.baseUrl ? { baseURL: config.baseUrl } : {}),
|
|
72
|
+
...(config.viewport ? { viewport: config.viewport } : {})
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
teardown: (context) => context.close()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
ctx.defineResource<Page>('page', {
|
|
79
|
+
scope: 'test',
|
|
80
|
+
async setup(res) {
|
|
81
|
+
const context = await res.resource<BrowserContext>('browser.context')
|
|
82
|
+
return context.newPage()
|
|
83
|
+
},
|
|
84
|
+
teardown: (page) => page.close()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
/* ---------------------------------------------------------------- */
|
|
88
|
+
/* Steps */
|
|
89
|
+
/* ---------------------------------------------------------------- */
|
|
90
|
+
|
|
91
|
+
const selectorSchema: InputSchema = {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: { selector: { type: 'string' }, timeout: { type: 'number' } },
|
|
94
|
+
required: ['selector'],
|
|
95
|
+
additionalProperties: false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ctx.defineStepType('browser.open', {
|
|
99
|
+
schema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: { url: { type: 'string' }, waitUntil: { type: 'string' } },
|
|
102
|
+
required: ['url'],
|
|
103
|
+
additionalProperties: false
|
|
104
|
+
},
|
|
105
|
+
async execute(exec, input) {
|
|
106
|
+
const page = await exec.resource<Page>('page')
|
|
107
|
+
const response = await page.goto(String(input.url), {
|
|
108
|
+
...(input.waitUntil ? { waitUntil: input.waitUntil } : {})
|
|
109
|
+
})
|
|
110
|
+
return { url: page.url(), title: await page.title(), status: response?.status() ?? null }
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
ctx.defineStepType('browser.click', {
|
|
115
|
+
schema: selectorSchema,
|
|
116
|
+
async execute(exec, input) {
|
|
117
|
+
const page = await exec.resource<Page>('page')
|
|
118
|
+
await page.click(String(input.selector), timeoutOf(exec.config<PlaywrightConfig>(), input))
|
|
119
|
+
return { selector: input.selector, url: page.url() }
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
ctx.defineStepType('browser.fill', {
|
|
124
|
+
schema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: { selector: { type: 'string' }, value: {}, timeout: { type: 'number' } },
|
|
127
|
+
required: ['selector', 'value'],
|
|
128
|
+
additionalProperties: false
|
|
129
|
+
},
|
|
130
|
+
async execute(exec, input) {
|
|
131
|
+
const page = await exec.resource<Page>('page')
|
|
132
|
+
await page.fill(String(input.selector), String(input.value), timeoutOf(exec.config<PlaywrightConfig>(), input))
|
|
133
|
+
return { selector: input.selector, value: input.value }
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
ctx.defineStepType('browser.press', {
|
|
138
|
+
schema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: { selector: { type: 'string' }, key: { type: 'string' }, timeout: { type: 'number' } },
|
|
141
|
+
required: ['selector', 'key'],
|
|
142
|
+
additionalProperties: false
|
|
143
|
+
},
|
|
144
|
+
async execute(exec, input) {
|
|
145
|
+
const page = await exec.resource<Page>('page')
|
|
146
|
+
await page.press(String(input.selector), String(input.key), timeoutOf(exec.config<PlaywrightConfig>(), input))
|
|
147
|
+
return { selector: input.selector, key: input.key }
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
ctx.defineStepType('browser.wait_for', {
|
|
152
|
+
schema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: { selector: { type: 'string' }, state: { type: 'string' }, timeout: { type: 'number' } },
|
|
155
|
+
required: ['selector'],
|
|
156
|
+
additionalProperties: false
|
|
157
|
+
},
|
|
158
|
+
async execute(exec, input) {
|
|
159
|
+
const page = await exec.resource<Page>('page')
|
|
160
|
+
await page.waitForSelector(String(input.selector), {
|
|
161
|
+
...timeoutOf(exec.config<PlaywrightConfig>(), input),
|
|
162
|
+
...(input.state ? { state: input.state } : {})
|
|
163
|
+
})
|
|
164
|
+
return { selector: input.selector }
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Reads the page into the variable namespace, so `${title.text}` is
|
|
169
|
+
// available to every later step exactly like an HTTP body would be.
|
|
170
|
+
ctx.defineStepType('browser.text', {
|
|
171
|
+
schema: selectorSchema,
|
|
172
|
+
async execute(exec, input) {
|
|
173
|
+
const page = await exec.resource<Page>('page')
|
|
174
|
+
const text = await page.textContent(String(input.selector), timeoutOf(exec.config<PlaywrightConfig>(), input))
|
|
175
|
+
return { selector: input.selector, text: text ?? '' }
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// The artifact half of the gate: bytes go to `attach` and the kernel is
|
|
180
|
+
// responsible for them from there. This plugin never learns where they
|
|
181
|
+
// are written, and no reporter has to be taught about screenshots.
|
|
182
|
+
ctx.defineStepType('browser.screenshot', {
|
|
183
|
+
schema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: { name: { type: 'string' }, selector: { type: 'string' }, fullPage: { type: 'boolean' } },
|
|
186
|
+
additionalProperties: false
|
|
187
|
+
},
|
|
188
|
+
async execute(exec, input) {
|
|
189
|
+
const page = await exec.resource<Page>('page')
|
|
190
|
+
const name = String(input.name ?? 'screenshot.png')
|
|
191
|
+
const bytes = await page.screenshot({ fullPage: input.fullPage === true })
|
|
192
|
+
exec.attach(name.endsWith('.png') ? name : `${name}.png`, bytes, 'image/png')
|
|
193
|
+
return { name, bytes: bytes.byteLength, url: page.url() }
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
/* ---------------------------------------------------------------- */
|
|
198
|
+
/* Assertions */
|
|
199
|
+
/* ---------------------------------------------------------------- */
|
|
200
|
+
|
|
201
|
+
// An assertion may reach for a resource too, so `visible` does not need
|
|
202
|
+
// a step in front of it just to have something to look at.
|
|
203
|
+
ctx.defineAssertion('visible', {
|
|
204
|
+
schema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: { selector: { type: 'string' } },
|
|
207
|
+
required: ['selector'],
|
|
208
|
+
additionalProperties: false
|
|
209
|
+
},
|
|
210
|
+
async evaluate(assert, input) {
|
|
211
|
+
const page = await assert.resource<Page>('page')
|
|
212
|
+
const selector = String(input.selector)
|
|
213
|
+
const visible = await page.isVisible(selector)
|
|
214
|
+
return outcome(visible, `${selector} is not visible`, `${selector} is visible`, true, visible)
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
ctx.defineAssertion('text_contains', {
|
|
219
|
+
schema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: { expected: { type: 'string' } },
|
|
222
|
+
required: ['expected'],
|
|
223
|
+
additionalProperties: false
|
|
224
|
+
},
|
|
225
|
+
evaluate(assert, input) {
|
|
226
|
+
const actual = String(assert.last?.text ?? '')
|
|
227
|
+
const needle = String(input.expected)
|
|
228
|
+
return outcome(
|
|
229
|
+
actual.includes(needle),
|
|
230
|
+
`expected text to contain ${JSON.stringify(needle)}, got ${JSON.stringify(actual.slice(0, 200))}`,
|
|
231
|
+
`text contains ${JSON.stringify(needle)}`,
|
|
232
|
+
needle,
|
|
233
|
+
actual
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
ctx.defineAssertion('title_is', {
|
|
239
|
+
schema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: { expected: { type: 'string' } },
|
|
242
|
+
required: ['expected'],
|
|
243
|
+
additionalProperties: false
|
|
244
|
+
},
|
|
245
|
+
async evaluate(assert, input) {
|
|
246
|
+
const page = await assert.resource<Page>('page')
|
|
247
|
+
const actual = await page.title()
|
|
248
|
+
return outcome(
|
|
249
|
+
actual === input.expected,
|
|
250
|
+
`expected title ${JSON.stringify(input.expected)}, got ${JSON.stringify(actual)}`,
|
|
251
|
+
`title is ${JSON.stringify(actual)}`,
|
|
252
|
+
input.expected,
|
|
253
|
+
actual
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
ctx.defineAssertion('url_contains', {
|
|
259
|
+
schema: {
|
|
260
|
+
type: 'object',
|
|
261
|
+
properties: { expected: { type: 'string' } },
|
|
262
|
+
required: ['expected'],
|
|
263
|
+
additionalProperties: false
|
|
264
|
+
},
|
|
265
|
+
async evaluate(assert, input) {
|
|
266
|
+
const page = await assert.resource<Page>('page')
|
|
267
|
+
const actual = page.url()
|
|
268
|
+
const needle = String(input.expected)
|
|
269
|
+
return outcome(
|
|
270
|
+
actual.includes(needle),
|
|
271
|
+
`expected url to contain ${JSON.stringify(needle)}, got ${JSON.stringify(actual)}`,
|
|
272
|
+
`url is ${actual}`,
|
|
273
|
+
needle,
|
|
274
|
+
actual
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
function timeoutOf(config: PlaywrightConfig, input: Record<string, unknown>): { timeout?: number } {
|
|
282
|
+
const timeout = typeof input.timeout === 'number' ? input.timeout : config.timeoutMs
|
|
283
|
+
return timeout === undefined ? {} : { timeout }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function outcome(
|
|
287
|
+
passed: boolean, whenFailed: string, whenPassed: string, expected?: unknown, actual?: unknown
|
|
288
|
+
): AssertOutcome {
|
|
289
|
+
return { passed, message: passed ? whenPassed : whenFailed, expected, actual }
|
|
290
|
+
}
|