Skip to content
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Package: `htest.dev` | Repo: https://github.com/htest-dev/htest | Site: https://
| `npm run dev` | Eleventy dev server for docs site |
| `npm run release` | Publish via release-it |

No Prettier, no EditorConfig — ESLint only (`eslint.config.js`).
ESLint (`eslint.config.js`) + Prettier. Run Prettier on JS files only — not on markdown.

## Architecture

Expand Down Expand Up @@ -56,11 +56,13 @@ src/
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
beforeEach run afterEach map check args expect throws maxTime maxTimeAsync skip
```

NOT inherited: `beforeAll`, `afterAll`, `name`. `data` inherits via prototype chain (child sees parent's data; own properties shadow parent's).

`name`, `data`, and `expect` support getter syntax (`get name()`, `get data()`, `get expect()`). `name` and `data` also support function shorthand (`name()`, `data()`); `expect` does not (it can be a function value). Getter/shorthand forms are inherited, unlike literal values. `getName` and `getExpect` are legacy (shipped in v0.0.25) — prefer getter syntax. `getData` is internal only (never shipped publicly).

## Self-hosted testing

hTest tests itself. All test files in `tests/` use hTest's own declarative format.
Expand Down
73 changes: 58 additions & 15 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,19 @@ 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. Inherited. Can be a getter (`get expect () { ... }`) to compute lazily. No function shorthand — `expect` can be a function value |
| `getExpect` | **(Legacy — prefer `get expect()`)** 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]`) |
| `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 be a getter (`get name () { ... }`) or function shorthand (`name () { ... }`) to compute lazily. Literal names are not inherited, but getter/shorthand names are |
| `getName` | **(Legacy — prefer `get name()` / `name()`)** Function to generate names dynamically. Called like `run`: `getName.apply(test, args)`. Inherited (unlike literal `name`). If the getter throws, falls through to default (first arg or "(No args)") |
| `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 be a getter (`get data () { ... }`) or function shorthand (`data () { ... }`) for fresh per-test 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 All @@ -103,7 +102,7 @@ Pass an object instead of a function to configure built-in comparison behavior:
| `subset` | `false` | Only check properties present in `expect` — extra properties in the result are ignored |
| `epsilon` | `0` | Numeric tolerance: passes if `Math.abs(actual - expect) <= epsilon` |
| `looseTypes` | `false` | Use `==` instead of `===` at leaf level |
| `deep` | `true` | Recurse into objects/arrays. Set `false` for shallow-only |
| `deep` | `false` | Recurse into objects/arrays |

**`subset: true` is the most useful option.** Use it when results may contain extra fields you don't want to validate:

Expand All @@ -121,9 +120,9 @@ Pass an object instead of a function to configure built-in comparison behavior:
Combine options freely:

```js
check: { epsilon: 0.005 } // numeric tolerance, deep (default)
check: { subset: true, epsilon: 0.0001 } // partial match + tolerance
check: { deep: false, looseTypes: true } // shallow loose equality
check: { epsilon: 0.005 } // numeric tolerance, shallow
check: { subset: true, deep: true } // partial match + deep
check: { looseTypes: true } // shallow loose equality
```

For a custom comparison, use an inline function — no import needed:
Expand Down Expand Up @@ -179,14 +178,58 @@ If a hook throws, the test is **skipped** (not failed). Hooks are infrastructure
| `afterEach` throws | Already ran | — | Test **skipped** |
| `afterAll` throws | Already ran | — | Test results **unaffected** |

### Accessor Support

Any test property supports native JS getter syntax (`get prop () { ... }`). Most properties also support function shorthand (`prop () { ... }`), except those whose value can be a function: `arg`, `expect`, `run`, hooks, `check`, `map`, `throws`.

Function shorthands receive the same arguments as `run`, so you can use them to compute values based on test args:

```js
{
skip (x) { return x > 100; }, // skip tests where arg > 100
run (x) { return transform(x); },
tests: [
{ arg: 5 },
{ arg: 200 }, // skipped
],
}
```

Getters and function shorthands are inherited by children (literal values are not, except `expect` — both literal and getter `expect` are inherited).

```js
{
get name () {
return `Level ${this.level}: ${this.arg}`;
},
get data () {
return { items: [] }; // fresh per test
},
get expect () {
return this.arg.toUpperCase();
},
run (input) {
this.data.items.push(input);
return this.data.items[0].toUpperCase();
},
tests: [
{ arg: "foo" },
{ arg: "bar" },
],
}
```

The legacy function properties `getName` and `getExpect` (shipped in v0.0.25) still work but getter syntax is preferred.

## `this` Inside `run`

`run` is called with `this` set to the Test instance. Available properties:

- `this.args` — argument array
- `this.arg` — the original `arg` value (if defined). Useful in getters; in `run`, prefer the parameter list
- `this.args` — argument array. Prefer `this.arg` over `this.args[0]` when the test uses `arg:`
- `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 name getters/shorthands 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 +378,13 @@ export default {
};
```

**Good use: `getData` for fresh per-test data**
**Good use: getter/shorthand 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), use a `get data()` getter or `data()` shorthand instead of `beforeEach`:

```js
export default {
getData () {
get data () {
return { items: [] };
},
run () {
Expand Down
8 changes: 4 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ export default {
data: { // custom inherited data
arr: [1, 2, 3]
},
getName () {
get name () {
if (this.level === 1) {
return "#" + this.data.method + "()";
}

return `should return ${this.expect} when the value is ${this.args[0]}`;
return `should return ${this.expect} when the value is ${this.arg}`;
}
tests: [
{
Expand Down Expand Up @@ -197,8 +197,8 @@ export default {
```

Here we moved the commonalities to test parents and used inherited data to pass the array and code to be tested to the tests.
We are also only specifying a name when it's non-obvious, and using the `getName` method to generate the name for us
(even with no `getName` method, hTest will generate a name for us based on the test parameters).
We are also only specifying a name when it's non-obvious, and using a `get name()` getter to generate the name for us
(even with no name getter, hTest will generate a name for us based on the test parameters).

Notice that there is a spectrum between how much you want to abstract away and how much you want to specify in each test.
It’s up to you where your tests would be in that spectrum.
Expand Down
83 changes: 55 additions & 28 deletions docs/define/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@ Tests at the same nesting level run **in parallel**, so don't rely on execution
| [`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. |
| [`getName`](#name) | Function | **(Legacy)** A function that generates the test name dynamically. |
| [`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. |
| [`getExpect`](#expect) | Function | **(Legacy)** A function that generates the expected result dynamically. |
| [`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 All @@ -40,6 +39,14 @@ Tests at the same nesting level run **in parallel**, so don't rely on execution
| [`skip`](#skip) | Any | Any truthy value skips the test(s). |
</div>

## Accessor support { #accessors }

Any test property supports native JS getter syntax (e.g. `get skip () { ... }`).
Most properties also support function shorthand to compute the property value dynamically (e.g. `skip () { return !cond; }`) — except those whose value can be a function: `arg`, `expect`, `run`, hooks, `check`, `map`, `throws`.

Function shorthands are called like `run` — with the same `this` context and arguments — so you can compute values based on test args.
Getters and function shorthands are inherited by children; literal values are generally not (see individual property docs for exceptions).

## Defining the test

### Defining the code to be tested (`run`) { #run }
Expand Down Expand Up @@ -98,24 +105,21 @@ If a hook throws, the test is **skipped** — not failed.

You can define a single `beforeEach` or `afterEach` on a parent or ancestor and differentiate child tests via [`args`](#args) and [`data`](#data).

### Context parameters (`data` and `getData`) { #data }
### Context parameters (`data`) { #data }

`data` is an optional object with data that will be accessible to the running function as `this.data`.
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.
To compute data dynamically, use a [getter or function shorthand](#accessors).
The returned object's properties are merged onto `this.data`.
Unlike literal `data` objects (which inherit via the prototype chain), getter/shorthand data is inherited by children — define once on a parent, and every child gets a fresh copy.

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

`getData` is useful for providing fresh per-test data without `beforeEach()`:
This is useful for providing fresh per-test data without `beforeEach()`:

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

## Describing the test

### Names and name generators (`name` and `getName()`) { #name }
### Names and name generators (`name`) { #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.
To compute names dynamically, use a [getter or function shorthand](#accessors):

Single names are not inherited, but name generator functions are.
```js
{
get name () {
if (this.level === 1) {
return "#" + this.data.method + "()";
}
return `should return ${this.expect} for ${this.arg}`;
},
tests: [...]
}
```

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.
Literal names are not inherited, but getter/shorthand names are.
This makes them useful for providing a default name for tests, that you can override on a case by case basis via a literal `name`.
You may find `this.level` useful in the getter/shorthand, 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.

**Legacy:** `getName` (a function property called like `run`) still works but getter/shorthand syntax is preferred.

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

Expand All @@ -167,17 +180,31 @@ E.g. you can use `maxTime` and `maxTimeAsync` together, but not with `expect` or

If you specify multiple criteria, nothing will break, but you will get a warning.

### Result-based criteria (`expect` and `getExpect()`) { #expect }
### Result-based criteria (`expect`) { #expect }

`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.
To compute expected values dynamically, use a [getter](#accessors) (`expect` does not support function shorthand — it can legitimately be a function value):

```js
{
get expect () {
return this.arg.toUpperCase();
},
run: toUpperCase,
tests: [
{ arg: "foo" },
{ arg: "bar" },
],
}
```

Both literal and getter `expect` are inherited by children.

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

If `getExpect()` throws an error, the error is caught and `expect` falls through to its default (`args[0]`).
**Legacy:** `getExpect` (a function property called like `run`) still works but getter syntax is preferred.

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

Expand Down
Loading