@flakiness/cucumberjs 1.0.0-alpha.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/.github/workflows/flakiness-upload-fork-prs.yml +30 -0
- package/.github/workflows/publish-npm.yml +41 -0
- package/.github/workflows/tests.yml +45 -0
- package/CONTRIBUTING.md +58 -0
- package/LICENSE +21 -0
- package/agenda.md +2 -0
- package/build.mts +34 -0
- package/cucumber.mjs +6 -0
- package/features/attachments.feature +32 -0
- package/features/basic.feature +27 -0
- package/features/data_tables.feature +45 -0
- package/features/description.feature +49 -0
- package/features/errors.feature +28 -0
- package/features/hooks_named.feature +32 -0
- package/features/hooks_unnamed.feature +33 -0
- package/features/locations.feature +37 -0
- package/features/retries.feature +30 -0
- package/features/rules.feature +25 -0
- package/features/scenario_outlines.feature +57 -0
- package/features/scenario_outlines_multiple.feature +44 -0
- package/features/statuses.feature +70 -0
- package/features/stdio.feature +29 -0
- package/features/steps.feature +24 -0
- package/features/support/attachments_steps.ts +32 -0
- package/features/support/basic_steps.ts +235 -0
- package/features/support/description_steps.ts +37 -0
- package/features/support/errors_steps.ts +48 -0
- package/features/support/harness.ts +196 -0
- package/features/support/project_steps.ts +24 -0
- package/features/support/stdio_steps.ts +21 -0
- package/features/support/tags_steps.ts +10 -0
- package/features/tags.feature +19 -0
- package/features/tags_hierarchy.feature +37 -0
- package/package.json +37 -0
- package/plan.md +59 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/formatter.ts +635 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Feature: Multiple Scenario Outline Examples
|
|
2
|
+
Scenario: captures multiple examples blocks
|
|
3
|
+
Given the project file "features/checkout-totals.feature":
|
|
4
|
+
"""
|
|
5
|
+
Feature: Checkout totals
|
|
6
|
+
Scenario Outline: totals
|
|
7
|
+
Given the subtotal is <subtotal> and tax is <tax> and the total is <total>
|
|
8
|
+
|
|
9
|
+
@priority
|
|
10
|
+
Examples: common orders
|
|
11
|
+
| subtotal | tax | total |
|
|
12
|
+
| 1 | 2 | 3 |
|
|
13
|
+
| 2 | 2 | 4 |
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
| subtotal | tax | total |
|
|
17
|
+
| 3 | 10 | 13 |
|
|
18
|
+
"""
|
|
19
|
+
And the project file "features/support/steps.js":
|
|
20
|
+
"""
|
|
21
|
+
const { Given } = require('@cucumber/cucumber');
|
|
22
|
+
|
|
23
|
+
Given('the subtotal is {int} and tax is {int} and the total is {int}', function(subtotal, tax, total) {});
|
|
24
|
+
"""
|
|
25
|
+
When I generate the Flakiness report for "multiple examples"
|
|
26
|
+
Then the report contains 3 tests
|
|
27
|
+
|
|
28
|
+
When I look at the test named "totals [subtotal=1, tax=2, total=3]"
|
|
29
|
+
And the test is in file "features/checkout-totals.feature" at line 8
|
|
30
|
+
And the test has tags "priority"
|
|
31
|
+
And the test contains 1 attempt
|
|
32
|
+
When I look at the attempt #1
|
|
33
|
+
Then the attempt contains 1 step:
|
|
34
|
+
"""
|
|
35
|
+
Given the subtotal is 1 and tax is 2 and the total is 3
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
When I look at the test named "totals [subtotal=2, tax=2, total=4]"
|
|
39
|
+
And the test is in file "features/checkout-totals.feature" at line 9
|
|
40
|
+
And the test has tags "priority"
|
|
41
|
+
|
|
42
|
+
When I look at the test named "totals [subtotal=3, tax=10, total=13]"
|
|
43
|
+
And the test is in file "features/checkout-totals.feature" at line 13
|
|
44
|
+
And the test has tags ""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Feature: Statuses
|
|
2
|
+
|
|
3
|
+
Scenario: captures non-passing Cucumber statuses
|
|
4
|
+
Given the project file "features/statuses.feature":
|
|
5
|
+
"""
|
|
6
|
+
Feature: Statuses
|
|
7
|
+
|
|
8
|
+
Scenario: it is pending
|
|
9
|
+
Given a pending step
|
|
10
|
+
|
|
11
|
+
Scenario: it is ambiguous
|
|
12
|
+
Given an ambiguous step
|
|
13
|
+
|
|
14
|
+
Scenario: it is undefined
|
|
15
|
+
Given an undefined step
|
|
16
|
+
"""
|
|
17
|
+
And the project file "features/support/steps.js":
|
|
18
|
+
"""
|
|
19
|
+
const { Given } = require('@cucumber/cucumber');
|
|
20
|
+
|
|
21
|
+
Given('a pending step', function() {
|
|
22
|
+
return 'pending';
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
Given('an ambiguous step', function() {});
|
|
26
|
+
Given('an ambiguous step', function() {});
|
|
27
|
+
"""
|
|
28
|
+
When I generate the Flakiness report for "cucumber statuses"
|
|
29
|
+
Then the report contains 3 tests
|
|
30
|
+
|
|
31
|
+
When I look at the test named "it is pending"
|
|
32
|
+
Then the test contains 1 attempt
|
|
33
|
+
And attempt #1 is "failed"
|
|
34
|
+
When I look at the "failed" attempt
|
|
35
|
+
Then the attempt contains 1 error
|
|
36
|
+
And the attempt error #1 has message "Step is pending"
|
|
37
|
+
And the attempt contains 1 step:
|
|
38
|
+
"""
|
|
39
|
+
Given a pending step
|
|
40
|
+
"""
|
|
41
|
+
When I look at the step #1
|
|
42
|
+
Then the step has an error with message "Step is pending"
|
|
43
|
+
|
|
44
|
+
When I look at the test named "it is ambiguous"
|
|
45
|
+
Then the test contains 1 attempt
|
|
46
|
+
And attempt #1 is "failed"
|
|
47
|
+
When I look at the "failed" attempt
|
|
48
|
+
Then the attempt contains 1 error
|
|
49
|
+
And the attempt error #1 has message containing "Multiple step definitions match:"
|
|
50
|
+
And the attempt contains 1 step:
|
|
51
|
+
"""
|
|
52
|
+
Given an ambiguous step
|
|
53
|
+
"""
|
|
54
|
+
When I look at the step #1
|
|
55
|
+
Then the step has an error with message containing "Multiple step definitions match:"
|
|
56
|
+
|
|
57
|
+
When I look at the test named "it is undefined"
|
|
58
|
+
Then the test contains 1 attempt
|
|
59
|
+
And attempt #1 is "failed"
|
|
60
|
+
When I look at the "failed" attempt
|
|
61
|
+
Then the attempt contains 1 error
|
|
62
|
+
And the attempt error #1 has message "Undefined step"
|
|
63
|
+
And the attempt error #1 has a snippet containing "an undefined step"
|
|
64
|
+
And the attempt contains 1 step:
|
|
65
|
+
"""
|
|
66
|
+
Given an undefined step
|
|
67
|
+
"""
|
|
68
|
+
When I look at the step #1
|
|
69
|
+
Then the step has an error with message "Undefined step"
|
|
70
|
+
And the step error has a snippet containing "an undefined step"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Feature: STDIO
|
|
2
|
+
Scenario: captures Cucumber log output as attempt stdio
|
|
3
|
+
Given the project file "features/logging.feature":
|
|
4
|
+
"""
|
|
5
|
+
Feature: Logging
|
|
6
|
+
Scenario: it logs
|
|
7
|
+
Given a logging step
|
|
8
|
+
"""
|
|
9
|
+
And the project file "features/support/steps.js":
|
|
10
|
+
"""
|
|
11
|
+
const { Given } = require('@cucumber/cucumber');
|
|
12
|
+
Given('a logging step', async function() {
|
|
13
|
+
this.log('hello from log');
|
|
14
|
+
await new Promise(resolve => setTimeout(resolve, 30));
|
|
15
|
+
this.log('second line');
|
|
16
|
+
});
|
|
17
|
+
"""
|
|
18
|
+
When I generate the Flakiness report for "scenario with cucumber log output"
|
|
19
|
+
When I look at the test named "it logs"
|
|
20
|
+
Then the test contains 1 attempt
|
|
21
|
+
When I look at the attempt #1
|
|
22
|
+
Then the attempt contains 2 stdio entries
|
|
23
|
+
|
|
24
|
+
When I look at the stdio entry #1
|
|
25
|
+
Then the stdio entry has text "hello from log"
|
|
26
|
+
|
|
27
|
+
When I look at the stdio entry #2
|
|
28
|
+
Then the stdio entry has text "second line"
|
|
29
|
+
And the stdio entry happened after the previous stdio entry
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Feature: Steps
|
|
2
|
+
Scenario: captures test steps
|
|
3
|
+
Given the project file "features/passing.feature":
|
|
4
|
+
"""
|
|
5
|
+
Feature: Passing Test Suite
|
|
6
|
+
Scenario: it passes
|
|
7
|
+
Given a passing step
|
|
8
|
+
"""
|
|
9
|
+
And the project file "features/support/steps.js":
|
|
10
|
+
"""
|
|
11
|
+
const { Given } = require('@cucumber/cucumber');
|
|
12
|
+
Given('a passing step', function() {});
|
|
13
|
+
"""
|
|
14
|
+
When I generate the Flakiness report for "passing steps"
|
|
15
|
+
When I look at the test named "it passes"
|
|
16
|
+
And the test contains 1 attempt
|
|
17
|
+
|
|
18
|
+
When I look at the attempt #1
|
|
19
|
+
And the attempt contains 1 step:
|
|
20
|
+
"""
|
|
21
|
+
Given a passing step
|
|
22
|
+
"""
|
|
23
|
+
When I look at the step #1
|
|
24
|
+
And the step is in file "features/passing.feature" at line 3
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Then, When } from '@cucumber/cucumber';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import type { TestWorld } from './harness.ts';
|
|
5
|
+
import { assertCount } from './harness.ts';
|
|
6
|
+
|
|
7
|
+
When<TestWorld>('I look at the attachment #{int}', function(attachmentIdx: number) {
|
|
8
|
+
this.attachment = this.attempt?.attachments?.[attachmentIdx - 1];
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
Then<TestWorld>('the attempt contains {int} attachments', function(expectedAttachments: number) {
|
|
12
|
+
assertCount(this.attempt?.attachments, expectedAttachments);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
Then<TestWorld>('the report contains {int} missing attachments', function(expectedMissingAttachments: number) {
|
|
16
|
+
assertCount(this.reportResult?.missingAttachments, expectedMissingAttachments);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
Then<TestWorld>('the attachment is called {string}', function(expectedName: string) {
|
|
20
|
+
assert.equal(this.attachment?.name, expectedName);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
Then<TestWorld>('the attachment has content type {string}', function(expectedContentType: string) {
|
|
24
|
+
assert.equal(this.attachment?.contentType, expectedContentType);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
Then<TestWorld>('the stored attachment has text {string}', function(expectedText: string) {
|
|
28
|
+
assert.ok(this.attachment, 'Expected attachment to be selected');
|
|
29
|
+
const storedAttachment = this.reportResult?.attachments.find(attachment => attachment.id === this.attachment?.id);
|
|
30
|
+
assert.ok(storedAttachment, `Expected stored attachment for ${this.attachment.id}`);
|
|
31
|
+
assert.equal(fs.readFileSync(storedAttachment.path, 'utf8'), expectedText);
|
|
32
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { BeforeAll, Then, When } from '@cucumber/cucumber';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import type { TestWorld } from './harness.ts';
|
|
5
|
+
import { ARTIFACTS_DIR, assertCount } from './harness.ts';
|
|
6
|
+
import type { FlakinessReport as FK } from '@flakiness/flakiness-report';
|
|
7
|
+
|
|
8
|
+
BeforeAll(function() {
|
|
9
|
+
fs.rmSync(ARTIFACTS_DIR, { recursive: true, force: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
Then<TestWorld>('the report should contain the basic metadata', function() {
|
|
13
|
+
assert.ok(this.reportResult, 'Expected report result to be defined');
|
|
14
|
+
const { report, log } = this.reportResult;
|
|
15
|
+
|
|
16
|
+
assert.equal(report.category, 'cucumberjs');
|
|
17
|
+
assert.equal(report.url, 'https://ci.example.test/build/123');
|
|
18
|
+
assert.equal(report.environments.length, 1);
|
|
19
|
+
assert.equal(report.environments[0]?.name, 'cucumberjs');
|
|
20
|
+
assert.ok(report.commitId, 'Expected commitId to be present');
|
|
21
|
+
assert.ok(report.startTimestamp > 0, 'Expected startTimestamp to be present');
|
|
22
|
+
assert.ok(report.duration > 0, 'Expected duration to be positive');
|
|
23
|
+
|
|
24
|
+
assert.ok((report.cpuCount ?? 0) > 0, 'Expected cpuCount to be populated');
|
|
25
|
+
assert.ok((report.cpuAvg?.length ?? 0) > 0, 'Expected cpuAvg telemetry to be populated');
|
|
26
|
+
assert.ok((report.cpuMax?.length ?? 0) > 0, 'Expected cpuMax telemetry to be populated');
|
|
27
|
+
assert.ok((report.ramBytes ?? 0) > 0, 'Expected ramBytes to be populated');
|
|
28
|
+
assert.ok((report.ram?.length ?? 0) > 0, 'Expected ram telemetry to be populated');
|
|
29
|
+
|
|
30
|
+
assert.equal(log.stderr, '', `Expected stderr to be empty.\n\nSTDERR:\n${log.stderr}`);
|
|
31
|
+
assert.ok(log.stdout.includes('npx flakiness show'), `Expected report hint in stdout.\n\nSTDOUT:\n${log.stdout}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
When<TestWorld>('I look at the suite named {string}', function(title: string) {
|
|
35
|
+
this.suite = findUnique(
|
|
36
|
+
collectSuites(this.reportResult?.report?.suites ?? []),
|
|
37
|
+
suite => suite.title === title,
|
|
38
|
+
`suite named ${JSON.stringify(title)}`,
|
|
39
|
+
);
|
|
40
|
+
this.test = undefined;
|
|
41
|
+
this.attempt = undefined;
|
|
42
|
+
this.step = undefined;
|
|
43
|
+
this.attachment = undefined;
|
|
44
|
+
this.stdio = undefined;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
When<TestWorld>('I look at the test named {string}', function(title: string) {
|
|
48
|
+
this.test = findUnique(
|
|
49
|
+
collectTests(this.reportResult?.report?.suites ?? []),
|
|
50
|
+
test => test.title === title,
|
|
51
|
+
`test named ${JSON.stringify(title)}`,
|
|
52
|
+
);
|
|
53
|
+
this.attempt = undefined;
|
|
54
|
+
this.step = undefined;
|
|
55
|
+
this.attachment = undefined;
|
|
56
|
+
this.stdio = undefined;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
When<TestWorld>('I look at the attempt #{int}', function(attemptIdx) {
|
|
60
|
+
this.attempt = this.test?.attempts[attemptIdx - 1];
|
|
61
|
+
this.step = undefined;
|
|
62
|
+
this.attachment = undefined;
|
|
63
|
+
this.stdio = undefined;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
When<TestWorld>('I look at the {string} attempt', function(status: string) {
|
|
67
|
+
this.attempt = findUnique(
|
|
68
|
+
this.test?.attempts ?? [],
|
|
69
|
+
attempt => (attempt.status ?? 'passed') === status,
|
|
70
|
+
`${JSON.stringify(status)} attempt`,
|
|
71
|
+
);
|
|
72
|
+
this.step = undefined;
|
|
73
|
+
this.attachment = undefined;
|
|
74
|
+
this.stdio = undefined;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
When<TestWorld>('I look at the step #{int}', function(stepIdx) {
|
|
78
|
+
this.step = this.attempt?.steps?.[stepIdx - 1];
|
|
79
|
+
this.attachment = undefined;
|
|
80
|
+
this.stdio = undefined;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
When<TestWorld>('I look at the step named {string}', function(title: string) {
|
|
84
|
+
this.step = findUnique(
|
|
85
|
+
this.attempt?.steps ?? [],
|
|
86
|
+
step => step.title === title,
|
|
87
|
+
`step named ${JSON.stringify(title)}`,
|
|
88
|
+
);
|
|
89
|
+
this.attachment = undefined;
|
|
90
|
+
this.stdio = undefined;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
Then<TestWorld>('the report contains {int} test(s)', function(expectedTests: number) {
|
|
94
|
+
assertCount(collectTests(this.reportResult?.report?.suites ?? []), expectedTests);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
Then<TestWorld>('the report hierarchy is:', function(expectedHierarchy: string) {
|
|
98
|
+
const hierarchy = renderReportHierarchy(this.reportResult?.report);
|
|
99
|
+
assert.equal(normalizeMultiline(hierarchy), normalizeMultiline(expectedHierarchy));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
Then<TestWorld>('the suite contains {int} test(s)', function(expectedTests: number) {
|
|
103
|
+
assertCount(this.suite?.tests, expectedTests);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
Then<TestWorld>('the test contains {int} attempt(s)', function(expectedAttempts: number) {
|
|
107
|
+
assertCount(this.test?.attempts, expectedAttempts);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
Then<TestWorld>('the attempt contains {int} step(s)', function(expectedSteps: number) {
|
|
111
|
+
assertCount(this.attempt?.steps, expectedSteps);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
Then<TestWorld>('the attempt contains {int} step(s):', function(expectedSteps: number, expectedStepTitles: string) {
|
|
115
|
+
const steps = assertCount(this.attempt?.steps, expectedSteps);
|
|
116
|
+
assert.deepEqual(
|
|
117
|
+
steps.map(step => step.title),
|
|
118
|
+
parseMultilineList(expectedStepTitles),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
Then<TestWorld>('the step contains {int} steps(s)', function(expectedSteps: number) {
|
|
123
|
+
assertCount(this.step?.steps, expectedSteps);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
Then<TestWorld>('attempt #{int} is {string}', function(attemptIdx, status) {
|
|
127
|
+
assert.equal(this.test?.attempts[attemptIdx - 1]?.status ?? 'passed', status);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
Then<TestWorld>('the suite is called {string}', function(title) {
|
|
131
|
+
assert.equal(this.suite?.title, title);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
Then<TestWorld>('the suite is in file {string} at line {int}', function(file, line) {
|
|
135
|
+
assert.equal(this.suite?.location?.file, file);
|
|
136
|
+
assert.equal(this.suite?.location?.line, line);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
Then<TestWorld>('the test is called {string}', function(title) {
|
|
140
|
+
assert.equal(this.test?.title, title);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
Then<TestWorld>('the test is in file {string} at line {int}', function(file, line) {
|
|
144
|
+
assert.equal(this.test?.location?.file, file);
|
|
145
|
+
assert.equal(this.test?.location?.line, line);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
Then<TestWorld>('the step is called {string}', function(title) {
|
|
149
|
+
assert.equal(this.step?.title, title);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
Then<TestWorld>('the step #{int} is called {string}', function(stepIdx: number, title: string) {
|
|
153
|
+
assert.equal(this.attempt?.steps?.[stepIdx - 1]?.title, title);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
Then<TestWorld>('the step is in file {string} at line {int}', function(file, line) {
|
|
157
|
+
assert.equal(this.step?.location?.file, file);
|
|
158
|
+
assert.equal(this.step?.location?.line, line);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
function collectSuites(suites: FK.Suite[]): FK.Suite[] {
|
|
162
|
+
return suites.flatMap(suite => [suite, ...collectSuites(suite.suites ?? [])]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function collectTests(suites: FK.Suite[]): FK.Test[] {
|
|
166
|
+
return suites.flatMap(suite => [...(suite.tests ?? []), ...collectTests(suite.suites ?? [])]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function findUnique<T>(elements: T[], predicate: (element: T) => boolean, description: string): T {
|
|
170
|
+
const matches = elements.filter(predicate);
|
|
171
|
+
assert.equal(matches.length, 1, `Expected exactly 1 ${description}, got ${matches.length}`);
|
|
172
|
+
return matches[0]!;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderReportHierarchy(report: FK.Report | undefined): string {
|
|
176
|
+
return renderTree(
|
|
177
|
+
(report?.suites ?? []).map(suite => renderSuiteNode(suite)),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type TreeNode = {
|
|
182
|
+
label: string,
|
|
183
|
+
children?: TreeNode[],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function renderSuiteNode(suite: FK.Suite): TreeNode {
|
|
187
|
+
const label = suite.type === 'file' ? `file ${suite.title}` : `suite ${suite.title}`;
|
|
188
|
+
return {
|
|
189
|
+
label,
|
|
190
|
+
children: [
|
|
191
|
+
...(suite.suites ?? []).map(renderSuiteNode),
|
|
192
|
+
...(suite.tests ?? []).map(renderTestNode),
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderTestNode(test: FK.Test): TreeNode {
|
|
198
|
+
return {
|
|
199
|
+
label: `test ${test.title}`,
|
|
200
|
+
children: test.attempts.map((attempt, index) => ({
|
|
201
|
+
label: `attempt #${index + 1} ${attempt.status ?? 'passed'}`,
|
|
202
|
+
children: (attempt.steps ?? []).map(step => ({
|
|
203
|
+
label: `step ${step.title}`,
|
|
204
|
+
})),
|
|
205
|
+
})),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function renderTree(nodes: TreeNode[]): string {
|
|
210
|
+
return nodes.flatMap((node, index) => renderTreeNode(node, '', index === nodes.length - 1)).join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderTreeNode(node: TreeNode, prefix: string, isLast: boolean): string[] {
|
|
214
|
+
const branch = isLast ? '`- ' : '|- ';
|
|
215
|
+
const childPrefix = prefix + (isLast ? ' ' : '| ');
|
|
216
|
+
return [
|
|
217
|
+
`${prefix}${branch}${node.label}`,
|
|
218
|
+
...(node.children ?? []).flatMap((child, index, children) => renderTreeNode(child, childPrefix, index === children.length - 1)),
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeMultiline(text: string): string {
|
|
223
|
+
return text
|
|
224
|
+
.trim()
|
|
225
|
+
.split('\n')
|
|
226
|
+
.map(line => line.trimEnd())
|
|
227
|
+
.join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseMultilineList(text: string): string[] {
|
|
231
|
+
return normalizeMultiline(text)
|
|
232
|
+
.split('\n')
|
|
233
|
+
.map(line => line.trim())
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Then } from '@cucumber/cucumber';
|
|
2
|
+
import type { FlakinessReport as FK } from '@flakiness/flakiness-report';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import type { TestWorld } from './harness.ts';
|
|
5
|
+
import { assertCount } from './harness.ts';
|
|
6
|
+
|
|
7
|
+
Then<TestWorld>('the attempt contains {int} annotation(s)', function(expectedAnnotations: number) {
|
|
8
|
+
assertCount(this.attempt?.annotations, expectedAnnotations);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
Then<TestWorld>('the attempt has an annotation {string} with description:', function(type: string, description: string) {
|
|
12
|
+
const annotation = findAnnotation(this.attempt?.annotations, type);
|
|
13
|
+
assert.equal(normalizeMultiline(annotation.description ?? ''), normalizeMultiline(description));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
Then<TestWorld>('the attempt has no annotation {string}', function(type: string) {
|
|
17
|
+
const annotations = this.attempt?.annotations ?? [];
|
|
18
|
+
assert.equal(
|
|
19
|
+
annotations.filter(annotation => annotation.type === type).length,
|
|
20
|
+
0,
|
|
21
|
+
`Expected no annotation of type ${JSON.stringify(type)}, got ${annotations.filter(annotation => annotation.type === type).length}`,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function findAnnotation(annotations: FK.Annotation[] | undefined, type: string): FK.Annotation {
|
|
26
|
+
const matches = (annotations ?? []).filter(annotation => annotation.type === type);
|
|
27
|
+
assert.equal(matches.length, 1, `Expected exactly 1 annotation of type ${JSON.stringify(type)}, got ${matches.length}`);
|
|
28
|
+
return matches[0]!;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeMultiline(text: string): string {
|
|
32
|
+
return text
|
|
33
|
+
.trim()
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map(line => line.trimEnd())
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Then } from '@cucumber/cucumber';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import type { TestWorld } from './harness.ts';
|
|
4
|
+
import { assertCount } from './harness.ts';
|
|
5
|
+
|
|
6
|
+
Then<TestWorld>('the attempt contains {int} error', function(expectedErrors: number) {
|
|
7
|
+
assertCount(this.attempt?.errors, expectedErrors);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
Then<TestWorld>('the attempt error #{int} has message {string}', function(errorIdx: number, message: string) {
|
|
11
|
+
assert.equal(this.attempt?.errors?.[errorIdx - 1]?.message, message);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
Then<TestWorld>('the attempt error #{int} has message containing {string}', function(errorIdx: number, text: string) {
|
|
15
|
+
assert.ok(
|
|
16
|
+
this.attempt?.errors?.[errorIdx - 1]?.message?.includes(text),
|
|
17
|
+
`Expected attempt error message to contain ${JSON.stringify(text)}, got ${JSON.stringify(this.attempt?.errors?.[errorIdx - 1]?.message)}`,
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
Then<TestWorld>('the attempt error #{int} has a stack trace', function(errorIdx: number) {
|
|
22
|
+
assert.ok(this.attempt?.errors?.[errorIdx - 1]?.stack, 'Expected error stack to be present');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
Then<TestWorld>('the attempt error #{int} has a snippet containing {string}', function(errorIdx: number, text: string) {
|
|
26
|
+
assert.ok(
|
|
27
|
+
this.attempt?.errors?.[errorIdx - 1]?.snippet?.includes(text),
|
|
28
|
+
`Expected attempt error snippet to contain ${JSON.stringify(text)}, got ${JSON.stringify(this.attempt?.errors?.[errorIdx - 1]?.snippet)}`,
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
Then<TestWorld>('the step has an error with message {string}', function(message: string) {
|
|
33
|
+
assert.equal(this.step?.error?.message, message);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
Then<TestWorld>('the step has an error with message containing {string}', function(text: string) {
|
|
37
|
+
assert.ok(
|
|
38
|
+
this.step?.error?.message?.includes(text),
|
|
39
|
+
`Expected step error message to contain ${JSON.stringify(text)}, got ${JSON.stringify(this.step?.error?.message)}`,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
Then<TestWorld>('the step error has a snippet containing {string}', function(text: string) {
|
|
44
|
+
assert.ok(
|
|
45
|
+
this.step?.error?.snippet?.includes(text),
|
|
46
|
+
`Expected step error snippet to contain ${JSON.stringify(text)}, got ${JSON.stringify(this.step?.error?.snippet)}`,
|
|
47
|
+
);
|
|
48
|
+
});
|