@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/dist/index.js CHANGED
@@ -220,20 +220,6 @@ function formatStartupReport(mode, services, app) {
220
220
  lines.push("");
221
221
  return lines.join("\n");
222
222
  }
223
- function formatStatusError(expectedStatus, receivedStatus, request, responseBody) {
224
- const lines = [];
225
- lines.push(`Expected status: ${GREEN}${expectedStatus}${RESET}`);
226
- lines.push(`Received status: ${RED}${receivedStatus}${RESET}`);
227
- lines.push("");
228
- lines.push(`${DIM}${request.method} ${request.path}${RESET}`);
229
- if (request.body) lines.push(formatJson(request.body, DIM));
230
- if (responseBody) {
231
- lines.push("");
232
- lines.push(`${DIM}Response:${RESET}`);
233
- lines.push(formatJson(responseBody, RED));
234
- }
235
- return lines.join("\n");
236
- }
237
223
  function formatTableDiff(table, columns, expected, actual) {
238
224
  const lines = [];
239
225
  lines.push(`Table "${table}" mismatch`);
@@ -282,66 +268,9 @@ function formatResponseDiff(file, expected, actual) {
282
268
  }
283
269
  return lines.join("\n");
284
270
  }
285
- function formatExitCodeError(expected, received, stdout, stderr) {
286
- const lines = [];
287
- lines.push(`Expected exit code: ${GREEN}${expected}${RESET}`);
288
- lines.push(`Received exit code: ${RED}${received}${RESET}`);
289
- if (stdout.trim()) {
290
- lines.push("");
291
- lines.push(`${DIM}stdout:${RESET}`);
292
- for (const line of stdout.trim().split("\n").slice(-15)) lines.push(` ${DIM}${line}${RESET}`);
293
- }
294
- if (stderr.trim()) {
295
- lines.push("");
296
- lines.push(`${DIM}stderr:${RESET}`);
297
- for (const line of stderr.trim().split("\n").slice(-15)) lines.push(` ${RED}${line}${RESET}`);
298
- }
299
- return lines.join("\n");
300
- }
301
- function formatStdoutDiff(file, expected, actual) {
302
- const lines = [];
303
- lines.push(`Output mismatch (${file})`);
304
- lines.push("");
305
- lines.push(`${GREEN}- Expected${RESET}`);
306
- lines.push(`${RED}+ Received${RESET}`);
307
- lines.push("");
308
- const expectedLines = expected.split("\n");
309
- const actualLines = actual.split("\n");
310
- const maxLines = Math.max(expectedLines.length, actualLines.length);
311
- for (let i = 0; i < maxLines; i++) {
312
- const exp = expectedLines[i];
313
- const act = actualLines[i];
314
- if (exp === act) lines.push(` ${exp}`);
315
- else {
316
- if (exp !== void 0) lines.push(`${GREEN}- ${exp}${RESET}`);
317
- if (act !== void 0) lines.push(`${RED}+ ${act}${RESET}`);
318
- }
319
- }
320
- return lines.join("\n");
321
- }
322
- function formatFileMissing(path) {
323
- return `Expected file to exist: ${RED}${path}${RESET}`;
324
- }
325
- function formatFileUnexpected(path) {
326
- return `Expected file NOT to exist: ${RED}${path}${RESET}`;
327
- }
328
- function formatFileContentMismatch(path, expected, actual) {
329
- const lines = [];
330
- lines.push(`File "${path}" does not contain expected content`);
331
- lines.push("");
332
- lines.push(`${GREEN}Expected to contain:${RESET}`);
333
- lines.push(` ${GREEN}${expected}${RESET}`);
334
- lines.push("");
335
- lines.push(`${RED}Actual content (first 20 lines):${RESET}`);
336
- for (const line of actual.split("\n").slice(0, 20)) lines.push(` ${DIM}${line}${RESET}`);
337
- return lines.join("\n");
338
- }
339
271
  function rowLabel(n) {
340
272
  return n === 1 ? "1 row" : `${n} rows`;
341
273
  }
342
- function formatJson(value, color) {
343
- return JSON.stringify(value, null, 2).split("\n").map((line) => `${color}${line}${RESET}`).join("\n");
344
- }
345
274
  function formatRow(row) {
346
275
  return row.map((v) => String(v ?? "null")).join(" | ");
347
276
  }
@@ -837,205 +766,35 @@ var HonoAdapter = class {
837
766
  }
838
767
  };
839
768
  //#endregion
840
- //#region src/specification/assertions/base.ts
841
- /**
842
- * Base assertion that handles .not negation.
843
- * Subclasses call this.assert(condition, message, negatedMessage) for each predicate.
844
- */
845
- var BaseAssertion = class {
846
- negated = false;
847
- get not() {
848
- const clone = Object.create(Object.getPrototypeOf(this));
849
- Object.assign(clone, this);
850
- clone.negated = !this.negated;
851
- return clone;
852
- }
853
- assert(condition, message, negatedMessage) {
854
- if (this.negated) {
855
- if (condition) throw new Error(negatedMessage);
856
- } else if (!condition) throw new Error(message);
857
- }
858
- };
859
- //#endregion
860
- //#region src/specification/assertions/file.ts
861
- /**
862
- * Assertions on a file in the working directory.
863
- * Usage: result.file("dist/index.js").toExist()
864
- */
865
- var FileAssertion = class extends BaseAssertion {
866
- filePath;
867
- resolvedPath;
868
- constructor(filePath, workDir) {
869
- super();
870
- this.filePath = filePath;
871
- this.resolvedPath = resolve(workDir, filePath);
872
- }
873
- toExist() {
874
- const exists = existsSync(this.resolvedPath);
875
- this.assert(exists, formatFileMissing(this.filePath), formatFileUnexpected(this.filePath));
876
- }
877
- toContain(expected) {
878
- if (!existsSync(this.resolvedPath)) {
879
- if (this.negated) return;
880
- throw new Error(formatFileMissing(this.filePath));
881
- }
882
- const content = readFileSync(this.resolvedPath, "utf8");
883
- const found = content.includes(expected);
884
- this.assert(found, formatFileContentMismatch(this.filePath, expected, content), `Expected file "${this.filePath}" NOT to contain "${expected}"`);
885
- }
886
- toMatch(pattern) {
887
- if (!existsSync(this.resolvedPath)) {
888
- if (this.negated) return;
889
- throw new Error(formatFileMissing(this.filePath));
890
- }
891
- const content = readFileSync(this.resolvedPath, "utf8");
892
- const found = pattern.test(content);
893
- 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}`);
894
- }
895
- };
896
- //#endregion
897
- //#region src/specification/assertions/response.ts
898
- /**
899
- * Assertions on an HTTP response body.
900
- * Usage: result.response.toMatchFile("expected.json")
901
- */
902
- var ResponseAssertion = class extends BaseAssertion {
903
- body;
904
- testDir;
905
- constructor(body, testDir) {
906
- super();
907
- this.body = body;
908
- this.testDir = testDir;
909
- }
910
- toMatchFile(file) {
911
- const expected = JSON.parse(readFileSync(resolve(this.testDir, "responses", file), "utf8"));
912
- const match = JSON.stringify(this.body) === JSON.stringify(expected);
913
- this.assert(match, formatResponseDiff(file, expected, this.body), `Expected response NOT to match file "${file}", but it did`);
914
- }
915
- toContain(subset) {
916
- const bodyStr = JSON.stringify(this.body);
917
- const subsetStr = JSON.stringify(subset);
918
- const bodyObj = typeof this.body === "object" && this.body !== null ? this.body : {};
919
- const match = Object.entries(subset).every(([key, value]) => JSON.stringify(bodyObj[key]) === JSON.stringify(value));
920
- this.assert(match, `Expected response to contain: ${subsetStr}\n\nActual response:\n${bodyStr}`, `Expected response NOT to contain: ${subsetStr}`);
921
- }
922
- };
923
- //#endregion
924
- //#region src/specification/assertions/string.ts
925
- /**
926
- * Assertions on a string (stdout, stderr, response body).
927
- * Usage: result.stdout.toContain("hello")
928
- */
929
- var StringAssertion = class extends BaseAssertion {
930
- actual;
931
- label;
932
- testDir;
933
- constructor(actual, label, testDir) {
934
- super();
935
- this.actual = actual;
936
- this.label = label;
937
- this.testDir = testDir;
938
- }
939
- toContain(expected, options) {
940
- if (options?.near) {
941
- const found = this.containsNear(expected, options.near);
942
- 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`);
943
- } else {
944
- const found = this.actual.includes(expected);
945
- 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}"`);
946
- }
947
- }
948
- toMatch(pattern) {
949
- const found = pattern.test(this.actual);
950
- 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}`);
951
- }
952
- toMatchFile(file) {
953
- if (!this.testDir) throw new Error("toMatchFile requires a test directory context");
954
- const expected = readFileSync(resolve(this.testDir, "expected", file), "utf8").trim();
955
- const actual = this.actual.trim();
956
- const match = actual === expected;
957
- this.assert(match, formatStdoutDiff(file, expected, actual), `Expected ${this.label} NOT to match file "${file}", but it did`);
958
- }
959
- toBeEmpty() {
960
- const empty = this.actual.trim() === "";
961
- 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`);
962
- }
963
- containsNear(target, near, proximity = 500) {
964
- const clean = this.stripAnsi(this.actual);
965
- const nearLower = near.toLowerCase();
966
- const targetLower = target.toLowerCase();
967
- let searchFrom = 0;
968
- while (true) {
969
- const idx = clean.toLowerCase().indexOf(nearLower, searchFrom);
970
- if (idx === -1) break;
971
- const windowStart = Math.max(0, idx - proximity);
972
- const windowEnd = Math.min(clean.length, idx + nearLower.length + proximity);
973
- if (clean.substring(windowStart, windowEnd).toLowerCase().includes(targetLower)) return true;
974
- searchFrom = idx + 1;
975
- }
976
- return false;
977
- }
978
- stripAnsi(str) {
979
- return str.replace(/\x1b\[[0-9;]*m/g, "");
980
- }
981
- truncate(str, maxLines = 20) {
982
- const lines = str.split("\n");
983
- if (lines.length <= maxLines) return str;
984
- return `${lines.slice(0, maxLines).join("\n")}\n... (${lines.length - maxLines} more lines)`;
985
- }
986
- };
987
- //#endregion
988
- //#region src/specification/assertions/table.ts
989
- /**
990
- * Assertions on a database table.
991
- * Usage: await result.table("users").toMatch({ columns: ["name"], rows: [["Alice"]] })
992
- */
993
- var TableAssertion = class extends BaseAssertion {
769
+ //#region src/specification/specification.ts
770
+ var TableAssertion = class {
994
771
  tableName;
995
772
  db;
996
773
  constructor(tableName, db) {
997
- super();
998
774
  this.tableName = tableName;
999
775
  this.db = db;
1000
776
  }
1001
777
  async toMatch(expected) {
1002
778
  const actual = await this.db.query(this.tableName, expected.columns);
1003
- const match = JSON.stringify(actual) === JSON.stringify(expected.rows);
1004
- this.assert(match, formatTableDiff(this.tableName, expected.columns, expected.rows, actual), `Expected table "${this.tableName}" NOT to match, but it did`);
779
+ if (JSON.stringify(actual) !== JSON.stringify(expected.rows)) throw new Error(formatTableDiff(this.tableName, expected.columns, expected.rows, actual));
1005
780
  }
1006
781
  async toBeEmpty() {
1007
782
  const actual = await this.db.query(this.tableName, ["*"]);
1008
- const empty = actual.length === 0;
1009
- 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`);
783
+ if (actual.length !== 0) throw new Error(`Expected table "${this.tableName}" to be empty, but it has ${actual.length} rows`);
1010
784
  }
1011
785
  };
1012
- //#endregion
1013
- //#region src/specification/assertions/value.ts
1014
- /**
1015
- * Assertions on a single value (exit code, status code).
1016
- * Usage: result.exitCode.toBe(0)
1017
- */
1018
- var ValueAssertion = class extends BaseAssertion {
1019
- actual;
1020
- label;
1021
- context;
1022
- constructor(actual, label, context) {
1023
- super();
1024
- this.actual = actual;
1025
- this.label = label;
1026
- this.context = context;
786
+ var ResponseAccessor = class {
787
+ body;
788
+ testDir;
789
+ constructor(body, testDir) {
790
+ this.body = body;
791
+ this.testDir = testDir;
1027
792
  }
1028
- toBe(expected) {
1029
- const match = this.actual === expected;
1030
- let message;
1031
- if (this.label === "exit code" && this.context?.stdout !== void 0) message = formatExitCodeError(expected, this.actual, this.context.stdout ?? "", this.context.stderr ?? "");
1032
- else if (this.label === "status" && this.context?.request) message = formatStatusError(expected, this.actual, this.context.request, this.context.responseBody);
1033
- else message = `Expected ${this.label}: ${expected}\nReceived ${this.label}: ${this.actual}`;
1034
- this.assert(match, message, `Expected ${this.label} NOT to be ${expected}, but it was`);
793
+ toMatchFile(file) {
794
+ const expected = JSON.parse(readFileSync(resolve(this.testDir, "responses", file), "utf8"));
795
+ if (JSON.stringify(this.body) !== JSON.stringify(expected)) throw new Error(formatResponseDiff(file, expected, this.body));
1035
796
  }
1036
797
  };
1037
- //#endregion
1038
- //#region src/specification/specification.ts
1039
798
  var SpecificationResult = class {
1040
799
  commandResult;
1041
800
  config;
@@ -1053,32 +812,34 @@ var SpecificationResult = class {
1053
812
  }
1054
813
  get exitCode() {
1055
814
  if (!this.commandResult) throw new Error(".exitCode requires a CLI action (.exec())");
1056
- return new ValueAssertion(this.commandResult.exitCode, "exit code", {
1057
- stderr: this.commandResult.stderr,
1058
- stdout: this.commandResult.stdout
1059
- });
815
+ return this.commandResult.exitCode;
1060
816
  }
1061
817
  get status() {
1062
- if (!this.responseData || !this.requestInfo) throw new Error(".status requires an HTTP action (.get(), .post(), etc.)");
1063
- return new ValueAssertion(this.responseData.status, "status", {
1064
- request: this.requestInfo,
1065
- responseBody: this.responseData.body
1066
- });
1067
- }
1068
- get response() {
1069
- if (!this.responseData) throw new Error(".response requires an HTTP action (.get(), .post(), etc.)");
1070
- return new ResponseAssertion(this.responseData.body, this.testDir);
818
+ if (!this.responseData) throw new Error(".status requires an HTTP action (.get(), .post(), etc.)");
819
+ return this.responseData.status;
1071
820
  }
1072
821
  get stdout() {
1073
822
  if (!this.commandResult) throw new Error(".stdout requires a CLI action (.exec())");
1074
- return new StringAssertion(this.commandResult.stdout, "stdout", this.testDir);
823
+ return this.commandResult.stdout;
1075
824
  }
1076
825
  get stderr() {
1077
826
  if (!this.commandResult) throw new Error(".stderr requires a CLI action (.exec())");
1078
- return new StringAssertion(this.commandResult.stderr, "stderr", this.testDir);
827
+ return this.commandResult.stderr;
828
+ }
829
+ get response() {
830
+ if (!this.responseData) throw new Error(".response requires an HTTP action (.get(), .post(), etc.)");
831
+ return new ResponseAccessor(this.responseData.body, this.testDir);
1079
832
  }
1080
833
  file(path) {
1081
- return new FileAssertion(path, this.workDir ?? this.testDir);
834
+ const resolvedPath = resolve(this.workDir ?? this.testDir, path);
835
+ const exists = existsSync(resolvedPath);
836
+ return {
837
+ get content() {
838
+ if (!exists) throw new Error(`File not found: ${path}`);
839
+ return readFileSync(resolvedPath, "utf8");
840
+ },
841
+ exists
842
+ };
1082
843
  }
1083
844
  table(tableName, options) {
1084
845
  const db = this.resolveDatabase(options?.service);
@@ -1191,6 +952,7 @@ var SpecificationBuilder = class {
1191
952
  return this.runCliAction(workDir);
1192
953
  }
1193
954
  prepareWorkDir() {
955
+ if (!this.projectName && this.fixtures.length === 0) return this.config.fixturesRoot ?? process.cwd();
1194
956
  const tempDir = mkdtempSync(resolve(tmpdir(), "spec-cli-"));
1195
957
  if (this.projectName && this.config.fixturesRoot) {
1196
958
  const projectDir = resolve(this.config.fixturesRoot, this.projectName);
@@ -1222,8 +984,8 @@ var SpecificationBuilder = class {
1222
984
  else if (Array.isArray(this.commandArgs)) {
1223
985
  commandResult = {
1224
986
  exitCode: 0,
1225
- stdout: "",
1226
- stderr: ""
987
+ stderr: "",
988
+ stdout: ""
1227
989
  };
1228
990
  for (const args of this.commandArgs) {
1229
991
  commandResult = await this.config.command.exec(args, workDir);
@@ -1252,16 +1014,26 @@ function getCallerDir() {
1252
1014
  }
1253
1015
  throw new Error("Cannot detect caller directory from stack trace");
1254
1016
  }
1255
- /**
1256
- * Create a specification runner.
1257
- * Automatically detects the test directory from the call site.
1258
- */
1259
1017
  function createSpecificationRunner(config) {
1260
1018
  return (label) => {
1261
1019
  return new SpecificationBuilder(config, getCallerDir(), label);
1262
1020
  };
1263
1021
  }
1264
1022
  //#endregion
1023
+ //#region src/specification/grep.ts
1024
+ /**
1025
+ * Extract text blocks from output that contain a pattern.
1026
+ * Splits by blank lines (how linter/compiler output is structured),
1027
+ * returns only blocks matching the pattern.
1028
+ *
1029
+ * @example
1030
+ * expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
1031
+ * expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
1032
+ */
1033
+ function grep(output, pattern) {
1034
+ return output.replace(/\x1b\[[0-9;]*m/g, "").split(/\n\s*\n/).filter((block) => block.includes(pattern)).join("\n\n");
1035
+ }
1036
+ //#endregion
1265
1037
  //#region src/specification/index.ts
1266
1038
  /**
1267
1039
  * Resolve root — if relative, resolves from the caller's directory.
@@ -1380,6 +1152,6 @@ async function cli(options) {
1380
1152
  return runner;
1381
1153
  }
1382
1154
  //#endregion
1383
- export { ExecAdapter, FetchAdapter, HonoAdapter, Orchestrator, cli, e2e, integration, mockOf, mockOfDate, normalizeOutput, postgres, redis, stripAnsi };
1155
+ export { ExecAdapter, FetchAdapter, HonoAdapter, Orchestrator, cli, e2e, grep, integration, mockOf, mockOfDate, normalizeOutput, postgres, redis, stripAnsi };
1384
1156
 
1385
1157
  //# sourceMappingURL=index.js.map