@qavajs/cypress-runner-adapter 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Change Log
2
+
3
+ All notable changes to the "@qavajs/cypress-runner-adapter" will be documented in this file.
4
+
5
+ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
6
+
7
+ :rocket: - new feature
8
+ :beetle: - bugfix
9
+ :x: - deprecation/removal
10
+ :pencil: - chore
11
+ :microscope: - experimental
12
+
13
+ ## [0.1.0]
14
+ - :rocket: initial implementation
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 @qavajs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @qavajs/cypress-runner-adapter
2
+ Adapter to run cucumberjs tests via cypress test runner
3
+
4
+ ## Installation
5
+
6
+ `npm install @qavajs/cypress-runner-adapter`
7
+
8
+ ## Basic Configuration
9
+
10
+ ```javascript
11
+ const { defineConfig } = require('cypress');
12
+ const cucumber = require('@qavajs/cypress-runner-adapter/adapter');
13
+
14
+ module.exports = defineConfig({
15
+ e2e: {
16
+ specPattern: 'cypress/feature/**/*.feature',
17
+ supportFile: 'cypress/support/e2e.js',
18
+ setupNodeEvents(on, config) {
19
+ on('file:preprocessor', cucumber)
20
+ },
21
+ },
22
+ });
23
+ ```
24
+
25
+ `support/e2e.js` is entry point with step definition;
26
+
27
+ ```javascript
28
+ import { When, setWorldConstructor } from '@qavajs/cypress-runner-adapter';
29
+
30
+ class World {
31
+
32
+ }
33
+ setWorldConstructor(World);
34
+
35
+ When('open {string} url', function (url) {
36
+ cy.visit(url);
37
+ });
38
+ ```
@@ -0,0 +1,128 @@
1
+ const {
2
+ ensureFileSync,
3
+ writeFileSync,
4
+ readFileSync,
5
+ } = require('fs-extra');
6
+ const { randomUUID } = require('node:crypto');
7
+ const { AstBuilder, compile, GherkinClassicTokenMatcher, Parser } = require('@cucumber/gherkin');
8
+ const cyBrowserify = require('@cypress/browserify-preprocessor')()
9
+
10
+ const uuidFn = () => randomUUID();
11
+ const builder = new AstBuilder(uuidFn)
12
+ const matcher = new GherkinClassicTokenMatcher();
13
+ const parser = new Parser(builder, matcher)
14
+
15
+ function adapter(testCases) {
16
+ return `
17
+ const tests = ${JSON.stringify(testCases)};
18
+
19
+ function keyword(step) {
20
+ switch (step.type) {
21
+ case 'Context': return 'Given';
22
+ case 'Action': return 'When';
23
+ case 'Outcome': return 'Then';
24
+ default: return 'Step';
25
+ }
26
+ }
27
+
28
+ function executeStep(pickle, world) {
29
+ if (pickle.argument && pickle.argument.dataTable) {
30
+ Cypress.log({ displayName: 'DataTable', message: pickle.argument.dataTable })
31
+ }
32
+ if (pickle.argument && pickle.argument.docString) {
33
+ Cypress.log({ displayName: 'Multiline', message: pickle.argument.docString.content })
34
+ }
35
+ const steps = supportCodeLibrary.stepDefinitions
36
+ .filter(stepDefinition => stepDefinition.matchesStepName(pickle.text));
37
+ if (steps.length === 0) throw new Error(\`Step '\${pickle.text}' is not defined\`);
38
+ if (steps.length > 1) throw new Error(\`'\${pickle.text}' matches multiple step definitions\`);
39
+ const [step] = steps;
40
+ const { parameters } = step.getInvocationParameters({
41
+ step: {
42
+ text: pickle.text,
43
+ argument: pickle.argument
44
+ },
45
+ world
46
+ });
47
+ step.code.apply(world, parameters);
48
+ }
49
+ for (const test of tests) {
50
+ describe('Scenario: ' + test.name, { testIsolation: false }, function () {
51
+ const world = new supportCodeLibrary.World();
52
+ let skip = false;
53
+ let result = 'passed';
54
+ afterEach(function() {
55
+ if (this.step) {
56
+ for (const afterStep of supportCodeLibrary.afterTestStepHookDefinitions) {
57
+ if (afterStep.appliesToTestCase(this.step)) {
58
+ afterStep.code.apply(world, [{
59
+ pickle: test,
60
+ pickleStep: this.step,
61
+ gherkinDocument: tests,
62
+ result: this.currentTest.state
63
+ }])
64
+ }
65
+ }
66
+ }
67
+ if (this.currentTest.state !== 'passed') {
68
+ skip = true;
69
+ }
70
+ result = this.currentTest.state;
71
+ });
72
+ for (const beforeTest of supportCodeLibrary.beforeTestCaseHookDefinitions) {
73
+ if (beforeTest.appliesToTestCase(test)) {
74
+ it(beforeTest.name, function () {
75
+ if (skip) return this.skip();
76
+ beforeTest.code.apply(world, [{
77
+ pickle: test,
78
+ gherkinDocument: tests,
79
+ willBeRetried: false
80
+ }]);
81
+ });
82
+ }
83
+ }
84
+ for (const step of test.steps) {
85
+ it(keyword(step) + ': ' + step.text, function () {
86
+ this.step = step;
87
+ if (skip) return this.skip();
88
+ for (const beforeStep of supportCodeLibrary.beforeTestStepHookDefinitions) {
89
+ if (beforeStep.appliesToTestCase(step)) {
90
+ beforeStep.code.apply(world, [{
91
+ pickle: test,
92
+ pickleStep: step,
93
+ gherkinDocument: tests
94
+ }]);
95
+ }
96
+ }
97
+ executeStep(step, world);
98
+ })
99
+ }
100
+ for (const afterTest of supportCodeLibrary.afterTestCaseHookDefinitions) {
101
+ if (afterTest.appliesToTestCase(test)) {
102
+ it(afterTest.name, function () {
103
+ afterTest.code.apply(world, [{
104
+ pickle: test,
105
+ result,
106
+ gherkinDocument: tests,
107
+ willBeRetried: false
108
+ }])
109
+ });
110
+ }
111
+ }
112
+ });
113
+ }
114
+ `;
115
+ }
116
+
117
+ module.exports = async function cucumber(file) {
118
+ const { filePath, outputPath, shouldWatch } = file;
119
+ if (!filePath.endsWith('.feature')) {
120
+ return cyBrowserify(file)
121
+ }
122
+ const gherkinDocument = parser.parse(readFileSync(filePath, 'utf-8'));
123
+ const testCases = compile(gherkinDocument, filePath, uuidFn);
124
+ ensureFileSync(outputPath);
125
+ writeFileSync(outputPath, adapter(testCases), 'utf-8');
126
+
127
+ return outputPath
128
+ }
package/adapter.d.ts ADDED
@@ -0,0 +1 @@
1
+ export default function (file: any): void;
package/adapter.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./adapter/index');
package/index.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ declare type Expression = string | RegExp;
2
+ declare type TestHookOptions = {
3
+ tags?: string,
4
+ name?: string
5
+ };
6
+ declare type StepHookOptions = {
7
+ tags?: string
8
+ };
9
+ declare type ParameterTypeOption = {
10
+ name: string,
11
+ preferForRegexpMatch?: boolean,
12
+ regexp: RegExp,
13
+ transformer?: Function,
14
+ useForSnippets?: boolean
15
+ }
16
+ declare interface IWorld {}
17
+ export function Given(expression: Expression, fn: Function): void;
18
+ export function When(expression: Expression, fn: Function): void;
19
+ export function Then(expression: Expression, fn: Function): void;
20
+ export function Before(fn: Function): void;
21
+ export function Before(options: TestHookOptions, fn: Function): void;
22
+ export function After(fn: Function): void;
23
+ export function After(options: TestHookOptions, fn: Function): void;
24
+ export function BeforeStep(fn: Function): void;
25
+ export function BeforeStep(options: StepHookOptions, fn: Function): void;
26
+ export function AfterStep(fn: Function): void;
27
+ export function AfterStep(options: StepHookOptions, fn: Function): void;
28
+ export function setWorldConstructor(world: IWorld): void;
29
+ export function defineParameterType(option: ParameterTypeOption): void;
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./supportCodeLibrary');
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@qavajs/cypress-runner-adapter",
3
+ "version": "0.1.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "debug": "cypress open --config-file test/cypress.config.js",
7
+ "test": "cypress run --config-file test/cypress.config.js"
8
+ },
9
+ "author": "Alexandr Galichenko",
10
+ "license": "MIT",
11
+ "description": "feature file preprocessor",
12
+ "dependencies": {
13
+ "@cucumber/cucumber-expressions": "^17.1.0",
14
+ "@cucumber/gherkin": "^28.0.0",
15
+ "@cucumber/tag-expressions": "^6.1.0",
16
+ "@cypress/browserify-preprocessor": "^3.0.2",
17
+ "fs-extra": "^11.2.0"
18
+ },
19
+ "devDependencies": {
20
+ "cypress": "^13.11.0"
21
+ }
22
+ }
@@ -0,0 +1,14 @@
1
+ import {ParameterType} from '@cucumber/cucumber-expressions'
2
+
3
+ export function buildParameterType({ name, regexp, transformer, useForSnippets, preferForRegexpMatch }) {
4
+ if (typeof useForSnippets !== 'boolean') useForSnippets = true
5
+ if (typeof preferForRegexpMatch !== 'boolean') preferForRegexpMatch = false
6
+ return new ParameterType(
7
+ name,
8
+ regexp,
9
+ null,
10
+ transformer,
11
+ useForSnippets,
12
+ preferForRegexpMatch
13
+ )
14
+ }
@@ -0,0 +1,52 @@
1
+ export default class Data_table {
2
+ constructor(sourceTable) {
3
+ if (sourceTable instanceof Array) {
4
+ this.rawTable = sourceTable
5
+ } else {
6
+ this.rawTable = sourceTable.rows.map((row) =>
7
+ row.cells.map((cell) => cell.value)
8
+ )
9
+ }
10
+ }
11
+
12
+ hashes() {
13
+ const copy = this.raw()
14
+ const keys = copy[0]
15
+ const valuesArray = copy.slice(1)
16
+ return valuesArray.map((values) => {
17
+ const rowObject = {}
18
+ keys.forEach((key, index) => (rowObject[key] = values[index]))
19
+ return rowObject
20
+ })
21
+ }
22
+
23
+ raw() {
24
+ return this.rawTable.slice(0)
25
+ }
26
+
27
+ rows() {
28
+ const copy = this.raw()
29
+ copy.shift()
30
+ return copy
31
+ }
32
+
33
+ rowsHash() {
34
+ const rows = this.raw()
35
+ const everyRowHasTwoColumns = rows.every((row) => row.length === 2)
36
+ if (!everyRowHasTwoColumns) {
37
+ throw new Error(
38
+ 'rowsHash can only be called on a data table where all rows have exactly two columns'
39
+ )
40
+ }
41
+ const result = {}
42
+ rows.forEach((x) => (result[x[0]] = x[1]))
43
+ return result
44
+ }
45
+
46
+ transpose() {
47
+ const transposed = this.rawTable[0].map((x, i) =>
48
+ this.rawTable.map((y) => y[i])
49
+ )
50
+ return new Data_table(transposed)
51
+ }
52
+ }
@@ -0,0 +1,5 @@
1
+ export default class Definition {
2
+ constructor(data) {
3
+ this.code = data.code;
4
+ }
5
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ CucumberExpression,
3
+ RegularExpression,
4
+ } from '@cucumber/cucumber-expressions'
5
+ import StepDefinition from './step_definition';
6
+ import {SourcedParameterTypeRegistry} from './sourced_parameter_type_registry';
7
+ import TestCaseHookDefinition from './test_case_hook_definition';
8
+ import TestStepHookDefinition from './test_step_hook_definition';
9
+ import {buildParameterType} from './build_parameter_type';
10
+ class World {}
11
+
12
+ const supportCodeLibrary = {
13
+ stepDefinitions: [],
14
+ afterTestCaseHookDefinitions: [],
15
+ afterTestRunHookDefinitions: [],
16
+ afterTestStepHookDefinitions: [],
17
+ beforeTestCaseHookDefinitions: [],
18
+ beforeTestRunHookDefinitions: [],
19
+ beforeTestStepHookDefinitions: [],
20
+ World,
21
+ parameterTypeRegistry: new SourcedParameterTypeRegistry()
22
+ };
23
+
24
+ window.supportCodeLibrary = supportCodeLibrary;
25
+ export function defineStep(keyword, pattern, code) {
26
+ const expression = typeof pattern === 'string'
27
+ ? new CucumberExpression(pattern, supportCodeLibrary.parameterTypeRegistry)
28
+ : new RegularExpression(pattern, supportCodeLibrary.parameterTypeRegistry);
29
+ supportCodeLibrary.stepDefinitions.push(new StepDefinition({
30
+ keyword,
31
+ expression,
32
+ pattern,
33
+ code
34
+ }));
35
+ }
36
+
37
+ export function When(pattern, code) {
38
+ defineStep('When', pattern, code);
39
+ }
40
+
41
+ export function Then(pattern, code) {
42
+ defineStep('Then', pattern, code);
43
+ }
44
+
45
+ export function Given(pattern, code) {
46
+ defineStep('Given', pattern, code);
47
+ }
48
+
49
+ export function Before(optionsOrCode, code) {
50
+ const defaultOptions = { name: 'Before' };
51
+ const options = code ? Object.assign(defaultOptions, optionsOrCode) : defaultOptions;
52
+ const handler = code ?? optionsOrCode;
53
+ supportCodeLibrary.beforeTestCaseHookDefinitions.push(new TestCaseHookDefinition({
54
+ code: handler,
55
+ options
56
+ }));
57
+ }
58
+
59
+ export function After(optionsOrCode, code) {
60
+ const defaultOptions = { name: 'After' };
61
+ const options = code ? Object.assign(defaultOptions, optionsOrCode) : defaultOptions;
62
+ const handler = code ?? optionsOrCode;
63
+ supportCodeLibrary.afterTestCaseHookDefinitions.push(new TestCaseHookDefinition({
64
+ code: handler,
65
+ options
66
+ }));
67
+ }
68
+
69
+ export function BeforeStep(optionsOrCode, code) {
70
+ const options = code ? optionsOrCode : { name: 'BeforeStep' };
71
+ const handler = code ?? optionsOrCode;
72
+ supportCodeLibrary.beforeTestStepHookDefinitions.push(new TestStepHookDefinition({
73
+ code: handler,
74
+ options
75
+ }));
76
+ }
77
+
78
+ export function AfterStep(optionsOrCode, code) {
79
+ const options = code ? optionsOrCode : { name: 'AfterStep' };
80
+ const handler = code ?? optionsOrCode;
81
+ supportCodeLibrary.afterTestStepHookDefinitions.push(new TestStepHookDefinition({
82
+ code: handler,
83
+ options
84
+ }));
85
+ }
86
+
87
+ export function setWorldConstructor(world) {
88
+ supportCodeLibrary.World = world;
89
+ }
90
+
91
+ export function defineParameterType(options) {
92
+ const parameterType = buildParameterType(options)
93
+ supportCodeLibrary.parameterTypeRegistry.defineSourcedParameterType(parameterType, {})
94
+ }
95
+
96
+
@@ -0,0 +1,17 @@
1
+ import {doesHaveValue, doesNotHaveValue} from './value_checker';
2
+ import parse from '@cucumber/tag-expressions';
3
+
4
+ export class PickleTagFilter {
5
+ constructor(tagExpression) {
6
+ if (doesHaveValue(tagExpression) && tagExpression !== '') {
7
+ this.tagExpressionNode = parse(tagExpression)
8
+ }
9
+ }
10
+
11
+ matchesAllTagExpressions(pickle) {
12
+ if (doesNotHaveValue(this.tagExpressionNode)) {
13
+ return true
14
+ }
15
+ return this.tagExpressionNode.evaluate(pickle.tags.map((x) => x.name))
16
+ }
17
+ }
@@ -0,0 +1,20 @@
1
+ import {
2
+ ParameterType,
3
+ ParameterTypeRegistry,
4
+ } from '@cucumber/cucumber-expressions'
5
+
6
+ export class SourcedParameterTypeRegistry extends ParameterTypeRegistry {
7
+ parameterTypeToSource = new WeakMap()
8
+
9
+ defineSourcedParameterType(
10
+ parameterType,
11
+ source
12
+ ) {
13
+ this.defineParameterType(parameterType)
14
+ this.parameterTypeToSource.set(parameterType, source)
15
+ }
16
+
17
+ lookupSource(parameterType) {
18
+ return this.parameterTypeToSource.get(parameterType)
19
+ }
20
+ }
@@ -0,0 +1,13 @@
1
+ import { doesHaveValue } from './value_checker';
2
+
3
+ export function parseStepArgument(
4
+ arg,
5
+ mapping
6
+ ) {
7
+ if (doesHaveValue(arg.dataTable)) {
8
+ return mapping.dataTable(arg.dataTable)
9
+ } else if (doesHaveValue(arg.docString)) {
10
+ return mapping.docString(arg.docString)
11
+ }
12
+ throw new Error(`Unknown step argument: ${arg}`)
13
+ }
@@ -0,0 +1,34 @@
1
+ import Definition from './definition';
2
+ import { doesHaveValue } from './value_checker';
3
+ import DataTable from './data_table';
4
+ import { parseStepArgument } from './step_argument';
5
+
6
+ export default class StepDefinition extends Definition {
7
+ constructor(data) {
8
+ super(data)
9
+ this.keyword = data.keyword
10
+ this.pattern = data.pattern
11
+ this.expression = data.expression
12
+ }
13
+
14
+ getInvocationParameters({ step, world }) {
15
+ const parameters = this.expression.match(step.text).map((arg) => arg.getValue(world))
16
+ if (doesHaveValue(step.argument)) {
17
+ const argumentParameter = parseStepArgument(step.argument, {
18
+ dataTable: (arg) => new DataTable(arg),
19
+ docString: (arg) => arg.content,
20
+ })
21
+ parameters.push(argumentParameter)
22
+ }
23
+ return {
24
+ getInvalidCodeLengthMessage: () =>
25
+ this.baseGetInvalidCodeLengthMessage(parameters),
26
+ parameters,
27
+ validCodeLengths: [parameters.length, parameters.length + 1],
28
+ }
29
+ }
30
+
31
+ matchesStepName(stepName) {
32
+ return doesHaveValue(this.expression.match(stepName))
33
+ }
34
+ }
@@ -0,0 +1,25 @@
1
+ import Definition from './definition';
2
+ import { PickleTagFilter } from './pickle_filter';
3
+
4
+ export default class TestCaseHookDefinition extends Definition {
5
+
6
+ constructor(data) {
7
+ super(data)
8
+ this.name = data.options.name
9
+ this.tagExpression = data.options.tags
10
+ this.pickleTagFilter = new PickleTagFilter(data.options.tags)
11
+ }
12
+
13
+ appliesToTestCase(pickle) {
14
+ return this.pickleTagFilter.matchesAllTagExpressions(pickle)
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
+ }
@@ -0,0 +1,23 @@
1
+ import Definition from './definition';
2
+ import {PickleTagFilter} from './pickle_filter';
3
+
4
+ export default class TestStepHookDefinition extends Definition {
5
+ constructor(data) {
6
+ super(data)
7
+ this.tagExpression = data.options.tags
8
+ this.pickleTagFilter = new PickleTagFilter(data.options.tags)
9
+ }
10
+
11
+ appliesToTestCase(pickle) {
12
+ return this.pickleTagFilter.matchesAllTagExpressions(pickle)
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
+ }
@@ -0,0 +1,14 @@
1
+ export function doesHaveValue(value) {
2
+ return !doesNotHaveValue(value)
3
+ }
4
+
5
+ export function doesNotHaveValue(value) {
6
+ return value === null || value === undefined
7
+ }
8
+
9
+ export function valueOrDefault(value, defaultValue) {
10
+ if (doesHaveValue(value)) {
11
+ return value
12
+ }
13
+ return defaultValue
14
+ }