tape-six 1.7.13 → 1.8.0
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/README.md +3 -1
- package/TESTING.md +246 -47
- package/bin/tape6-server.js +19 -19
- package/index.d.ts +67 -0
- package/llms-full.txt +102 -0
- package/llms.txt +66 -0
- package/package.json +13 -4
- package/skills/write-tests/SKILL.md +63 -16
- package/src/State.js +26 -21
- package/src/Tester.js +59 -27
- package/src/deep6/env.d.ts +174 -0
- package/src/deep6/env.js +4 -4
- package/src/deep6/index.d.ts +86 -0
- package/src/deep6/index.js +10 -7
- package/src/deep6/traverse/assemble.d.ts +59 -0
- package/src/deep6/traverse/assemble.js +4 -3
- package/src/deep6/traverse/clone.d.ts +57 -0
- package/src/deep6/traverse/clone.js +4 -2
- package/src/deep6/traverse/deref.d.ts +59 -0
- package/src/deep6/traverse/deref.js +3 -2
- package/src/deep6/traverse/preprocess.d.ts +65 -0
- package/src/deep6/traverse/preprocess.js +2 -1
- package/src/deep6/traverse/walk.d.ts +219 -0
- package/src/deep6/traverse/walk.js +9 -4
- package/src/deep6/unifiers/matchCondition.d.ts +45 -0
- package/src/deep6/unifiers/matchCondition.js +1 -0
- package/src/deep6/unifiers/matchInstanceOf.d.ts +37 -0
- package/src/deep6/unifiers/matchInstanceOf.js +1 -0
- package/src/deep6/unifiers/matchString.d.ts +56 -0
- package/src/deep6/unifiers/matchString.js +1 -0
- package/src/deep6/unifiers/matchTypeOf.d.ts +37 -0
- package/src/deep6/unifiers/matchTypeOf.js +1 -0
- package/src/deep6/unifiers/ref.d.ts +52 -0
- package/src/deep6/unifiers/ref.js +1 -0
- package/src/deep6/unify.d.ts +95 -0
- package/src/deep6/unify.js +130 -66
- package/src/deep6/utils/replaceVars.d.ts +25 -0
- package/src/deep6/utils/replaceVars.js +23 -19
- package/src/response.d.ts +43 -0
- package/src/response.js +57 -0
- package/src/server.d.ts +81 -0
- package/src/server.js +69 -0
- package/src/test.js +26 -53
package/llms.txt
CHANGED
|
@@ -139,6 +139,37 @@ test('suite', async t => {
|
|
|
139
139
|
test.beforeAll(() => { /* ... */ });
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
## Subpath modules
|
|
143
|
+
|
|
144
|
+
### tape-six/server — HTTP server harness
|
|
145
|
+
|
|
146
|
+
Helpers for tests that need an ephemeral `node:http` server. Cross-runtime (Node, Bun, Deno; not for browser-side tests).
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import {withServer, startServer, setupServer} from 'tape-six/server';
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `withServer(serverHandler, clientHandler, opts?)` — scoped resource: spin up a server, run the test body with the base URL, tear down in `finally`. The 95% case.
|
|
153
|
+
- `startServer(server, opts?)` — procedural primitive: returns `{server, base, port, host, close}`. For multi-phase tests or non-test code (e.g., `bin/tape6-server`) that wants long-term control. Races `'listening'` against `'error'` so port-busy rejects instead of hanging.
|
|
154
|
+
- `setupServer(serverHandler, opts?)` — registers `beforeAll`/`afterAll` and returns a live-getter context for suite-shared servers. Don't destructure the returned object at module load — properties read live state on each access.
|
|
155
|
+
- Options: `{host = '127.0.0.1', port = 0}`. Default host is explicit IPv4.
|
|
156
|
+
|
|
157
|
+
`serverHandler` is the per-request callback (Node calls it once per incoming request). `clientHandler` is the per-scope test body (called once with the base URL). Either side may be the SUT.
|
|
158
|
+
|
|
159
|
+
### tape-six/response — HTTP response helpers
|
|
160
|
+
|
|
161
|
+
Reading helpers that work uniformly with both `Response` (fetch results) and `http.IncomingMessage` (Node low-level requests).
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
import {asText, asJson, asBytes, header, headers} from 'tape-six/response';
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- `asText(res)` — body as UTF-8 string.
|
|
168
|
+
- `asJson(res)` — body parsed as JSON.
|
|
169
|
+
- `asBytes(res)` — body as `Uint8Array`.
|
|
170
|
+
- `header(res, name)` — single header value, case-insensitive. Returns `null` if absent.
|
|
171
|
+
- `headers(res)` — all headers as a plain object with lowercase keys.
|
|
172
|
+
|
|
142
173
|
## Common patterns
|
|
143
174
|
|
|
144
175
|
### Basic test file
|
|
@@ -213,6 +244,41 @@ describe('my module', () => {
|
|
|
213
244
|
});
|
|
214
245
|
```
|
|
215
246
|
|
|
247
|
+
### Testing HTTP code with `withServer`
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
import test from 'tape-six';
|
|
251
|
+
import {withServer} from 'tape-six/server';
|
|
252
|
+
import {asJson} from 'tape-six/response';
|
|
253
|
+
|
|
254
|
+
test('GET /users returns the list', t =>
|
|
255
|
+
withServer(myHandler, async base => {
|
|
256
|
+
const res = await fetch(`${base}/users`);
|
|
257
|
+
t.equal(res.status, 200);
|
|
258
|
+
const body = await asJson(res);
|
|
259
|
+
t.equal(body.length, 3);
|
|
260
|
+
}));
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Suite-shared server (multiple tests against one instance):
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
import test, {beforeEach} from 'tape-six';
|
|
267
|
+
import {setupServer} from 'tape-six/server';
|
|
268
|
+
|
|
269
|
+
const server = setupServer((req, res) => {
|
|
270
|
+
recorded.push({method: req.method, url: req.url});
|
|
271
|
+
res.writeHead(204).end();
|
|
272
|
+
});
|
|
273
|
+
let recorded;
|
|
274
|
+
beforeEach(() => { recorded = []; });
|
|
275
|
+
|
|
276
|
+
test('records one request', async t => {
|
|
277
|
+
await fetch(`${server.base}/foo`);
|
|
278
|
+
t.equal(recorded.length, 1);
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
216
282
|
### Skip and TODO
|
|
217
283
|
|
|
218
284
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tape-six",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "TAP-inspired unit test library for Node, Deno, Bun, and browsers. ES modules, TypeScript, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
"types": "index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|
|
11
|
+
"./server": {
|
|
12
|
+
"types": "./src/server.d.ts",
|
|
13
|
+
"default": "./src/server.js"
|
|
14
|
+
},
|
|
15
|
+
"./response": {
|
|
16
|
+
"types": "./src/response.d.ts",
|
|
17
|
+
"default": "./src/response.js"
|
|
18
|
+
},
|
|
11
19
|
"./bin/*": "./bin/*",
|
|
12
20
|
"./*": "./src/*"
|
|
13
21
|
},
|
|
@@ -87,7 +95,8 @@
|
|
|
87
95
|
"/tests/test-*.mjs"
|
|
88
96
|
],
|
|
89
97
|
"cli": [
|
|
90
|
-
"/tests/test-*.cjs"
|
|
98
|
+
"/tests/test-*.cjs",
|
|
99
|
+
"/tests/cli/test-*.js"
|
|
91
100
|
],
|
|
92
101
|
"browser": [
|
|
93
102
|
"/tests/browser/test-*.html"
|
|
@@ -102,8 +111,8 @@
|
|
|
102
111
|
},
|
|
103
112
|
"devDependencies": {
|
|
104
113
|
"@types/chai": "^5.2.3",
|
|
105
|
-
"@types/node": "^25.
|
|
114
|
+
"@types/node": "^25.6.0",
|
|
106
115
|
"chai": "^6.2.2",
|
|
107
|
-
"typescript": "^6.0.
|
|
116
|
+
"typescript": "^6.0.3"
|
|
108
117
|
}
|
|
109
118
|
}
|
|
@@ -12,29 +12,76 @@ Write or update tests using the tape-six testing library.
|
|
|
12
12
|
- `tape-six` supports ES modules (`.js`, `.mjs`, `.ts`, `.mts`) and CommonJS (`.cjs`, `.cts`).
|
|
13
13
|
- TypeScript is supported natively — no transpilation needed (Node 22+, Deno, Bun run `.ts` files directly).
|
|
14
14
|
- The default `tape6` runner uses worker threads for parallel execution. `tape6-seq` runs sequentially in-process — useful for debugging or when tests share state.
|
|
15
|
+
- `tape-six` catches `AssertionError` automatically, so you can use Chai or `node:assert` inside tape-six tests if a project already uses them.
|
|
15
16
|
|
|
16
17
|
## Steps
|
|
17
18
|
|
|
18
|
-
1. Read the testing guide at `node_modules/tape-six/TESTING.md` for API reference and patterns.
|
|
19
|
+
1. Read the testing guide at `node_modules/tape-six/TESTING.md` for the full API reference and patterns.
|
|
19
20
|
2. Identify the module or feature to test. Read its source code to understand the public API.
|
|
20
21
|
3. Create or update the test file in `tests/test-<name>.js` (or `.ts` for TypeScript, `.cjs` for CommonJS):
|
|
21
22
|
- **ESM** (`.js`): `import test from 'tape-six'` and import the module under test using the project's package name.
|
|
22
23
|
- **CJS** (`.cjs`): `const {test} = require('tape-six')` and `const {...} = require('my-package')`. Always use `require()` — it is the correct CJS pattern. **Do NOT use `await import()` unless you have confirmed** (e.g., grep for `^await` at the top level) that the module under test uses top-level `await`, which is rare.
|
|
23
24
|
- Write one top-level `test()` per logical group.
|
|
24
|
-
- Use embedded `await t.test()` for sub-cases.
|
|
25
|
-
- Use `t.beforeEach`/`t.afterEach` for shared setup/teardown.
|
|
25
|
+
- Use embedded `await t.test()` for sub-cases. **Always `await` embedded tests** to preserve execution order.
|
|
26
|
+
- Use `t.beforeEach`/`t.afterEach` for shared setup/teardown; `t.beforeAll`/`t.afterAll` (aliases `t.before`/`t.after`) for one-shot fixtures.
|
|
26
27
|
- Cover: normal operation, edge cases, error conditions.
|
|
27
|
-
- Use `t.equal` for primitives, `t.deepEqual` for objects/arrays, `t.throws` for errors, `await t.rejects` for async errors.
|
|
28
28
|
- All `msg` arguments are optional but recommended for clarity.
|
|
29
|
-
4. **
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
4. **Pick the right assertion:**
|
|
30
|
+
- Primitives: `t.equal(a, b)` (strict). Use `t.deepEqual(a, b)` for objects/arrays.
|
|
31
|
+
- Truthiness: `t.ok(value)`, `t.notOk(value)`.
|
|
32
|
+
- Errors: `t.error(err)` asserts `err` is falsy (callback-style "no error").
|
|
33
|
+
- Sync throws: `t.throws(fn)`, `t.doesNotThrow(fn)`.
|
|
34
|
+
- Async: `await t.rejects(promise)`, `await t.resolves(promise)`.
|
|
35
|
+
- Strings: `t.matchString(str, /re/)`, `t.doesNotMatchString(str, /re/)`.
|
|
36
|
+
- Structural: `t.match(actual, pattern)` / `t.doesNotMatch(actual, pattern)` for partial object matching (uses deep6 `match()`).
|
|
37
|
+
5. **Match error/value shape** (`throws` / `rejects` / `resolves` accept an optional matcher as the second arg):
|
|
38
|
+
```js
|
|
39
|
+
t.throws(() => parse(''), TypeError); // Error subclass
|
|
40
|
+
t.throws(() => parse(''), /unexpected end/); // RegExp on error.message
|
|
41
|
+
t.throws(
|
|
42
|
+
() => parse(''),
|
|
43
|
+
e => e.code === 'EPARSE'
|
|
44
|
+
); // predicate
|
|
45
|
+
t.throws(() => parse(''), {code: 'EPARSE'}); // deep6 object pattern
|
|
46
|
+
await t.rejects(fetchData(), /404/);
|
|
47
|
+
await t.resolves(fetchData(), {status: 200}); // matches resolved value
|
|
48
|
+
```
|
|
49
|
+
A string second arg is still treated as the message for backward compatibility.
|
|
50
|
+
6. **Wildcards in deep equality** — use `t.any` (alias `t._`) inside expected values to skip non-deterministic fields:
|
|
51
|
+
```js
|
|
52
|
+
t.deepEqual(result, {id: t.any, name: 'Alice', createdAt: t.any});
|
|
53
|
+
```
|
|
54
|
+
7. **Async cancellation** — `t.signal` is an `AbortSignal` that fires when the test ends, times out, or is stopped. Pass it to long-running async work so it cancels cleanly:
|
|
55
|
+
```js
|
|
56
|
+
test('aborts on test end', async t => {
|
|
57
|
+
const res = await fetch(url, {signal: t.signal});
|
|
58
|
+
t.equal(res.status, 200);
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
8. **Test options** — `test(name, options, fn)` accepts `{timeout, skip, todo, before, after, beforeAll, afterAll, beforeEach, afterEach}`. Use `timeout` (ms) for tests with bounded async work; the test fails and `t.signal` fires when exceeded.
|
|
62
|
+
9. **Misc:**
|
|
63
|
+
- `test.skip(name, fn)` / `test.todo(name, fn)` — `skip` doesn't run; `todo` runs but failures don't fail the suite.
|
|
64
|
+
- `t.comment(msg)` — emit a TAP comment line.
|
|
65
|
+
- `t.skipTest(msg)` — skip the _current_ test from inside it.
|
|
66
|
+
- `t.bailOut(msg)` — stop the entire run (catastrophic).
|
|
67
|
+
- `t.OK(expr, msg)` (aliases `t.TRUE`, `t.ASSERT`) — returns a code string for `eval()` that asserts an expression and dumps top-level variables on failure. Useful for compact arithmetic/state checks: `eval(t.OK('a + b === 3'))`. Do not use in CSP-restricted contexts.
|
|
68
|
+
10. **Testing HTTP code** — for tests that need an ephemeral HTTP server or want to read responses uniformly:
|
|
69
|
+
- **`tape-six/server`** (`withServer` / `startServer` / `setupServer`) — wraps the `node:http` lifecycle. `withServer(serverHandler, clientHandler, opts?)` is the per-test scoped resource: starts a server, runs the test body with the bound base URL, tears down in `finally`. `setupServer(serverHandler, opts?)` registers `beforeAll`/`afterAll` and returns a live-getter handle for suite-shared servers (don't destructure at module load — properties read live state). `startServer(server, opts?)` is the procedural primitive for multi-phase tests. Default host is `'127.0.0.1'`. Cross-runtime (Node, Bun, Deno).
|
|
70
|
+
- **`tape-six/response`** (`asText` / `asJson` / `asBytes` / `header` / `headers`) — reading helpers that work uniformly on both W3C `Response` (fetch results) and Node `http.IncomingMessage`.
|
|
71
|
+
- Per-test mock-server state reset (e.g., clearing a `recorded[]` array) stays user-side: compose your own `beforeEach`. `setupServer` owns the server lifecycle; you own state.
|
|
72
|
+
- **Placement:** these tests can't run in the browser worker — `node:http` doesn't exist there. Don't drop them into a folder matched by the universal `tape6.tests` glob; put them in a CLI-only path (e.g. `tests/cli/test-*.js`) and ensure the matching glob is in `tape6.cli` instead.
|
|
73
|
+
- See "Testing HTTP code" in `TESTING.md` for examples.
|
|
74
|
+
11. **Browser-specific tests** — if the project uses browser testing with `tape6-server`. See "Browser testing" in `TESTING.md`.
|
|
75
|
+
- Browsers run `.js` and `.mjs` only — no TypeScript, no CommonJS.
|
|
76
|
+
- Browsers can also run `.html` shim files (with inline importmap and `<script type="module">`).
|
|
77
|
+
- Place browser-only files in `tests/browser/` and add patterns to `"browser"` in the `tape6` config.
|
|
78
|
+
- Run: `npx tape6-server --trace`, then open `http://localhost:3000`. Use `?q=<glob>` to filter, `?flags=FO` and `?par=N` to control output and parallelism.
|
|
79
|
+
12. **Environment-specific tests** — the `tape6` config in `package.json` supports per-env patterns (`tests`, `cli`, `node`, `bun`, `deno`, `browser`). All are additive. See "Configuring test discovery" in `TESTING.md`.
|
|
80
|
+
13. **Verify (CLI):** run the new test file directly: `node tests/test-<name>.js`
|
|
81
|
+
- Run the full suite to check for regressions: `npm test`
|
|
82
|
+
- For debugging, use `npm run test:seq` (sequential, in-process).
|
|
83
|
+
- To see which files are being run, add `--flags fo` (overrides the default `--flags FO`).
|
|
84
|
+
- To inspect the resolved config without running, use `npx tape6 --info`.
|
|
85
|
+
- For tests with heavy `beforeAll` (Docker spawn, big fixture loads), bump the worker startup timer with `TAPE6_WORKER_START_TIMEOUT=60000`. The default is 5 s.
|
|
86
|
+
14. **Verify (browser):** start `npx tape6-server --trace`, then open `http://localhost:3000/?q=/tests/test-<name>.js` to run a specific file. Use multiple `?q=` parameters to run several files. Open `http://localhost:3000/` to run all configured tests.
|
|
87
|
+
15. Report results and any failures.
|
package/src/State.js
CHANGED
|
@@ -186,29 +186,34 @@ export class State {
|
|
|
186
186
|
typeof event.diffTime !== 'number' && (event.diffTime = event.time - this.startTime);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
189
|
+
// Stack walking is only needed when the event will be reported as a failure.
|
|
190
|
+
// Reporters (TapReporter, TTYReporter) only read event.at / event.stackList
|
|
191
|
+
// inside `if (event.fail)` blocks, so for passing assertions this is wasted work.
|
|
192
|
+
if (isFailed) {
|
|
193
|
+
if (
|
|
194
|
+
event.type === 'assert' &&
|
|
195
|
+
(event.operator === 'error' || event.operator === 'exception') &&
|
|
196
|
+
typeof event.data?.actual?.stack == 'string'
|
|
197
|
+
) {
|
|
198
|
+
event.stackList = getStackList(event.data.actual);
|
|
199
|
+
event.at = event.stackList[0];
|
|
200
|
+
}
|
|
197
201
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
if (event.type === 'assertion-error' && typeof event.data?.error?.stack == 'string') {
|
|
203
|
+
event.stackList = getStackList(event.data.error);
|
|
204
|
+
event.at = event.stackList[0];
|
|
205
|
+
}
|
|
202
206
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
if (!event.at && typeof event.marker?.stack == 'string') {
|
|
208
|
+
event.stackList = getStackList(event.marker);
|
|
209
|
+
event.at =
|
|
210
|
+
event.stackList[
|
|
211
|
+
Math.min(
|
|
212
|
+
typeof event.markerIndex == 'number' ? event.markerIndex : 1,
|
|
213
|
+
event.stackList.length
|
|
214
|
+
)
|
|
215
|
+
];
|
|
216
|
+
}
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
if ((event.type === 'assert' || event.type === 'assertion-error') && event.data) {
|
package/src/Tester.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import {equal, match, any} from './deep6/index.js';
|
|
2
2
|
import {getTimer} from './utils/timer.js';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const tryFn = fn => {
|
|
5
5
|
try {
|
|
6
6
|
fn();
|
|
7
|
+
return {threw: false};
|
|
7
8
|
} catch (error) {
|
|
8
|
-
return error;
|
|
9
|
+
return {threw: true, error};
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const isErrorClass = fn =>
|
|
14
|
+
fn === Error || (typeof fn === 'function' && fn.prototype instanceof Error);
|
|
15
|
+
|
|
16
|
+
const applyMatcher = (actual, matcher) => {
|
|
17
|
+
if (matcher === undefined) return true;
|
|
18
|
+
if (typeof matcher === 'function') {
|
|
19
|
+
if (isErrorClass(matcher)) return actual instanceof matcher;
|
|
20
|
+
return !!matcher(actual);
|
|
21
|
+
}
|
|
22
|
+
if (matcher instanceof RegExp) {
|
|
23
|
+
const str = actual instanceof Error ? actual.message : String(actual);
|
|
24
|
+
return matcher.test(str);
|
|
25
|
+
}
|
|
26
|
+
if (matcher !== null && typeof matcher === 'object') return match(actual, matcher);
|
|
27
|
+
return actual === matcher;
|
|
11
28
|
};
|
|
12
29
|
|
|
13
30
|
export class Tester {
|
|
@@ -264,36 +281,41 @@ export class Tester {
|
|
|
264
281
|
});
|
|
265
282
|
}
|
|
266
283
|
|
|
267
|
-
throws(fn, msg) {
|
|
284
|
+
throws(fn, matcher, msg) {
|
|
268
285
|
if (typeof fn != 'function') throw new TypeError('the first argument should be a function');
|
|
269
|
-
|
|
286
|
+
if (typeof matcher === 'string' && msg === undefined) {
|
|
287
|
+
msg = matcher;
|
|
288
|
+
matcher = undefined;
|
|
289
|
+
}
|
|
290
|
+
const {threw, error} = tryFn(fn);
|
|
291
|
+
const matched = threw && applyMatcher(error, matcher);
|
|
270
292
|
this.reporter.report({
|
|
271
293
|
name: msg || 'should throw',
|
|
272
294
|
test: this.testNumber,
|
|
273
295
|
marker: new Error(),
|
|
274
296
|
time: this.timer.now(),
|
|
275
297
|
operator: 'throws',
|
|
276
|
-
fail: !
|
|
298
|
+
fail: !matched,
|
|
277
299
|
data: {
|
|
278
|
-
expected: null,
|
|
279
|
-
actual:
|
|
300
|
+
expected: matcher === undefined ? null : matcher,
|
|
301
|
+
actual: threw ? error : null
|
|
280
302
|
}
|
|
281
303
|
});
|
|
282
304
|
}
|
|
283
305
|
|
|
284
306
|
doesNotThrow(fn, msg) {
|
|
285
307
|
if (typeof fn != 'function') throw new TypeError('the first argument should be a function');
|
|
286
|
-
const
|
|
308
|
+
const {threw, error} = tryFn(fn);
|
|
287
309
|
this.reporter.report({
|
|
288
310
|
name: msg || 'should not throw',
|
|
289
311
|
test: this.testNumber,
|
|
290
312
|
marker: new Error(),
|
|
291
313
|
time: this.timer.now(),
|
|
292
314
|
operator: 'doesNotThrow',
|
|
293
|
-
fail:
|
|
315
|
+
fail: threw,
|
|
294
316
|
data: {
|
|
295
317
|
expected: null,
|
|
296
|
-
actual:
|
|
318
|
+
actual: threw ? error : null
|
|
297
319
|
}
|
|
298
320
|
});
|
|
299
321
|
}
|
|
@@ -325,7 +347,7 @@ export class Tester {
|
|
|
325
347
|
test: this.testNumber,
|
|
326
348
|
marker: new Error(),
|
|
327
349
|
time: this.timer.now(),
|
|
328
|
-
operator: '
|
|
350
|
+
operator: 'doesNotMatchString',
|
|
329
351
|
fail: regexp.test(string),
|
|
330
352
|
data: {
|
|
331
353
|
expected: regexp,
|
|
@@ -355,7 +377,7 @@ export class Tester {
|
|
|
355
377
|
test: this.testNumber,
|
|
356
378
|
marker: new Error(),
|
|
357
379
|
time: this.timer.now(),
|
|
358
|
-
operator: '
|
|
380
|
+
operator: 'doesNotMatch',
|
|
359
381
|
fail: match(a, b),
|
|
360
382
|
data: {
|
|
361
383
|
expected: b,
|
|
@@ -364,49 +386,59 @@ export class Tester {
|
|
|
364
386
|
});
|
|
365
387
|
}
|
|
366
388
|
|
|
367
|
-
rejects(promise, msg) {
|
|
389
|
+
rejects(promise, matcher, msg) {
|
|
368
390
|
if (!promise || typeof promise.then != 'function')
|
|
369
391
|
throw new TypeError('the first argument should be a promise');
|
|
392
|
+
if (typeof matcher === 'string' && msg === undefined) {
|
|
393
|
+
msg = matcher;
|
|
394
|
+
matcher = undefined;
|
|
395
|
+
}
|
|
370
396
|
return promise
|
|
371
397
|
.then(
|
|
372
|
-
() =>
|
|
373
|
-
error => error
|
|
398
|
+
() => ({resolved: true}),
|
|
399
|
+
error => ({resolved: false, error})
|
|
374
400
|
)
|
|
375
|
-
.then(
|
|
401
|
+
.then(({resolved, error}) => {
|
|
402
|
+
const matched = !resolved && applyMatcher(error, matcher);
|
|
376
403
|
this.reporter.report({
|
|
377
404
|
name: msg || 'should be rejected',
|
|
378
405
|
test: this.testNumber,
|
|
379
406
|
marker: new Error(),
|
|
380
407
|
time: this.timer.now(),
|
|
381
408
|
operator: 'rejects',
|
|
382
|
-
fail: !
|
|
409
|
+
fail: !matched,
|
|
383
410
|
data: {
|
|
384
|
-
expected: null,
|
|
385
|
-
actual:
|
|
411
|
+
expected: matcher === undefined ? null : matcher,
|
|
412
|
+
actual: resolved ? null : error
|
|
386
413
|
}
|
|
387
414
|
});
|
|
388
415
|
});
|
|
389
416
|
}
|
|
390
417
|
|
|
391
|
-
resolves(promise, msg) {
|
|
418
|
+
resolves(promise, matcher, msg) {
|
|
392
419
|
if (!promise || typeof promise.then != 'function')
|
|
393
420
|
throw new TypeError('the first argument should be a promise');
|
|
421
|
+
if (typeof matcher === 'string' && msg === undefined) {
|
|
422
|
+
msg = matcher;
|
|
423
|
+
matcher = undefined;
|
|
424
|
+
}
|
|
394
425
|
return promise
|
|
395
426
|
.then(
|
|
396
|
-
|
|
397
|
-
error => error
|
|
427
|
+
value => ({resolved: true, value}),
|
|
428
|
+
error => ({resolved: false, error})
|
|
398
429
|
)
|
|
399
|
-
.then(
|
|
430
|
+
.then(({resolved, value, error}) => {
|
|
431
|
+
const matched = resolved && applyMatcher(value, matcher);
|
|
400
432
|
this.reporter.report({
|
|
401
433
|
name: msg || 'should not be rejected',
|
|
402
434
|
test: this.testNumber,
|
|
403
435
|
marker: new Error(),
|
|
404
436
|
time: this.timer.now(),
|
|
405
437
|
operator: 'resolves',
|
|
406
|
-
fail:
|
|
438
|
+
fail: !matched,
|
|
407
439
|
data: {
|
|
408
|
-
expected: null,
|
|
409
|
-
actual:
|
|
440
|
+
expected: matcher === undefined ? null : matcher,
|
|
441
|
+
actual: resolved ? (matcher === undefined ? null : value) : error
|
|
410
442
|
}
|
|
411
443
|
});
|
|
412
444
|
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Type definitions for deep6 environment
|
|
2
|
+
// Generated from src/env.js
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Unification environment managing variable bindings and stack frames
|
|
6
|
+
*
|
|
7
|
+
* Maintains two parallel prototype-chain structures:
|
|
8
|
+
* - `variables`: maps variable names to their alias groups
|
|
9
|
+
* - `values`: maps variable names/aliases to their bound values
|
|
10
|
+
*
|
|
11
|
+
* Stack frames enable scoped bindings that can be reverted.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Env {
|
|
14
|
+
/** Map of variable names to their alias groups */
|
|
15
|
+
variables: Record<string | symbol, unknown>;
|
|
16
|
+
/** Map of variable names/aliases to their bound values */
|
|
17
|
+
values: Record<string | symbol, unknown>;
|
|
18
|
+
/** Current stack frame depth */
|
|
19
|
+
depth: number;
|
|
20
|
+
|
|
21
|
+
constructor();
|
|
22
|
+
|
|
23
|
+
/** Pushes a new stack frame for nested scoping */
|
|
24
|
+
push(): void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pops the current stack frame, reverting bindings
|
|
28
|
+
* @throws {Error} If stack is empty
|
|
29
|
+
*/
|
|
30
|
+
pop(): void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reverts to a specific stack frame depth
|
|
34
|
+
* @param depth - Target depth to revert to
|
|
35
|
+
* @throws {Error} If depth is higher than current depth
|
|
36
|
+
*/
|
|
37
|
+
revert(depth: number): void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates an alias between two variable names
|
|
41
|
+
* @param name1 - First variable name
|
|
42
|
+
* @param name2 - Second variable name
|
|
43
|
+
*/
|
|
44
|
+
bindVar(name1: string | symbol, name2: string | symbol): void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Binds a variable name (and its aliases) to a value
|
|
48
|
+
* @param name - Variable name to bind
|
|
49
|
+
* @param val - Value to bind
|
|
50
|
+
*/
|
|
51
|
+
bindVal(name: string | symbol, val: unknown): void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Checks if a variable is bound to a value
|
|
55
|
+
* @param name - Variable name to check
|
|
56
|
+
* @returns True if variable has a bound value
|
|
57
|
+
*/
|
|
58
|
+
isBound(name: string | symbol): boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Checks if two variable names are aliases
|
|
62
|
+
* @param name1 - First variable name
|
|
63
|
+
* @param name2 - Second variable name
|
|
64
|
+
* @returns True if variables are aliases
|
|
65
|
+
*/
|
|
66
|
+
isAlias(name1: string | symbol, name2: string | symbol): boolean;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Gets the value bound to a variable
|
|
70
|
+
* @param name - Variable name
|
|
71
|
+
* @returns The bound value, or undefined if unbound
|
|
72
|
+
*/
|
|
73
|
+
get(name: string | symbol): unknown;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns all variable bindings (for debugging)
|
|
77
|
+
* @returns Array of name/value pairs
|
|
78
|
+
*/
|
|
79
|
+
getAllValues(): Array<{name: string | symbol; value: unknown}>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Base class for custom unification behavior
|
|
84
|
+
*
|
|
85
|
+
* Subclasses must implement the `unify` method.
|
|
86
|
+
*/
|
|
87
|
+
export declare class Unifier {
|
|
88
|
+
/**
|
|
89
|
+
* Unifies this unifier with a value
|
|
90
|
+
* @param val - Value to unify with
|
|
91
|
+
* @param ls - Left argument stack
|
|
92
|
+
* @param rs - Right argument stack
|
|
93
|
+
* @param env - Unification environment
|
|
94
|
+
* @returns True if unification succeeds
|
|
95
|
+
*/
|
|
96
|
+
unify(val: unknown, ls: unknown[], rs: unknown[], env: Env): boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Type guard for Unifier instances
|
|
101
|
+
* @param x - Value to check
|
|
102
|
+
* @returns True if x is a Unifier
|
|
103
|
+
*/
|
|
104
|
+
export declare const isUnifier: (x: unknown) => x is Unifier;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wildcard symbol that matches any value during unification
|
|
108
|
+
*/
|
|
109
|
+
export declare const any: unique symbol;
|
|
110
|
+
|
|
111
|
+
/** Alias for `any` */
|
|
112
|
+
export declare const _: typeof any;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Logical variable for capturing values during unification
|
|
116
|
+
*
|
|
117
|
+
* Variables can be bound to values, aliased to other variables,
|
|
118
|
+
* and dereferenced through an Env.
|
|
119
|
+
*/
|
|
120
|
+
export declare class Variable extends Unifier {
|
|
121
|
+
/** Variable identifier */
|
|
122
|
+
name: string | symbol;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param name - Optional identifier (defaults to a unique Symbol)
|
|
126
|
+
*/
|
|
127
|
+
constructor(name?: string | symbol);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Checks if this variable is bound in the given environment
|
|
131
|
+
* @param env - Unification environment
|
|
132
|
+
* @returns True if bound
|
|
133
|
+
*/
|
|
134
|
+
isBound(env: Env): boolean;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Checks if this variable is aliased to another
|
|
138
|
+
* @param name - Variable name, symbol, or Variable instance
|
|
139
|
+
* @param env - Unification environment
|
|
140
|
+
* @returns True if aliased
|
|
141
|
+
*/
|
|
142
|
+
isAlias(name: Variable | string | symbol, env: Env): boolean;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Gets the bound value of this variable
|
|
146
|
+
* @param env - Unification environment
|
|
147
|
+
* @returns The bound value, or undefined if unbound
|
|
148
|
+
*/
|
|
149
|
+
get(env: Env): unknown;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Unifies this variable with a value
|
|
153
|
+
* @param val - Value to unify with
|
|
154
|
+
* @param ls - Left argument stack
|
|
155
|
+
* @param rs - Right argument stack
|
|
156
|
+
* @param env - Unification environment
|
|
157
|
+
* @returns True if unification succeeds
|
|
158
|
+
*/
|
|
159
|
+
unify(val: unknown, ls: unknown[], rs: unknown[], env: Env): boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Type guard for Variable instances
|
|
164
|
+
* @param x - Value to check
|
|
165
|
+
* @returns True if x is a Variable
|
|
166
|
+
*/
|
|
167
|
+
export declare const isVariable: (x: unknown) => x is Variable;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Creates a new Variable
|
|
171
|
+
* @param name - Optional identifier (defaults to a unique Symbol)
|
|
172
|
+
* @returns A new Variable instance
|
|
173
|
+
*/
|
|
174
|
+
export declare const variable: (name?: string | symbol) => Variable;
|
package/src/deep6/env.js
CHANGED
|
@@ -13,7 +13,7 @@ const collectSymbols = object => {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const ensure = (object, depth) => {
|
|
16
|
-
while (object[keyDepth] > depth) object =
|
|
16
|
+
while (object[keyDepth] > depth) object = Object.getPrototypeOf(object);
|
|
17
17
|
if (object[keyDepth] < depth) {
|
|
18
18
|
object = Object.create(object);
|
|
19
19
|
object[keyDepth] = depth;
|
|
@@ -90,14 +90,14 @@ class Env {
|
|
|
90
90
|
}
|
|
91
91
|
// helpers
|
|
92
92
|
isBound(name) {
|
|
93
|
-
return name in
|
|
93
|
+
return name in this.values;
|
|
94
94
|
}
|
|
95
95
|
isAlias(name1, name2) {
|
|
96
|
-
const u =
|
|
96
|
+
const u = this.variables[name2];
|
|
97
97
|
return u && u[name1] === 1;
|
|
98
98
|
}
|
|
99
99
|
get(name) {
|
|
100
|
-
return
|
|
100
|
+
return this.values[name];
|
|
101
101
|
}
|
|
102
102
|
// debugging
|
|
103
103
|
getAllValues() {
|