@qavajs/playwright-runner-adapter 2.4.0 → 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 +28 -0
  2. package/README.md +160 -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 +253 -98
  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 +1 -0
  19. package/adapter/index.js +1 -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 +253 -98
  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 +1 -0
  32. package/adapter/js/index.js +1 -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 +10 -0
  36. package/adapter/js/testDetails.js +27 -1
  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 +10 -0
  46. package/adapter/testDetails.js +27 -1
  47. package/adapter/testDetails.js.map +1 -1
  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 +6 -4
package/CHANGELOG.md CHANGED
@@ -14,6 +14,34 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
14
14
 
15
15
  :microscope: - experimental
16
16
 
17
+ ## [2.5.0]
18
+ - :beetle: `requireModules` are now required as modules (e.g. `ts-node/register`) resolved from the current working directory instead of being treated as globs
19
+ - :beetle: `worldParameters` config option is passed to the world as `this.parameters`
20
+ - :beetle: `this.attach` accepts media type as a string (`attach(data, 'image/png')`), decodes `;base64` media types and supports readable streams
21
+ - :beetle: `this.log` attaches text to the report (attachment `log` on the current step) instead of printing to stdout
22
+ - :beetle: steps returning `'pending'` fail the scenario (or mark it as fixme with `strict: false`); steps and `Before` hooks returning `'skipped'` skip the scenario
23
+ - :beetle: `After` hooks receive `FAILED` status when a `Before` hook fails; soft assertion failures, undefined, ambiguous and pending steps are reported with their own status
24
+ - :beetle: features with the same name in different files no longer produce duplicate test titles; unnamed features use the file path as title
25
+ - :rocket: undefined step error includes the feature file location and a step definition snippet
26
+ - :rocket: added `tags` config option and `CUCUMBER_TAGS` env variable to select scenarios by tag expression evaluated on Gherkin tags
27
+ - :rocket: added `CUCUMBER_PATHS` env variable to override feature paths
28
+ - :rocket: `executeStep` accepts a doc string (`string`) or data table (`string[][]` / `DataTable`) argument
29
+ - :rocket: `Template` supports data tables and doc strings
30
+ - :rocket: tests and describe blocks are reported at their location in the `.feature` file
31
+ - :rocket: step titles include a preview of their doc string or data table argument
32
+ - :rocket: added `@timeout(ms)` tag to set test timeout and `@retries(n)` feature tag to set retries
33
+ - :rocket: added `withWorld<World>()` helper returning step and hook functions with typed `this`
34
+ - :rocket: added `stepLocation` config option to point steps to the feature file instead of step definitions
35
+ - :rocket: step and hook `{ timeout }` option and `setDefaultTimeout` are applied (no implicit 5s default)
36
+ - :rocket: steps run by `executeStep` and `Template` are shown as nested steps in the report
37
+ - :rocket: invalid tag expressions report their source (`tags()`, `tags` option, `CUCUMBER_TAGS`); tags without `@` are rejected
38
+ - :pencil: replaced `glob` dependency with built-in `fs.globSync`; matched files are loaded in sorted order
39
+ - :pencil: tests no longer depend on external websites
40
+ - :pencil: CI: tests cover all test projects (TS, CommonJS, ESM configs), `CUCUMBER_PATHS` / `CUCUMBER_TAGS`, type checks and a smoke test of the packed package in fresh CommonJS, ESM and TypeScript projects
41
+ - :pencil: CI: publish no longer runs e2e tests
42
+ - :pencil: README: clarified the difference between the main and `./js` entrypoints
43
+ - :pencil: documented and tested supported Cucumber APIs: `defineParameterType`, `setDefinitionFunctionWrapper`, localized Gherkin, Rule tags
44
+
17
45
  ## [2.4.0]
18
46
  - :rocket: added `@lock(name)` tag mapped to playwright test locks (`@lock(-exclusive-)` for the built-in exclusive lock)
19
47
  - :rocket: added `@skip(reason)`, `@fixme(reason)`, `@fail(reason)`, `@slow(reason)` tags mapped to playwright test modifiers (reason is optional: `@skip()`)
package/README.md CHANGED
@@ -13,15 +13,20 @@ Adapter to run [Cucumber](https://cucumber.io/) BDD tests via the [Playwright](h
13
13
  - [TestWorld](#testworld)
14
14
  - [PlaywrightWorld](#playwrightworld)
15
15
  - [Custom World](#custom-world)
16
+ - [Typed Steps](#typed-steps)
16
17
  - [Hooks](#hooks)
17
18
  - [Step Arguments](#step-arguments)
18
19
  - [Template Steps](#template-steps)
20
+ - [Step Results](#step-results)
19
21
  - [Filtering Tests](#filtering-tests)
20
22
  - [Tag Expressions](#tag-expressions)
21
23
  - [Predicate Filter](#predicate-filter)
22
24
  - [Annotations, Modifiers and Locks](#annotations-modifiers-and-locks)
23
25
  - [Parallelism](#parallelism)
24
- - [JavaScript / CommonJS Support](#javascript--commonjs-support)
26
+ - [Reports](#reports)
27
+ - [Entrypoints and JavaScript Support](#entrypoints-and-javascript-support)
28
+ - [Timeouts](#timeouts)
29
+ - [Supported Cucumber APIs](#supported-cucumber-apis)
25
30
  - [Limitations](#limitations)
26
31
 
27
32
  ---
@@ -55,10 +60,13 @@ export default defineConfig({
55
60
  import { defineConfig } from '@playwright/test';
56
61
 
57
62
  export default defineConfig({
58
- testMatch: 'cucumber.config.ts'
63
+ testMatch: 'cucumber.config.ts',
64
+ fullyParallel: true
59
65
  });
60
66
  ```
61
67
 
68
+ > All scenarios are defined in a single test file (`cucumber.config.ts`), so without `fullyParallel: true` Playwright runs the whole suite in one worker.
69
+
62
70
  **3. Run tests**
63
71
 
64
72
  ```bash
@@ -76,7 +84,11 @@ npx playwright test
76
84
  | `paths` | `string[]` | Glob patterns for `.feature` files |
77
85
  | `require` | `string[]` | Glob patterns for step definition files (CommonJS `require`) |
78
86
  | `import` | `string[]` | Glob patterns for step definition files (ESM `import`) |
79
- | `requireModules` | `string[]` | Modules to require before step definitions (e.g. `ts-node/register`) |
87
+ | `requireModules` | `string[]` | Module names or paths (resolved from the current working directory) to require before step definitions (e.g. `ts-node/register`) |
88
+ | `worldParameters` | `unknown` | Value available in steps and hooks as `this.parameters` |
89
+ | `tags` | `string` | Cucumber tag expression selecting scenarios to run |
90
+ | `strict` | `boolean` | Fail scenarios with pending steps (default `true`). When `false` they are marked as `fixme` |
91
+ | `stepLocation` | `'definition' \| 'feature'` | Location reported for steps: step definition code (default) or the step line in the `.feature` file |
80
92
 
81
93
  ```typescript
82
94
  // cucumber.config.ts
@@ -85,10 +97,19 @@ import { defineConfig } from '@qavajs/playwright-runner-adapter';
85
97
  export default defineConfig({
86
98
  paths: ['test/features/**/*.feature'],
87
99
  require: ['test/step_definitions/**/*.ts'],
88
- requireModules: [],
100
+ requireModules: ['./test/support/setup.ts'],
101
+ worldParameters: { baseUrl: 'https://example.com' },
102
+ tags: 'not @wip',
89
103
  });
90
104
  ```
91
105
 
106
+ Environment variables:
107
+
108
+ | Variable | Description |
109
+ |---|---|
110
+ | `CUCUMBER_TAGS` | Tag expression combined with the `tags` option using `and`: `CUCUMBER_TAGS='@smoke' npx playwright test` |
111
+ | `CUCUMBER_PATHS` | Comma-separated feature globs replacing `paths`: `CUCUMBER_PATHS=features/login.feature npx playwright test` |
112
+
92
113
  ---
93
114
 
94
115
  ## World Classes
@@ -106,17 +127,29 @@ import { TestWorld } from '@qavajs/playwright-runner-adapter';
106
127
  | `test` | `TestType` | Playwright `test` instance (default or custom) |
107
128
  | `expect` | `ExpectType` | Playwright `expect` instance (default or custom) |
108
129
  | `attach` | `function` | Attach files/data to the test report |
109
- | `log` | `function` | Log data to console |
110
- | `parameters` | `string` | Cucumber world parameters |
130
+ | `log` | `function` | Log text to the test report (attachment named `log` on the current step) |
131
+ | `parameters` | `unknown` | Value of the `worldParameters` config option |
111
132
  | `config` | `any` | Cucumber config object |
112
133
  | `supportCodeLibrary` | `any` | Access to the loaded support code library |
113
134
 
114
- **`executeStep(text, extraParam?)`** — programmatically invoke a step by its text:
135
+ **`attach(data, mediaTypeOrOptions?)`** — same signature as in Cucumber:
136
+
137
+ ```typescript
138
+ await this.attach('plain text'); // text/plain
139
+ await this.attach(JSON.stringify(data), 'application/json');
140
+ await this.attach(screenshot, { mediaType: 'image/png', fileName: 'screenshot.png' });
141
+ await this.attach(base64Png, 'image/png;base64'); // base64 string is decoded
142
+ await this.attach(fs.createReadStream('report.pdf'), 'application/pdf'); // streams are supported
143
+ ```
144
+
145
+ **`executeStep(text, argument?)`** — programmatically invoke a step by its text. `argument` is passed as a doc string (`string`) or data table (`string[][]` or `DataTable`); any other value is passed as an extra parameter. Each executed step is shown in the report as a nested step of the calling step, and its `{ timeout }` option applies:
115
146
 
116
147
  ```typescript
117
148
  When('composite step', async function (this: PlaywrightWorld) {
118
149
  await this.executeStep('I open the home page');
119
150
  await this.executeStep('I click "Login"');
151
+ await this.executeStep('I fill the form', [['username', 'alice'], ['password', 'secret']]);
152
+ await this.executeStep('I set the body', '{"key": "value"}');
120
153
  });
121
154
  ```
122
155
 
@@ -198,6 +231,24 @@ The `init` function receives Playwright fixtures as its argument. Its signature
198
231
 
199
232
  ---
200
233
 
234
+ ### Typed Steps
235
+
236
+ `withWorld<World>()` returns step and hook functions with `this` typed as your world, so you don't need a `this: MyWorld` annotation in every step:
237
+
238
+ ```typescript
239
+ import { withWorld } from '@qavajs/playwright-runner-adapter';
240
+
241
+ const { Given, When, Then, Before, After } = withWorld<MyWorld>();
242
+
243
+ When('I open {string}', async function (url: string) {
244
+ await this.page.goto(url); // `this` is MyWorld
245
+ });
246
+ ```
247
+
248
+ Cucumber's generic form works too: `When<MyWorld>('I open {string}', async function (url) { ... })`.
249
+
250
+ ---
251
+
201
252
  ## Hooks
202
253
 
203
254
  All standard Cucumber hooks are supported. Hooks run as named Playwright steps and appear in the trace viewer and HTML report.
@@ -310,7 +361,45 @@ When('I log in as {string}', Template((username: string) => `
310
361
  `));
311
362
  ```
312
363
 
313
- Each non-empty line in the returned string is executed as an individual step against the current world. Steps run sequentially; a failure stops the sequence.
364
+ Each non-empty line in the returned string is executed as an individual step against the current world and shown as a nested step in the report. Steps run sequentially; a failure stops the sequence.
365
+
366
+ Steps in a template can have data tables and doc strings, written the same way as in feature files:
367
+
368
+ ```typescript
369
+ When('I register as {string}', Template((username: string) => `
370
+ I fill the form
371
+ | username | ${username} |
372
+ | password | secret |
373
+ I set the body
374
+ """
375
+ {"user": "${username}"}
376
+ """
377
+ `));
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Step Results
383
+
384
+ Steps and hooks follow Cucumber semantics:
385
+
386
+ - a step returning `'pending'` fails the scenario (or marks it as `fixme` with `strict: false`)
387
+ - a step or `Before` hook returning `'skipped'` skips the rest of the scenario and marks it as skipped
388
+ - an undefined step fails the scenario with its location in the feature file and a ready-to-paste snippet:
389
+
390
+ ```
391
+ Step 'I have 5 "apples"' is not defined
392
+ at /project/features/cart.feature:12
393
+
394
+ Implement it with the following snippet:
395
+
396
+ When('I have {int} {string}', async function (int, string) {
397
+ // Write code here that turns the phrase above into concrete actions
398
+ return 'pending';
399
+ });
400
+ ```
401
+
402
+ `After` and `AfterStep` hooks receive the actual status (`PASSED`, `FAILED`, `SKIPPED`, `PENDING`, `UNDEFINED`, `AMBIGUOUS`), including failures in `Before` hooks and soft assertions.
314
403
 
315
404
  ---
316
405
 
@@ -318,7 +407,9 @@ Each non-empty line in the returned string is executed as an individual step aga
318
407
 
319
408
  ### Tag Expressions
320
409
 
321
- Use `tags` to translate a Cucumber tag expression into a Playwright `grep` pattern:
410
+ The simplest way to select scenarios is the `tags` [config option](#configuration-reference) and the `CUCUMBER_TAGS` environment variable. They are evaluated against the scenario's Gherkin tags, and scenarios that don't match are not created at all.
411
+
412
+ Use `tags()` to translate a Cucumber tag expression into a Playwright `grep` pattern:
322
413
 
323
414
  ```typescript
324
415
  // playwright.config.ts
@@ -332,6 +423,10 @@ export default defineConfig({
332
423
 
333
424
  Supports the full [Cucumber tag expression syntax](https://github.com/cucumber/tag-expressions): `and`, `or`, `not`, parentheses.
334
425
 
426
+ Invalid expressions fail fast with an error naming their source (`tags()`, the `tags` option or `CUCUMBER_TAGS`). Tags written without `@` (e.g. `smoke and not @wip`) are rejected too, because they would silently match nothing.
427
+
428
+ `tags()` is evaluated against Playwright's grep string (titles + tags), so, like Playwright itself, it treats `@words` in feature and scenario titles as tags. Use the `tags` config option when that matters.
429
+
335
430
  ### Predicate Filter
336
431
 
337
432
  Use `filter` when you need arbitrary logic to select scenarios:
@@ -362,6 +457,8 @@ Gherkin tags are translated into Playwright [test details](https://playwright.de
362
457
  | `@lock(name)` | `lock: 'name'` — scenarios sharing a lock never run concurrently |
363
458
  | `@lock(-exclusive-)` | built-in exclusive lock — runs only when no other test is running |
364
459
  | `@annotation(type,description)` | `annotation: { type, description }` — description is optional: `@annotation(flaky)` |
460
+ | `@timeout(ms)` | sets the test timeout, e.g. `@timeout(60000)`; overrides `@slow()`, the last tag wins (scenario over feature) |
461
+ | `@retries(n)` | number of retries for all scenarios of the feature — **Feature only**, using it on a Rule or Scenario is an error |
365
462
 
366
463
  Modifiers are passed as annotations of type `skip`, `fixme`, `fail`, `slow`, which Playwright applies like `test.skip()` and the other modifiers, and the optional reason is shown in the report. Plain tags such as `@skip` or `@slow` are ordinary tags and do nothing. Tags set on a `Feature` or `Rule` apply to all its scenarios.
367
464
 
@@ -416,13 +513,31 @@ export default defineConfig({
416
513
 
417
514
  Tag scenarios that must run serially with `@serial` (or any tag of your choice) and keep everything else fully parallel.
418
515
 
516
+ To run a single feature or scenario use `CUCUMBER_PATHS=features/login.feature npx playwright test` or `npx playwright test -g "Feature name"`. Passing a `.feature` path as a Playwright CLI argument does not work, because all scenarios are defined in `cucumber.config.ts`.
517
+
419
518
  > To get the current worker index inside a step, use `this.test.info().parallelIndex`.
420
519
 
421
520
  ---
422
521
 
423
- ## JavaScript / CommonJS Support
522
+ ## Reports
523
+
524
+ - Scenarios are reported at their location in the `.feature` file (Scenario Outline examples point to their example row), so reporters and `--list` show `features/login.feature:12:3`.
525
+ - Steps point to their step definition; set `stepLocation: 'feature'` to point them to the step line in the `.feature` file instead.
526
+ - Steps are reported with a short preview of their argument: `I submit the form [DataTable: | field | value | | username | alice |]`.
527
+ - Features with the same name in different files get the file path appended to their title to keep test titles unique.
528
+
529
+ ---
530
+
531
+ ## Entrypoints and JavaScript Support
532
+
533
+ The package is compiled to CommonJS JavaScript with type declarations, so it works from TypeScript and plain JavaScript, with `require` and `import`. It has two entrypoints with the same API:
424
534
 
425
- If your step definitions are plain JavaScript (`.js`), import from the `./js` entrypoint to avoid TypeScript overhead:
535
+ | Entrypoint | Difference |
536
+ |---|---|
537
+ | `@qavajs/playwright-runner-adapter` | ships source maps |
538
+ | `@qavajs/playwright-runner-adapter/js` | the same build without source maps — stack traces show the compiled `.js` files |
539
+
540
+ Plain JavaScript projects don't need any TypeScript setup with either entrypoint:
426
541
 
427
542
  ```javascript
428
543
  // cucumber.config.js
@@ -441,6 +556,40 @@ const { Given, When, Then, PlaywrightWorld } = require('@qavajs/playwright-runne
441
556
 
442
557
  ---
443
558
 
559
+ ## Timeouts
560
+
561
+ - The Playwright test timeout (`timeout` in `playwright.config.ts`, `@timeout(ms)` or `@slow()` tags) limits the whole scenario.
562
+ - A step or hook `{ timeout }` option limits that single step or hook, as in Cucumber:
563
+
564
+ ```typescript
565
+ When('I upload a big file', { timeout: 60_000 }, async function () { /* ... */ });
566
+ Before({ tags: '@db', timeout: 10_000 }, async function () { /* ... */ });
567
+ ```
568
+
569
+ - `setDefaultTimeout(ms)` sets the timeout for all steps and hooks without their own `{ timeout }`. Unlike Cucumber, there is no 5 second default: without `setDefaultTimeout` only the Playwright test timeout applies. `-1` disables the timeout for a definition.
570
+
571
+ ---
572
+
573
+ ## Supported Cucumber APIs
574
+
575
+ | API | Supported |
576
+ |---|---|
577
+ | `Given` / `When` / `Then` / `defineStep` (Cucumber expressions and RegExp) | ✅ |
578
+ | `Before` / `After` / `BeforeStep` / `AfterStep` (named, tagged) | ✅ |
579
+ | `BeforeAll` / `AfterAll` | ✅ once per worker |
580
+ | `setWorldConstructor` | ✅ |
581
+ | `defineParameterType` | ✅ |
582
+ | `setDefinitionFunctionWrapper` (incl. `wrapperOptions`) | ✅ |
583
+ | Data tables, doc strings | ✅ |
584
+ | Scenario Outline, Background, Rule (incl. Rule tags) | ✅ |
585
+ | Localized Gherkin (`# language: de`) | ✅ |
586
+ | `worldParameters`, `tags`, `strict` | ✅ |
587
+ | `setDefaultTimeout`, step and hook `{ timeout }` option | ✅ see [Timeouts](#timeouts) |
588
+ | `setParallelCanAssign` | ❌ see [Limitations](#limitations) |
589
+ | Cucumber formatters (`format`) | ❌ use Playwright [reporters](https://playwright.dev/docs/test-reporters) |
590
+
591
+ ---
592
+
444
593
  ## Limitations
445
594
 
446
595
  - **ES modules** — not supported for Node.js < 22 (where experimental ESM `require` is available). Use CommonJS or TypeScript with `ts-node`.
@@ -1,18 +1,20 @@
1
1
  import { APIRequestContext, Browser, BrowserContext, Page } from '@playwright/test';
2
- import type { CucumberAdapterConfig, SupportCodeLibrary, WorldOptions } from './types';
2
+ import type { AttachFn, CucumberAdapterConfig, SupportCodeLibrary, WorldOptions } from './types';
3
3
  export declare class TestWorld {
4
4
  log: (data: unknown) => void;
5
- attach: (data: unknown, details?: {
6
- fileName?: string;
7
- mediaType?: string;
8
- }) => Promise<void>;
5
+ attach: AttachFn;
9
6
  parameters: unknown;
10
7
  config: CucumberAdapterConfig;
11
8
  supportCodeLibrary: SupportCodeLibrary;
12
9
  test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
13
10
  expect: import("@playwright/test").Expect<{}>;
14
11
  constructor(options: WorldOptions);
15
- executeStep(this: TestWorld, text: string, extraParam?: unknown): Promise<void>;
12
+ /**
13
+ * Execute step by its text
14
+ * @param text - step text
15
+ * @param argument - doc string (`string`), data table (`string[][]` or `DataTable`) or any extra parameter
16
+ */
17
+ executeStep(this: TestWorld, text: string, argument?: unknown): Promise<void>;
16
18
  }
17
19
  export declare class PlaywrightWorld extends TestWorld {
18
20
  page: Page;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PlaywrightWorld = exports.TestWorld = void 0;
4
4
  const test_1 = require("@playwright/test");
5
+ const gherkin_1 = require("./gherkin");
6
+ const timeout_1 = require("./timeout");
5
7
  class TestWorld {
6
8
  log;
7
9
  attach;
@@ -17,7 +19,12 @@ class TestWorld {
17
19
  this.supportCodeLibrary = options.supportCodeLibrary;
18
20
  this.config = options.config;
19
21
  }
20
- async executeStep(text, extraParam) {
22
+ /**
23
+ * Execute step by its text
24
+ * @param text - step text
25
+ * @param argument - doc string (`string`), data table (`string[][]` or `DataTable`) or any extra parameter
26
+ */
27
+ async executeStep(text, argument) {
21
28
  const steps = this.supportCodeLibrary.stepDefinitions
22
29
  .filter(s => s.matchesStepName(text));
23
30
  if (steps.length === 0)
@@ -27,13 +34,19 @@ class TestWorld {
27
34
  throw new Error(`Step "${text}" matches multiple step definitions:\n${locations}`);
28
35
  }
29
36
  const step = steps[0];
30
- const { parameters } = await step.getInvocationParameters({ step: { text }, world: this });
31
- try {
32
- await step.code.apply(this, [...parameters, extraParam]);
33
- }
34
- catch (err) {
35
- throw new Error(`Step failed: ${text}`, { cause: err });
36
- }
37
+ const { argument: pickleArgument, extra } = (0, gherkin_1.toPickleArgument)(argument);
38
+ const location = step.uri ? { file: step.uri, line: step.line, column: 1 } : undefined;
39
+ // reported as a nested step, so failures inside composite steps point to the exact inner step
40
+ await this.test.step((0, gherkin_1.stepTitle)({ text, argument: pickleArgument }), async () => {
41
+ const { parameters } = await step.getInvocationParameters({ step: { text, argument: pickleArgument }, world: this });
42
+ const args = extra === undefined ? parameters : [...parameters, extra];
43
+ try {
44
+ await (0, timeout_1.runWithTimeout)(() => step.code.apply(this, args), (0, timeout_1.resolveTimeout)(step, this.supportCodeLibrary.defaultTimeout), `Step '${text}' timed out after {timeout} ms\n at ${step.uri}:${step.line}`);
45
+ }
46
+ catch (err) {
47
+ throw new Error(`Step failed: ${text}`, { cause: err });
48
+ }
49
+ }, { location });
37
50
  }
38
51
  }
39
52
  exports.TestWorld = TestWorld;
@@ -1 +1 @@
1
- {"version":3,"file":"PlaywrightWorld.js","sourceRoot":"","sources":["../src/PlaywrightWorld.ts"],"names":[],"mappings":";;;AAAA,2CAAkG;AAGlG;IACI,GAAG,CAA2B;IAC9B,MAAM,CAAyF;IAC/F,UAAU,CAAW;IACrB,MAAM,CAAyB;IAC/B,kBAAkB,CAAsB;IACxC,IAAI,GAAG,WAAI,CAAC;IACZ,MAAM,GAAG,aAAM,CAAC;IAEhB,YAAY,OAAqB;QAC7B,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAO,CAAC;QAC9B,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,kBAAkB,GAAG,OAAO,CAAC,kBAAmB,CAAC;QACtD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAO,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,WAAW,CAAkB,IAAY,EAAE,UAAoB;QACjE,MAAM,KAAK,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe;aAChD,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,kBAAkB,CAAC,CAAC;QACzE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnB,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpE,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,yCAAyC,SAAS,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3F,IAAI,CAAC;YACD,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5D,CAAC;IACL,CAAC;CACJ;;AAED,qBAA6B,SAAQ,SAAS;IAC1C,IAAI,CAAQ;IACZ,OAAO,CAAkB;IACzB,OAAO,CAAW;IAClB,WAAW,CAAU;IACrB,OAAO,CAAqB;IAE5B,uGAAuG;IACvG,IAAI,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAyF,EAAE,EAAE;QACnI,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC,CAA4B,CAAC;CACjC"}
1
+ {"version":3,"file":"PlaywrightWorld.js","sourceRoot":"","sources":["../src/PlaywrightWorld.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,21 @@
1
+ export interface TemplateStep {
2
+ text: string;
3
+ argument?: string | string[][];
4
+ }
5
+ /**
6
+ * Parse template text into steps. Supports data tables (`| a | b |` lines)
7
+ * and doc strings (`"""` or ``` blocks) placed after a step.
8
+ */
9
+ export declare function parseTemplate(scenario: string): TemplateStep[];
1
10
  /**
2
11
  * Define template step
3
- * @param {string} scenario - multiline string with steps
12
+ * @param {string} scenario - multiline string with steps; steps can have data tables and doc strings
4
13
  * @example
5
14
  * When('I click {string} and verify {string}', Template((locator, expected) => `
6
15
  * I click '${locator}'
7
16
  * I expect '${locator} > Value' to equal '${expected}'
17
+ * I fill form
18
+ * | name | John |
8
19
  * `));
9
20
  */
10
- export declare function Template(scenario: (...args: unknown[]) => string): (...args: unknown[]) => string;
21
+ export declare function Template(scenario: (...args: any[]) => string): (...args: unknown[]) => string;
@@ -1,25 +1,74 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTemplate = parseTemplate;
3
4
  exports.Template = Template;
5
+ const DOC_STRING_DELIMITERS = ['"""', '```'];
6
+ function parseRow(line) {
7
+ const content = line.replace(/^\|/, '').replace(/(?<!\\)\|$/, '');
8
+ return content
9
+ .split(/(?<!\\)\|/)
10
+ .map(cell => cell.trim().replace(/\\\|/g, '|'));
11
+ }
12
+ function stripIndent(line, indent) {
13
+ const leading = line.length - line.trimStart().length;
14
+ return line.slice(Math.min(indent, leading));
15
+ }
16
+ /**
17
+ * Parse template text into steps. Supports data tables (`| a | b |` lines)
18
+ * and doc strings (`"""` or ``` blocks) placed after a step.
19
+ */
20
+ function parseTemplate(scenario) {
21
+ const lines = scenario.split('\n');
22
+ const steps = [];
23
+ const previousStep = (line) => {
24
+ const step = steps.at(-1);
25
+ if (!step)
26
+ throw new Error(`Template argument "${line.trim()}" must follow a step`);
27
+ return step;
28
+ };
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const line = lines[i];
31
+ const trimmed = line.trim();
32
+ if (!trimmed)
33
+ continue;
34
+ const delimiter = DOC_STRING_DELIMITERS.find(d => trimmed.startsWith(d));
35
+ if (delimiter) {
36
+ const step = previousStep(line);
37
+ const indent = line.indexOf(delimiter);
38
+ const content = [];
39
+ for (i++; i < lines.length && lines[i].trim() !== delimiter; i++) {
40
+ content.push(stripIndent(lines[i], indent));
41
+ }
42
+ step.argument = content.join('\n');
43
+ continue;
44
+ }
45
+ if (trimmed.startsWith('|')) {
46
+ const step = previousStep(line);
47
+ if (!Array.isArray(step.argument))
48
+ step.argument = [];
49
+ step.argument.push(parseRow(trimmed));
50
+ continue;
51
+ }
52
+ steps.push({ text: trimmed });
53
+ }
54
+ return steps;
55
+ }
4
56
  /**
5
57
  * Define template step
6
- * @param {string} scenario - multiline string with steps
58
+ * @param {string} scenario - multiline string with steps; steps can have data tables and doc strings
7
59
  * @example
8
60
  * When('I click {string} and verify {string}', Template((locator, expected) => `
9
61
  * I click '${locator}'
10
62
  * I expect '${locator} > Value' to equal '${expected}'
63
+ * I fill form
64
+ * | name | John |
11
65
  * `));
12
66
  */
13
67
  function Template(scenario) {
14
68
  return new Proxy(scenario, {
15
69
  apply: async function (template, world, args) {
16
- const scenario = template(...args);
17
- const steps = scenario
18
- .split('\n')
19
- .map(step => step.trim())
20
- .filter(Boolean);
21
- for (const step of steps) {
22
- await world.executeStep(step);
70
+ for (const step of parseTemplate(template(...args))) {
71
+ await world.executeStep(step.text, step.argument);
23
72
  }
24
73
  },
25
74
  });
@@ -1 +1 @@
1
- {"version":3,"file":"Template.js","sourceRoot":"","sources":["../src/Template.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;GAQG;AACH,kBAAyB,QAAwC;IAC7D,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE;QACvB,KAAK,EAAE,KAAK,WAAW,QAAwC,EAAE,KAAgB,EAAE,IAAe;YAC9F,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC;YACnC,MAAM,KAAK,GAAG,QAAQ;iBACjB,KAAK,CAAC,IAAI,CAAC;iBACX,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;iBACxB,MAAM,CAAC,OAAO,CAAC,CAAC;YACrB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACvB,MAAM,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YAClC,CAAC;QACL,CAAC;KACJ,CAAC,CAAC;AACP,CAAC"}
1
+ {"version":3,"file":"Template.js","sourceRoot":"","sources":["../src/Template.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,9 @@
1
+ import type { Pickle } from '@cucumber/messages';
1
2
  import type { CucumberAdapterConfig } from './types';
3
+ /**
4
+ * Create scenario filter from tag expressions keyed by their source (used in error messages),
5
+ * e.g. `{ 'tags option': config.tags, 'CUCUMBER_TAGS env variable': process.env.CUCUMBER_TAGS }`.
6
+ * All expressions must match.
7
+ */
8
+ export declare function createTagFilter(sources: Record<string, string | undefined>): (pickle: Pickle) => boolean;
2
9
  export declare function defineConfig(config: CucumberAdapterConfig): void;