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.
Files changed (43) hide show
  1. package/README.md +177 -4
  2. package/dist/errors.d.ts +8 -0
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +12 -0
  5. package/dist/eslint-plugin/index.d.ts +18 -0
  6. package/dist/eslint-plugin/index.d.ts.map +1 -0
  7. package/dist/eslint-plugin/index.js +37 -0
  8. package/dist/eslint-plugin/rules/no-conditional-skip.d.ts +4 -0
  9. package/dist/eslint-plugin/rules/no-conditional-skip.d.ts.map +1 -0
  10. package/dist/eslint-plugin/rules/no-conditional-skip.js +56 -0
  11. package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.d.ts +4 -0
  12. package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.d.ts.map +1 -0
  13. package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.js +63 -0
  14. package/dist/eslint-plugin/rules/no-wait-for-timeout.d.ts +4 -0
  15. package/dist/eslint-plugin/rules/no-wait-for-timeout.d.ts.map +1 -0
  16. package/dist/eslint-plugin/rules/no-wait-for-timeout.js +69 -0
  17. package/dist/eslint-plugin/rules/no-weak-assertions.d.ts +4 -0
  18. package/dist/eslint-plugin/rules/no-weak-assertions.d.ts.map +1 -0
  19. package/dist/eslint-plugin/rules/no-weak-assertions.js +73 -0
  20. package/dist/eslint-plugin/rules/warn-serial-mode.d.ts +4 -0
  21. package/dist/eslint-plugin/rules/warn-serial-mode.d.ts.map +1 -0
  22. package/dist/eslint-plugin/rules/warn-serial-mode.js +59 -0
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/playwright/driver.d.ts +18 -3
  27. package/dist/playwright/driver.d.ts.map +1 -1
  28. package/dist/playwright/driver.js +82 -18
  29. package/dist/playwright/index.d.ts +4 -4
  30. package/dist/playwright/index.d.ts.map +1 -1
  31. package/dist/playwright/index.js +1 -1
  32. package/dist/rtl/driver.d.ts +55 -9
  33. package/dist/rtl/driver.d.ts.map +1 -1
  34. package/dist/rtl/driver.js +135 -35
  35. package/dist/rtl/index.d.ts +5 -4
  36. package/dist/rtl/index.d.ts.map +1 -1
  37. package/dist/rtl/index.js +2 -2
  38. package/dist/session.d.ts +41 -4
  39. package/dist/session.d.ts.map +1 -1
  40. package/dist/session.js +72 -1
  41. package/dist/types.d.ts +35 -3
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +10 -2
@@ -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
- constructor(user, container) {
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.container = container ? rtlWithin(container) : screen;
26
+ this.root = container ?? document.body;
27
+ this.container = rtlWithin(this.root);
28
+ this.timeout = timeout;
14
29
  }
15
30
  rootElement() {
16
- return this.container === screen
17
- ? document.body
18
- : (this.container
19
- .container ?? document.body);
31
+ return this.root;
20
32
  }
21
- async findFieldByLabelOrPlaceholder(label) {
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.findFieldByLabelOrPlaceholder(label);
69
+ const input = await this.findField(label);
46
70
  await this.user.clear(input);
47
- await this.user.type(input, value);
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.container.findByLabelText(label);
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.container.findByLabelText(label);
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.container.findByLabelText(label);
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 upload(label, path) {
96
- const input = await this.container.findByLabelText(label);
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.findFieldByLabelOrPlaceholder(label);
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.container.findByLabelText(label);
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.container.findByLabelText(label);
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.container.findByLabelText(label));
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.container.findByLabelText(label));
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({ user: this.user, container: this.container });
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 new RTLDriver(this.user, element);
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
  }
@@ -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 declare function createSession(): Session<RTLStepContext>;
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
@@ -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;AAE7D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7D,wBAAgB,aAAa,IAAI,OAAO,CAAC,cAAc,CAAC,CAEvD"}
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
- within(selector: string, fn: (scoped: Session<TContext>) => Session<TContext>): this;
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
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,UAAU,EACX,MAAM,YAAY,CAAC;AAGpB,qBAAa,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAE,YAAW,WAAW,CAAC,IAAI,CAAC;IAKvD,OAAO,CAAC,MAAM;IAJ1B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,SAAS,CAAK;gBAEF,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC;IAEhD,IAAI,CAAC,QAAQ,GAAG,IAAI,EAAE,QAAQ,GAAG,KAAK,EACpC,WAAW,CAAC,EACR,CAAC,CAAC,KAAK,EAAE,IAAI,KAAK,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,GACnD,IAAI,EACR,UAAU,CAAC,EACP,CAAC,CAAC,MAAM,EAAE,OAAO,KAAK,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,GACvD,IAAI,GACP,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;YAIjB,YAAY;IAuB1B,OAAO,CAAC,OAAO;IAOf,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMzB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIzB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM7B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM/B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAM1C,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAMjD,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM3B,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAMzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQ9C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM9B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM9B,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAM/C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAMlC,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAMlC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAOxD,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI;IAO1D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAO1D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAO1D,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,IAAI;IAMxD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQ9B;;;;;OAKG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI;IAMrE,MAAM,CACJ,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,GACnD,IAAI;IAUP,KAAK,IAAI,IAAI;CAGd"}
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(label, path));
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
- upload(label: string, path: string): Promise<void>;
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
- within(selector: string): Promise<TestDriver<TContext>>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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.2.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",