@jterrazz/test 4.0.0 → 5.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 +52 -43
- package/dist/index.cjs +49 -276
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -105
- package/dist/index.d.ts +43 -105
- package/dist/index.js +49 -277
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ test("creates a user", async () => {
|
|
|
39
39
|
.run();
|
|
40
40
|
|
|
41
41
|
// Then — user created
|
|
42
|
-
result.status.toBe(201);
|
|
42
|
+
expect(result.status).toBe(201);
|
|
43
43
|
await result.table("users").toMatch({
|
|
44
44
|
columns: ["name"],
|
|
45
45
|
rows: [["Alice"], ["Bob"]],
|
|
@@ -69,11 +69,11 @@ test("builds the project", async () => {
|
|
|
69
69
|
const result = await spec("build").project("sample-app").exec("build").run();
|
|
70
70
|
|
|
71
71
|
// Then — ESM output with source maps
|
|
72
|
-
result.exitCode.toBe(0);
|
|
73
|
-
result.stdout.toContain("Build completed");
|
|
74
|
-
result.file("dist/index.js").
|
|
75
|
-
result.file("dist/index.cjs").
|
|
76
|
-
result.file("dist/index.js").toContain("Hello");
|
|
72
|
+
expect(result.exitCode).toBe(0);
|
|
73
|
+
expect(result.stdout).toContain("Build completed");
|
|
74
|
+
expect(result.file("dist/index.js").exists).toBe(true);
|
|
75
|
+
expect(result.file("dist/index.cjs").exists).toBe(false);
|
|
76
|
+
expect(result.file("dist/index.js").content).toContain("Hello");
|
|
77
77
|
});
|
|
78
78
|
```
|
|
79
79
|
|
|
@@ -165,42 +165,50 @@ Every test follows the same pattern: `spec("label") → setup → action → ass
|
|
|
165
165
|
|
|
166
166
|
### Assertions
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
**
|
|
171
|
-
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
| `result.
|
|
175
|
-
| `result.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
|
182
|
-
|
|
|
183
|
-
| `result.
|
|
184
|
-
| `result.
|
|
185
|
-
| `result.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
| `result.
|
|
203
|
-
| `result.
|
|
168
|
+
Result properties are raw values — use vitest `expect()` for assertions. Database and response file assertions use custom async methods.
|
|
169
|
+
|
|
170
|
+
**Raw values (vitest expect):**
|
|
171
|
+
|
|
172
|
+
| Expression | Description |
|
|
173
|
+
| -------------------------------------------- | --------------------------- |
|
|
174
|
+
| `expect(result.exitCode).toBe(0)` | CLI exit code |
|
|
175
|
+
| `expect(result.status).toBe(201)` | HTTP status code |
|
|
176
|
+
| `expect(result.stdout).toContain("hello")` | CLI stdout contains string |
|
|
177
|
+
| `expect(result.stderr).not.toContain("err")` | CLI stderr does not contain |
|
|
178
|
+
|
|
179
|
+
**Files (result.file returns {exists, content}):**
|
|
180
|
+
|
|
181
|
+
| Expression | Description |
|
|
182
|
+
| ----------------------------------------------------------------- | --------------------------- |
|
|
183
|
+
| `expect(result.file("dist/index.js").exists).toBe(true)` | Assert file exists |
|
|
184
|
+
| `expect(result.file("dist/index.js").content).toContain("Hello")` | Assert file contains string |
|
|
185
|
+
| `expect(result.file("dist/index.cjs").exists).toBe(false)` | Assert file does not exist |
|
|
186
|
+
|
|
187
|
+
**Grep (scoped text matching):**
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { grep } from "@jterrazz/test";
|
|
191
|
+
|
|
192
|
+
expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars");
|
|
193
|
+
expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports");
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`grep(output, pattern)` filters multi-line output to the block matching `pattern`, returning a string for vitest assertions.
|
|
197
|
+
|
|
198
|
+
**Response (HTTP body):**
|
|
199
|
+
|
|
200
|
+
| Expression | Description |
|
|
201
|
+
| ----------------------------------------------- | --------------------------------------------------------------------------- |
|
|
202
|
+
| `result.response.toMatchFile("expected.json")` | Custom — compares body to `responses/expected.json`, shows diff on mismatch |
|
|
203
|
+
| `expect(result.response.body).toEqual({ ... })` | Raw body object for vitest assertions |
|
|
204
|
+
|
|
205
|
+
**Tables (custom async — database queries):**
|
|
206
|
+
|
|
207
|
+
| Expression | Description |
|
|
208
|
+
| ------------------------------------------------------------------------------- | ------------------------------ |
|
|
209
|
+
| `await result.table("users").toMatch({ columns: ["name"], rows: [["Alice"]] })` | Assert database table contents |
|
|
210
|
+
| `await result.table("events", { service: "analytics-db" }).toMatch({ ... })` | Assert on a specific database |
|
|
211
|
+
| `await result.table("users").toBeEmpty()` | Assert database table is empty |
|
|
204
212
|
|
|
205
213
|
## Multi-database support
|
|
206
214
|
|
|
@@ -221,6 +229,7 @@ const result = await spec("cross-db")
|
|
|
221
229
|
.post("/users", "request.json")
|
|
222
230
|
.run();
|
|
223
231
|
|
|
232
|
+
expect(result.status).toBe(201);
|
|
224
233
|
await result.table("users").toMatch({ columns: ["name"], rows: [["Alice"]] });
|
|
225
234
|
await result.table("events", { service: "analytics-db" }).toMatch({
|
|
226
235
|
columns: ["type"],
|
|
@@ -306,7 +315,7 @@ test("creates a user and returns 201", async () => {
|
|
|
306
315
|
.run();
|
|
307
316
|
|
|
308
317
|
// Then — user created with all three in table
|
|
309
|
-
result.status.toBe(201);
|
|
318
|
+
expect(result.status).toBe(201);
|
|
310
319
|
await result.table("users").toMatch({
|
|
311
320
|
columns: ["name"],
|
|
312
321
|
rows: [["Alice"], ["Bob"], ["Charlie"]],
|
package/dist/index.cjs
CHANGED
|
@@ -244,20 +244,6 @@ function formatStartupReport(mode, services, app) {
|
|
|
244
244
|
lines.push("");
|
|
245
245
|
return lines.join("\n");
|
|
246
246
|
}
|
|
247
|
-
function formatStatusError(expectedStatus, receivedStatus, request, responseBody) {
|
|
248
|
-
const lines = [];
|
|
249
|
-
lines.push(`Expected status: ${GREEN}${expectedStatus}${RESET}`);
|
|
250
|
-
lines.push(`Received status: ${RED}${receivedStatus}${RESET}`);
|
|
251
|
-
lines.push("");
|
|
252
|
-
lines.push(`${DIM}${request.method} ${request.path}${RESET}`);
|
|
253
|
-
if (request.body) lines.push(formatJson(request.body, DIM));
|
|
254
|
-
if (responseBody) {
|
|
255
|
-
lines.push("");
|
|
256
|
-
lines.push(`${DIM}Response:${RESET}`);
|
|
257
|
-
lines.push(formatJson(responseBody, RED));
|
|
258
|
-
}
|
|
259
|
-
return lines.join("\n");
|
|
260
|
-
}
|
|
261
247
|
function formatTableDiff(table, columns, expected, actual) {
|
|
262
248
|
const lines = [];
|
|
263
249
|
lines.push(`Table "${table}" mismatch`);
|
|
@@ -306,66 +292,9 @@ function formatResponseDiff(file, expected, actual) {
|
|
|
306
292
|
}
|
|
307
293
|
return lines.join("\n");
|
|
308
294
|
}
|
|
309
|
-
function formatExitCodeError(expected, received, stdout, stderr) {
|
|
310
|
-
const lines = [];
|
|
311
|
-
lines.push(`Expected exit code: ${GREEN}${expected}${RESET}`);
|
|
312
|
-
lines.push(`Received exit code: ${RED}${received}${RESET}`);
|
|
313
|
-
if (stdout.trim()) {
|
|
314
|
-
lines.push("");
|
|
315
|
-
lines.push(`${DIM}stdout:${RESET}`);
|
|
316
|
-
for (const line of stdout.trim().split("\n").slice(-15)) lines.push(` ${DIM}${line}${RESET}`);
|
|
317
|
-
}
|
|
318
|
-
if (stderr.trim()) {
|
|
319
|
-
lines.push("");
|
|
320
|
-
lines.push(`${DIM}stderr:${RESET}`);
|
|
321
|
-
for (const line of stderr.trim().split("\n").slice(-15)) lines.push(` ${RED}${line}${RESET}`);
|
|
322
|
-
}
|
|
323
|
-
return lines.join("\n");
|
|
324
|
-
}
|
|
325
|
-
function formatStdoutDiff(file, expected, actual) {
|
|
326
|
-
const lines = [];
|
|
327
|
-
lines.push(`Output mismatch (${file})`);
|
|
328
|
-
lines.push("");
|
|
329
|
-
lines.push(`${GREEN}- Expected${RESET}`);
|
|
330
|
-
lines.push(`${RED}+ Received${RESET}`);
|
|
331
|
-
lines.push("");
|
|
332
|
-
const expectedLines = expected.split("\n");
|
|
333
|
-
const actualLines = actual.split("\n");
|
|
334
|
-
const maxLines = Math.max(expectedLines.length, actualLines.length);
|
|
335
|
-
for (let i = 0; i < maxLines; i++) {
|
|
336
|
-
const exp = expectedLines[i];
|
|
337
|
-
const act = actualLines[i];
|
|
338
|
-
if (exp === act) lines.push(` ${exp}`);
|
|
339
|
-
else {
|
|
340
|
-
if (exp !== void 0) lines.push(`${GREEN}- ${exp}${RESET}`);
|
|
341
|
-
if (act !== void 0) lines.push(`${RED}+ ${act}${RESET}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return lines.join("\n");
|
|
345
|
-
}
|
|
346
|
-
function formatFileMissing(path) {
|
|
347
|
-
return `Expected file to exist: ${RED}${path}${RESET}`;
|
|
348
|
-
}
|
|
349
|
-
function formatFileUnexpected(path) {
|
|
350
|
-
return `Expected file NOT to exist: ${RED}${path}${RESET}`;
|
|
351
|
-
}
|
|
352
|
-
function formatFileContentMismatch(path, expected, actual) {
|
|
353
|
-
const lines = [];
|
|
354
|
-
lines.push(`File "${path}" does not contain expected content`);
|
|
355
|
-
lines.push("");
|
|
356
|
-
lines.push(`${GREEN}Expected to contain:${RESET}`);
|
|
357
|
-
lines.push(` ${GREEN}${expected}${RESET}`);
|
|
358
|
-
lines.push("");
|
|
359
|
-
lines.push(`${RED}Actual content (first 20 lines):${RESET}`);
|
|
360
|
-
for (const line of actual.split("\n").slice(0, 20)) lines.push(` ${DIM}${line}${RESET}`);
|
|
361
|
-
return lines.join("\n");
|
|
362
|
-
}
|
|
363
295
|
function rowLabel(n) {
|
|
364
296
|
return n === 1 ? "1 row" : `${n} rows`;
|
|
365
297
|
}
|
|
366
|
-
function formatJson(value, color) {
|
|
367
|
-
return JSON.stringify(value, null, 2).split("\n").map((line) => `${color}${line}${RESET}`).join("\n");
|
|
368
|
-
}
|
|
369
298
|
function formatRow(row) {
|
|
370
299
|
return row.map((v) => String(v ?? "null")).join(" | ");
|
|
371
300
|
}
|
|
@@ -861,205 +790,35 @@ var HonoAdapter = class {
|
|
|
861
790
|
}
|
|
862
791
|
};
|
|
863
792
|
//#endregion
|
|
864
|
-
//#region src/specification/
|
|
865
|
-
|
|
866
|
-
* Base assertion that handles .not negation.
|
|
867
|
-
* Subclasses call this.assert(condition, message, negatedMessage) for each predicate.
|
|
868
|
-
*/
|
|
869
|
-
var BaseAssertion = class {
|
|
870
|
-
negated = false;
|
|
871
|
-
get not() {
|
|
872
|
-
const clone = Object.create(Object.getPrototypeOf(this));
|
|
873
|
-
Object.assign(clone, this);
|
|
874
|
-
clone.negated = !this.negated;
|
|
875
|
-
return clone;
|
|
876
|
-
}
|
|
877
|
-
assert(condition, message, negatedMessage) {
|
|
878
|
-
if (this.negated) {
|
|
879
|
-
if (condition) throw new Error(negatedMessage);
|
|
880
|
-
} else if (!condition) throw new Error(message);
|
|
881
|
-
}
|
|
882
|
-
};
|
|
883
|
-
//#endregion
|
|
884
|
-
//#region src/specification/assertions/file.ts
|
|
885
|
-
/**
|
|
886
|
-
* Assertions on a file in the working directory.
|
|
887
|
-
* Usage: result.file("dist/index.js").toExist()
|
|
888
|
-
*/
|
|
889
|
-
var FileAssertion = class extends BaseAssertion {
|
|
890
|
-
filePath;
|
|
891
|
-
resolvedPath;
|
|
892
|
-
constructor(filePath, workDir) {
|
|
893
|
-
super();
|
|
894
|
-
this.filePath = filePath;
|
|
895
|
-
this.resolvedPath = (0, node_path.resolve)(workDir, filePath);
|
|
896
|
-
}
|
|
897
|
-
toExist() {
|
|
898
|
-
const exists = (0, node_fs.existsSync)(this.resolvedPath);
|
|
899
|
-
this.assert(exists, formatFileMissing(this.filePath), formatFileUnexpected(this.filePath));
|
|
900
|
-
}
|
|
901
|
-
toContain(expected) {
|
|
902
|
-
if (!(0, node_fs.existsSync)(this.resolvedPath)) {
|
|
903
|
-
if (this.negated) return;
|
|
904
|
-
throw new Error(formatFileMissing(this.filePath));
|
|
905
|
-
}
|
|
906
|
-
const content = (0, node_fs.readFileSync)(this.resolvedPath, "utf8");
|
|
907
|
-
const found = content.includes(expected);
|
|
908
|
-
this.assert(found, formatFileContentMismatch(this.filePath, expected, content), `Expected file "${this.filePath}" NOT to contain "${expected}"`);
|
|
909
|
-
}
|
|
910
|
-
toMatch(pattern) {
|
|
911
|
-
if (!(0, node_fs.existsSync)(this.resolvedPath)) {
|
|
912
|
-
if (this.negated) return;
|
|
913
|
-
throw new Error(formatFileMissing(this.filePath));
|
|
914
|
-
}
|
|
915
|
-
const content = (0, node_fs.readFileSync)(this.resolvedPath, "utf8");
|
|
916
|
-
const found = pattern.test(content);
|
|
917
|
-
this.assert(found, `Expected file "${this.filePath}" to match: ${pattern}\n\nActual content:\n${content.slice(0, 500)}`, `Expected file "${this.filePath}" NOT to match: ${pattern}`);
|
|
918
|
-
}
|
|
919
|
-
};
|
|
920
|
-
//#endregion
|
|
921
|
-
//#region src/specification/assertions/response.ts
|
|
922
|
-
/**
|
|
923
|
-
* Assertions on an HTTP response body.
|
|
924
|
-
* Usage: result.response.toMatchFile("expected.json")
|
|
925
|
-
*/
|
|
926
|
-
var ResponseAssertion = class extends BaseAssertion {
|
|
927
|
-
body;
|
|
928
|
-
testDir;
|
|
929
|
-
constructor(body, testDir) {
|
|
930
|
-
super();
|
|
931
|
-
this.body = body;
|
|
932
|
-
this.testDir = testDir;
|
|
933
|
-
}
|
|
934
|
-
toMatchFile(file) {
|
|
935
|
-
const expected = JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "responses", file), "utf8"));
|
|
936
|
-
const match = JSON.stringify(this.body) === JSON.stringify(expected);
|
|
937
|
-
this.assert(match, formatResponseDiff(file, expected, this.body), `Expected response NOT to match file "${file}", but it did`);
|
|
938
|
-
}
|
|
939
|
-
toContain(subset) {
|
|
940
|
-
const bodyStr = JSON.stringify(this.body);
|
|
941
|
-
const subsetStr = JSON.stringify(subset);
|
|
942
|
-
const bodyObj = typeof this.body === "object" && this.body !== null ? this.body : {};
|
|
943
|
-
const match = Object.entries(subset).every(([key, value]) => JSON.stringify(bodyObj[key]) === JSON.stringify(value));
|
|
944
|
-
this.assert(match, `Expected response to contain: ${subsetStr}\n\nActual response:\n${bodyStr}`, `Expected response NOT to contain: ${subsetStr}`);
|
|
945
|
-
}
|
|
946
|
-
};
|
|
947
|
-
//#endregion
|
|
948
|
-
//#region src/specification/assertions/string.ts
|
|
949
|
-
/**
|
|
950
|
-
* Assertions on a string (stdout, stderr, response body).
|
|
951
|
-
* Usage: result.stdout.toContain("hello")
|
|
952
|
-
*/
|
|
953
|
-
var StringAssertion = class extends BaseAssertion {
|
|
954
|
-
actual;
|
|
955
|
-
label;
|
|
956
|
-
testDir;
|
|
957
|
-
constructor(actual, label, testDir) {
|
|
958
|
-
super();
|
|
959
|
-
this.actual = actual;
|
|
960
|
-
this.label = label;
|
|
961
|
-
this.testDir = testDir;
|
|
962
|
-
}
|
|
963
|
-
toContain(expected, options) {
|
|
964
|
-
if (options?.near) {
|
|
965
|
-
const found = this.containsNear(expected, options.near);
|
|
966
|
-
this.assert(found, `Expected ${this.label} to contain "${expected}" near "${options.near}"\n\n${this.label}:\n${this.truncate(this.actual)}`, `Expected ${this.label} NOT to contain "${expected}" near "${options.near}", but it was found`);
|
|
967
|
-
} else {
|
|
968
|
-
const found = this.actual.includes(expected);
|
|
969
|
-
this.assert(found, `Expected ${this.label} to contain: "${expected}"\n\nActual ${this.label}:\n${this.truncate(this.actual)}`, `Expected ${this.label} NOT to contain: "${expected}"`);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
toMatch(pattern) {
|
|
973
|
-
const found = pattern.test(this.actual);
|
|
974
|
-
this.assert(found, `Expected ${this.label} to match: ${pattern}\n\nActual ${this.label}:\n${this.truncate(this.actual)}`, `Expected ${this.label} NOT to match: ${pattern}`);
|
|
975
|
-
}
|
|
976
|
-
toMatchFile(file) {
|
|
977
|
-
if (!this.testDir) throw new Error("toMatchFile requires a test directory context");
|
|
978
|
-
const expected = (0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "expected", file), "utf8").trim();
|
|
979
|
-
const actual = this.actual.trim();
|
|
980
|
-
const match = actual === expected;
|
|
981
|
-
this.assert(match, formatStdoutDiff(file, expected, actual), `Expected ${this.label} NOT to match file "${file}", but it did`);
|
|
982
|
-
}
|
|
983
|
-
toBeEmpty() {
|
|
984
|
-
const empty = this.actual.trim() === "";
|
|
985
|
-
this.assert(empty, `Expected ${this.label} to be empty\n\nActual ${this.label}:\n${this.truncate(this.actual)}`, `Expected ${this.label} NOT to be empty`);
|
|
986
|
-
}
|
|
987
|
-
containsNear(target, near, proximity = 500) {
|
|
988
|
-
const clean = this.stripAnsi(this.actual);
|
|
989
|
-
const nearLower = near.toLowerCase();
|
|
990
|
-
const targetLower = target.toLowerCase();
|
|
991
|
-
let searchFrom = 0;
|
|
992
|
-
while (true) {
|
|
993
|
-
const idx = clean.toLowerCase().indexOf(nearLower, searchFrom);
|
|
994
|
-
if (idx === -1) break;
|
|
995
|
-
const windowStart = Math.max(0, idx - proximity);
|
|
996
|
-
const windowEnd = Math.min(clean.length, idx + nearLower.length + proximity);
|
|
997
|
-
if (clean.substring(windowStart, windowEnd).toLowerCase().includes(targetLower)) return true;
|
|
998
|
-
searchFrom = idx + 1;
|
|
999
|
-
}
|
|
1000
|
-
return false;
|
|
1001
|
-
}
|
|
1002
|
-
stripAnsi(str) {
|
|
1003
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1004
|
-
}
|
|
1005
|
-
truncate(str, maxLines = 20) {
|
|
1006
|
-
const lines = str.split("\n");
|
|
1007
|
-
if (lines.length <= maxLines) return str;
|
|
1008
|
-
return `${lines.slice(0, maxLines).join("\n")}\n... (${lines.length - maxLines} more lines)`;
|
|
1009
|
-
}
|
|
1010
|
-
};
|
|
1011
|
-
//#endregion
|
|
1012
|
-
//#region src/specification/assertions/table.ts
|
|
1013
|
-
/**
|
|
1014
|
-
* Assertions on a database table.
|
|
1015
|
-
* Usage: await result.table("users").toMatch({ columns: ["name"], rows: [["Alice"]] })
|
|
1016
|
-
*/
|
|
1017
|
-
var TableAssertion = class extends BaseAssertion {
|
|
793
|
+
//#region src/specification/specification.ts
|
|
794
|
+
var TableAssertion = class {
|
|
1018
795
|
tableName;
|
|
1019
796
|
db;
|
|
1020
797
|
constructor(tableName, db) {
|
|
1021
|
-
super();
|
|
1022
798
|
this.tableName = tableName;
|
|
1023
799
|
this.db = db;
|
|
1024
800
|
}
|
|
1025
801
|
async toMatch(expected) {
|
|
1026
802
|
const actual = await this.db.query(this.tableName, expected.columns);
|
|
1027
|
-
|
|
1028
|
-
this.assert(match, formatTableDiff(this.tableName, expected.columns, expected.rows, actual), `Expected table "${this.tableName}" NOT to match, but it did`);
|
|
803
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected.rows)) throw new Error(formatTableDiff(this.tableName, expected.columns, expected.rows, actual));
|
|
1029
804
|
}
|
|
1030
805
|
async toBeEmpty() {
|
|
1031
806
|
const actual = await this.db.query(this.tableName, ["*"]);
|
|
1032
|
-
|
|
1033
|
-
this.assert(empty, `Expected table "${this.tableName}" to be empty, but it has ${actual.length} rows`, `Expected table "${this.tableName}" NOT to be empty, but it is`);
|
|
807
|
+
if (actual.length !== 0) throw new Error(`Expected table "${this.tableName}" to be empty, but it has ${actual.length} rows`);
|
|
1034
808
|
}
|
|
1035
809
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
var ValueAssertion = class extends BaseAssertion {
|
|
1043
|
-
actual;
|
|
1044
|
-
label;
|
|
1045
|
-
context;
|
|
1046
|
-
constructor(actual, label, context) {
|
|
1047
|
-
super();
|
|
1048
|
-
this.actual = actual;
|
|
1049
|
-
this.label = label;
|
|
1050
|
-
this.context = context;
|
|
810
|
+
var ResponseAccessor = class {
|
|
811
|
+
body;
|
|
812
|
+
testDir;
|
|
813
|
+
constructor(body, testDir) {
|
|
814
|
+
this.body = body;
|
|
815
|
+
this.testDir = testDir;
|
|
1051
816
|
}
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
if (this.label === "exit code" && this.context?.stdout !== void 0) message = formatExitCodeError(expected, this.actual, this.context.stdout ?? "", this.context.stderr ?? "");
|
|
1056
|
-
else if (this.label === "status" && this.context?.request) message = formatStatusError(expected, this.actual, this.context.request, this.context.responseBody);
|
|
1057
|
-
else message = `Expected ${this.label}: ${expected}\nReceived ${this.label}: ${this.actual}`;
|
|
1058
|
-
this.assert(match, message, `Expected ${this.label} NOT to be ${expected}, but it was`);
|
|
817
|
+
toMatchFile(file) {
|
|
818
|
+
const expected = JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "responses", file), "utf8"));
|
|
819
|
+
if (JSON.stringify(this.body) !== JSON.stringify(expected)) throw new Error(formatResponseDiff(file, expected, this.body));
|
|
1059
820
|
}
|
|
1060
821
|
};
|
|
1061
|
-
//#endregion
|
|
1062
|
-
//#region src/specification/specification.ts
|
|
1063
822
|
var SpecificationResult = class {
|
|
1064
823
|
commandResult;
|
|
1065
824
|
config;
|
|
@@ -1077,32 +836,34 @@ var SpecificationResult = class {
|
|
|
1077
836
|
}
|
|
1078
837
|
get exitCode() {
|
|
1079
838
|
if (!this.commandResult) throw new Error(".exitCode requires a CLI action (.exec())");
|
|
1080
|
-
return
|
|
1081
|
-
stderr: this.commandResult.stderr,
|
|
1082
|
-
stdout: this.commandResult.stdout
|
|
1083
|
-
});
|
|
839
|
+
return this.commandResult.exitCode;
|
|
1084
840
|
}
|
|
1085
841
|
get status() {
|
|
1086
|
-
if (!this.responseData
|
|
1087
|
-
return
|
|
1088
|
-
request: this.requestInfo,
|
|
1089
|
-
responseBody: this.responseData.body
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
|
-
get response() {
|
|
1093
|
-
if (!this.responseData) throw new Error(".response requires an HTTP action (.get(), .post(), etc.)");
|
|
1094
|
-
return new ResponseAssertion(this.responseData.body, this.testDir);
|
|
842
|
+
if (!this.responseData) throw new Error(".status requires an HTTP action (.get(), .post(), etc.)");
|
|
843
|
+
return this.responseData.status;
|
|
1095
844
|
}
|
|
1096
845
|
get stdout() {
|
|
1097
846
|
if (!this.commandResult) throw new Error(".stdout requires a CLI action (.exec())");
|
|
1098
|
-
return
|
|
847
|
+
return this.commandResult.stdout;
|
|
1099
848
|
}
|
|
1100
849
|
get stderr() {
|
|
1101
850
|
if (!this.commandResult) throw new Error(".stderr requires a CLI action (.exec())");
|
|
1102
|
-
return
|
|
851
|
+
return this.commandResult.stderr;
|
|
852
|
+
}
|
|
853
|
+
get response() {
|
|
854
|
+
if (!this.responseData) throw new Error(".response requires an HTTP action (.get(), .post(), etc.)");
|
|
855
|
+
return new ResponseAccessor(this.responseData.body, this.testDir);
|
|
1103
856
|
}
|
|
1104
857
|
file(path) {
|
|
1105
|
-
|
|
858
|
+
const resolvedPath = (0, node_path.resolve)(this.workDir ?? this.testDir, path);
|
|
859
|
+
const exists = (0, node_fs.existsSync)(resolvedPath);
|
|
860
|
+
return {
|
|
861
|
+
get content() {
|
|
862
|
+
if (!exists) throw new Error(`File not found: ${path}`);
|
|
863
|
+
return (0, node_fs.readFileSync)(resolvedPath, "utf8");
|
|
864
|
+
},
|
|
865
|
+
exists
|
|
866
|
+
};
|
|
1106
867
|
}
|
|
1107
868
|
table(tableName, options) {
|
|
1108
869
|
const db = this.resolveDatabase(options?.service);
|
|
@@ -1215,6 +976,7 @@ var SpecificationBuilder = class {
|
|
|
1215
976
|
return this.runCliAction(workDir);
|
|
1216
977
|
}
|
|
1217
978
|
prepareWorkDir() {
|
|
979
|
+
if (!this.projectName && this.fixtures.length === 0) return this.config.fixturesRoot ?? process.cwd();
|
|
1218
980
|
const tempDir = (0, node_fs.mkdtempSync)((0, node_path.resolve)((0, node_os.tmpdir)(), "spec-cli-"));
|
|
1219
981
|
if (this.projectName && this.config.fixturesRoot) {
|
|
1220
982
|
const projectDir = (0, node_path.resolve)(this.config.fixturesRoot, this.projectName);
|
|
@@ -1246,8 +1008,8 @@ var SpecificationBuilder = class {
|
|
|
1246
1008
|
else if (Array.isArray(this.commandArgs)) {
|
|
1247
1009
|
commandResult = {
|
|
1248
1010
|
exitCode: 0,
|
|
1249
|
-
|
|
1250
|
-
|
|
1011
|
+
stderr: "",
|
|
1012
|
+
stdout: ""
|
|
1251
1013
|
};
|
|
1252
1014
|
for (const args of this.commandArgs) {
|
|
1253
1015
|
commandResult = await this.config.command.exec(args, workDir);
|
|
@@ -1276,16 +1038,26 @@ function getCallerDir() {
|
|
|
1276
1038
|
}
|
|
1277
1039
|
throw new Error("Cannot detect caller directory from stack trace");
|
|
1278
1040
|
}
|
|
1279
|
-
/**
|
|
1280
|
-
* Create a specification runner.
|
|
1281
|
-
* Automatically detects the test directory from the call site.
|
|
1282
|
-
*/
|
|
1283
1041
|
function createSpecificationRunner(config) {
|
|
1284
1042
|
return (label) => {
|
|
1285
1043
|
return new SpecificationBuilder(config, getCallerDir(), label);
|
|
1286
1044
|
};
|
|
1287
1045
|
}
|
|
1288
1046
|
//#endregion
|
|
1047
|
+
//#region src/specification/grep.ts
|
|
1048
|
+
/**
|
|
1049
|
+
* Extract text blocks from output that contain a pattern.
|
|
1050
|
+
* Splits by blank lines (how linter/compiler output is structured),
|
|
1051
|
+
* returns only blocks matching the pattern.
|
|
1052
|
+
*
|
|
1053
|
+
* @example
|
|
1054
|
+
* expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
|
|
1055
|
+
* expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
|
|
1056
|
+
*/
|
|
1057
|
+
function grep(output, pattern) {
|
|
1058
|
+
return output.replace(/\x1b\[[0-9;]*m/g, "").split(/\n\s*\n/).filter((block) => block.includes(pattern)).join("\n\n");
|
|
1059
|
+
}
|
|
1060
|
+
//#endregion
|
|
1289
1061
|
//#region src/specification/index.ts
|
|
1290
1062
|
/**
|
|
1291
1063
|
* Resolve root — if relative, resolves from the caller's directory.
|
|
@@ -1410,6 +1182,7 @@ exports.HonoAdapter = HonoAdapter;
|
|
|
1410
1182
|
exports.Orchestrator = Orchestrator;
|
|
1411
1183
|
exports.cli = cli;
|
|
1412
1184
|
exports.e2e = e2e;
|
|
1185
|
+
exports.grep = grep;
|
|
1413
1186
|
exports.integration = integration;
|
|
1414
1187
|
exports.mockOf = mockOf;
|
|
1415
1188
|
exports.mockOfDate = mockOfDate;
|