Skip to content

Commit 3f58e46

Browse files
Merge pull request #24 from ysfaran/feat/previous-attempts
feat: add previous attempts to Ctrf test results
2 parents e244543 + 9efcffb commit 3f58e46

File tree

4 files changed

+204
-0
lines changed

4 files changed

+204
-0
lines changed

src/generate-report.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
type CtrfTest,
1717
type CtrfEnvironment,
1818
type CtrfAttachment,
19+
type CtrfTestAttempt,
1920
} from '../types/ctrf'
2021

2122
interface ReporterConfigOptions {
@@ -222,6 +223,22 @@ class GenerateCtrfReport implements Reporter {
222223
if (this.reporterConfigOptions.annotations !== undefined) {
223224
test.extra = { annotations: testCase.annotations }
224225
}
226+
227+
if (testCase.results.length > 1) {
228+
const retryResults = testCase.results.slice(0, -1)
229+
test.retryAttempts = []
230+
231+
for (const retryResult of retryResults) {
232+
const retryAttempt: CtrfTestAttempt = {
233+
status: this.mapPlaywrightStatusToCtrf(retryResult.status),
234+
duration: retryResult.duration,
235+
message: this.extractFailureDetails(retryResult).message,
236+
trace: this.extractFailureDetails(retryResult).trace,
237+
snippet: this.extractFailureDetails(retryResult).snippet,
238+
}
239+
test.retryAttempts.push(retryAttempt)
240+
}
241+
}
225242
}
226243

227244
ctrfReport.results.tests.push(test)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import {
2+
type Suite,
3+
type TestCase,
4+
type Location,
5+
type TestResult,
6+
type TestError,
7+
} from '@playwright/test/reporter'
8+
9+
/**
10+
* Creates a minimal Suite object with a single flaky test
11+
* with 2 failed attempts and one passed attempt
12+
*/
13+
export const createFlakyTestSuite = (): Suite => {
14+
const testError: TestError = {
15+
message: 'test-error-message',
16+
stack: 'test-error-stack',
17+
snippet: 'test-error-snippet',
18+
}
19+
20+
const failedTestResult: TestResult = {
21+
retry: 0,
22+
duration: 4444,
23+
status: 'failed',
24+
startTime: new Date('2023-01-01T00:00:00.000Z'),
25+
parallelIndex: 0,
26+
workerIndex: 0,
27+
attachments: [],
28+
errors: [testError],
29+
error: testError,
30+
steps: [],
31+
stdout: [],
32+
stderr: [],
33+
}
34+
35+
const testError2: TestError = {
36+
message: 'test-error-message2',
37+
stack: 'test-error-stack2',
38+
snippet: 'test-error-snippet2',
39+
}
40+
41+
const failedTestResult2: TestResult = {
42+
retry: 1,
43+
duration: 5555,
44+
status: 'failed',
45+
startTime: new Date('2023-01-01T00:00:00.000Z'),
46+
parallelIndex: 0,
47+
workerIndex: 0,
48+
attachments: [],
49+
errors: [testError2],
50+
error: testError2,
51+
steps: [],
52+
stdout: [],
53+
stderr: [],
54+
}
55+
56+
const passedTestResult: TestResult = {
57+
retry: 2,
58+
duration: 888,
59+
status: 'passed',
60+
startTime: new Date('2023-01-01T00:00:05.200Z'),
61+
parallelIndex: 0,
62+
workerIndex: 0,
63+
attachments: [],
64+
errors: [],
65+
steps: [],
66+
stdout: [],
67+
stderr: [],
68+
}
69+
70+
const testCase: TestCase = {
71+
title: 'should validate the expected condition',
72+
id: 'test-id-123',
73+
annotations: [],
74+
expectedStatus: 'passed',
75+
timeout: 30000,
76+
results: [failedTestResult, failedTestResult2, passedTestResult],
77+
location: {
78+
file: 'flaky-test.spec.ts',
79+
line: 42,
80+
column: 3,
81+
},
82+
parent: undefined as any, // Will be set later
83+
outcome: () => 'flaky',
84+
ok: () => true,
85+
titlePath: () => ['Flaky Test Suite', 'should be flaky'],
86+
repeatEachIndex: 0,
87+
retries: 1,
88+
}
89+
90+
const suite: Suite = {
91+
title: 'Flaky Test Suite',
92+
titlePath: () => ['Flaky Test Suite'],
93+
location: {
94+
file: 'flaky-test.spec.ts',
95+
line: 10,
96+
column: 1,
97+
} as Location,
98+
project: () => ({
99+
name: 'Test Project',
100+
outputDir: './test-results',
101+
grep: /.*/,
102+
grepInvert: null,
103+
metadata: {},
104+
dependencies: [],
105+
repeatEach: 1,
106+
retries: 3,
107+
timeout: 30000,
108+
use: {},
109+
testDir: './tests',
110+
testIgnore: [],
111+
testMatch: [],
112+
snapshotDir: './snapshots',
113+
}),
114+
allTests: () => [testCase],
115+
tests: [testCase],
116+
suites: [],
117+
}
118+
119+
testCase.parent = suite
120+
121+
return suite
122+
}

tests/flaky-tests.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createFlakyTestSuite } from './dummy-suites/flaky-test-suite'
2+
import GenerateCtrfReport from '../src/generate-report'
3+
import fs from 'fs'
4+
import { CtrfReport } from '../types/ctrf'
5+
6+
jest.mock('fs', () => ({
7+
writeFileSync: jest.fn(),
8+
existsSync: jest.fn(() => true),
9+
}))
10+
const nowDateMock = new Date('2023-01-01T00:00:00.000Z')
11+
jest.useFakeTimers().setSystemTime(nowDateMock)
12+
13+
const mockedFs = fs as jest.Mocked<typeof fs>
14+
15+
describe('Flaky Tests', () => {
16+
it('should generate report with retry attempts correctly', async () => {
17+
// Arrange
18+
const testSuite = createFlakyTestSuite()
19+
const report = new GenerateCtrfReport()
20+
21+
// Act
22+
report.onBegin(undefined as any, testSuite)
23+
report.onEnd()
24+
25+
// Assert
26+
expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(1)
27+
28+
const reportJsonContent = mockedFs.writeFileSync.mock.calls[0][1] as string
29+
const parsedReport: CtrfReport = JSON.parse(reportJsonContent)
30+
31+
expect(parsedReport.results.tests).toHaveLength(1)
32+
33+
const test = parsedReport.results.tests[0]
34+
35+
expect(test.status).toBe('passed')
36+
expect(test.retries).toBe(2)
37+
expect(test.flaky).toBe(true)
38+
expect(test.duration).toBe(888)
39+
40+
expect(test.retryAttempts).toHaveLength(2)
41+
42+
const failedAttempt = test.retryAttempts![0]
43+
expect(failedAttempt.status).toBe('failed')
44+
expect(failedAttempt.duration).toBe(4444)
45+
expect(failedAttempt.message).toBe('test-error-message')
46+
expect(failedAttempt.trace).toBe('test-error-stack')
47+
expect(failedAttempt.snippet).toBe('test-error-snippet')
48+
49+
const failedAttempt2 = test.retryAttempts![1]
50+
expect(failedAttempt2.status).toBe('failed')
51+
expect(failedAttempt2.duration).toBe(5555)
52+
expect(failedAttempt2.message).toBe('test-error-message2')
53+
expect(failedAttempt2.trace).toBe('test-error-stack2')
54+
expect(failedAttempt2.snippet).toBe('test-error-snippet2')
55+
})
56+
})

types/ctrf.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,18 @@ export interface CtrfTest {
4747
screenshot?: string
4848
parameters?: Record<string, any>
4949
steps?: Step[]
50+
retryAttempts?: CtrfTestAttempt[]
5051
extra?: Record<string, any>
5152
}
5253

54+
export interface CtrfTestAttempt {
55+
status: CtrfTestState
56+
duration: number
57+
message?: string
58+
trace?: string
59+
snippet?: string
60+
}
61+
5362
export interface CtrfEnvironment {
5463
appName?: string
5564
appVersion?: string

0 commit comments

Comments
 (0)