feather-testing-core 0.1.1 → 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
@@ -1,20 +1,75 @@
1
1
  # feather-testing-core
2
2
 
3
+ A readable testing DSL that turns async test boilerplate into fluent, chainable steps.
4
+
3
5
  Part of the [Feather Framework](https://github.com/siraj-samsudeen/feather-framework) ecosystem.
4
6
 
5
- Phoenix Test-inspired fluent testing DSL for Playwright and React Testing Library.
7
+ ## The Core Idea
8
+
9
+ This DSL defines a universal vocabulary — `fillIn`, `clickButton`, `assertText`, and more — that can be backed by **any** test framework. Playwright and React Testing Library are just the first two adapters. You write your tests once in a fluent, chainable style; the adapter handles the framework-specific details.
6
10
 
7
- Write browser tests that read like user stories:
11
+ ### Before / After Playwright E2E
8
12
 
13
+ **Before (Vanilla Playwright):**
9
14
  ```ts
10
- await session
11
- .visit("/projects")
12
- .clickLink("New Project")
13
- .fillIn("Name", "My Project")
14
- .submit()
15
- .assertText("My Project");
15
+ test("sign up", async ({ page }) => {
16
+ await page.goto("/");
17
+ await expect(page.getByText("Hello, Anonymous!")).toBeVisible();
18
+ await page.getByText("Sign up instead").click();
19
+ await page.getByLabel("Email").fill("e2e@example.com");
20
+ await page.getByLabel("Password").fill("password123");
21
+ await page.getByRole("button", { name: "Sign up" }).click();
22
+ await expect(page.getByText("Hello! You are signed in.")).toBeVisible();
23
+ });
24
+ ```
25
+
26
+ **After:**
27
+ ```ts
28
+ test("sign up", async ({ session }) => {
29
+ await session
30
+ .visit("/")
31
+ .assertText("Hello, Anonymous!")
32
+ .click("Sign up instead")
33
+ .fillIn("Email", "e2e@example.com")
34
+ .fillIn("Password", "password123")
35
+ .clickButton("Sign up")
36
+ .assertText("Hello! You are signed in.");
37
+ });
38
+ ```
39
+
40
+ ### Before / After — React Testing Library
41
+
42
+ **Before (Vanilla RTL):**
43
+ ```ts
44
+ test("form submission", async () => {
45
+ render(<App />);
46
+ const user = userEvent.setup();
47
+
48
+ await user.type(screen.getByLabelText("Email"), "test@example.com");
49
+ await user.type(screen.getByLabelText("Password"), "password123");
50
+ await user.click(screen.getByRole("button", { name: "Sign in" }));
51
+ expect(await screen.findByText("Hello! You are signed in.")).toBeInTheDocument();
52
+ });
16
53
  ```
17
54
 
55
+ **After:**
56
+ ```ts
57
+ test("form submission", async () => {
58
+ render(<App />);
59
+ const session = createSession();
60
+
61
+ await session
62
+ .fillIn("Email", "test@example.com")
63
+ .fillIn("Password", "password123")
64
+ .clickButton("Sign in")
65
+ .assertText("Hello! You are signed in.");
66
+ });
67
+ ```
68
+
69
+ ### Same DSL, Any Backend
70
+
71
+ Notice both examples use the **exact same methods** — `fillIn`, `clickButton`, `assertText`. The DSL is framework-agnostic. Playwright and React Testing Library are just the first two adapters. You can implement the `TestDriver` interface for any testing library and get the same fluent syntax.
72
+
18
73
  Inspired by [Phoenix Test](https://hexdocs.pm/phoenix_test/PhoenixTest.html) — Elixir's pipe-chain testing DSL.
19
74
 
20
75
  ## Installation
@@ -114,25 +169,74 @@ Every method returns `this` for chaining. A single `await` at the start of the c
114
169
  | `check(label)` / `uncheck(label)` | Toggle checkbox by label |
115
170
  | `choose(label)` | Select radio button by label |
116
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 |
117
174
 
118
175
  #### How `submit()` finds the submit button
119
176
 
120
177
  `submit()` tracks the `<form>` element from the last `fillIn`, `selectOption`, `check`, `uncheck`, or `choose` call, then uses this strategy:
121
178
 
122
- 1. **By accessible name** — looks for a `<button>` whose name contains "submit" (case-insensitive)
123
- 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)
124
181
  3. **Enter key fallback** — presses Enter on the last form field
125
182
 
126
183
  If no form was previously interacted with, `submit()` throws an error.
127
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
+
128
197
  ### Assertions
129
198
 
130
199
  | Method | Description |
131
200
  |--------|-------------|
132
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 |
133
206
  | `assertHas(selector, opts?)` / `refuteHas(...)` | Assert element exists (Playwright only, see options below) |
134
207
  | `assertPath(path, opts?)` / `refutePath(path)` | Assert URL path (Playwright only, see options below) |
135
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
+
136
240
  #### `assertHas` / `refuteHas` options
137
241
 
138
242
  | Option | Type | Description |
@@ -162,6 +266,8 @@ await session.refuteHas(".card", { text: "Deleted Item" });
162
266
 
163
267
  #### `assertPath` / `refutePath` options
164
268
 
269
+ The path is compared against the URL's parsed `pathname` exactly — `assertPath("/import")` does **not** pass on `/re/import`.
270
+
165
271
  ```ts
166
272
  // Assert path (ignores query params)
167
273
  await session.assertPath("/projects");
@@ -189,6 +295,19 @@ await session
189
295
  .assertText("Dashboard"); // back to full-page scope after within()
190
296
  ```
191
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
+
192
311
  ### Debug
193
312
 
194
313
  | Method | Description |
@@ -261,6 +380,10 @@ Chain:
261
380
  [skipped] assertText('Hello! You are signed in.')
262
381
  ```
263
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
+
264
387
  ## RTL Adapter Limitations
265
388
 
266
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.1",
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",