feather-testing-core 0.1.0 → 0.1.2
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 +137 -13
- package/dist/playwright/driver.d.ts.map +1 -1
- package/dist/playwright/driver.js +15 -5
- package/dist/rtl/driver.d.ts.map +1 -1
- package/dist/rtl/driver.js +12 -6
- package/package.json +17 -3
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
|
-
|
|
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
|
-
|
|
11
|
+
### Before / After — Playwright E2E
|
|
8
12
|
|
|
13
|
+
**Before (Vanilla Playwright):**
|
|
9
14
|
```ts
|
|
10
|
-
|
|
11
|
-
.
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.
|
|
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
|
|
@@ -23,6 +78,8 @@ Inspired by [Phoenix Test](https://hexdocs.pm/phoenix_test/PhoenixTest.html) —
|
|
|
23
78
|
npm install feather-testing-core
|
|
24
79
|
```
|
|
25
80
|
|
|
81
|
+
> **Note:** This package is ESM-only (`"type": "module"`). It works with modern bundlers and test runners out of the box. If your project uses CommonJS `require()`, you'll need to update your config to support ESM imports.
|
|
82
|
+
|
|
26
83
|
All test framework dependencies are optional peers — install only what you use:
|
|
27
84
|
|
|
28
85
|
```bash
|
|
@@ -111,15 +168,65 @@ Every method returns `this` for chaining. A single `await` at the start of the c
|
|
|
111
168
|
| `selectOption(label, option)` | Select dropdown option by label |
|
|
112
169
|
| `check(label)` / `uncheck(label)` | Toggle checkbox by label |
|
|
113
170
|
| `choose(label)` | Select radio button by label |
|
|
114
|
-
| `submit()` | Submit the most recently interacted form |
|
|
171
|
+
| `submit()` | Submit the most recently interacted form (see below) |
|
|
172
|
+
|
|
173
|
+
#### How `submit()` finds the submit button
|
|
174
|
+
|
|
175
|
+
`submit()` tracks the `<form>` element from the last `fillIn`, `selectOption`, `check`, `uncheck`, or `choose` call, then uses this strategy:
|
|
176
|
+
|
|
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
|
+
3. **Enter key fallback** — presses Enter on the last form field
|
|
180
|
+
|
|
181
|
+
If no form was previously interacted with, `submit()` throws an error.
|
|
115
182
|
|
|
116
183
|
### Assertions
|
|
117
184
|
|
|
118
185
|
| Method | Description |
|
|
119
186
|
|--------|-------------|
|
|
120
187
|
| `assertText(text)` / `refuteText(text)` | Assert text is visible / not visible |
|
|
121
|
-
| `assertHas(selector, opts?)` / `refuteHas(...)` | Assert element exists (Playwright only) |
|
|
122
|
-
| `assertPath(path, opts?)` / `refutePath(path)` | Assert URL path (Playwright only) |
|
|
188
|
+
| `assertHas(selector, opts?)` / `refuteHas(...)` | Assert element exists (Playwright only, see options below) |
|
|
189
|
+
| `assertPath(path, opts?)` / `refutePath(path)` | Assert URL path (Playwright only, see options below) |
|
|
190
|
+
|
|
191
|
+
#### `assertHas` / `refuteHas` options
|
|
192
|
+
|
|
193
|
+
| Option | Type | Description |
|
|
194
|
+
|--------|------|-------------|
|
|
195
|
+
| `text` | `string` | Filter elements to those containing this text |
|
|
196
|
+
| `count` | `number` | Assert exact number of matching elements |
|
|
197
|
+
| `exact` | `boolean` | When `true`, `text` matches as an exact substring. When `false` (default), matches as a regex |
|
|
198
|
+
| `timeout` | `number` | Custom timeout in milliseconds (overrides Playwright default) |
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// Assert at least one .card element is visible
|
|
202
|
+
await session.assertHas(".card");
|
|
203
|
+
|
|
204
|
+
// Assert a .card containing specific text
|
|
205
|
+
await session.assertHas(".card", { text: "Overdue" });
|
|
206
|
+
|
|
207
|
+
// Assert exact count
|
|
208
|
+
await session.assertHas("li.todo-item", { count: 3 });
|
|
209
|
+
|
|
210
|
+
// Assert with custom timeout
|
|
211
|
+
await session.assertHas(".loaded", { timeout: 10000 });
|
|
212
|
+
|
|
213
|
+
// Refute: assert no matching elements exist
|
|
214
|
+
await session.refuteHas(".spinner");
|
|
215
|
+
await session.refuteHas(".card", { text: "Deleted Item" });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### `assertPath` / `refutePath` options
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// Assert path (ignores query params)
|
|
222
|
+
await session.assertPath("/projects");
|
|
223
|
+
|
|
224
|
+
// Assert path with specific query params
|
|
225
|
+
await session.assertPath("/search", { queryParams: { q: "hello", page: "1" } });
|
|
226
|
+
|
|
227
|
+
// Refute: assert you are NOT on this path
|
|
228
|
+
await session.refutePath("/login");
|
|
229
|
+
```
|
|
123
230
|
|
|
124
231
|
### Scoping
|
|
125
232
|
|
|
@@ -127,11 +234,21 @@ Every method returns `this` for chaining. A single `await` at the start of the c
|
|
|
127
234
|
|--------|-------------|
|
|
128
235
|
| `within(selector, fn)` | Scope actions to a container element |
|
|
129
236
|
|
|
237
|
+
```ts
|
|
238
|
+
// All actions inside the callback are scoped to the matched element
|
|
239
|
+
await session
|
|
240
|
+
.visit("/dashboard")
|
|
241
|
+
.within(".sidebar", (s) =>
|
|
242
|
+
s.clickLink("Settings").assertText("Preferences")
|
|
243
|
+
)
|
|
244
|
+
.assertText("Dashboard"); // back to full-page scope after within()
|
|
245
|
+
```
|
|
246
|
+
|
|
130
247
|
### Debug
|
|
131
248
|
|
|
132
249
|
| Method | Description |
|
|
133
250
|
|--------|-------------|
|
|
134
|
-
| `debug()` |
|
|
251
|
+
| `debug()` | Playwright: saves a full-page screenshot to `debug-{timestamp}.png` in the CWD. RTL: calls `screen.debug()` to log the current DOM to the console. |
|
|
135
252
|
|
|
136
253
|
## How It Works
|
|
137
254
|
|
|
@@ -210,7 +327,7 @@ The RTL adapter runs in JSDOM, which has no real browser. These methods are not
|
|
|
210
327
|
## Exports
|
|
211
328
|
|
|
212
329
|
```ts
|
|
213
|
-
// Core
|
|
330
|
+
// Core (Session class + types)
|
|
214
331
|
import { Session, StepError, type TestDriver } from "feather-testing-core";
|
|
215
332
|
|
|
216
333
|
// Playwright adapter
|
|
@@ -220,6 +337,13 @@ import { test, createSession, expect } from "feather-testing-core/playwright";
|
|
|
220
337
|
import { createSession } from "feather-testing-core/rtl";
|
|
221
338
|
```
|
|
222
339
|
|
|
340
|
+
Both adapter subpaths also re-export `Session` and `StepError`, so you can import everything from a single path:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { test, Session, StepError } from "feather-testing-core/playwright";
|
|
344
|
+
import { createSession, Session, StepError } from "feather-testing-core/rtl";
|
|
345
|
+
```
|
|
346
|
+
|
|
223
347
|
## License
|
|
224
348
|
|
|
225
349
|
MIT
|
|
@@ -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;
|
|
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"}
|
|
@@ -57,11 +57,21 @@ export class PlaywrightDriver {
|
|
|
57
57
|
throw new Error("submit() called but no form was previously interacted with. " +
|
|
58
58
|
"Use fillIn(), selectOption(), check(), uncheck(), or choose() first.");
|
|
59
59
|
}
|
|
60
|
+
// First try: find a button by accessible name containing "submit"
|
|
61
|
+
const byRole = this.lastFormLocator.getByRole("button", {
|
|
62
|
+
name: /submit/i,
|
|
63
|
+
});
|
|
64
|
+
if ((await byRole.count()) > 0) {
|
|
65
|
+
await byRole.first().click();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Second try: find an explicit type="submit" element
|
|
60
69
|
const submitBtn = this.lastFormLocator.locator('button[type="submit"], input[type="submit"]');
|
|
61
70
|
if ((await submitBtn.count()) > 0) {
|
|
62
71
|
await submitBtn.first().click();
|
|
63
72
|
}
|
|
64
73
|
else {
|
|
74
|
+
// Last resort: press Enter on the last form field
|
|
65
75
|
await this.lastFormLocator
|
|
66
76
|
.locator("input, textarea, select")
|
|
67
77
|
.last()
|
|
@@ -87,7 +97,9 @@ export class PlaywrightDriver {
|
|
|
87
97
|
async refuteHas(selector, opts) {
|
|
88
98
|
let locator = this.scope.locator(selector);
|
|
89
99
|
if (opts?.text) {
|
|
90
|
-
locator =
|
|
100
|
+
locator = opts.exact
|
|
101
|
+
? locator.filter({ hasText: opts.text })
|
|
102
|
+
: locator.filter({ hasText: new RegExp(opts.text) });
|
|
91
103
|
}
|
|
92
104
|
await expect(locator).toHaveCount(0, { timeout: opts?.timeout });
|
|
93
105
|
}
|
|
@@ -108,10 +120,8 @@ export class PlaywrightDriver {
|
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
async refutePath(path) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
throw new Error(`Expected path to NOT be '${path}', but it is.`);
|
|
114
|
-
}
|
|
123
|
+
const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
124
|
+
await expect(this.page).not.toHaveURL(new RegExp(`^[^?]*${escaped}(\\?.*)?$`));
|
|
115
125
|
}
|
|
116
126
|
async within(selector) {
|
|
117
127
|
const scopedLocator = this.scope.locator(selector);
|
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":"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;
|
|
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"}
|
package/dist/rtl/driver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { screen, within as rtlWithin } from "@testing-library/react";
|
|
1
|
+
import { 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.
|
|
@@ -41,7 +41,11 @@ export class RTLDriver {
|
|
|
41
41
|
}
|
|
42
42
|
async selectOption(label, option) {
|
|
43
43
|
const select = await this.container.findByLabelText(label);
|
|
44
|
-
|
|
44
|
+
const optionEl = Array.from(select.querySelectorAll("option")).find((o) => o.textContent?.trim() === option);
|
|
45
|
+
if (!optionEl) {
|
|
46
|
+
throw new Error(`selectOption('${label}', '${option}'): no <option> with text '${option}' found.`);
|
|
47
|
+
}
|
|
48
|
+
await this.user.selectOptions(select, optionEl);
|
|
45
49
|
this.lastFormElement = select.closest("form");
|
|
46
50
|
}
|
|
47
51
|
async check(label) {
|
|
@@ -88,10 +92,12 @@ export class RTLDriver {
|
|
|
88
92
|
await this.container.findByText(text);
|
|
89
93
|
}
|
|
90
94
|
async refuteText(text) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
const el = this.container.queryByText(text);
|
|
97
|
+
if (el) {
|
|
98
|
+
throw new Error(`Expected NOT to find text '${text}', but it was present.`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
95
101
|
}
|
|
96
102
|
async assertPath() {
|
|
97
103
|
throw new Error("assertPath() is not available in the RTL adapter (no real URL in JSDOM).");
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feather-testing-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Phoenix Test-inspired fluent testing DSL for Playwright and React Testing Library",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/siraj-samsudeen/feather-testing-core"
|
|
9
|
+
},
|
|
6
10
|
"keywords": [
|
|
7
11
|
"testing",
|
|
8
12
|
"playwright",
|
|
@@ -50,12 +54,22 @@
|
|
|
50
54
|
}
|
|
51
55
|
},
|
|
52
56
|
"scripts": {
|
|
53
|
-
"build": "tsc"
|
|
57
|
+
"build": "tsc",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"test:pw": "playwright test",
|
|
60
|
+
"test:all": "vitest run && playwright test"
|
|
54
61
|
},
|
|
55
62
|
"devDependencies": {
|
|
56
63
|
"@playwright/test": "^1.58.0",
|
|
64
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
57
65
|
"@testing-library/react": "^16.3.0",
|
|
58
66
|
"@testing-library/user-event": "^14.6.0",
|
|
59
|
-
"
|
|
67
|
+
"@types/react": "^19.2.14",
|
|
68
|
+
"@types/react-dom": "^19.2.3",
|
|
69
|
+
"jsdom": "^28.1.0",
|
|
70
|
+
"react": "^19.2.4",
|
|
71
|
+
"react-dom": "^19.2.4",
|
|
72
|
+
"typescript": "~5.9.3",
|
|
73
|
+
"vitest": "^4.0.18"
|
|
60
74
|
}
|
|
61
75
|
}
|