@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 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.
@@ -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"}
@@ -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
+ }