@flakiness/vitest 0.1.0-alpha.2 → 1.0.0

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 CHANGED
@@ -11,7 +11,7 @@ A custom Vitest reporter that generates Flakiness Reports from your Vitest test
11
11
  - [Viewing Reports](#viewing-reports)
12
12
  - [Features](#features)
13
13
  - [Test Location Tracking](#test-location-tracking)
14
- - [Duplicate Test Detection](#duplicate-test-detection)
14
+ - [Handling Test Duplicates](#handling-test-duplicates)
15
15
  - [Environment Detection](#environment-detection)
16
16
  - [CI Integration](#ci-integration)
17
17
  - [Configuration Options](#configuration-options)
@@ -19,6 +19,7 @@ A custom Vitest reporter that generates Flakiness Reports from your Vitest test
19
19
  - [`endpoint?: string`](#endpoint-string)
20
20
  - [`token?: string`](#token-string)
21
21
  - [`outputFolder?: string`](#outputfolder-string)
22
+ - [`duplicates?: 'fail' | 'rename'`](#duplicates-fail--rename)
22
23
  - [`open?: 'always' | 'never' | 'on-failure'`](#open-always--never--on-failure)
23
24
  - [`disableUpload?: boolean`](#disableupload-boolean)
24
25
  - [Environment Variables](#environment-variables)
@@ -47,13 +48,18 @@ export default defineConfig({
47
48
  test: {
48
49
  includeTaskLocation: true,
49
50
  reporters: [
50
- ['@flakiness/vitest']
51
+ 'default',
52
+ ['@flakiness/vitest', { flakinessProject: 'my-org/my-project' }]
51
53
  ],
52
54
  },
53
55
  });
54
56
  ```
55
57
 
56
- > **Note:** Setting `includeTaskLocation: true` is recommended to enable test locations the report.
58
+ > [!NOTE]
59
+ > The flakiness reporter should be added alongside your other reporters. Include `'default'` (or another built-in reporter) to retain the standard terminal output, since Vitest only uses the reporters listed in the `reporters` array.
60
+
61
+ > [!TIP]
62
+ > Setting `includeTaskLocation: true` is recommended to enable test locations in the report.
57
63
 
58
64
  Run your tests. The report will be automatically generated in the `./flakiness-report` folder:
59
65
 
@@ -92,16 +98,30 @@ npx flakiness show ./flakiness-report
92
98
 
93
99
  When `includeTaskLocation: true` is set in your Vitest config, the reporter records the exact file, line, and column for each test. This enables precise navigation from the Flakiness.io dashboard back to your source code.
94
100
 
95
- ### Duplicate Test Detection
101
+ ### Handling Test Duplicates
96
102
 
97
- Vitest allows multiple tests to share the same name:
103
+ Vitest allows creating tests with identical names:
98
104
 
99
105
  ```typescript
106
+ // Trivial duplicates
100
107
  it('should work', () => { /* ... */ });
101
108
  it('should work', () => { /* ... */ });
109
+
110
+ // More common way to end up with test duplicates:
111
+ it.for([
112
+ { input: 1, expected: 2 },
113
+ { input: 1, expected: 3 },
114
+ ])('should handle $input', ({ input, expected }) => { /* ... */ });
102
115
  ```
103
116
 
104
- Flakiness.io relies on full test names (all parent suite names + test name) to construct test history, so each test must have a unique full name. The reporter detects duplicates, issues warnings, and marks them as failed with a descriptive error message. Rename your tests to have unique full names to resolve this.
117
+ Flakiness.io, however, **does not allow** test duplicates.
118
+
119
+ Flakiness.io considers two tests to be *duplicates* when they:
120
+ 1. Share the same parent suite hierarchy
121
+ 2. Have the same test name
122
+ 3. And run in the same Vitest project
123
+
124
+ Flakiness.io relies on full test names to construct test history, so each test must have a unique full name. When Vitest reporter detects tests with identical full names, it issues warnings and handles them according to the [`duplicates`](#duplicates-fail--rename) option.
105
125
 
106
126
  ### Environment Detection
107
127
 
@@ -183,6 +203,23 @@ reporters: [
183
203
  ]
184
204
  ```
185
205
 
206
+ ### `duplicates?: 'fail' | 'rename'`
207
+
208
+ Controls how the reporter handles tests with duplicate full names. Defaults to `'fail'`.
209
+
210
+ > [!WARNING]
211
+ > The `'rename'` mode is **not recommended** for regular use. There is no guarantee that test histories will remain stable for duplicate tests, since the renaming is based on internal Vitest identifiers that may change between runs. This mode exists to help evaluate the reporter against large Vitest projects that have not yet resolved their duplicate test names.
212
+
213
+ - **`'fail'`** (default): Duplicate tests are marked as failed with a descriptive error message and a `dupe` annotation. The first duplicate gets a single failed attempt explaining the problem; the remaining duplicates are stripped of their attempts (effectively hidden from the report). This is the recommended mode — it surfaces the problem so you can fix it by renaming your tests.
214
+ - **`'rename'`**: Duplicate tests are automatically renamed by appending a suffix (e.g., ` – dupe #2`, ` – dupe #3`) to their titles. Each renamed test also receives a `dupe` annotation on all its attempts. The first test with a given name keeps its original title; only the subsequent duplicates are renamed.
215
+
216
+ ```typescript
217
+ reporters: [
218
+ ['@flakiness/vitest', { duplicates: 'rename' }]
219
+ ]
220
+ ```
221
+
222
+
186
223
  ### `open?: 'always' | 'never' | 'on-failure'`
187
224
 
188
225
  Controls when the report viewer should automatically open in your browser after test completion.
@@ -227,11 +264,13 @@ export default defineConfig({
227
264
  test: {
228
265
  includeTaskLocation: true,
229
266
  reporters: [
267
+ 'default',
230
268
  ['@flakiness/vitest', {
231
269
  flakinessProject: 'my-org/my-project',
232
270
  endpoint: process.env.FLAKINESS_ENDPOINT,
233
271
  token: process.env.FLAKINESS_ACCESS_TOKEN,
234
272
  outputFolder: './flakiness-report',
273
+ duplicates: 'fail',
235
274
  open: 'on-failure',
236
275
  disableUpload: false,
237
276
  }]
package/lib/reporter.js CHANGED
@@ -9,6 +9,7 @@ class FKVitestReporter {
9
9
  this._options = _options;
10
10
  }
11
11
  _impl;
12
+ _vitest;
12
13
  _logger = {
13
14
  warn: (txt) => console.warn(chalk.yellow(txt)),
14
15
  error: (txt) => console.error(chalk.red(txt)),
@@ -17,58 +18,47 @@ class FKVitestReporter {
17
18
  setLoggerForTest(logger) {
18
19
  this._logger = logger;
19
20
  }
20
- onUserConsoleLog(log) {
21
- this._impl?.onUserConsoleLog(log);
21
+ async onUserConsoleLog(log) {
22
+ await this._impl?.onUserConsoleLog(log);
22
23
  }
23
24
  onInit(vitest) {
24
- this._impl = ReporterImpl.create(vitest.config.root, this._options, this._logger);
25
- this._impl?.onInit(vitest);
26
- }
27
- /**
28
- * Called when annotation is added via the `task.annotate` API.
29
- */
30
- onTestCaseAnnotate(testCase, annotation) {
31
- this._impl?.onTestCaseAnnotate(testCase, annotation);
25
+ this._vitest = vitest;
32
26
  }
33
27
  onTestRunStart() {
34
- this._impl?.onTestRunStart();
35
- }
36
- async onTestCaseResult(testCase) {
37
- this._impl?.onTestCaseResult(testCase);
28
+ assert(this._vitest, "onInit must be called before onTestRunStart");
29
+ this._impl = ReporterImpl.create(this._vitest.config.root, this._options, this._logger, this._vitest.config.config);
38
30
  }
39
31
  async onTestRunEnd(testModules, unhandledErrors, reason) {
40
- this._impl?.onTestRunEnd(testModules, unhandledErrors, reason);
32
+ await this._impl?.onTestRunEnd(testModules, unhandledErrors, reason);
33
+ this._impl = void 0;
41
34
  }
42
35
  }
36
+ const gTestToTestCaseId = /* @__PURE__ */ new WeakMap();
43
37
  class ReporterImpl {
44
- constructor(_worktree, _commitId, _options, _logger) {
38
+ constructor(_worktree, _commitId, _options, _logger, _configPath) {
45
39
  this._worktree = _worktree;
46
40
  this._commitId = _commitId;
47
41
  this._options = _options;
48
42
  this._logger = _logger;
43
+ this._configPath = _configPath;
49
44
  this._sampleSystem = this._sampleSystem.bind(this);
45
+ this._sampleSystem();
50
46
  }
51
47
  _telemetryTimer;
52
48
  _cpuUtilization = new CPUUtilization({ precision: 10 });
53
49
  _ramUtilization = new RAMUtilization({ precision: 10 });
54
50
  _startTimestamp = Date.now();
55
- _testCaseIdToTest = /* @__PURE__ */ new Map();
56
- _testToTestCaseId = /* @__PURE__ */ new Map();
57
51
  _stdio = /* @__PURE__ */ new Map();
58
- _annotations = /* @__PURE__ */ new Map();
59
52
  // In Vitest, all projects MUST HAVE UNIQUE NAMES.
60
53
  // So we create environments per project name.
61
54
  _environments = [];
62
- _allSuites = /* @__PURE__ */ new Map();
63
- _fileSuites = /* @__PURE__ */ new Map();
64
- _configPath;
65
- static create(rootPath, options, logger) {
55
+ static create(rootPath, options, logger, config) {
66
56
  let commitId;
67
57
  let worktree;
68
58
  try {
69
59
  worktree = GitWorktree.create(rootPath);
70
60
  commitId = worktree.headCommitId();
71
- return new ReporterImpl(worktree, commitId, options, logger);
61
+ return new ReporterImpl(worktree, commitId, options, logger, config);
72
62
  } catch (e) {
73
63
  logger.warn(`[flakiness.io] Failed to fetch commit info - is this a git repo?`);
74
64
  logger.error(`[flakiness.io] Report is NOT generated.`);
@@ -80,9 +70,6 @@ class ReporterImpl {
80
70
  this._ramUtilization.sample();
81
71
  this._telemetryTimer = setTimeout(this._sampleSystem, 1e3);
82
72
  }
83
- onInit(vitest) {
84
- this._configPath = vitest.config.config;
85
- }
86
73
  onUserConsoleLog(log) {
87
74
  if (!log.taskId)
88
75
  return;
@@ -93,70 +80,6 @@ class ReporterImpl {
93
80
  }
94
81
  entries.push(log);
95
82
  }
96
- onTestCaseAnnotate(testCase, annotation) {
97
- let entries = this._annotations.get(testCase.id);
98
- if (!entries) {
99
- entries = [];
100
- this._annotations.set(testCase.id, entries);
101
- }
102
- entries.push(annotation);
103
- }
104
- onTestRunStart() {
105
- this._startTimestamp = Date.now();
106
- this._sampleSystem();
107
- }
108
- _ensureFKSuite(p) {
109
- let suite = this._allSuites.get(p.id);
110
- if (suite)
111
- return suite;
112
- if ("name" in p) {
113
- const parent = this._ensureFKSuite(p.parent);
114
- suite = {
115
- type: "suite",
116
- title: p.name,
117
- location: p.location ? {
118
- file: this._worktree.gitPath(p.module.moduleId),
119
- column: p.location.column,
120
- line: p.location.line
121
- } : void 0
122
- };
123
- parent.suites ??= [];
124
- parent.suites.push(suite);
125
- } else {
126
- suite = {
127
- type: "file",
128
- title: p.relativeModuleId,
129
- location: {
130
- file: this._worktree.gitPath(p.moduleId),
131
- column: 0,
132
- line: 0
133
- }
134
- };
135
- this._fileSuites.set(p.id, suite);
136
- }
137
- this._allSuites.set(p.id, suite);
138
- return suite;
139
- }
140
- _ensureTest(testCase) {
141
- let fkTest = this._testCaseIdToTest.get(testCase.id);
142
- if (!fkTest) {
143
- fkTest = {
144
- attempts: [],
145
- title: testCase.name,
146
- location: testCase.location ? {
147
- file: this._worktree.gitPath(testCase.module.moduleId),
148
- column: testCase.location.column,
149
- line: testCase.location.line
150
- } : void 0
151
- };
152
- this._testToTestCaseId.set(fkTest, testCase.id);
153
- this._testCaseIdToTest.set(testCase.id, fkTest);
154
- const parent = this._ensureFKSuite(testCase.parent);
155
- parent.tests ??= [];
156
- parent.tests.push(fkTest);
157
- }
158
- return fkTest;
159
- }
160
83
  _ensureEnvironmentIdx(testCase) {
161
84
  const projectName = testCase.project.name || "vitest";
162
85
  let idx = this._environments.findIndex((env) => env.name === projectName);
@@ -176,13 +99,42 @@ class ReporterImpl {
176
99
  column: frame.column
177
100
  } : void 0;
178
101
  }
179
- async onTestCaseResult(testCase) {
102
+ _collectSuite(fkParent, suite) {
103
+ const fkSuite = {
104
+ type: "suite",
105
+ title: suite.name,
106
+ location: suite.location ? {
107
+ file: this._worktree.gitPath(suite.module.moduleId),
108
+ column: suite.location.column,
109
+ line: suite.location.line
110
+ } : void 0
111
+ };
112
+ fkParent.suites ??= [];
113
+ fkParent.suites.push(fkSuite);
114
+ for (const s of suite.children.suites())
115
+ this._collectSuite(fkSuite, s);
116
+ for (const t of suite.children.tests())
117
+ this._collectTest(fkSuite, t);
118
+ }
119
+ async _collectTest(fkParent, testCase) {
180
120
  const environmentIdx = this._ensureEnvironmentIdx(testCase);
181
- const fkTest = this._ensureTest(testCase);
121
+ const fkTest = {
122
+ attempts: [],
123
+ title: testCase.name,
124
+ location: testCase.location ? {
125
+ file: this._worktree.gitPath(testCase.module.moduleId),
126
+ column: testCase.location.column,
127
+ line: testCase.location.line
128
+ } : void 0
129
+ };
130
+ fkParent.tests ??= [];
131
+ fkParent.tests.push(fkTest);
132
+ gTestToTestCaseId.set(fkTest, testCase.id);
182
133
  const result = testCase.result();
183
- if (result.state === "skipped") {
134
+ if (result.state === "skipped" || result.state === "pending") {
184
135
  fkTest.attempts.push({
185
136
  environmentIdx,
137
+ annotations: testCase.options.mode === "skip" ? [{ type: "skip" }] : testCase.options.mode === "todo" ? [{ type: "todo" }] : void 0,
186
138
  startTimestamp: Date.now(),
187
139
  duration: 0,
188
140
  status: "skipped",
@@ -190,11 +142,11 @@ class ReporterImpl {
190
142
  });
191
143
  return;
192
144
  }
193
- const diag = testCase.diagnostic();
194
- assert(diag, `Diagnostic must be present in finished test cases`);
195
- ;
145
+ const startTime = testCase.diagnostic()?.startTime ?? this._startTimestamp;
146
+ const duration = testCase.diagnostic()?.duration ?? testCase.module.diagnostic()?.duration ?? 0;
147
+ const retryCount = testCase.diagnostic()?.retryCount ?? 0;
196
148
  const stdio = [];
197
- let ts = diag.startTime;
149
+ let ts = startTime;
198
150
  for (const entry of this._stdio.get(testCase.id) ?? []) {
199
151
  stdio.push({
200
152
  text: entry.content,
@@ -210,7 +162,7 @@ class ReporterImpl {
210
162
  stack: error.stack,
211
163
  location: this._errorLocation(error.stacks, testFile)
212
164
  }));
213
- const annotations = (this._annotations.get(testCase.id) ?? []).map((annotation) => ({
165
+ const annotations = testCase.annotations().map((annotation) => ({
214
166
  type: annotation.type,
215
167
  description: annotation.message,
216
168
  location: annotation.location ? {
@@ -219,12 +171,14 @@ class ReporterImpl {
219
171
  column: annotation.location.column
220
172
  } : void 0
221
173
  }));
174
+ if (testCase.options.fails)
175
+ annotations.push({ type: "fail" });
222
176
  const expectedStatus = testCase.options.fails ? "failed" : "passed";
223
177
  const oppositeStatus = expectedStatus === "failed" ? "passed" : "failed";
224
- for (let i = 0; i < diag.retryCount; ++i) {
178
+ for (let i = 0; i < retryCount; ++i) {
225
179
  fkTest.attempts.push({
226
180
  environmentIdx,
227
- startTimestamp: diag.startTime,
181
+ startTimestamp: startTime,
228
182
  duration: 0,
229
183
  // retries have an opposite status from expected status to
230
184
  // trigger retry.
@@ -232,15 +186,17 @@ class ReporterImpl {
232
186
  expectedStatus,
233
187
  // TODO: ideally, we can differentiate STDIO between attempts.
234
188
  // However, vitest doesn't let us do so easily.
235
- stdio,
236
- errors,
237
- annotations
189
+ // We slice these arrays just to be safe: if someone downstream from us
190
+ // decides to push a new annotation, then they can do it per-attempt safely.
191
+ stdio: stdio.slice(),
192
+ errors: errors.slice(),
193
+ annotations: annotations.slice()
238
194
  });
239
195
  }
240
196
  fkTest.attempts.push({
241
197
  environmentIdx,
242
- startTimestamp: diag.startTime,
243
- duration: diag.duration,
198
+ startTimestamp: startTime,
199
+ duration,
244
200
  status: result.state === "failed" ? oppositeStatus : expectedStatus,
245
201
  expectedStatus,
246
202
  // TODO: ideally, we can differentiate STDIO between attempts.
@@ -250,38 +206,17 @@ class ReporterImpl {
250
206
  annotations
251
207
  });
252
208
  }
253
- _detectAndHandleTestDuplicates() {
254
- const TITLE_SEPARATOR = crypto.randomBytes(128).toString("base64");
255
- const testIdToTests = /* @__PURE__ */ new Map();
256
- const testFullNames = /* @__PURE__ */ new Map();
257
- function visitSuite(suite, parentTitles = []) {
258
- parentTitles.push(suite.title);
259
- for (const childSuite of suite.suites ?? [])
260
- visitSuite(childSuite, parentTitles);
261
- for (const test of suite.tests ?? []) {
262
- const envs = Array.from(new Set(test.attempts.map((attempt) => attempt.environmentIdx ?? 0))).sort((a, b) => a - b);
263
- const testId = [`[${envs.join(", ")}]`, ...parentTitles, test.title].join(TITLE_SEPARATOR);
264
- testFullNames.set(test, [...parentTitles, test.title].join(" > "));
265
- let tests = testIdToTests.get(testId);
266
- if (!tests) {
267
- tests = [];
268
- testIdToTests.set(testId, tests);
269
- }
270
- tests.push(test);
271
- }
272
- parentTitles.pop();
273
- }
274
- for (const fileSuite of this._fileSuites.values())
275
- visitSuite(fileSuite);
276
- if (Array.from(testIdToTests.values()).every((tests) => tests.length <= 1))
277
- return;
209
+ _warnDuplicates(duplicates) {
278
210
  const warnMessages = [];
279
- for (const [testId, tests] of testIdToTests) {
280
- if (tests.length <= 1)
281
- continue;
282
- const testFullName = testFullNames.get(tests[0]);
283
- const warnMessage = `${tests.length} tests: ${testFullName}`;
284
- warnMessages.push(warnMessage);
211
+ for (const duplicate of duplicates)
212
+ warnMessages.push(`${duplicate.tests.length} tests: ${duplicate.fullName}`);
213
+ this._logger.warn(`[flakiness.io] \u26A0 Detected test with duplicate names!`);
214
+ for (const warnMessage of warnMessages)
215
+ this._logger.warn(`[flakiness.io] - ${warnMessage}`);
216
+ this._logger.warn(`[flakiness.io] Please rename tests so that they all have unique full names.`);
217
+ }
218
+ _failDuplicates(duplicates) {
219
+ for (const { fullName, tests } of duplicates) {
285
220
  const test = tests[0];
286
221
  const envs = new Set(test.attempts.map((a) => a.environmentIdx ?? 0));
287
222
  test.attempts = Array.from(envs).map((envIdx) => ({
@@ -292,15 +227,15 @@ class ReporterImpl {
292
227
  status: "failed",
293
228
  errors: [{
294
229
  message: [
295
- `Flakiness.io detected ${tests.length} tests with identical full name "${testFullName}"`,
230
+ `Flakiness.io detected ${tests.length} tests with identical full name "${fullName}"`,
296
231
  `Please rename tests to ensure they all have unique full names.`
297
232
  ].join("\n")
298
233
  }],
299
234
  annotations: [{
300
- type: "duplicates",
235
+ type: "dupe",
301
236
  description: [
302
237
  `Flakiness.io failed to process this test because there are ${tests.length} tests with`,
303
- `identical full name: "${testFullName}".`,
238
+ `identical full name: "${fullName}".`,
304
239
  `Please make sure that all your tests have unique full names.`
305
240
  ].join("\n")
306
241
  }]
@@ -308,18 +243,88 @@ class ReporterImpl {
308
243
  for (let i = 1; i < tests.length; ++i)
309
244
  tests[i].attempts = [];
310
245
  }
311
- if (warnMessages.length) {
312
- this._logger.warn(`[flakiness.io] \u26A0 Detected test with duplicate names!`);
313
- for (const warnMessage of warnMessages)
314
- this._logger.warn(`[flakiness.io] - ${warnMessage}`);
315
- this._logger.warn(`[flakiness.io] Please rename tests so that they all have unique full names.`);
246
+ }
247
+ _renameDuplicates(duplicates, testIdToTests) {
248
+ for (const { tests, testId } of duplicates) {
249
+ tests.sort((test1, test2) => {
250
+ const id1 = gTestToTestCaseId.get(test1) ?? "";
251
+ const id2 = gTestToTestCaseId.get(test2) ?? "";
252
+ return id1 < id2 ? -1 : id1 > id2 ? 1 : 0;
253
+ });
254
+ let dupeIndex = 2;
255
+ for (let i = 1; i < tests.length; ++i) {
256
+ while (testIdToTests.has(testId + dupeSuffix(dupeIndex)))
257
+ ++dupeIndex;
258
+ tests[i].title += dupeSuffix(dupeIndex);
259
+ tests[i].attempts.forEach((attempt) => {
260
+ attempt.annotations ??= [];
261
+ attempt.annotations.push({ type: "dupe" });
262
+ });
263
+ testIdToTests.set(testId + dupeSuffix(dupeIndex), [tests[i]]);
264
+ }
316
265
  }
317
266
  }
267
+ _detectDuplicates(fileSuites) {
268
+ const TITLE_SEPARATOR = crypto.randomBytes(128).toString("base64");
269
+ const testIdToTests = /* @__PURE__ */ new Map();
270
+ const testFullNames = /* @__PURE__ */ new Map();
271
+ function visitSuite(suite, parentTitles = []) {
272
+ parentTitles.push(suite.title);
273
+ for (const childSuite of suite.suites ?? [])
274
+ visitSuite(childSuite, parentTitles);
275
+ for (const test of suite.tests ?? []) {
276
+ const envs = Array.from(new Set(test.attempts.map((attempt) => attempt.environmentIdx ?? 0))).sort((a, b) => a - b);
277
+ const testId = [`[${envs.join(", ")}]`, ...parentTitles, test.title].join(TITLE_SEPARATOR);
278
+ testFullNames.set(test, [...parentTitles, test.title].join(" > "));
279
+ let tests = testIdToTests.get(testId);
280
+ if (!tests) {
281
+ tests = [];
282
+ testIdToTests.set(testId, tests);
283
+ }
284
+ tests.push(test);
285
+ }
286
+ parentTitles.pop();
287
+ }
288
+ for (const fileSuite of fileSuites)
289
+ visitSuite(fileSuite);
290
+ const duplicates = [];
291
+ for (const [testId, tests] of testIdToTests) {
292
+ if (tests.length <= 1)
293
+ continue;
294
+ const fullName = testFullNames.get(tests[0]);
295
+ duplicates.push({ fullName, testId, tests });
296
+ }
297
+ return { duplicates, testIdToTests };
298
+ }
318
299
  async onTestRunEnd(testModules, unhandledErrors, reason) {
300
+ const fileSuites = testModules.map((file) => {
301
+ const fkFileSuite = {
302
+ type: "file",
303
+ title: file.relativeModuleId,
304
+ location: {
305
+ file: this._worktree.gitPath(file.moduleId),
306
+ column: 0,
307
+ line: 0
308
+ }
309
+ };
310
+ for (const suite of file.children.suites())
311
+ this._collectSuite(fkFileSuite, suite);
312
+ for (const test of file.children.tests())
313
+ this._collectTest(fkFileSuite, test);
314
+ return fkFileSuite;
315
+ });
319
316
  clearTimeout(this._telemetryTimer);
320
317
  this._cpuUtilization.sample();
321
318
  this._ramUtilization.sample();
322
- this._detectAndHandleTestDuplicates();
319
+ const { duplicates, testIdToTests } = this._detectDuplicates(fileSuites);
320
+ if (duplicates.length) {
321
+ this._warnDuplicates(duplicates);
322
+ if (this._options.duplicates === "rename") {
323
+ this._renameDuplicates(duplicates, testIdToTests);
324
+ } else {
325
+ this._failDuplicates(duplicates);
326
+ }
327
+ }
323
328
  const duration = Date.now() - this._startTimestamp;
324
329
  const report = ReportUtils.normalizeReport({
325
330
  url: CIUtils.runUrl(),
@@ -330,7 +335,7 @@ class ReporterImpl {
330
335
  environments: this._environments,
331
336
  startTimestamp: this._startTimestamp,
332
337
  duration,
333
- suites: Array.from(this._fileSuites.values()),
338
+ suites: fileSuites,
334
339
  unattributedErrors: unhandledErrors.map((error) => ({
335
340
  message: error.message,
336
341
  stack: error.stack,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flakiness/vitest",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "1.0.0",
4
4
  "description": "",
5
5
  "main": "lib/reporter.js",
6
6
  "keywords": [],
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "type": "module",
14
14
  "devDependencies": {
15
+ "@flakiness/vitest": "0.1.0-alpha.2",
15
16
  "@types/node": "^25.3.3",
16
17
  "@vitest/utils": "^4.0.18",
17
18
  "esbuild": "^0.27.3",
@@ -1,4 +1,4 @@
1
- import type { SerializedError, TestCase, TestModule, TestRunEndReason, Vitest } from 'vitest/node';
1
+ import type { SerializedError, TestModule, TestRunEndReason, Vitest } from 'vitest/node';
2
2
  import type { Reporter } from 'vitest/reporters';
3
3
  interface UserConsoleLog {
4
4
  content: string;
@@ -9,28 +9,6 @@ interface UserConsoleLog {
9
9
  time: number;
10
10
  size: number;
11
11
  }
12
- interface TestArtifactLocation {
13
- /** Line number in the source file (1-indexed) */
14
- line: number;
15
- /** Column number in the line (1-indexed) */
16
- column: number;
17
- /** Path to the source file */
18
- file: string;
19
- }
20
- interface TestAttachment {
21
- /** MIME type of the attachment (e.g., 'image/png', 'text/plain') */
22
- contentType?: string;
23
- /** File system path to the attachment */
24
- path?: string;
25
- /** Inline attachment content as a string or raw binary data */
26
- body?: string | Uint8Array;
27
- }
28
- interface TestAnnotation {
29
- message: string;
30
- type: string;
31
- location?: TestArtifactLocation;
32
- attachment?: TestAttachment;
33
- }
34
12
  export type OpenMode = 'always' | 'never' | 'on-failure';
35
13
  export type FKVitestReporterOptions = {
36
14
  disableUpload?: boolean;
@@ -38,6 +16,7 @@ export type FKVitestReporterOptions = {
38
16
  endpoint?: string;
39
17
  token?: string;
40
18
  outputFolder?: string;
19
+ duplicates?: 'fail' | 'rename';
41
20
  open?: OpenMode;
42
21
  };
43
22
  export interface FKVitestLogger {
@@ -48,17 +27,13 @@ export interface FKVitestLogger {
48
27
  export default class FKVitestReporter implements Reporter {
49
28
  private _options;
50
29
  private _impl?;
30
+ private _vitest?;
51
31
  private _logger;
52
32
  constructor(_options: FKVitestReporterOptions);
53
33
  setLoggerForTest(logger: FKVitestLogger): void;
54
- onUserConsoleLog(log: UserConsoleLog): void;
34
+ onUserConsoleLog(log: UserConsoleLog): Promise<void>;
55
35
  onInit(vitest: Vitest): void;
56
- /**
57
- * Called when annotation is added via the `task.annotate` API.
58
- */
59
- onTestCaseAnnotate(testCase: TestCase, annotation: TestAnnotation): void;
60
36
  onTestRunStart(): void;
61
- onTestCaseResult(testCase: TestCase): Promise<void>;
62
37
  onTestRunEnd(testModules: ReadonlyArray<TestModule>, unhandledErrors: ReadonlyArray<SerializedError>, reason: TestRunEndReason): Promise<void>;
63
38
  }
64
39
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/reporter.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAa,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIjD,UAAU,cAAc;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AACD,UAAU,oBAAoB;IAC7B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;CACb;AACD,UAAU,cAAc;IACvB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;CAC3B;AACD,UAAU,cAAc;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;AAEzD,MAAM,MAAM,uBAAuB,GAAG;IACpC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ;IAQ3C,OAAO,CAAC,QAAQ;IAP5B,OAAO,CAAC,KAAK,CAAC,CAAe;IAC7B,OAAO,CAAC,OAAO,CAId;gBAEmB,QAAQ,EAAE,uBAAuB;IAGrD,gBAAgB,CAAC,MAAM,EAAE,cAAc;IAIvC,gBAAgB,CAAC,GAAG,EAAE,cAAc;IAIpC,MAAM,CAAC,MAAM,EAAE,MAAM;IAKtB;;MAEE;IACF,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc;IAIhE,cAAc;IAIR,gBAAgB,CAAC,QAAQ,EAAE,QAAQ;IAInC,YAAY,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,eAAe,EAAE,aAAa,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,gBAAgB;CAGrI"}
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/reporter.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAY,UAAU,EAAE,gBAAgB,EAAa,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIjD,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;AAEzD,MAAM,MAAM,uBAAuB,GAAG;IACpC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAC,QAAQ,CAAC;IAC7B,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ;IAS3C,OAAO,CAAC,QAAQ;IAR5B,OAAO,CAAC,KAAK,CAAC,CAAe;IAC7B,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,OAAO,CAId;gBAEmB,QAAQ,EAAE,uBAAuB;IAGrD,gBAAgB,CAAC,MAAM,EAAE,cAAc;IAIjC,gBAAgB,CAAC,GAAG,EAAE,cAAc;IAI1C,MAAM,CAAC,MAAM,EAAE,MAAM;IAIrB,cAAc;IAOR,YAAY,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,eAAe,EAAE,aAAa,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,gBAAgB;CAIrI"}