feather-testing-core 0.2.0 → 0.3.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 CHANGED
@@ -172,6 +172,20 @@ Every method returns `this` for chaining. A single `await` at the start of the c
172
172
  | `upload(label, path)` | Set a file input (found by label) to the file at `path` |
173
173
  | `dropFile(selector, path)` | Dispatch a `DataTransfer` drop of the file onto a drop area |
174
174
 
175
+ #### Interactions address controls **exactly**
176
+
177
+ Every interaction above names the control it wants, and that name is matched in full: `clickButton("Check")` clicks the button named *Check*, never the sidebar chip named *"Checklist Run — checklist"*. Whitespace is still normalized, so multi-line markup and padded labels keep working.
178
+
179
+ This matters because Playwright's bare-string matchers are case-insensitive *substring* matchers. Left as-is, a verb aimed at one control silently widens to any other control whose name merely contains the same text — and the run dies on a strict-mode violation that only appears when both are on screen at once, which turns a naming collision into an ordering-dependent flake. RTL matches whole strings by default, so with this both adapters answer the same question.
180
+
181
+ Assertions are the deliberate exception: `assertText` / `refuteText` / `assertHas` ask *"does this text appear"*, so they stay substring matches. An exact `refuteText("Check")` would pass while *Checklist Run* is plainly on the page.
182
+
183
+ To act on a control whose name is genuinely a prefix of another's, scope the lookup rather than loosening it:
184
+
185
+ ```ts
186
+ await session.within("main", (s) => s.clickButton("Check"));
187
+ ```
188
+
175
189
  #### How `submit()` finds the submit button
176
190
 
177
191
  `submit()` tracks the `<form>` element from the last `fillIn`, `selectOption`, `check`, `uncheck`, or `choose` call, then uses this strategy:
@@ -392,6 +406,36 @@ The RTL adapter runs in JSDOM, which has no real browser. These methods are not
392
406
  - `assertPath()` / `refutePath()` — no URL in JSDOM
393
407
  - `assertHas()` / `refuteHas()` — RTL discourages CSS selectors; use `assertText()` instead
394
408
 
409
+ ### Extending the RTL adapter
410
+
411
+ `RTLDriver` is meant to be subclassed when an app's markup needs a different lookup, so that a host harness binds *this* DSL rather than reimplementing it. Everything worth specializing is `protected`:
412
+
413
+ | Member | Why you'd override it |
414
+ |--------|----------------------|
415
+ | `findField(label)` | The single label-addressed lookup. Every labelled verb — `fillIn`, `selectOption`, `check`, `uncheck`, `upload`, `assertValue`, `assertChecked`, `assertSelected`, `assertOptions` — goes through it, so one override retargets them all |
416
+ | `scoped(element)` | Factory used by `within()`, so a scoped session keeps your driver's behaviour |
417
+ | `user`, `root`, `container`, `lastFormElement`, `timeout` | Shared state the built-in verbs read and write |
418
+
419
+ ```ts
420
+ class WrapperLabelDriver extends RTLDriver {
421
+ // Labels with no htmlFor, control is a sibling inside a wrapper div
422
+ protected override async findField(label: string): Promise<HTMLElement> {
423
+ for (const l of this.rootElement().querySelectorAll("label")) {
424
+ if (l.textContent?.trim() !== label) continue;
425
+ const control = l.parentElement?.querySelector("input, textarea, select");
426
+ if (control) return control as HTMLElement;
427
+ }
428
+ throw new Error(`no field labelled '${label}'`);
429
+ }
430
+
431
+ protected override scoped(element: HTMLElement) {
432
+ return new WrapperLabelDriver(this.user, element, this.timeout);
433
+ }
434
+ }
435
+ ```
436
+
437
+ The third constructor argument is a per-lookup timeout in ms; omit it to keep RTL's own default.
438
+
395
439
  ## Exports
396
440
 
397
441
  ```ts
@@ -15,6 +15,8 @@ export declare class PlaywrightDriver implements TestDriver<PlaywrightStepContex
15
15
  click(text: string): Promise<void>;
16
16
  clickLink(text: string): Promise<void>;
17
17
  clickButton(text: string): Promise<void>;
18
+ /** The single label-addressed lookup: every labelled verb goes through it. */
19
+ private labelled;
18
20
  private fieldByLabelOrPlaceholder;
19
21
  fillIn(label: string, value: string): Promise<void>;
20
22
  selectOption(label: string, option: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/playwright/driver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/playwright/driver.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,21 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { basename } from "node:path";
3
3
  import { expect, test } from "@playwright/test";
4
+ /**
5
+ * Playwright matches a bare string name/label/text as a case-insensitive
6
+ * SUBSTRING. That makes every text-addressed verb ambient: `clickButton('Check')`
7
+ * also matches an unrelated "Checklist Run — checklist" control that happens to
8
+ * be on the page, and the run dies on a strict-mode violation whose appearance
9
+ * depends on what else rendered. When a spec names a control it means *that*
10
+ * control, so every addressing matcher here passes `exact: true`. Playwright
11
+ * still normalizes whitespace under exact matching, so multi-line markup and
12
+ * padded labels keep working.
13
+ *
14
+ * Assertions (assertText/refuteText/assertHas) deliberately stay substring:
15
+ * they ask "does this text appear", and an exact `refuteText` would pass while
16
+ * the text is plainly on the page inside a longer string.
17
+ */
18
+ const EXACT = { exact: true };
4
19
  export class PlaywrightDriver {
5
20
  page;
6
21
  scope;
@@ -13,23 +28,25 @@ export class PlaywrightDriver {
13
28
  await this.page.goto(path);
14
29
  }
15
30
  async click(text) {
16
- await this.scope.getByText(text).click();
31
+ await this.scope.getByText(text, EXACT).click();
17
32
  }
18
33
  async clickLink(text) {
19
- await this.scope.getByRole("link", { name: text }).click();
34
+ await this.scope.getByRole("link", { name: text, ...EXACT }).click();
20
35
  }
21
36
  async clickButton(text) {
22
- await this.scope.getByRole("button", { name: text }).click();
37
+ await this.scope.getByRole("button", { name: text, ...EXACT }).click();
38
+ }
39
+ /** The single label-addressed lookup: every labelled verb goes through it. */
40
+ labelled(label) {
41
+ return this.scope.getByLabel(label, EXACT);
23
42
  }
24
43
  fieldByLabelOrPlaceholder(label) {
25
44
  // .or() lets Playwright auto-wait on whichever appears, so
26
45
  // async-rendered labeled fields don't fall through to the
27
- // placeholder branch. Placeholder matching is exact — substring
46
+ // placeholder branch. Both branches match exactly — substring
28
47
  // matching would collide with labels ("Name" vs placeholder
29
48
  // "Nickname") and trip strict mode.
30
- return this.scope
31
- .getByLabel(label)
32
- .or(this.scope.getByPlaceholder(label, { exact: true }));
49
+ return this.labelled(label).or(this.scope.getByPlaceholder(label, EXACT));
33
50
  }
34
51
  async fillIn(label, value) {
35
52
  const field = this.fieldByLabelOrPlaceholder(label);
@@ -37,22 +54,22 @@ export class PlaywrightDriver {
37
54
  this.lastFormLocator = this.scope.locator("form", { has: field });
38
55
  }
39
56
  async selectOption(label, option) {
40
- const select = this.scope.getByLabel(label);
57
+ const select = this.labelled(label);
41
58
  await select.selectOption({ label: option });
42
59
  this.lastFormLocator = this.scope.locator("form", { has: select });
43
60
  }
44
61
  async check(label) {
45
- const checkbox = this.scope.getByLabel(label);
62
+ const checkbox = this.labelled(label);
46
63
  await checkbox.check();
47
64
  this.lastFormLocator = this.scope.locator("form", { has: checkbox });
48
65
  }
49
66
  async uncheck(label) {
50
- const checkbox = this.scope.getByLabel(label);
67
+ const checkbox = this.labelled(label);
51
68
  await checkbox.uncheck();
52
69
  this.lastFormLocator = this.scope.locator("form", { has: checkbox });
53
70
  }
54
71
  async choose(label) {
55
- const radio = this.scope.getByRole("radio", { name: label });
72
+ const radio = this.scope.getByRole("radio", { name: label, ...EXACT });
56
73
  await radio.check();
57
74
  this.lastFormLocator = this.scope.locator("form", { has: radio });
58
75
  }
@@ -83,7 +100,7 @@ export class PlaywrightDriver {
83
100
  }
84
101
  }
85
102
  async upload(label, path) {
86
- const input = this.scope.getByLabel(label);
103
+ const input = this.labelled(label);
87
104
  await input.setInputFiles(path);
88
105
  this.lastFormLocator = this.scope.locator("form", { has: input });
89
106
  }
@@ -133,17 +150,17 @@ export class PlaywrightDriver {
133
150
  await expect(this.fieldByLabelOrPlaceholder(label)).toHaveValue(value);
134
151
  }
135
152
  async assertChecked(label) {
136
- await expect(this.scope.getByLabel(label)).toBeChecked();
153
+ await expect(this.labelled(label)).toBeChecked();
137
154
  }
138
155
  async refuteChecked(label) {
139
- await expect(this.scope.getByLabel(label)).not.toBeChecked();
156
+ await expect(this.labelled(label)).not.toBeChecked();
140
157
  }
141
158
  async assertSelected(label, optionLabel) {
142
- const select = this.scope.getByLabel(label);
159
+ const select = this.labelled(label);
143
160
  await expect(select.locator("option:checked")).toHaveText(optionLabel);
144
161
  }
145
162
  async assertOptions(label, optionLabels) {
146
- const select = this.scope.getByLabel(label);
163
+ const select = this.labelled(label);
147
164
  await expect(select.locator("option")).toHaveText(optionLabels);
148
165
  }
149
166
  async assertPath(path, opts) {
@@ -10,14 +10,36 @@ export interface RTLStepContext {
10
10
  /**
11
11
  * RTL adapter implementing the subset of TestDriver that applies in JSDOM.
12
12
  * Navigation methods (visit, assertPath, refutePath) are not supported.
13
+ *
14
+ * Everything a host adapter is likely to specialize is `protected`: the
15
+ * label-to-control lookup (`findField`), the scoped-driver factory
16
+ * (`scoped`), and the `user` / `root` / `lastFormElement` state the verbs
17
+ * share. Subclass it rather than reimplementing the DSL when an app's markup
18
+ * needs a different lookup — that is how feather-testing-postgres binds this
19
+ * driver to markup whose labels are wrapper siblings rather than `htmlFor`
20
+ * targets.
13
21
  */
14
22
  export declare class RTLDriver implements TestDriver<RTLStepContext> {
15
- private user;
16
- private container;
17
- private lastFormElement;
18
- constructor(user?: UserEvent, container?: HTMLElement);
19
- private rootElement;
20
- private findFieldByLabelOrPlaceholder;
23
+ protected user: UserEvent;
24
+ /** The element every query and selector in this driver resolves against. */
25
+ protected root: HTMLElement;
26
+ protected container: ReturnType<typeof rtlWithin>;
27
+ protected lastFormElement: HTMLFormElement | null;
28
+ /** Per-lookup timeout in ms; undefined leaves RTL's own default in place. */
29
+ protected timeout: number | undefined;
30
+ constructor(user?: UserEvent, container?: HTMLElement, timeout?: number);
31
+ protected rootElement(): HTMLElement;
32
+ /** Options forwarded to every async query and waitFor call. */
33
+ protected waitOpts(): {
34
+ timeout?: number;
35
+ };
36
+ /**
37
+ * The single label-addressed lookup: every labelled verb goes through it,
38
+ * so overriding this one method retargets them all.
39
+ */
40
+ protected findField(label: string): Promise<HTMLElement>;
41
+ /** Subclasses override this so within() yields a driver of their own type. */
42
+ protected scoped(element: HTMLElement): TestDriver<RTLStepContext>;
21
43
  visit(): Promise<void>;
22
44
  click(text: string): Promise<void>;
23
45
  clickLink(text: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/rtl/driver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/rtl/driver.ts"],"names":[],"mappings":"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"}
@@ -3,52 +3,76 @@ import userEvent from "@testing-library/user-event";
3
3
  /**
4
4
  * RTL adapter implementing the subset of TestDriver that applies in JSDOM.
5
5
  * Navigation methods (visit, assertPath, refutePath) are not supported.
6
+ *
7
+ * Everything a host adapter is likely to specialize is `protected`: the
8
+ * label-to-control lookup (`findField`), the scoped-driver factory
9
+ * (`scoped`), and the `user` / `root` / `lastFormElement` state the verbs
10
+ * share. Subclass it rather than reimplementing the DSL when an app's markup
11
+ * needs a different lookup — that is how feather-testing-postgres binds this
12
+ * driver to markup whose labels are wrapper siblings rather than `htmlFor`
13
+ * targets.
6
14
  */
7
15
  export class RTLDriver {
8
16
  user;
17
+ /** The element every query and selector in this driver resolves against. */
18
+ root;
9
19
  container;
10
20
  lastFormElement = null;
11
- constructor(user, container) {
21
+ /** Per-lookup timeout in ms; undefined leaves RTL's own default in place. */
22
+ timeout;
23
+ constructor(user, container, timeout) {
12
24
  this.user = user ?? userEvent.setup();
13
- this.container = container ? rtlWithin(container) : screen;
25
+ this.root = container ?? document.body;
26
+ this.container = rtlWithin(this.root);
27
+ this.timeout = timeout;
14
28
  }
15
29
  rootElement() {
16
- return this.container === screen
17
- ? document.body
18
- : (this.container
19
- .container ?? document.body);
30
+ return this.root;
20
31
  }
21
- async findFieldByLabelOrPlaceholder(label) {
32
+ /** Options forwarded to every async query and waitFor call. */
33
+ waitOpts() {
34
+ return this.timeout === undefined ? {} : { timeout: this.timeout };
35
+ }
36
+ /**
37
+ * The single label-addressed lookup: every labelled verb goes through it,
38
+ * so overriding this one method retargets them all.
39
+ */
40
+ async findField(label) {
22
41
  try {
23
- return await this.container.findByLabelText(label);
42
+ return await this.container.findByLabelText(label, undefined, this.waitOpts());
24
43
  }
25
44
  catch {
26
- return await this.container.findByPlaceholderText(label);
45
+ return await this.container.findByPlaceholderText(label, undefined, this.waitOpts());
27
46
  }
28
47
  }
48
+ /** Subclasses override this so within() yields a driver of their own type. */
49
+ scoped(element) {
50
+ return new RTLDriver(this.user, element, this.timeout);
51
+ }
29
52
  async visit() {
30
53
  throw new Error("visit() is not available in the RTL adapter. Render the desired component directly.");
31
54
  }
32
55
  async click(text) {
33
- const element = await this.container.findByText(text);
56
+ const element = await this.container.findByText(text, undefined, this.waitOpts());
34
57
  await this.user.click(element);
35
58
  }
36
59
  async clickLink(text) {
37
- const link = await this.container.findByRole("link", { name: text });
60
+ const link = await this.container.findByRole("link", { name: text }, this.waitOpts());
38
61
  await this.user.click(link);
39
62
  }
40
63
  async clickButton(text) {
41
- const button = await this.container.findByRole("button", { name: text });
64
+ const button = await this.container.findByRole("button", { name: text }, this.waitOpts());
42
65
  await this.user.click(button);
43
66
  }
44
67
  async fillIn(label, value) {
45
- const input = await this.findFieldByLabelOrPlaceholder(label);
68
+ const input = await this.findField(label);
46
69
  await this.user.clear(input);
47
- await this.user.type(input, value);
70
+ if (value)
71
+ await this.user.type(input, value);
48
72
  this.lastFormElement = input.closest("form");
49
73
  }
50
74
  async selectOption(label, option) {
51
- const select = await this.container.findByLabelText(label);
75
+ const select = await this.findField(label);
52
76
  const optionEl = Array.from(select.querySelectorAll("option")).find((o) => o.textContent?.trim() === option);
53
77
  if (!optionEl) {
54
78
  throw new Error(`selectOption('${label}', '${option}'): no <option> with text '${option}' found.`);
@@ -57,21 +81,21 @@ export class RTLDriver {
57
81
  this.lastFormElement = select.closest("form");
58
82
  }
59
83
  async check(label) {
60
- const checkbox = await this.container.findByLabelText(label);
84
+ const checkbox = await this.findField(label);
61
85
  if (!checkbox.checked) {
62
86
  await this.user.click(checkbox);
63
87
  }
64
88
  this.lastFormElement = checkbox.closest("form");
65
89
  }
66
90
  async uncheck(label) {
67
- const checkbox = await this.container.findByLabelText(label);
91
+ const checkbox = await this.findField(label);
68
92
  if (checkbox.checked) {
69
93
  await this.user.click(checkbox);
70
94
  }
71
95
  this.lastFormElement = checkbox.closest("form");
72
96
  }
73
97
  async choose(label) {
74
- const radio = await this.container.findByRole("radio", { name: label });
98
+ const radio = await this.container.findByRole("radio", { name: label }, this.waitOpts());
75
99
  await this.user.click(radio);
76
100
  this.lastFormElement = radio.closest("form");
77
101
  }
@@ -93,7 +117,7 @@ export class RTLDriver {
93
117
  }
94
118
  }
95
119
  async upload(label, path) {
96
- const input = await this.container.findByLabelText(label);
120
+ const input = await this.findField(label);
97
121
  // JSDOM has no filesystem access; synthesize a File from the basename.
98
122
  const name = path.split(/[\\/]/).pop() ?? path;
99
123
  const file = new File([""], name);
@@ -122,7 +146,7 @@ export class RTLDriver {
122
146
  throw new Error("refuteHas() with CSS selectors is not recommended in RTL. Use refuteText() instead.");
123
147
  }
124
148
  async assertText(text) {
125
- await this.container.findByText(text);
149
+ await this.container.findByText(text, undefined, this.waitOpts());
126
150
  }
127
151
  async refuteText(text) {
128
152
  await waitFor(() => {
@@ -130,44 +154,44 @@ export class RTLDriver {
130
154
  if (el) {
131
155
  throw new Error(`Expected NOT to find text '${text}', but it was present.`);
132
156
  }
133
- });
157
+ }, this.waitOpts());
134
158
  }
135
159
  async assertValue(label, value) {
136
- const field = await this.findFieldByLabelOrPlaceholder(label);
160
+ const field = await this.findField(label);
137
161
  await waitFor(() => {
138
162
  const actual = field.value;
139
163
  if (actual !== value) {
140
164
  throw new Error(`assertValue('${label}', '${value}'): expected value '${value}', but found '${actual}'.`);
141
165
  }
142
- });
166
+ }, this.waitOpts());
143
167
  }
144
168
  async assertChecked(label) {
145
- const checkbox = await this.container.findByLabelText(label);
169
+ const checkbox = await this.findField(label);
146
170
  await waitFor(() => {
147
171
  if (!checkbox.checked) {
148
172
  throw new Error(`assertChecked('${label}'): expected checkbox to be checked, but it was not.`);
149
173
  }
150
- });
174
+ }, this.waitOpts());
151
175
  }
152
176
  async refuteChecked(label) {
153
- const checkbox = await this.container.findByLabelText(label);
177
+ const checkbox = await this.findField(label);
154
178
  await waitFor(() => {
155
179
  if (checkbox.checked) {
156
180
  throw new Error(`refuteChecked('${label}'): expected checkbox NOT to be checked, but it was.`);
157
181
  }
158
- });
182
+ }, this.waitOpts());
159
183
  }
160
184
  async assertSelected(label, optionLabel) {
161
- const select = (await this.container.findByLabelText(label));
185
+ const select = (await this.findField(label));
162
186
  await waitFor(() => {
163
187
  const selected = select.selectedOptions[0]?.textContent?.trim();
164
188
  if (selected !== optionLabel) {
165
189
  throw new Error(`assertSelected('${label}', '${optionLabel}'): expected selected option '${optionLabel}', but found '${selected ?? "(none)"}'.`);
166
190
  }
167
- });
191
+ }, this.waitOpts());
168
192
  }
169
193
  async assertOptions(label, optionLabels) {
170
- const select = (await this.container.findByLabelText(label));
194
+ const select = (await this.findField(label));
171
195
  await waitFor(() => {
172
196
  const actual = Array.from(select.querySelectorAll("option")).map((o) => o.textContent?.trim() ?? "");
173
197
  const matches = actual.length === optionLabels.length &&
@@ -175,7 +199,7 @@ export class RTLDriver {
175
199
  if (!matches) {
176
200
  throw new Error(`assertOptions('${label}'): expected options [${optionLabels.join(", ")}], but found [${actual.join(", ")}].`);
177
201
  }
178
- });
202
+ }, this.waitOpts());
179
203
  }
180
204
  async assertPath() {
181
205
  throw new Error("assertPath() is not available in the RTL adapter (no real URL in JSDOM).");
@@ -190,9 +214,9 @@ export class RTLDriver {
190
214
  const element = this.rootElement().querySelector(selector);
191
215
  if (!element)
192
216
  throw new Error(`within('${selector}'): element not found`);
193
- return new RTLDriver(this.user, element);
217
+ return this.scoped(element);
194
218
  }
195
219
  async debug() {
196
- screen.debug();
220
+ screen.debug(this.rootElement());
197
221
  }
198
222
  }
package/dist/session.d.ts CHANGED
@@ -38,7 +38,12 @@ export declare class Session<TContext = unknown> implements PromiseLike<void> {
38
38
  * StepError output like any built-in step.
39
39
  */
40
40
  step(name: string, fn: (context: TContext) => Promise<unknown>): this;
41
- within(selector: string, fn: (scoped: Session<TContext>) => Session<TContext>): this;
41
+ /**
42
+ * `fn` must either return the scoped session — so its queued steps run — or
43
+ * a promise it already awaited. Returning anything else would silently drop
44
+ * the scoped chain, which is why the callback's return type is not `unknown`.
45
+ */
46
+ within(selector: string, fn: (scoped: Session<TContext>) => Session<TContext> | PromiseLike<unknown>): this;
42
47
  debug(): this;
43
48
  }
44
49
  //# 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":"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;;;;OAIG;IACH,MAAM,CACJ,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,GAC1E,IAAI;IAUP,KAAK,IAAI,IAAI;CAGd"}
package/dist/session.js CHANGED
@@ -120,6 +120,11 @@ export class Session {
120
120
  return this.enqueue(`step('${name}')`, () => this.driver.step(fn));
121
121
  }
122
122
  // --- Scoping ---
123
+ /**
124
+ * `fn` must either return the scoped session — so its queued steps run — or
125
+ * a promise it already awaited. Returning anything else would silently drop
126
+ * the scoped chain, which is why the callback's return type is not `unknown`.
127
+ */
123
128
  within(selector, fn) {
124
129
  return this.enqueue(`within('${selector}')`, async () => {
125
130
  const scopedDriver = await this.driver.within(selector);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feather-testing-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Phoenix Test-inspired fluent testing DSL for Playwright and React Testing Library",
5
5
  "license": "MIT",
6
6
  "repository": {