xtrm-tools 2.1.5 → 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.
@@ -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,4 @@
1
+ import { JestReporter } from './JestReporter'
2
+
3
+ export { JestReporter }
4
+ export default JestReporter
@@ -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
+ }