xtrm-tools 2.1.4 → 2.1.6
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 +10 -4
- package/cli/dist/index.cjs +1562 -1392
- package/cli/dist/index.cjs.map +1 -1
- package/config/hooks.json +4 -4
- package/hooks/README.md +3 -3
- package/hooks/main-guard.mjs +10 -1
- package/package.json +8 -3
- package/project-skills/py-quality-gate/.claude/hooks/quality-check.py +15 -2
- package/project-skills/py-quality-gate/.claude/settings.json +1 -1
- package/project-skills/service-skills-set/.claude/settings.json +2 -2
- package/project-skills/service-skills-set/install-service-skills.py +43 -13
- package/project-skills/tdd-guard/.claude/hooks/tdd-guard-pretool-bridge.cjs +87 -0
- package/project-skills/tdd-guard/.claude/settings.json +2 -2
- package/project-skills/tdd-guard/README.md +6 -4
- package/project-skills/tdd-guard/docs/linting.md +2 -2
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.test-data.ts +199 -0
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.test.ts +302 -0
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.ts +201 -0
- package/project-skills/tdd-guard/reporters/jest/src/index.ts +4 -0
- package/project-skills/tdd-guard/reporters/jest/src/types.ts +42 -0
- package/project-skills/tdd-guard/reporters/jest/tsconfig.json +11 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.test-data.ts +85 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.test.ts +446 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.ts +110 -0
- package/project-skills/tdd-guard/reporters/vitest/src/index.ts +4 -0
- package/project-skills/tdd-guard/reporters/vitest/src/types.ts +39 -0
- package/project-skills/tdd-guard/reporters/vitest/tsconfig.json +11 -0
- package/project-skills/ts-quality-gate/.claude/hooks/quality-check.cjs +36 -1
- package/project-skills/ts-quality-gate/.claude/settings.json +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { JestReporter } from './JestReporter'
|
|
3
|
+
import {
|
|
4
|
+
FileStorage,
|
|
5
|
+
MemoryStorage,
|
|
6
|
+
Config as TDDConfig,
|
|
7
|
+
DEFAULT_DATA_DIR,
|
|
8
|
+
} from 'tdd-guard'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import {
|
|
11
|
+
createTest,
|
|
12
|
+
createTestResult,
|
|
13
|
+
createAggregatedResult,
|
|
14
|
+
createUnhandledError,
|
|
15
|
+
createModuleError,
|
|
16
|
+
createTestResultWithModuleError,
|
|
17
|
+
} from './JestReporter.test-data'
|
|
18
|
+
|
|
19
|
+
describe('JestReporter', () => {
|
|
20
|
+
let sut: ReturnType<typeof setupJestReporter>
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
sut = setupJestReporter()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('constructor', () => {
|
|
27
|
+
it('uses FileStorage by default', () => {
|
|
28
|
+
const reporter = new JestReporter()
|
|
29
|
+
expect(reporter['storage']).toBeInstanceOf(FileStorage)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('accepts Storage instance in reporterOptions', () => {
|
|
33
|
+
const storage = new MemoryStorage()
|
|
34
|
+
const globalConfig = undefined
|
|
35
|
+
const reporterOptions = { storage }
|
|
36
|
+
const reporter = new JestReporter(globalConfig, reporterOptions)
|
|
37
|
+
expect(reporter['storage']).toBe(storage)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('accepts projectRoot string in reporterOptions', () => {
|
|
41
|
+
const rootPath = '/some/project/root'
|
|
42
|
+
const globalConfig = undefined
|
|
43
|
+
const reporterOptions = { projectRoot: rootPath }
|
|
44
|
+
const reporter = new JestReporter(globalConfig, reporterOptions)
|
|
45
|
+
|
|
46
|
+
// Verify the storage is configured with the correct path
|
|
47
|
+
const fileStorage = reporter['storage'] as FileStorage
|
|
48
|
+
const config = fileStorage['config'] as TDDConfig
|
|
49
|
+
const expectedDataDir = path.join(
|
|
50
|
+
rootPath,
|
|
51
|
+
...DEFAULT_DATA_DIR.split('/')
|
|
52
|
+
)
|
|
53
|
+
expect(config.dataDir).toBe(expectedDataDir)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('onTestResult', () => {
|
|
58
|
+
it('collects test results', () => {
|
|
59
|
+
const test = createTest()
|
|
60
|
+
const testResult = createTestResult()
|
|
61
|
+
|
|
62
|
+
sut.reporter.onTestResult(test, testResult)
|
|
63
|
+
|
|
64
|
+
expect(sut.reporter['testModules'].size).toBe(1)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('onRunComplete', () => {
|
|
69
|
+
it('saves test results to storage', async () => {
|
|
70
|
+
const test = createTest()
|
|
71
|
+
const testResult = createTestResult()
|
|
72
|
+
const aggregatedResult = createAggregatedResult()
|
|
73
|
+
|
|
74
|
+
// Collect test results first
|
|
75
|
+
sut.reporter.onTestResult(test, testResult)
|
|
76
|
+
|
|
77
|
+
// Run complete
|
|
78
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
79
|
+
|
|
80
|
+
// Verify results were saved
|
|
81
|
+
const parsed = await sut.getParsedData()
|
|
82
|
+
expect(parsed).toBeTruthy()
|
|
83
|
+
expect(parsed.testModules).toHaveLength(1)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('includes test case details in output', async () => {
|
|
87
|
+
const test = createTest()
|
|
88
|
+
const testResult = createTestResult()
|
|
89
|
+
const aggregatedResult = createAggregatedResult()
|
|
90
|
+
|
|
91
|
+
// Collect test results
|
|
92
|
+
sut.reporter.onTestResult(test, testResult)
|
|
93
|
+
|
|
94
|
+
// Run complete
|
|
95
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
96
|
+
|
|
97
|
+
// Verify test details are included
|
|
98
|
+
const parsed = await sut.getParsedData()
|
|
99
|
+
const module = parsed.testModules[0]
|
|
100
|
+
expect(module.tests).toHaveLength(1)
|
|
101
|
+
expect(module.tests[0].name).toBe('should pass')
|
|
102
|
+
expect(module.tests[0].fullName).toBe('Example Suite should pass')
|
|
103
|
+
expect(module.tests[0].state).toBe('passed')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('includes error details for failed tests', async () => {
|
|
107
|
+
const test = createTest()
|
|
108
|
+
const failedTestResult = createTestResult({ numFailingTests: 1 })
|
|
109
|
+
const aggregatedResult = createAggregatedResult()
|
|
110
|
+
|
|
111
|
+
// Collect test results
|
|
112
|
+
sut.reporter.onTestResult(test, failedTestResult)
|
|
113
|
+
|
|
114
|
+
// Run complete
|
|
115
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
116
|
+
|
|
117
|
+
// Verify error details are included
|
|
118
|
+
const parsed = await sut.getParsedData()
|
|
119
|
+
const module = parsed.testModules[0]
|
|
120
|
+
const failedTest = module.tests[0]
|
|
121
|
+
expect(failedTest.state).toBe('failed')
|
|
122
|
+
expect(failedTest.errors).toBeDefined()
|
|
123
|
+
expect(failedTest.errors).toHaveLength(1)
|
|
124
|
+
expect(failedTest.errors[0].message).toBe('expected 2 to be 3')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('handles empty test runs', async () => {
|
|
128
|
+
// Run complete without any tests
|
|
129
|
+
await sut.reporter.onRunComplete(new Set(), createAggregatedResult())
|
|
130
|
+
|
|
131
|
+
// Verify empty output
|
|
132
|
+
const parsed = await sut.getParsedData()
|
|
133
|
+
expect(parsed.testModules).toEqual([])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('includes unhandled errors in output', async () => {
|
|
137
|
+
const error = createUnhandledError()
|
|
138
|
+
const aggregatedResult = createAggregatedResult({
|
|
139
|
+
runExecError: error,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Run complete with unhandled error
|
|
143
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
144
|
+
|
|
145
|
+
// Verify unhandled errors are included
|
|
146
|
+
const parsed = await sut.getParsedData()
|
|
147
|
+
expect(parsed.unhandledErrors).toBeDefined()
|
|
148
|
+
expect(parsed.unhandledErrors).toHaveLength(1)
|
|
149
|
+
expect(parsed.unhandledErrors[0].message).toBe(
|
|
150
|
+
'Cannot find module "./helpers"'
|
|
151
|
+
)
|
|
152
|
+
expect(parsed.unhandledErrors[0].name).toBe('Error')
|
|
153
|
+
expect(parsed.unhandledErrors[0].stack).toContain('imported from')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('includes test run reason when tests pass', async () => {
|
|
157
|
+
const test = createTest()
|
|
158
|
+
const testResult = createTestResult()
|
|
159
|
+
const aggregatedResult = createAggregatedResult({
|
|
160
|
+
success: true,
|
|
161
|
+
numFailedTests: 0,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
sut.reporter.onTestResult(test, testResult)
|
|
165
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
166
|
+
|
|
167
|
+
const parsed = await sut.getParsedData()
|
|
168
|
+
expect(parsed.reason).toBe('passed')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('handles SerializableError without name property', async () => {
|
|
172
|
+
const aggregatedResult = createAggregatedResult({
|
|
173
|
+
runExecError: {
|
|
174
|
+
message: 'Module not found',
|
|
175
|
+
stack: 'at test.js:1:1',
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
180
|
+
|
|
181
|
+
const parsed = await sut.getParsedData()
|
|
182
|
+
expect(parsed.unhandledErrors[0].message).toBe('Module not found')
|
|
183
|
+
expect(parsed.unhandledErrors[0].name).toBe('Error')
|
|
184
|
+
expect(parsed.unhandledErrors[0].stack).toBe('at test.js:1:1')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('includes module import errors as failed tests', async () => {
|
|
188
|
+
const test = createTest()
|
|
189
|
+
const testResult = createTestResultWithModuleError()
|
|
190
|
+
const aggregatedResult = createAggregatedResult()
|
|
191
|
+
|
|
192
|
+
sut.reporter.onTestResult(test, testResult)
|
|
193
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
194
|
+
|
|
195
|
+
const parsed = await sut.getParsedData()
|
|
196
|
+
const module = parsed.testModules[0]
|
|
197
|
+
expect(module.tests).toHaveLength(1)
|
|
198
|
+
|
|
199
|
+
const importErrorTest = module.tests[0]
|
|
200
|
+
expect(importErrorTest.name).toBe('Module failed to load (Error)')
|
|
201
|
+
expect(importErrorTest.fullName).toBe('Module failed to load (Error)')
|
|
202
|
+
expect(importErrorTest.state).toBe('failed')
|
|
203
|
+
expect(importErrorTest.errors).toHaveLength(1)
|
|
204
|
+
expect(importErrorTest.errors[0].message).toBe(
|
|
205
|
+
"Cannot find module './non-existent-module'"
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('preserves error stack trace from module import errors', async () => {
|
|
210
|
+
const test = createTest()
|
|
211
|
+
const moduleError = createModuleError({
|
|
212
|
+
message: "Cannot find module './helpers'",
|
|
213
|
+
stack:
|
|
214
|
+
"Error: Cannot find module './helpers'\n at Function.Module._resolveFilename",
|
|
215
|
+
name: 'Error',
|
|
216
|
+
})
|
|
217
|
+
const testResult = createTestResultWithModuleError({
|
|
218
|
+
testExecError: moduleError,
|
|
219
|
+
})
|
|
220
|
+
const aggregatedResult = createAggregatedResult()
|
|
221
|
+
|
|
222
|
+
sut.reporter.onTestResult(test, testResult)
|
|
223
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
224
|
+
|
|
225
|
+
const parsed = await sut.getParsedData()
|
|
226
|
+
const importErrorTest = parsed.testModules[0].tests[0]
|
|
227
|
+
|
|
228
|
+
expect(importErrorTest.errors[0].stack).toBe(
|
|
229
|
+
"Error: Cannot find module './helpers'\n at Function.Module._resolveFilename"
|
|
230
|
+
)
|
|
231
|
+
expect(importErrorTest.errors[0].name).toBe('Error')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('uses error type in test name for module import errors', async () => {
|
|
235
|
+
const test = createTest()
|
|
236
|
+
const testResult = createTestResultWithModuleError({
|
|
237
|
+
testExecError: createModuleError({
|
|
238
|
+
message: 'Module parse failed',
|
|
239
|
+
stack: 'SyntaxError: Unexpected token',
|
|
240
|
+
name: 'SyntaxError',
|
|
241
|
+
}),
|
|
242
|
+
})
|
|
243
|
+
const aggregatedResult = createAggregatedResult()
|
|
244
|
+
|
|
245
|
+
sut.reporter.onTestResult(test, testResult)
|
|
246
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
247
|
+
|
|
248
|
+
const parsed = await sut.getParsedData()
|
|
249
|
+
const importErrorTest = parsed.testModules[0].tests[0]
|
|
250
|
+
|
|
251
|
+
expect(importErrorTest.name).toBe('Module failed to load (SyntaxError)')
|
|
252
|
+
expect(importErrorTest.fullName).toBe(
|
|
253
|
+
'Module failed to load (SyntaxError)'
|
|
254
|
+
)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('handles SerializableError with type field for module import errors', async () => {
|
|
258
|
+
const test = createTest()
|
|
259
|
+
const testResult = createTestResultWithModuleError({
|
|
260
|
+
testExecError: createModuleError({
|
|
261
|
+
message: 'Module error',
|
|
262
|
+
stack: 'at test.js:1',
|
|
263
|
+
type: 'ReferenceError',
|
|
264
|
+
code: 'ERR_MODULE_NOT_FOUND',
|
|
265
|
+
}),
|
|
266
|
+
})
|
|
267
|
+
const aggregatedResult = createAggregatedResult()
|
|
268
|
+
|
|
269
|
+
sut.reporter.onTestResult(test, testResult)
|
|
270
|
+
await sut.reporter.onRunComplete(new Set(), aggregatedResult)
|
|
271
|
+
|
|
272
|
+
const parsed = await sut.getParsedData()
|
|
273
|
+
const importErrorTest = parsed.testModules[0].tests[0]
|
|
274
|
+
|
|
275
|
+
expect(importErrorTest.name).toBe(
|
|
276
|
+
'Module failed to load (ReferenceError)'
|
|
277
|
+
)
|
|
278
|
+
expect(importErrorTest.errors[0].name).toBe('ReferenceError')
|
|
279
|
+
expect(importErrorTest.errors[0].operator).toBe('ERR_MODULE_NOT_FOUND')
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Test setup helper function
|
|
285
|
+
function setupJestReporter() {
|
|
286
|
+
const storage = new MemoryStorage()
|
|
287
|
+
const globalConfig = undefined
|
|
288
|
+
const reporterOptions = { storage }
|
|
289
|
+
const reporter = new JestReporter(globalConfig, reporterOptions)
|
|
290
|
+
|
|
291
|
+
// Helper to get parsed test data
|
|
292
|
+
const getParsedData = async () => {
|
|
293
|
+
const savedData = await storage.getTest()
|
|
294
|
+
return savedData ? JSON.parse(savedData) : null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
reporter,
|
|
299
|
+
storage,
|
|
300
|
+
getParsedData,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { BaseReporter, Config } from '@jest/reporters'
|
|
2
|
+
import type { Test, TestResult, AggregatedResult } from '@jest/reporters'
|
|
3
|
+
import type { AssertionResult } from '@jest/test-result'
|
|
4
|
+
import type { TestContext } from '@jest/test-result'
|
|
5
|
+
import { Storage, FileStorage, Config as TDDConfig } from 'tdd-guard'
|
|
6
|
+
import type {
|
|
7
|
+
TDDGuardReporterOptions,
|
|
8
|
+
CapturedError,
|
|
9
|
+
CapturedTest,
|
|
10
|
+
CapturedTestRun,
|
|
11
|
+
CapturedUnhandledError,
|
|
12
|
+
CapturedModule,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
export class JestReporter extends BaseReporter {
|
|
16
|
+
private readonly storage: Storage
|
|
17
|
+
private readonly testModules: Map<
|
|
18
|
+
string,
|
|
19
|
+
{ test: Test; testResult: TestResult }
|
|
20
|
+
> = new Map()
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
_globalConfig?: Config.GlobalConfig,
|
|
24
|
+
reporterOptions?: TDDGuardReporterOptions
|
|
25
|
+
) {
|
|
26
|
+
super()
|
|
27
|
+
this.storage = this.initializeStorage(reporterOptions)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private initializeStorage(options?: TDDGuardReporterOptions): Storage {
|
|
31
|
+
if (options?.storage) {
|
|
32
|
+
return options.storage
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options?.projectRoot) {
|
|
36
|
+
const config = new TDDConfig({ projectRoot: options.projectRoot })
|
|
37
|
+
return new FileStorage(config)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new FileStorage()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override onTestResult(test: Test, testResult: TestResult): void {
|
|
44
|
+
this.testModules.set(test.path, { test, testResult })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override async onRunComplete(
|
|
48
|
+
_contexts: Set<TestContext>,
|
|
49
|
+
results: AggregatedResult
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const output: CapturedTestRun = {
|
|
52
|
+
testModules: this.buildTestModules(),
|
|
53
|
+
unhandledErrors: this.buildUnhandledErrors(results),
|
|
54
|
+
reason: this.determineTestRunReason(results),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await this.storage.saveTest(JSON.stringify(output, null, 2))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private determineTestRunReason(
|
|
61
|
+
results: AggregatedResult
|
|
62
|
+
): 'passed' | 'failed' | 'interrupted' {
|
|
63
|
+
if (results.wasInterrupted) {
|
|
64
|
+
return 'interrupted'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (results.numFailedTestSuites === 0 && results.numTotalTestSuites > 0) {
|
|
68
|
+
return 'passed'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return 'failed'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private mapTestResult(test: AssertionResult): CapturedTest {
|
|
75
|
+
const result: CapturedTest = {
|
|
76
|
+
name: test.title,
|
|
77
|
+
fullName: test.fullName,
|
|
78
|
+
state: test.status,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Process failure details if present
|
|
82
|
+
if (test.failureMessages.length > 0) {
|
|
83
|
+
result.errors = this.processTestErrors(test)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private processTestErrors(test: AssertionResult): CapturedError[] {
|
|
90
|
+
if (test.failureDetails.length === 0) {
|
|
91
|
+
return test.failureMessages.map((message) => ({ message }))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return test.failureDetails.map((detail: unknown, index: number) => {
|
|
95
|
+
const message = test.failureMessages[index] || ''
|
|
96
|
+
const error: CapturedError = { message }
|
|
97
|
+
|
|
98
|
+
if (detail && typeof detail === 'object') {
|
|
99
|
+
this.extractErrorDetails(error, detail as Record<string, unknown>)
|
|
100
|
+
this.parseExpectedActualFromMessage(error, message)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return error
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private extractErrorDetails(
|
|
108
|
+
error: CapturedError,
|
|
109
|
+
obj: Record<string, unknown>
|
|
110
|
+
): void {
|
|
111
|
+
if ('actual' in obj) error.actual = String(obj.actual)
|
|
112
|
+
if ('expected' in obj) error.expected = String(obj.expected)
|
|
113
|
+
if ('showDiff' in obj) error.showDiff = Boolean(obj.showDiff)
|
|
114
|
+
if ('operator' in obj) error.operator = String(obj.operator)
|
|
115
|
+
if ('diff' in obj) error.diff = String(obj.diff)
|
|
116
|
+
if ('name' in obj) error.name = String(obj.name)
|
|
117
|
+
if ('ok' in obj) error.ok = Boolean(obj.ok)
|
|
118
|
+
if ('stack' in obj) error.stack = String(obj.stack)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private parseExpectedActualFromMessage(
|
|
122
|
+
error: CapturedError,
|
|
123
|
+
message: string
|
|
124
|
+
): void {
|
|
125
|
+
if (!error.expected || !error.actual) {
|
|
126
|
+
const expectedMatch = /Expected:\s*(\d+)/.exec(message)
|
|
127
|
+
const receivedMatch = /Received:\s*(\d+)/.exec(message)
|
|
128
|
+
if (expectedMatch && !error.expected) error.expected = expectedMatch[1]
|
|
129
|
+
if (receivedMatch && !error.actual) error.actual = receivedMatch[1]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private createTestFromExecError(execError: unknown): CapturedTest {
|
|
134
|
+
const errorObj = execError as Record<string, unknown>
|
|
135
|
+
const message = String(errorObj.message ?? 'Unknown error')
|
|
136
|
+
|
|
137
|
+
const error: CapturedError = {
|
|
138
|
+
message,
|
|
139
|
+
name: typeof errorObj.name === 'string' ? errorObj.name : 'Error',
|
|
140
|
+
stack: typeof errorObj.stack === 'string' ? errorObj.stack : undefined,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extract additional fields from Jest's SerializableError
|
|
144
|
+
if ('code' in errorObj && errorObj.code !== undefined) {
|
|
145
|
+
error.operator = String(errorObj.code)
|
|
146
|
+
}
|
|
147
|
+
if ('type' in errorObj && typeof errorObj.type === 'string') {
|
|
148
|
+
error.name = errorObj.type
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const errorType = error.name ?? 'Error'
|
|
152
|
+
const testName = `Module failed to load (${errorType})`
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
name: testName,
|
|
156
|
+
fullName: testName,
|
|
157
|
+
state: 'failed',
|
|
158
|
+
errors: [error],
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private buildTestModules(): CapturedModule[] {
|
|
163
|
+
return Array.from(this.testModules.entries()).map(([path, data]) => {
|
|
164
|
+
const { testResult } = data
|
|
165
|
+
|
|
166
|
+
// Handle module/import errors
|
|
167
|
+
if (testResult.testExecError && testResult.testResults.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
moduleId: path,
|
|
170
|
+
tests: [this.createTestFromExecError(testResult.testExecError)],
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
moduleId: path,
|
|
176
|
+
tests: testResult.testResults.map((test: AssertionResult) =>
|
|
177
|
+
this.mapTestResult(test)
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private buildUnhandledErrors(
|
|
184
|
+
results: AggregatedResult
|
|
185
|
+
): CapturedUnhandledError[] {
|
|
186
|
+
if (!results.runExecError) {
|
|
187
|
+
return []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const error = results.runExecError
|
|
191
|
+
const errorObj = error as Record<string, unknown>
|
|
192
|
+
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
message: String(errorObj.message ?? 'Unknown error'),
|
|
196
|
+
name: typeof errorObj.name === 'string' ? errorObj.name : 'Error',
|
|
197
|
+
stack: typeof errorObj.stack === 'string' ? errorObj.stack : undefined,
|
|
198
|
+
},
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Storage } from 'tdd-guard'
|
|
2
|
+
|
|
3
|
+
export interface TDDGuardReporterOptions {
|
|
4
|
+
storage?: Storage
|
|
5
|
+
projectRoot?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CapturedError {
|
|
9
|
+
message: string
|
|
10
|
+
actual?: string
|
|
11
|
+
expected?: string
|
|
12
|
+
showDiff?: boolean
|
|
13
|
+
operator?: string
|
|
14
|
+
diff?: string
|
|
15
|
+
name?: string
|
|
16
|
+
ok?: boolean
|
|
17
|
+
stack?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CapturedTest {
|
|
21
|
+
name: string
|
|
22
|
+
fullName: string
|
|
23
|
+
state: string
|
|
24
|
+
errors?: CapturedError[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CapturedModule {
|
|
28
|
+
moduleId: string
|
|
29
|
+
tests: CapturedTest[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CapturedUnhandledError {
|
|
33
|
+
message: string
|
|
34
|
+
name: string
|
|
35
|
+
stack?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CapturedTestRun {
|
|
39
|
+
testModules: CapturedModule[]
|
|
40
|
+
unhandledErrors?: CapturedUnhandledError[]
|
|
41
|
+
reason?: 'passed' | 'failed' | 'interrupted'
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*"],
|
|
10
|
+
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { TestModule, TestCase, TestResult } from 'vitest/node'
|
|
2
|
+
import type { SerializedError } from '@vitest/utils'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MODULE_ID = '/test/example.test.ts'
|
|
5
|
+
const DEFAULT_TEST_NAME = 'should pass'
|
|
6
|
+
const DEFAULT_TEST_FULL_NAME = 'Example Suite > should pass'
|
|
7
|
+
|
|
8
|
+
// Helper to create a valid TestResult for a given state
|
|
9
|
+
export function createTestResult(state: TestResult['state']): TestResult {
|
|
10
|
+
switch (state) {
|
|
11
|
+
case 'failed':
|
|
12
|
+
return { state: 'failed', errors: [] }
|
|
13
|
+
case 'passed':
|
|
14
|
+
return { state: 'passed', errors: undefined }
|
|
15
|
+
case 'skipped':
|
|
16
|
+
return { state: 'skipped', errors: undefined, note: undefined }
|
|
17
|
+
case 'pending':
|
|
18
|
+
return { state: 'pending', errors: undefined }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Creates a minimal TestModule mock for testing
|
|
23
|
+
function createTestModule(props: {
|
|
24
|
+
moduleId: string
|
|
25
|
+
errors?: () => SerializedError[]
|
|
26
|
+
}): TestModule {
|
|
27
|
+
return {
|
|
28
|
+
moduleId: props.moduleId,
|
|
29
|
+
errors: props.errors ?? ((): SerializedError[] => []),
|
|
30
|
+
} as TestModule
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function testModule(overrides?: {
|
|
34
|
+
moduleId?: string
|
|
35
|
+
errors?: () => SerializedError[]
|
|
36
|
+
}): TestModule {
|
|
37
|
+
return createTestModule({
|
|
38
|
+
moduleId: overrides?.moduleId ?? DEFAULT_MODULE_ID,
|
|
39
|
+
errors: overrides?.errors,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createTestCase(overrides?: Partial<TestCase>): TestCase {
|
|
44
|
+
const defaultModule = createTestModule({ moduleId: DEFAULT_MODULE_ID })
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name: DEFAULT_TEST_NAME,
|
|
48
|
+
fullName: DEFAULT_TEST_FULL_NAME,
|
|
49
|
+
module: defaultModule,
|
|
50
|
+
result: () => ({ state: 'passed', errors: [] }) as TestResult,
|
|
51
|
+
...overrides,
|
|
52
|
+
} as TestCase
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function failedTestCase(overrides?: Partial<TestCase>): TestCase {
|
|
56
|
+
return createTestCase({
|
|
57
|
+
name: 'should fail',
|
|
58
|
+
fullName: 'Example Suite > should fail',
|
|
59
|
+
result: () =>
|
|
60
|
+
({
|
|
61
|
+
state: 'failed',
|
|
62
|
+
errors: [
|
|
63
|
+
{
|
|
64
|
+
message: 'expected 2 to be 3',
|
|
65
|
+
stack: 'Error: expected 2 to be 3\n at test.ts:7:19',
|
|
66
|
+
expected: '3',
|
|
67
|
+
actual: '2',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}) as TestResult,
|
|
71
|
+
...overrides,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createUnhandledError(
|
|
76
|
+
overrides: Partial<SerializedError> = {}
|
|
77
|
+
): SerializedError {
|
|
78
|
+
return {
|
|
79
|
+
name: 'Error',
|
|
80
|
+
message: 'Cannot find module "./helpers"',
|
|
81
|
+
stack:
|
|
82
|
+
"Error: Cannot find module './helpers' imported from '/src/example.test.ts'",
|
|
83
|
+
...overrides,
|
|
84
|
+
}
|
|
85
|
+
}
|