@ultimat3/testing 19.2.0 → 19.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -0
- package/README.md +11 -0
- package/package.json +12 -12
- package/src/errors.ts +1 -1
- package/src/fixture-island.ts +35 -2
- package/src/matchers.ts +31 -17
package/CLAUDE.md
CHANGED
|
@@ -27,6 +27,8 @@ is its own entry point and not part of the barrel.
|
|
|
27
27
|
| A FIXED interval, and never a curve | doubling the gap makes the last look land long after the state changed, and the caller's deadline is the contract. There is exactly one backoff curve in the framework (`@ultimat3/core`'s `backoff.ts`) and this is deliberately not a second one. A zero or negative interval is REFUSED: the failure mode is a test that never fails — it hangs, and CI reports a runner timeout with no assertion in it |
|
|
28
28
|
| A matcher that may throw a coded error MUST NOT be `async` | measured against Bun 1.4.0: an `async` matcher has any error it throws REPLACED by bun's own `Matcher \`x\` returned a promise that rejected`, so the code, the cause and the fix are gone before a reader sees them. `X_TEST_SCHEMA_EXPECTED` and `X_TEST_JOB_EXPECTED` were declared, registered, titled — and unreachable by any caller for their whole life, because nothing asserted them. The shape is a SYNCHRONOUS prologue that validates the receiver (`assertStandardSchema`, `assertJobDeclaration`, `assertVisibilityProbe`) and a promise returned after it. `matcher-receiver.test.ts` pins all four |
|
|
29
29
|
| Wrong receiver throws, wrong value returns | a matcher handed a page where a locator belongs is not FALSE, it is unanswerable — `pass: false` would read as "the element was hidden". A wrong-shaped receiver is a coded throw; a wrong value is `result(false, …)` |
|
|
30
|
+
| A matcher MESSAGE is a thunk, and never `JSON.stringify` | `result(pass, () => …)` — `expect.extend` reads `message()` only on the wrong verdict, and the string used to be built EAGERLY. `JSON.stringify` refuses a BigInt and throws on anything cyclic, so `expect(schema).toRejectInput({ n: 1n })` — a schema that DID reject — came back as ``Matcher `toRejectInput` returned a promise that rejected``, with the real answer gone, on the PASSING path. Three matchers quoted their input that way. `renderCauseValue` (`@ultimat3/core`) is the renderer: it quotes what JSON can quote and names the shape of what it cannot, so the message still reads `expected the schema to reject {"id":"ok"}` |
|
|
31
|
+
| Every message thunk needs a test that PROVOKES it | a thunk is a function, so an unread message is an UNCOVERED function and `bun run scripts/coverage-gate.ts --package testing` is what says so — the eleven thunks landed with two of them read and took the package from 95.62% to 94.03%. That number is the useful half: `.not` on a `pass: false` never asks for the message, so `expect({ paths: {} }).not.toMatchOpenApi(…)` passes with the type guard deleted, and the same test reads as covering the matcher. `messageOf()` in `matchers.test.ts` awaits the FAILING side and asserts the sentence, whichever way bun delivers it |
|
|
30
32
|
| Test names | the filename picks the step; `testName(type, name)` on the outer `describe` puts that type on every failure line under it. Never on the inner `test` too — the prefix would print twice |
|
|
31
33
|
| `test` here is `fixtureTest`, and it takes no timeout | `(name, body)`, nothing else (`fixtures.ts:248`, exported as `test` by `index.ts:107`) — `bunTest` is called with two arguments, so bun's own third one is unreachable and no case can ask for more than the default 5s. Slow work goes in `beforeAll(fn, 60_000)`, which is `bun:test`'s own re-export and does take one; that is where both generated island tests and `examples/dummy/apps/web/app/settings/settings.island.test.ts` build their chunk, once, for every case to share — a Babel pass plus a browser bundle is seconds. A case that needs 60s of its own is work sitting in the wrong place |
|
|
32
34
|
| Injection | `SqlRunner` and `connect` are parameters, so unit tests need no server |
|
|
@@ -69,6 +71,7 @@ is its own entry point and not part of the barrel.
|
|
|
69
71
|
| Shared examples | `behavesLike` calls `describe`, so it goes at declaration scope; bun rejects a `describe` inside a test body |
|
|
70
72
|
| An island needs a BUILDER, not an import | `buildIslands` is `@ultimat3/cli`'s and both packages are tier 5; the one declared edge is `cli → testing`, so the reverse is a `bun run boundaries` failure. `mountIsland({ build, root, file })` takes the function as a parameter and declares only the two fields it reads — `IslandChunkLike` is `{ file, code }`, so a CSS artifact, a source map or a dev/production flag the bundler grows is invisible here. Moving the bundler down a tier was the alternative and it drags `@babel/core` and `babel-preset-solid` with it, into a package whose whole point is being importable from tier 0 |
|
|
71
73
|
| `mountIsland` AWAITS `mount` | `IslandEntry['mount']` returns `unknown`, not `void`, and the call is awaited — the shipped runtime chains `import(e).then((m) => m.mount(el, props))` (`packages/render/src/hydrate.ts`) and only marks the element mounted when that settles, so an `async` mount is an ordinary island and `like.island.tsx` already is one. Typed `=> void` and called bare, the fixture returned before an island that opens a queue or a socket had rendered anything, so every assertion after it read an empty wrapper — and worse, the mount RESUMED after `restore()` had taken the fake `document` back out, failing with `document is not defined` inside whichever later test happened to be running. Not a breaking change: `IslandEntry` is module-private and an island module is matched structurally off an `unknown` import, so nothing implements it. |
|
|
74
|
+
| Dispose STOPS the island, then restores the globals | `mountIsland` keeps what `mount` resolved to and, when it is a function, calls it in `[Symbol.dispose]` BEFORE `restore()` — the island's disposer clears an interval whose callback reads `document`, or removes a listener from it, and must find the `document` it was made against. Solid's `render` returns a disposer, so `return render(…)` is an island's whole side of it (`X_TEST_ISLAND_NO_MOUNT`'s fix line and `x g island`'s template both say so). Restoring the globals was the whole teardown until 2026-09-07, and an island that polled on an interval kept ticking after the DOM was gone: measured in ai-maxxing as `document is not defined` thrown into whichever later test was running and as one file's fetch stub receiving another file's POSTs. Once — `using` and an `afterAll` that also disposes by hand are both real — and a disposer that throws still hands the globals back in a `finally`; the throw is the test's to see |
|
|
72
75
|
| The micro-DOM is the fixture's, once **for islands** | `island-dom.ts`. **"Once" is scoped to this job, and said so only from 2026-08-23**: `packages/ui/src/fake-dom.ts` is a second micro-DOM (201 lines, test-only, off that package's barrel) for a different one — focus, `activeElement`, `contains` and a `:not()` selector grammar for keyboard code, none of which parses a `<template>`. It is not a copy to collapse: `ui` is tier 4 and this package is tier 5, so `ui -> testing` is an upward import the boundary check refuses, and the merge would need the shared half moved down to a tier neither grammar belongs in. `bun test` has no DOM and no DOM library may be added; `generate: 'dom'` builds every element from `_$template("<label …>")`, so a stub without a parsed `<template>` cannot run one line of a compiled island. It lived twice, ~200 lines each, in `packages/cli/src/island-bundle.test.ts` and the reference app's island test |
|
|
73
76
|
| `style` and `classList` RECORD, `As of 2026-08` | `FakeStyle` is one declaration map behind all four spellings compiled Solid uses on one element: a STATIC entry baked into the template's `style=` attribute, a dynamic one through `setStyleProperty` → `style.setProperty`, a whole-object or string prop through `style()` → `cssText`, and a cleared one through `removeAttribute`. It was `style = {}` until 2026-08-21 — `<Form>`, `<Stack>`, `<Grid>` and `<Container>` each set a CSS custom property, so every one of them died inside `mount` with `e.style.setProperty is not a function`, and `x g resource` emitted a plain `<form>` rather than the design system's. A design-system component kept out of generated code by the limits of a TEST DOUBLE. A no-op `setProperty` would have stopped the crash and left "the component set `--form-gap`" unassertable, which is the same hole one layer down |
|
|
74
77
|
| `classList` is the class attribute | not a list of its own, so `classList.toggle` — what the compiler emits INLINE for `classList={{ … }}`, with no runtime helper in front of it — and `className` can never answer one element two ways. `add` was the only method the stand-in had and is the one Solid never calls |
|
package/README.md
CHANGED
|
@@ -303,6 +303,17 @@ removed.
|
|
|
303
303
|
`island[Symbol.dispose]()` in an `afterAll`. Left installed it hands a fake `document` to every
|
|
304
304
|
later FILE in the run.
|
|
305
305
|
|
|
306
|
+
**Dispose also stops the island** (`As of 2026-09-07`): when `mount` returns a function, that is
|
|
307
|
+
the island's disposer and dispose calls it — before the globals go back, so a disposer that
|
|
308
|
+
removes a listener from `document` or clears an interval whose callback reads it finds the same
|
|
309
|
+
`document` `mount` ran under. Solid's `render` already returns exactly that, so an island's whole
|
|
310
|
+
side of the contract is `return render(() => <Counter {...props} />, el)` — and a disposed root
|
|
311
|
+
runs every `onCleanup` inside it. Until this landed, restoring the globals was the whole of the
|
|
312
|
+
teardown: an island whose `mount` polled on an interval kept ticking after the DOM was gone,
|
|
313
|
+
which surfaced as `document is not defined` thrown into whichever later test happened to be
|
|
314
|
+
running, and as one file's fetch stub receiving POSTs from another file's island. A `mount` that
|
|
315
|
+
returns nothing disposes as it always did.
|
|
316
|
+
|
|
306
317
|
### Selectors
|
|
307
318
|
|
|
308
319
|
`find`, `all`, `text`, `fire`, `resize`, `scroll` and `observing` take one grammar, `As of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/testing",
|
|
3
|
-
"version": "19.2
|
|
3
|
+
"version": "19.3.2",
|
|
4
4
|
"description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,16 +33,16 @@
|
|
|
33
33
|
"test": "bun test"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ultimat3/cache": "19.2
|
|
37
|
-
"@ultimat3/core": "19.2
|
|
38
|
-
"@ultimat3/db": "19.2
|
|
39
|
-
"@ultimat3/entity": "19.2
|
|
40
|
-
"@ultimat3/i18n": "19.2
|
|
41
|
-
"@ultimat3/jobs": "19.2
|
|
42
|
-
"@ultimat3/mail": "19.2
|
|
43
|
-
"@ultimat3/policy": "19.2
|
|
44
|
-
"@ultimat3/query": "19.2
|
|
45
|
-
"@ultimat3/realtime": "19.2
|
|
46
|
-
"@ultimat3/time": "19.2
|
|
36
|
+
"@ultimat3/cache": "19.3.2",
|
|
37
|
+
"@ultimat3/core": "19.3.2",
|
|
38
|
+
"@ultimat3/db": "19.3.2",
|
|
39
|
+
"@ultimat3/entity": "19.3.2",
|
|
40
|
+
"@ultimat3/i18n": "19.3.2",
|
|
41
|
+
"@ultimat3/jobs": "19.3.2",
|
|
42
|
+
"@ultimat3/mail": "19.3.2",
|
|
43
|
+
"@ultimat3/policy": "19.3.2",
|
|
44
|
+
"@ultimat3/query": "19.3.2",
|
|
45
|
+
"@ultimat3/realtime": "19.3.2",
|
|
46
|
+
"@ultimat3/time": "19.3.2"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/errors.ts
CHANGED
|
@@ -466,7 +466,7 @@ export class IslandMountMissingError extends UltimateError {
|
|
|
466
466
|
fix:
|
|
467
467
|
component === undefined
|
|
468
468
|
? `${file} exports nothing a mount could render — x g island <name> --at apps/web/site writes an island whose mount() is already there, and its shape is the one to copy`
|
|
469
|
-
: `in ${file} add: import { render } from 'solid-js/web'; export function mount(el: HTMLElement, props: Parameters<typeof ${component}>[0]): void { el.textContent = ''; render(() => <${component} {...props} />, el); }`,
|
|
469
|
+
: `in ${file} add: import { render } from 'solid-js/web'; export function mount(el: HTMLElement, props: Parameters<typeof ${component}>[0]): () => void { el.textContent = ''; return render(() => <${component} {...props} />, el); }`,
|
|
470
470
|
});
|
|
471
471
|
}
|
|
472
472
|
}
|
package/src/fixture-island.ts
CHANGED
|
@@ -57,6 +57,10 @@ export interface MountIslandOptions {
|
|
|
57
57
|
readonly size?: ResizeInput;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* `Disposable`: dispose calls the disposer the island's `mount` returned (Solid's `render` answers
|
|
62
|
+
* one — `return render(…)`), then hands the process its globals back. Both, in that order.
|
|
63
|
+
*/
|
|
60
64
|
export interface MountedIsland extends Disposable {
|
|
61
65
|
/** The chunk's source, for asserting on what did or did not reach the browser. */
|
|
62
66
|
readonly code: string;
|
|
@@ -101,11 +105,19 @@ export interface MountedIsland extends Disposable {
|
|
|
101
105
|
* opens a queue or a socket first is therefore an ordinary island (`like.island.tsx` is `async`),
|
|
102
106
|
* and a fixture typed `=> void` could only ever call it and walk away. Widening, not narrowing: an
|
|
103
107
|
* island module is matched structurally off an `unknown` import, so nothing implements this type.
|
|
108
|
+
*
|
|
109
|
+
* What `mount` answers, once awaited, is the island's DISPOSER when it is a function — the one
|
|
110
|
+
* way an island hands back what it started, and Solid's `render` already returns exactly that, so
|
|
111
|
+
* `return render(…)` is the whole of an island's side. The fixture calls it on dispose.
|
|
104
112
|
*/
|
|
105
113
|
interface IslandEntry {
|
|
106
114
|
readonly mount: (el: unknown, props: unknown) => unknown;
|
|
107
115
|
}
|
|
108
116
|
|
|
117
|
+
/** The island's disposer, when `mount` answered one — anything else is an island with nothing to stop. */
|
|
118
|
+
const disposerOf = (returned: unknown): (() => void) | undefined =>
|
|
119
|
+
typeof returned === 'function' ? (returned as () => void) : undefined;
|
|
120
|
+
|
|
109
121
|
/** `unknown` + a check, not a cast: a chunk whose `mount` was renamed is a real authoring mistake
|
|
110
122
|
* and `entry.mount is not a function` names neither the file nor the export it wanted. */
|
|
111
123
|
function entryOf(module: unknown, file: string): IslandEntry {
|
|
@@ -198,6 +210,13 @@ function installGlobals(values: Readonly<Record<string, unknown>>): () => void {
|
|
|
198
210
|
* The island fixture. Everything it installs is process-global, so the result is `Disposable` and
|
|
199
211
|
* the idiom is `using mounted = await mountIsland(…)` — a mount left installed hands a fake
|
|
200
212
|
* `document` to every later FILE in the run.
|
|
213
|
+
*
|
|
214
|
+
* Dispose is two things in one order: the island's own disposer first, then the globals back.
|
|
215
|
+
* Restoring the globals was the whole of it until 2026-09-07, so an island whose `mount` started
|
|
216
|
+
* an interval kept ticking after the DOM was gone — measured as `document is not defined` thrown
|
|
217
|
+
* into whichever later test was running, and as one file's fetch stub receiving POSTs from another
|
|
218
|
+
* file's island. The disposer runs while the fake `document` is still installed, because what it
|
|
219
|
+
* clears (a listener on `document`, an interval whose callback reads it) was made against that one.
|
|
201
220
|
*/
|
|
202
221
|
export async function mountIsland(options: MountIslandOptions): Promise<MountedIsland> {
|
|
203
222
|
// `only`, because this fixture has always KNOWN which island it wants and asked for all of them
|
|
@@ -240,7 +259,21 @@ export async function mountIsland(options: MountIslandOptions): Promise<MountedI
|
|
|
240
259
|
// island's `mount` resumed AFTER the `restore()` below had taken the fake `document` back out
|
|
241
260
|
// — so it failed with `document is not defined` inside whichever later test happened to be
|
|
242
261
|
// running, with no thread back here.
|
|
243
|
-
await entry.mount(el, options.props);
|
|
262
|
+
const unmount = disposerOf(await entry.mount(el, options.props));
|
|
263
|
+
let disposed = false;
|
|
264
|
+
const dispose = (): void => {
|
|
265
|
+
// Once: `using` and an `afterAll` that also disposes by hand are both real, and Solid's own
|
|
266
|
+
// disposer is idempotent while an island's `clearInterval` wrapper need not be.
|
|
267
|
+
if (disposed) return;
|
|
268
|
+
disposed = true;
|
|
269
|
+
try {
|
|
270
|
+
unmount?.();
|
|
271
|
+
} finally {
|
|
272
|
+
// Whatever the disposer did — including throw, which is the test's to see — the process
|
|
273
|
+
// gets its globals back, or the fake `document` reaches every later file in the run.
|
|
274
|
+
restore();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
244
277
|
const resolve = (target: string | FakeElement | null | undefined): FakeElement | null =>
|
|
245
278
|
typeof target === 'string' ? el.querySelector(target) : (target ?? null);
|
|
246
279
|
return {
|
|
@@ -271,7 +304,7 @@ export async function mountIsland(options: MountIslandOptions): Promise<MountedI
|
|
|
271
304
|
const node = resolve(target);
|
|
272
305
|
return node !== null && [...resizeObservers].some((each) => each.targets.has(node));
|
|
273
306
|
},
|
|
274
|
-
[Symbol.dispose]:
|
|
307
|
+
[Symbol.dispose]: dispose,
|
|
275
308
|
};
|
|
276
309
|
} catch (error) {
|
|
277
310
|
// A mount that throws restores the process before it rethrows: the alternative leaves every
|
package/src/matchers.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExpectExtendMatchers } from 'bun:test';
|
|
6
6
|
import { expect } from 'bun:test';
|
|
7
|
-
import { describeValue, isUltimateError, stringField } from '@ultimat3/core';
|
|
7
|
+
import { describeValue, isUltimateError, renderCauseValue, stringField } from '@ultimat3/core';
|
|
8
8
|
import { TestJobExpectedError, TestSchemaExpectedError } from './errors';
|
|
9
9
|
import type { MatcherResult } from './matcher-result';
|
|
10
10
|
import type { UltimateMatchers } from './matcher-surface';
|
|
@@ -144,10 +144,15 @@ export async function recordSteps(job: unknown, input: unknown = {}): Promise<re
|
|
|
144
144
|
return names;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
/**
|
|
148
|
+
* A THUNK, never a built string. `expect.extend` reads `message()` only when the verdict is the
|
|
149
|
+
* wrong one, and building the sentence eagerly made three matchers raise from inside the assertion
|
|
150
|
+
* library on values they were answering correctly: `JSON.stringify` refuses a BigInt and throws on
|
|
151
|
+
* anything cyclic, so `expect(schema).toRejectInput({ n: 1n })` — a schema that DID reject — came
|
|
152
|
+
* back as `Matcher \`toRejectInput\` returned a promise that rejected`, with the real answer gone.
|
|
153
|
+
* Deferred, a value that cannot be rendered costs nothing on the path that passes.
|
|
154
|
+
*/
|
|
155
|
+
const result = (pass: boolean, message: () => string): MatcherResult => ({ pass, message });
|
|
151
156
|
|
|
152
157
|
const isOpenApiLike = (value: unknown): value is OpenApiLike =>
|
|
153
158
|
isRecord(value) &&
|
|
@@ -221,11 +226,12 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
|
|
|
221
226
|
if (actual === undefined) {
|
|
222
227
|
return result(
|
|
223
228
|
false,
|
|
224
|
-
|
|
229
|
+
() =>
|
|
230
|
+
`expected an UltimateError (an X_ code with a cause and a fix), received ${describeValue(received)}`,
|
|
225
231
|
);
|
|
226
232
|
}
|
|
227
|
-
if (code === undefined) return result(true,
|
|
228
|
-
return result(actual === code, `expected error code ${code}, received ${actual}`);
|
|
233
|
+
if (code === undefined) return result(true, () => 'expected not to be an UltimateError');
|
|
234
|
+
return result(actual === code, () => `expected error code ${code}, received ${actual}`);
|
|
229
235
|
},
|
|
230
236
|
|
|
231
237
|
async toDenyPolicy(received: unknown, context: Readonly<Record<string, unknown>>) {
|
|
@@ -233,10 +239,13 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
|
|
|
233
239
|
if (allowed === undefined) {
|
|
234
240
|
return result(
|
|
235
241
|
false,
|
|
236
|
-
'expected a policy — an object with run() (@ultimat3/policy) or evaluate()',
|
|
242
|
+
() => 'expected a policy — an object with run() (@ultimat3/policy) or evaluate()',
|
|
237
243
|
);
|
|
238
244
|
}
|
|
239
|
-
|
|
245
|
+
// `renderCauseValue`, never `JSON.stringify`: the context is a value a test authored, and this
|
|
246
|
+
// matcher is asked about policies whose input holds a BigInt id or a back-reference. It quotes
|
|
247
|
+
// what JSON can quote and names the shape of what it cannot, instead of throwing over either.
|
|
248
|
+
return result(!allowed, () => `expected the policy to deny ${renderCauseValue(context)}`);
|
|
240
249
|
},
|
|
241
250
|
|
|
242
251
|
// Not `async` — see `assertStandardSchema`. The guard is the synchronous prologue; the wait is
|
|
@@ -246,7 +255,7 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
|
|
|
246
255
|
return recordSteps(job).then((names) =>
|
|
247
256
|
result(
|
|
248
257
|
JSON.stringify(names) === JSON.stringify(expected),
|
|
249
|
-
`expected steps ${expected.join(' -> ')}, ran ${names.join(' -> ')}`,
|
|
258
|
+
() => `expected steps ${expected.join(' -> ')}, ran ${names.join(' -> ')}`,
|
|
250
259
|
),
|
|
251
260
|
);
|
|
252
261
|
},
|
|
@@ -255,13 +264,15 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
|
|
|
255
264
|
if (!isOpenApiLike(received)) {
|
|
256
265
|
return result(
|
|
257
266
|
false,
|
|
258
|
-
|
|
267
|
+
() =>
|
|
268
|
+
'expected an OpenAPI document — an object with operations: [{ operationId, required? }]',
|
|
259
269
|
);
|
|
260
270
|
}
|
|
261
271
|
const broke = breakingChanges(committed, received);
|
|
262
272
|
return result(
|
|
263
273
|
broke.length === 0,
|
|
264
|
-
|
|
274
|
+
() =>
|
|
275
|
+
`contract broke: ${broke.join('; ')} — bump the package version or restore the old shape`,
|
|
265
276
|
);
|
|
266
277
|
},
|
|
267
278
|
|
|
@@ -269,23 +280,26 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
|
|
|
269
280
|
if (typeof received !== 'number') {
|
|
270
281
|
return result(
|
|
271
282
|
false,
|
|
272
|
-
`expected a number to compare against the budget, got ${typeof received}`,
|
|
283
|
+
() => `expected a number to compare against the budget, got ${typeof received}`,
|
|
273
284
|
);
|
|
274
285
|
}
|
|
275
|
-
return result(
|
|
286
|
+
return result(
|
|
287
|
+
received <= limit,
|
|
288
|
+
() => `expected ${received} to be within the budget of ${limit}`,
|
|
289
|
+
);
|
|
276
290
|
},
|
|
277
291
|
|
|
278
292
|
toRejectInput(received: unknown, input: unknown) {
|
|
279
293
|
const schema = assertStandardSchema(received);
|
|
280
294
|
return hasIssues(schema, input).then((rejected) =>
|
|
281
|
-
result(rejected, `expected the schema to reject ${
|
|
295
|
+
result(rejected, () => `expected the schema to reject ${renderCauseValue(input)}`),
|
|
282
296
|
);
|
|
283
297
|
},
|
|
284
298
|
|
|
285
299
|
toAcceptInput(received: unknown, input: unknown) {
|
|
286
300
|
const schema = assertStandardSchema(received);
|
|
287
301
|
return hasIssues(schema, input).then((rejected) =>
|
|
288
|
-
result(!rejected, `expected the schema to accept ${
|
|
302
|
+
result(!rejected, () => `expected the schema to accept ${renderCauseValue(input)}`),
|
|
289
303
|
);
|
|
290
304
|
},
|
|
291
305
|
};
|