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 +225 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +21 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/playwright/driver.d.ts +27 -0
- package/dist/playwright/driver.d.ts.map +1 -0
- package/dist/playwright/driver.js +127 -0
- package/dist/playwright/index.d.ts +12 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +15 -0
- package/dist/rtl/driver.d.ts +31 -0
- package/dist/rtl/driver.d.ts.map +1 -0
- package/dist/rtl/driver.js +115 -0
- package/dist/rtl/index.d.ts +6 -0
- package/dist/rtl/index.d.ts.map +1 -0
- package/dist/rtl/index.js +8 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +97 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +61 -0
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
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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 @@
|
|
|
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"}
|
package/dist/session.js
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|