@jterrazz/test 3.5.0 → 4.0.1
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 +39 -33
- package/dist/index.cjs +228 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -16
- package/dist/index.d.ts +102 -16
- package/dist/index.js +228 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -110,6 +110,99 @@ declare class Orchestrator {
|
|
|
110
110
|
getAppUrl(): null | string;
|
|
111
111
|
}
|
|
112
112
|
//#endregion
|
|
113
|
+
//#region src/specification/assertions/base.d.ts
|
|
114
|
+
/**
|
|
115
|
+
* Base assertion that handles .not negation.
|
|
116
|
+
* Subclasses call this.assert(condition, message, negatedMessage) for each predicate.
|
|
117
|
+
*/
|
|
118
|
+
declare class BaseAssertion {
|
|
119
|
+
protected negated: boolean;
|
|
120
|
+
get not(): this;
|
|
121
|
+
protected assert(condition: boolean, message: string, negatedMessage: string): void;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/specification/assertions/file.d.ts
|
|
125
|
+
/**
|
|
126
|
+
* Assertions on a file in the working directory.
|
|
127
|
+
* Usage: result.file("dist/index.js").toExist()
|
|
128
|
+
*/
|
|
129
|
+
declare class FileAssertion extends BaseAssertion {
|
|
130
|
+
private filePath;
|
|
131
|
+
private resolvedPath;
|
|
132
|
+
constructor(filePath: string, workDir: string);
|
|
133
|
+
toExist(): void;
|
|
134
|
+
toContain(expected: string): void;
|
|
135
|
+
toMatch(pattern: RegExp): void;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/specification/assertions/response.d.ts
|
|
139
|
+
/**
|
|
140
|
+
* Assertions on an HTTP response body.
|
|
141
|
+
* Usage: result.response.toMatchFile("expected.json")
|
|
142
|
+
*/
|
|
143
|
+
declare class ResponseAssertion extends BaseAssertion {
|
|
144
|
+
private body;
|
|
145
|
+
private testDir;
|
|
146
|
+
constructor(body: unknown, testDir: string);
|
|
147
|
+
toMatchFile(file: string): void;
|
|
148
|
+
toContain(subset: Record<string, unknown>): void;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/specification/assertions/string.d.ts
|
|
152
|
+
/**
|
|
153
|
+
* Assertions on a string (stdout, stderr, response body).
|
|
154
|
+
* Usage: result.stdout.toContain("hello")
|
|
155
|
+
*/
|
|
156
|
+
declare class StringAssertion extends BaseAssertion {
|
|
157
|
+
private actual;
|
|
158
|
+
private label;
|
|
159
|
+
private testDir?;
|
|
160
|
+
constructor(actual: string, label: string, testDir?: string);
|
|
161
|
+
toContain(expected: string, options?: {
|
|
162
|
+
near?: string;
|
|
163
|
+
}): void;
|
|
164
|
+
toMatch(pattern: RegExp): void;
|
|
165
|
+
toMatchFile(file: string): void;
|
|
166
|
+
toBeEmpty(): void;
|
|
167
|
+
private containsNear;
|
|
168
|
+
private stripAnsi;
|
|
169
|
+
private truncate;
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/specification/assertions/table.d.ts
|
|
173
|
+
/**
|
|
174
|
+
* Assertions on a database table.
|
|
175
|
+
* Usage: await result.table("users").toMatch({ columns: ["name"], rows: [["Alice"]] })
|
|
176
|
+
*/
|
|
177
|
+
declare class TableAssertion extends BaseAssertion {
|
|
178
|
+
private tableName;
|
|
179
|
+
private db;
|
|
180
|
+
constructor(tableName: string, db: DatabasePort);
|
|
181
|
+
toMatch(expected: {
|
|
182
|
+
columns: string[];
|
|
183
|
+
rows: unknown[][];
|
|
184
|
+
}): Promise<void>;
|
|
185
|
+
toBeEmpty(): Promise<void>;
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/specification/assertions/value.d.ts
|
|
189
|
+
/**
|
|
190
|
+
* Assertions on a single value (exit code, status code).
|
|
191
|
+
* Usage: result.exitCode.toBe(0)
|
|
192
|
+
*/
|
|
193
|
+
declare class ValueAssertion extends BaseAssertion {
|
|
194
|
+
private actual;
|
|
195
|
+
private label;
|
|
196
|
+
private context?;
|
|
197
|
+
constructor(actual: number, label: string, context?: {
|
|
198
|
+
request?: any;
|
|
199
|
+
responseBody?: unknown;
|
|
200
|
+
stdout?: string;
|
|
201
|
+
stderr?: string;
|
|
202
|
+
});
|
|
203
|
+
toBe(expected: number): void;
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
113
206
|
//#region src/specification/ports/command.port.d.ts
|
|
114
207
|
/**
|
|
115
208
|
* Result of executing a CLI command.
|
|
@@ -174,7 +267,7 @@ declare class SpecificationResult {
|
|
|
174
267
|
private commandResult?;
|
|
175
268
|
private config;
|
|
176
269
|
private requestInfo?;
|
|
177
|
-
private
|
|
270
|
+
private responseData?;
|
|
178
271
|
private testDir;
|
|
179
272
|
private workDir?;
|
|
180
273
|
constructor(options: {
|
|
@@ -185,23 +278,16 @@ declare class SpecificationResult {
|
|
|
185
278
|
testDir: string;
|
|
186
279
|
workDir?: string;
|
|
187
280
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expectTable(table: string, options: {
|
|
196
|
-
columns: string[];
|
|
197
|
-
rows: unknown[][];
|
|
281
|
+
get exitCode(): ValueAssertion;
|
|
282
|
+
get status(): ValueAssertion;
|
|
283
|
+
get response(): ResponseAssertion;
|
|
284
|
+
get stdout(): StringAssertion;
|
|
285
|
+
get stderr(): StringAssertion;
|
|
286
|
+
file(path: string): FileAssertion;
|
|
287
|
+
table(tableName: string, options?: {
|
|
198
288
|
service?: string;
|
|
199
|
-
}):
|
|
200
|
-
expectFile(path: string): this;
|
|
201
|
-
expectNoFile(path: string): this;
|
|
202
|
-
expectFileContains(path: string, content: string): this;
|
|
289
|
+
}): TableAssertion;
|
|
203
290
|
private resolveDatabase;
|
|
204
|
-
private resolveWorkPath;
|
|
205
291
|
}
|
|
206
292
|
declare class SpecificationBuilder {
|
|
207
293
|
private commandArgs;
|
package/dist/index.js
CHANGED
|
@@ -837,92 +837,258 @@ var HonoAdapter = class {
|
|
|
837
837
|
}
|
|
838
838
|
};
|
|
839
839
|
//#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 {
|
|
994
|
+
tableName;
|
|
995
|
+
db;
|
|
996
|
+
constructor(tableName, db) {
|
|
997
|
+
super();
|
|
998
|
+
this.tableName = tableName;
|
|
999
|
+
this.db = db;
|
|
1000
|
+
}
|
|
1001
|
+
async toMatch(expected) {
|
|
1002
|
+
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`);
|
|
1005
|
+
}
|
|
1006
|
+
async toBeEmpty() {
|
|
1007
|
+
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`);
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
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;
|
|
1027
|
+
}
|
|
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`);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
//#endregion
|
|
840
1038
|
//#region src/specification/specification.ts
|
|
841
1039
|
var SpecificationResult = class {
|
|
842
1040
|
commandResult;
|
|
843
1041
|
config;
|
|
844
1042
|
requestInfo;
|
|
845
|
-
|
|
1043
|
+
responseData;
|
|
846
1044
|
testDir;
|
|
847
1045
|
workDir;
|
|
848
1046
|
constructor(options) {
|
|
849
|
-
this.
|
|
1047
|
+
this.responseData = options.response;
|
|
850
1048
|
this.commandResult = options.commandResult;
|
|
851
1049
|
this.config = options.config;
|
|
852
1050
|
this.testDir = options.testDir;
|
|
853
1051
|
this.requestInfo = options.requestInfo;
|
|
854
1052
|
this.workDir = options.workDir;
|
|
855
1053
|
}
|
|
856
|
-
|
|
857
|
-
if (!this.
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (!this.response) throw new Error("expectResponse requires an HTTP action (.get(), .post(), etc.)");
|
|
863
|
-
const expected = JSON.parse(readFileSync(resolve(this.testDir, "responses", file), "utf8"));
|
|
864
|
-
if (JSON.stringify(this.response.body) !== JSON.stringify(expected)) throw new Error(formatResponseDiff(file, expected, this.response.body));
|
|
865
|
-
return this;
|
|
866
|
-
}
|
|
867
|
-
expectExitCode(code) {
|
|
868
|
-
if (!this.commandResult) throw new Error("expectExitCode requires a CLI action (.exec())");
|
|
869
|
-
if (this.commandResult.exitCode !== code) throw new Error(formatExitCodeError(code, this.commandResult.exitCode, this.commandResult.stdout, this.commandResult.stderr));
|
|
870
|
-
return this;
|
|
871
|
-
}
|
|
872
|
-
expectStdout(file) {
|
|
873
|
-
if (!this.commandResult) throw new Error("expectStdout requires a CLI action (.exec())");
|
|
874
|
-
const expected = readFileSync(resolve(this.testDir, "expected", file), "utf8").trim();
|
|
875
|
-
const actual = this.commandResult.stdout.trim();
|
|
876
|
-
if (actual !== expected) throw new Error(formatStdoutDiff(file, expected, actual));
|
|
877
|
-
return this;
|
|
878
|
-
}
|
|
879
|
-
expectStdoutContains(str) {
|
|
880
|
-
if (!this.commandResult) throw new Error("expectStdoutContains requires a CLI action (.exec())");
|
|
881
|
-
if (!this.commandResult.stdout.includes(str)) throw new Error(`Expected stdout to contain: "${str}"\n\nActual stdout:\n${this.commandResult.stdout}`);
|
|
882
|
-
return this;
|
|
1054
|
+
get exitCode() {
|
|
1055
|
+
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
|
+
});
|
|
883
1060
|
}
|
|
884
|
-
|
|
885
|
-
if (!this.
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1061
|
+
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
|
+
});
|
|
890
1067
|
}
|
|
891
|
-
|
|
892
|
-
if (!this.
|
|
893
|
-
|
|
894
|
-
return this;
|
|
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);
|
|
895
1071
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const actual = await db.query(table, options.columns);
|
|
900
|
-
if (JSON.stringify(actual) !== JSON.stringify(options.rows)) throw new Error(formatTableDiff(table, options.columns, options.rows, actual));
|
|
901
|
-
return this;
|
|
1072
|
+
get stdout() {
|
|
1073
|
+
if (!this.commandResult) throw new Error(".stdout requires a CLI action (.exec())");
|
|
1074
|
+
return new StringAssertion(this.commandResult.stdout, "stdout", this.testDir);
|
|
902
1075
|
}
|
|
903
|
-
|
|
904
|
-
if (!
|
|
905
|
-
return this;
|
|
1076
|
+
get stderr() {
|
|
1077
|
+
if (!this.commandResult) throw new Error(".stderr requires a CLI action (.exec())");
|
|
1078
|
+
return new StringAssertion(this.commandResult.stderr, "stderr", this.testDir);
|
|
906
1079
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
return this;
|
|
1080
|
+
file(path) {
|
|
1081
|
+
return new FileAssertion(path, this.workDir ?? this.testDir);
|
|
910
1082
|
}
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
if (!
|
|
914
|
-
|
|
915
|
-
if (!actual.includes(content)) throw new Error(formatFileContentMismatch(path, content, actual));
|
|
916
|
-
return this;
|
|
1083
|
+
table(tableName, options) {
|
|
1084
|
+
const db = this.resolveDatabase(options?.service);
|
|
1085
|
+
if (!db) throw new Error(options?.service ? `table("${tableName}") requires database "${options.service}" but it was not found` : `table("${tableName}") requires a database adapter`);
|
|
1086
|
+
return new TableAssertion(tableName, db);
|
|
917
1087
|
}
|
|
918
1088
|
resolveDatabase(serviceName) {
|
|
919
1089
|
if (serviceName && this.config.databases) return this.config.databases.get(serviceName);
|
|
920
1090
|
return this.config.database;
|
|
921
1091
|
}
|
|
922
|
-
resolveWorkPath(path) {
|
|
923
|
-
if (this.workDir) return resolve(this.workDir, path);
|
|
924
|
-
return resolve(this.testDir, path);
|
|
925
|
-
}
|
|
926
1092
|
};
|
|
927
1093
|
var SpecificationBuilder = class {
|
|
928
1094
|
commandArgs = null;
|
|
@@ -1025,6 +1191,7 @@ var SpecificationBuilder = class {
|
|
|
1025
1191
|
return this.runCliAction(workDir);
|
|
1026
1192
|
}
|
|
1027
1193
|
prepareWorkDir() {
|
|
1194
|
+
if (!this.projectName && this.fixtures.length === 0) return this.config.fixturesRoot ?? process.cwd();
|
|
1028
1195
|
const tempDir = mkdtempSync(resolve(tmpdir(), "spec-cli-"));
|
|
1029
1196
|
if (this.projectName && this.config.fixturesRoot) {
|
|
1030
1197
|
const projectDir = resolve(this.config.fixturesRoot, this.projectName);
|