feather-testing-core 0.3.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 +133 -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 +16 -3
  27. package/dist/playwright/driver.d.ts.map +1 -1
  28. package/dist/playwright/driver.js +49 -2
  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 +28 -4
  33. package/dist/rtl/driver.d.ts.map +1 -1
  34. package/dist/rtl/driver.js +78 -2
  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 +36 -4
  39. package/dist/session.d.ts.map +1 -1
  40. package/dist/session.js +67 -1
  41. package/dist/types.d.ts +35 -3
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +10 -2
package/README.md CHANGED
@@ -169,8 +169,12 @@ Every method returns `this` for chaining. A single `await` at the start of the c
169
169
  | `check(label)` / `uncheck(label)` | Toggle checkbox by label |
170
170
  | `choose(label)` | Select radio button by label |
171
171
  | `submit()` | Submit the most recently interacted form (see below) |
172
- | `upload(label, path)` | Set a file input (found by label) to the file at `path` |
172
+ | `attachFile(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
+ | `pressKey(key)` | Press a key on the focused control — `'Enter'`, `'Escape'`, `'Control+A'` |
175
+ | `hover(text)` | Hover the element with this text |
176
+
177
+ `upload(label, path)` is the former name of `attachFile` and still works, deprecated.
174
178
 
175
179
  #### Interactions address controls **exactly**
176
180
 
@@ -200,7 +204,7 @@ If no form was previously interacted with, `submit()` throws an error.
200
204
 
201
205
  ```ts
202
206
  // Standard file input, found by its label
203
- await session.upload("Avatar", "fixtures/avatar.png");
207
+ await session.attachFile("Avatar", "fixtures/avatar.png");
204
208
 
205
209
  // Custom drop area (drag-and-drop upload zones)
206
210
  await session.dropFile("#dropzone", "fixtures/report.pdf");
@@ -208,6 +212,10 @@ await session.dropFile("#dropzone", "fixtures/report.pdf");
208
212
 
209
213
  In Playwright, `dropFile` reads the real file and dispatches a `drop` event with a `DataTransfer`. In RTL (JSDOM has no filesystem), both verbs synthesize an empty `File` named after the path's basename — assert on the file name, not its contents.
210
214
 
215
+ #### Keys
216
+
217
+ `pressKey` names keys the Playwright way on both adapters: a single character types itself, a named key is `'Enter'` / `'Escape'` / `'ArrowDown'`, and modifiers combine with `+` (`'Control+A'`, `'Meta+Enter'`). The RTL adapter translates that to user-event's keyboard syntax, so one spec reads the same in both places. A prefix that is not `Control`, `Shift`, `Alt`, or `Meta` is rejected rather than typed as text.
218
+
211
219
  ### Assertions
212
220
 
213
221
  | Method | Description |
@@ -219,6 +227,7 @@ In Playwright, `dropFile` reads the real file and dispatches a `drop` event with
219
227
  | `assertOptions(label, [labels])` | Assert a select offers exactly these options, in order |
220
228
  | `assertHas(selector, opts?)` / `refuteHas(...)` | Assert element exists (Playwright only, see options below) |
221
229
  | `assertPath(path, opts?)` / `refutePath(path)` | Assert URL path (Playwright only, see options below) |
230
+ | `assertDownload(filename, trigger, opts?)` | Assert `trigger` starts a download with this filename (Playwright only) |
222
231
 
223
232
  #### Form-state assertions
224
233
 
@@ -293,6 +302,47 @@ await session.assertPath("/search", { queryParams: { q: "hello", page: "1" } });
293
302
  await session.refutePath("/login");
294
303
  ```
295
304
 
305
+ #### Downloads
306
+
307
+ The wait has to be armed before the click that starts the download, so the triggering steps go in a callback — the same shape as `within()`:
308
+
309
+ ```ts
310
+ await session
311
+ .visit("/exports")
312
+ .assertDownload("report.csv", (s) => s.clickButton("Export"))
313
+ .assertText("Export complete");
314
+ ```
315
+
316
+ `filename` is matched against the browser's suggested filename, exactly for a string or by `test()` for a `RegExp`. `opts.timeout` bounds the wait for the download to start. This is browser-only: the RTL adapter throws a `BrowserOnlyVerbError`, wrapped by the chain into a `StepError` that names the step.
317
+
318
+ ### Waiting: `until(description, fn)`
319
+
320
+ | Method | Description |
321
+ |--------|-------------|
322
+ | `until(description, fn, opts?)` | Poll `fn` until it returns something truthy, then continue |
323
+
324
+ Tests wait for conditions, not for clocks. `until()` is the honest alternative to a sleep: it polls a predicate you write, and the **mandatory** description is what the trace prints, so a timeout names the thing you were waiting for instead of the mechanism you waited with.
325
+
326
+ ```ts
327
+ await session
328
+ .visit("/exports")
329
+ .clickButton("Export")
330
+ .until("the export job reports done", ({ page }) =>
331
+ page.evaluate(() => window.__exportDone),
332
+ )
333
+ .assertText("Download ready");
334
+ ```
335
+
336
+ The predicate receives the adapter context — `{ page, scope }` for Playwright, `{ user, container }` for RTL — and may be sync or async. `opts` takes `{ timeout, interval }` in ms; omit them to inherit the adapter's own budget (Playwright's `expect.poll`, RTL's `waitFor`).
337
+
338
+ When the budget runs out, the chain trace says what you were waiting for:
339
+
340
+ ```
341
+ >>> [FAILED] until: the export job reports done
342
+ ```
343
+
344
+ The description is required at the call site, before the chain runs — a blank one throws immediately, because a step named `until: ` teaches a reader nothing. The `feather-testing/no-wait-for-timeout` lint rule (see [Lint plugin](#lint-plugin)) points at this verb, so "no sleeps" stops being a review convention and becomes a check.
345
+
296
346
  ### Scoping
297
347
 
298
348
  | Method | Description |
@@ -309,7 +359,7 @@ await session
309
359
  .assertText("Dashboard"); // back to full-page scope after within()
310
360
  ```
311
361
 
312
- ### Escape hatch: `step(name, fn)`
362
+ ### Escape hatches: `step(name, fn)` and `raw(label, fn)`
313
363
 
314
364
  When you need something the DSL doesn't cover, queue a named custom step instead of abandoning the chain. The callback receives the adapter's context — `{ page, scope }` for Playwright, `{ user, container }` for RTL — and the name shows up in `StepError` output like any built-in step:
315
365
 
@@ -322,6 +372,26 @@ await session
322
372
  .assertText("Done (1)");
323
373
  ```
324
374
 
375
+ `raw(label, fn)` goes one level lower: it hands over the driver itself — Playwright's `page`, RTL's scoped query object — with no context wrapper. Use it when you want the native API and nothing else:
376
+
377
+ ```ts
378
+ await session
379
+ .visit("/board")
380
+ .raw("stub the clipboard", (page) =>
381
+ page.evaluate(() => navigator.clipboard.writeText("copied")),
382
+ )
383
+ .clickButton("Paste")
384
+ .assertText("copied");
385
+ ```
386
+
387
+ Both hatches take a **mandatory** label and register as named steps, so a failure inside one still names intent:
388
+
389
+ ```
390
+ >>> [FAILED] raw('stub the clipboard')
391
+ ```
392
+
393
+ That is the whole point of having them: an untraced raw tail ends the trace at the last DSL verb, and no hatch at all pushes teams to abandon the DSL mid-spec. In Playwright, `raw` always hands over the page, not the `within()` scope — re-scoping is the caller's job once you have left the DSL.
394
+
325
395
  ### Debug
326
396
 
327
397
  | Method | Description |
@@ -405,6 +475,9 @@ The RTL adapter runs in JSDOM, which has no real browser. These methods are not
405
475
  - `visit()` — render the component directly instead
406
476
  - `assertPath()` / `refutePath()` — no URL in JSDOM
407
477
  - `assertHas()` / `refuteHas()` — RTL discourages CSS selectors; use `assertText()` instead
478
+ - `assertDownload()` — JSDOM has no download machinery; it throws `BrowserOnlyVerbError` naming the verb and pointing at a Playwright spec
479
+
480
+ The verbs JSDOM *can* honestly do, it does: `attachFile` synthesizes a `File` from the path's basename, `pressKey` translates to user-event's keyboard syntax, `hover` fires real pointer events, and `raw` hands over the scoped query object.
408
481
 
409
482
  ### Extending the RTL adapter
410
483
 
@@ -440,13 +513,21 @@ The third constructor argument is a per-lookup timeout in ms; omit it to keep RT
440
513
 
441
514
  ```ts
442
515
  // Core (Session class + types)
443
- import { Session, StepError, type TestDriver } from "feather-testing-core";
516
+ import {
517
+ Session,
518
+ StepError,
519
+ BrowserOnlyVerbError,
520
+ type TestDriver,
521
+ } from "feather-testing-core";
444
522
 
445
523
  // Playwright adapter
446
524
  import { test, createSession, expect } from "feather-testing-core/playwright";
447
525
 
448
526
  // RTL adapter
449
527
  import { createSession } from "feather-testing-core/rtl";
528
+
529
+ // ESLint plugin (see below)
530
+ import featherTesting from "feather-testing-core/eslint-plugin";
450
531
  ```
451
532
 
452
533
  Both adapter subpaths also re-export `Session` and `StepError`, so you can import everything from a single path:
@@ -456,6 +537,54 @@ import { test, Session, StepError } from "feather-testing-core/playwright";
456
537
  import { createSession, Session, StepError } from "feather-testing-core/rtl";
457
538
  ```
458
539
 
540
+ ## Lint plugin
541
+
542
+ The DSL can only offer good habits; a linter can insist on them. This package ships an ESLint plugin whose rules are the defect classes a real suite audit found by expensive reading — each one now a check that runs in a second, with a message that names the fix so whoever hits it (person or agent) learns the alternative from the error alone.
543
+
544
+ ```js
545
+ // eslint.config.js — flat config
546
+ import featherTesting from "feather-testing-core/eslint-plugin";
547
+
548
+ export default [
549
+ {
550
+ files: ["tests/**/*.ts", "e2e/**/*.spec.ts"],
551
+ ...featherTesting.configs.recommended,
552
+ },
553
+ ];
554
+ ```
555
+
556
+ Or wire the rules yourself:
557
+
558
+ ```js
559
+ import featherTesting from "feather-testing-core/eslint-plugin";
560
+
561
+ export default [
562
+ {
563
+ files: ["tests/**/*.ts"],
564
+ plugins: { "feather-testing": featherTesting },
565
+ rules: {
566
+ "feather-testing/no-weak-assertions": ["error", { matchers: ["toBeTruthy", "toBeDefined", "toBeFalsy"] }],
567
+ },
568
+ },
569
+ ];
570
+ ```
571
+
572
+ | Rule | Catches | Points at |
573
+ |------|---------|-----------|
574
+ | `no-wait-for-timeout` | `page.waitForTimeout(...)`, and the `new Promise(r => setTimeout(r, n))` sleep idiom | `session.until(description, fn)`, `expect.poll`, web-first assertions |
575
+ | `no-conditional-skip` | `test.skip(cond)`, `test.skip()`, `this.skip()` — a spec that un-tests itself at runtime | making the precondition part of the test, or `test.fixme` so the report names it |
576
+ | `no-weak-assertions` | `expect(x).toBeTruthy()` / `.toBeDefined()` (configurable) | asserting the shape you mean |
577
+ | `no-swallowed-cleanup-catch` | `.catch(() => {})` and empty `catch {}` blocks | asserting on the error, rethrowing with context, or annotating the deliberate ignore |
578
+ | `warn-serial-mode` | `test.describe.serial(...)`, `configure({ mode: "serial" })` — warning, not error | independent tests, or an `eslint-disable` line saying why serial is required |
579
+
580
+ Deliberate exceptions stay possible and stay visible: an `eslint-disable-next-line` comment with a reason is exactly the annotation these rules are trying to force.
581
+
582
+ **Why these five.** They are not style preferences. Each one is a way a suite goes green while proving nothing: a sleep passes on a slow machine and fails on a fast one, a conditional skip silently un-tests a spec for its entire life, `toBeTruthy()` accepts almost any value, a swallowed cleanup error surfaces three tests later as something else, and serial mode turns one failure into a wall of red that hides its own cause. `session.until()` exists so the first rule has an honest alternative to point at — see [the document set](docs/document-set.md) for why conventions belong in executable form rather than in a style guide nobody re-reads.
583
+
584
+ ## Documentation
585
+
586
+ - [The document set](docs/document-set.md) — the minimal set of documents a project needs, what each one answers, and why hand-maintained cross-reference matrices lose to generated reports plus CI checks.
587
+
459
588
  ## License
460
589
 
461
590
  MIT
package/dist/errors.d.ts CHANGED
@@ -2,4 +2,12 @@ import type { QueuedStep } from "./types.js";
2
2
  export declare class StepError extends Error {
3
3
  constructor(failedStep: QueuedStep, allSteps: QueuedStep[], cause: unknown);
4
4
  }
5
+ /**
6
+ * Thrown by an adapter asked for something only a real browser can do.
7
+ * A session wraps it in a StepError, so the chain trace names the verb that
8
+ * could not run and this message says why and what to do instead.
9
+ */
10
+ export declare class BrowserOnlyVerbError extends Error {
11
+ constructor(verb: string, alternative: string);
12
+ }
5
13
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,qBAAa,SAAU,SAAQ,KAAK;gBAEhC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,UAAU,EAAE,EACtB,KAAK,EAAE,OAAO;CA4BjB"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,qBAAa,SAAU,SAAQ,KAAK;gBAEhC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,UAAU,EAAE,EACtB,KAAK,EAAE,OAAO;CA4BjB;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;CAO9C"}
package/dist/errors.js CHANGED
@@ -19,3 +19,15 @@ export class StepError extends Error {
19
19
  this.name = "StepError";
20
20
  }
21
21
  }
22
+ /**
23
+ * Thrown by an adapter asked for something only a real browser can do.
24
+ * A session wraps it in a StepError, so the chain trace names the verb that
25
+ * could not run and this message says why and what to do instead.
26
+ */
27
+ export class BrowserOnlyVerbError extends Error {
28
+ constructor(verb, alternative) {
29
+ super(`feather-testing-core: ${verb} is a browser-only verb — this adapter runs ` +
30
+ `in JSDOM, which has no browser to do it. ${alternative}`);
31
+ this.name = "BrowserOnlyVerbError";
32
+ }
33
+ }
@@ -0,0 +1,18 @@
1
+ import type { Linter, Rule } from "eslint";
2
+ /**
3
+ * The defect classes an expensive suite audit found by reading, turned into
4
+ * checks. Every message names the fix, so an agent or a reviewer mid-task
5
+ * learns what to do from the error alone.
6
+ */
7
+ export declare const rules: Record<string, Rule.RuleModule>;
8
+ export declare const recommendedRules: Linter.RulesRecord;
9
+ export interface FeatherTestingPlugin {
10
+ meta: {
11
+ name: string;
12
+ };
13
+ rules: Record<string, Rule.RuleModule>;
14
+ configs: Record<string, Linter.Config>;
15
+ }
16
+ declare const plugin: FeatherTestingPlugin;
17
+ export default plugin;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint-plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAO3C;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAMjD,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,WAQrC,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;CACxC;AAED,QAAA,MAAM,MAAM,EAAE,oBAIb,CAAC;AAQF,eAAe,MAAM,CAAC"}
@@ -0,0 +1,37 @@
1
+ import noWaitForTimeout from "./rules/no-wait-for-timeout.js";
2
+ import noConditionalSkip from "./rules/no-conditional-skip.js";
3
+ import noWeakAssertions from "./rules/no-weak-assertions.js";
4
+ import noSwallowedCleanupCatch from "./rules/no-swallowed-cleanup-catch.js";
5
+ import warnSerialMode from "./rules/warn-serial-mode.js";
6
+ /**
7
+ * The defect classes an expensive suite audit found by reading, turned into
8
+ * checks. Every message names the fix, so an agent or a reviewer mid-task
9
+ * learns what to do from the error alone.
10
+ */
11
+ export const rules = {
12
+ "no-wait-for-timeout": noWaitForTimeout,
13
+ "no-conditional-skip": noConditionalSkip,
14
+ "no-weak-assertions": noWeakAssertions,
15
+ "no-swallowed-cleanup-catch": noSwallowedCleanupCatch,
16
+ "warn-serial-mode": warnSerialMode,
17
+ };
18
+ export const recommendedRules = {
19
+ "feather-testing/no-wait-for-timeout": "error",
20
+ "feather-testing/no-conditional-skip": "error",
21
+ "feather-testing/no-weak-assertions": "error",
22
+ "feather-testing/no-swallowed-cleanup-catch": "error",
23
+ // Serial mode is sometimes genuinely required; a warning asks for the
24
+ // annotation rather than forbidding the choice.
25
+ "feather-testing/warn-serial-mode": "warn",
26
+ };
27
+ const plugin = {
28
+ meta: { name: "feather-testing" },
29
+ rules,
30
+ configs: {},
31
+ };
32
+ plugin.configs.recommended = {
33
+ name: "feather-testing/recommended",
34
+ plugins: { "feather-testing": plugin },
35
+ rules: recommendedRules,
36
+ };
37
+ export default plugin;
@@ -0,0 +1,4 @@
1
+ import type { Rule } from "eslint";
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-conditional-skip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-conditional-skip.d.ts","sourceRoot":"","sources":["../../../src/eslint-plugin/rules/no-conditional-skip.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAqBnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA6ChB,CAAC;AAEF,eAAe,IAAI,CAAC"}
@@ -0,0 +1,56 @@
1
+ const RUNNERS = new Set(["test", "it", "describe", "suite"]);
2
+ /** The identifier a member chain like `test.describe.skip` starts from. */
3
+ function rootIdentifier(node) {
4
+ let current = node;
5
+ while (current.type === "MemberExpression")
6
+ current = current.object;
7
+ return current.type === "Identifier" ? current.name : null;
8
+ }
9
+ /** `test.skip("name", fn)` is declarative — the report still lists it. */
10
+ function isDeclarativeSkip(args) {
11
+ const [first] = args;
12
+ return (first !== undefined &&
13
+ (first.type === "Literal" || first.type === "TemplateLiteral"));
14
+ }
15
+ const rule = {
16
+ meta: {
17
+ type: "problem",
18
+ docs: {
19
+ description: "Do not skip tests at runtime — a self-skipping spec is green while proving nothing",
20
+ recommended: true,
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ conditionalSkip: "A runtime skip un-tests this spec silently: the run stays green while " +
25
+ "nothing is checked, sometimes for the life of the file. Make the " +
26
+ "precondition part of the test (set it up, or fail with a message that " +
27
+ "says what is missing), or mark the spec test.fixme(...) so the report " +
28
+ "names it as not running.",
29
+ },
30
+ },
31
+ create(context) {
32
+ return {
33
+ CallExpression(node) {
34
+ const callee = node.callee;
35
+ if (callee.type !== "MemberExpression" ||
36
+ callee.computed ||
37
+ callee.property.type !== "Identifier" ||
38
+ callee.property.name !== "skip") {
39
+ return;
40
+ }
41
+ // this.skip() — Mocha's runtime skip.
42
+ if (callee.object.type === "ThisExpression") {
43
+ context.report({ node, messageId: "conditionalSkip" });
44
+ return;
45
+ }
46
+ const root = rootIdentifier(callee.object);
47
+ if (root === null || !RUNNERS.has(root))
48
+ return;
49
+ if (isDeclarativeSkip(node.arguments))
50
+ return;
51
+ context.report({ node, messageId: "conditionalSkip" });
52
+ },
53
+ };
54
+ },
55
+ };
56
+ export default rule;
@@ -0,0 +1,4 @@
1
+ import type { Rule } from "eslint";
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-swallowed-cleanup-catch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-swallowed-cleanup-catch.d.ts","sourceRoot":"","sources":["../../../src/eslint-plugin/rules/no-swallowed-cleanup-catch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAmBnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAgDhB,CAAC;AAEF,eAAe,IAAI,CAAC"}
@@ -0,0 +1,63 @@
1
+ /** `() => {}`, `() => undefined`, `function () {}` — a handler that does nothing. */
2
+ function isEmptyHandler(node) {
3
+ if (node.type !== "ArrowFunctionExpression" &&
4
+ node.type !== "FunctionExpression") {
5
+ return false;
6
+ }
7
+ const body = node.body;
8
+ if (body.type === "BlockStatement")
9
+ return body.body.length === 0;
10
+ if (body.type === "Identifier")
11
+ return body.name === "undefined";
12
+ if (body.type === "Literal")
13
+ return body.value === null;
14
+ if (body.type === "ObjectExpression")
15
+ return body.properties.length === 0;
16
+ return false;
17
+ }
18
+ const rule = {
19
+ meta: {
20
+ type: "problem",
21
+ docs: {
22
+ description: "Do not swallow errors from setup or cleanup — a discarded failure reads as a pass",
23
+ recommended: true,
24
+ },
25
+ schema: [],
26
+ messages: {
27
+ swallowedCatch: "An empty .catch() turns a failed setup or cleanup into a green run, " +
28
+ "and the test that follows fails somewhere else entirely. Assert on the " +
29
+ "error, rethrow it with context, or narrow the catch to the one error " +
30
+ "you expect and say why.",
31
+ emptyCatchBlock: "This catch block discards the error, so a failure here is invisible. " +
32
+ "Assert on it, rethrow it, or leave a comment saying which error is " +
33
+ "expected and why ignoring it is safe.",
34
+ },
35
+ },
36
+ create(context) {
37
+ const sourceCode = context.sourceCode;
38
+ return {
39
+ CallExpression(node) {
40
+ const callee = node.callee;
41
+ if (callee.type !== "MemberExpression" ||
42
+ callee.computed ||
43
+ callee.property.type !== "Identifier" ||
44
+ callee.property.name !== "catch") {
45
+ return;
46
+ }
47
+ const [handler] = node.arguments;
48
+ if (handler && isEmptyHandler(handler)) {
49
+ context.report({ node, messageId: "swallowedCatch" });
50
+ }
51
+ },
52
+ CatchClause(node) {
53
+ if (node.body.body.length > 0)
54
+ return;
55
+ // A comment inside is the author annotating a deliberate ignore.
56
+ if (sourceCode.getCommentsInside(node.body).length > 0)
57
+ return;
58
+ context.report({ node, messageId: "emptyCatchBlock" });
59
+ },
60
+ };
61
+ },
62
+ };
63
+ export default rule;
@@ -0,0 +1,4 @@
1
+ import type { Rule } from "eslint";
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-wait-for-timeout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-wait-for-timeout.d.ts","sourceRoot":"","sources":["../../../src/eslint-plugin/rules/no-wait-for-timeout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AA4CnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAqChB,CAAC;AAEF,eAAe,IAAI,CAAC"}
@@ -0,0 +1,69 @@
1
+ const SLEEP_MESSAGE = "This sleeps for a fixed time instead of waiting for a condition, so it is " +
2
+ "slow when it passes and lying when it fails. Use " +
3
+ "session.until('<what you are waiting for>', fn) — or expect.poll / findBy* " +
4
+ "— so the wait names the thing it is waiting for.";
5
+ function isGlobalSetTimeout(callee) {
6
+ if (callee.type === "Identifier")
7
+ return callee.name === "setTimeout";
8
+ return (callee.type === "MemberExpression" &&
9
+ !callee.computed &&
10
+ callee.property.type === "Identifier" &&
11
+ callee.property.name === "setTimeout" &&
12
+ callee.object.type === "Identifier" &&
13
+ ["window", "globalThis", "global"].includes(callee.object.name));
14
+ }
15
+ /**
16
+ * Only sleeps, not every timer. `setTimeout(resolve, 100)` and a setTimeout
17
+ * inside a `new Promise(...)` executor are the sleep idiom; a timer whose
18
+ * callback does real work (a fixture component, a debounce test) is not.
19
+ */
20
+ function isSleep(node) {
21
+ const [first] = node.arguments;
22
+ if (first && first.type === "Identifier")
23
+ return true;
24
+ let current = node.parent ?? null;
25
+ for (let depth = 0; current && depth < 4; depth += 1) {
26
+ if (current.type === "NewExpression" &&
27
+ current.callee.type === "Identifier" &&
28
+ current.callee.name === "Promise") {
29
+ return true;
30
+ }
31
+ current = current.parent ?? null;
32
+ }
33
+ return false;
34
+ }
35
+ const rule = {
36
+ meta: {
37
+ type: "problem",
38
+ docs: {
39
+ description: "Wait for a condition instead of sleeping for a fixed duration",
40
+ recommended: true,
41
+ },
42
+ schema: [],
43
+ messages: {
44
+ waitForTimeout: "page.waitForTimeout() sleeps for a fixed time. Use " +
45
+ "session.until('<what you are waiting for>', fn) — or expect.poll / a " +
46
+ "web-first assertion — so the wait names its condition and ends as " +
47
+ "soon as it holds.",
48
+ sleep: SLEEP_MESSAGE,
49
+ },
50
+ },
51
+ create(context) {
52
+ return {
53
+ CallExpression(node) {
54
+ const callee = node.callee;
55
+ if (callee.type === "MemberExpression" &&
56
+ !callee.computed &&
57
+ callee.property.type === "Identifier" &&
58
+ callee.property.name === "waitForTimeout") {
59
+ context.report({ node, messageId: "waitForTimeout" });
60
+ return;
61
+ }
62
+ if (isGlobalSetTimeout(callee) && isSleep(node)) {
63
+ context.report({ node, messageId: "sleep" });
64
+ }
65
+ },
66
+ };
67
+ },
68
+ };
69
+ export default rule;
@@ -0,0 +1,4 @@
1
+ import type { Rule } from "eslint";
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-weak-assertions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-weak-assertions.d.ts","sourceRoot":"","sources":["../../../src/eslint-plugin/rules/no-weak-assertions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AA2BnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA4DhB,CAAC;AAEF,eAAe,IAAI,CAAC"}
@@ -0,0 +1,73 @@
1
+ const DEFAULT_MATCHERS = ["toBeTruthy", "toBeDefined"];
2
+ const ALTERNATIVES = {
3
+ toBeTruthy: "assert the value you mean — toBe/toEqual, toHaveLength, toHaveText, or a " +
4
+ "DSL assertion like assertText()",
5
+ toBeDefined: "assert the value you mean — toBe/toEqual, or toBeInstanceOf when the type " +
6
+ "is the point",
7
+ toBeFalsy: "assert the exact falsy value — toBe(false), toBe(0), toBeNull()",
8
+ toBeNull: "assert the value you mean, or pair it with a positive assertion",
9
+ };
10
+ /** Does this member chain hang off an `expect(...)` call? */
11
+ function rootsAtExpect(node) {
12
+ let current = node;
13
+ while (current.type === "MemberExpression")
14
+ current = current.object;
15
+ return (current.type === "CallExpression" &&
16
+ current.callee.type === "Identifier" &&
17
+ current.callee.name === "expect");
18
+ }
19
+ const rule = {
20
+ meta: {
21
+ type: "problem",
22
+ docs: {
23
+ description: "Assert the shape you mean instead of truthiness or definedness",
24
+ recommended: true,
25
+ },
26
+ schema: [
27
+ {
28
+ type: "object",
29
+ properties: {
30
+ matchers: {
31
+ type: "array",
32
+ items: { type: "string" },
33
+ minItems: 1,
34
+ },
35
+ },
36
+ additionalProperties: false,
37
+ },
38
+ ],
39
+ messages: {
40
+ weak: "{{matcher}}() passes for almost any value, so it proves little more " +
41
+ "than that the line ran. Assert the shape you mean with toBe/toEqual " +
42
+ "and friends: {{alternative}}.",
43
+ },
44
+ },
45
+ create(context) {
46
+ const configured = context.options[0];
47
+ const matchers = new Set(configured?.matchers ?? DEFAULT_MATCHERS);
48
+ return {
49
+ CallExpression(node) {
50
+ const callee = node.callee;
51
+ if (callee.type !== "MemberExpression" ||
52
+ callee.computed ||
53
+ callee.property.type !== "Identifier") {
54
+ return;
55
+ }
56
+ const matcher = callee.property.name;
57
+ if (!matchers.has(matcher))
58
+ return;
59
+ if (!rootsAtExpect(callee.object))
60
+ return;
61
+ context.report({
62
+ node,
63
+ messageId: "weak",
64
+ data: {
65
+ matcher,
66
+ alternative: ALTERNATIVES[matcher] ?? "assert the shape you actually expect",
67
+ },
68
+ });
69
+ },
70
+ };
71
+ },
72
+ };
73
+ export default rule;
@@ -0,0 +1,4 @@
1
+ import type { Rule } from "eslint";
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=warn-serial-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"warn-serial-mode.d.ts","sourceRoot":"","sources":["../../../src/eslint-plugin/rules/warn-serial-mode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAgBnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAkDhB,CAAC;AAEF,eAAe,IAAI,CAAC"}