@lssm/lib.testing 0.0.0-canary-20251217062943 → 0.0.0-canary-20251217072406
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/adapters/jest-adapter.js +25 -11
- package/dist/adapters/vitest-adapter.js +23 -11
- package/dist/generator/assertion-builder.js +15 -2
- package/dist/generator/golden-test-generator.js +86 -1
- package/dist/index.js +7 -1
- package/dist/recorder/traffic-recorder.js +59 -1
- package/package.json +4 -4
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import{serialize
|
|
2
|
-
expect(result).toEqual(${e(n.expectedOutput??null)});`,o=`await expect(${t.runnerFunction}(input${n.id}, metadata${n.id})).rejects.toMatchObject(${e(n.expectedError??{message:`expected failure`})});`;return`
|
|
3
|
-
test('${n.name}', async () => {
|
|
4
|
-
const input${n.id} = ${r};
|
|
5
|
-
const metadata${n.id} = ${i};
|
|
6
|
-
${n.success?a:o}
|
|
7
|
-
});`}).join(`
|
|
8
|
-
`);return`
|
|
9
|
-
import { ${t.runnerFunction} } from '${t.runnerImport}';
|
|
1
|
+
import { serialize } from "../generator/assertion-builder.js";
|
|
10
2
|
|
|
11
|
-
|
|
3
|
+
//#region src/adapters/jest-adapter.ts
|
|
4
|
+
function generateJestSuite(options) {
|
|
5
|
+
const caseBlocks = options.cases.map((testCase) => {
|
|
6
|
+
const inputConst = serialize(testCase.input);
|
|
7
|
+
const metadataConst = serialize(testCase.metadata ?? {});
|
|
8
|
+
const successBlock = `const result = await ${options.runnerFunction}(input${testCase.id}, metadata${testCase.id});
|
|
9
|
+
expect(result).toEqual(${serialize(testCase.expectedOutput ?? null)});`;
|
|
10
|
+
const failureBlock = `await expect(${options.runnerFunction}(input${testCase.id}, metadata${testCase.id})).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`;
|
|
11
|
+
return `
|
|
12
|
+
test('${testCase.name}', async () => {
|
|
13
|
+
const input${testCase.id} = ${inputConst};
|
|
14
|
+
const metadata${testCase.id} = ${metadataConst};
|
|
15
|
+
${testCase.success ? successBlock : failureBlock}
|
|
16
|
+
});`;
|
|
17
|
+
}).join("\n");
|
|
18
|
+
return `
|
|
19
|
+
import { ${options.runnerFunction} } from '${options.runnerImport}';
|
|
20
|
+
|
|
21
|
+
describe('${options.suiteName}', () => {${caseBlocks}
|
|
12
22
|
});
|
|
13
|
-
`.trim()
|
|
23
|
+
`.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { generateJestSuite };
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import{serialize
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
`);
|
|
1
|
+
import { serialize } from "../generator/assertion-builder.js";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/vitest-adapter.ts
|
|
4
|
+
function generateVitestSuite(options) {
|
|
5
|
+
const caseBlocks = options.cases.map((testCase) => {
|
|
6
|
+
const inputConst = serialize(testCase.input);
|
|
7
|
+
const metadataConst = serialize(testCase.metadata ?? {});
|
|
8
|
+
const assertions = testCase.success ? [`const result = await ${options.runnerFunction}(input${testCase.id}, metadata${testCase.id});`, `expect(result).toEqual(${serialize(testCase.expectedOutput ?? null)});`] : [`await expect(${options.runnerFunction}(input${testCase.id}, metadata${testCase.id})).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`];
|
|
9
|
+
return `
|
|
10
|
+
it('${testCase.name}', async () => {
|
|
11
|
+
const input${testCase.id} = ${inputConst};
|
|
12
|
+
const metadata${testCase.id} = ${metadataConst};
|
|
13
|
+
${assertions.join("\n ")}
|
|
14
|
+
});`;
|
|
15
|
+
}).join("\n");
|
|
16
|
+
return `
|
|
9
17
|
import { describe, it, expect } from 'bun:test';
|
|
10
|
-
import { ${
|
|
18
|
+
import { ${options.runnerFunction} } from '${options.runnerImport}';
|
|
11
19
|
|
|
12
|
-
describe('${
|
|
20
|
+
describe('${options.suiteName}', () => {${caseBlocks}
|
|
13
21
|
});
|
|
14
|
-
`.trim()
|
|
22
|
+
`.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { generateVitestSuite };
|
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
//#region src/generator/assertion-builder.ts
|
|
2
|
+
function buildAssertions(testCase, ctx) {
|
|
3
|
+
if (testCase.success) return [`const result = await ${ctx.runnerCall};`, `expect(result).toEqual(${serialize(testCase.expectedOutput ?? null)});`].join("\n ");
|
|
4
|
+
return `await expect(${ctx.runnerCall}).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`;
|
|
5
|
+
}
|
|
6
|
+
function serialize(value) {
|
|
7
|
+
return JSON.stringify(value, (_key, val) => {
|
|
8
|
+
if (val instanceof Date) return val.toISOString();
|
|
9
|
+
if (typeof val === "undefined") return null;
|
|
10
|
+
return val;
|
|
11
|
+
}, 2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { buildAssertions, serialize };
|
|
@@ -1 +1,86 @@
|
|
|
1
|
-
import{generateVitestSuite
|
|
1
|
+
import { generateVitestSuite } from "../adapters/vitest-adapter.js";
|
|
2
|
+
import { generateJestSuite } from "../adapters/jest-adapter.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
5
|
+
|
|
6
|
+
//#region src/generator/golden-test-generator.ts
|
|
7
|
+
var GoldenTestGenerator = class {
|
|
8
|
+
constructor(serializeMetadata = (snapshot) => ({
|
|
9
|
+
tenantId: snapshot.tenantId,
|
|
10
|
+
userId: snapshot.userId,
|
|
11
|
+
channel: snapshot.channel
|
|
12
|
+
})) {
|
|
13
|
+
this.serializeMetadata = serializeMetadata;
|
|
14
|
+
}
|
|
15
|
+
createCases(snapshots) {
|
|
16
|
+
return snapshots.map((snapshot, index) => ({
|
|
17
|
+
id: snapshot.id ?? randomUUID(),
|
|
18
|
+
name: snapshot.success ? `case-${index + 1}-success` : `case-${index + 1}-failure`,
|
|
19
|
+
input: snapshot.input,
|
|
20
|
+
expectedOutput: snapshot.output,
|
|
21
|
+
expectedError: snapshot.error,
|
|
22
|
+
success: snapshot.success,
|
|
23
|
+
metadata: this.serializeMetadata?.(snapshot)
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
generate(snapshots, options) {
|
|
27
|
+
const cases = this.createCases(snapshots);
|
|
28
|
+
if (options.framework === "jest") return generateJestSuite({
|
|
29
|
+
suiteName: options.suiteName,
|
|
30
|
+
cases,
|
|
31
|
+
runnerImport: options.runnerImport,
|
|
32
|
+
runnerFunction: options.runnerFunction
|
|
33
|
+
});
|
|
34
|
+
return generateVitestSuite({
|
|
35
|
+
suiteName: options.suiteName,
|
|
36
|
+
cases,
|
|
37
|
+
runnerImport: options.runnerImport,
|
|
38
|
+
runnerFunction: options.runnerFunction
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
async function runGoldenTests(cases, runner) {
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const testCase of cases) {
|
|
45
|
+
const startedAt = performance.now();
|
|
46
|
+
try {
|
|
47
|
+
const output = await runner(testCase.input, testCase.metadata);
|
|
48
|
+
if (!testCase.success) {
|
|
49
|
+
results.push({
|
|
50
|
+
caseId: testCase.id,
|
|
51
|
+
passed: false,
|
|
52
|
+
durationMs: performance.now() - startedAt,
|
|
53
|
+
error: /* @__PURE__ */ new Error("Expected failure but runner resolved")
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const matches = JSON.stringify(output) === JSON.stringify(testCase.expectedOutput ?? null);
|
|
58
|
+
results.push({
|
|
59
|
+
caseId: testCase.id,
|
|
60
|
+
passed: matches,
|
|
61
|
+
durationMs: performance.now() - startedAt,
|
|
62
|
+
error: matches ? void 0 : {
|
|
63
|
+
expected: testCase.expectedOutput,
|
|
64
|
+
received: output
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const durationMs = performance.now() - startedAt;
|
|
69
|
+
if (!testCase.success) results.push({
|
|
70
|
+
caseId: testCase.id,
|
|
71
|
+
passed: true,
|
|
72
|
+
durationMs
|
|
73
|
+
});
|
|
74
|
+
else results.push({
|
|
75
|
+
caseId: testCase.id,
|
|
76
|
+
passed: false,
|
|
77
|
+
durationMs,
|
|
78
|
+
error
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
export { GoldenTestGenerator, runGoldenTests };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
import{InMemoryTrafficStore
|
|
1
|
+
import { InMemoryTrafficStore, TrafficRecorder } from "./recorder/traffic-recorder.js";
|
|
2
|
+
import { buildAssertions, serialize } from "./generator/assertion-builder.js";
|
|
3
|
+
import { generateVitestSuite } from "./adapters/vitest-adapter.js";
|
|
4
|
+
import { generateJestSuite } from "./adapters/jest-adapter.js";
|
|
5
|
+
import { GoldenTestGenerator, runGoldenTests } from "./generator/golden-test-generator.js";
|
|
6
|
+
|
|
7
|
+
export { GoldenTestGenerator, InMemoryTrafficStore, TrafficRecorder, buildAssertions, generateJestSuite, generateVitestSuite, runGoldenTests, serialize };
|
|
@@ -1 +1,59 @@
|
|
|
1
|
-
import{randomUUID
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
//#region src/recorder/traffic-recorder.ts
|
|
4
|
+
var InMemoryTrafficStore = class {
|
|
5
|
+
items = [];
|
|
6
|
+
async save(snapshot) {
|
|
7
|
+
this.items.push(snapshot);
|
|
8
|
+
}
|
|
9
|
+
async list(operation) {
|
|
10
|
+
if (!operation) return [...this.items];
|
|
11
|
+
return this.items.filter((item) => item.operation.name === operation);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var TrafficRecorder = class {
|
|
15
|
+
store;
|
|
16
|
+
sampleRate;
|
|
17
|
+
sanitize;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.store = options.store;
|
|
20
|
+
this.sampleRate = options.sampleRate ?? 1;
|
|
21
|
+
this.sanitize = options.sanitize;
|
|
22
|
+
}
|
|
23
|
+
async record(input) {
|
|
24
|
+
if (!this.shouldSample()) return;
|
|
25
|
+
const snapshot = {
|
|
26
|
+
id: randomUUID(),
|
|
27
|
+
operation: input.operation,
|
|
28
|
+
input: structuredCloneSafe(input.input),
|
|
29
|
+
output: structuredCloneSafe(input.output),
|
|
30
|
+
error: input.error ? structuredCloneSafe(input.error) : void 0,
|
|
31
|
+
success: input.success,
|
|
32
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
33
|
+
durationMs: input.durationMs,
|
|
34
|
+
tenantId: input.tenantId,
|
|
35
|
+
userId: input.userId,
|
|
36
|
+
channel: input.channel,
|
|
37
|
+
metadata: input.metadata
|
|
38
|
+
};
|
|
39
|
+
const sanitized = this.sanitize ? this.sanitize(snapshot) : snapshot;
|
|
40
|
+
await this.store.save(sanitized);
|
|
41
|
+
}
|
|
42
|
+
shouldSample() {
|
|
43
|
+
if (this.sampleRate >= 1) return true;
|
|
44
|
+
return Math.random() <= this.sampleRate;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function structuredCloneSafe(value) {
|
|
48
|
+
if (value == null) return value ?? void 0;
|
|
49
|
+
try {
|
|
50
|
+
const clone = globalThis.structuredClone;
|
|
51
|
+
if (typeof clone === "function") return clone(value);
|
|
52
|
+
return JSON.parse(JSON.stringify(value));
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { InMemoryTrafficStore, TrafficRecorder };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lssm/lib.testing",
|
|
3
|
-
"version": "0.0.0-canary-
|
|
3
|
+
"version": "0.0.0-canary-20251217072406",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"test": "bun run"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@lssm/lib.contracts": "0.0.0-canary-
|
|
26
|
+
"@lssm/lib.contracts": "0.0.0-canary-20251217072406"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@lssm/tool.tsdown": "0.0.0-canary-
|
|
30
|
-
"@lssm/tool.typescript": "0.0.0-canary-
|
|
29
|
+
"@lssm/tool.tsdown": "0.0.0-canary-20251217072406",
|
|
30
|
+
"@lssm/tool.typescript": "0.0.0-canary-20251217072406",
|
|
31
31
|
"tsdown": "^0.17.4",
|
|
32
32
|
"typescript": "^5.9.3"
|
|
33
33
|
},
|