Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ These cascade from parent to child (set in `Test.js` constructor):
beforeEach run afterEach map check getName getData args expect getExpect throws maxTime maxTimeAsync skip
```

NOT inherited: `beforeAll`, `afterAll`, `name`. `data` inherits via prototype chain (child sees parent's data; own properties shadow parent's).
NOT inherited as values: `beforeAll`, `afterAll`, `name`, `data`. However, **accessor descriptors** for `name` and `data` (e.g. `get name () {}`, `get data () {}`) ARE inherited — children get the same getter, invoked with their own `this`. `data` (literal) inherits via prototype chain (child sees parent's data; own properties shadow parent's).

For lazy `name` / `data` / `expect`, you can use either the old `getName` / `getData` / `getExpect` methods (eager, evaluated at construction) or the newer accessor syntax `get name () {...}` / `get data () {...}` / `get expect () {...}` (lazy, evaluated on first access, cached). Both work; accessors win if both are defined on the same property.

## Self-hosted testing

Expand Down
22 changes: 12 additions & 10 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,20 @@ All properties are optional and inherit from parent to child.
| `run` | Function to execute. Called via `run.apply(testInstance, args)`. Inherited from parent — define once, never repeat. If omitted, result defaults to `args[0]` |
| `arg` | Single argument passed to `run`. Can be any value |
| `args` | Array of arguments passed to `run`. Non-arrays auto-wrapped. `arg` takes precedence |
| `expect` | Expected result. Deep equality by default |
| `getExpect` | Function to generate expected value dynamically. Called like `run`: `getExpect.apply(test, args)`. Inherited. `expect` takes precedence if both are set. If the getter throws, falls through to default (`args[0]`) |
| `expect` | Expected result. Deep equality by default. Can also be a getter (`get expect () { ... }`) to compute lazily from `this.args`/`this.data`. Inherited as a value or accessor. If the getter throws, falls through to default (`args[0]`) |
| `getExpect` | Legacy alternative to `get expect ()` — a function that generates the expected value dynamically. Called like `run`: `getExpect.apply(test, args)`. Inherited. Eager (evaluated at construction). `expect` (literal or accessor) wins if both are defined. If the getter throws, falls through to default (`args[0]`) |
| `throws` | `true` (any error), `false` (asserts no error thrown), Error subclass (`TypeError`), or predicate `e => e.code === "ENOENT"`. Inherited |

### Structure

| Property | Description |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `name` | Test/group label. Also accessible via `this.name` and `this.parent.name` in `run`. If a function, it's used as `getName` instead |
| `getName` | Function to generate names dynamically. Called like `run`: `getName.apply(test, args)`. Inherited (unlike `name`). If the getter throws, falls through to default (first arg or "(No args)") |
| `name` | Test/group label. Also accessible via `this.name` and `this.parent.name` in `run`. Can also be a getter (`get name () { ... }`) or function shorthand (`name () { ... }`) to compute lazily from `this.args`/`this.level`. Literal names are not inherited, but accessor descriptors are. If the getter throws, falls through to default (`stringify(args[0])` or `"(No args)"`) |
| `getName` | Legacy alternative to `get name ()` — a function that generates names dynamically. Called like `run`: `getName.apply(test, args)`. Inherited (unlike literal `name`). Eager (evaluated at construction). `name` (literal or accessor) wins if both are defined. If the getter throws, falls through to default |
| `description` | Human-readable explanation of the test's intent or edge case. Ignored by the runner |
| `tests` | Array of child tests. If present, this is a group (parent); if absent, a leaf test |
| `data` | Inherited object accessible via `this.data`. Child inherits parent's data via prototype chain; own properties shadow parent's. If a function, it's used as `getData` instead |
| `getData` | Function to generate data dynamically. Called like `run`: `getData.apply(test, args)`. Inherited. `data` (literal) takes precedence if both are set. If the getter throws, falls through to empty data |
| `data` | Inherited object accessible via `this.data`. Child inherits parent's data via prototype chain; own properties shadow parent's. Can also be a getter (`get data () { ... }`) or function shorthand (`data () { ... }`) to produce fresh per-test data — the returned object is wired into the parent's data chain. Literal data is not inherited (the chain handles that), but accessor descriptors are. If the getter throws, falls through to empty data |
| `getData` | Legacy alternative to `get data ()` — a function that generates data dynamically. Called like `run`: `getData.apply(test, args)`. Inherited. Eager (evaluated at construction). Literal `data` and `get data ()` accessors win if defined. If the getter throws, falls through to empty data |
| `skip` | Any truthy value to skip. Can be an expression evaluated at load time, e.g. `skip: !globalThis.structuredClone`. Inherited — setting on a parent skips all children |

### Comparison
Expand Down Expand Up @@ -186,7 +186,7 @@ If a hook throws, the test is **skipped** (not failed). Hooks are infrastructure
- `this.args` — argument array
- `this.data` — inherited data object
- `this.name` — test name
- `this.level` — nesting depth (root = 0). Useful in `getName` for depth-aware labels
- `this.level` — nesting depth (root = 0). Useful in `get name ()` / `getName` for depth-aware labels
- `this.parent` — parent test/group. Useful for extending the parent's `run` in a child: call `this.parent.run(...args)` first, then transform the result
- `this.expect` — expected value

Expand Down Expand Up @@ -335,13 +335,13 @@ export default {
};
```

**Good use: `getData` for fresh per-test data**
**Good use: `data` getter for fresh per-test data**

When each test needs its own fresh data (e.g., an empty array to push into), use `getData` instead of `beforeEach`:
When each test needs its own fresh data (e.g., an empty array to push into), define `data` as a getter (or function shorthand) instead of using `beforeEach`:

```js
export default {
getData () {
get data () {
return { items: [] };
},
run () {
Expand All @@ -355,6 +355,8 @@ export default {
};
```

The same goes for `name` and `expect` — use `get name ()`, `name () {}`, or `get expect ()` to compute them lazily from `this.args` / `this.data`. The getter runs once per test instance, with `this` bound to the Test, and the result is cached. If the getter throws, the property falls through to its default.

**Good use: setup hook builds a per-test fixture for `run`**

When setup is more involved than a literal value — instantiating a class, opening a connection, assembling a DOM tree — do it in `beforeEach` and stash the result on `this.data` so `run` (and `afterEach`) can use it. `beforeEach`/`afterEach` receive the same args as `run`, so you can unpack them in the parameter list and write `this.data`.
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export default {
}

return `should return ${this.expect} when the value is ${this.args[0]}`;
}
},
tests: [
{
data: {
Expand Down
68 changes: 41 additions & 27 deletions docs/define/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ Tests at the same nesting level run **in parallel**, so don't rely on execution
| [`afterEach`](#setup-teardown) | Function | Code to run after each test. |
| [`beforeAll`](#setup-teardown) | Function | Code to run before all tests in the group. |
| [`afterAll`](#setup-teardown) | Function | Code to run after all tests in the group. |
| [`data`](#data) | Object | Data that will be accessible to the running function as `this.data`. |
| [`getData`](#data) | Function | A function that generates data dynamically. |
| [`name`](#name) | String or Function | A string that describes the test. |
| [`getName`](#name) | Function | A function that generates the test name dynamically. |
| [`data`](#data) | Object, Function, or Accessor | Data that will be accessible to the running function as `this.data`. |
| [`getData`](#data) | Function | Legacy: a function that generates data dynamically (eager). Prefer `get data () { ... }`. |
| [`name`](#name) | String, Function, or Accessor | A string that describes the test. |
| [`getName`](#name) | Function | Legacy: a function that generates the test name dynamically (eager). Prefer `get name () { ... }`. |
| [`description`](#description) | String | A longer description of the test or group of tests. |
| [`id`](#id) | String | A unique identifier for the test. |
| [`expect`](#expect) | Any | The expected result. |
| [`getExpect`](#expect) | Function | A function that generates the expected result dynamically. |
| [`expect`](#expect) | Any or Accessor | The expected result. |
| [`getExpect`](#expect) | Function | Legacy: a function that generates the expected result dynamically (eager). Prefer `get expect () { ... }`. |
| [`throws`](#throws) | Boolean, Error subclass, or Function | Whether an error is expected to be thrown. |
| [`maxTime`](#maxtime) | Number | The maximum time (in ms) that the test should take to run. |
| [`maxTimeAsync`](#maxtime) | Number | The maximum time (in ms) that the test should take to resolve. |
Expand Down Expand Up @@ -104,18 +104,11 @@ You can define a single `beforeEach` or `afterEach` on a parent or ancestor and
A child’s data inherits from its parent’s, so you can define common data at a higher level and override it where needed.
It is useful for differentiating the behavior of `run()` across groups of tests without having to redefine it or pass repetitive arguments.

`data` can also be a *data generator* function.
It is called with the same context and arguments as `run()` and returns an object whose properties are merged onto `this.data`.
You can also explicitly provide a function, via `getData`.
In fact, if `data` is a function, it gets rewritten as `getData` internally.

If both `data` (as a literal object) and `getData` are defined, `data` wins.

`getData` is useful for providing fresh per-test data without `beforeEach()`:
`data` can also be a getter or function shorthand that generates fresh data per test:

```js
{
getData () { return { items: [] }; },
get data () { return { items: [] }; },
run () {
this.data.items.push(1);
return this.data.items.length;
Expand All @@ -127,28 +120,39 @@ If both `data` (as a literal object) and `getData` are defined, `data` wins.
}
```

The function shorthand `data () { ... }` is equivalent to `get data () { ... }` — both run with `this` bound to the Test instance, so you can read `this.args`, `this.parent.data`, etc. Returned objects are wired into the parent's data prototype chain. Accessor descriptors are inherited from parent to child, but literal data values are not (children see parent data through the chain). If the getter throws, `this.data` falls through to an empty object.

You can also explicitly provide a data generator function via the legacy `getData` property. It is called with the same context and arguments as `run()` and the returned object's properties are merged into `this.data` eagerly at construction time. `get data ()` is preferred (lazy and idiomatic JS), but `getData` remains supported. If both are defined, the accessor wins.

## Describing the test

### Names and name generators (`name` and `getName()`) { #name }

`name` is a string that describes the test.
It is optional, but recommended, as it makes it easier to identify the test in the results.

`name` can also be a *name generator* function.
It is called with the same context and arguments as `run()` and returns the name as a string.
You can also explicitly provide a function, via `getName`.
This can be useful if you want to specify a name for the root of tests, as well as a name generator for child tests.
In fact, if `name` is a function, it gets rewritten as `getName` internally.
`name` can also be a getter or function shorthand that generates the name lazily:

```js
{
get name () { return "Test " + this.args[0]; },
tests: [{ arg: "foo" }], // → name: "Test foo"
}
```

The function shorthand `name () { ... }` is equivalent to `get name () { ... }` — both run with `this` bound to the Test instance.

Single names are not inherited, but name generator functions are.
Literal `name` values are not inherited, but accessor descriptors are. This means a parent can define a `get name ()` and every child inherits the same generator (invoked with its own `this`). Children with an explicit `name` literal override the inherited accessor.

You can also explicitly provide a name generator function via the legacy `getName` property. It is called with the same context and arguments as `run()` and the returned string is used as the name (eagerly, at construction). `get name ()` is preferred (lazy and idiomatic JS), but `getName` remains supported. If both are defined, the accessor wins.

Name generators are useful for providing a default name for tests, that you can override on a case by case basis via `name`.
You may find `this.level` useful in the name generator, as it tells you how deep in the hierarchy the test is, allowing you to provide depth-sensitive name patterns.

If no name is provided, it defaults to the first argument passed to `run`, if any.

If `getName()` throws an error (e.g. by accessing `this.run` on a group with no `run`), the error is caught and the name falls through to its default.
This allows defining a `getName` that only works in certain contexts without crashing the test tree.
If the `name` getter throws an error (e.g. by accessing `this.run` on a group with no `run`), the error is caught and the name falls through to its default.
This allows defining a `name` getter that only works in certain contexts without crashing the test tree.

### Description (`description`) { #description }

Expand All @@ -172,12 +176,22 @@ If you specify multiple criteria, nothing will break, but you will get a warning
`expect` defines the expected result, so you'll be using it the most.
If `expect` is *not defined*, it defaults to the first argument passed to `run()`, i.e. `this.args[0]`.

The expected result can also be generated dynamically via `getExpect`.
It is called with the same context and arguments as `run()` and returns the expected result.
The expected result can also be generated dynamically with a getter:

```js
{
run: double,
tests: [
{ arg: 5, get expect () { return this.args[0] * 2; } },
],
}
```

The getter runs with `this` bound to the Test instance, so you can read `this.args` and `this.data`. The result is cached on first access. Accessor descriptors are inherited from parent to child, just like literal values.

If both `expect` and `getExpect` are defined, `expect` wins.
You can also explicitly provide a generator function via the legacy `getExpect` property. It is called with the same context and arguments as `run()` and the returned value is used as `expect` (eagerly, at construction). `get expect ()` is preferred (lazy and idiomatic JS), but `getExpect` remains supported. If both are defined, the accessor wins.

If `getExpect()` throws an error, the error is caught and `expect` falls through to its default (`args[0]`).
If the `expect` getter throws an error, the error is caught and `expect` falls through to its default (`args[0]`).

### Error-based criteria (`throws`) { #throws }

Expand Down
Loading