From 7a1e0df93c8bccbd2779425afc83915588694e45 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Tue, 26 May 2026 16:09:35 +0200 Subject: [PATCH 1/7] Lazy property resolution, accessor support for name/data/expect, closes #152 Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 4 +- SKILL.md | 56 +++++-- docs/README.md | 8 +- docs/define/README.md | 76 ++++++---- src/classes/Test.js | 165 ++++++++++++++------ src/util.js | 31 ++++ tests/accessors.js | 345 ++++++++++++++++++++++++++++++++++++++++++ tests/properties.js | 96 ++++++++++++ 8 files changed, 687 insertions(+), 94 deletions(-) create mode 100644 tests/accessors.js create mode 100644 tests/properties.js diff --git a/CLAUDE.md b/CLAUDE.md index 81da77e..68e8823 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -61,6 +61,8 @@ beforeEach run afterEach map check getName getData args expect getExpec 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. diff --git a/SKILL.md b/SKILL.md index 2a5443b..6d29b14 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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 @@ -179,14 +178,49 @@ 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 + +`name`, `data`, and `expect` support native JS getter syntax. `name` and `data` also support function shorthand (method syntax). For `name` and `data`, getters/shorthands are inherited by children (literal values are not). For `expect`, both literal and getter forms are inherited. + +| Property | Getter | Function shorthand | Notes | +|----------|--------|--------------------|-------| +| `name` | `get name () { ... }` | `name () { ... }` | Literal `name` is not inherited; getter/shorthand is | +| `data` | `get data () { ... }` | `data () { ... }` | Returns an object whose properties are merged onto `this.data` | +| `expect` | `get expect () { ... }` | **Not supported** | Both literal and getter `expect` are inherited. No shorthand — `expect` can be a function value | + +```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 @@ -335,13 +369,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 () { diff --git a/docs/README.md b/docs/README.md index 58c292a..7512e81 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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: [ { @@ -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. diff --git a/docs/define/README.md b/docs/define/README.md index 72c77e6..11615f2 100644 --- a/docs/define/README.md +++ b/docs/define/README.md @@ -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. | @@ -98,24 +97,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 (`get data()`) or function shorthand (`data()`). +The getter/shorthand is called with the same context as `run()` and returns an object whose 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; @@ -129,26 +125,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 (`get name()`) or function shorthand (`name()`): -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 } @@ -167,17 +172,32 @@ 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 (`get expect()`): + +```js +{ + get expect () { + return this.arg.toUpperCase(); + }, + run: toUpperCase, + tests: [ + { arg: "foo" }, + { arg: "bar" }, + ], +} +``` + +Unlike `name` and `data`, `expect` does **not** support function shorthand — because `expect` can legitimately be a function value. +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 } diff --git a/src/classes/Test.js b/src/classes/Test.js index 0da162b..e986ee0 100644 --- a/src/classes/Test.js +++ b/src/classes/Test.js @@ -1,5 +1,5 @@ import * as check from "../check.js"; -import { stringify } from "../util.js"; +import { stringify, defineLazyProperty } from "../util.js"; const INHERITED_PROPS = [ "beforeEach", @@ -23,8 +23,6 @@ const INHERITED_PROPS = [ * Represents a single test or a group of tests */ export default class Test { - data = {}; - constructor (test, parent) { if (!test) { console.warn("Empty test: ", test); @@ -63,48 +61,69 @@ export default class Test { } Object.defineProperties(this, descriptors); - if (typeof this.data === "function") { - this.getData = this.data; - this.data = {}; - } - - this.data = Object.create( - this.parent?.data ?? null, - Object.getOwnPropertyDescriptors(this.data), - ); - this.originalName = this.name; - - if (typeof this.name === "function") { - this.getName = this.name; - delete this.name; + // expect is getter-only — function shorthand is NOT converted because + // expect can legitimately be a function value + let converted = new Set(); + for (let prop of ["name", "data", "expect"]) { + let accessor = + Object.getOwnPropertyDescriptor(this, prop)?.get ?? + (prop !== "expect" && typeof this[prop] === "function" && this[prop]); + if (accessor) { + let key = "get" + prop[0].toUpperCase() + prop.slice(1); + this[key] = accessor; + delete this[prop]; + converted.add(prop); + } } // Inherit properties from parent // This works recursively because the parent constructor runs before its children if (this.parent) { for (let prop of INHERITED_PROPS) { - if (!(prop in this) && prop in this.parent) { - this[prop] = this.parent[prop]; + // converted guard: get expect() → getExpect deletes expect; without it the parent's value would be inherited back + if (!(prop in this) && prop in this.parent && !converted.has(prop)) { + Object.defineProperty( + this, + prop, + Object.getOwnPropertyDescriptor(this.parent, prop), + ); } } } - if (!this.check) { - this.check = check.equals; - } - else if (typeof this.check === "object") { - let { deep, ...options } = this.check; - let shallowEquals = check.shallowEquals(options); - this.check = deep ? check.deep(shallowEquals) : shallowEquals; - } - + // Lazy args (arg takes precedence over inherited args) if ("arg" in this) { - // Single argument - this.args = [this.arg]; + let getter = Object.getOwnPropertyDescriptor(this, "arg")?.get; + if (getter) { + defineLazyProperty(this, "args", function () { + try { + return [getter.call(this)]; + } + catch { + return []; + } + }); + } + else { + // Single argument + this.args = [this.arg]; + } } else if ("args" in this) { + let getter = Object.getOwnPropertyDescriptor(this, "args")?.get; + if (getter) { + defineLazyProperty(this, "args", function () { + try { + let args = getter.call(this); + return Array.isArray(args) ? args : [args]; + } + catch { + return []; + } + }); + } // Single args don't need to be wrapped in an array - if (!Array.isArray(this.args)) { + else if (!Array.isArray(this.args)) { this.args = [this.args]; } } @@ -113,26 +132,57 @@ 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)); + if (!("check" in this)) { + this.check = check.equals; + } + else { + let getter = Object.getOwnPropertyDescriptor(this, "check")?.get; + if (getter) { + defineLazyProperty(this, "check", function () { + let value; + try { + value = getter.call(this); + } + catch {} + + if (value && typeof value === "object") { + let { deep = true, ...options } = value; + let shallow = check.shallowEquals(options); + return deep ? check.deep(shallow) : shallow; + } + // Falsy or non-callable values (e.g. getter threw) fall back to default + return typeof value === "function" ? value : check.equals; + }); + } + else if (typeof this.check === "object") { + let { deep = true, ...options } = this.check; + let shallow = check.shallowEquals(options); + this.check = deep ? check.deep(shallow) : shallow; + } + // Falsy or non-callable values (e.g. check: false) fall back to default + else if (typeof this.check !== "function") { + this.check = check.equals; } - catch {} } - if (!this.name) { - if (this.getName) { + // Prototype chain deferred — avoids triggering parent getters; data getters already converted to getData above + let ownData = this.data ?? {}; + defineLazyProperty(this, "data", function () { + let data = Object.create( + this.parent?.data ?? null, + Object.getOwnPropertyDescriptors(ownData), + ); + + if (this.getData && Object.keys(data).length === 0) { try { - this.name = this.getName.apply(this, this.args); + let computed = this.getData.apply(this, this.args); + Object.defineProperties(data, Object.getOwnPropertyDescriptors(computed)); } catch {} } - if (!this.name && this.isTest) { - this.name = this.args.length > 0 ? stringify(this.args[0]) : "(No args)"; - } - } + return data; + }); if (this.isGroup) { this.tests = this.tests @@ -140,17 +190,32 @@ export default class Test { .map(t => (t instanceof Test ? t : new Test(t, this))); } - if (!("expect" in this)) { - if (this.getExpect) { + if (!this.name && (this.getName || this.isTest)) { + defineLazyProperty(this, "name", function () { + let name; try { - this.expect = this.getExpect.apply(this, this.args); + name = this.getName?.apply(this, this.args); } catch {} - } - if (!("expect" in this)) { - this.expect = this.args[0]; - } + if (!name && this.isTest) { + name = this.args.length > 0 ? stringify(this.args[0]) : "(No args)"; + } + + return name; + }); + } + + if (!("expect" in this)) { + defineLazyProperty(this, "expect", function () { + if (this.getExpect) { + try { + return this.getExpect.apply(this, this.args); + } + catch {} + } + return this.args[0]; + }); } } diff --git a/src/util.js b/src/util.js index 37b56cf..d3c0cae 100644 --- a/src/util.js +++ b/src/util.js @@ -247,3 +247,34 @@ export async function interceptConsole (fn) { export function pluralize (n, singular, plural) { return n === 1 ? singular : plural; } + +/** + * Define a property that resolves on first access and caches the result. + * @param {object} obj Object to define the property on + * @param {string} prop Property name + * @param {function} resolve Called with `this` bound to the accessing object. Must not read `obj[prop]` (would recurse) + */ +export function defineLazyProperty (obj, prop, resolve) { + Object.defineProperty(obj, prop, { + get () { + let value = resolve.call(this); + Object.defineProperty(this, prop, { + value, + writable: true, + enumerable: true, + configurable: true, + }); + return value; + }, + set (value) { + Object.defineProperty(this, prop, { + value, + writable: true, + enumerable: true, + configurable: true, + }); + }, + enumerable: true, + configurable: true, + }); +} diff --git a/tests/accessors.js b/tests/accessors.js new file mode 100644 index 0000000..53de944 --- /dev/null +++ b/tests/accessors.js @@ -0,0 +1,345 @@ +import Test from "../src/classes/Test.js"; + +let add = (a, b) => a + b; + +export default { + name: "Accessors", + tests: [ + { + name: "Getters do not fire during Test construction", + run () { + let log = []; + new Test({ + get arg () { + log.push("arg"); + return 42; + }, + get expect () { + log.push("expect"); + return 84; + }, + get name () { + log.push("name"); + return "test"; + }, + get check () { + log.push("check"); + return (a, b) => a === b; + }, + get data () { + log.push("data"); + return { x: 1 }; + }, + run (x) { + return x * 2; + }, + }); + return log; + }, + expect: [], + }, + { + name: "Getters resolve to correct values on access", + run () { + let test = new Test({ + get arg () { + return 42; + }, + get expect () { + return 84; + }, + run (x) { + return x * 2; + }, + }); + return { args: test.args, expect: test.expect }; + }, + expect: { args: [42], expect: 84 }, + }, + { + name: "get arg() — children inherit resolved value", + run () { + let test = new Test({ + get arg () { + return 42; + }, + run (x) { + return x; + }, + tests: [{}], + }); + return test.tests[0].args; + }, + expect: [42], + }, + { + name: "Parent getters do not fire during child construction", + run () { + let log = []; + new Test({ + get data () { + log.push("parent-data"); + return { x: 1 }; + }, + get name () { + log.push("parent-name"); + return "parent"; + }, + run (x) { + return x; + }, + tests: [{ arg: 1 }, { arg: 2 }], + }); + return log; + }, + expect: [], + }, + { + name: "get name() — inherited by children", + run () { + let test = new Test({ + get name () { + return "computed " + this.arg; + }, + run (x) { + return x; + }, + tests: [{ arg: 7 }], + }); + return test.tests[0].name; + }, + expect: "computed 7", + }, + { + name: "get name() — own getter wins over inherited getName", + run () { + let test = new Test({ + getName () { + return "parent"; + }, + run (x) { + return x; + }, + tests: [ + { + get name () { + return "child"; + }, + arg: 1, + }, + ], + }); + return test.tests[0].name; + }, + expect: "child", + }, + { + name: "get name() — failure falls through to default", + run () { + let test = new Test({ + get name () { + throw new Error(); + }, + arg: 42, + }); + return test.name; + }, + expect: "42", + }, + { + name: "get name() — fails on group without run, works on children", + run () { + let test = new Test({ + get name () { + return this.run.name + "()"; + }, + tests: [{ + run: function foo () { + return 42; + }, + arg: "foo", + }], + }); + return test.tests[0].name; + }, + expect: "foo()", + }, + { + name: "get expect() — accesses test args", + get expect () { + return this.arg * 2; + }, + run (x) { + return x * 2; + }, + arg: 5, + }, + { + name: "get expect() — explicit expect wins over inherited getter", + get expect () { + return "generated"; + }, + run () { + return "explicit"; + }, + tests: [{ arg: "foo", expect: "explicit" }], + }, + { + name: "get expect() — own getter wins over inherited plain expect", + run () { + let test = new Test({ + expect: "parent", + run () { + return "child"; + }, + tests: [ + { + get expect () { + return "child"; + }, + }, + ], + }); + return test.tests[0].expect; + }, + expect: "child", + }, + { + name: "get expect() — failure falls through to args[0]", + get expect () { + throw new Error(); + }, + run (x) { + return x; + }, + arg: "foo", + }, + { + name: "get data() — fresh per test", + description: "Each sibling gets its own data object, mutations don't leak", + run () { + let test = new Test({ + get data () { + return { items: [] }; + }, + run () { + return this.data; + }, + tests: [{}, {}], + }); + test.tests[0].data.items.push("a"); + return { first: test.tests[0].data.items, second: test.tests[1].data.items }; + }, + expect: { first: ["a"], second: [] }, + }, + { + name: "get data() — merges with parent data", + run () { + let test = new Test({ + data: { parent: 1 }, + tests: [ + { + get data () { + return { child: 2 }; + }, + run () { + return this.data; + }, + tests: [{}], + }, + ], + }); + let data = test.tests[0].data; + return { parent: data.parent, child: data.child }; + }, + expect: { parent: 1, child: 2 }, + }, + { + name: "name() shorthand — inherited by children", + run () { + let test = new Test({ + name () { + return "group " + this.level; + }, + tests: [{ arg: 42 }], + }); + return { parent: test.name, child: test.tests[0].name }; + }, + expect: { parent: "group 0", child: "group 1" }, + }, + { + name: "data() shorthand — provides fresh data per test", + run () { + let test = new Test({ + data () { + return { x: 42 }; + }, + run () { + return this.data.x; + }, + tests: [{}, {}], + }); + return { first: test.tests[0].data.x, second: test.tests[1].data.x }; + }, + expect: { first: 42, second: 42 }, + }, + { + name: "get arg() — failure falls through to empty args", + run () { + let test = new Test({ + get arg () { + throw new Error(); + }, + }); + return test.args; + }, + expect: [], + }, + { + name: "get data() — failure falls through to empty data", + run () { + let test = new Test({ + get data () { + throw new Error(); + }, + }); + return Object.keys(test.data); + }, + expect: [], + }, + { + name: "get check() — getter return value is used as check function", + run () { + let custom = () => "custom"; + let test = new Test({ + get check () { + return custom; + }, + }); + return test.check(); + }, + expect: "custom", + }, + { + name: "get check() — throwing getter falls back to equality", + run () { + let test = new Test({ + get check () { + throw new Error(); + }, + }); + return { same: test.check(1, 1), diff: test.check(1, 2) }; + }, + expect: { same: true, diff: false }, + }, + { + name: "Function-valued expect is not called as a factory", + description: "expect can be a function value (the expected result *is* a function)", + run () { + let test = new Test({ expect: add }); + return test.expect; + }, + expect: add, + }, + ], +}; diff --git a/tests/properties.js b/tests/properties.js new file mode 100644 index 0000000..055259d --- /dev/null +++ b/tests/properties.js @@ -0,0 +1,96 @@ +import Test from "../src/classes/Test.js"; + +export default { + name: "Defaults and inheritance", + tests: [ + { + name: "No name → auto-generated from args", + run () { + let test = new Test({ arg: 42 }); + return test.name; + }, + expect: "42", + }, + { + name: "No expect → defaults to args[0]", + run () { + let test = new Test({ arg: 42 }); + return test.expect; + }, + expect: 42, + }, + { + name: "expect inherits from parent", + run () { + let test = new Test({ + expect: "foo", + run () { + return "foo"; + }, + tests: [{}], + }); + return test.tests[0].expect; + }, + expect: "foo", + }, + { + name: "name string does not inherit", + run () { + let test = new Test({ + name: "Parent Group", + run (x) { + return x; + }, + tests: [{ arg: 42 }], + }); + return test.tests[0].name; + }, + expect: "42", + }, + { + name: "check: { subset: true } ignores extra properties", + run () { + let test = new Test({ check: { subset: true } }); + return test.check({ a: 1, b: 2 }, { a: 1 }); + }, + expect: true, + }, + { + name: "check: { deep: true } matches nested objects by value", + description: "Without deep, { b: 1 } !== { b: 1 } (different references)", + run () { + let test = new Test({ check: { deep: true } }); + return test.check({ a: { b: 1 } }, { a: { b: 1 } }); + }, + expect: true, + }, + { + name: "arg inherits via args", + run () { + let test = new Test({ + arg: 1, + run (x) { + return x; + }, + tests: [{}], + }); + return test.tests[0].args; + }, + expect: [1], + }, + { + name: "arg overrides inherited args", + run () { + let test = new Test({ + args: [1, 2], + run (x) { + return x; + }, + tests: [{ arg: 5 }], + }); + return test.tests[0].args; + }, + expect: [5], + }, + ], +}; From 10f1fa7f5339de981e90bea0e9831c31246c3ea5 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Tue, 26 May 2026 22:50:19 +0200 Subject: [PATCH 2/7] Property-agnostic getter/shorthand conversion, generic lazy resolver Co-Authored-By: Claude Opus 4.6 (1M context) --- src/classes/Test.js | 168 +++++++++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 71 deletions(-) diff --git a/src/classes/Test.js b/src/classes/Test.js index e986ee0..7d5a556 100644 --- a/src/classes/Test.js +++ b/src/classes/Test.js @@ -7,11 +7,8 @@ const INHERITED_PROPS = [ "afterEach", "map", "check", - "getName", - "getData", "args", "expect", - "getExpect", "throws", "maxTime", "maxTimeAsync", @@ -19,6 +16,20 @@ const INHERITED_PROPS = [ "file", ]; +// Properties whose value can be a function — function shorthand not converted to getXXX +const NO_SHORTHAND = [ + "arg", + "expect", + "run", + "beforeEach", + "afterEach", + "beforeAll", + "afterAll", + "check", + "map", + "throws", +]; + /** * Represents a single test or a group of tests */ @@ -61,18 +72,23 @@ export default class Test { } Object.defineProperties(this, descriptors); - // expect is getter-only — function shorthand is NOT converted because - // expect can legitimately be a function value + // Convert getters and function shorthands to getXXX + // Getters are always converted; function shorthands only for non-function-valued props let converted = new Set(); - for (let prop of ["name", "data", "expect"]) { + for (let key of Object.keys(descriptors)) { + // getXXX are already getter functions (legacy API or from parent conversion) + if (/^get[A-Z]/.test(key)) { + continue; + } + let accessor = - Object.getOwnPropertyDescriptor(this, prop)?.get ?? - (prop !== "expect" && typeof this[prop] === "function" && this[prop]); + Object.getOwnPropertyDescriptor(this, key)?.get ?? + (!NO_SHORTHAND.includes(key) && typeof this[key] === "function" && this[key]); if (accessor) { - let key = "get" + prop[0].toUpperCase() + prop.slice(1); - this[key] = accessor; - delete this[prop]; - converted.add(prop); + let prop = "get" + key[0].toUpperCase() + key.slice(1); + this[prop] = accessor; + delete this[key]; + converted.add(key); } } @@ -80,7 +96,8 @@ export default class Test { // This works recursively because the parent constructor runs before its children if (this.parent) { for (let prop of INHERITED_PROPS) { - // converted guard: get expect() → getExpect deletes expect; without it the parent's value would be inherited back + // Conversion deletes the original prop; without this guard + // the parent's value would be inherited back over the child's getter if (!(prop in this) && prop in this.parent && !converted.has(prop)) { Object.defineProperty( this, @@ -89,41 +106,40 @@ export default class Test { ); } } + + // Inherit getXXX properties (from getter/shorthand conversion) + for (let key of Object.keys(this.parent)) { + if (/^get[A-Z]/.test(key) && !(key in this)) { + Object.defineProperty( + this, + key, + Object.getOwnPropertyDescriptor(this.parent, key), + ); + } + } } // Lazy args (arg takes precedence over inherited args) - if ("arg" in this) { - let getter = Object.getOwnPropertyDescriptor(this, "arg")?.get; - if (getter) { - defineLazyProperty(this, "args", function () { - try { - return [getter.call(this)]; - } - catch { - return []; - } - }); - } - else { - // Single argument - this.args = [this.arg]; - } + if (this.getArg) { + converted.delete("arg"); + defineLazyProperty(this, "args", function () { + return [this.getArg()]; + }); + } + else if ("arg" in this) { + // Single argument + this.args = [this.arg]; + } + else if (this.getArgs) { + converted.delete("args"); + defineLazyProperty(this, "args", function () { + let args = this.getArgs(); + return Array.isArray(args) ? args : [args]; + }); } else if ("args" in this) { - let getter = Object.getOwnPropertyDescriptor(this, "args")?.get; - if (getter) { - defineLazyProperty(this, "args", function () { - try { - let args = getter.call(this); - return Array.isArray(args) ? args : [args]; - } - catch { - return []; - } - }); - } // Single args don't need to be wrapped in an array - else if (!Array.isArray(this.args)) { + if (!Array.isArray(this.args)) { this.args = [this.args]; } } @@ -132,41 +148,40 @@ export default class Test { this.args = []; } - if (!("check" in this)) { + if (this.getCheck) { + converted.delete("check"); + defineLazyProperty(this, "check", function () { + let value; + try { + value = this.getCheck(); + } + catch {} + + if (value && typeof value === "object") { + let { deep = true, ...options } = value; + let shallow = check.shallowEquals(options); + return deep ? check.deep(shallow) : shallow; + } + // Falsy or non-callable values (e.g. getter threw) fall back to default + return typeof value === "function" ? value : check.equals; + }); + } + else if (!("check" in this)) { this.check = check.equals; } - else { - let getter = Object.getOwnPropertyDescriptor(this, "check")?.get; - if (getter) { - defineLazyProperty(this, "check", function () { - let value; - try { - value = getter.call(this); - } - catch {} - - if (value && typeof value === "object") { - let { deep = true, ...options } = value; - let shallow = check.shallowEquals(options); - return deep ? check.deep(shallow) : shallow; - } - // Falsy or non-callable values (e.g. getter threw) fall back to default - return typeof value === "function" ? value : check.equals; - }); - } - else if (typeof this.check === "object") { - let { deep = true, ...options } = this.check; - let shallow = check.shallowEquals(options); - this.check = deep ? check.deep(shallow) : shallow; - } - // Falsy or non-callable values (e.g. check: false) fall back to default - else if (typeof this.check !== "function") { - this.check = check.equals; - } + else if (typeof this.check === "object") { + let { deep = true, ...options } = this.check; + let shallow = check.shallowEquals(options); + this.check = deep ? check.deep(shallow) : shallow; + } + // Falsy or non-callable values (e.g. check: false) fall back to default + else if (typeof this.check !== "function") { + this.check = check.equals; } // Prototype chain deferred — avoids triggering parent getters; data getters already converted to getData above let ownData = this.data ?? {}; + converted.delete("data"); defineLazyProperty(this, "data", function () { let data = Object.create( this.parent?.data ?? null, @@ -184,6 +199,17 @@ export default class Test { return data; }); + // Generic lazy resolution for converted properties without custom resolvers + // name/expect are handled below (they have fallback logic) + converted.delete("name"); + converted.delete("expect"); + for (let prop of converted) { + let key = "get" + prop[0].toUpperCase() + prop.slice(1); + defineLazyProperty(this, prop, function () { + return this[key].apply(this, this.args); + }); + } + if (this.isGroup) { this.tests = this.tests .filter(Boolean) From 853e2b30ee21d59b91c52a8bfe3f77b0455a103d Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Tue, 26 May 2026 23:53:07 +0200 Subject: [PATCH 3/7] Update docs and SKILL.md for generic accessor support Co-Authored-By: Claude Opus 4.6 (1M context) --- SKILL.md | 21 +++++++++++++++------ docs/define/README.md | 17 ++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/SKILL.md b/SKILL.md index 6d29b14..72ef741 100644 --- a/SKILL.md +++ b/SKILL.md @@ -180,13 +180,22 @@ If a hook throws, the test is **skipped** (not failed). Hooks are infrastructure ### Accessor Support -`name`, `data`, and `expect` support native JS getter syntax. `name` and `data` also support function shorthand (method syntax). For `name` and `data`, getters/shorthands are inherited by children (literal values are not). For `expect`, both literal and getter forms are inherited. +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`. -| Property | Getter | Function shorthand | Notes | -|----------|--------|--------------------|-------| -| `name` | `get name () { ... }` | `name () { ... }` | Literal `name` is not inherited; getter/shorthand is | -| `data` | `get data () { ... }` | `data () { ... }` | Returns an object whose properties are merged onto `this.data` | -| `expect` | `get expect () { ... }` | **Not supported** | Both literal and getter `expect` are inherited. No shorthand — `expect` can be a function value | +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 { diff --git a/docs/define/README.md b/docs/define/README.md index 11615f2..6320f02 100644 --- a/docs/define/README.md +++ b/docs/define/README.md @@ -39,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). | +## 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 } @@ -103,8 +111,8 @@ 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. -To compute data dynamically, use a getter (`get data()`) or function shorthand (`data()`). -The getter/shorthand is called with the same context as `run()` and returns an object whose properties are merged onto `this.data`. +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. This is useful for providing fresh per-test data without `beforeEach()`: @@ -130,7 +138,7 @@ This is useful for providing fresh per-test data without `beforeEach()`: `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. -To compute names dynamically, use a getter (`get name()`) or function shorthand (`name()`): +To compute names dynamically, use a [getter or function shorthand](#accessors): ```js { @@ -177,7 +185,7 @@ 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]`. -To compute expected values dynamically, use a getter (`get expect()`): +To compute expected values dynamically, use a [getter](#accessors) (`expect` does not support function shorthand — it can legitimately be a function value): ```js { @@ -192,7 +200,6 @@ To compute expected values dynamically, use a getter (`get expect()`): } ``` -Unlike `name` and `data`, `expect` does **not** support function shorthand — because `expect` can legitimately be a function value. Both literal and getter `expect` are inherited by children. If the expect getter throws an error, the error is caught and `expect` falls through to its default (`args[0]`). From 704fa682484833f562ca932f7ae8fb1d7a27a73a Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Wed, 27 May 2026 00:05:28 +0200 Subject: [PATCH 4/7] Deduplicate shorthand tests, add function shorthand args test Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/accessors.js | 28 ++++++++++++++++------------ tests/getters.js | 13 ------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/tests/accessors.js b/tests/accessors.js index 53de944..f041e12 100644 --- a/tests/accessors.js +++ b/tests/accessors.js @@ -283,18 +283,6 @@ export default { }, expect: { first: 42, second: 42 }, }, - { - name: "get arg() — failure falls through to empty args", - run () { - let test = new Test({ - get arg () { - throw new Error(); - }, - }); - return test.args; - }, - expect: [], - }, { name: "get data() — failure falls through to empty data", run () { @@ -332,6 +320,22 @@ export default { }, expect: { same: true, diff: false }, }, + { + name: "Function shorthand receives test args", + run () { + let test = new Test({ + skip (x) { + return x > 100; + }, + run (x) { + return x; + }, + tests: [{ arg: 5 }, { arg: 200 }], + }); + return [test.tests[0].skip, test.tests[1].skip]; + }, + expect: [false, true], + }, { name: "Function-valued expect is not called as a factory", description: "expect can be a function value (the expected result *is* a function)", diff --git a/tests/getters.js b/tests/getters.js index a9d9b80..0191af4 100644 --- a/tests/getters.js +++ b/tests/getters.js @@ -30,19 +30,6 @@ export default { }, tests: [{ arg: 42, expect: "42" }], }, - { - name: "name() function shorthand", - run () { - let t = new Test({ - name () { - return "group " + this.level; - }, - tests: [{ arg: 42 }], - }); - return { parent: t.name, child: t.tests[0].name }; - }, - expect: { parent: "group 0", child: "group 1" }, - }, { name: "Failure on group with no run() does not crash children (issue #119)", run () { From 7065aa0a75e55a7ca93d96646fbebac40525de65 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Wed, 27 May 2026 09:36:40 +0200 Subject: [PATCH 5/7] Revert shallowEquals variable rename back to original name Co-Authored-By: Claude Opus 4.6 (1M context) --- src/classes/Test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/classes/Test.js b/src/classes/Test.js index 7d5a556..392e766 100644 --- a/src/classes/Test.js +++ b/src/classes/Test.js @@ -159,8 +159,8 @@ export default class Test { if (value && typeof value === "object") { let { deep = true, ...options } = value; - let shallow = check.shallowEquals(options); - return deep ? check.deep(shallow) : shallow; + let shallowEquals = check.shallowEquals(options); + return deep ? check.deep(shallowEquals) : shallowEquals; } // Falsy or non-callable values (e.g. getter threw) fall back to default return typeof value === "function" ? value : check.equals; @@ -171,8 +171,8 @@ export default class Test { } else if (typeof this.check === "object") { let { deep = true, ...options } = this.check; - let shallow = check.shallowEquals(options); - this.check = deep ? check.deep(shallow) : shallow; + let shallowEquals = check.shallowEquals(options); + this.check = deep ? check.deep(shallowEquals) : shallowEquals; } // Falsy or non-callable values (e.g. check: false) fall back to default else if (typeof this.check !== "function") { From 8d29615db950e6e05966de6fc27fe3637dfa0824 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Wed, 27 May 2026 11:30:16 +0200 Subject: [PATCH 6/7] Update CLAUDE.md inherited props list to match INHERITED_PROPS Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 68e8823..be2dcac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ 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). From 722c5082aa967a78e0ff9a45bbd68e0bc67d61c6 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Wed, 27 May 2026 12:17:00 +0200 Subject: [PATCH 7/7] Define lazy arg property for get arg(), revert deep default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get arg() was converted to getArg but no lazy arg was defined, so this.arg was undefined inside other lazy accessors like get expect(). Define lazy arg alongside lazy args to fix. Also revert deep default in check object shorthand — the PR accidentally changed it from undefined (falsy) to true. Remove unrelated check tests from properties.js. Co-Authored-By: Claude Opus 4.6 (1M context) --- SKILL.md | 8 ++++---- src/classes/Test.js | 10 +++++++--- tests/accessors.js | 12 ++++++++++++ tests/properties.js | 17 ----------------- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/SKILL.md b/SKILL.md index 72ef741..99cde53 100644 --- a/SKILL.md +++ b/SKILL.md @@ -102,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: @@ -120,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: diff --git a/src/classes/Test.js b/src/classes/Test.js index 392e766..cb4c27b 100644 --- a/src/classes/Test.js +++ b/src/classes/Test.js @@ -122,8 +122,12 @@ export default class Test { // Lazy args (arg takes precedence over inherited args) if (this.getArg) { converted.delete("arg"); + defineLazyProperty(this, "arg", function () { + return this.getArg(); + }); + // Can't fall through to `this.args = [this.arg]` below — that would eagerly trigger the getter defineLazyProperty(this, "args", function () { - return [this.getArg()]; + return [this.arg]; }); } else if ("arg" in this) { @@ -158,7 +162,7 @@ export default class Test { catch {} if (value && typeof value === "object") { - let { deep = true, ...options } = value; + let { deep, ...options } = value; let shallowEquals = check.shallowEquals(options); return deep ? check.deep(shallowEquals) : shallowEquals; } @@ -170,7 +174,7 @@ export default class Test { this.check = check.equals; } else if (typeof this.check === "object") { - let { deep = true, ...options } = this.check; + let { deep, ...options } = this.check; let shallowEquals = check.shallowEquals(options); this.check = deep ? check.deep(shallowEquals) : shallowEquals; } diff --git a/tests/accessors.js b/tests/accessors.js index f041e12..4f13dcf 100644 --- a/tests/accessors.js +++ b/tests/accessors.js @@ -174,6 +174,18 @@ export default { }, arg: 5, }, + { + name: "get arg() — lazy arg property resolves on access", + run () { + let test = new Test({ + get arg () { + return 42; + }, + }); + return test.arg; + }, + expect: 42, + }, { name: "get expect() — explicit expect wins over inherited getter", get expect () { diff --git a/tests/properties.js b/tests/properties.js index 055259d..e69983f 100644 --- a/tests/properties.js +++ b/tests/properties.js @@ -47,23 +47,6 @@ export default { }, expect: "42", }, - { - name: "check: { subset: true } ignores extra properties", - run () { - let test = new Test({ check: { subset: true } }); - return test.check({ a: 1, b: 2 }, { a: 1 }); - }, - expect: true, - }, - { - name: "check: { deep: true } matches nested objects by value", - description: "Without deep, { b: 1 } !== { b: 1 } (different references)", - run () { - let test = new Test({ check: { deep: true } }); - return test.check({ a: { b: 1 } }, { a: { b: 1 } }); - }, - expect: true, - }, { name: "arg inherits via args", run () {