@flakiness/vitest 0.1.0-alpha.1 → 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,76 +80,13 @@ 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
- let idx = this._environments.findIndex((env) => env.name === testCase.project.name);
84
+ const projectName = testCase.project.name || "vitest";
85
+ let idx = this._environments.findIndex((env) => env.name === projectName);
162
86
  if (idx === -1) {
163
87
  idx = this._environments.length;
164
88
  this._environments.push(ReportUtils.createEnvironment({
165
- name: testCase.project.name
89
+ name: projectName
166
90
  }));
167
91
  }
168
92
  return idx;
@@ -175,13 +99,42 @@ class ReporterImpl {
175
99
  column: frame.column
176
100
  } : void 0;
177
101
  }
178
- 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) {
179
120
  const environmentIdx = this._ensureEnvironmentIdx(testCase);
180
- 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);
181
133
  const result = testCase.result();
182
- if (result.state === "skipped") {
134
+ if (result.state === "skipped" || result.state === "pending") {
183
135
  fkTest.attempts.push({
184
136
  environmentIdx,
137
+ annotations: testCase.options.mode === "skip" ? [{ type: "skip" }] : testCase.options.mode === "todo" ? [{ type: "todo" }] : void 0,
185
138
  startTimestamp: Date.now(),
186
139
  duration: 0,
187
140
  status: "skipped",
@@ -189,11 +142,11 @@ class ReporterImpl {
189
142
  });
190
143
  return;
191
144
  }
192
- const diag = testCase.diagnostic();
193
- assert(diag, `Diagnostic must be present in finished test cases`);
194
- ;
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;
195
148
  const stdio = [];
196
- let ts = diag.startTime;
149
+ let ts = startTime;
197
150
  for (const entry of this._stdio.get(testCase.id) ?? []) {
198
151
  stdio.push({
199
152
  text: entry.content,
@@ -209,7 +162,7 @@ class ReporterImpl {
209
162
  stack: error.stack,
210
163
  location: this._errorLocation(error.stacks, testFile)
211
164
  }));
212
- const annotations = (this._annotations.get(testCase.id) ?? []).map((annotation) => ({
165
+ const annotations = testCase.annotations().map((annotation) => ({
213
166
  type: annotation.type,
214
167
  description: annotation.message,
215
168
  location: annotation.location ? {
@@ -218,12 +171,14 @@ class ReporterImpl {
218
171
  column: annotation.location.column
219
172
  } : void 0
220
173
  }));
174
+ if (testCase.options.fails)
175
+ annotations.push({ type: "fail" });
221
176
  const expectedStatus = testCase.options.fails ? "failed" : "passed";
222
177
  const oppositeStatus = expectedStatus === "failed" ? "passed" : "failed";
223
- for (let i = 0; i < diag.retryCount; ++i) {
178
+ for (let i = 0; i < retryCount; ++i) {
224
179
  fkTest.attempts.push({
225
180
  environmentIdx,
226
- startTimestamp: diag.startTime,
181
+ startTimestamp: startTime,
227
182
  duration: 0,
228
183
  // retries have an opposite status from expected status to
229
184
  // trigger retry.
@@ -231,15 +186,17 @@ class ReporterImpl {
231
186
  expectedStatus,
232
187
  // TODO: ideally, we can differentiate STDIO between attempts.
233
188
  // However, vitest doesn't let us do so easily.
234
- stdio,
235
- errors,
236
- 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()
237
194
  });
238
195
  }
239
196
  fkTest.attempts.push({
240
197
  environmentIdx,
241
- startTimestamp: diag.startTime,
242
- duration: diag.duration,
198
+ startTimestamp: startTime,
199
+ duration,
243
200
  status: result.state === "failed" ? oppositeStatus : expectedStatus,
244
201
  expectedStatus,
245
202
  // TODO: ideally, we can differentiate STDIO between attempts.
@@ -249,38 +206,17 @@ class ReporterImpl {
249
206
  annotations
250
207
  });
251
208
  }
252
- _detectAndHandleTestDuplicates() {
253
- const TITLE_SEPARATOR = crypto.randomBytes(128).toString("base64");
254
- const testIdToTests = /* @__PURE__ */ new Map();
255
- const testFullNames = /* @__PURE__ */ new Map();
256
- function visitSuite(suite, parentTitles = []) {
257
- parentTitles.push(suite.title);
258
- for (const childSuite of suite.suites ?? [])
259
- visitSuite(childSuite, parentTitles);
260
- for (const test of suite.tests ?? []) {
261
- const envs = Array.from(new Set(test.attempts.map((attempt) => attempt.environmentIdx ?? 0))).sort((a, b) => a - b);
262
- const testId = [`[${envs.join(", ")}]`, ...parentTitles, test.title].join(TITLE_SEPARATOR);
263
- testFullNames.set(test, [...parentTitles, test.title].join(" > "));
264
- let tests = testIdToTests.get(testId);
265
- if (!tests) {
266
- tests = [];
267
- testIdToTests.set(testId, tests);
268
- }
269
- tests.push(test);
270
- }
271
- parentTitles.pop();
272
- }
273
- for (const fileSuite of this._fileSuites.values())
274
- visitSuite(fileSuite);
275
- if (Array.from(testIdToTests.values()).every((tests) => tests.length <= 1))
276
- return;
209
+ _warnDuplicates(duplicates) {
277
210
  const warnMessages = [];
278
- for (const [testId, tests] of testIdToTests) {
279
- if (tests.length <= 1)
280
- continue;
281
- const testFullName = testFullNames.get(tests[0]);
282
- const warnMessage = `${tests.length} tests: ${testFullName}`;
283
- 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) {
284
220
  const test = tests[0];
285
221
  const envs = new Set(test.attempts.map((a) => a.environmentIdx ?? 0));
286
222
  test.attempts = Array.from(envs).map((envIdx) => ({
@@ -291,15 +227,15 @@ class ReporterImpl {
291
227
  status: "failed",
292
228
  errors: [{
293
229
  message: [
294
- `Flakiness.io detected ${tests.length} tests with identical full name "${testFullName}"`,
230
+ `Flakiness.io detected ${tests.length} tests with identical full name "${fullName}"`,
295
231
  `Please rename tests to ensure they all have unique full names.`
296
232
  ].join("\n")
297
233
  }],
298
234
  annotations: [{
299
- type: "duplicates",
235
+ type: "dupe",
300
236
  description: [
301
237
  `Flakiness.io failed to process this test because there are ${tests.length} tests with`,
302
- `identical full name: "${testFullName}".`,
238
+ `identical full name: "${fullName}".`,
303
239
  `Please make sure that all your tests have unique full names.`
304
240
  ].join("\n")
305
241
  }]
@@ -307,18 +243,88 @@ class ReporterImpl {
307
243
  for (let i = 1; i < tests.length; ++i)
308
244
  tests[i].attempts = [];
309
245
  }
310
- if (warnMessages.length) {
311
- this._logger.warn(`[flakiness.io] \u26A0 Detected test with duplicate names!`);
312
- for (const warnMessage of warnMessages)
313
- this._logger.warn(`[flakiness.io] - ${warnMessage}`);
314
- 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
+ }
315
265
  }
316
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
+ }
317
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
+ });
318
316
  clearTimeout(this._telemetryTimer);
319
317
  this._cpuUtilization.sample();
320
318
  this._ramUtilization.sample();
321
- 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
+ }
322
328
  const duration = Date.now() - this._startTimestamp;
323
329
  const report = ReportUtils.normalizeReport({
324
330
  url: CIUtils.runUrl(),
@@ -329,7 +335,7 @@ class ReporterImpl {
329
335
  environments: this._environments,
330
336
  startTimestamp: this._startTimestamp,
331
337
  duration,
332
- suites: Array.from(this._fileSuites.values()),
338
+ suites: fileSuites,
333
339
  unattributedErrors: unhandledErrors.map((error) => ({
334
340
  message: error.message,
335
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.1",
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"}