feather-testing-core 0.2.0 → 0.4.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 +177 -4
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +12 -0
- package/dist/eslint-plugin/index.d.ts +18 -0
- package/dist/eslint-plugin/index.d.ts.map +1 -0
- package/dist/eslint-plugin/index.js +37 -0
- package/dist/eslint-plugin/rules/no-conditional-skip.d.ts +4 -0
- package/dist/eslint-plugin/rules/no-conditional-skip.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/no-conditional-skip.js +56 -0
- package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.d.ts +4 -0
- package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.js +63 -0
- package/dist/eslint-plugin/rules/no-wait-for-timeout.d.ts +4 -0
- package/dist/eslint-plugin/rules/no-wait-for-timeout.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/no-wait-for-timeout.js +69 -0
- package/dist/eslint-plugin/rules/no-weak-assertions.d.ts +4 -0
- package/dist/eslint-plugin/rules/no-weak-assertions.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/no-weak-assertions.js +73 -0
- package/dist/eslint-plugin/rules/warn-serial-mode.d.ts +4 -0
- package/dist/eslint-plugin/rules/warn-serial-mode.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/warn-serial-mode.js +59 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/playwright/driver.d.ts +18 -3
- package/dist/playwright/driver.d.ts.map +1 -1
- package/dist/playwright/driver.js +82 -18
- package/dist/playwright/index.d.ts +4 -4
- package/dist/playwright/index.d.ts.map +1 -1
- package/dist/playwright/index.js +1 -1
- package/dist/rtl/driver.d.ts +55 -9
- package/dist/rtl/driver.d.ts.map +1 -1
- package/dist/rtl/driver.js +135 -35
- package/dist/rtl/index.d.ts +5 -4
- package/dist/rtl/index.d.ts.map +1 -1
- package/dist/rtl/index.js +2 -2
- package/dist/session.d.ts +41 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +72 -1
- package/dist/types.d.ts +35 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -2
package/dist/rtl/driver.js
CHANGED
|
@@ -1,54 +1,79 @@
|
|
|
1
1
|
import { fireEvent, screen, waitFor, within as rtlWithin, } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { BrowserOnlyVerbError } from "../errors.js";
|
|
3
4
|
/**
|
|
4
5
|
* RTL adapter implementing the subset of TestDriver that applies in JSDOM.
|
|
5
6
|
* Navigation methods (visit, assertPath, refutePath) are not supported.
|
|
7
|
+
*
|
|
8
|
+
* Everything a host adapter is likely to specialize is `protected`: the
|
|
9
|
+
* label-to-control lookup (`findField`), the scoped-driver factory
|
|
10
|
+
* (`scoped`), and the `user` / `root` / `lastFormElement` state the verbs
|
|
11
|
+
* share. Subclass it rather than reimplementing the DSL when an app's markup
|
|
12
|
+
* needs a different lookup — that is how feather-testing-postgres binds this
|
|
13
|
+
* driver to markup whose labels are wrapper siblings rather than `htmlFor`
|
|
14
|
+
* targets.
|
|
6
15
|
*/
|
|
7
16
|
export class RTLDriver {
|
|
8
17
|
user;
|
|
18
|
+
/** The element every query and selector in this driver resolves against. */
|
|
19
|
+
root;
|
|
9
20
|
container;
|
|
10
21
|
lastFormElement = null;
|
|
11
|
-
|
|
22
|
+
/** Per-lookup timeout in ms; undefined leaves RTL's own default in place. */
|
|
23
|
+
timeout;
|
|
24
|
+
constructor(user, container, timeout) {
|
|
12
25
|
this.user = user ?? userEvent.setup();
|
|
13
|
-
this.
|
|
26
|
+
this.root = container ?? document.body;
|
|
27
|
+
this.container = rtlWithin(this.root);
|
|
28
|
+
this.timeout = timeout;
|
|
14
29
|
}
|
|
15
30
|
rootElement() {
|
|
16
|
-
return this.
|
|
17
|
-
? document.body
|
|
18
|
-
: (this.container
|
|
19
|
-
.container ?? document.body);
|
|
31
|
+
return this.root;
|
|
20
32
|
}
|
|
21
|
-
async
|
|
33
|
+
/** Options forwarded to every async query and waitFor call. */
|
|
34
|
+
waitOpts() {
|
|
35
|
+
return this.timeout === undefined ? {} : { timeout: this.timeout };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The single label-addressed lookup: every labelled verb goes through it,
|
|
39
|
+
* so overriding this one method retargets them all.
|
|
40
|
+
*/
|
|
41
|
+
async findField(label) {
|
|
22
42
|
try {
|
|
23
|
-
return await this.container.findByLabelText(label);
|
|
43
|
+
return await this.container.findByLabelText(label, undefined, this.waitOpts());
|
|
24
44
|
}
|
|
25
45
|
catch {
|
|
26
|
-
return await this.container.findByPlaceholderText(label);
|
|
46
|
+
return await this.container.findByPlaceholderText(label, undefined, this.waitOpts());
|
|
27
47
|
}
|
|
28
48
|
}
|
|
49
|
+
/** Subclasses override this so within() yields a driver of their own type. */
|
|
50
|
+
scoped(element) {
|
|
51
|
+
return new RTLDriver(this.user, element, this.timeout);
|
|
52
|
+
}
|
|
29
53
|
async visit() {
|
|
30
54
|
throw new Error("visit() is not available in the RTL adapter. Render the desired component directly.");
|
|
31
55
|
}
|
|
32
56
|
async click(text) {
|
|
33
|
-
const element = await this.container.findByText(text);
|
|
57
|
+
const element = await this.container.findByText(text, undefined, this.waitOpts());
|
|
34
58
|
await this.user.click(element);
|
|
35
59
|
}
|
|
36
60
|
async clickLink(text) {
|
|
37
|
-
const link = await this.container.findByRole("link", { name: text });
|
|
61
|
+
const link = await this.container.findByRole("link", { name: text }, this.waitOpts());
|
|
38
62
|
await this.user.click(link);
|
|
39
63
|
}
|
|
40
64
|
async clickButton(text) {
|
|
41
|
-
const button = await this.container.findByRole("button", { name: text });
|
|
65
|
+
const button = await this.container.findByRole("button", { name: text }, this.waitOpts());
|
|
42
66
|
await this.user.click(button);
|
|
43
67
|
}
|
|
44
68
|
async fillIn(label, value) {
|
|
45
|
-
const input = await this.
|
|
69
|
+
const input = await this.findField(label);
|
|
46
70
|
await this.user.clear(input);
|
|
47
|
-
|
|
71
|
+
if (value)
|
|
72
|
+
await this.user.type(input, value);
|
|
48
73
|
this.lastFormElement = input.closest("form");
|
|
49
74
|
}
|
|
50
75
|
async selectOption(label, option) {
|
|
51
|
-
const select = await this.
|
|
76
|
+
const select = await this.findField(label);
|
|
52
77
|
const optionEl = Array.from(select.querySelectorAll("option")).find((o) => o.textContent?.trim() === option);
|
|
53
78
|
if (!optionEl) {
|
|
54
79
|
throw new Error(`selectOption('${label}', '${option}'): no <option> with text '${option}' found.`);
|
|
@@ -57,21 +82,21 @@ export class RTLDriver {
|
|
|
57
82
|
this.lastFormElement = select.closest("form");
|
|
58
83
|
}
|
|
59
84
|
async check(label) {
|
|
60
|
-
const checkbox = await this.
|
|
85
|
+
const checkbox = await this.findField(label);
|
|
61
86
|
if (!checkbox.checked) {
|
|
62
87
|
await this.user.click(checkbox);
|
|
63
88
|
}
|
|
64
89
|
this.lastFormElement = checkbox.closest("form");
|
|
65
90
|
}
|
|
66
91
|
async uncheck(label) {
|
|
67
|
-
const checkbox = await this.
|
|
92
|
+
const checkbox = await this.findField(label);
|
|
68
93
|
if (checkbox.checked) {
|
|
69
94
|
await this.user.click(checkbox);
|
|
70
95
|
}
|
|
71
96
|
this.lastFormElement = checkbox.closest("form");
|
|
72
97
|
}
|
|
73
98
|
async choose(label) {
|
|
74
|
-
const radio = await this.container.findByRole("radio", { name: label });
|
|
99
|
+
const radio = await this.container.findByRole("radio", { name: label }, this.waitOpts());
|
|
75
100
|
await this.user.click(radio);
|
|
76
101
|
this.lastFormElement = radio.closest("form");
|
|
77
102
|
}
|
|
@@ -92,14 +117,30 @@ export class RTLDriver {
|
|
|
92
117
|
this.lastFormElement.requestSubmit();
|
|
93
118
|
}
|
|
94
119
|
}
|
|
95
|
-
async
|
|
96
|
-
const input = await this.
|
|
120
|
+
async attachFile(label, path) {
|
|
121
|
+
const input = await this.findField(label);
|
|
97
122
|
// JSDOM has no filesystem access; synthesize a File from the basename.
|
|
98
123
|
const name = path.split(/[\\/]/).pop() ?? path;
|
|
99
124
|
const file = new File([""], name);
|
|
100
125
|
await this.user.upload(input, file);
|
|
101
126
|
this.lastFormElement = input.closest("form");
|
|
102
127
|
}
|
|
128
|
+
/** @deprecated Older name for {@link RTLDriver.attachFile}. */
|
|
129
|
+
async upload(label, path) {
|
|
130
|
+
await this.attachFile(label, path);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Keys are named the Playwright way ('Enter', 'Control+A') and translated
|
|
134
|
+
* to user-event's keyboard syntax, so one spec reads the same on both
|
|
135
|
+
* adapters.
|
|
136
|
+
*/
|
|
137
|
+
async pressKey(key) {
|
|
138
|
+
await this.user.keyboard(toKeyboardSyntax(key));
|
|
139
|
+
}
|
|
140
|
+
async hover(text) {
|
|
141
|
+
const element = await this.container.findByText(text, undefined, this.waitOpts());
|
|
142
|
+
await this.user.hover(element);
|
|
143
|
+
}
|
|
103
144
|
async dropFile(selector, path) {
|
|
104
145
|
const target = this.rootElement().querySelector(selector);
|
|
105
146
|
if (!target) {
|
|
@@ -122,7 +163,7 @@ export class RTLDriver {
|
|
|
122
163
|
throw new Error("refuteHas() with CSS selectors is not recommended in RTL. Use refuteText() instead.");
|
|
123
164
|
}
|
|
124
165
|
async assertText(text) {
|
|
125
|
-
await this.container.findByText(text);
|
|
166
|
+
await this.container.findByText(text, undefined, this.waitOpts());
|
|
126
167
|
}
|
|
127
168
|
async refuteText(text) {
|
|
128
169
|
await waitFor(() => {
|
|
@@ -130,44 +171,44 @@ export class RTLDriver {
|
|
|
130
171
|
if (el) {
|
|
131
172
|
throw new Error(`Expected NOT to find text '${text}', but it was present.`);
|
|
132
173
|
}
|
|
133
|
-
});
|
|
174
|
+
}, this.waitOpts());
|
|
134
175
|
}
|
|
135
176
|
async assertValue(label, value) {
|
|
136
|
-
const field = await this.
|
|
177
|
+
const field = await this.findField(label);
|
|
137
178
|
await waitFor(() => {
|
|
138
179
|
const actual = field.value;
|
|
139
180
|
if (actual !== value) {
|
|
140
181
|
throw new Error(`assertValue('${label}', '${value}'): expected value '${value}', but found '${actual}'.`);
|
|
141
182
|
}
|
|
142
|
-
});
|
|
183
|
+
}, this.waitOpts());
|
|
143
184
|
}
|
|
144
185
|
async assertChecked(label) {
|
|
145
|
-
const checkbox = await this.
|
|
186
|
+
const checkbox = await this.findField(label);
|
|
146
187
|
await waitFor(() => {
|
|
147
188
|
if (!checkbox.checked) {
|
|
148
189
|
throw new Error(`assertChecked('${label}'): expected checkbox to be checked, but it was not.`);
|
|
149
190
|
}
|
|
150
|
-
});
|
|
191
|
+
}, this.waitOpts());
|
|
151
192
|
}
|
|
152
193
|
async refuteChecked(label) {
|
|
153
|
-
const checkbox = await this.
|
|
194
|
+
const checkbox = await this.findField(label);
|
|
154
195
|
await waitFor(() => {
|
|
155
196
|
if (checkbox.checked) {
|
|
156
197
|
throw new Error(`refuteChecked('${label}'): expected checkbox NOT to be checked, but it was.`);
|
|
157
198
|
}
|
|
158
|
-
});
|
|
199
|
+
}, this.waitOpts());
|
|
159
200
|
}
|
|
160
201
|
async assertSelected(label, optionLabel) {
|
|
161
|
-
const select = (await this.
|
|
202
|
+
const select = (await this.findField(label));
|
|
162
203
|
await waitFor(() => {
|
|
163
204
|
const selected = select.selectedOptions[0]?.textContent?.trim();
|
|
164
205
|
if (selected !== optionLabel) {
|
|
165
206
|
throw new Error(`assertSelected('${label}', '${optionLabel}'): expected selected option '${optionLabel}', but found '${selected ?? "(none)"}'.`);
|
|
166
207
|
}
|
|
167
|
-
});
|
|
208
|
+
}, this.waitOpts());
|
|
168
209
|
}
|
|
169
210
|
async assertOptions(label, optionLabels) {
|
|
170
|
-
const select = (await this.
|
|
211
|
+
const select = (await this.findField(label));
|
|
171
212
|
await waitFor(() => {
|
|
172
213
|
const actual = Array.from(select.querySelectorAll("option")).map((o) => o.textContent?.trim() ?? "");
|
|
173
214
|
const matches = actual.length === optionLabels.length &&
|
|
@@ -175,7 +216,7 @@ export class RTLDriver {
|
|
|
175
216
|
if (!matches) {
|
|
176
217
|
throw new Error(`assertOptions('${label}'): expected options [${optionLabels.join(", ")}], but found [${actual.join(", ")}].`);
|
|
177
218
|
}
|
|
178
|
-
});
|
|
219
|
+
}, this.waitOpts());
|
|
179
220
|
}
|
|
180
221
|
async assertPath() {
|
|
181
222
|
throw new Error("assertPath() is not available in the RTL adapter (no real URL in JSDOM).");
|
|
@@ -183,16 +224,75 @@ export class RTLDriver {
|
|
|
183
224
|
async refutePath() {
|
|
184
225
|
throw new Error("refutePath() is not available in the RTL adapter (no real URL in JSDOM).");
|
|
185
226
|
}
|
|
227
|
+
async assertDownload(_expected, _trigger, _opts) {
|
|
228
|
+
throw new BrowserOnlyVerbError("assertDownload()", "Assert the request the download would make, or move this case to a " +
|
|
229
|
+
"Playwright spec where a real browser can accept the file.");
|
|
230
|
+
}
|
|
231
|
+
async until(description, predicate, opts) {
|
|
232
|
+
await waitFor(async () => {
|
|
233
|
+
if (!(await predicate(this.context()))) {
|
|
234
|
+
throw new Error(`until: ${description} — condition was still not met.`);
|
|
235
|
+
}
|
|
236
|
+
}, {
|
|
237
|
+
...this.waitOpts(),
|
|
238
|
+
...(opts?.timeout === undefined ? {} : { timeout: opts.timeout }),
|
|
239
|
+
...(opts?.interval === undefined ? {} : { interval: opts.interval }),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
186
242
|
async step(fn) {
|
|
187
|
-
await fn(
|
|
243
|
+
await fn(this.context());
|
|
244
|
+
}
|
|
245
|
+
/** raw() hands over the scoped query object — `within(root)`, i.e. screen
|
|
246
|
+
* when the driver is unscoped. */
|
|
247
|
+
async raw(fn) {
|
|
248
|
+
await fn(this.container);
|
|
249
|
+
}
|
|
250
|
+
/** The adapter context handed to step(), until(), and friends. */
|
|
251
|
+
context() {
|
|
252
|
+
return { user: this.user, container: this.container };
|
|
188
253
|
}
|
|
189
254
|
async within(selector) {
|
|
190
255
|
const element = this.rootElement().querySelector(selector);
|
|
191
256
|
if (!element)
|
|
192
257
|
throw new Error(`within('${selector}'): element not found`);
|
|
193
|
-
return
|
|
258
|
+
return this.scoped(element);
|
|
194
259
|
}
|
|
195
260
|
async debug() {
|
|
196
|
-
screen.debug();
|
|
261
|
+
screen.debug(this.rootElement());
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** Playwright key names -> user-event keyboard syntax. */
|
|
265
|
+
const KEY_ALIASES = {
|
|
266
|
+
Ctrl: "Control",
|
|
267
|
+
Cmd: "Meta",
|
|
268
|
+
Command: "Meta",
|
|
269
|
+
Space: " ",
|
|
270
|
+
};
|
|
271
|
+
const MODIFIERS = new Set(["Control", "Shift", "Alt", "Meta"]);
|
|
272
|
+
/**
|
|
273
|
+
* 'Enter' -> '{Enter}', 'a' -> 'a', 'Control+A' -> '{Control>}A{/Control}'.
|
|
274
|
+
* Printable characters are typed literally with user-event's own `{` and `[`
|
|
275
|
+
* escapes applied, so a key name never turns into a descriptor by accident.
|
|
276
|
+
*/
|
|
277
|
+
export function toKeyboardSyntax(key) {
|
|
278
|
+
const parts = key.split("+").map((p) => KEY_ALIASES[p] ?? p);
|
|
279
|
+
const target = parts.pop();
|
|
280
|
+
if (target === undefined || target === "") {
|
|
281
|
+
throw new Error(`pressKey('${key}'): no key to press.`);
|
|
282
|
+
}
|
|
283
|
+
const held = parts.filter((p) => MODIFIERS.has(p));
|
|
284
|
+
if (held.length !== parts.length) {
|
|
285
|
+
throw new Error(`pressKey('${key}'): '${parts.find((p) => !MODIFIERS.has(p))}' is not a ` +
|
|
286
|
+
"modifier. Use Control, Shift, Alt, or Meta.");
|
|
197
287
|
}
|
|
288
|
+
const pressed = target.length === 1
|
|
289
|
+
? target.replace(/[{[]/g, (c) => c + c)
|
|
290
|
+
: `{${target}}`;
|
|
291
|
+
return (held.map((m) => `{${m}>}`).join("") +
|
|
292
|
+
pressed +
|
|
293
|
+
held
|
|
294
|
+
.slice()
|
|
295
|
+
.reverse()
|
|
296
|
+
.map((m) => `{/${m}}`)
|
|
297
|
+
.join(""));
|
|
198
298
|
}
|
package/dist/rtl/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Session } from "../session.js";
|
|
2
|
-
import { type RTLStepContext } from "./driver.js";
|
|
2
|
+
import { type RTLQueries, type RTLStepContext } from "./driver.js";
|
|
3
3
|
export { Session } from "../session.js";
|
|
4
|
-
export { StepError } from "../errors.js";
|
|
5
|
-
export { RTLDriver, type RTLStepContext } from "./driver.js";
|
|
6
|
-
export
|
|
4
|
+
export { StepError, BrowserOnlyVerbError } from "../errors.js";
|
|
5
|
+
export { RTLDriver, type RTLQueries, type RTLStepContext, } from "./driver.js";
|
|
6
|
+
export type { AssertHasOptions, DownloadOptions, TestDriver, UntilOptions, UntilPredicate, } from "../types.js";
|
|
7
|
+
export declare function createSession(): Session<RTLStepContext, RTLQueries>;
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/rtl/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rtl/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAa,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rtl/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAa,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9E,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EACL,SAAS,EACT,KAAK,UAAU,EACf,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,wBAAgB,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,UAAU,CAAC,CAEnE"}
|
package/dist/rtl/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Session } from "../session.js";
|
|
2
2
|
import { RTLDriver } from "./driver.js";
|
|
3
3
|
export { Session } from "../session.js";
|
|
4
|
-
export { StepError } from "../errors.js";
|
|
5
|
-
export { RTLDriver } from "./driver.js";
|
|
4
|
+
export { StepError, BrowserOnlyVerbError } from "../errors.js";
|
|
5
|
+
export { RTLDriver, } from "./driver.js";
|
|
6
6
|
export function createSession() {
|
|
7
7
|
return new Session(new RTLDriver());
|
|
8
8
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { AssertHasOptions, AssertPathOptions, TestDriver } from "./types.js";
|
|
2
|
-
export declare class Session<TContext = unknown> implements PromiseLike<void> {
|
|
1
|
+
import type { AssertHasOptions, AssertPathOptions, DownloadOptions, TestDriver, UntilOptions, UntilPredicate } from "./types.js";
|
|
2
|
+
export declare class Session<TContext = unknown, TNative = unknown> implements PromiseLike<void> {
|
|
3
3
|
private driver;
|
|
4
4
|
private steps;
|
|
5
5
|
private executedSteps;
|
|
6
6
|
private stepIndex;
|
|
7
|
-
constructor(driver: TestDriver<TContext>);
|
|
7
|
+
constructor(driver: TestDriver<TContext, TNative>);
|
|
8
8
|
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
9
9
|
private executeSteps;
|
|
10
10
|
private enqueue;
|
|
@@ -18,8 +18,15 @@ export declare class Session<TContext = unknown> implements PromiseLike<void> {
|
|
|
18
18
|
uncheck(label: string): this;
|
|
19
19
|
choose(label: string): this;
|
|
20
20
|
submit(): this;
|
|
21
|
+
/** Set a file input, found by its label, to the file at `path`. */
|
|
22
|
+
attachFile(label: string, path: string): this;
|
|
23
|
+
/** @deprecated Older name for {@link Session.attachFile}. */
|
|
21
24
|
upload(label: string, path: string): this;
|
|
22
25
|
dropFile(selector: string, path: string): this;
|
|
26
|
+
/** Press a key on the focused element, e.g. 'Enter' or 'Control+A'. */
|
|
27
|
+
pressKey(key: string): this;
|
|
28
|
+
/** Hover the element with this text. */
|
|
29
|
+
hover(text: string): this;
|
|
23
30
|
assertText(text: string): this;
|
|
24
31
|
refuteText(text: string): this;
|
|
25
32
|
assertValue(label: string, value: string): this;
|
|
@@ -31,6 +38,24 @@ export declare class Session<TContext = unknown> implements PromiseLike<void> {
|
|
|
31
38
|
refuteHas(selector: string, opts?: AssertHasOptions): this;
|
|
32
39
|
assertPath(path: string, opts?: AssertPathOptions): this;
|
|
33
40
|
refutePath(path: string): this;
|
|
41
|
+
/**
|
|
42
|
+
* Assert that running `trigger` makes the browser offer a download whose
|
|
43
|
+
* suggested filename matches `expected`. The trigger is a callback because
|
|
44
|
+
* the wait has to be armed before the click that starts the download.
|
|
45
|
+
*
|
|
46
|
+
* Browser-only: the RTL adapter throws a BrowserOnlyVerbError.
|
|
47
|
+
*/
|
|
48
|
+
assertDownload(expected: string | RegExp, trigger: (scoped: Session<TContext, TNative>) => Session<TContext, TNative> | PromiseLike<unknown>, opts?: DownloadOptions): this;
|
|
49
|
+
/**
|
|
50
|
+
* Wait for a condition instead of sleeping. `description` is mandatory: it
|
|
51
|
+
* is what the chain trace prints, so a timeout reads
|
|
52
|
+
* `[FAILED] until: the export finishes` rather than naming a mechanism.
|
|
53
|
+
*
|
|
54
|
+
* The predicate receives the adapter's context ({ page, scope } for
|
|
55
|
+
* Playwright, { user, container } for RTL) and may be sync or async; it
|
|
56
|
+
* is polled until it returns something truthy or the budget is spent.
|
|
57
|
+
*/
|
|
58
|
+
until(description: string, predicate: UntilPredicate<TContext>, opts?: UntilOptions): this;
|
|
34
59
|
/**
|
|
35
60
|
* Queue a named custom step. `fn` receives the adapter's context
|
|
36
61
|
* ({ page, scope } for Playwright, { user, container } for RTL), so a
|
|
@@ -38,7 +63,19 @@ export declare class Session<TContext = unknown> implements PromiseLike<void> {
|
|
|
38
63
|
* StepError output like any built-in step.
|
|
39
64
|
*/
|
|
40
65
|
step(name: string, fn: (context: TContext) => Promise<unknown>): this;
|
|
41
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Drop to the driver itself — Playwright's `page`, RTL's scoped queries —
|
|
68
|
+
* without leaving the chain. `label` is mandatory and registers as a named
|
|
69
|
+
* step, so an escape hatch still names intent in the trace instead of
|
|
70
|
+
* ending it: `[FAILED] raw('drag the card to Done')`.
|
|
71
|
+
*/
|
|
72
|
+
raw(label: string, fn: (native: TNative) => unknown | Promise<unknown>): this;
|
|
73
|
+
/**
|
|
74
|
+
* `fn` must either return the scoped session — so its queued steps run — or
|
|
75
|
+
* a promise it already awaited. Returning anything else would silently drop
|
|
76
|
+
* the scoped chain, which is why the callback's return type is not `unknown`.
|
|
77
|
+
*/
|
|
78
|
+
within(selector: string, fn: (scoped: Session<TContext, TNative>) => Session<TContext, TNative> | PromiseLike<unknown>): this;
|
|
42
79
|
debug(): this;
|
|
43
80
|
}
|
|
44
81
|
//# sourceMappingURL=session.d.ts.map
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/dist/session.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { StepError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verbs that take a human description exist so a failure names intent rather
|
|
4
|
+
* than mechanics. An empty description defeats that, so it is rejected where
|
|
5
|
+
* the mistake is — at the call site, before the chain runs.
|
|
6
|
+
*/
|
|
7
|
+
function requireDescription(verb, value) {
|
|
8
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
9
|
+
throw new Error(`feather-testing-core: ${verb}() requires a non-empty description as its ` +
|
|
10
|
+
"first argument — it is what the chain trace prints when the step fails.");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** How a string-or-regex expectation reads in the chain trace. */
|
|
14
|
+
function describe(expected) {
|
|
15
|
+
return typeof expected === "string" ? expected : String(expected);
|
|
16
|
+
}
|
|
2
17
|
export class Session {
|
|
3
18
|
driver;
|
|
4
19
|
steps = [];
|
|
@@ -62,12 +77,27 @@ export class Session {
|
|
|
62
77
|
submit() {
|
|
63
78
|
return this.enqueue("submit()", () => this.driver.submit());
|
|
64
79
|
}
|
|
80
|
+
/** Set a file input, found by its label, to the file at `path`. */
|
|
81
|
+
attachFile(label, path) {
|
|
82
|
+
return this.enqueue(`attachFile('${label}', '${path}')`, () => this.driver.attachFile(label, path));
|
|
83
|
+
}
|
|
84
|
+
/** @deprecated Older name for {@link Session.attachFile}. */
|
|
65
85
|
upload(label, path) {
|
|
66
|
-
return this.enqueue(`upload('${label}', '${path}')`, () => this.driver.upload
|
|
86
|
+
return this.enqueue(`upload('${label}', '${path}')`, () => this.driver.upload
|
|
87
|
+
? this.driver.upload(label, path)
|
|
88
|
+
: this.driver.attachFile(label, path));
|
|
67
89
|
}
|
|
68
90
|
dropFile(selector, path) {
|
|
69
91
|
return this.enqueue(`dropFile('${selector}', '${path}')`, () => this.driver.dropFile(selector, path));
|
|
70
92
|
}
|
|
93
|
+
/** Press a key on the focused element, e.g. 'Enter' or 'Control+A'. */
|
|
94
|
+
pressKey(key) {
|
|
95
|
+
return this.enqueue(`pressKey('${key}')`, () => this.driver.pressKey(key));
|
|
96
|
+
}
|
|
97
|
+
/** Hover the element with this text. */
|
|
98
|
+
hover(text) {
|
|
99
|
+
return this.enqueue(`hover('${text}')`, () => this.driver.hover(text));
|
|
100
|
+
}
|
|
71
101
|
// --- Assertions ---
|
|
72
102
|
assertText(text) {
|
|
73
103
|
return this.enqueue(`assertText('${text}')`, () => this.driver.assertText(text));
|
|
@@ -109,6 +139,32 @@ export class Session {
|
|
|
109
139
|
refutePath(path) {
|
|
110
140
|
return this.enqueue(`refutePath('${path}')`, () => this.driver.refutePath(path));
|
|
111
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Assert that running `trigger` makes the browser offer a download whose
|
|
144
|
+
* suggested filename matches `expected`. The trigger is a callback because
|
|
145
|
+
* the wait has to be armed before the click that starts the download.
|
|
146
|
+
*
|
|
147
|
+
* Browser-only: the RTL adapter throws a BrowserOnlyVerbError.
|
|
148
|
+
*/
|
|
149
|
+
assertDownload(expected, trigger, opts) {
|
|
150
|
+
return this.enqueue(`assertDownload('${describe(expected)}')`, () => this.driver.assertDownload(expected, async () => {
|
|
151
|
+
await trigger(new Session(this.driver));
|
|
152
|
+
}, opts));
|
|
153
|
+
}
|
|
154
|
+
// --- Waiting ---
|
|
155
|
+
/**
|
|
156
|
+
* Wait for a condition instead of sleeping. `description` is mandatory: it
|
|
157
|
+
* is what the chain trace prints, so a timeout reads
|
|
158
|
+
* `[FAILED] until: the export finishes` rather than naming a mechanism.
|
|
159
|
+
*
|
|
160
|
+
* The predicate receives the adapter's context ({ page, scope } for
|
|
161
|
+
* Playwright, { user, container } for RTL) and may be sync or async; it
|
|
162
|
+
* is polled until it returns something truthy or the budget is spent.
|
|
163
|
+
*/
|
|
164
|
+
until(description, predicate, opts) {
|
|
165
|
+
requireDescription("until", description);
|
|
166
|
+
return this.enqueue(`until: ${description}`, () => this.driver.until(description, predicate, opts));
|
|
167
|
+
}
|
|
112
168
|
// --- Escape hatch ---
|
|
113
169
|
/**
|
|
114
170
|
* Queue a named custom step. `fn` receives the adapter's context
|
|
@@ -119,7 +175,22 @@ export class Session {
|
|
|
119
175
|
step(name, fn) {
|
|
120
176
|
return this.enqueue(`step('${name}')`, () => this.driver.step(fn));
|
|
121
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Drop to the driver itself — Playwright's `page`, RTL's scoped queries —
|
|
180
|
+
* without leaving the chain. `label` is mandatory and registers as a named
|
|
181
|
+
* step, so an escape hatch still names intent in the trace instead of
|
|
182
|
+
* ending it: `[FAILED] raw('drag the card to Done')`.
|
|
183
|
+
*/
|
|
184
|
+
raw(label, fn) {
|
|
185
|
+
requireDescription("raw", label);
|
|
186
|
+
return this.enqueue(`raw('${label}')`, () => this.driver.raw(fn));
|
|
187
|
+
}
|
|
122
188
|
// --- Scoping ---
|
|
189
|
+
/**
|
|
190
|
+
* `fn` must either return the scoped session — so its queued steps run — or
|
|
191
|
+
* a promise it already awaited. Returning anything else would silently drop
|
|
192
|
+
* the scoped chain, which is why the callback's return type is not `unknown`.
|
|
193
|
+
*/
|
|
123
194
|
within(selector, fn) {
|
|
124
195
|
return this.enqueue(`within('${selector}')`, async () => {
|
|
125
196
|
const scopedDriver = await this.driver.within(selector);
|
package/dist/types.d.ts
CHANGED
|
@@ -7,16 +7,31 @@ export interface AssertHasOptions {
|
|
|
7
7
|
export interface AssertPathOptions {
|
|
8
8
|
queryParams?: Record<string, string>;
|
|
9
9
|
}
|
|
10
|
+
export interface UntilOptions {
|
|
11
|
+
/** Overall budget in ms. Defaults to the adapter's own wait timeout. */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
/** Gap between polls in ms. Defaults to the adapter's own cadence. */
|
|
14
|
+
interval?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface DownloadOptions {
|
|
17
|
+
/** How long to wait for the download to start, in ms. */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
}
|
|
10
20
|
export interface QueuedStep {
|
|
11
21
|
name: string;
|
|
12
22
|
action: () => Promise<void>;
|
|
13
23
|
index: number;
|
|
14
24
|
}
|
|
25
|
+
/** A condition polled by `until()`; may be sync or async. */
|
|
26
|
+
export type UntilPredicate<TContext> = (context: TContext) => unknown | Promise<unknown>;
|
|
15
27
|
/**
|
|
16
28
|
* TContext is the adapter-specific context handed to custom step() callbacks
|
|
17
29
|
* (e.g. { page, scope } for Playwright, { user, container } for RTL).
|
|
30
|
+
*
|
|
31
|
+
* TNative is the adapter's own driving handle, handed to raw() callbacks:
|
|
32
|
+
* Playwright's `Page`, RTL's scoped query object.
|
|
18
33
|
*/
|
|
19
|
-
export interface TestDriver<TContext = unknown> {
|
|
34
|
+
export interface TestDriver<TContext = unknown, TNative = unknown> {
|
|
20
35
|
visit(path: string): Promise<void>;
|
|
21
36
|
click(text: string): Promise<void>;
|
|
22
37
|
clickLink(text: string): Promise<void>;
|
|
@@ -27,8 +42,12 @@ export interface TestDriver<TContext = unknown> {
|
|
|
27
42
|
uncheck(label: string): Promise<void>;
|
|
28
43
|
choose(label: string): Promise<void>;
|
|
29
44
|
submit(): Promise<void>;
|
|
30
|
-
|
|
45
|
+
attachFile(label: string, path: string): Promise<void>;
|
|
46
|
+
/** @deprecated The older name for {@link TestDriver.attachFile}. */
|
|
47
|
+
upload?(label: string, path: string): Promise<void>;
|
|
31
48
|
dropFile(selector: string, path: string): Promise<void>;
|
|
49
|
+
pressKey(key: string): Promise<void>;
|
|
50
|
+
hover(text: string): Promise<void>;
|
|
32
51
|
assertHas(selector: string, opts?: AssertHasOptions): Promise<void>;
|
|
33
52
|
refuteHas(selector: string, opts?: AssertHasOptions): Promise<void>;
|
|
34
53
|
assertText(text: string): Promise<void>;
|
|
@@ -40,8 +59,21 @@ export interface TestDriver<TContext = unknown> {
|
|
|
40
59
|
assertOptions(label: string, optionLabels: string[]): Promise<void>;
|
|
41
60
|
assertPath(path: string, opts?: AssertPathOptions): Promise<void>;
|
|
42
61
|
refutePath(path: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Run `trigger` and assert the browser offers a download whose suggested
|
|
64
|
+
* filename matches `expected`. Browser-only: the RTL adapter throws.
|
|
65
|
+
*/
|
|
66
|
+
assertDownload(expected: string | RegExp, trigger: () => Promise<void>, opts?: DownloadOptions): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Poll `predicate` until it returns something truthy, or fail once the
|
|
69
|
+
* timeout is spent. `description` is what the chain trace prints, so it
|
|
70
|
+
* has to say what is being awaited in plain language.
|
|
71
|
+
*/
|
|
72
|
+
until(description: string, predicate: UntilPredicate<TContext>, opts?: UntilOptions): Promise<void>;
|
|
43
73
|
step(fn: (context: TContext) => Promise<unknown>): Promise<void>;
|
|
44
|
-
|
|
74
|
+
/** Hand the caller the adapter's own driving handle, untyped by the DSL. */
|
|
75
|
+
raw(fn: (native: TNative) => unknown | Promise<unknown>): Promise<void>;
|
|
76
|
+
within(selector: string): Promise<TestDriver<TContext, TNative>>;
|
|
45
77
|
debug(): Promise<void>;
|
|
46
78
|
/**
|
|
47
79
|
* Optional hook: wrap a queued step's execution (e.g. in Playwright's
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf;AAED
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feather-testing-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Phoenix Test-inspired fluent testing DSL for Playwright and React Testing Library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"types": "./dist/rtl/index.d.ts",
|
|
33
33
|
"import": "./dist/rtl/index.js",
|
|
34
34
|
"default": "./dist/rtl/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./eslint-plugin": {
|
|
37
|
+
"types": "./dist/eslint-plugin/index.d.ts",
|
|
38
|
+
"import": "./dist/eslint-plugin/index.js",
|
|
39
|
+
"default": "./dist/eslint-plugin/index.js"
|
|
35
40
|
}
|
|
36
41
|
},
|
|
37
42
|
"files": [
|
|
@@ -57,7 +62,8 @@
|
|
|
57
62
|
"build": "tsc",
|
|
58
63
|
"test": "vitest run",
|
|
59
64
|
"test:pw": "playwright test",
|
|
60
|
-
"test:all": "vitest run && playwright test"
|
|
65
|
+
"test:all": "vitest run && playwright test",
|
|
66
|
+
"lint": "npm run build && eslint ."
|
|
61
67
|
},
|
|
62
68
|
"devDependencies": {
|
|
63
69
|
"@playwright/test": "^1.58.0",
|
|
@@ -67,6 +73,8 @@
|
|
|
67
73
|
"@types/node": "^26.1.2",
|
|
68
74
|
"@types/react": "^19.2.14",
|
|
69
75
|
"@types/react-dom": "^19.2.3",
|
|
76
|
+
"@typescript-eslint/parser": "^8.68.0",
|
|
77
|
+
"eslint": "^9.39.5",
|
|
70
78
|
"jsdom": "^28.1.0",
|
|
71
79
|
"react": "^19.2.4",
|
|
72
80
|
"react-dom": "^19.2.4",
|