@qavajs/cypress-runner-adapter 1.9.1 → 1.10.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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @qavajs/cypress-runner-adapter
2
- Adapter to run Gherkin tests via cypress test runner
2
+
3
+ Cypress preprocessor that compiles Gherkin `.feature` files into Cypress/Mocha test suites, enabling full BDD-style testing with Cucumber syntax inside Cypress.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@qavajs/cypress-runner-adapter)](https://www.npmjs.com/package/@qavajs/cypress-runner-adapter)
6
+ [![license](https://img.shields.io/npm/l/@qavajs/cypress-runner-adapter)](LICENSE)
3
7
 
4
8
  ## Installation
5
9
 
@@ -7,7 +11,9 @@ Adapter to run Gherkin tests via cypress test runner
7
11
  npm install @qavajs/cypress-runner-adapter
8
12
  ```
9
13
 
10
- ## Basic Configuration
14
+ ## Configuration
15
+
16
+ Register the preprocessor in `cypress.config.js`:
11
17
 
12
18
  ```javascript
13
19
  const { defineConfig } = require('cypress');
@@ -18,39 +24,171 @@ module.exports = defineConfig({
18
24
  specPattern: 'cypress/feature/**/*.feature',
19
25
  supportFile: 'cypress/support/e2e.js',
20
26
  setupNodeEvents(on, config) {
21
- on('file:preprocessor', cucumber)
27
+ on('file:preprocessor', cucumber);
22
28
  },
23
29
  },
24
30
  });
25
31
  ```
26
32
 
27
- `support/e2e.js` is entry point with step definition;
33
+ ## Step Definitions
34
+
35
+ Define steps in your support file (e.g. `cypress/support/e2e.js`):
28
36
 
29
37
  ```javascript
30
- import { When, setWorldConstructor } from '@qavajs/cypress-runner-adapter';
38
+ import { Given, When, Then, setWorldConstructor, World } from '@qavajs/cypress-runner-adapter';
31
39
 
32
- class World {
33
- parameter = 42;
40
+ class CustomWorld extends World {
41
+ constructor(options) {
42
+ super(options);
43
+ this.myValue = null;
44
+ }
34
45
  }
35
46
 
36
- setWorldConstructor(World);
47
+ setWorldConstructor(CustomWorld);
37
48
 
38
- When('open {string} url', function (url) {
49
+ Given('I navigate to {string}', function (url) {
39
50
  cy.visit(url);
40
51
  });
52
+
53
+ When('I click {string}', function (selector) {
54
+ cy.get(selector).click();
55
+ });
56
+
57
+ Then('{string} should be visible', function (selector) {
58
+ cy.get(selector).should('be.visible');
59
+ });
60
+ ```
61
+
62
+ ## Hooks
63
+
64
+ All standard Cucumber hooks are supported. Hooks receive a `params` object with context about the current test.
65
+
66
+ ```javascript
67
+ import { Before, After, BeforeStep, AfterStep, BeforeAll, AfterAll } from '@qavajs/cypress-runner-adapter';
68
+
69
+ BeforeAll(function () {
70
+ // runs once before all tests
71
+ });
72
+
73
+ Before(function ({ gherkinDocument, pickle, testCaseStartedId }) {
74
+ // runs before each scenario
75
+ });
76
+
77
+ Before({ tags: '@login' }, function () {
78
+ // runs before scenarios tagged with @login
79
+ });
80
+
81
+ Before({ name: 'setup' }, function () {
82
+ // named hook
83
+ });
84
+
85
+ BeforeStep(function ({ testStepId }) {
86
+ // runs before each step
87
+ });
88
+
89
+ AfterStep(function ({ result }) {
90
+ // runs after each step
91
+ });
92
+
93
+ After(function ({ result, error }) {
94
+ // runs after each scenario
95
+ });
96
+
97
+ AfterAll(function () {
98
+ // runs once after all tests
99
+ });
100
+ ```
101
+
102
+ ## Parameter Types
103
+
104
+ Define custom parameter types using `defineParameterType`:
105
+
106
+ ```javascript
107
+ import { defineParameterType } from '@qavajs/cypress-runner-adapter';
108
+
109
+ defineParameterType({
110
+ name: 'color',
111
+ regexp: /(red|blue|green)/,
112
+ transformer: color => color.toUpperCase()
113
+ });
114
+
115
+ // usage in step definition:
116
+ // When('I select {color} theme', function (color) { ... });
117
+ ```
118
+
119
+ ## World
120
+
121
+ The `World` class is instantiated for each scenario and is available as `this` inside step definitions and hooks. Extend it to share state across steps within a scenario.
122
+
123
+ | Property / Method | Description |
124
+ |---|---|
125
+ | `this.log(message)` | Log a message to the Cypress command log |
126
+ | `this.attach(data)` | Attach data to the test report |
127
+ | `this.link(url)` | Attach a link to the test report |
128
+ | `this.executeStep(text)` | Programmatically execute a step by its text |
129
+
130
+ ```javascript
131
+ import { When, World, setWorldConstructor } from '@qavajs/cypress-runner-adapter';
132
+
133
+ class AppWorld extends World {
134
+ constructor(options) {
135
+ super(options);
136
+ this.userId = null;
137
+ }
138
+ }
139
+
140
+ setWorldConstructor(AppWorld);
141
+
142
+ When('I store user {string}', function (id) {
143
+ this.userId = id;
144
+ });
145
+
146
+ When('I use stored user', function () {
147
+ cy.log(this.userId);
148
+ });
149
+
150
+ When('I execute another step', function () {
151
+ this.executeStep('I navigate to "https://example.com"');
152
+ });
41
153
  ```
42
154
 
43
- ## Tags
44
- Test can be filtered using Cucumber tag expressions provided via environment variable `TAGS`
155
+ ## Template Steps
156
+
157
+ `Template` composes a step from other steps using a multiline string. The function receives the same arguments as the step and returns the steps to execute:
158
+
159
+ ```javascript
160
+ import { When, Template } from '@qavajs/cypress-runner-adapter';
161
+
162
+ When('I search for {string} on Wikipedia', Template(term => `
163
+ I navigate to 'https://en.wikipedia.org/'
164
+ I search '${term}'
165
+ `));
45
166
  ```
46
- TAGS='@first and @second' npx cypress run
167
+
168
+ ## Tag Filtering
169
+
170
+ Filter scenarios using Cucumber tag expressions via the `TAGS` environment variable:
171
+
172
+ ```bash
173
+ TAGS='@smoke' npx cypress run
174
+ TAGS='@smoke and not @slow' npx cypress run
175
+ TAGS='@login or @auth' npx cypress run
47
176
  ```
48
177
 
49
- ## Translation Mode
50
- Gherkin tests can be translated in different modes
51
- - `describe` - default mode. Scenario will be translated as `describe`, each step will be translated as `it`
52
- - `it` - Scenario will be translated as `it`
178
+ ## Translation Modes
179
+
180
+ Controls how Gherkin scenarios are mapped to Mocha constructs. Set via the `MODE` environment variable.
181
+
182
+ | Mode | Scenario maps to | Steps map to |
183
+ |---|---|---|
184
+ | `describe` (default) | `describe` | `it` |
185
+ | `it` | `it` | _(inline)_ |
53
186
 
54
187
  ```bash
188
+ # default (describe) mode
189
+ npx cypress run
190
+
191
+ # it mode
192
+ MODE=it npx cypress run
55
193
  MODE=it npx cypress open
56
- ```
194
+ ```
package/adapter/index.js CHANGED
@@ -19,7 +19,7 @@ function adapter(testCases) {
19
19
  }
20
20
 
21
21
  module.exports = async function cucumber(file) {
22
- const { filePath, outputPath, shouldWatch } = file;
22
+ const { filePath, outputPath } = file;
23
23
  if (!filePath.endsWith('.feature')) {
24
24
  return webpackPreprocessor(file);
25
25
  }
@@ -3,6 +3,21 @@ module.exports = function makeMochaTest(tests) {
3
3
  cy.log(data);
4
4
  }
5
5
 
6
+ function attach(data, options) {
7
+ const type = typeof options === 'string' ? options : options?.mediaType ? options.mediaType : 'text/plain';
8
+ const name = options?.name ?? 'attachment';
9
+ const src = typeof data === 'string' && data.startsWith('data:')
10
+ ? data
11
+ : `data:${type};base64,${btoa(unescape(encodeURIComponent(String(data))))}`;
12
+ cy.then(() => {
13
+ Cypress.log({
14
+ displayName: name,
15
+ message: type,
16
+ consoleProps: () => ({ name, src })
17
+ });
18
+ });
19
+ }
20
+
6
21
  function keyword(step) {
7
22
  switch (step.type) {
8
23
  case 'Context': return 'Given';
@@ -63,7 +78,7 @@ module.exports = function makeMochaTest(tests) {
63
78
  describe('Scenario: ' + test.name, { testIsolation: false }, function () {
64
79
  const world = new supportCodeLibrary.World({
65
80
  log,
66
- attach: log,
81
+ attach,
67
82
  link: log
68
83
  });
69
84
  world.executeStep = executeStepByText;
@@ -72,12 +87,14 @@ module.exports = function makeMochaTest(tests) {
72
87
  let duration = 0;
73
88
  let exception;
74
89
  let message;
90
+ let willBeRetried = false;
75
91
  afterEach(function () {
76
92
  if (this.currentTest.state !== 'passed') {
77
93
  skip = true;
78
94
  }
79
95
  duration += this.currentTest.duration;
80
96
  if (this.currentTest.state === 'failed') {
97
+ willBeRetried = this.currentTest.currentRetry() < this.currentTest._retries;
81
98
  status = this.currentTest.state;
82
99
  exception = this.currentTest.err;
83
100
  message = this.currentTest.err?.message;
@@ -150,7 +167,7 @@ module.exports = function makeMochaTest(tests) {
150
167
  message
151
168
  },
152
169
  gherkinDocument: tests,
153
- willBeRetried: false,
170
+ willBeRetried,
154
171
  testCaseStartedId: test.id,
155
172
  }]);
156
173
  });
@@ -3,6 +3,21 @@ module.exports = function makeMochaTest(tests) {
3
3
  cy.log(data);
4
4
  }
5
5
 
6
+ function attach(data, options) {
7
+ const type = typeof options === 'string' ? options : options?.mediaType ? options.mediaType : 'text/plain';
8
+ const name = options?.name ?? 'attachment';
9
+ const src = typeof data === 'string' && data.startsWith('data:')
10
+ ? data
11
+ : `data:${type};base64,${btoa(unescape(encodeURIComponent(String(data))))}`;
12
+ cy.then(() => {
13
+ Cypress.log({
14
+ displayName: name,
15
+ message: type,
16
+ consoleProps: () => ({ name, src })
17
+ });
18
+ });
19
+ }
20
+
6
21
  function keyword(step) {
7
22
  switch (step.type) {
8
23
  case 'Context':
@@ -100,7 +115,7 @@ module.exports = function makeMochaTest(tests) {
100
115
  this.currentTest.body = renderGherkinTest(test.steps);
101
116
  const world = this.world = new supportCodeLibrary.World({
102
117
  log,
103
- attach: log,
118
+ attach,
104
119
  link: log
105
120
  });
106
121
  for (const beforeTest of supportCodeLibrary.beforeTestCaseHookDefinitions) {
@@ -121,6 +136,7 @@ module.exports = function makeMochaTest(tests) {
121
136
  const test = findTest(tests, this.currentTest.title);
122
137
  const world = this.world;
123
138
  const result = getResult(this.currentTest);
139
+ const willBeRetried = result.status === 'failed' && this.currentTest.currentRetry() < this.currentTest._retries;
124
140
  // corner case to complete AfterStep if test is failed
125
141
  if (result.status === 'failed' && this.step) {
126
142
  for (const afterStep of supportCodeLibrary.afterTestStepHookDefinitions) {
@@ -144,7 +160,7 @@ module.exports = function makeMochaTest(tests) {
144
160
  result,
145
161
  gherkinDocument: tests,
146
162
  testCaseStartedId: test.id,
147
- willBeRetried: false
163
+ willBeRetried
148
164
  }]);
149
165
  });
150
166
  }
package/index.d.ts CHANGED
@@ -13,7 +13,7 @@ declare type ParameterTypeOption = {
13
13
  transformer?: Function,
14
14
  useForSnippets?: boolean
15
15
  }
16
- declare interface IWorld {}
16
+ export function defineStep(keyword: string, expression: Expression, fn: Function): void;
17
17
  export function Given(expression: Expression, fn: Function): void;
18
18
  export function When(expression: Expression, fn: Function): void;
19
19
  export function Then(expression: Expression, fn: Function): void;
@@ -29,12 +29,16 @@ export function BeforeAll(fn: Function): void;
29
29
  export function AfterAll(fn: Function): void;
30
30
  export function setWorldConstructor(world: IWorld): void;
31
31
  export function defineParameterType(option: ParameterTypeOption): void;
32
+ export function setDefaultTimeout(milliseconds: number): void;
32
33
  export function Template(template: (...args: any[]) => string): () => void;
33
34
  export class World {
35
+ log: (...args: any) => void;
36
+ attach: (...args: any) => void;
37
+ link: (...args: any) => void;
38
+ executeStep(text: string, argument?: any): void;
34
39
  constructor(options: {
35
40
  log: (...args: any) => void;
36
41
  attach: (...args: any) => void;
37
42
  link: (...args: any) => void;
38
- executeStep: (...args: any) => Promise<any>;
39
43
  });
40
44
  }
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@qavajs/cypress-runner-adapter",
3
- "version": "1.9.1",
4
- "main": "index.js",
3
+ "version": "1.10.0",
4
+ "main": "supportCodeLibrary/index.js",
5
+ "exports": {
6
+ ".": "./supportCodeLibrary/index.js",
7
+ "./adapter": "./adapter/index.js"
8
+ },
5
9
  "files": [
6
10
  "adapter",
7
11
  "supportCodeLibrary",
8
- "index.js",
9
12
  "index.d.ts",
10
- "adapter.js",
11
13
  "adapter.d.ts"
12
14
  ],
13
15
  "scripts": {
@@ -31,13 +33,12 @@
31
33
  "homepage": "https://qavajs.github.io/docs/intro",
32
34
  "dependencies": {
33
35
  "@cucumber/cucumber-expressions": "^19.0.0",
34
- "@cucumber/gherkin": "^39.0.0",
36
+ "@cucumber/gherkin": "^39.1.0",
35
37
  "@cucumber/tag-expressions": "^9.1.0",
36
- "@cypress/webpack-preprocessor": "^7.0.2"
38
+ "@cypress/webpack-preprocessor": "^7.1.0"
37
39
  },
38
40
  "devDependencies": {
39
- "cypress": "^15.12.0",
40
- "mochawesome": "^7.1.4"
41
+ "cypress": "^15.14.2"
41
42
  },
42
43
  "keywords": [
43
44
  "cypress",
@@ -108,6 +108,10 @@ export function defineParameterType(options) {
108
108
  supportCodeLibrary.parameterTypeRegistry.defineSourcedParameterType(parameterType, {})
109
109
  }
110
110
 
111
+ export function setDefaultTimeout(milliseconds) {
112
+ Cypress.config('defaultCommandTimeout', milliseconds);
113
+ }
114
+
111
115
  /**
112
116
  * Define template step
113
117
  * @param {() => string} scenario - multiline string with steps
@@ -13,8 +13,4 @@ export class SourcedParameterTypeRegistry extends ParameterTypeRegistry {
13
13
  this.defineParameterType(parameterType)
14
14
  this.parameterTypeToSource.set(parameterType, source)
15
15
  }
16
-
17
- lookupSource(parameterType) {
18
- return this.parameterTypeToSource.get(parameterType)
19
- }
20
16
  }
@@ -21,8 +21,6 @@ export default class StepDefinition extends Definition {
21
21
  parameters.push(argumentParameter)
22
22
  }
23
23
  return {
24
- getInvalidCodeLengthMessage: () =>
25
- this.baseGetInvalidCodeLengthMessage(parameters),
26
24
  parameters,
27
25
  validCodeLengths: [parameters.length, parameters.length + 1],
28
26
  }
@@ -5,7 +5,7 @@ export default class TestCaseHookDefinition extends Definition {
5
5
 
6
6
  constructor(data) {
7
7
  super(data)
8
- this.name = data.options.name
8
+ this.name = data.options.name ?? 'Hook'
9
9
  this.tagExpression = data.options.tags
10
10
  this.pickleTagFilter = new PickleTagFilter(data.options.tags)
11
11
  }
@@ -13,13 +13,4 @@ export default class TestCaseHookDefinition extends Definition {
13
13
  appliesToTestCase(pickle) {
14
14
  return this.pickleTagFilter.matchesAllTagExpressions(pickle)
15
15
  }
16
-
17
- async getInvocationParameters({ hookParameter }) {
18
- return {
19
- getInvalidCodeLengthMessage: () =>
20
- this.buildInvalidCodeLengthMessage('0 or 1', '2'),
21
- parameters: [hookParameter],
22
- validCodeLengths: [0, 1, 2],
23
- }
24
- }
25
16
  }
@@ -11,13 +11,4 @@ export default class TestStepHookDefinition extends Definition {
11
11
  appliesToTestCase(pickle) {
12
12
  return this.pickleTagFilter.matchesAllTagExpressions(pickle)
13
13
  }
14
-
15
- async getInvocationParameters({ hookParameter }) {
16
- return {
17
- getInvalidCodeLengthMessage: () =>
18
- this.buildInvalidCodeLengthMessage('0 or 1', '2'),
19
- parameters: [hookParameter],
20
- validCodeLengths: [0, 1, 2],
21
- }
22
- }
23
14
  }
package/adapter.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./adapter/index');
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./supportCodeLibrary');