feather-testing-core 0.1.2 → 0.2.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 +70 -2
- package/dist/playwright/driver.d.ts +18 -2
- package/dist/playwright/driver.d.ts.map +1 -1
- package/dist/playwright/driver.js +80 -23
- package/dist/playwright/index.d.ts +4 -3
- package/dist/playwright/index.d.ts.map +1 -1
- package/dist/rtl/driver.d.ts +19 -2
- package/dist/rtl/driver.d.ts.map +1 -1
- package/dist/rtl/driver.js +94 -17
- package/dist/rtl/index.d.ts +3 -2
- package/dist/rtl/index.d.ts.map +1 -1
- package/dist/session.d.ts +18 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +40 -3
- package/dist/types.d.ts +19 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -169,25 +169,74 @@ Every method returns `this` for chaining. A single `await` at the start of the c
|
|
|
169
169
|
| `check(label)` / `uncheck(label)` | Toggle checkbox by label |
|
|
170
170
|
| `choose(label)` | Select radio button by label |
|
|
171
171
|
| `submit()` | Submit the most recently interacted form (see below) |
|
|
172
|
+
| `upload(label, path)` | Set a file input (found by label) to the file at `path` |
|
|
173
|
+
| `dropFile(selector, path)` | Dispatch a `DataTransfer` drop of the file onto a drop area |
|
|
172
174
|
|
|
173
175
|
#### How `submit()` finds the submit button
|
|
174
176
|
|
|
175
177
|
`submit()` tracks the `<form>` element from the last `fillIn`, `selectOption`, `check`, `uncheck`, or `choose` call, then uses this strategy:
|
|
176
178
|
|
|
177
|
-
1. **By
|
|
178
|
-
2. **By
|
|
179
|
+
1. **By `type="submit"`** — looks for `<button type="submit">` or `<input type="submit">` (the DOM's ground truth)
|
|
180
|
+
2. **By accessible name** — looks for a `<button>` whose name contains "submit" (case-insensitive)
|
|
179
181
|
3. **Enter key fallback** — presses Enter on the last form field
|
|
180
182
|
|
|
181
183
|
If no form was previously interacted with, `submit()` throws an error.
|
|
182
184
|
|
|
185
|
+
#### File uploads
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// Standard file input, found by its label
|
|
189
|
+
await session.upload("Avatar", "fixtures/avatar.png");
|
|
190
|
+
|
|
191
|
+
// Custom drop area (drag-and-drop upload zones)
|
|
192
|
+
await session.dropFile("#dropzone", "fixtures/report.pdf");
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
In Playwright, `dropFile` reads the real file and dispatches a `drop` event with a `DataTransfer`. In RTL (JSDOM has no filesystem), both verbs synthesize an empty `File` named after the path's basename — assert on the file name, not its contents.
|
|
196
|
+
|
|
183
197
|
### Assertions
|
|
184
198
|
|
|
185
199
|
| Method | Description |
|
|
186
200
|
|--------|-------------|
|
|
187
201
|
| `assertText(text)` / `refuteText(text)` | Assert text is visible / not visible |
|
|
202
|
+
| `assertValue(label, value)` | Assert a field (by label or placeholder) has this value |
|
|
203
|
+
| `assertChecked(label)` / `refuteChecked(label)` | Assert a checkbox is checked / not checked |
|
|
204
|
+
| `assertSelected(label, optionLabel)` | Assert the select's currently selected option |
|
|
205
|
+
| `assertOptions(label, [labels])` | Assert a select offers exactly these options, in order |
|
|
188
206
|
| `assertHas(selector, opts?)` / `refuteHas(...)` | Assert element exists (Playwright only, see options below) |
|
|
189
207
|
| `assertPath(path, opts?)` / `refutePath(path)` | Assert URL path (Playwright only, see options below) |
|
|
190
208
|
|
|
209
|
+
#### Form-state assertions
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
await session
|
|
213
|
+
.fillIn("Email", "a@b.com")
|
|
214
|
+
.assertValue("Email", "a@b.com")
|
|
215
|
+
.check("Subscribe")
|
|
216
|
+
.assertChecked("Subscribe")
|
|
217
|
+
.refuteChecked("Receive ads")
|
|
218
|
+
.selectOption("Plan", "Pro")
|
|
219
|
+
.assertSelected("Plan", "Pro")
|
|
220
|
+
.assertOptions("Plan", ["Free", "Pro", "Enterprise"]);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
In Playwright these are backed by `toHaveValue` / `toBeChecked` / `toHaveText`, so they auto-retry. The RTL adapter polls the DOM with `waitFor` for the same retry semantics.
|
|
224
|
+
|
|
225
|
+
#### Pair every refute with a positive assertion
|
|
226
|
+
|
|
227
|
+
`refuteText` / `refuteHas` assert *absence* — and absence also holds when the page failed to render at all. A blank page passes `refuteHas(".delete-button")`. Always pair a refute with a positive assertion on the same region so the test proves the page actually rendered:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// ❌ Passes even if the action bar never rendered
|
|
231
|
+
await session.refuteHas(".action-bar button", { text: "Delete" });
|
|
232
|
+
|
|
233
|
+
// ✅ The positive complement proves the action bar rendered with exactly [Import]
|
|
234
|
+
await session
|
|
235
|
+
.assertHas(".action-bar button", { count: 1 })
|
|
236
|
+
.assertHas(".action-bar button", { text: "Import" })
|
|
237
|
+
.refuteHas(".action-bar button", { text: "Delete" });
|
|
238
|
+
```
|
|
239
|
+
|
|
191
240
|
#### `assertHas` / `refuteHas` options
|
|
192
241
|
|
|
193
242
|
| Option | Type | Description |
|
|
@@ -217,6 +266,8 @@ await session.refuteHas(".card", { text: "Deleted Item" });
|
|
|
217
266
|
|
|
218
267
|
#### `assertPath` / `refutePath` options
|
|
219
268
|
|
|
269
|
+
The path is compared against the URL's parsed `pathname` exactly — `assertPath("/import")` does **not** pass on `/re/import`.
|
|
270
|
+
|
|
220
271
|
```ts
|
|
221
272
|
// Assert path (ignores query params)
|
|
222
273
|
await session.assertPath("/projects");
|
|
@@ -244,6 +295,19 @@ await session
|
|
|
244
295
|
.assertText("Dashboard"); // back to full-page scope after within()
|
|
245
296
|
```
|
|
246
297
|
|
|
298
|
+
### Escape hatch: `step(name, fn)`
|
|
299
|
+
|
|
300
|
+
When you need something the DSL doesn't cover, queue a named custom step instead of abandoning the chain. The callback receives the adapter's context — `{ page, scope }` for Playwright, `{ user, container }` for RTL — and the name shows up in `StepError` output like any built-in step:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
await session
|
|
304
|
+
.visit("/board")
|
|
305
|
+
.step("drag card to Done column", async ({ page }) => {
|
|
306
|
+
await page.getByText("My card").dragTo(page.locator("#done"));
|
|
307
|
+
})
|
|
308
|
+
.assertText("Done (1)");
|
|
309
|
+
```
|
|
310
|
+
|
|
247
311
|
### Debug
|
|
248
312
|
|
|
249
313
|
| Method | Description |
|
|
@@ -316,6 +380,10 @@ Chain:
|
|
|
316
380
|
[skipped] assertText('Hello! You are signed in.')
|
|
317
381
|
```
|
|
318
382
|
|
|
383
|
+
The session keeps a history of executed steps, so when you break a flow into multiple chains (multiple `await`s), the `StepError` still shows the full walk — steps from earlier chains appear as `[ok]` above the failing chain.
|
|
384
|
+
|
|
385
|
+
With the Playwright adapter, each queued step is also wrapped in `test.step()`, so chains appear as named steps in the trace viewer and HTML report.
|
|
386
|
+
|
|
319
387
|
## RTL Adapter Limitations
|
|
320
388
|
|
|
321
389
|
The RTL adapter runs in JSDOM, which has no real browser. These methods are not available and will throw:
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type Page, type Locator } from "@playwright/test";
|
|
2
2
|
import type { AssertHasOptions, AssertPathOptions, TestDriver } from "../types.js";
|
|
3
|
-
|
|
3
|
+
/** Context handed to custom step() callbacks in the Playwright adapter. */
|
|
4
|
+
export interface PlaywrightStepContext {
|
|
5
|
+
page: Page;
|
|
6
|
+
/** Current scope: the page, or the container locator inside within(). */
|
|
7
|
+
scope: Page | Locator;
|
|
8
|
+
}
|
|
9
|
+
export declare class PlaywrightDriver implements TestDriver<PlaywrightStepContext> {
|
|
4
10
|
private page;
|
|
5
11
|
private scope;
|
|
6
12
|
private lastFormLocator;
|
|
@@ -9,19 +15,29 @@ export declare class PlaywrightDriver implements TestDriver {
|
|
|
9
15
|
click(text: string): Promise<void>;
|
|
10
16
|
clickLink(text: string): Promise<void>;
|
|
11
17
|
clickButton(text: string): Promise<void>;
|
|
18
|
+
private fieldByLabelOrPlaceholder;
|
|
12
19
|
fillIn(label: string, value: string): Promise<void>;
|
|
13
20
|
selectOption(label: string, option: string): Promise<void>;
|
|
14
21
|
check(label: string): Promise<void>;
|
|
15
22
|
uncheck(label: string): Promise<void>;
|
|
16
23
|
choose(label: string): Promise<void>;
|
|
17
24
|
submit(): Promise<void>;
|
|
25
|
+
upload(label: string, path: string): Promise<void>;
|
|
26
|
+
dropFile(selector: string, path: string): Promise<void>;
|
|
18
27
|
assertHas(selector: string, opts?: AssertHasOptions): Promise<void>;
|
|
19
28
|
refuteHas(selector: string, opts?: AssertHasOptions): Promise<void>;
|
|
20
29
|
assertText(text: string): Promise<void>;
|
|
21
30
|
refuteText(text: string): Promise<void>;
|
|
31
|
+
assertValue(label: string, value: string): Promise<void>;
|
|
32
|
+
assertChecked(label: string): Promise<void>;
|
|
33
|
+
refuteChecked(label: string): Promise<void>;
|
|
34
|
+
assertSelected(label: string, optionLabel: string): Promise<void>;
|
|
35
|
+
assertOptions(label: string, optionLabels: string[]): Promise<void>;
|
|
22
36
|
assertPath(path: string, opts?: AssertPathOptions): Promise<void>;
|
|
23
37
|
refutePath(path: string): Promise<void>;
|
|
24
|
-
|
|
38
|
+
step(fn: (context: PlaywrightStepContext) => Promise<unknown>): Promise<void>;
|
|
39
|
+
within(selector: string): Promise<TestDriver<PlaywrightStepContext>>;
|
|
25
40
|
debug(): Promise<void>;
|
|
41
|
+
wrapStep(name: string, fn: () => Promise<void>): Promise<void>;
|
|
26
42
|
}
|
|
27
43
|
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/playwright/driver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/playwright/driver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { expect, test } from "@playwright/test";
|
|
2
4
|
export class PlaywrightDriver {
|
|
3
5
|
page;
|
|
4
6
|
scope;
|
|
@@ -19,18 +21,20 @@ export class PlaywrightDriver {
|
|
|
19
21
|
async clickButton(text) {
|
|
20
22
|
await this.scope.getByRole("button", { name: text }).click();
|
|
21
23
|
}
|
|
24
|
+
fieldByLabelOrPlaceholder(label) {
|
|
25
|
+
// .or() lets Playwright auto-wait on whichever appears, so
|
|
26
|
+
// async-rendered labeled fields don't fall through to the
|
|
27
|
+
// placeholder branch. Placeholder matching is exact — substring
|
|
28
|
+
// matching would collide with labels ("Name" vs placeholder
|
|
29
|
+
// "Nickname") and trip strict mode.
|
|
30
|
+
return this.scope
|
|
31
|
+
.getByLabel(label)
|
|
32
|
+
.or(this.scope.getByPlaceholder(label, { exact: true }));
|
|
33
|
+
}
|
|
22
34
|
async fillIn(label, value) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this.lastFormLocator = this.scope.locator("form", { has: byLabel });
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const byPlaceholder = this.scope.getByPlaceholder(label);
|
|
30
|
-
await byPlaceholder.fill(value);
|
|
31
|
-
this.lastFormLocator = this.scope.locator("form", {
|
|
32
|
-
has: byPlaceholder,
|
|
33
|
-
});
|
|
35
|
+
const field = this.fieldByLabelOrPlaceholder(label);
|
|
36
|
+
await field.fill(value);
|
|
37
|
+
this.lastFormLocator = this.scope.locator("form", { has: field });
|
|
34
38
|
}
|
|
35
39
|
async selectOption(label, option) {
|
|
36
40
|
const select = this.scope.getByLabel(label);
|
|
@@ -57,18 +61,18 @@ export class PlaywrightDriver {
|
|
|
57
61
|
throw new Error("submit() called but no form was previously interacted with. " +
|
|
58
62
|
"Use fillIn(), selectOption(), check(), uncheck(), or choose() first.");
|
|
59
63
|
}
|
|
60
|
-
// First try:
|
|
64
|
+
// First try: an explicit type="submit" element — the DOM's ground truth
|
|
65
|
+
const submitBtn = this.lastFormLocator.locator('button[type="submit"], input[type="submit"]');
|
|
66
|
+
if ((await submitBtn.count()) > 0) {
|
|
67
|
+
await submitBtn.first().click();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Second try: a button whose accessible name contains "submit"
|
|
61
71
|
const byRole = this.lastFormLocator.getByRole("button", {
|
|
62
72
|
name: /submit/i,
|
|
63
73
|
});
|
|
64
74
|
if ((await byRole.count()) > 0) {
|
|
65
75
|
await byRole.first().click();
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
// Second try: find an explicit type="submit" element
|
|
69
|
-
const submitBtn = this.lastFormLocator.locator('button[type="submit"], input[type="submit"]');
|
|
70
|
-
if ((await submitBtn.count()) > 0) {
|
|
71
|
-
await submitBtn.first().click();
|
|
72
76
|
}
|
|
73
77
|
else {
|
|
74
78
|
// Last resort: press Enter on the last form field
|
|
@@ -78,6 +82,22 @@ export class PlaywrightDriver {
|
|
|
78
82
|
.press("Enter");
|
|
79
83
|
}
|
|
80
84
|
}
|
|
85
|
+
async upload(label, path) {
|
|
86
|
+
const input = this.scope.getByLabel(label);
|
|
87
|
+
await input.setInputFiles(path);
|
|
88
|
+
this.lastFormLocator = this.scope.locator("form", { has: input });
|
|
89
|
+
}
|
|
90
|
+
async dropFile(selector, path) {
|
|
91
|
+
const content = await readFile(path);
|
|
92
|
+
const name = basename(path);
|
|
93
|
+
const dataTransfer = await this.page.evaluateHandle(([fileName, base64]) => {
|
|
94
|
+
const dt = new DataTransfer();
|
|
95
|
+
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
|
96
|
+
dt.items.add(new File([bytes], fileName));
|
|
97
|
+
return dt;
|
|
98
|
+
}, [name, content.toString("base64")]);
|
|
99
|
+
await this.scope.locator(selector).dispatchEvent("drop", { dataTransfer });
|
|
100
|
+
}
|
|
81
101
|
async assertHas(selector, opts) {
|
|
82
102
|
let locator = this.scope.locator(selector);
|
|
83
103
|
if (opts?.text) {
|
|
@@ -109,19 +129,45 @@ export class PlaywrightDriver {
|
|
|
109
129
|
async refuteText(text) {
|
|
110
130
|
await expect(this.scope.getByText(text)).toHaveCount(0);
|
|
111
131
|
}
|
|
132
|
+
async assertValue(label, value) {
|
|
133
|
+
await expect(this.fieldByLabelOrPlaceholder(label)).toHaveValue(value);
|
|
134
|
+
}
|
|
135
|
+
async assertChecked(label) {
|
|
136
|
+
await expect(this.scope.getByLabel(label)).toBeChecked();
|
|
137
|
+
}
|
|
138
|
+
async refuteChecked(label) {
|
|
139
|
+
await expect(this.scope.getByLabel(label)).not.toBeChecked();
|
|
140
|
+
}
|
|
141
|
+
async assertSelected(label, optionLabel) {
|
|
142
|
+
const select = this.scope.getByLabel(label);
|
|
143
|
+
await expect(select.locator("option:checked")).toHaveText(optionLabel);
|
|
144
|
+
}
|
|
145
|
+
async assertOptions(label, optionLabels) {
|
|
146
|
+
const select = this.scope.getByLabel(label);
|
|
147
|
+
await expect(select.locator("option")).toHaveText(optionLabels);
|
|
148
|
+
}
|
|
112
149
|
async assertPath(path, opts) {
|
|
113
150
|
if (opts?.queryParams) {
|
|
114
151
|
const params = new URLSearchParams(opts.queryParams).toString();
|
|
115
152
|
await expect(this.page).toHaveURL(`${path}?${params}`);
|
|
116
153
|
}
|
|
117
154
|
else {
|
|
118
|
-
|
|
119
|
-
|
|
155
|
+
await expect
|
|
156
|
+
.poll(() => new URL(this.page.url()).pathname, {
|
|
157
|
+
message: `assertPath('${path}')`,
|
|
158
|
+
})
|
|
159
|
+
.toBe(path);
|
|
120
160
|
}
|
|
121
161
|
}
|
|
122
162
|
async refutePath(path) {
|
|
123
|
-
|
|
124
|
-
|
|
163
|
+
await expect
|
|
164
|
+
.poll(() => new URL(this.page.url()).pathname, {
|
|
165
|
+
message: `refutePath('${path}')`,
|
|
166
|
+
})
|
|
167
|
+
.not.toBe(path);
|
|
168
|
+
}
|
|
169
|
+
async step(fn) {
|
|
170
|
+
await fn({ page: this.page, scope: this.scope });
|
|
125
171
|
}
|
|
126
172
|
async within(selector) {
|
|
127
173
|
const scopedLocator = this.scope.locator(selector);
|
|
@@ -134,4 +180,15 @@ export class PlaywrightDriver {
|
|
|
134
180
|
fullPage: true,
|
|
135
181
|
});
|
|
136
182
|
}
|
|
183
|
+
async wrapStep(name, fn) {
|
|
184
|
+
try {
|
|
185
|
+
// Throws when not running inside @playwright/test — fall back to
|
|
186
|
+
// executing the step directly.
|
|
187
|
+
test.info();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return fn();
|
|
191
|
+
}
|
|
192
|
+
return test.step(name, fn);
|
|
193
|
+
}
|
|
137
194
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { type Page } from "@playwright/test";
|
|
2
2
|
import { Session } from "../session.js";
|
|
3
|
+
import { type PlaywrightStepContext } from "./driver.js";
|
|
3
4
|
export { Session } from "../session.js";
|
|
4
5
|
export { StepError } from "../errors.js";
|
|
5
|
-
export { PlaywrightDriver } from "./driver.js";
|
|
6
|
+
export { PlaywrightDriver, type PlaywrightStepContext } from "./driver.js";
|
|
6
7
|
export type { AssertHasOptions, AssertPathOptions, TestDriver, } from "../types.js";
|
|
7
|
-
export declare function createSession(page: Page): Session
|
|
8
|
+
export declare function createSession(page: Page): Session<PlaywrightStepContext>;
|
|
8
9
|
export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
|
|
9
|
-
session: Session
|
|
10
|
+
session: Session<PlaywrightStepContext>;
|
|
10
11
|
}, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions>;
|
|
11
12
|
export { expect } from "@playwright/test";
|
|
12
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAoB,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC3E,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAExE;AAED,eAAO,MAAM,IAAI;aAA0B,OAAO,CAAC,qBAAqB,CAAC;sGAIvE,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/rtl/driver.d.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
import { screen, within as rtlWithin } from "@testing-library/react";
|
|
1
2
|
import { type UserEvent } from "@testing-library/user-event";
|
|
2
3
|
import type { AssertHasOptions, TestDriver } from "../types.js";
|
|
4
|
+
/** Context handed to custom step() callbacks in the RTL adapter. */
|
|
5
|
+
export interface RTLStepContext {
|
|
6
|
+
user: UserEvent;
|
|
7
|
+
/** Current scope: `screen`, or a within()-scoped query container. */
|
|
8
|
+
container: ReturnType<typeof rtlWithin> | typeof screen;
|
|
9
|
+
}
|
|
3
10
|
/**
|
|
4
11
|
* RTL adapter implementing the subset of TestDriver that applies in JSDOM.
|
|
5
12
|
* Navigation methods (visit, assertPath, refutePath) are not supported.
|
|
6
13
|
*/
|
|
7
|
-
export declare class RTLDriver implements TestDriver {
|
|
14
|
+
export declare class RTLDriver implements TestDriver<RTLStepContext> {
|
|
8
15
|
private user;
|
|
9
16
|
private container;
|
|
10
17
|
private lastFormElement;
|
|
11
18
|
constructor(user?: UserEvent, container?: HTMLElement);
|
|
19
|
+
private rootElement;
|
|
20
|
+
private findFieldByLabelOrPlaceholder;
|
|
12
21
|
visit(): Promise<void>;
|
|
13
22
|
click(text: string): Promise<void>;
|
|
14
23
|
clickLink(text: string): Promise<void>;
|
|
@@ -19,13 +28,21 @@ export declare class RTLDriver implements TestDriver {
|
|
|
19
28
|
uncheck(label: string): Promise<void>;
|
|
20
29
|
choose(label: string): Promise<void>;
|
|
21
30
|
submit(): Promise<void>;
|
|
31
|
+
upload(label: string, path: string): Promise<void>;
|
|
32
|
+
dropFile(selector: string, path: string): Promise<void>;
|
|
22
33
|
assertHas(_selector: string, _opts?: AssertHasOptions): Promise<void>;
|
|
23
34
|
refuteHas(_selector: string, _opts?: AssertHasOptions): Promise<void>;
|
|
24
35
|
assertText(text: string): Promise<void>;
|
|
25
36
|
refuteText(text: string): Promise<void>;
|
|
37
|
+
assertValue(label: string, value: string): Promise<void>;
|
|
38
|
+
assertChecked(label: string): Promise<void>;
|
|
39
|
+
refuteChecked(label: string): Promise<void>;
|
|
40
|
+
assertSelected(label: string, optionLabel: string): Promise<void>;
|
|
41
|
+
assertOptions(label: string, optionLabels: string[]): Promise<void>;
|
|
26
42
|
assertPath(): Promise<void>;
|
|
27
43
|
refutePath(): Promise<void>;
|
|
28
|
-
|
|
44
|
+
step(fn: (context: RTLStepContext) => Promise<unknown>): Promise<void>;
|
|
45
|
+
within(selector: string): Promise<TestDriver<RTLStepContext>>;
|
|
29
46
|
debug(): Promise<void>;
|
|
30
47
|
}
|
|
31
48
|
//# sourceMappingURL=driver.d.ts.map
|
package/dist/rtl/driver.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/rtl/driver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/rtl/driver.ts"],"names":[],"mappings":"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"}
|
package/dist/rtl/driver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { screen, waitFor, within as rtlWithin } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, screen, waitFor, within as rtlWithin, } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
/**
|
|
4
4
|
* RTL adapter implementing the subset of TestDriver that applies in JSDOM.
|
|
@@ -12,6 +12,20 @@ export class RTLDriver {
|
|
|
12
12
|
this.user = user ?? userEvent.setup();
|
|
13
13
|
this.container = container ? rtlWithin(container) : screen;
|
|
14
14
|
}
|
|
15
|
+
rootElement() {
|
|
16
|
+
return this.container === screen
|
|
17
|
+
? document.body
|
|
18
|
+
: (this.container
|
|
19
|
+
.container ?? document.body);
|
|
20
|
+
}
|
|
21
|
+
async findFieldByLabelOrPlaceholder(label) {
|
|
22
|
+
try {
|
|
23
|
+
return await this.container.findByLabelText(label);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return await this.container.findByPlaceholderText(label);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
async visit() {
|
|
16
30
|
throw new Error("visit() is not available in the RTL adapter. Render the desired component directly.");
|
|
17
31
|
}
|
|
@@ -28,13 +42,7 @@ export class RTLDriver {
|
|
|
28
42
|
await this.user.click(button);
|
|
29
43
|
}
|
|
30
44
|
async fillIn(label, value) {
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
input = await this.container.findByLabelText(label);
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
input = await this.container.findByPlaceholderText(label);
|
|
37
|
-
}
|
|
45
|
+
const input = await this.findFieldByLabelOrPlaceholder(label);
|
|
38
46
|
await this.user.clear(input);
|
|
39
47
|
await this.user.type(input, value);
|
|
40
48
|
this.lastFormElement = input.closest("form");
|
|
@@ -71,10 +79,12 @@ export class RTLDriver {
|
|
|
71
79
|
if (!this.lastFormElement) {
|
|
72
80
|
throw new Error("submit() called but no form was previously interacted with.");
|
|
73
81
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.lastFormElement.
|
|
82
|
+
// Prefer an explicit type="submit" element (the DOM's ground truth),
|
|
83
|
+
// then a button whose accessible name contains "submit".
|
|
84
|
+
const submitBtn = this.lastFormElement.querySelector('button[type="submit"], input[type="submit"]') ??
|
|
85
|
+
rtlWithin(this.lastFormElement).queryByRole("button", {
|
|
86
|
+
name: /submit/i,
|
|
87
|
+
});
|
|
78
88
|
if (submitBtn) {
|
|
79
89
|
await this.user.click(submitBtn);
|
|
80
90
|
}
|
|
@@ -82,6 +92,29 @@ export class RTLDriver {
|
|
|
82
92
|
this.lastFormElement.requestSubmit();
|
|
83
93
|
}
|
|
84
94
|
}
|
|
95
|
+
async upload(label, path) {
|
|
96
|
+
const input = await this.container.findByLabelText(label);
|
|
97
|
+
// JSDOM has no filesystem access; synthesize a File from the basename.
|
|
98
|
+
const name = path.split(/[\\/]/).pop() ?? path;
|
|
99
|
+
const file = new File([""], name);
|
|
100
|
+
await this.user.upload(input, file);
|
|
101
|
+
this.lastFormElement = input.closest("form");
|
|
102
|
+
}
|
|
103
|
+
async dropFile(selector, path) {
|
|
104
|
+
const target = this.rootElement().querySelector(selector);
|
|
105
|
+
if (!target) {
|
|
106
|
+
throw new Error(`dropFile('${selector}'): element not found`);
|
|
107
|
+
}
|
|
108
|
+
const name = path.split(/[\\/]/).pop() ?? path;
|
|
109
|
+
const file = new File([""], name);
|
|
110
|
+
fireEvent.drop(target, {
|
|
111
|
+
dataTransfer: {
|
|
112
|
+
files: [file],
|
|
113
|
+
items: [{ kind: "file", type: file.type, getAsFile: () => file }],
|
|
114
|
+
types: ["Files"],
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
85
118
|
async assertHas(_selector, _opts) {
|
|
86
119
|
throw new Error("assertHas() with CSS selectors is not recommended in RTL. Use assertText() instead.");
|
|
87
120
|
}
|
|
@@ -99,18 +132,62 @@ export class RTLDriver {
|
|
|
99
132
|
}
|
|
100
133
|
});
|
|
101
134
|
}
|
|
135
|
+
async assertValue(label, value) {
|
|
136
|
+
const field = await this.findFieldByLabelOrPlaceholder(label);
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
const actual = field.value;
|
|
139
|
+
if (actual !== value) {
|
|
140
|
+
throw new Error(`assertValue('${label}', '${value}'): expected value '${value}', but found '${actual}'.`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async assertChecked(label) {
|
|
145
|
+
const checkbox = await this.container.findByLabelText(label);
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
if (!checkbox.checked) {
|
|
148
|
+
throw new Error(`assertChecked('${label}'): expected checkbox to be checked, but it was not.`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async refuteChecked(label) {
|
|
153
|
+
const checkbox = await this.container.findByLabelText(label);
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
if (checkbox.checked) {
|
|
156
|
+
throw new Error(`refuteChecked('${label}'): expected checkbox NOT to be checked, but it was.`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async assertSelected(label, optionLabel) {
|
|
161
|
+
const select = (await this.container.findByLabelText(label));
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
const selected = select.selectedOptions[0]?.textContent?.trim();
|
|
164
|
+
if (selected !== optionLabel) {
|
|
165
|
+
throw new Error(`assertSelected('${label}', '${optionLabel}'): expected selected option '${optionLabel}', but found '${selected ?? "(none)"}'.`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async assertOptions(label, optionLabels) {
|
|
170
|
+
const select = (await this.container.findByLabelText(label));
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
const actual = Array.from(select.querySelectorAll("option")).map((o) => o.textContent?.trim() ?? "");
|
|
173
|
+
const matches = actual.length === optionLabels.length &&
|
|
174
|
+
actual.every((text, i) => text === optionLabels[i]);
|
|
175
|
+
if (!matches) {
|
|
176
|
+
throw new Error(`assertOptions('${label}'): expected options [${optionLabels.join(", ")}], but found [${actual.join(", ")}].`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
102
180
|
async assertPath() {
|
|
103
181
|
throw new Error("assertPath() is not available in the RTL adapter (no real URL in JSDOM).");
|
|
104
182
|
}
|
|
105
183
|
async refutePath() {
|
|
106
184
|
throw new Error("refutePath() is not available in the RTL adapter (no real URL in JSDOM).");
|
|
107
185
|
}
|
|
186
|
+
async step(fn) {
|
|
187
|
+
await fn({ user: this.user, container: this.container });
|
|
188
|
+
}
|
|
108
189
|
async within(selector) {
|
|
109
|
-
const
|
|
110
|
-
? document.body
|
|
111
|
-
: (this.container
|
|
112
|
-
.container ?? document.body);
|
|
113
|
-
const element = root.querySelector(selector);
|
|
190
|
+
const element = this.rootElement().querySelector(selector);
|
|
114
191
|
if (!element)
|
|
115
192
|
throw new Error(`within('${selector}'): element not found`);
|
|
116
193
|
return new RTLDriver(this.user, element);
|
package/dist/rtl/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Session } from "../session.js";
|
|
2
|
+
import { type RTLStepContext } from "./driver.js";
|
|
2
3
|
export { Session } from "../session.js";
|
|
3
4
|
export { StepError } from "../errors.js";
|
|
4
|
-
export { RTLDriver } from "./driver.js";
|
|
5
|
-
export declare function createSession(): Session
|
|
5
|
+
export { RTLDriver, type RTLStepContext } from "./driver.js";
|
|
6
|
+
export declare function createSession(): Session<RTLStepContext>;
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/rtl/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rtl/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rtl/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAa,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7D,wBAAgB,aAAa,IAAI,OAAO,CAAC,cAAc,CAAC,CAEvD"}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { AssertHasOptions, AssertPathOptions, TestDriver } from "./types.js";
|
|
2
|
-
export declare class Session implements PromiseLike<void> {
|
|
2
|
+
export declare class Session<TContext = unknown> implements PromiseLike<void> {
|
|
3
3
|
private driver;
|
|
4
4
|
private steps;
|
|
5
|
+
private executedSteps;
|
|
5
6
|
private stepIndex;
|
|
6
|
-
constructor(driver: TestDriver);
|
|
7
|
+
constructor(driver: TestDriver<TContext>);
|
|
7
8
|
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
8
9
|
private executeSteps;
|
|
9
10
|
private enqueue;
|
|
@@ -17,13 +18,27 @@ export declare class Session implements PromiseLike<void> {
|
|
|
17
18
|
uncheck(label: string): this;
|
|
18
19
|
choose(label: string): this;
|
|
19
20
|
submit(): this;
|
|
21
|
+
upload(label: string, path: string): this;
|
|
22
|
+
dropFile(selector: string, path: string): this;
|
|
20
23
|
assertText(text: string): this;
|
|
21
24
|
refuteText(text: string): this;
|
|
25
|
+
assertValue(label: string, value: string): this;
|
|
26
|
+
assertChecked(label: string): this;
|
|
27
|
+
refuteChecked(label: string): this;
|
|
28
|
+
assertSelected(label: string, optionLabel: string): this;
|
|
29
|
+
assertOptions(label: string, optionLabels: string[]): this;
|
|
22
30
|
assertHas(selector: string, opts?: AssertHasOptions): this;
|
|
23
31
|
refuteHas(selector: string, opts?: AssertHasOptions): this;
|
|
24
32
|
assertPath(path: string, opts?: AssertPathOptions): this;
|
|
25
33
|
refutePath(path: string): this;
|
|
26
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Queue a named custom step. `fn` receives the adapter's context
|
|
36
|
+
* ({ page, scope } for Playwright, { user, container } for RTL), so a
|
|
37
|
+
* missing verb never forces abandoning the chain. The name shows up in
|
|
38
|
+
* StepError output like any built-in step.
|
|
39
|
+
*/
|
|
40
|
+
step(name: string, fn: (context: TContext) => Promise<unknown>): this;
|
|
41
|
+
within(selector: string, fn: (scoped: Session<TContext>) => Session<TContext>): this;
|
|
27
42
|
debug(): this;
|
|
28
43
|
}
|
|
29
44
|
//# sourceMappingURL=session.d.ts.map
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,UAAU,EACX,MAAM,YAAY,CAAC;AAGpB,qBAAa,
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,UAAU,EACX,MAAM,YAAY,CAAC;AAGpB,qBAAa,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAE,YAAW,WAAW,CAAC,IAAI,CAAC;IAKvD,OAAO,CAAC,MAAM;IAJ1B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,SAAS,CAAK;gBAEF,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC;IAEhD,IAAI,CAAC,QAAQ,GAAG,IAAI,EAAE,QAAQ,GAAG,KAAK,EACpC,WAAW,CAAC,EACR,CAAC,CAAC,KAAK,EAAE,IAAI,KAAK,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,GACnD,IAAI,EACR,UAAU,CAAC,EACP,CAAC,CAAC,MAAM,EAAE,OAAO,KAAK,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,GACvD,IAAI,GACP,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;YAIjB,YAAY;IAuB1B,OAAO,CAAC,OAAO;IAOf,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMzB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIzB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM7B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM/B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAM1C,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAMjD,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM3B,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAMzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQ9C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM9B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM9B,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAM/C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAMlC,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAMlC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAOxD,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI;IAO1D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAO1D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAO1D,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,IAAI;IAMxD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQ9B;;;;;OAKG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI;IAMrE,MAAM,CACJ,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,GACnD,IAAI;IAUP,KAAK,IAAI,IAAI;CAGd"}
|
package/dist/session.js
CHANGED
|
@@ -2,6 +2,7 @@ import { StepError } from "./errors.js";
|
|
|
2
2
|
export class Session {
|
|
3
3
|
driver;
|
|
4
4
|
steps = [];
|
|
5
|
+
executedSteps = [];
|
|
5
6
|
stepIndex = 0;
|
|
6
7
|
constructor(driver) {
|
|
7
8
|
this.driver = driver;
|
|
@@ -12,12 +13,16 @@ export class Session {
|
|
|
12
13
|
async executeSteps() {
|
|
13
14
|
const steps = [...this.steps];
|
|
14
15
|
this.steps = [];
|
|
15
|
-
|
|
16
|
+
const wrap = this.driver.wrapStep?.bind(this.driver) ?? ((_name, fn) => fn());
|
|
17
|
+
for (const [i, step] of steps.entries()) {
|
|
16
18
|
try {
|
|
17
|
-
await step.action
|
|
19
|
+
await wrap(step.name, step.action);
|
|
20
|
+
this.executedSteps.push(step);
|
|
18
21
|
}
|
|
19
22
|
catch (error) {
|
|
20
|
-
|
|
23
|
+
// Include steps executed in earlier chains so the full walk is
|
|
24
|
+
// visible even when the user broke the chain across multiple awaits.
|
|
25
|
+
throw new StepError(step, [...this.executedSteps, ...steps.slice(i)], error);
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
}
|
|
@@ -57,6 +62,12 @@ export class Session {
|
|
|
57
62
|
submit() {
|
|
58
63
|
return this.enqueue("submit()", () => this.driver.submit());
|
|
59
64
|
}
|
|
65
|
+
upload(label, path) {
|
|
66
|
+
return this.enqueue(`upload('${label}', '${path}')`, () => this.driver.upload(label, path));
|
|
67
|
+
}
|
|
68
|
+
dropFile(selector, path) {
|
|
69
|
+
return this.enqueue(`dropFile('${selector}', '${path}')`, () => this.driver.dropFile(selector, path));
|
|
70
|
+
}
|
|
60
71
|
// --- Assertions ---
|
|
61
72
|
assertText(text) {
|
|
62
73
|
return this.enqueue(`assertText('${text}')`, () => this.driver.assertText(text));
|
|
@@ -64,6 +75,22 @@ export class Session {
|
|
|
64
75
|
refuteText(text) {
|
|
65
76
|
return this.enqueue(`refuteText('${text}')`, () => this.driver.refuteText(text));
|
|
66
77
|
}
|
|
78
|
+
assertValue(label, value) {
|
|
79
|
+
return this.enqueue(`assertValue('${label}', '${value}')`, () => this.driver.assertValue(label, value));
|
|
80
|
+
}
|
|
81
|
+
assertChecked(label) {
|
|
82
|
+
return this.enqueue(`assertChecked('${label}')`, () => this.driver.assertChecked(label));
|
|
83
|
+
}
|
|
84
|
+
refuteChecked(label) {
|
|
85
|
+
return this.enqueue(`refuteChecked('${label}')`, () => this.driver.refuteChecked(label));
|
|
86
|
+
}
|
|
87
|
+
assertSelected(label, optionLabel) {
|
|
88
|
+
return this.enqueue(`assertSelected('${label}', '${optionLabel}')`, () => this.driver.assertSelected(label, optionLabel));
|
|
89
|
+
}
|
|
90
|
+
assertOptions(label, optionLabels) {
|
|
91
|
+
const list = optionLabels.map((l) => `'${l}'`).join(", ");
|
|
92
|
+
return this.enqueue(`assertOptions('${label}', [${list}])`, () => this.driver.assertOptions(label, optionLabels));
|
|
93
|
+
}
|
|
67
94
|
assertHas(selector, opts) {
|
|
68
95
|
const desc = opts?.text
|
|
69
96
|
? `assertHas('${selector}', text: '${opts.text}')`
|
|
@@ -82,6 +109,16 @@ export class Session {
|
|
|
82
109
|
refutePath(path) {
|
|
83
110
|
return this.enqueue(`refutePath('${path}')`, () => this.driver.refutePath(path));
|
|
84
111
|
}
|
|
112
|
+
// --- Escape hatch ---
|
|
113
|
+
/**
|
|
114
|
+
* Queue a named custom step. `fn` receives the adapter's context
|
|
115
|
+
* ({ page, scope } for Playwright, { user, container } for RTL), so a
|
|
116
|
+
* missing verb never forces abandoning the chain. The name shows up in
|
|
117
|
+
* StepError output like any built-in step.
|
|
118
|
+
*/
|
|
119
|
+
step(name, fn) {
|
|
120
|
+
return this.enqueue(`step('${name}')`, () => this.driver.step(fn));
|
|
121
|
+
}
|
|
85
122
|
// --- Scoping ---
|
|
86
123
|
within(selector, fn) {
|
|
87
124
|
return this.enqueue(`within('${selector}')`, async () => {
|
package/dist/types.d.ts
CHANGED
|
@@ -12,7 +12,11 @@ export interface QueuedStep {
|
|
|
12
12
|
action: () => Promise<void>;
|
|
13
13
|
index: number;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* TContext is the adapter-specific context handed to custom step() callbacks
|
|
17
|
+
* (e.g. { page, scope } for Playwright, { user, container } for RTL).
|
|
18
|
+
*/
|
|
19
|
+
export interface TestDriver<TContext = unknown> {
|
|
16
20
|
visit(path: string): Promise<void>;
|
|
17
21
|
click(text: string): Promise<void>;
|
|
18
22
|
clickLink(text: string): Promise<void>;
|
|
@@ -23,13 +27,26 @@ export interface TestDriver {
|
|
|
23
27
|
uncheck(label: string): Promise<void>;
|
|
24
28
|
choose(label: string): Promise<void>;
|
|
25
29
|
submit(): Promise<void>;
|
|
30
|
+
upload(label: string, path: string): Promise<void>;
|
|
31
|
+
dropFile(selector: string, path: string): Promise<void>;
|
|
26
32
|
assertHas(selector: string, opts?: AssertHasOptions): Promise<void>;
|
|
27
33
|
refuteHas(selector: string, opts?: AssertHasOptions): Promise<void>;
|
|
28
34
|
assertText(text: string): Promise<void>;
|
|
29
35
|
refuteText(text: string): Promise<void>;
|
|
36
|
+
assertValue(label: string, value: string): Promise<void>;
|
|
37
|
+
assertChecked(label: string): Promise<void>;
|
|
38
|
+
refuteChecked(label: string): Promise<void>;
|
|
39
|
+
assertSelected(label: string, optionLabel: string): Promise<void>;
|
|
40
|
+
assertOptions(label: string, optionLabels: string[]): Promise<void>;
|
|
30
41
|
assertPath(path: string, opts?: AssertPathOptions): Promise<void>;
|
|
31
42
|
refutePath(path: string): Promise<void>;
|
|
32
|
-
|
|
43
|
+
step(fn: (context: TContext) => Promise<unknown>): Promise<void>;
|
|
44
|
+
within(selector: string): Promise<TestDriver<TContext>>;
|
|
33
45
|
debug(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Optional hook: wrap a queued step's execution (e.g. in Playwright's
|
|
48
|
+
* test.step()) so chains appear in trace viewers and reporters.
|
|
49
|
+
*/
|
|
50
|
+
wrapStep?(name: string, fn: () => Promise<void>): Promise<void>;
|
|
34
51
|
}
|
|
35
52
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feather-testing-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Phoenix Test-inspired fluent testing DSL for Playwright and React Testing Library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"@testing-library/jest-dom": "^6.9.1",
|
|
65
65
|
"@testing-library/react": "^16.3.0",
|
|
66
66
|
"@testing-library/user-event": "^14.6.0",
|
|
67
|
+
"@types/node": "^26.1.2",
|
|
67
68
|
"@types/react": "^19.2.14",
|
|
68
69
|
"@types/react-dom": "^19.2.3",
|
|
69
70
|
"jsdom": "^28.1.0",
|