@fixsense/cypress-plugin 0.2.1 → 0.2.4
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 +19 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -3
- package/dist/local.d.ts +49 -3
- package/dist/local.js +115 -7
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -55,9 +55,28 @@ actually wrong, so a green suite stays quiet.
|
|
|
55
55
|
On GitHub Actions the same table is written to the job summary, so it appears
|
|
56
56
|
on the run's own page instead of scrolling past in the log.
|
|
57
57
|
|
|
58
|
+
On GitHub Actions it also writes an annotation for each flaky or broken test,
|
|
59
|
+
so the finding shows up inline on the pull request, carrying how many of the
|
|
60
|
+
last runs the test misbehaved in.
|
|
61
|
+
|
|
58
62
|
`.fixsense/` is git-ignored for you: committing it would mean a diff on every
|
|
59
63
|
run and merge conflicts in a file nobody reads.
|
|
60
64
|
|
|
65
|
+
### Keeping the history in CI
|
|
66
|
+
|
|
67
|
+
Every CI job starts from a clean checkout, so the history is gone before the
|
|
68
|
+
next run reads it. The plugin says so in one line instead of staying silent.
|
|
69
|
+
On GitHub Actions, cache the directory between runs:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
- name: Keep the test history between runs
|
|
73
|
+
uses: actions/cache@v4
|
|
74
|
+
with:
|
|
75
|
+
path: .fixsense
|
|
76
|
+
key: fixsense-history-${{ github.run_id }}
|
|
77
|
+
restore-keys: fixsense-history-
|
|
78
|
+
```
|
|
79
|
+
|
|
61
80
|
### Why an account, then
|
|
62
81
|
|
|
63
82
|
The local history lives on one machine and dies with the CI container. The
|
package/dist/index.d.ts
CHANGED
|
@@ -40,4 +40,4 @@ export declare function registerFixSense(on: On, options?: FixSenseCypressOption
|
|
|
40
40
|
export declare function sendRun(tests: TestRecord[], startedAt: Date, options: FixSenseCypressOptions): Promise<void>;
|
|
41
41
|
export default registerFixSense;
|
|
42
42
|
export * from "./payload";
|
|
43
|
-
export { computeFlakiness, readHistory, recordAndReport, renderMarkdown, renderSummary, scoreHistory, HISTORY_DIR, HISTORY_FILE, MAX_RUNS, } from "./local";
|
|
43
|
+
export { computeFlakiness, readHistory, recordAndReport, renderMarkdown, renderSummary, renderAnnotations, scoreHistory, HISTORY_DIR, HISTORY_FILE, MAX_RUNS, } from "./local";
|
package/dist/index.js
CHANGED
|
@@ -23,12 +23,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
23
23
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.MAX_RUNS = exports.HISTORY_FILE = exports.HISTORY_DIR = exports.scoreHistory = exports.renderSummary = exports.renderMarkdown = exports.recordAndReport = exports.readHistory = exports.computeFlakiness = void 0;
|
|
26
|
+
exports.MAX_RUNS = exports.HISTORY_FILE = exports.HISTORY_DIR = exports.scoreHistory = exports.renderAnnotations = exports.renderSummary = exports.renderMarkdown = exports.recordAndReport = exports.readHistory = exports.computeFlakiness = void 0;
|
|
27
27
|
exports.registerFixSense = registerFixSense;
|
|
28
28
|
exports.sendRun = sendRun;
|
|
29
29
|
const payload_1 = require("./payload");
|
|
30
30
|
const local_1 = require("./local");
|
|
31
31
|
const DEFAULT_API_URL = "https://fix-sense.com";
|
|
32
|
+
const DOCS_URL = "https://fix-sense.com/docs/integrations/cypress-plugin#keeping-the-history-in-ci";
|
|
32
33
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
33
34
|
/**
|
|
34
35
|
* Register the plugin from `setupNodeEvents`.
|
|
@@ -60,16 +61,19 @@ async function sendRun(tests, startedAt, options) {
|
|
|
60
61
|
return;
|
|
61
62
|
// The local history first, because it needs no account and is the only
|
|
62
63
|
// part most first-time users will ever see.
|
|
64
|
+
const apiKey = options.apiKey ?? process.env.FIXSENSE_API_KEY;
|
|
63
65
|
if (options.local !== false) {
|
|
64
66
|
try {
|
|
65
|
-
(0, local_1.recordAndReport)(options.cwd ?? process.cwd(), tests)
|
|
67
|
+
(0, local_1.recordAndReport)(options.cwd ?? process.cwd(), tests, new Date(), {
|
|
68
|
+
connected: Boolean(apiKey),
|
|
69
|
+
docsUrl: DOCS_URL,
|
|
70
|
+
});
|
|
66
71
|
}
|
|
67
72
|
catch (error) {
|
|
68
73
|
const reason = error instanceof Error ? error.message : String(error);
|
|
69
74
|
console.error(`[fixsense] Local history not updated: ${reason}`);
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
|
-
const apiKey = options.apiKey ?? process.env.FIXSENSE_API_KEY;
|
|
73
77
|
// Silent without a key: a developer running Cypress locally has none and
|
|
74
78
|
// does not need telling on every run.
|
|
75
79
|
if (!apiKey)
|
|
@@ -119,6 +123,7 @@ Object.defineProperty(exports, "readHistory", { enumerable: true, get: function
|
|
|
119
123
|
Object.defineProperty(exports, "recordAndReport", { enumerable: true, get: function () { return local_2.recordAndReport; } });
|
|
120
124
|
Object.defineProperty(exports, "renderMarkdown", { enumerable: true, get: function () { return local_2.renderMarkdown; } });
|
|
121
125
|
Object.defineProperty(exports, "renderSummary", { enumerable: true, get: function () { return local_2.renderSummary; } });
|
|
126
|
+
Object.defineProperty(exports, "renderAnnotations", { enumerable: true, get: function () { return local_2.renderAnnotations; } });
|
|
122
127
|
Object.defineProperty(exports, "scoreHistory", { enumerable: true, get: function () { return local_2.scoreHistory; } });
|
|
123
128
|
Object.defineProperty(exports, "HISTORY_DIR", { enumerable: true, get: function () { return local_2.HISTORY_DIR; } });
|
|
124
129
|
Object.defineProperty(exports, "HISTORY_FILE", { enumerable: true, get: function () { return local_2.HISTORY_FILE; } });
|
package/dist/local.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type Outcome = "expected" | "unexpected" | "flaky" | "skipped";
|
|
|
20
20
|
export interface StoredTest {
|
|
21
21
|
title: string;
|
|
22
22
|
file: string | null;
|
|
23
|
+
/** Where the test is declared, so a GitHub annotation can point at it. */
|
|
24
|
+
line?: number | null;
|
|
23
25
|
outcome: Outcome;
|
|
24
26
|
}
|
|
25
27
|
export interface StoredRun {
|
|
@@ -32,6 +34,7 @@ export interface LocalHistory {
|
|
|
32
34
|
}
|
|
33
35
|
export declare const HISTORY_DIR = ".fixsense";
|
|
34
36
|
export declare const HISTORY_FILE = "history.json";
|
|
37
|
+
export declare const SITE_URL = "https://fix-sense.com";
|
|
35
38
|
/**
|
|
36
39
|
* How many runs are kept.
|
|
37
40
|
*
|
|
@@ -85,6 +88,8 @@ export declare function writeHistory(cwd: string, history: LocalHistory): void;
|
|
|
85
88
|
export interface TestVerdict extends FlakinessReport {
|
|
86
89
|
title: string;
|
|
87
90
|
file: string | null;
|
|
91
|
+
/** The line this test is declared on, when the runner reports it. */
|
|
92
|
+
line: number | null;
|
|
88
93
|
/** How many of the stored runs this test failed or needed a retry in. */
|
|
89
94
|
badRuns: number;
|
|
90
95
|
}
|
|
@@ -97,9 +102,9 @@ export declare function scoreHistory(history: LocalHistory): TestVerdict[];
|
|
|
97
102
|
* so this stays empty unless the history actually found something. "suspect"
|
|
98
103
|
* is deliberately not reported: it means the tool is not sure yet.
|
|
99
104
|
*/
|
|
100
|
-
export declare function renderSummary(verdicts: TestVerdict[], totalRuns: number): string;
|
|
105
|
+
export declare function renderSummary(verdicts: TestVerdict[], totalRuns: number, connected?: boolean): string;
|
|
101
106
|
/** The same summary as GitHub-flavoured Markdown, for the run's own page. */
|
|
102
|
-
export declare function renderMarkdown(verdicts: TestVerdict[], totalRuns: number): string;
|
|
107
|
+
export declare function renderMarkdown(verdicts: TestVerdict[], totalRuns: number, connected?: boolean): string;
|
|
103
108
|
/**
|
|
104
109
|
* Write the Markdown where GitHub Actions renders it on the run's page.
|
|
105
110
|
*
|
|
@@ -107,6 +112,29 @@ export declare function renderMarkdown(verdicts: TestVerdict[], totalRuns: numbe
|
|
|
107
112
|
* page it is the first thing a reviewer sees.
|
|
108
113
|
*/
|
|
109
114
|
export declare function writeStepSummary(markdown: string): void;
|
|
115
|
+
/**
|
|
116
|
+
* GitHub workflow annotations for the tests that earned a verdict.
|
|
117
|
+
*
|
|
118
|
+
* This is the one thing a single-run flaky reporter does that we did not:
|
|
119
|
+
* `::warning file=…,line=…::` makes GitHub draw the finding inline on the exact
|
|
120
|
+
* line in the pull request's "Files changed" tab, not only in the job log.
|
|
121
|
+
* Ours carries what a single run cannot — how many of the last N runs the test
|
|
122
|
+
* misbehaved in — so the annotation is a measurement, not a one-off retry.
|
|
123
|
+
*
|
|
124
|
+
* A `flaky` verdict is a warning: the test is noise to quarantine. A `broken`
|
|
125
|
+
* verdict — fails every run — is an error: a real failure hiding behind a
|
|
126
|
+
* retry, and the louder level is the point. "suspect" is never annotated, for
|
|
127
|
+
* the same reason it is never summarised: the tool is not sure yet.
|
|
128
|
+
*/
|
|
129
|
+
export declare function renderAnnotations(verdicts: TestVerdict[], totalRuns: number): string[];
|
|
130
|
+
/** True when we are inside a GitHub Actions job, where annotations render. */
|
|
131
|
+
export declare function isGitHubActions(env: Record<string, string | undefined>): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Emit the annotations as workflow commands on stdout, where the runner reads
|
|
134
|
+
* them. Each must be alone on its line, so this writes them directly rather
|
|
135
|
+
* than folding them into the human summary.
|
|
136
|
+
*/
|
|
137
|
+
export declare function writeAnnotations(lines: string[]): void;
|
|
110
138
|
/**
|
|
111
139
|
* One entry per test per run, whatever the runner reported.
|
|
112
140
|
*
|
|
@@ -116,9 +144,27 @@ export declare function writeStepSummary(markdown: string): void;
|
|
|
116
144
|
* knows how the test finally ended.
|
|
117
145
|
*/
|
|
118
146
|
export declare function dedupe(tests: StoredTest[]): StoredTest[];
|
|
147
|
+
/** Every supported CI sets CI=true, except Azure Pipelines, which sets TF_BUILD. */
|
|
148
|
+
export declare function isCi(env: Record<string, string | undefined>): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Said when a CI job finds no earlier runs to compare with.
|
|
151
|
+
*
|
|
152
|
+
* A CI job starts from a clean checkout, so without a cache the history holds
|
|
153
|
+
* one run every time, never reaches a verdict, and the package stays silent
|
|
154
|
+
* forever — which reads as "it does nothing". One line saying why is the
|
|
155
|
+
* difference between a team fixing that and a team uninstalling.
|
|
156
|
+
*/
|
|
157
|
+
export declare function renderNoHistoryHint(docsUrl: string): string;
|
|
158
|
+
export interface ReportContext {
|
|
159
|
+
/** An API key is set, so the history is also kept by the service. */
|
|
160
|
+
connected?: boolean;
|
|
161
|
+
/** Where the package's own docs explain keeping the history in CI. */
|
|
162
|
+
docsUrl?: string;
|
|
163
|
+
env?: Record<string, string | undefined>;
|
|
164
|
+
}
|
|
119
165
|
/**
|
|
120
166
|
* Record this run and report what the history now says.
|
|
121
167
|
*
|
|
122
168
|
* Returns the text that was printed, which is what the tests assert on.
|
|
123
169
|
*/
|
|
124
|
-
export declare function recordAndReport(cwd: string, tests: StoredTest[], now?: Date): string;
|
|
170
|
+
export declare function recordAndReport(cwd: string, tests: StoredTest[], now?: Date, context?: ReportContext): string;
|
package/dist/local.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* asserts the two copies are the same file, so they cannot drift.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.MIN_FLAKY_RATE = exports.MIN_FLAKY_RUNS = exports.FLAKY_SCORE_THRESHOLD = exports.MIN_RUNS_FOR_VERDICT = exports.MAX_RUNS = exports.HISTORY_FILE = exports.HISTORY_DIR = void 0;
|
|
21
|
+
exports.MIN_FLAKY_RATE = exports.MIN_FLAKY_RUNS = exports.FLAKY_SCORE_THRESHOLD = exports.MIN_RUNS_FOR_VERDICT = exports.MAX_RUNS = exports.SITE_URL = exports.HISTORY_FILE = exports.HISTORY_DIR = void 0;
|
|
22
22
|
exports.countTransitions = countTransitions;
|
|
23
23
|
exports.computeFlakiness = computeFlakiness;
|
|
24
24
|
exports.historyPath = historyPath;
|
|
@@ -30,12 +30,18 @@ exports.scoreHistory = scoreHistory;
|
|
|
30
30
|
exports.renderSummary = renderSummary;
|
|
31
31
|
exports.renderMarkdown = renderMarkdown;
|
|
32
32
|
exports.writeStepSummary = writeStepSummary;
|
|
33
|
+
exports.renderAnnotations = renderAnnotations;
|
|
34
|
+
exports.isGitHubActions = isGitHubActions;
|
|
35
|
+
exports.writeAnnotations = writeAnnotations;
|
|
33
36
|
exports.dedupe = dedupe;
|
|
37
|
+
exports.isCi = isCi;
|
|
38
|
+
exports.renderNoHistoryHint = renderNoHistoryHint;
|
|
34
39
|
exports.recordAndReport = recordAndReport;
|
|
35
40
|
const fs_1 = require("fs");
|
|
36
41
|
const path_1 = require("path");
|
|
37
42
|
exports.HISTORY_DIR = ".fixsense";
|
|
38
43
|
exports.HISTORY_FILE = "history.json";
|
|
44
|
+
exports.SITE_URL = "https://fix-sense.com";
|
|
39
45
|
/**
|
|
40
46
|
* How many runs are kept.
|
|
41
47
|
*
|
|
@@ -191,7 +197,11 @@ function scoreHistory(history) {
|
|
|
191
197
|
for (const run of history.runs) {
|
|
192
198
|
for (const test of run.tests ?? []) {
|
|
193
199
|
const key = `${test.file ?? ""}\u0000${test.title}`;
|
|
194
|
-
const entry = byTest.get(key) ?? { file: test.file ?? null, outcomes: [] };
|
|
200
|
+
const entry = byTest.get(key) ?? { file: test.file ?? null, line: test.line ?? null, outcomes: [] };
|
|
201
|
+
// The newest run wins on location: a test that moved is reported where
|
|
202
|
+
// it now lives, not where it first failed.
|
|
203
|
+
if (test.line != null)
|
|
204
|
+
entry.line = test.line;
|
|
195
205
|
entry.outcomes.push(test.outcome);
|
|
196
206
|
byTest.set(key, entry);
|
|
197
207
|
}
|
|
@@ -203,6 +213,7 @@ function scoreHistory(history) {
|
|
|
203
213
|
...report,
|
|
204
214
|
title: key.split("\u0000")[1] ?? "",
|
|
205
215
|
file: entry.file,
|
|
216
|
+
line: entry.line,
|
|
206
217
|
badRuns: entry.outcomes.filter((o) => o === "flaky" || o === "unexpected").length,
|
|
207
218
|
});
|
|
208
219
|
}
|
|
@@ -215,7 +226,7 @@ function scoreHistory(history) {
|
|
|
215
226
|
* so this stays empty unless the history actually found something. "suspect"
|
|
216
227
|
* is deliberately not reported: it means the tool is not sure yet.
|
|
217
228
|
*/
|
|
218
|
-
function renderSummary(verdicts, totalRuns) {
|
|
229
|
+
function renderSummary(verdicts, totalRuns, connected = false) {
|
|
219
230
|
const flaky = verdicts.filter((v) => v.verdict === "flaky");
|
|
220
231
|
const broken = verdicts.filter((v) => v.verdict === "broken");
|
|
221
232
|
if (flaky.length === 0 && broken.length === 0)
|
|
@@ -233,10 +244,13 @@ function renderSummary(verdicts, totalRuns) {
|
|
|
233
244
|
lines.push(` ${v.title} — failed in ${v.badRuns} of ${v.runs} runs`);
|
|
234
245
|
}
|
|
235
246
|
}
|
|
247
|
+
if (!connected) {
|
|
248
|
+
lines.push(`Why they fail, and the fix as a pull request: ${exports.SITE_URL}`);
|
|
249
|
+
}
|
|
236
250
|
return lines.join("\n");
|
|
237
251
|
}
|
|
238
252
|
/** The same summary as GitHub-flavoured Markdown, for the run's own page. */
|
|
239
|
-
function renderMarkdown(verdicts, totalRuns) {
|
|
253
|
+
function renderMarkdown(verdicts, totalRuns, connected = false) {
|
|
240
254
|
const interesting = verdicts.filter((v) => v.verdict === "flaky" || v.verdict === "broken");
|
|
241
255
|
if (interesting.length === 0)
|
|
242
256
|
return "";
|
|
@@ -250,6 +264,14 @@ function renderMarkdown(verdicts, totalRuns) {
|
|
|
250
264
|
"| --- | --- | --- | --- |",
|
|
251
265
|
rows,
|
|
252
266
|
"",
|
|
267
|
+
// The run page is read by the whole team, not only whoever installed the
|
|
268
|
+
// package, so this is where the rest of them learn where the table came from.
|
|
269
|
+
...(connected
|
|
270
|
+
? []
|
|
271
|
+
: [
|
|
272
|
+
`<sub>Recorded by [FixSense](${exports.SITE_URL}?utm_source=step-summary) — it also says whether the test or the application is at fault, and opens the fix.</sub>`,
|
|
273
|
+
"",
|
|
274
|
+
]),
|
|
253
275
|
].join("\n");
|
|
254
276
|
}
|
|
255
277
|
/**
|
|
@@ -269,6 +291,57 @@ function writeStepSummary(markdown) {
|
|
|
269
291
|
// The step summary is a nicety; losing it is not worth a word.
|
|
270
292
|
}
|
|
271
293
|
}
|
|
294
|
+
/** A `::warning`/`::error` workflow command cannot carry a raw newline or `::`. */
|
|
295
|
+
function escapeAnnotation(message) {
|
|
296
|
+
return message.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* GitHub workflow annotations for the tests that earned a verdict.
|
|
300
|
+
*
|
|
301
|
+
* This is the one thing a single-run flaky reporter does that we did not:
|
|
302
|
+
* `::warning file=…,line=…::` makes GitHub draw the finding inline on the exact
|
|
303
|
+
* line in the pull request's "Files changed" tab, not only in the job log.
|
|
304
|
+
* Ours carries what a single run cannot — how many of the last N runs the test
|
|
305
|
+
* misbehaved in — so the annotation is a measurement, not a one-off retry.
|
|
306
|
+
*
|
|
307
|
+
* A `flaky` verdict is a warning: the test is noise to quarantine. A `broken`
|
|
308
|
+
* verdict — fails every run — is an error: a real failure hiding behind a
|
|
309
|
+
* retry, and the louder level is the point. "suspect" is never annotated, for
|
|
310
|
+
* the same reason it is never summarised: the tool is not sure yet.
|
|
311
|
+
*/
|
|
312
|
+
function renderAnnotations(verdicts, totalRuns) {
|
|
313
|
+
const lines = [];
|
|
314
|
+
for (const v of verdicts) {
|
|
315
|
+
if (v.verdict !== "flaky" && v.verdict !== "broken")
|
|
316
|
+
continue;
|
|
317
|
+
const level = v.verdict === "broken" ? "error" : "warning";
|
|
318
|
+
const what = v.verdict === "broken"
|
|
319
|
+
? `fails every run — a real failure, not flakiness`
|
|
320
|
+
: `flaky in ${v.badRuns} of ${v.runs} runs (score ${v.score})`;
|
|
321
|
+
const props = [
|
|
322
|
+
v.file ? `file=${v.file}` : null,
|
|
323
|
+
v.line != null ? `line=${v.line}` : null,
|
|
324
|
+
`title=FixSense: ${escapeAnnotation(v.verdict)} test`,
|
|
325
|
+
]
|
|
326
|
+
.filter(Boolean)
|
|
327
|
+
.join(",");
|
|
328
|
+
lines.push(`::${level} ${props}::${escapeAnnotation(`${v.title} — ${what}, across the last ${totalRuns} runs`)}`);
|
|
329
|
+
}
|
|
330
|
+
return lines;
|
|
331
|
+
}
|
|
332
|
+
/** True when we are inside a GitHub Actions job, where annotations render. */
|
|
333
|
+
function isGitHubActions(env) {
|
|
334
|
+
return env.GITHUB_ACTIONS === "true";
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Emit the annotations as workflow commands on stdout, where the runner reads
|
|
338
|
+
* them. Each must be alone on its line, so this writes them directly rather
|
|
339
|
+
* than folding them into the human summary.
|
|
340
|
+
*/
|
|
341
|
+
function writeAnnotations(lines) {
|
|
342
|
+
for (const line of lines)
|
|
343
|
+
console.log(line);
|
|
344
|
+
}
|
|
272
345
|
/**
|
|
273
346
|
* One entry per test per run, whatever the runner reported.
|
|
274
347
|
*
|
|
@@ -283,27 +356,62 @@ function dedupe(tests) {
|
|
|
283
356
|
byKey.set(`${t.file ?? ""}\u0000${t.title}`, {
|
|
284
357
|
title: t.title,
|
|
285
358
|
file: t.file,
|
|
359
|
+
line: t.line ?? null,
|
|
286
360
|
outcome: t.outcome,
|
|
287
361
|
});
|
|
288
362
|
}
|
|
289
363
|
return [...byKey.values()];
|
|
290
364
|
}
|
|
365
|
+
/** Every supported CI sets CI=true, except Azure Pipelines, which sets TF_BUILD. */
|
|
366
|
+
function isCi(env) {
|
|
367
|
+
return Boolean(env.CI && env.CI !== "false") || env.TF_BUILD === "True";
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Said when a CI job finds no earlier runs to compare with.
|
|
371
|
+
*
|
|
372
|
+
* A CI job starts from a clean checkout, so without a cache the history holds
|
|
373
|
+
* one run every time, never reaches a verdict, and the package stays silent
|
|
374
|
+
* forever — which reads as "it does nothing". One line saying why is the
|
|
375
|
+
* difference between a team fixing that and a team uninstalling.
|
|
376
|
+
*/
|
|
377
|
+
function renderNoHistoryHint(docsUrl) {
|
|
378
|
+
return [
|
|
379
|
+
"FixSense: recorded this run, but found no earlier runs to compare it with.",
|
|
380
|
+
" CI starts every job clean, so the history is lost between runs.",
|
|
381
|
+
` Cache .fixsense/ between runs, or set FIXSENSE_API_KEY: ${docsUrl}`,
|
|
382
|
+
].join("\n");
|
|
383
|
+
}
|
|
291
384
|
/**
|
|
292
385
|
* Record this run and report what the history now says.
|
|
293
386
|
*
|
|
294
387
|
* Returns the text that was printed, which is what the tests assert on.
|
|
295
388
|
*/
|
|
296
|
-
function recordAndReport(cwd, tests, now = new Date()) {
|
|
389
|
+
function recordAndReport(cwd, tests, now = new Date(), context = {}) {
|
|
297
390
|
const history = appendRun(readHistory(cwd), {
|
|
298
391
|
at: now.toISOString(),
|
|
299
392
|
tests: dedupe(tests),
|
|
300
393
|
});
|
|
301
394
|
writeHistory(cwd, history);
|
|
395
|
+
const connected = context.connected === true;
|
|
396
|
+
if (history.runs.length === 1 &&
|
|
397
|
+
!connected &&
|
|
398
|
+
context.docsUrl &&
|
|
399
|
+
isCi(context.env ?? process.env)) {
|
|
400
|
+
const hint = renderNoHistoryHint(context.docsUrl);
|
|
401
|
+
console.log(`\n${hint}\n`);
|
|
402
|
+
return hint;
|
|
403
|
+
}
|
|
302
404
|
const verdicts = scoreHistory(history);
|
|
303
|
-
const summary = renderSummary(verdicts, history.runs.length);
|
|
405
|
+
const summary = renderSummary(verdicts, history.runs.length, connected);
|
|
304
406
|
if (summary) {
|
|
305
407
|
console.log(`\n${summary}\n`);
|
|
306
|
-
writeStepSummary(renderMarkdown(verdicts, history.runs.length));
|
|
408
|
+
writeStepSummary(renderMarkdown(verdicts, history.runs.length, connected));
|
|
409
|
+
// On GitHub, also pin each verdict to its line in the PR. The summary and
|
|
410
|
+
// the annotations carry the same findings to two different places a
|
|
411
|
+
// reviewer looks: the run page, and the diff.
|
|
412
|
+
if (isGitHubActions(context.env ?? process.env)) {
|
|
413
|
+
writeAnnotations(renderAnnotations(verdicts, history.runs.length));
|
|
414
|
+
}
|
|
307
415
|
}
|
|
308
416
|
return summary;
|
|
309
417
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fixsense/cypress-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Cypress plugin that records test outcomes across runs so flaky tests can be identified over time.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
"keywords": [
|
|
14
14
|
"cypress",
|
|
15
15
|
"cypress-plugin",
|
|
16
|
+
"flaky",
|
|
17
|
+
"flaky-test",
|
|
16
18
|
"flaky-tests",
|
|
19
|
+
"flaky-test-detection",
|
|
20
|
+
"test-history",
|
|
21
|
+
"retry",
|
|
22
|
+
"quarantine",
|
|
23
|
+
"e2e",
|
|
17
24
|
"ci",
|
|
18
25
|
"test-reporting"
|
|
19
26
|
],
|
|
@@ -22,7 +29,7 @@
|
|
|
22
29
|
"url": "https://github.com/CopilotMe/fix-sense.git",
|
|
23
30
|
"directory": "packages/cypress-plugin"
|
|
24
31
|
},
|
|
25
|
-
"homepage": "https://fix-sense.com",
|
|
32
|
+
"homepage": "https://fix-sense.com/docs/integrations/cypress-plugin",
|
|
26
33
|
"scripts": {
|
|
27
34
|
"build": "tsc -p tsconfig.json",
|
|
28
35
|
"prepublishOnly": "npm run build"
|