@jterrazz/test 13.0.2 → 14.0.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 +20 -20
- package/dist/checker.js +1079 -895
- package/dist/index.d.ts +15 -15
- package/dist/index.js +23 -23
- package/dist/literate.js +11 -11
- package/dist/match.js +44 -7
- package/dist/oxlint.cjs +179 -43
- package/dist/oxlint.js +179 -43
- package/dist/spec-document.js +2 -2
- package/package.json +1 -1
- package/schema/spec.schema.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { DeepMockProxy } from "vitest-mock-extended";
|
|
|
6
6
|
* by every assertion of a single spec execution.
|
|
7
7
|
*
|
|
8
8
|
* Matchers are used inside `toMatchRows` rows and any structural comparison;
|
|
9
|
-
* their file-side twins are the `{{placeholder}}` forms in `
|
|
10
|
-
* fixtures (body AND headers), `
|
|
11
|
-
* (`
|
|
9
|
+
* their file-side twins are the `{{placeholder}}` forms in `_expected/*.http`
|
|
10
|
+
* fixtures (body AND headers), `_expected/*.json` files, and text snapshots
|
|
11
|
+
* (`_expected/*.txt`). One grammar, one vocabulary (CONVENTIONS D4).
|
|
12
12
|
*/
|
|
13
13
|
/** The frozen token vocabulary (CONVENTIONS D4) plus the code-only kinds. */
|
|
14
14
|
type MatcherKind = 'any' | 'base64' | 'date' | 'duration' | 'email' | 'float' | 'hex' | 'includes' | 'int' | 'ip' | 'iso8601' | 'number' | 'path' | 'port' | 'ref' | 'regex' | 'semver' | 'sha' | 'string' | 'time' | 'ulid' | 'url' | 'uuid' | 'workdir';
|
|
@@ -136,7 +136,7 @@ declare class CaptureScope {
|
|
|
136
136
|
*
|
|
137
137
|
* Assertions go through `expect()` (async — they walk the disk):
|
|
138
138
|
* `await expect(result.directory('out')).toMatch('scaffold/out')`
|
|
139
|
-
* compares the tree against the fixture directory `
|
|
139
|
+
* compares the tree against the fixture directory `_expected/<name>/`.
|
|
140
140
|
*/
|
|
141
141
|
declare class DirectoryAccessor {
|
|
142
142
|
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
@@ -159,7 +159,7 @@ declare class DirectoryAccessor {
|
|
|
159
159
|
*
|
|
160
160
|
* Assertions go through `expect()` (async — they walk the disk):
|
|
161
161
|
* `await expect(result.filesystem).toMatch('scaffolded')` compares the
|
|
162
|
-
* tree against the fixture directory `
|
|
162
|
+
* tree against the fixture directory `_expected/<name>/`.
|
|
163
163
|
*/
|
|
164
164
|
declare class FilesystemAccessor {
|
|
165
165
|
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
@@ -189,7 +189,7 @@ declare class FilesystemAccessor {
|
|
|
189
189
|
* covered by the `{{token}}` grammar.
|
|
190
190
|
*
|
|
191
191
|
* Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
|
|
192
|
-
* (deep-equal against `
|
|
192
|
+
* (deep-equal against `_expected/<name>`, with `{{placeholder}}` support).
|
|
193
193
|
*/
|
|
194
194
|
declare class JsonAccessor {
|
|
195
195
|
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
@@ -232,7 +232,7 @@ interface ServerPort {
|
|
|
232
232
|
*
|
|
233
233
|
* Assertions go through `expect()`:
|
|
234
234
|
* `expect(result.response).toMatch('created.http')` compares status,
|
|
235
|
-
* a subset of headers, and the JSON body against `
|
|
235
|
+
* a subset of headers, and the JSON body against `_expected/<name>` —
|
|
236
236
|
* with `{{placeholder}}` support in all three.
|
|
237
237
|
*/
|
|
238
238
|
declare class ResponseAccessor {
|
|
@@ -302,14 +302,14 @@ declare class TableAccessor {
|
|
|
302
302
|
* grammar.
|
|
303
303
|
*
|
|
304
304
|
* Text operations are **closed** over the type: `.grep(pattern)` returns a
|
|
305
|
-
* `TextAccessor` (not a bare string), preserving the `
|
|
305
|
+
* `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
|
|
306
306
|
* context and the `transform`, so results are chainable
|
|
307
307
|
* (`result.stdout.grep(a).grep(b)`) and snapshot-able
|
|
308
308
|
* (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
|
|
309
309
|
*
|
|
310
310
|
* Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
|
|
311
311
|
* and `expect(result.stdout).toMatch('name.txt')` (resolved against
|
|
312
|
-
* `
|
|
312
|
+
* `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
|
|
313
313
|
*/
|
|
314
314
|
declare class TextAccessor {
|
|
315
315
|
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
@@ -349,7 +349,7 @@ declare class TextAccessor {
|
|
|
349
349
|
*
|
|
350
350
|
* ```typescript
|
|
351
351
|
* const message = await catchMessage(() => expect(result.response).toMatch('wrong-body.http'));
|
|
352
|
-
* expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to
|
|
352
|
+
* expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to _expected/
|
|
353
353
|
* ```
|
|
354
354
|
*
|
|
355
355
|
* ANSI is stripped before every comparison (the raw form stays on `.text`),
|
|
@@ -1240,7 +1240,7 @@ interface ApiSpecification<DatabaseKey extends string = string> {
|
|
|
1240
1240
|
headers: (headers: Record<string, string>) => ApiSpecification<DatabaseKey>;
|
|
1241
1241
|
/** Declare outgoing calls — a contract, a list, a composite, or an inline request + response pair. */
|
|
1242
1242
|
intercept: InterceptMethod<ApiSpecification<DatabaseKey>>;
|
|
1243
|
-
/** Queue a SQL seed file from `
|
|
1243
|
+
/** Queue a SQL seed file from `_seeds/` to run before the action. */
|
|
1244
1244
|
seed: (file: string, options?: {
|
|
1245
1245
|
database?: DatabaseKey;
|
|
1246
1246
|
}) => ApiSpecification<DatabaseKey>;
|
|
@@ -1252,7 +1252,7 @@ interface ApiSpecification<DatabaseKey extends string = string> {
|
|
|
1252
1252
|
post: (path: string, body?: unknown) => Promise<HttpResult>;
|
|
1253
1253
|
/** Send a PUT request (optional inline JSON body) and resolve with the result. */
|
|
1254
1254
|
put: (path: string, body?: unknown) => Promise<HttpResult>;
|
|
1255
|
-
/** Send the complete request described by `
|
|
1255
|
+
/** Send the complete request described by `_requests/<file>` (.http format). */
|
|
1256
1256
|
request: (file: string) => Promise<HttpResult>;
|
|
1257
1257
|
}
|
|
1258
1258
|
/**
|
|
@@ -1262,7 +1262,7 @@ interface ApiSpecification<DatabaseKey extends string = string> {
|
|
|
1262
1262
|
interface JobsSpecification<DatabaseKey extends string = string> {
|
|
1263
1263
|
/** Declare outgoing calls — a contract, a list, a composite, or an inline request + response pair. */
|
|
1264
1264
|
intercept: InterceptMethod<JobsSpecification<DatabaseKey>>;
|
|
1265
|
-
/** Queue a SQL seed file from `
|
|
1265
|
+
/** Queue a SQL seed file from `_seeds/` to run before the action. */
|
|
1266
1266
|
seed: (file: string, options?: {
|
|
1267
1267
|
database?: DatabaseKey;
|
|
1268
1268
|
}) => JobsSpecification<DatabaseKey>;
|
|
@@ -1285,7 +1285,7 @@ interface CliSpecification<DatabaseKey extends string = string> {
|
|
|
1285
1285
|
* directory (or file) is copied under its own name. Chained calls layer.
|
|
1286
1286
|
*/
|
|
1287
1287
|
fixture: (path: string) => CliSpecification<DatabaseKey>;
|
|
1288
|
-
/** Queue a SQL seed file from `
|
|
1288
|
+
/** Queue a SQL seed file from `_seeds/` to run against a database before the action. */
|
|
1289
1289
|
seed: (file: string, options?: {
|
|
1290
1290
|
database?: DatabaseKey;
|
|
1291
1291
|
}) => CliSpecification<DatabaseKey>;
|
|
@@ -2454,7 +2454,7 @@ declare module 'vitest' {
|
|
|
2454
2454
|
toBeRunning: T extends ContainerAccessor ? () => Promise<void> : never;
|
|
2455
2455
|
/**
|
|
2456
2456
|
* On `@jterrazz/test` accessors: assert the subject matches a fixture
|
|
2457
|
-
* file under `
|
|
2457
|
+
* file under `_expected/<name>` (flat — a slash creates a subfolder;
|
|
2458
2458
|
* `.http` format for `result.response`). Async for filesystem/directory
|
|
2459
2459
|
* subjects (tree compare on disk). Other subjects keep vitest-native
|
|
2460
2460
|
* `toMatch` semantics (string substring / regexp).
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as copyPlan, i as match, n as Matcher, t as CaptureScope } from "./match.js";
|
|
1
|
+
import { a as copyPlan, c as GROUND_EXPECTED, d as GROUND_SEEDS, i as match, n as Matcher, t as CaptureScope, u as GROUND_REQUESTS } from "./match.js";
|
|
2
2
|
import { C as formatResponseDiff, E as formatTableDiff, M as structuralEquals, N as structuralSubset, O as mergePreservingPlaceholders, P as textEquals, S as formatDirectoryDiff, T as formatStdoutDiff, _ as getCallerDir, a as shouldUpdateSnapshots, b as diffDirectories, c as BaseResult, d as inspectContainer, f as removeContainers, g as text, h as TextAccessor, i as UPDATE_HINT, j as renderExpected, k as mergeTextPreservingPlaceholders, l as validateDatabaseOption, m as JsonAccessor, o as ContainerAccessor, p as TableAccessor, r as ServeAdapter, s as CliResult, t as runSpecDocument, u as findContainersByLabel, v as FilesystemAccessor, w as formatStartupReport, x as walkDirectory, y as DirectoryAccessor } from "./literate.js";
|
|
3
3
|
import { a as isContract, i as defineContracts, n as contractsOf, o as isContracts, r as defineContract, t as ContractQueue } from "./queue.js";
|
|
4
4
|
import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -450,7 +450,7 @@ function parseHeaders(headerLines, fileName) {
|
|
|
450
450
|
}
|
|
451
451
|
return headers;
|
|
452
452
|
}
|
|
453
|
-
/** Parse a `
|
|
453
|
+
/** Parse a `_requests/*.http` file. First line must be `METHOD /path`. */
|
|
454
454
|
function parseRequestFile(content, fileName) {
|
|
455
455
|
const { body, headerLines, startLine } = splitSections(content);
|
|
456
456
|
const start = /^(?<method>[A-Z]+)\s+(?<path>\S+)$/.exec(startLine.trim());
|
|
@@ -526,7 +526,7 @@ function isJsonLike(text) {
|
|
|
526
526
|
*
|
|
527
527
|
* Assertions go through `expect()`:
|
|
528
528
|
* `expect(result.response).toMatch('created.http')` compares status,
|
|
529
|
-
* a subset of headers, and the JSON body against `
|
|
529
|
+
* a subset of headers, and the JSON body against `_expected/<name>` —
|
|
530
530
|
* with `{{placeholder}}` support in all three.
|
|
531
531
|
*/
|
|
532
532
|
var ResponseAccessor = class {
|
|
@@ -577,36 +577,36 @@ function formatJson(value) {
|
|
|
577
577
|
}
|
|
578
578
|
function matchStreamFile(accessor, name, frozen) {
|
|
579
579
|
requireExtension(name, "stream");
|
|
580
|
-
const filePath = resolve(accessor.testDir,
|
|
580
|
+
const filePath = resolve(accessor.testDir, GROUND_EXPECTED, name);
|
|
581
581
|
const actual = accessor.comparableText;
|
|
582
582
|
if (shouldUpdateSnapshots() && !frozen) {
|
|
583
583
|
const merged = mergeTextPreservingPlaceholders(existsSync(filePath) ? readFileSync(filePath, "utf8") : null, actual, accessor.captures);
|
|
584
584
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
585
585
|
writeFileSync(filePath, merged);
|
|
586
|
-
return PASS(`updated
|
|
586
|
+
return PASS(`updated ${GROUND_EXPECTED}/${name}`);
|
|
587
587
|
}
|
|
588
588
|
if (!existsSync(filePath)) return FAIL(`${accessor.streamName} fixture "${name}" does not exist at ${filePath}.\n${UPDATE_HINT}`);
|
|
589
589
|
const expected = readFileSync(filePath, "utf8");
|
|
590
|
-
if (textEquals(expected, actual, accessor.captures)) return PASS(`expected ${accessor.streamName} not to match
|
|
590
|
+
if (textEquals(expected, actual, accessor.captures)) return PASS(`expected ${accessor.streamName} not to match ${GROUND_EXPECTED}/${name}`);
|
|
591
591
|
return FAIL(formatStdoutDiff(name, expected, actual, { equals: (expectedLine, actualLine) => textEquals(expectedLine, actualLine, new CaptureScope(accessor.captures.workdir)) }));
|
|
592
592
|
}
|
|
593
593
|
function matchJsonFile(accessor, name, frozen) {
|
|
594
594
|
requireExtension(name, "json");
|
|
595
|
-
const filePath = resolve(accessor.testDir,
|
|
595
|
+
const filePath = resolve(accessor.testDir, GROUND_EXPECTED, name);
|
|
596
596
|
const actual = accessor.value;
|
|
597
597
|
if (shouldUpdateSnapshots() && !frozen) {
|
|
598
598
|
const merged = existsSync(filePath) ? mergePreservingPlaceholders(JSON.parse(readFileSync(filePath, "utf8")), actual, accessor.captures.workdir) : mergePreservingPlaceholders(null, actual, accessor.captures.workdir);
|
|
599
599
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
600
600
|
writeFileSync(filePath, formatJson(merged));
|
|
601
|
-
return PASS(`updated
|
|
601
|
+
return PASS(`updated ${GROUND_EXPECTED}/${name}`);
|
|
602
602
|
}
|
|
603
603
|
if (!existsSync(filePath)) return FAIL(`JSON fixture "${name}" does not exist at ${filePath}.\n${UPDATE_HINT}`);
|
|
604
604
|
const expected = JSON.parse(readFileSync(filePath, "utf8"));
|
|
605
|
-
if (structuralEquals(expected, actual, accessor.captures)) return PASS(`expected JSON not to match
|
|
605
|
+
if (structuralEquals(expected, actual, accessor.captures)) return PASS(`expected JSON not to match ${GROUND_EXPECTED}/${name}`);
|
|
606
606
|
return FAIL(formatResponseDiff(name, renderExpected(expected), actual));
|
|
607
607
|
}
|
|
608
608
|
/**
|
|
609
|
-
* Build the updated `
|
|
609
|
+
* Build the updated `_expected/*.http` fixture content from the previous
|
|
610
610
|
* fixture and the actual response (CONVENTIONS D5). Headers are the
|
|
611
611
|
* INTERSECTION with the actual response: placeholders still matching are
|
|
612
612
|
* preserved, stale values are replaced, headers absent from the actual
|
|
@@ -633,7 +633,7 @@ function buildUpdatedResponse(previous, actual, workdir) {
|
|
|
633
633
|
};
|
|
634
634
|
}
|
|
635
635
|
/**
|
|
636
|
-
* Compare a parsed `
|
|
636
|
+
* Compare a parsed `_expected/*.http` fixture against an actual response.
|
|
637
637
|
* Returns the failure message, or null when everything matches.
|
|
638
638
|
*
|
|
639
639
|
* @internal Exported for unit tests.
|
|
@@ -649,19 +649,19 @@ function compareResponse(name, expected, actual, scope) {
|
|
|
649
649
|
}
|
|
650
650
|
function matchResponseFile(accessor, name, frozen) {
|
|
651
651
|
requireExtension(name, "response");
|
|
652
|
-
const filePath = resolve(accessor.testDir,
|
|
652
|
+
const filePath = resolve(accessor.testDir, GROUND_EXPECTED, name);
|
|
653
653
|
if (shouldUpdateSnapshots() && !frozen) {
|
|
654
|
-
const updated = buildUpdatedResponse(existsSync(filePath) ? parseResponseFile(readFileSync(filePath, "utf8"),
|
|
654
|
+
const updated = buildUpdatedResponse(existsSync(filePath) ? parseResponseFile(readFileSync(filePath, "utf8"), `${GROUND_EXPECTED}/${name}`) : null, accessor, accessor.captures.workdir);
|
|
655
655
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
656
656
|
writeFileSync(filePath, serializeResponseFile(updated));
|
|
657
|
-
return PASS(`updated
|
|
657
|
+
return PASS(`updated ${GROUND_EXPECTED}/${name}`);
|
|
658
658
|
}
|
|
659
659
|
if (!existsSync(filePath)) return FAIL(`Response fixture "${name}" does not exist at ${filePath}.\n${UPDATE_HINT}`);
|
|
660
|
-
const failure = compareResponse(name, parseResponseFile(readFileSync(filePath, "utf8"),
|
|
661
|
-
return failure === null ? PASS(`expected response not to match
|
|
660
|
+
const failure = compareResponse(name, parseResponseFile(readFileSync(filePath, "utf8"), `${GROUND_EXPECTED}/${name}`), accessor, accessor.captures);
|
|
661
|
+
return failure === null ? PASS(`expected response not to match ${GROUND_EXPECTED}/${name}`) : FAIL(failure);
|
|
662
662
|
}
|
|
663
663
|
async function matchTreeFile(actualRoot, testDir, name, scope, frozen) {
|
|
664
|
-
const fixtureDir = resolve(testDir,
|
|
664
|
+
const fixtureDir = resolve(testDir, GROUND_EXPECTED, name);
|
|
665
665
|
if (shouldUpdateSnapshots() && !frozen) {
|
|
666
666
|
const previousContents = /* @__PURE__ */ new Map();
|
|
667
667
|
if (existsSync(fixtureDir)) for (const file of await walkDirectory(fixtureDir)) previousContents.set(file, readFileSync(resolve(fixtureDir, file), "utf8"));
|
|
@@ -677,11 +677,11 @@ async function matchTreeFile(actualRoot, testDir, name, scope, frozen) {
|
|
|
677
677
|
const merged = mergeTextPreservingPlaceholders(previous, actual, scope);
|
|
678
678
|
if (merged !== actual) writeFileSync(resolve(fixtureDir, file), merged);
|
|
679
679
|
}
|
|
680
|
-
return PASS(`updated
|
|
680
|
+
return PASS(`updated ${GROUND_EXPECTED}/${name}/`);
|
|
681
681
|
}
|
|
682
682
|
if (!existsSync(fixtureDir)) return FAIL(`Directory fixture "${name}" does not exist at ${fixtureDir}.\n${UPDATE_HINT}`);
|
|
683
683
|
const diff = await diffDirectories(fixtureDir, actualRoot, { scope });
|
|
684
|
-
if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) return PASS(`expected directory not to match
|
|
684
|
+
if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) return PASS(`expected directory not to match ${GROUND_EXPECTED}/${name}/`);
|
|
685
685
|
return FAIL(formatDirectoryDiff(name, diff, "Run with TEST_UPDATE=1 to update the fixture."));
|
|
686
686
|
}
|
|
687
687
|
/**
|
|
@@ -1107,7 +1107,7 @@ var SpecificationBuilder = class {
|
|
|
1107
1107
|
}
|
|
1108
1108
|
/**
|
|
1109
1109
|
* Set HTTP headers for the request. Multiple calls merge; chain headers
|
|
1110
|
-
* win over headers from a `
|
|
1110
|
+
* win over headers from a `_requests/*.http` file.
|
|
1111
1111
|
*
|
|
1112
1112
|
* @example
|
|
1113
1113
|
* api.headers({ 'Accept-Language': 'fr' }).get("/articles");
|
|
@@ -1137,7 +1137,7 @@ var SpecificationBuilder = class {
|
|
|
1137
1137
|
return this;
|
|
1138
1138
|
}
|
|
1139
1139
|
/**
|
|
1140
|
-
* Send the complete request described by `
|
|
1140
|
+
* Send the complete request described by `_requests/<file>` — first line
|
|
1141
1141
|
* `METHOD /path`, then headers until a blank line, then the body.
|
|
1142
1142
|
* Headers set via `.headers()` merge on top of the file's headers.
|
|
1143
1143
|
*
|
|
@@ -1321,7 +1321,7 @@ var SpecificationBuilder = class {
|
|
|
1321
1321
|
if (!db) throw new Error(`seed() targets database "${entry.database}" but it was not found. Available: ${[...this.config.databases.keys()].join(", ")}`);
|
|
1322
1322
|
} else db = this.config.database;
|
|
1323
1323
|
if (!db) throw new Error("seed() requires a database adapter");
|
|
1324
|
-
const sql = readFileSync(resolve(this.testDir,
|
|
1324
|
+
const sql = readFileSync(resolve(this.testDir, GROUND_SEEDS, entry.file), "utf8");
|
|
1325
1325
|
await db.seed(sql);
|
|
1326
1326
|
}
|
|
1327
1327
|
if (this.fixtures.length > 0 && workDir) for (const entry of this.fixtures) {
|
|
@@ -1363,7 +1363,7 @@ var SpecificationBuilder = class {
|
|
|
1363
1363
|
let { body, method, path } = request;
|
|
1364
1364
|
let fileHeaders;
|
|
1365
1365
|
if (request.requestFile) {
|
|
1366
|
-
const parsed = parseRequestFile(readFileSync(resolve(this.testDir,
|
|
1366
|
+
const parsed = parseRequestFile(readFileSync(resolve(this.testDir, GROUND_REQUESTS, request.requestFile), "utf8"), `${GROUND_REQUESTS}/${request.requestFile}`);
|
|
1367
1367
|
body = parsed.body;
|
|
1368
1368
|
fileHeaders = parsed.headers;
|
|
1369
1369
|
method = parsed.method;
|
package/dist/literate.js
CHANGED
|
@@ -16,8 +16,8 @@ import { createServer } from "node:net";
|
|
|
16
16
|
* - strings containing `{{placeholder}}` forms (file-side fixtures)
|
|
17
17
|
*
|
|
18
18
|
* One unified `{{token}}` grammar (CONVENTIONS D4) — the same vocabulary
|
|
19
|
-
* works in `
|
|
20
|
-
* text snapshots (`
|
|
19
|
+
* works in `_expected/*.http` (body and headers), `_expected/*.json`, and
|
|
20
|
+
* text snapshots (`_expected/*.txt`).
|
|
21
21
|
*
|
|
22
22
|
* Ref captures (`match.ref(name)` / `{{type#name}}`) are recorded in the
|
|
23
23
|
* {@link CaptureScope} supplied by the caller.
|
|
@@ -232,7 +232,7 @@ function structuralSubset(expected, actual, scope) {
|
|
|
232
232
|
}
|
|
233
233
|
/**
|
|
234
234
|
* Multi-line text comparison with `{{token}}` support — used by text
|
|
235
|
-
* snapshots (`
|
|
235
|
+
* snapshots (`_expected/*.txt`). Without placeholders this is strict equality.
|
|
236
236
|
*/
|
|
237
237
|
function textEquals(expected, actual, scope) {
|
|
238
238
|
if (!hasPlaceholders(expected)) return expected === actual;
|
|
@@ -592,7 +592,7 @@ async function diffDirectories(expectedRoot, actualRoot, options = {}) {
|
|
|
592
592
|
*
|
|
593
593
|
* Assertions go through `expect()` (async — they walk the disk):
|
|
594
594
|
* `await expect(result.directory('out')).toMatch('scaffold/out')`
|
|
595
|
-
* compares the tree against the fixture directory `
|
|
595
|
+
* compares the tree against the fixture directory `_expected/<name>/`.
|
|
596
596
|
*/
|
|
597
597
|
var DirectoryAccessor = class {
|
|
598
598
|
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
@@ -619,7 +619,7 @@ var DirectoryAccessor = class {
|
|
|
619
619
|
*
|
|
620
620
|
* Assertions go through `expect()` (async — they walk the disk):
|
|
621
621
|
* `await expect(result.filesystem).toMatch('scaffolded')` compares the
|
|
622
|
-
* tree against the fixture directory `
|
|
622
|
+
* tree against the fixture directory `_expected/<name>/`.
|
|
623
623
|
*/
|
|
624
624
|
var FilesystemAccessor = class {
|
|
625
625
|
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
@@ -691,8 +691,8 @@ function isFrameworkFrame(filePath, frameworkTree = FRAMEWORK_TREE) {
|
|
|
691
691
|
}
|
|
692
692
|
/**
|
|
693
693
|
* Detect the directory of the first stack frame that lives outside this
|
|
694
|
-
* package. Used to anchor fixture resolution (`
|
|
695
|
-
* `
|
|
694
|
+
* package. Used to anchor fixture resolution (`_seeds/`, `_requests/`,
|
|
695
|
+
* `_expected/`, …) on the calling test / specification file.
|
|
696
696
|
*
|
|
697
697
|
* @internal
|
|
698
698
|
*/
|
|
@@ -745,14 +745,14 @@ function stripAnsiCodes(value) {
|
|
|
745
745
|
* grammar.
|
|
746
746
|
*
|
|
747
747
|
* Text operations are **closed** over the type: `.grep(pattern)` returns a
|
|
748
|
-
* `TextAccessor` (not a bare string), preserving the `
|
|
748
|
+
* `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
|
|
749
749
|
* context and the `transform`, so results are chainable
|
|
750
750
|
* (`result.stdout.grep(a).grep(b)`) and snapshot-able
|
|
751
751
|
* (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
|
|
752
752
|
*
|
|
753
753
|
* Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
|
|
754
754
|
* and `expect(result.stdout).toMatch('name.txt')` (resolved against
|
|
755
|
-
* `
|
|
755
|
+
* `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
|
|
756
756
|
*/
|
|
757
757
|
var TextAccessor = class TextAccessor {
|
|
758
758
|
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
@@ -807,7 +807,7 @@ var TextAccessor = class TextAccessor {
|
|
|
807
807
|
*
|
|
808
808
|
* ```typescript
|
|
809
809
|
* const message = await catchMessage(() => expect(result.response).toMatch('wrong-body.http'));
|
|
810
|
-
* expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to
|
|
810
|
+
* expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to _expected/
|
|
811
811
|
* ```
|
|
812
812
|
*
|
|
813
813
|
* ANSI is stripped before every comparison (the raw form stays on `.text`),
|
|
@@ -832,7 +832,7 @@ function text(value) {
|
|
|
832
832
|
* covered by the `{{token}}` grammar.
|
|
833
833
|
*
|
|
834
834
|
* Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
|
|
835
|
-
* (deep-equal against `
|
|
835
|
+
* (deep-equal against `_expected/<name>`, with `{{placeholder}}` support).
|
|
836
836
|
*/
|
|
837
837
|
var JsonAccessor = class {
|
|
838
838
|
/** @internal Ref-capture scope shared by the current spec execution. */
|
package/dist/match.js
CHANGED
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
import { basename, dirname, resolve } from "node:path";
|
|
2
|
+
//#region src/core/specification/shared/ground.ts
|
|
3
|
+
/**
|
|
4
|
+
* The GROUND a row of specs stands on — the naming law of a spec tree.
|
|
5
|
+
*
|
|
6
|
+
* In a row of specs, a MEMBER is a spec: `<aspect>.test.ts`, `<case>.spec.yaml`,
|
|
7
|
+
* `<name>.specification.ts`, and the facet/domain folders that hold them.
|
|
8
|
+
* Everything else in that row is what the members STAND ON — inert material the
|
|
9
|
+
* framework resolves BY PATH, never imported, never executed — and it carries a
|
|
10
|
+
* LEADING UNDERSCORE. A spec's own folder never does.
|
|
11
|
+
*
|
|
12
|
+
* Four names, and only four: `_fixtures/` (file state), `_expected/` (goldens),
|
|
13
|
+
* `_requests/` (inputs), `_seeds/` (database state). `contracts/` is NOT ground —
|
|
14
|
+
* a contract is TypeScript a spec imports, behaviour composed with the spec's own,
|
|
15
|
+
* so it stays a member. `docker/` is not ground either: it sits at the PROJECT
|
|
16
|
+
* root beside `package.json`, outside any row of specs.
|
|
17
|
+
*
|
|
18
|
+
* This module is the single home of those names. Every resolver, matcher, lint
|
|
19
|
+
* rule and checker pass reads them from here, so the runtime and the static
|
|
20
|
+
* channels cannot disagree about where a spec's ground lives.
|
|
21
|
+
*/
|
|
22
|
+
/** Goldens: every expected fixture, flat, extension included. */
|
|
23
|
+
const GROUND_EXPECTED = "_expected";
|
|
24
|
+
/** File state copied into a working directory by `.fixture()`. */
|
|
25
|
+
const GROUND_FIXTURES = "_fixtures";
|
|
26
|
+
/** Inputs: complete `.http` request documents. */
|
|
27
|
+
const GROUND_REQUESTS = "_requests";
|
|
28
|
+
/** Database state: `.sql` fragments loaded by `.seed()`. */
|
|
29
|
+
const GROUND_SEEDS = "_seeds";
|
|
30
|
+
/** The four ground directory names, in the order they read. */
|
|
31
|
+
const GROUND_DIRS = [
|
|
32
|
+
GROUND_EXPECTED,
|
|
33
|
+
GROUND_FIXTURES,
|
|
34
|
+
GROUND_REQUESTS,
|
|
35
|
+
GROUND_SEEDS
|
|
36
|
+
];
|
|
37
|
+
//#endregion
|
|
2
38
|
//#region src/core/specification/shared/fixtures.ts
|
|
3
39
|
/**
|
|
4
40
|
* Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
|
|
5
41
|
*
|
|
6
42
|
* A fixture path is one of two shapes:
|
|
7
43
|
*
|
|
8
|
-
* - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/
|
|
44
|
+
* - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/_fixtures/<rest>`,
|
|
9
45
|
* where `<specs-root>` is the nearest ancestor directory named `specs`.
|
|
10
|
-
* - `<path>` (no marker) — feature-local, at `<test-dir>/
|
|
46
|
+
* - `<path>` (no marker) — feature-local, at `<test-dir>/_fixtures/<path>`.
|
|
11
47
|
*
|
|
48
|
+
* The leading underscore is the ground rule of a spec tree ({@link GROUND_FIXTURES}).
|
|
12
49
|
* Any other `$`-prefixed marker is a usage error (listed against the known
|
|
13
50
|
* markers). Copy semantics mirror rsync's trailing-slash rule (see
|
|
14
51
|
* {@link copyPlan}).
|
|
@@ -32,7 +69,7 @@ function discoverSpecsRoot(startDir) {
|
|
|
32
69
|
if (parent === dir) break;
|
|
33
70
|
dir = parent;
|
|
34
71
|
}
|
|
35
|
-
throw new Error(`.fixture(): the $FIXTURES marker resolves to <specs-root
|
|
72
|
+
throw new Error(`.fixture(): the $FIXTURES marker resolves to <specs-root>/${GROUND_FIXTURES}, but no directory named "specs" was found walking up from ${startDir}. Move the specification under a specs/ directory, or use a feature-local ${GROUND_FIXTURES}/ path (no $FIXTURES marker).`);
|
|
36
73
|
}
|
|
37
74
|
/**
|
|
38
75
|
* Resolve the absolute source path of a fixture, applying marker resolution.
|
|
@@ -43,13 +80,13 @@ function resolveFixtureSource(path, testDir) {
|
|
|
43
80
|
const clean = path.replace(/\/+$/, "");
|
|
44
81
|
if (clean === "$FIXTURES" || path.startsWith("$FIXTURES/")) {
|
|
45
82
|
const rest = clean.slice(9).replace(/^\/+/, "");
|
|
46
|
-
return resolve(discoverSpecsRoot(testDir),
|
|
83
|
+
return resolve(discoverSpecsRoot(testDir), GROUND_FIXTURES, rest);
|
|
47
84
|
}
|
|
48
85
|
if (path.startsWith("$")) {
|
|
49
86
|
const marker = clean.split("/")[0];
|
|
50
|
-
throw new Error(`.fixture("${path}"): unknown marker "${marker}". Known markers: ${KNOWN_FIXTURE_MARKERS.join(", ")}. A path without a marker is feature-local (resolved under <test-dir
|
|
87
|
+
throw new Error(`.fixture("${path}"): unknown marker "${marker}". Known markers: ${KNOWN_FIXTURE_MARKERS.join(", ")}. A path without a marker is feature-local (resolved under <test-dir>/${GROUND_FIXTURES}/).`);
|
|
51
88
|
}
|
|
52
|
-
return resolve(testDir,
|
|
89
|
+
return resolve(testDir, GROUND_FIXTURES, clean);
|
|
53
90
|
}
|
|
54
91
|
/**
|
|
55
92
|
* Compute the source + destination for copying a fixture into the working
|
|
@@ -233,4 +270,4 @@ var CaptureScope = class {
|
|
|
233
270
|
}
|
|
234
271
|
};
|
|
235
272
|
//#endregion
|
|
236
|
-
export { copyPlan as a, match as i, Matcher as n, resolveFixtureSource as o, TOKEN_KINDS as r, CaptureScope as t };
|
|
273
|
+
export { copyPlan as a, GROUND_EXPECTED as c, GROUND_SEEDS as d, match as i, GROUND_FIXTURES as l, Matcher as n, resolveFixtureSource as o, TOKEN_KINDS as r, GROUND_DIRS as s, CaptureScope as t, GROUND_REQUESTS as u };
|