@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.
Files changed (38) hide show
  1. package/.github/workflows/flakiness-upload-fork-prs.yml +30 -0
  2. package/.github/workflows/publish-npm.yml +41 -0
  3. package/.github/workflows/tests.yml +45 -0
  4. package/CONTRIBUTING.md +58 -0
  5. package/LICENSE +21 -0
  6. package/agenda.md +2 -0
  7. package/build.mts +34 -0
  8. package/cucumber.mjs +6 -0
  9. package/features/attachments.feature +32 -0
  10. package/features/basic.feature +27 -0
  11. package/features/data_tables.feature +45 -0
  12. package/features/description.feature +49 -0
  13. package/features/errors.feature +28 -0
  14. package/features/hooks_named.feature +32 -0
  15. package/features/hooks_unnamed.feature +33 -0
  16. package/features/locations.feature +37 -0
  17. package/features/retries.feature +30 -0
  18. package/features/rules.feature +25 -0
  19. package/features/scenario_outlines.feature +57 -0
  20. package/features/scenario_outlines_multiple.feature +44 -0
  21. package/features/statuses.feature +70 -0
  22. package/features/stdio.feature +29 -0
  23. package/features/steps.feature +24 -0
  24. package/features/support/attachments_steps.ts +32 -0
  25. package/features/support/basic_steps.ts +235 -0
  26. package/features/support/description_steps.ts +37 -0
  27. package/features/support/errors_steps.ts +48 -0
  28. package/features/support/harness.ts +196 -0
  29. package/features/support/project_steps.ts +24 -0
  30. package/features/support/stdio_steps.ts +21 -0
  31. package/features/support/tags_steps.ts +10 -0
  32. package/features/tags.feature +19 -0
  33. package/features/tags_hierarchy.feature +37 -0
  34. package/package.json +37 -0
  35. package/plan.md +59 -0
  36. package/pnpm-workspace.yaml +2 -0
  37. package/src/formatter.ts +635 -0
  38. 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
+ });