From 2923c0fb5ed529f7ada8d305023dcc47c72cbf99 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 16:03:29 +0000 Subject: [PATCH] Preserve accessor descriptors on test objects for lazy name/data/expect Test definitions can now use native getters (and function shorthand) for `name`, `data`, and `expect`. The getter is preserved on the Test instance, runs once when the property is first accessed, and the result is cached. Accessor descriptors are inherited from parent to child (so a parent's `get name ()` is reused by each child with its own `this`). The legacy `getName` / `getData` / `getExpect` methods continue to work unchanged. Accessor wins if both are defined on the same property. https://claude.ai/code/session_01EFM1qJJBFmTsckYiwwhitG --- CLAUDE.md | 4 +- SKILL.md | 22 +++-- docs/README.md | 2 +- docs/define/README.md | 68 ++++++++----- src/classes/Test.js | 202 +++++++++++++++++++++++++++++--------- src/classes/TestResult.js | 7 +- tests/data.js | 109 ++++++++++++++++++++ tests/getters.js | 111 +++++++++++++++++++++ 8 files changed, 437 insertions(+), 88 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 81da77e..531bbd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/SKILL.md b/SKILL.md index 2a5443b..2ce2aff 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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 @@ -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 @@ -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 () { @@ -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`. diff --git a/docs/README.md b/docs/README.md index 58c292a..90c7cb5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -149,7 +149,7 @@ export default { } return `should return ${this.expect} when the value is ${this.args[0]}`; - } + }, tests: [ { data: { diff --git a/docs/define/README.md b/docs/define/README.md index 72c77e6..0203ea5 100644 --- a/docs/define/README.md +++ b/docs/define/README.md @@ -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. | @@ -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; @@ -127,6 +120,10 @@ 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 } @@ -134,21 +131,28 @@ If both `data` (as a literal object) and `getData` are defined, `data` wins. `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 } @@ -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 } diff --git a/src/classes/Test.js b/src/classes/Test.js index d326c79..aaa9e06 100644 --- a/src/classes/Test.js +++ b/src/classes/Test.js @@ -1,12 +1,51 @@ import * as check from "../check.js"; import { stringify } from "../util.js"; +const INHERITED_PROPS = [ + "beforeEach", + "run", + "afterEach", + "map", + "check", + "getName", + "getData", + "args", + "expect", + "getExpect", + "throws", + "maxTime", + "maxTimeAsync", + "skip", +]; + +// `name` and `data` are not inherited as values, but accessor descriptors are. +const ACCESSOR_INHERITED_PROPS = ["name", "data"]; + +/** + * Define a one-shot lazy property: the getter runs at most once per instance, + * then the result replaces it as a writable data property. + */ +function defineLazy (target, prop, compute) { + Object.defineProperty(target, prop, { + configurable: true, + enumerable: true, + get () { + let value = compute.call(this); + Object.defineProperty(this, prop, { + value, + writable: true, + configurable: true, + enumerable: true, + }); + return value; + }, + }); +} + /** * Represents a single test or a group of tests */ export default class Test { - data = {}; - constructor (test, parent) { if (!test) { console.warn("Empty test: ", test); @@ -21,45 +60,95 @@ export default class Test { this.level = 0; } - Object.assign(this, test); + // Copy properties from the test definition, preserving accessor descriptors. + Object.defineProperties(this, Object.getOwnPropertyDescriptors(test)); - if (typeof this.data === "function") { - this.getData = this.data; - this.data = {}; + // Function shorthand: `name () {...}` / `data () {...}` become accessors. + for (let prop of ["name", "data"]) { + let desc = Object.getOwnPropertyDescriptor(this, prop); + if (desc && "value" in desc && typeof desc.value === "function") { + let fn = desc.value; + Object.defineProperty(this, prop, { + configurable: true, + enumerable: true, + get () { + return fn.apply(this, this.args); + }, + }); + } } - this.data = Object.create( - this.parent?.data ?? null, - Object.getOwnPropertyDescriptors(this.data), - ); - this.originalName = this.name; + // Wrap own user-defined accessors with safe-fallback + caching wrappers. + // Done before inheritance so children that inherit these descriptors get the + // already-wrapped version (no double-wrap on each level). + let nameDesc = Object.getOwnPropertyDescriptor(this, "name"); + if (nameDesc?.get) { + let userGetter = nameDesc.get; + defineLazy(this, "name", function () { + let value; + try { + value = userGetter.call(this); + } + catch {} - if (typeof this.name === "function") { - this.getName = this.name; - delete this.name; + if (!value && this.isTest) { + value = this.args.length > 0 ? stringify(this.args[0]) : "(No args)"; + } + + return value; + }); + } + + let dataDesc = Object.getOwnPropertyDescriptor(this, "data"); + if (dataDesc?.get) { + let userGetter = dataDesc.get; + defineLazy(this, "data", function () { + let value; + try { + value = userGetter.call(this); + } + catch { + value = {}; + } + return Object.create( + this.parent?.data ?? null, + Object.getOwnPropertyDescriptors(value ?? {}), + ); + }); + } + + let expectDesc = Object.getOwnPropertyDescriptor(this, "expect"); + if (expectDesc?.get) { + let userGetter = expectDesc.get; + defineLazy(this, "expect", function () { + try { + return userGetter.call(this); + } + catch { + return this.args[0]; + } + }); } - // Inherit properties from parent - // This works recursively because the parent constructor runs before its children + // Inherit properties from parent (preserving descriptors, including accessors). + // This works recursively because the parent constructor runs before its children. if (this.parent) { - for (let prop of [ - "beforeEach", - "run", - "afterEach", - "map", - "check", - "getName", - "getData", - "args", - "expect", - "getExpect", - "throws", - "maxTime", - "maxTimeAsync", - "skip", - ]) { - if (!(prop in this) && prop in this.parent) { - this[prop] = this.parent[prop]; + for (let prop of INHERITED_PROPS) { + if (!Object.prototype.hasOwnProperty.call(this, prop)) { + let desc = Object.getOwnPropertyDescriptor(this.parent, prop); + if (desc) { + Object.defineProperty(this, prop, desc); + } + } + } + + // `name` and `data`: only inherit accessor descriptors (not values). + for (let prop of ACCESSOR_INHERITED_PROPS) { + if (!Object.prototype.hasOwnProperty.call(this, prop)) { + let desc = Object.getOwnPropertyDescriptor(this.parent, prop); + if (desc?.get) { + Object.defineProperty(this, prop, desc); + } } } } @@ -88,15 +177,36 @@ export default class Test { this.args = []; } - if (this.getData && Object.keys(this.data).length === 0) { - try { - let data = this.getData.apply(this, this.args); - Object.defineProperties(this.data, Object.getOwnPropertyDescriptors(data)); + // Wire `data`. If `data` is an accessor (own or inherited), the wrapper handles wiring. + // Otherwise: literal data is wired via Object.create. If `getData` is defined and no + // literal data was provided, fall back to the legacy eager getData call. + dataDesc = Object.getOwnPropertyDescriptor(this, "data"); + if (!dataDesc?.get) { + let own = dataDesc && "value" in dataDesc ? dataDesc.value : {}; + this.data = Object.create( + this.parent?.data ?? null, + Object.getOwnPropertyDescriptors(own ?? {}), + ); + + if (this.getData && Object.keys(this.data).length === 0) { + try { + let data = this.getData.apply(this, this.args); + Object.defineProperties(this.data, Object.getOwnPropertyDescriptors(data)); + } + catch {} } - catch {} } - if (!this.name) { + if (this.isGroup) { + this.tests = this.tests + .filter(Boolean) + .map(t => (t instanceof Test ? t : new Test(t, this))); + } + + // Default name for leaf tests with no `name` defined. + // Falls back to `getName()` (legacy) if defined. + nameDesc = Object.getOwnPropertyDescriptor(this, "name"); + if (!nameDesc) { if (this.getName) { try { this.name = this.getName.apply(this, this.args); @@ -109,13 +219,9 @@ export default class Test { } } - if (this.isGroup) { - this.tests = this.tests - .filter(Boolean) - .map(t => (t instanceof Test ? t : new Test(t, this))); - } - - if (!("expect" in this)) { + // Default `expect`. Falls back to `getExpect()` (legacy) if defined, then to `args[0]`. + expectDesc = Object.getOwnPropertyDescriptor(this, "expect"); + if (!expectDesc) { if (this.getExpect) { try { this.expect = this.getExpect.apply(this, this.args); @@ -123,7 +229,7 @@ export default class Test { catch {} } - if (!("expect" in this)) { + if (!Object.getOwnPropertyDescriptor(this, "expect")) { this.expect = this.args[0]; } } diff --git a/src/classes/TestResult.js b/src/classes/TestResult.js index e69729f..500a9f6 100644 --- a/src/classes/TestResult.js +++ b/src/classes/TestResult.js @@ -124,7 +124,12 @@ export default class TestResult extends BubblingEventTarget { // Duck-type assertion errors (Node assert, Chai, etc.) — use their actual/expected for diffs if ("actual" in e) { this.actual = e.actual; - this.test.expect = e.expected; + Object.defineProperty(this.test, "expect", { + value: e.expected, + writable: true, + configurable: true, + enumerable: true, + }); } this.error = e; } diff --git a/tests/data.js b/tests/data.js index f249811..9b9b085 100644 --- a/tests/data.js +++ b/tests/data.js @@ -142,5 +142,114 @@ export default { }, ], }, + { + name: "data accessor", + tests: [ + { + name: "Called with this.args", + tests: [ + { + get data () { + return { doubled: this.args[0] * 2 }; + }, + run () { + return this.data.doubled; + }, + arg: 5, + expect: 10, + }, + ], + }, + { + name: "Inherited via accessor descriptor", + run () { + return this.data.x; + }, + tests: [ + { + get data () { + return { x: 7 }; + }, + tests: [{ expect: 7 }], + }, + ], + }, + { + name: "Fresh data per test (accessor)", + run () { + this.data.items.push("foo"); + return this.data.items.length; + }, + tests: [ + { + get data () { + return { items: [] }; + }, + tests: [{ expect: 1 }, { expect: 1 }], + }, + ], + }, + { + name: "Literal data wins over inherited data accessor", + run () { + return this.data.x; + }, + tests: [ + { + get data () { + return { x: "generated" }; + }, + tests: [{ data: { x: "literal" }, expect: "literal" }], + }, + ], + }, + { + name: "Merges with parent data", + data: { parent: 1 }, + run () { + return { parent: this.data.parent, child: this.data.child }; + }, + tests: [ + { + get data () { + return { child: 2 }; + }, + expect: { parent: 1, child: 2 }, + }, + ], + }, + { + name: "Getter failure falls through to empty data", + run () { + return Object.keys(this.data).length; + }, + tests: [ + { + get data () { + throw new Error("Fail"); + }, + expect: 0, + }, + ], + }, + { + name: "Accessor wins over getData when both defined", + run () { + return this.data.x; + }, + tests: [ + { + get data () { + return { x: "from accessor" }; + }, + getData () { + return { x: "from getData" }; + }, + tests: [{ expect: "from accessor" }], + }, + ], + }, + ], + }, ], }; diff --git a/tests/getters.js b/tests/getters.js index a9d9b80..7c84201 100644 --- a/tests/getters.js +++ b/tests/getters.js @@ -98,5 +98,116 @@ export default { }, ], }, + { + name: "name accessor", + run () { + return this.name; + }, + tests: [ + { + name: "Called with test args via this.args", + tests: [ + { + get name () { + return "Test " + this.args[0]; + }, + arg: "foo", + expect: "Test foo", + }, + ], + }, + { + name: "Inherited from parent accessor descriptor", + tests: [ + { + get name () { + return "Test " + this.args[0]; + }, + tests: [{ arg: "bar", expect: "Test bar" }], + }, + ], + }, + { + name: "Explicit name literal wins over inherited accessor", + tests: [ + { + get name () { + return "generated"; + }, + tests: [{ name: "explicit", expect: "explicit" }], + }, + ], + }, + { + name: "Failure falls through to default name (args[0])", + tests: [ + { + get name () { + throw new Error(); + }, + tests: [{ arg: 42, expect: "42" }], + }, + ], + }, + { + name: "Accessor wins over getName when both defined", + tests: [ + { + get name () { + return "from accessor"; + }, + getName () { + return "from getName"; + }, + tests: [{ arg: "x", expect: "from accessor" }], + }, + ], + }, + ], + }, + { + name: "expect accessor", + tests: [ + { + name: "Called with test args via this.args", + run (x) { + return x * 2; + }, + tests: [ + { + get expect () { + return this.args[0] * 2; + }, + arg: 5, + }, + ], + }, + { + name: "Inherited from parent accessor descriptor", + run (x) { + return x * 2; + }, + tests: [ + { + get expect () { + return this.args[0] * 2; + }, + tests: [{ arg: 7 }], + }, + ], + }, + { + name: "Failure falls through to default expect (args[0])", + tests: [ + { + get expect () { + throw new Error(); + }, + tests: [{ args: ["foo", "bar"], expect: "foo" }], + }, + ], + }, + ], + }, ], };