@qavajs/playwright-runner-adapter 2.3.3 → 2.5.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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +198 -11
  3. package/adapter/PlaywrightWorld.d.ts +8 -6
  4. package/adapter/PlaywrightWorld.js +21 -8
  5. package/adapter/PlaywrightWorld.js.map +1 -1
  6. package/adapter/Template.d.ts +13 -2
  7. package/adapter/Template.js +57 -8
  8. package/adapter/Template.js.map +1 -1
  9. package/adapter/cucumberAdapter.d.ts +7 -0
  10. package/adapter/cucumberAdapter.js +254 -114
  11. package/adapter/cucumberAdapter.js.map +1 -1
  12. package/adapter/filter.d.ts +8 -0
  13. package/adapter/filter.js +27 -1
  14. package/adapter/filter.js.map +1 -1
  15. package/adapter/gherkin.d.ts +45 -0
  16. package/adapter/gherkin.js +155 -0
  17. package/adapter/gherkin.js.map +1 -0
  18. package/adapter/index.d.ts +2 -0
  19. package/adapter/index.js +2 -0
  20. package/adapter/index.js.map +1 -1
  21. package/adapter/js/PlaywrightWorld.d.ts +8 -6
  22. package/adapter/js/PlaywrightWorld.js +21 -8
  23. package/adapter/js/Template.d.ts +13 -2
  24. package/adapter/js/Template.js +57 -8
  25. package/adapter/js/cucumberAdapter.d.ts +7 -0
  26. package/adapter/js/cucumberAdapter.js +254 -114
  27. package/adapter/js/filter.d.ts +8 -0
  28. package/adapter/js/filter.js +27 -1
  29. package/adapter/js/gherkin.d.ts +45 -0
  30. package/adapter/js/gherkin.js +154 -0
  31. package/adapter/js/index.d.ts +2 -0
  32. package/adapter/js/index.js +2 -0
  33. package/adapter/js/loader.d.ts +9 -1
  34. package/adapter/js/loader.js +35 -15
  35. package/adapter/js/testDetails.d.ts +28 -0
  36. package/adapter/js/testDetails.js +78 -0
  37. package/adapter/js/timeout.d.ts +17 -0
  38. package/adapter/js/timeout.js +24 -0
  39. package/adapter/js/types.d.ts +35 -6
  40. package/adapter/js/withWorld.d.ts +43 -0
  41. package/adapter/js/withWorld.js +16 -0
  42. package/adapter/loader.d.ts +9 -1
  43. package/adapter/loader.js +35 -15
  44. package/adapter/loader.js.map +1 -1
  45. package/adapter/testDetails.d.ts +28 -0
  46. package/adapter/testDetails.js +79 -0
  47. package/adapter/testDetails.js.map +1 -0
  48. package/adapter/timeout.d.ts +17 -0
  49. package/adapter/timeout.js +25 -0
  50. package/adapter/timeout.js.map +1 -0
  51. package/adapter/types.d.ts +35 -6
  52. package/adapter/withWorld.d.ts +43 -0
  53. package/adapter/withWorld.js +17 -0
  54. package/adapter/withWorld.js.map +1 -0
  55. package/package.json +11 -9
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTagFilter = createTagFilter;
3
4
  exports.defineConfig = defineConfig;
5
+ const node_path_1 = require("node:path");
6
+ const node_stream_1 = require("node:stream");
7
+ const node_util_1 = require("node:util");
8
+ const filter_1 = require("./filter");
4
9
  const loader_1 = require("./loader");
5
- const ANNOTATION_TYPES = {
6
- NAME: 'name',
7
- URI: 'uri',
8
- TAGS: 'tags'
9
- };
10
+ const testDetails_1 = require("./testDetails");
11
+ const timeout_1 = require("./timeout");
12
+ const gherkin_1 = require("./gherkin");
10
13
  const HOOK_NAMES = {
11
14
  BEFORE_ALL: 'Before All',
12
15
  AFTER_ALL: 'After All',
@@ -14,21 +17,28 @@ const HOOK_NAMES = {
14
17
  AFTER: 'After',
15
18
  BEFORE_STEP: 'Before Step',
16
19
  AFTER_STEP: 'After Step',
17
- WORLD: 'World',
18
- BEFORE_HOOKS: 'Before Hooks',
19
- AFTER_HOOKS: 'After Hooks'
20
20
  };
21
21
  const DEFAULT_VALUES = {
22
22
  ATTACHMENT_FILENAME: 'attachment',
23
- CONTENT_TYPE: 'text/plain',
23
+ LOG_ATTACHMENT_NAME: 'log',
24
+ TEXT_CONTENT_TYPE: 'text/plain',
25
+ BINARY_CONTENT_TYPE: 'application/octet-stream',
24
26
  COLUMN: 1,
25
27
  TEST_CASE_ID: '0',
26
28
  TEST_STEP_ID: '0'
27
29
  };
28
- const STEP_ARGUMENT_LABELS = {
29
- DOC_STRING: ' [MultiLine]',
30
- DATA_TABLE: ' [DataTable]'
31
- };
30
+ const BASE64_SUFFIX = ';base64';
31
+ /**
32
+ * Error raised for undefined, ambiguous and pending steps
33
+ */
34
+ class StepStatusError extends Error {
35
+ status;
36
+ constructor(message, status) {
37
+ super(message);
38
+ this.status = status;
39
+ this.name = 'StepStatusError';
40
+ }
41
+ }
32
42
  function getLine(step) {
33
43
  if (!step.uri)
34
44
  return {};
@@ -40,22 +50,42 @@ function getLine(step) {
40
50
  }
41
51
  };
42
52
  }
53
+ /**
54
+ * Log data to the report as `log` attachment of the current step (same as cucumber `this.log`)
55
+ */
43
56
  function log(data) {
44
- console.log(data);
45
- }
46
- function attach(body, details) {
47
- const fileName = details?.fileName ?? DEFAULT_VALUES.ATTACHMENT_FILENAME;
48
- const contentType = details?.mediaType ?? DEFAULT_VALUES.CONTENT_TYPE;
49
- return this.test.info().attach(fileName, { body: body, contentType });
50
- }
51
- function stepName(pickleStep) {
52
- let step = pickleStep.text;
53
- if (pickleStep.argument) {
54
- step += pickleStep.argument.docString
55
- ? STEP_ARGUMENT_LABELS.DOC_STRING
56
- : STEP_ARGUMENT_LABELS.DATA_TABLE;
57
+ const body = typeof data === 'string' ? data : (0, node_util_1.inspect)(data);
58
+ this.test.info()
59
+ .attach(DEFAULT_VALUES.LOG_ATTACHMENT_NAME, { body, contentType: DEFAULT_VALUES.TEXT_CONTENT_TYPE })
60
+ .catch(() => console.log(body));
61
+ }
62
+ async function streamToBuffer(stream) {
63
+ const chunks = [];
64
+ for await (const chunk of stream) {
65
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
57
66
  }
58
- return step;
67
+ return Buffer.concat(chunks);
68
+ }
69
+ /**
70
+ * Attach data to the report. Compatible with cucumber `this.attach`:
71
+ * - `attach(data, 'image/png')` or `attach(data, { mediaType, fileName })`
72
+ * - `attach(base64String, 'image/png;base64')` decodes base64 data
73
+ * - readable streams are buffered
74
+ */
75
+ async function attach(data, mediaTypeOrOptions) {
76
+ const options = typeof mediaTypeOrOptions === 'string'
77
+ ? { mediaType: mediaTypeOrOptions }
78
+ : mediaTypeOrOptions ?? {};
79
+ let body = data instanceof node_stream_1.Readable ? await streamToBuffer(data) : data;
80
+ let contentType = options.mediaType;
81
+ if (contentType?.endsWith(BASE64_SUFFIX)) {
82
+ contentType = contentType.slice(0, -BASE64_SUFFIX.length);
83
+ if (typeof body === 'string')
84
+ body = Buffer.from(body, 'base64');
85
+ }
86
+ contentType ??= typeof body === 'string' ? DEFAULT_VALUES.TEXT_CONTENT_TYPE : DEFAULT_VALUES.BINARY_CONTENT_TYPE;
87
+ const fileName = options.fileName ?? DEFAULT_VALUES.ATTACHMENT_FILENAME;
88
+ return this.test.info().attach(fileName, { body, contentType });
59
89
  }
60
90
  function createTestExecutionResult() {
61
91
  return {
@@ -71,17 +101,6 @@ function createHookResult(result) {
71
101
  exception: result.error
72
102
  };
73
103
  }
74
- function createScenarioAnnotation(testCase) {
75
- const tag = [...new Set(testCase.tags.map(item => item.name))];
76
- return [
77
- { type: ANNOTATION_TYPES.NAME, description: testCase.name },
78
- { type: ANNOTATION_TYPES.URI, description: testCase.uri },
79
- { type: ANNOTATION_TYPES.TAGS, description: JSON.stringify(tag) }
80
- ];
81
- }
82
- function createScenarioTags(testCase) {
83
- return [...new Set(testCase.tags.map(item => item.name))];
84
- }
85
104
  function createCaseHookPayload(feature, testCase) {
86
105
  return {
87
106
  gherkinDocument: feature.gherkinDocument,
@@ -89,24 +108,29 @@ function createCaseHookPayload(feature, testCase) {
89
108
  testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID
90
109
  };
91
110
  }
92
- async function executeCaseHooks(test, hooks, world, testCase, defaultHookName, createPayload) {
93
- for (const hook of hooks) {
94
- if (!hook.appliesToTestCase(testCase)) {
95
- continue;
96
- }
97
- const hookName = hook.name ?? defaultHookName;
98
- const location = getLine(hook);
99
- await test.step(hookName, () => hook.code.apply(world, [createPayload(hook)]), location);
100
- }
111
+ /**
112
+ * Invoke hook code applying `{ timeout }` option or default timeout
113
+ */
114
+ function invokeHook(hook, thisArg, args, hookName, defaultTimeout) {
115
+ return (0, timeout_1.runWithTimeout)(() => hook.code.apply(thisArg, args), (0, timeout_1.resolveTimeout)(hook, defaultTimeout), `${hookName} hook timed out after {timeout} ms\n at ${hook.uri}:${hook.line}`);
101
116
  }
102
- async function executeStepHooks(test, hooks, world, testCase, hookName, createPayload) {
117
+ /**
118
+ * Execute hooks applicable to the test case.
119
+ * @returns `'skipped'` if any hook returned `'skipped'` (cucumber semantics)
120
+ */
121
+ async function executeHooks(test, hooks, world, testCase, defaultHookName, createPayload, defaultTimeout) {
122
+ let status;
103
123
  for (const hook of hooks) {
104
124
  if (!hook.appliesToTestCase(testCase)) {
105
125
  continue;
106
126
  }
127
+ const hookName = hook.name ?? defaultHookName;
107
128
  const location = getLine(hook);
108
- await test.step(hookName, () => hook.code.apply(world, [createPayload(hook)]), location);
129
+ const returned = await test.step(hookName, () => invokeHook(hook, world, [createPayload(hook)], hookName, defaultTimeout), location);
130
+ if (returned === 'skipped')
131
+ status = 'skipped';
109
132
  }
133
+ return status;
110
134
  }
111
135
  function createStepHookParameter(feature, testCase, pickleStep, result) {
112
136
  return {
@@ -126,103 +150,219 @@ function createCaseHookParameter(feature, testCase, result) {
126
150
  result: createHookResult(result)
127
151
  };
128
152
  }
129
- function findSingleStepDefinition(supportCodeLibrary, pickleStep, featureUri) {
153
+ function findSingleStepDefinition(supportCodeLibrary, pickleStep, stepLocation) {
130
154
  const matchingSteps = supportCodeLibrary.stepDefinitions
131
155
  .filter(stepDefinition => stepDefinition.matchesStepName(pickleStep.text));
132
156
  if (matchingSteps.length === 0) {
133
- throw new Error(`Step '${pickleStep.text}' is not defined\n at ${featureUri}`);
157
+ const snippet = (0, gherkin_1.buildSnippet)(supportCodeLibrary, pickleStep);
158
+ throw new StepStatusError([
159
+ `Step '${pickleStep.text}' is not defined`,
160
+ ` at ${stepLocation}`,
161
+ ...(snippet ? ['', 'Implement it with the following snippet:', '', snippet] : [])
162
+ ].join('\n'), 'undefined');
134
163
  }
135
164
  if (matchingSteps.length > 1) {
136
165
  const locations = matchingSteps
137
- .map(s => ` ${s.uri}:${s.line}`)
166
+ .map(s => ` ${String(s.pattern ?? '')} - ${s.uri}:${s.line}`)
138
167
  .join('\n');
139
- throw new Error(`Step '${pickleStep.text}' matches multiple step definitions:\n${locations}`);
168
+ throw new StepStatusError(`Step '${pickleStep.text}' matches multiple step definitions:\n${locations}\n at ${stepLocation}`, 'ambiguous');
140
169
  }
141
170
  return matchingSteps[0];
142
171
  }
143
- function executeBeforeAllHooks(test, hooks) {
172
+ function executeBeforeAllHooks(test, hooks, defaultTimeout) {
144
173
  if (hooks.length === 0)
145
174
  return;
146
175
  test.beforeAll(async () => {
147
176
  for (const hook of hooks) {
148
177
  const location = getLine(hook);
149
- await test.step(HOOK_NAMES.BEFORE_ALL, () => hook.code.apply({}), location);
178
+ await test.step(HOOK_NAMES.BEFORE_ALL, () => invokeHook(hook, {}, [], HOOK_NAMES.BEFORE_ALL, defaultTimeout), location);
150
179
  }
151
180
  });
152
181
  }
153
- function executeAfterAllHooks(test, hooks) {
182
+ function executeAfterAllHooks(test, hooks, defaultTimeout) {
154
183
  if (hooks.length === 0)
155
184
  return;
156
185
  test.afterAll(async () => {
157
186
  for (const hook of hooks) {
158
187
  const location = getLine(hook);
159
- await test.step(HOOK_NAMES.AFTER_ALL, () => hook.code.apply({}), location);
188
+ await test.step(HOOK_NAMES.AFTER_ALL, () => invokeHook(hook, {}, [], HOOK_NAMES.AFTER_ALL, defaultTimeout), location);
160
189
  }
161
190
  });
162
191
  }
192
+ /**
193
+ * Create scenario filter from tag expressions keyed by their source (used in error messages),
194
+ * e.g. `{ 'tags option': config.tags, 'CUCUMBER_TAGS env variable': process.env.CUCUMBER_TAGS }`.
195
+ * All expressions must match.
196
+ */
197
+ function createTagFilter(sources) {
198
+ const nodes = Object.entries(sources)
199
+ .map(([source, expression]) => [source, expression?.trim()])
200
+ .filter((entry) => !!entry[1])
201
+ .map(([source, expression]) => (0, filter_1.parseTagExpression)(expression, source));
202
+ return pickle => {
203
+ const tagNames = pickle.tags.map(tag => tag.name);
204
+ return nodes.every(node => node.evaluate(tagNames));
205
+ };
206
+ }
207
+ /**
208
+ * `@retries(n)` is applied with `test.describe.configure`, which works per feature only
209
+ */
210
+ function assertRetriesOnFeatureOnly(feature, featureTags) {
211
+ const featureTagNames = new Set(featureTags.map(tag => tag.name));
212
+ for (const testCase of feature.tests) {
213
+ const misplaced = testCase.tags.find(tag => tag.name.startsWith('@retries(') && !featureTagNames.has(tag.name));
214
+ if (misplaced) {
215
+ throw new Error(`${misplaced.name} is supported only on Feature, found on '${testCase.name}' in ${feature.uri}`);
216
+ }
217
+ }
218
+ }
219
+ /**
220
+ * Make feature titles unique, so scenarios from features with the same name do not clash
221
+ */
222
+ function uniqueFeatureTitles(features) {
223
+ const titles = features.map(feature => (0, gherkin_1.featureTitle)(feature.feature, (0, node_path_1.relative)(process.cwd(), feature.uri)));
224
+ return titles.map((title, index) => titles.indexOf(title) === index
225
+ ? title
226
+ : `${title} (${(0, node_path_1.relative)(process.cwd(), features[index].uri)})`);
227
+ }
163
228
  function defineConfig(config) {
164
229
  const { features, supportCodeLibrary } = (0, loader_1.load)(config);
165
- const fixture = new supportCodeLibrary.World({ config });
230
+ const strict = config.strict ?? true;
231
+ const worldParameters = config.worldParameters ?? {};
232
+ const tagFilter = createTagFilter({
233
+ '`tags` config option': config.tags,
234
+ 'CUCUMBER_TAGS env variable': process.env.CUCUMBER_TAGS
235
+ });
236
+ const stepLocationMode = config.stepLocation ?? 'definition';
237
+ const fixture = new supportCodeLibrary.World({ config, parameters: worldParameters });
166
238
  const test = fixture.test;
167
- executeBeforeAllHooks(test, supportCodeLibrary.beforeTestRunHookDefinitions);
168
- for (const feature of features) {
169
- const tests = feature.tests;
170
- test.describe(feature.feature, () => {
171
- for (const testCase of tests) {
172
- const tag = createScenarioTags(testCase);
173
- const annotation = createScenarioAnnotation(testCase);
174
- const testBody = async (fixtures) => {
175
- const world = new supportCodeLibrary.World({
176
- log,
177
- attach,
178
- supportCodeLibrary,
179
- config
180
- });
181
- world.init(fixtures);
182
- const result = createTestExecutionResult();
183
- await executeCaseHooks(test, supportCodeLibrary.beforeTestCaseHookDefinitions, world, testCase, HOOK_NAMES.BEFORE, () => createCaseHookPayload(feature, testCase));
184
- try {
185
- for (const pickleStep of testCase.steps) {
186
- if (result.status !== 'passed') {
187
- break;
188
- }
189
- const step = findSingleStepDefinition(supportCodeLibrary, pickleStep, feature.uri);
190
- const location = getLine(step);
191
- await test.step(stepName(pickleStep), async () => {
192
- await executeStepHooks(test, supportCodeLibrary.beforeTestStepHookDefinitions, world, testCase, HOOK_NAMES.BEFORE_STEP, () => ({
193
- gherkinDocument: feature.gherkinDocument,
194
- pickle: testCase,
195
- pickleStep
196
- }));
197
- const { parameters } = await step.getInvocationParameters({
198
- step: {
199
- text: pickleStep.text,
200
- argument: pickleStep.argument
201
- },
202
- world
203
- });
239
+ const defaultTimeout = supportCodeLibrary.defaultTimeout;
240
+ executeBeforeAllHooks(test, supportCodeLibrary.beforeTestRunHookDefinitions, defaultTimeout);
241
+ const titles = uniqueFeatureTitles(features);
242
+ features.forEach((feature, featureIndex) => {
243
+ const tests = feature.tests.filter(tagFilter);
244
+ if (tests.length === 0)
245
+ return;
246
+ const positions = (0, gherkin_1.collectPositions)(feature.gherkinDocument);
247
+ const featureTags = feature.gherkinDocument.feature?.tags ?? [];
248
+ const retries = (0, testDetails_1.resolveRetries)(featureTags);
249
+ assertRetriesOnFeatureOnly(feature, featureTags);
250
+ const featureLocation = feature.gherkinDocument.feature
251
+ ? { file: feature.uri, ...feature.gherkinDocument.feature.location, column: feature.gherkinDocument.feature.location.column ?? 1 }
252
+ : undefined;
253
+ (0, gherkin_1.callAt)(featureLocation, test.describe, [titles[featureIndex], () => {
254
+ if (retries !== undefined)
255
+ test.describe.configure({ retries });
256
+ for (const testCase of tests) {
257
+ const { tag, annotation, lock, timeout } = (0, testDetails_1.resolveTestDetails)(testCase);
258
+ const testBody = async (fixtures) => {
259
+ const world = new supportCodeLibrary.World({
260
+ log,
261
+ attach,
262
+ supportCodeLibrary,
263
+ config,
264
+ parameters: worldParameters
265
+ });
266
+ world.init(fixtures);
267
+ const testInfo = world.test.info();
268
+ if (timeout !== undefined)
269
+ testInfo.setTimeout(timeout);
270
+ const result = createTestExecutionResult();
271
+ const markStatus = (status, error) => {
272
+ if (result.status !== 'passed')
273
+ return;
274
+ result.status = status;
275
+ result.error = error;
276
+ };
277
+ // soft assertions do not throw, but fail the test
278
+ const markSoftErrors = () => {
279
+ const softError = testInfo.errors[0];
280
+ if (softError)
281
+ markStatus('failed', new Error(softError.message ?? softError.value));
282
+ };
283
+ const markError = (err) => {
284
+ if (testInfo.expectedStatus === 'skipped')
285
+ return markStatus('skipped', err);
286
+ markStatus(err instanceof StepStatusError ? err.status : 'failed', err);
287
+ };
288
+ try {
289
+ const beforeStatus = await executeHooks(test, supportCodeLibrary.beforeTestCaseHookDefinitions, world, testCase, HOOK_NAMES.BEFORE, () => createCaseHookPayload(feature, testCase), defaultTimeout);
290
+ if (beforeStatus)
291
+ markStatus(beforeStatus);
292
+ for (const pickleStep of testCase.steps) {
293
+ if (result.status === 'skipped' || result.status === 'pending') {
294
+ break;
295
+ }
296
+ const stepLocation = (0, gherkin_1.resolveLocation)(positions, feature.uri, pickleStep.astNodeIds);
297
+ let step;
298
+ let lookupError;
204
299
  try {
205
- await step.code.apply(world, parameters);
300
+ step = findSingleStepDefinition(supportCodeLibrary, pickleStep, (0, gherkin_1.formatLocation)(stepLocation));
206
301
  }
207
302
  catch (err) {
208
- result.status = 'failed';
209
- result.error = err;
210
- throw err;
303
+ // undefined or ambiguous: reported as a failed step pointing to the feature file
304
+ lookupError = err;
211
305
  }
212
- finally {
213
- await executeStepHooks(test, supportCodeLibrary.afterTestStepHookDefinitions, world, testCase, HOOK_NAMES.AFTER_STEP, () => createStepHookParameter(feature, testCase, pickleStep, result));
214
- }
215
- }, location);
306
+ const location = step && stepLocationMode === 'definition' ? getLine(step) : { location: stepLocation };
307
+ await test.step((0, gherkin_1.stepTitle)(pickleStep), async () => {
308
+ if (!step)
309
+ throw lookupError;
310
+ await executeHooks(test, supportCodeLibrary.beforeTestStepHookDefinitions, world, testCase, HOOK_NAMES.BEFORE_STEP, () => ({
311
+ gherkinDocument: feature.gherkinDocument,
312
+ pickle: testCase,
313
+ pickleStep
314
+ }), defaultTimeout);
315
+ try {
316
+ const { parameters } = await step.getInvocationParameters({
317
+ step: {
318
+ text: pickleStep.text,
319
+ argument: pickleStep.argument
320
+ },
321
+ world
322
+ });
323
+ const returned = await (0, timeout_1.runWithTimeout)(() => step.code.apply(world, parameters), (0, timeout_1.resolveTimeout)(step, defaultTimeout), `Step '${pickleStep.text}' timed out after {timeout} ms\n at ${step.uri}:${step.line}`);
324
+ if (returned === 'skipped') {
325
+ markStatus('skipped');
326
+ }
327
+ else if (returned === 'pending') {
328
+ const error = new StepStatusError(`Step '${pickleStep.text}' is pending\n at ${(0, gherkin_1.formatLocation)(stepLocation)}`, 'pending');
329
+ markStatus('pending', error);
330
+ if (strict)
331
+ throw error;
332
+ }
333
+ markSoftErrors();
334
+ }
335
+ catch (err) {
336
+ markError(err);
337
+ throw err;
338
+ }
339
+ finally {
340
+ await executeHooks(test, supportCodeLibrary.afterTestStepHookDefinitions, world, testCase, HOOK_NAMES.AFTER_STEP, () => createStepHookParameter(feature, testCase, pickleStep, result), defaultTimeout);
341
+ }
342
+ }, location);
343
+ }
216
344
  }
217
- }
218
- finally {
219
- await executeCaseHooks(test, supportCodeLibrary.afterTestCaseHookDefinitions, world, testCase, HOOK_NAMES.AFTER, () => createCaseHookParameter(feature, testCase, result));
220
- }
221
- };
222
- testBody.toString = () => fixture.init.toString();
223
- test(testCase.name, { tag, annotation }, testBody);
224
- }
225
- });
226
- }
227
- executeAfterAllHooks(test, supportCodeLibrary.afterTestRunHookDefinitions);
345
+ catch (err) {
346
+ markError(err);
347
+ throw err;
348
+ }
349
+ finally {
350
+ markSoftErrors();
351
+ await executeHooks(test, supportCodeLibrary.afterTestCaseHookDefinitions, world, testCase, HOOK_NAMES.AFTER, () => createCaseHookParameter(feature, testCase, result), defaultTimeout);
352
+ }
353
+ if (result.status === 'skipped') {
354
+ world.test.skip(true, 'Scenario skipped by step or hook');
355
+ }
356
+ if (result.status === 'pending') {
357
+ world.test.fixme(true, result.error?.message);
358
+ }
359
+ };
360
+ testBody.toString = () => fixture.init.toString();
361
+ const details = lock.length > 0 ? { tag, annotation, lock } : { tag, annotation };
362
+ const testLocation = (0, gherkin_1.resolveLocation)(positions, feature.uri, testCase.astNodeIds);
363
+ (0, gherkin_1.callAt)(testLocation, test, [testCase.name, details, testBody]);
364
+ }
365
+ }]);
366
+ });
367
+ executeAfterAllHooks(test, supportCodeLibrary.afterTestRunHookDefinitions, defaultTimeout);
228
368
  }
@@ -1,3 +1,11 @@
1
+ import { parse } from '@cucumber/tag-expressions';
2
+ /**
3
+ * Parse cucumber tag expression with an error naming its source.
4
+ * Also rejects tags without `@` (e.g. `smoke`), which parse fine but never match any scenario.
5
+ * @param expression - tag expression
6
+ * @param source - where the expression comes from, used in error messages
7
+ */
8
+ export declare function parseTagExpression(expression: string, source: string): ReturnType<typeof parse>;
1
9
  /**
2
10
  * Translate cucumber tag expression to playwright grep
3
11
  * @param {string} tagExpression
@@ -1,15 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTagExpression = parseTagExpression;
3
4
  exports.tags = tags;
4
5
  exports.filter = filter;
5
6
  const tag_expressions_1 = require("@cucumber/tag-expressions");
7
+ const OPERATORS = new Set(['and', 'or', 'not']);
8
+ /**
9
+ * Parse cucumber tag expression with an error naming its source.
10
+ * Also rejects tags without `@` (e.g. `smoke`), which parse fine but never match any scenario.
11
+ * @param expression - tag expression
12
+ * @param source - where the expression comes from, used in error messages
13
+ */
14
+ function parseTagExpression(expression, source) {
15
+ let node;
16
+ try {
17
+ node = (0, tag_expressions_1.parse)(expression);
18
+ }
19
+ catch (err) {
20
+ throw new Error(`Invalid tag expression in ${source}: ${err.message}`, { cause: err });
21
+ }
22
+ const invalidTags = expression
23
+ .replace(/\\./g, '')
24
+ .split(/[\s()]+/)
25
+ .filter(token => token && !OPERATORS.has(token) && !token.startsWith('@'));
26
+ if (invalidTags.length > 0) {
27
+ throw new Error(`Invalid tag expression in ${source}: "${expression}". ` +
28
+ `Tags must start with '@': ${invalidTags.map(tag => `'${tag}' → '@${tag}'`).join(', ')}`);
29
+ }
30
+ return node;
31
+ }
6
32
  class TagExpression extends RegExp {
7
33
  tagExpression;
8
34
  expressionNode;
9
35
  constructor(tagExpression) {
10
36
  super('');
11
37
  this.tagExpression = tagExpression;
12
- this.expressionNode = (0, tag_expressions_1.parse)(this.tagExpression);
38
+ this.expressionNode = parseTagExpression(this.tagExpression, 'tags()');
13
39
  }
14
40
  test(testName) {
15
41
  const tokens = testName.split(/\s+/);
@@ -0,0 +1,45 @@
1
+ import type { GherkinDocument, PickleStep, PickleStepArgument } from '@cucumber/messages';
2
+ import type { SupportCodeLibrary } from './types';
3
+ export interface SourceLocation {
4
+ file: string;
5
+ line: number;
6
+ column: number;
7
+ }
8
+ type Position = Omit<SourceLocation, 'file'>;
9
+ /**
10
+ * Map ast node ids (scenarios, steps, example rows, rules) of a gherkin document to their positions
11
+ */
12
+ export declare function collectPositions(gherkinDocument: GherkinDocument): Map<string, Position>;
13
+ /**
14
+ * Resolve location of a pickle or pickle step.
15
+ * The last ast node id is used, so scenario outline examples point to their example row.
16
+ */
17
+ export declare function resolveLocation(positions: Map<string, Position>, file: string, astNodeIds: readonly string[]): SourceLocation | undefined;
18
+ /**
19
+ * Call function so that the immediate caller frame reports the given source location.
20
+ * Playwright takes test and describe locations from the caller frame,
21
+ * so this makes reports, `--list` and IDE integrations point to the feature file instead of the adapter.
22
+ */
23
+ export declare function callAt<T>(location: SourceLocation | undefined, fn: (...args: any[]) => T, args: unknown[]): T;
24
+ /**
25
+ * Build step title for reports including short preview of doc string or data table argument
26
+ */
27
+ export declare function stepTitle(pickleStep: Pick<PickleStep, 'text' | 'argument'>): string;
28
+ /**
29
+ * Convert value passed to executeStep into a pickle step argument.
30
+ * Strings become doc strings, `string[][]` and DataTable instances become data tables.
31
+ */
32
+ export declare function toPickleArgument(value: unknown): {
33
+ argument?: PickleStepArgument;
34
+ extra?: unknown;
35
+ };
36
+ /**
37
+ * Build cucumber-style snippet for undefined step
38
+ */
39
+ export declare function buildSnippet(supportCodeLibrary: Pick<SupportCodeLibrary, 'parameterTypeRegistry'>, pickleStep: PickleStep): string | undefined;
40
+ export declare function formatLocation(location?: SourceLocation): string;
41
+ /**
42
+ * Feature title used for describe block; falls back to file path for unnamed features
43
+ */
44
+ export declare function featureTitle(name: string | undefined, uri: string): string;
45
+ export {};