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 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 accessible name** — looks for a `<button>` whose name contains "submit" (case-insensitive)
178
- 2. **By `type="submit"`** — looks for `<button type="submit">` or `<input type="submit">`
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
- export declare class PlaywrightDriver implements TestDriver {
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
- within(selector: string): Promise<TestDriver>;
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":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,OAAO,EAAU,MAAM,kBAAkB,CAAC;AACnE,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACX,MAAM,aAAa,CAAC;AAErB,qBAAa,gBAAiB,YAAW,UAAU;IAI/C,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,KAAK;IAJf,OAAO,CAAC,eAAe,CAAwB;gBAGrC,IAAI,EAAE,IAAI,EACV,KAAK,GAAE,IAAI,GAAG,OAAc;IAGhC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcnD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM1D,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMnC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BvB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBnE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUnE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAM7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAM7B"}
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 { expect } from "@playwright/test";
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 byLabel = this.scope.getByLabel(label);
24
- if ((await byLabel.count()) > 0) {
25
- await byLabel.fill(value);
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: find a button by accessible name containing "submit"
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
- const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
119
- await expect(this.page).toHaveURL(new RegExp(`^[^?]*${escaped}(\\?.*)?$`));
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
- const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
124
- await expect(this.page).not.toHaveURL(new RegExp(`^[^?]*${escaped}(\\?.*)?$`));
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;AAGxC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAEjD;AAED,eAAO,MAAM,IAAI;aAA0B,OAAO;sGAIhD,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,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"}
@@ -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
- within(selector: string): Promise<TestDriver>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/rtl/driver.ts"],"names":[],"mappings":"AACA,OAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEhE;;;GAGG;AACH,qBAAa,SAAU,YAAW,UAAU;IAC1C,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,eAAe,CAAgC;gBAE3C,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,WAAW;IAK/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYnD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc1D,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBvB,SAAS,CACb,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC;IAMV,SAAS,CACb,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC;IAMV,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAW7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
1
+ {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/rtl/driver.ts"],"names":[],"mappings":"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"}
@@ -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
- let input;
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
- const submitBtn = rtlWithin(this.lastFormElement).queryByRole("button", {
75
- name: /submit/i,
76
- }) ??
77
- this.lastFormElement.querySelector('button[type="submit"], input[type="submit"]');
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 root = this.container === screen
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);
@@ -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
@@ -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;AAGxC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
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
- within(selector: string, fn: (scoped: Session) => Session): this;
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
@@ -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,OAAQ,YAAW,WAAW,CAAC,IAAI,CAAC;IAInC,OAAO,CAAC,MAAM;IAH1B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,SAAS,CAAK;gBAEF,MAAM,EAAE,UAAU;IAEtC,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;IAa1B,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;IAMd,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM9B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM9B,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,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,IAAI;IAUhE,KAAK,IAAI,IAAI;CAGd"}
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
- for (const step of steps) {
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
- throw new StepError(step, steps, error);
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
- export interface TestDriver {
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
- within(selector: string): Promise<TestDriver>;
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
@@ -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;IACzB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
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.1.2",
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",