feather-testing-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # feather-testing-core
2
+
3
+ Part of the [Feather Framework](https://github.com/siraj-samsudeen/feather-framework) ecosystem.
4
+
5
+ Phoenix Test-inspired fluent testing DSL for Playwright and React Testing Library.
6
+
7
+ Write browser tests that read like user stories:
8
+
9
+ ```ts
10
+ await session
11
+ .visit("/projects")
12
+ .clickLink("New Project")
13
+ .fillIn("Name", "My Project")
14
+ .submit()
15
+ .assertText("My Project");
16
+ ```
17
+
18
+ Inspired by [Phoenix Test](https://hexdocs.pm/phoenix_test/PhoenixTest.html) — Elixir's pipe-chain testing DSL.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install feather-testing-core
24
+ ```
25
+
26
+ All test framework dependencies are optional peers — install only what you use:
27
+
28
+ ```bash
29
+ # For Playwright E2E tests
30
+ npm install @playwright/test
31
+
32
+ # For React Testing Library integration tests
33
+ npm install @testing-library/react @testing-library/user-event
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### Playwright E2E
39
+
40
+ ```ts
41
+ // e2e/fixtures.ts
42
+ import { test as featherTest } from "feather-testing-core/playwright";
43
+ export const test = featherTest;
44
+ export { expect } from "@playwright/test";
45
+ ```
46
+
47
+ ```ts
48
+ // e2e/auth.spec.ts
49
+ import { test } from "./fixtures";
50
+
51
+ test("full auth lifecycle", async ({ session }) => {
52
+ // Sign up
53
+ await session
54
+ .visit("/")
55
+ .assertText("Hello, Anonymous!")
56
+ .click("Sign up instead")
57
+ .fillIn("Email", "e2e@example.com")
58
+ .fillIn("Password", "password123")
59
+ .clickButton("Sign up")
60
+ .assertText("Hello! You are signed in.");
61
+
62
+ // Sign out
63
+ await session
64
+ .clickButton("Sign out")
65
+ .assertText("Hello, Anonymous!");
66
+
67
+ // Sign in
68
+ await session
69
+ .fillIn("Email", "e2e@example.com")
70
+ .fillIn("Password", "password123")
71
+ .clickButton("Sign in")
72
+ .assertText("Hello! You are signed in.");
73
+ });
74
+ ```
75
+
76
+ ### React Testing Library
77
+
78
+ ```ts
79
+ import { createSession } from "feather-testing-core/rtl";
80
+
81
+ test("form submission", async () => {
82
+ render(<App />);
83
+ const session = createSession();
84
+
85
+ await session
86
+ .fillIn("Email", "test@example.com")
87
+ .fillIn("Password", "password123")
88
+ .clickButton("Sign in")
89
+ .assertText("Hello! You are signed in.");
90
+ });
91
+ ```
92
+
93
+ ## API
94
+
95
+ Every method returns `this` for chaining. A single `await` at the start of the chain executes all steps sequentially.
96
+
97
+ ### Navigation
98
+
99
+ | Method | Description |
100
+ |--------|-------------|
101
+ | `visit(path)` | Navigate to URL (Playwright only) |
102
+
103
+ ### Interactions
104
+
105
+ | Method | Description |
106
+ |--------|-------------|
107
+ | `click(text)` | Find any element by text and click it |
108
+ | `clickLink(text)` | Click `<a>` by accessible name |
109
+ | `clickButton(text)` | Click `<button>` by accessible name |
110
+ | `fillIn(label, value)` | Fill input by label or placeholder |
111
+ | `selectOption(label, option)` | Select dropdown option by label |
112
+ | `check(label)` / `uncheck(label)` | Toggle checkbox by label |
113
+ | `choose(label)` | Select radio button by label |
114
+ | `submit()` | Submit the most recently interacted form |
115
+
116
+ ### Assertions
117
+
118
+ | Method | Description |
119
+ |--------|-------------|
120
+ | `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) |
123
+
124
+ ### Scoping
125
+
126
+ | Method | Description |
127
+ |--------|-------------|
128
+ | `within(selector, fn)` | Scope actions to a container element |
129
+
130
+ ### Debug
131
+
132
+ | Method | Description |
133
+ |--------|-------------|
134
+ | `debug()` | Screenshot (Playwright) or log DOM (RTL) |
135
+
136
+ ## How It Works
137
+
138
+ The `Session` class uses a **thenable action-queue pattern**. Each method pushes an async operation onto an internal queue and returns `this`. The class implements `PromiseLike<void>`, so `await` triggers execution of the entire queue.
139
+
140
+ ```
141
+ session.visit("/").fillIn("Name", "x").clickButton("Go")
142
+ ↓ ↓ ↓
143
+ [push thunk] [push thunk] [push thunk]
144
+
145
+ await triggers
146
+ sequential execution
147
+ ```
148
+
149
+ This means you write one `await` per chain, not one per line.
150
+
151
+ ### Breaking chains
152
+
153
+ If you need conditional logic mid-flow, break into multiple chains:
154
+
155
+ ```ts
156
+ await session.visit("/").fillIn("Email", email);
157
+
158
+ if (isNewUser) {
159
+ await session.click("Sign up instead").clickButton("Sign up");
160
+ } else {
161
+ await session.clickButton("Sign in");
162
+ }
163
+ ```
164
+
165
+ ### Composable helpers
166
+
167
+ Functions that take and return a Session work as reusable steps:
168
+
169
+ ```ts
170
+ function signIn(session: Session, email: string, password: string): Session {
171
+ return session
172
+ .fillIn("Email", email)
173
+ .fillIn("Password", password)
174
+ .clickButton("Sign in");
175
+ }
176
+
177
+ test("authenticated flow", async ({ session }) => {
178
+ await signIn(session.visit("/"), "test@example.com", "pass123")
179
+ .assertText("Welcome!");
180
+ });
181
+ ```
182
+
183
+ ## Error Messages
184
+
185
+ When a step fails, `StepError` shows the full chain with status markers:
186
+
187
+ ```
188
+ feather-testing-core: Step 4 of 6 failed
189
+
190
+ Failed at: clickButton('Sign up')
191
+ Cause: locator.click: getByRole('button', { name: 'Sign up' }) resolved to 0 elements
192
+
193
+ Chain:
194
+ [ok] visit('/')
195
+ [ok] assertText('Hello, Anonymous!')
196
+ [ok] fillIn('Email', 'e2e@example.com')
197
+ >>> [FAILED] clickButton('Sign up')
198
+ [skipped] fillIn('Password', 'password123')
199
+ [skipped] assertText('Hello! You are signed in.')
200
+ ```
201
+
202
+ ## RTL Adapter Limitations
203
+
204
+ The RTL adapter runs in JSDOM, which has no real browser. These methods are not available and will throw:
205
+
206
+ - `visit()` — render the component directly instead
207
+ - `assertPath()` / `refutePath()` — no URL in JSDOM
208
+ - `assertHas()` / `refuteHas()` — RTL discourages CSS selectors; use `assertText()` instead
209
+
210
+ ## Exports
211
+
212
+ ```ts
213
+ // Core types (for building custom drivers)
214
+ import { Session, StepError, type TestDriver } from "feather-testing-core";
215
+
216
+ // Playwright adapter
217
+ import { test, createSession, expect } from "feather-testing-core/playwright";
218
+
219
+ // RTL adapter
220
+ import { createSession } from "feather-testing-core/rtl";
221
+ ```
222
+
223
+ ## License
224
+
225
+ MIT
@@ -0,0 +1,5 @@
1
+ import type { QueuedStep } from "./types.js";
2
+ export declare class StepError extends Error {
3
+ constructor(failedStep: QueuedStep, allSteps: QueuedStep[], cause: unknown);
4
+ }
5
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,qBAAa,SAAU,SAAQ,KAAK;gBAEhC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,UAAU,EAAE,EACtB,KAAK,EAAE,OAAO;CA4BjB"}
package/dist/errors.js ADDED
@@ -0,0 +1,21 @@
1
+ export class StepError extends Error {
2
+ constructor(failedStep, allSteps, cause) {
3
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
4
+ const stepList = allSteps
5
+ .map((step) => {
6
+ const prefix = step === failedStep ? ">>> " : " ";
7
+ const status = step.index < failedStep.index
8
+ ? "[ok]"
9
+ : step.index === failedStep.index
10
+ ? "[FAILED]"
11
+ : "[skipped]";
12
+ return `${prefix}${status} ${step.name}`;
13
+ })
14
+ .join("\n");
15
+ super(`feather-testing-core: Step ${failedStep.index + 1} of ${allSteps.length} failed\n\n` +
16
+ `Failed at: ${failedStep.name}\n` +
17
+ `Cause: ${causeMessage}\n\n` +
18
+ `Chain:\n${stepList}\n`, { cause });
19
+ this.name = "StepError";
20
+ }
21
+ }
@@ -0,0 +1,4 @@
1
+ export { Session } from "./session.js";
2
+ export { StepError } from "./errors.js";
3
+ export type { AssertHasOptions, AssertPathOptions, QueuedStep, TestDriver, } from "./types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Session } from "./session.js";
2
+ export { StepError } from "./errors.js";
@@ -0,0 +1,27 @@
1
+ import { type Page, type Locator } from "@playwright/test";
2
+ import type { AssertHasOptions, AssertPathOptions, TestDriver } from "../types.js";
3
+ export declare class PlaywrightDriver implements TestDriver {
4
+ private page;
5
+ private scope;
6
+ private lastFormLocator;
7
+ constructor(page: Page, scope?: Page | Locator);
8
+ visit(path: string): Promise<void>;
9
+ click(text: string): Promise<void>;
10
+ clickLink(text: string): Promise<void>;
11
+ clickButton(text: string): Promise<void>;
12
+ fillIn(label: string, value: string): Promise<void>;
13
+ selectOption(label: string, option: string): Promise<void>;
14
+ check(label: string): Promise<void>;
15
+ uncheck(label: string): Promise<void>;
16
+ choose(label: string): Promise<void>;
17
+ submit(): Promise<void>;
18
+ assertHas(selector: string, opts?: AssertHasOptions): Promise<void>;
19
+ refuteHas(selector: string, opts?: AssertHasOptions): Promise<void>;
20
+ assertText(text: string): Promise<void>;
21
+ refuteText(text: string): Promise<void>;
22
+ assertPath(path: string, opts?: AssertPathOptions): Promise<void>;
23
+ refutePath(path: string): Promise<void>;
24
+ within(selector: string): Promise<TestDriver>;
25
+ debug(): Promise<void>;
26
+ }
27
+ //# sourceMappingURL=driver.d.ts.map
@@ -0,0 +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;IAoBvB,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;IAQnE,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"}
@@ -0,0 +1,127 @@
1
+ import { expect } from "@playwright/test";
2
+ export class PlaywrightDriver {
3
+ page;
4
+ scope;
5
+ lastFormLocator = null;
6
+ constructor(page, scope = page) {
7
+ this.page = page;
8
+ this.scope = scope;
9
+ }
10
+ async visit(path) {
11
+ await this.page.goto(path);
12
+ }
13
+ async click(text) {
14
+ await this.scope.getByText(text).click();
15
+ }
16
+ async clickLink(text) {
17
+ await this.scope.getByRole("link", { name: text }).click();
18
+ }
19
+ async clickButton(text) {
20
+ await this.scope.getByRole("button", { name: text }).click();
21
+ }
22
+ 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
+ });
34
+ }
35
+ async selectOption(label, option) {
36
+ const select = this.scope.getByLabel(label);
37
+ await select.selectOption({ label: option });
38
+ this.lastFormLocator = this.scope.locator("form", { has: select });
39
+ }
40
+ async check(label) {
41
+ const checkbox = this.scope.getByLabel(label);
42
+ await checkbox.check();
43
+ this.lastFormLocator = this.scope.locator("form", { has: checkbox });
44
+ }
45
+ async uncheck(label) {
46
+ const checkbox = this.scope.getByLabel(label);
47
+ await checkbox.uncheck();
48
+ this.lastFormLocator = this.scope.locator("form", { has: checkbox });
49
+ }
50
+ async choose(label) {
51
+ const radio = this.scope.getByRole("radio", { name: label });
52
+ await radio.check();
53
+ this.lastFormLocator = this.scope.locator("form", { has: radio });
54
+ }
55
+ async submit() {
56
+ if (!this.lastFormLocator) {
57
+ throw new Error("submit() called but no form was previously interacted with. " +
58
+ "Use fillIn(), selectOption(), check(), uncheck(), or choose() first.");
59
+ }
60
+ const submitBtn = this.lastFormLocator.locator('button[type="submit"], input[type="submit"]');
61
+ if ((await submitBtn.count()) > 0) {
62
+ await submitBtn.first().click();
63
+ }
64
+ else {
65
+ await this.lastFormLocator
66
+ .locator("input, textarea, select")
67
+ .last()
68
+ .press("Enter");
69
+ }
70
+ }
71
+ async assertHas(selector, opts) {
72
+ let locator = this.scope.locator(selector);
73
+ if (opts?.text) {
74
+ locator = opts.exact
75
+ ? locator.filter({ hasText: opts.text })
76
+ : locator.filter({ hasText: new RegExp(opts.text) });
77
+ }
78
+ if (opts?.count !== undefined) {
79
+ await expect(locator).toHaveCount(opts.count, {
80
+ timeout: opts?.timeout,
81
+ });
82
+ }
83
+ else {
84
+ await expect(locator.first()).toBeVisible({ timeout: opts?.timeout });
85
+ }
86
+ }
87
+ async refuteHas(selector, opts) {
88
+ let locator = this.scope.locator(selector);
89
+ if (opts?.text) {
90
+ locator = locator.filter({ hasText: opts.text });
91
+ }
92
+ await expect(locator).toHaveCount(0, { timeout: opts?.timeout });
93
+ }
94
+ async assertText(text) {
95
+ await expect(this.scope.getByText(text).first()).toBeVisible();
96
+ }
97
+ async refuteText(text) {
98
+ await expect(this.scope.getByText(text)).toHaveCount(0);
99
+ }
100
+ async assertPath(path, opts) {
101
+ if (opts?.queryParams) {
102
+ const params = new URLSearchParams(opts.queryParams).toString();
103
+ await expect(this.page).toHaveURL(`${path}?${params}`);
104
+ }
105
+ else {
106
+ const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
107
+ await expect(this.page).toHaveURL(new RegExp(`^[^?]*${escaped}(\\?.*)?$`));
108
+ }
109
+ }
110
+ async refutePath(path) {
111
+ const url = new URL(this.page.url());
112
+ if (url.pathname === path) {
113
+ throw new Error(`Expected path to NOT be '${path}', but it is.`);
114
+ }
115
+ }
116
+ async within(selector) {
117
+ const scopedLocator = this.scope.locator(selector);
118
+ await expect(scopedLocator).toBeAttached();
119
+ return new PlaywrightDriver(this.page, scopedLocator);
120
+ }
121
+ async debug() {
122
+ await this.page.screenshot({
123
+ path: `debug-${Date.now()}.png`,
124
+ fullPage: true,
125
+ });
126
+ }
127
+ }
@@ -0,0 +1,12 @@
1
+ import { type Page } from "@playwright/test";
2
+ import { Session } from "../session.js";
3
+ export { Session } from "../session.js";
4
+ export { StepError } from "../errors.js";
5
+ export { PlaywrightDriver } from "./driver.js";
6
+ export type { AssertHasOptions, AssertPathOptions, TestDriver, } from "../types.js";
7
+ export declare function createSession(page: Page): Session;
8
+ export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
9
+ session: Session;
10
+ }, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions>;
11
+ export { expect } from "@playwright/test";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,15 @@
1
+ import { test as base } from "@playwright/test";
2
+ import { Session } from "../session.js";
3
+ import { PlaywrightDriver } from "./driver.js";
4
+ export { Session } from "../session.js";
5
+ export { StepError } from "../errors.js";
6
+ export { PlaywrightDriver } from "./driver.js";
7
+ export function createSession(page) {
8
+ return new Session(new PlaywrightDriver(page));
9
+ }
10
+ export const test = base.extend({
11
+ session: async ({ page }, use) => {
12
+ await use(createSession(page));
13
+ },
14
+ });
15
+ export { expect } from "@playwright/test";
@@ -0,0 +1,31 @@
1
+ import { type UserEvent } from "@testing-library/user-event";
2
+ import type { AssertHasOptions, TestDriver } from "../types.js";
3
+ /**
4
+ * RTL adapter implementing the subset of TestDriver that applies in JSDOM.
5
+ * Navigation methods (visit, assertPath, refutePath) are not supported.
6
+ */
7
+ export declare class RTLDriver implements TestDriver {
8
+ private user;
9
+ private container;
10
+ private lastFormElement;
11
+ constructor(user?: UserEvent, container?: HTMLElement);
12
+ visit(): Promise<void>;
13
+ click(text: string): Promise<void>;
14
+ clickLink(text: string): Promise<void>;
15
+ clickButton(text: string): Promise<void>;
16
+ fillIn(label: string, value: string): Promise<void>;
17
+ selectOption(label: string, option: string): Promise<void>;
18
+ check(label: string): Promise<void>;
19
+ uncheck(label: string): Promise<void>;
20
+ choose(label: string): Promise<void>;
21
+ submit(): Promise<void>;
22
+ assertHas(_selector: string, _opts?: AssertHasOptions): Promise<void>;
23
+ refuteHas(_selector: string, _opts?: AssertHasOptions): Promise<void>;
24
+ assertText(text: string): Promise<void>;
25
+ refuteText(text: string): Promise<void>;
26
+ assertPath(): Promise<void>;
27
+ refutePath(): Promise<void>;
28
+ within(selector: string): Promise<TestDriver>;
29
+ debug(): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=driver.d.ts.map
@@ -0,0 +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;IAM1D,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;IASvC,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"}
@@ -0,0 +1,115 @@
1
+ import { screen, within as rtlWithin } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ /**
4
+ * RTL adapter implementing the subset of TestDriver that applies in JSDOM.
5
+ * Navigation methods (visit, assertPath, refutePath) are not supported.
6
+ */
7
+ export class RTLDriver {
8
+ user;
9
+ container;
10
+ lastFormElement = null;
11
+ constructor(user, container) {
12
+ this.user = user ?? userEvent.setup();
13
+ this.container = container ? rtlWithin(container) : screen;
14
+ }
15
+ async visit() {
16
+ throw new Error("visit() is not available in the RTL adapter. Render the desired component directly.");
17
+ }
18
+ async click(text) {
19
+ const element = await this.container.findByText(text);
20
+ await this.user.click(element);
21
+ }
22
+ async clickLink(text) {
23
+ const link = await this.container.findByRole("link", { name: text });
24
+ await this.user.click(link);
25
+ }
26
+ async clickButton(text) {
27
+ const button = await this.container.findByRole("button", { name: text });
28
+ await this.user.click(button);
29
+ }
30
+ 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
+ }
38
+ await this.user.clear(input);
39
+ await this.user.type(input, value);
40
+ this.lastFormElement = input.closest("form");
41
+ }
42
+ async selectOption(label, option) {
43
+ const select = await this.container.findByLabelText(label);
44
+ await this.user.selectOptions(select, option);
45
+ this.lastFormElement = select.closest("form");
46
+ }
47
+ async check(label) {
48
+ const checkbox = await this.container.findByLabelText(label);
49
+ if (!checkbox.checked) {
50
+ await this.user.click(checkbox);
51
+ }
52
+ this.lastFormElement = checkbox.closest("form");
53
+ }
54
+ async uncheck(label) {
55
+ const checkbox = await this.container.findByLabelText(label);
56
+ if (checkbox.checked) {
57
+ await this.user.click(checkbox);
58
+ }
59
+ this.lastFormElement = checkbox.closest("form");
60
+ }
61
+ async choose(label) {
62
+ const radio = await this.container.findByRole("radio", { name: label });
63
+ await this.user.click(radio);
64
+ this.lastFormElement = radio.closest("form");
65
+ }
66
+ async submit() {
67
+ if (!this.lastFormElement) {
68
+ throw new Error("submit() called but no form was previously interacted with.");
69
+ }
70
+ const submitBtn = rtlWithin(this.lastFormElement).queryByRole("button", {
71
+ name: /submit/i,
72
+ }) ??
73
+ this.lastFormElement.querySelector('button[type="submit"], input[type="submit"]');
74
+ if (submitBtn) {
75
+ await this.user.click(submitBtn);
76
+ }
77
+ else {
78
+ this.lastFormElement.requestSubmit();
79
+ }
80
+ }
81
+ async assertHas(_selector, _opts) {
82
+ throw new Error("assertHas() with CSS selectors is not recommended in RTL. Use assertText() instead.");
83
+ }
84
+ async refuteHas(_selector, _opts) {
85
+ throw new Error("refuteHas() with CSS selectors is not recommended in RTL. Use refuteText() instead.");
86
+ }
87
+ async assertText(text) {
88
+ await this.container.findByText(text);
89
+ }
90
+ async refuteText(text) {
91
+ const el = this.container.queryByText(text);
92
+ if (el) {
93
+ throw new Error(`Expected NOT to find text '${text}', but it was present.`);
94
+ }
95
+ }
96
+ async assertPath() {
97
+ throw new Error("assertPath() is not available in the RTL adapter (no real URL in JSDOM).");
98
+ }
99
+ async refutePath() {
100
+ throw new Error("refutePath() is not available in the RTL adapter (no real URL in JSDOM).");
101
+ }
102
+ async within(selector) {
103
+ const root = this.container === screen
104
+ ? document.body
105
+ : (this.container
106
+ .container ?? document.body);
107
+ const element = root.querySelector(selector);
108
+ if (!element)
109
+ throw new Error(`within('${selector}'): element not found`);
110
+ return new RTLDriver(this.user, element);
111
+ }
112
+ async debug() {
113
+ screen.debug();
114
+ }
115
+ }
@@ -0,0 +1,6 @@
1
+ import { Session } from "../session.js";
2
+ export { Session } from "../session.js";
3
+ export { StepError } from "../errors.js";
4
+ export { RTLDriver } from "./driver.js";
5
+ export declare function createSession(): Session;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,8 @@
1
+ import { Session } from "../session.js";
2
+ import { RTLDriver } from "./driver.js";
3
+ export { Session } from "../session.js";
4
+ export { StepError } from "../errors.js";
5
+ export { RTLDriver } from "./driver.js";
6
+ export function createSession() {
7
+ return new Session(new RTLDriver());
8
+ }
@@ -0,0 +1,29 @@
1
+ import type { AssertHasOptions, AssertPathOptions, TestDriver } from "./types.js";
2
+ export declare class Session implements PromiseLike<void> {
3
+ private driver;
4
+ private steps;
5
+ private stepIndex;
6
+ constructor(driver: TestDriver);
7
+ then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
8
+ private executeSteps;
9
+ private enqueue;
10
+ visit(path: string): this;
11
+ click(text: string): this;
12
+ clickLink(text: string): this;
13
+ clickButton(text: string): this;
14
+ fillIn(label: string, value: string): this;
15
+ selectOption(label: string, option: string): this;
16
+ check(label: string): this;
17
+ uncheck(label: string): this;
18
+ choose(label: string): this;
19
+ submit(): this;
20
+ assertText(text: string): this;
21
+ refuteText(text: string): this;
22
+ assertHas(selector: string, opts?: AssertHasOptions): this;
23
+ refuteHas(selector: string, opts?: AssertHasOptions): this;
24
+ assertPath(path: string, opts?: AssertPathOptions): this;
25
+ refutePath(path: string): this;
26
+ within(selector: string, fn: (scoped: Session) => Session): this;
27
+ debug(): this;
28
+ }
29
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,97 @@
1
+ import { StepError } from "./errors.js";
2
+ export class Session {
3
+ driver;
4
+ steps = [];
5
+ stepIndex = 0;
6
+ constructor(driver) {
7
+ this.driver = driver;
8
+ }
9
+ then(onfulfilled, onrejected) {
10
+ return this.executeSteps().then(onfulfilled, onrejected);
11
+ }
12
+ async executeSteps() {
13
+ const steps = [...this.steps];
14
+ this.steps = [];
15
+ for (const step of steps) {
16
+ try {
17
+ await step.action();
18
+ }
19
+ catch (error) {
20
+ throw new StepError(step, steps, error);
21
+ }
22
+ }
23
+ }
24
+ enqueue(name, action) {
25
+ this.steps.push({ name, action, index: this.stepIndex++ });
26
+ return this;
27
+ }
28
+ // --- Navigation ---
29
+ visit(path) {
30
+ return this.enqueue(`visit('${path}')`, () => this.driver.visit(path));
31
+ }
32
+ // --- Interactions ---
33
+ click(text) {
34
+ return this.enqueue(`click('${text}')`, () => this.driver.click(text));
35
+ }
36
+ clickLink(text) {
37
+ return this.enqueue(`clickLink('${text}')`, () => this.driver.clickLink(text));
38
+ }
39
+ clickButton(text) {
40
+ return this.enqueue(`clickButton('${text}')`, () => this.driver.clickButton(text));
41
+ }
42
+ fillIn(label, value) {
43
+ return this.enqueue(`fillIn('${label}', '${value}')`, () => this.driver.fillIn(label, value));
44
+ }
45
+ selectOption(label, option) {
46
+ return this.enqueue(`selectOption('${label}', '${option}')`, () => this.driver.selectOption(label, option));
47
+ }
48
+ check(label) {
49
+ return this.enqueue(`check('${label}')`, () => this.driver.check(label));
50
+ }
51
+ uncheck(label) {
52
+ return this.enqueue(`uncheck('${label}')`, () => this.driver.uncheck(label));
53
+ }
54
+ choose(label) {
55
+ return this.enqueue(`choose('${label}')`, () => this.driver.choose(label));
56
+ }
57
+ submit() {
58
+ return this.enqueue("submit()", () => this.driver.submit());
59
+ }
60
+ // --- Assertions ---
61
+ assertText(text) {
62
+ return this.enqueue(`assertText('${text}')`, () => this.driver.assertText(text));
63
+ }
64
+ refuteText(text) {
65
+ return this.enqueue(`refuteText('${text}')`, () => this.driver.refuteText(text));
66
+ }
67
+ assertHas(selector, opts) {
68
+ const desc = opts?.text
69
+ ? `assertHas('${selector}', text: '${opts.text}')`
70
+ : `assertHas('${selector}')`;
71
+ return this.enqueue(desc, () => this.driver.assertHas(selector, opts));
72
+ }
73
+ refuteHas(selector, opts) {
74
+ const desc = opts?.text
75
+ ? `refuteHas('${selector}', text: '${opts.text}')`
76
+ : `refuteHas('${selector}')`;
77
+ return this.enqueue(desc, () => this.driver.refuteHas(selector, opts));
78
+ }
79
+ assertPath(path, opts) {
80
+ return this.enqueue(`assertPath('${path}')`, () => this.driver.assertPath(path, opts));
81
+ }
82
+ refutePath(path) {
83
+ return this.enqueue(`refutePath('${path}')`, () => this.driver.refutePath(path));
84
+ }
85
+ // --- Scoping ---
86
+ within(selector, fn) {
87
+ return this.enqueue(`within('${selector}')`, async () => {
88
+ const scopedDriver = await this.driver.within(selector);
89
+ const scopedSession = new Session(scopedDriver);
90
+ await fn(scopedSession);
91
+ });
92
+ }
93
+ // --- Debug ---
94
+ debug() {
95
+ return this.enqueue("debug()", () => this.driver.debug());
96
+ }
97
+ }
@@ -0,0 +1,35 @@
1
+ export interface AssertHasOptions {
2
+ text?: string;
3
+ count?: number;
4
+ exact?: boolean;
5
+ timeout?: number;
6
+ }
7
+ export interface AssertPathOptions {
8
+ queryParams?: Record<string, string>;
9
+ }
10
+ export interface QueuedStep {
11
+ name: string;
12
+ action: () => Promise<void>;
13
+ index: number;
14
+ }
15
+ export interface TestDriver {
16
+ visit(path: string): Promise<void>;
17
+ click(text: string): Promise<void>;
18
+ clickLink(text: string): Promise<void>;
19
+ clickButton(text: string): Promise<void>;
20
+ fillIn(label: string, value: string): Promise<void>;
21
+ selectOption(label: string, option: string): Promise<void>;
22
+ check(label: string): Promise<void>;
23
+ uncheck(label: string): Promise<void>;
24
+ choose(label: string): Promise<void>;
25
+ submit(): Promise<void>;
26
+ assertHas(selector: string, opts?: AssertHasOptions): Promise<void>;
27
+ refuteHas(selector: string, opts?: AssertHasOptions): Promise<void>;
28
+ assertText(text: string): Promise<void>;
29
+ refuteText(text: string): Promise<void>;
30
+ assertPath(path: string, opts?: AssertPathOptions): Promise<void>;
31
+ refutePath(path: string): Promise<void>;
32
+ within(selector: string): Promise<TestDriver>;
33
+ debug(): Promise<void>;
34
+ }
35
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +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"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "feather-testing-core",
3
+ "version": "0.1.0",
4
+ "description": "Phoenix Test-inspired fluent testing DSL for Playwright and React Testing Library",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "testing",
8
+ "playwright",
9
+ "react-testing-library",
10
+ "fluent",
11
+ "dsl",
12
+ "e2e",
13
+ "integration"
14
+ ],
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./playwright": {
23
+ "types": "./dist/playwright/index.d.ts",
24
+ "import": "./dist/playwright/index.js",
25
+ "default": "./dist/playwright/index.js"
26
+ },
27
+ "./rtl": {
28
+ "types": "./dist/rtl/index.d.ts",
29
+ "import": "./dist/rtl/index.js",
30
+ "default": "./dist/rtl/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "peerDependencies": {
37
+ "@playwright/test": ">=1.40.0",
38
+ "@testing-library/react": ">=14.0.0",
39
+ "@testing-library/user-event": ">=14.0.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@playwright/test": {
43
+ "optional": true
44
+ },
45
+ "@testing-library/react": {
46
+ "optional": true
47
+ },
48
+ "@testing-library/user-event": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "scripts": {
53
+ "build": "tsc"
54
+ },
55
+ "devDependencies": {
56
+ "@playwright/test": "^1.58.0",
57
+ "@testing-library/react": "^16.3.0",
58
+ "@testing-library/user-event": "^14.6.0",
59
+ "typescript": "~5.9.3"
60
+ }
61
+ }