@qavajs/playwright-runner-adapter 2.1.0 → 2.3.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.
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineConfig = defineConfig;
4
+ const loader_1 = require("./loader");
5
+ // Constants
6
+ const ANNOTATION_TYPES = {
7
+ NAME: 'name',
8
+ URI: 'uri',
9
+ TAGS: 'tags'
10
+ };
11
+ const HOOK_NAMES = {
12
+ BEFORE_ALL: 'Before All',
13
+ AFTER_ALL: 'After All',
14
+ BEFORE: 'Before',
15
+ AFTER: 'After',
16
+ BEFORE_STEP: 'Before Step',
17
+ AFTER_STEP: 'After Step',
18
+ WORLD: 'World',
19
+ BEFORE_HOOKS: 'Before Hooks',
20
+ AFTER_HOOKS: 'After Hooks'
21
+ };
22
+ const DEFAULT_VALUES = {
23
+ ATTACHMENT_FILENAME: 'attachment',
24
+ CONTENT_TYPE: 'text/plain',
25
+ COLUMN: 1,
26
+ TEST_CASE_ID: '0',
27
+ TEST_STEP_ID: '0'
28
+ };
29
+ const STEP_ARGUMENT_LABELS = {
30
+ DOC_STRING: ' [MultiLine]',
31
+ DATA_TABLE: ' [DataTable]'
32
+ };
33
+ /**
34
+ * Creates a location object from step definition
35
+ */
36
+ function getLine(step) {
37
+ return {
38
+ location: {
39
+ column: DEFAULT_VALUES.COLUMN,
40
+ file: step.uri,
41
+ line: step.line
42
+ }
43
+ };
44
+ }
45
+ /**
46
+ * Logs data to console
47
+ */
48
+ function log(data) {
49
+ console.log(data);
50
+ }
51
+ /**
52
+ * Attaches content to test report
53
+ */
54
+ function attach(body, details) {
55
+ const fileName = details?.fileName ?? DEFAULT_VALUES.ATTACHMENT_FILENAME;
56
+ const contentType = details?.mediaType ?? DEFAULT_VALUES.CONTENT_TYPE;
57
+ return this.test.info().attach(fileName, { body, contentType });
58
+ }
59
+ /**
60
+ * Generates a human-readable step name with argument type indicator
61
+ */
62
+ function stepName(pickleStep) {
63
+ let step = pickleStep.text;
64
+ if (pickleStep.argument) {
65
+ step += pickleStep.argument.docString
66
+ ? STEP_ARGUMENT_LABELS.DOC_STRING
67
+ : STEP_ARGUMENT_LABELS.DATA_TABLE;
68
+ }
69
+ return step;
70
+ }
71
+ function createTestExecutionResult() {
72
+ return {
73
+ status: 'passed',
74
+ start: Date.now()
75
+ };
76
+ }
77
+ function createHookResult(result) {
78
+ return {
79
+ duration: (Date.now() - result.start),
80
+ message: result.error?.message,
81
+ status: result.status.toUpperCase(),
82
+ exception: result.error
83
+ };
84
+ }
85
+ function createScenarioAnnotation(testCase) {
86
+ const tag = [...new Set(testCase.tags.map((item) => item.name))];
87
+ return [
88
+ { type: ANNOTATION_TYPES.NAME, description: testCase.name },
89
+ { type: ANNOTATION_TYPES.URI, description: testCase.uri },
90
+ { type: ANNOTATION_TYPES.TAGS, description: JSON.stringify(tag) }
91
+ ];
92
+ }
93
+ function createScenarioTags(testCase) {
94
+ return [...new Set(testCase.tags.map((item) => item.name))];
95
+ }
96
+ function createCaseHookPayload(feature, testCase) {
97
+ return {
98
+ gherkinDocument: feature.gherkinDocument,
99
+ pickle: testCase
100
+ };
101
+ }
102
+ async function executeCaseHooks(test, hooks, world, testCase, defaultHookName, createPayload) {
103
+ for (const hook of hooks) {
104
+ if (!hook.appliesToTestCase(testCase)) {
105
+ continue;
106
+ }
107
+ const hookName = hook.name ?? defaultHookName;
108
+ const location = getLine(hook);
109
+ await test.step(hookName, () => hook.code.apply(world, [createPayload(hook)]), location);
110
+ }
111
+ }
112
+ async function executeStepHooks(test, hooks, world, testCase, hookName, createPayload) {
113
+ for (const hook of hooks) {
114
+ if (!hook.appliesToTestCase(testCase)) {
115
+ continue;
116
+ }
117
+ const location = getLine(hook);
118
+ await test.step(hookName, () => hook.code.apply(world, [createPayload(hook)]), location);
119
+ }
120
+ }
121
+ function createStepHookParameter(feature, testCase, pickleStep, result) {
122
+ return {
123
+ gherkinDocument: feature.gherkinDocument,
124
+ testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID,
125
+ testStepId: DEFAULT_VALUES.TEST_STEP_ID,
126
+ pickle: testCase,
127
+ pickleStep,
128
+ result: createHookResult(result)
129
+ };
130
+ }
131
+ function createCaseHookParameter(feature, testCase, result) {
132
+ return {
133
+ gherkinDocument: feature.gherkinDocument,
134
+ testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID,
135
+ pickle: testCase,
136
+ result: createHookResult(result)
137
+ };
138
+ }
139
+ function findSingleStepDefinition(supportCodeLibrary, pickleStep) {
140
+ const matchingSteps = supportCodeLibrary.stepDefinitions
141
+ .filter((stepDefinition) => stepDefinition.matchesStepName(pickleStep.text));
142
+ if (matchingSteps.length === 0) {
143
+ throw new Error(`Step '${pickleStep.text}' is not defined`);
144
+ }
145
+ if (matchingSteps.length > 1) {
146
+ throw new Error(`Step '${pickleStep.text}' matches multiple step definitions`);
147
+ }
148
+ return matchingSteps[0];
149
+ }
150
+ /**
151
+ * Executes before all hooks
152
+ */
153
+ function executeBeforeAllHooks(test, hooks) {
154
+ if (hooks.length === 0)
155
+ return;
156
+ test.beforeAll(async () => {
157
+ for (const hook of hooks) {
158
+ const location = getLine(hook);
159
+ await test.step(HOOK_NAMES.BEFORE_ALL, () => hook.code.apply({}), location);
160
+ }
161
+ });
162
+ }
163
+ /**
164
+ * Executes after all hooks
165
+ */
166
+ function executeAfterAllHooks(test, hooks) {
167
+ if (hooks.length === 0)
168
+ return;
169
+ test.afterAll(async () => {
170
+ for (const hook of hooks) {
171
+ const location = getLine(hook);
172
+ await test.step(HOOK_NAMES.AFTER_ALL, () => hook.code.apply({}), location);
173
+ }
174
+ });
175
+ }
176
+ /**
177
+ * Main configuration function that sets up Cucumber adapter for Playwright
178
+ */
179
+ function defineConfig(config) {
180
+ const { features, supportCodeLibrary } = (0, loader_1.load)(config);
181
+ const fixture = new supportCodeLibrary.World({ config });
182
+ const test = fixture.test;
183
+ executeBeforeAllHooks(test, supportCodeLibrary.beforeTestRunHookDefinitions);
184
+ for (const feature of features) {
185
+ const tests = feature.tests;
186
+ test.describe(feature.feature, () => {
187
+ for (const testCase of tests) {
188
+ const tag = createScenarioTags(testCase);
189
+ const annotation = createScenarioAnnotation(testCase);
190
+ const testBody = async (fixtures) => {
191
+ const world = new supportCodeLibrary.World({
192
+ log,
193
+ attach,
194
+ supportCodeLibrary,
195
+ config
196
+ });
197
+ world.init(fixtures);
198
+ const result = createTestExecutionResult();
199
+ await executeCaseHooks(test, supportCodeLibrary.beforeTestCaseHookDefinitions, world, testCase, HOOK_NAMES.BEFORE, () => createCaseHookPayload(feature, testCase));
200
+ try {
201
+ for (const pickleStep of testCase.steps) {
202
+ if (result.status !== 'passed') {
203
+ break;
204
+ }
205
+ const step = findSingleStepDefinition(supportCodeLibrary, pickleStep);
206
+ const location = getLine(step);
207
+ await test.step(stepName(pickleStep), async () => {
208
+ await executeStepHooks(test, supportCodeLibrary.beforeTestStepHookDefinitions, world, testCase, HOOK_NAMES.BEFORE_STEP, () => ({
209
+ gherkinDocument: feature.gherkinDocument,
210
+ pickle: testCase,
211
+ pickleStep
212
+ }));
213
+ const { parameters } = await step.getInvocationParameters({
214
+ step: {
215
+ text: pickleStep.text,
216
+ argument: pickleStep.argument
217
+ },
218
+ world
219
+ });
220
+ try {
221
+ await step.code.apply(world, parameters);
222
+ }
223
+ catch (err) {
224
+ result.status = 'failed';
225
+ result.error = err;
226
+ throw err;
227
+ }
228
+ finally {
229
+ await executeStepHooks(test, supportCodeLibrary.afterTestStepHookDefinitions, world, testCase, HOOK_NAMES.AFTER_STEP, () => createStepHookParameter(feature, testCase, pickleStep, result));
230
+ }
231
+ }, location);
232
+ }
233
+ }
234
+ finally {
235
+ await executeCaseHooks(test, supportCodeLibrary.afterTestCaseHookDefinitions, world, testCase, HOOK_NAMES.AFTER, () => createCaseHookParameter(feature, testCase, result));
236
+ }
237
+ };
238
+ testBody.toString = () => fixture.init.toString();
239
+ test(testCase.name, { tag, annotation }, testBody);
240
+ }
241
+ });
242
+ }
243
+ executeAfterAllHooks(test, supportCodeLibrary.afterTestRunHookDefinitions);
244
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Translate cucumber tag expression to playwright grep
3
+ * @param {string} tagExpression
4
+ * @example
5
+ * import { tags } from '@qavajs/playwright-runner-adapter';
6
+ * export default defineConfig({
7
+ * grep: tags('@oneTag and @anotherTag')
8
+ * });
9
+ */
10
+ export declare function tags(tagExpression: string): RegExp;
11
+ /**
12
+ * Filter test cases by name
13
+ * @param {(testName: string) => boolean} predicate
14
+ * import { filter } from '@qavajs/playwright-runner-adapter';
15
+ * export default defineConfig({
16
+ * grep: filter(name => name.includes('login test'))
17
+ * });
18
+ */
19
+ export declare function filter(predicate: (testName: string) => boolean): RegExp;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.tags = tags;
7
+ exports.filter = filter;
8
+ const tag_expressions_1 = __importDefault(require("@cucumber/tag-expressions"));
9
+ class TagExpression extends RegExp {
10
+ tagExpression;
11
+ expressionNode;
12
+ constructor(tagExpression) {
13
+ super('');
14
+ this.tagExpression = tagExpression;
15
+ this.expressionNode = (0, tag_expressions_1.default)(this.tagExpression);
16
+ }
17
+ test(testName) {
18
+ const tokens = testName.split(/\s+/);
19
+ return this.expressionNode.evaluate(tokens);
20
+ }
21
+ }
22
+ /**
23
+ * Translate cucumber tag expression to playwright grep
24
+ * @param {string} tagExpression
25
+ * @example
26
+ * import { tags } from '@qavajs/playwright-runner-adapter';
27
+ * export default defineConfig({
28
+ * grep: tags('@oneTag and @anotherTag')
29
+ * });
30
+ */
31
+ function tags(tagExpression) {
32
+ return new TagExpression(tagExpression);
33
+ }
34
+ class Filter extends RegExp {
35
+ predicate;
36
+ constructor(predicate) {
37
+ super('');
38
+ this.predicate = predicate;
39
+ this.predicate = predicate;
40
+ }
41
+ test(testName) {
42
+ return this.predicate(testName);
43
+ }
44
+ }
45
+ /**
46
+ * Filter test cases by name
47
+ * @param {(testName: string) => boolean} predicate
48
+ * import { filter } from '@qavajs/playwright-runner-adapter';
49
+ * export default defineConfig({
50
+ * grep: filter(name => name.includes('login test'))
51
+ * });
52
+ */
53
+ function filter(predicate) {
54
+ return new Filter(predicate);
55
+ }
@@ -0,0 +1,5 @@
1
+ export * from './PlaywrightWorld';
2
+ export * from './filter';
3
+ export * from './Template';
4
+ export * from '@cucumber/cucumber';
5
+ export * from './cucumberAdapter';
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./PlaywrightWorld"), exports);
18
+ __exportStar(require("./filter"), exports);
19
+ __exportStar(require("./Template"), exports);
20
+ __exportStar(require("@cucumber/cucumber"), exports);
21
+ __exportStar(require("./cucumberAdapter"), exports);
@@ -0,0 +1,14 @@
1
+ import type { Pickle, GherkinDocument } from '@cucumber/messages';
2
+ type Feature = {
3
+ feature?: string;
4
+ gherkinDocument: GherkinDocument;
5
+ tests: readonly Pickle[];
6
+ uri: string;
7
+ };
8
+ export declare function loadFeatures(globPattern: string[]): Feature[];
9
+ export declare function loadStepDefinitions(globPattern: string[]): import("@cucumber/cucumber/lib/support_code_library_builder/types").SupportCodeLibrary;
10
+ export declare function load(config: any): {
11
+ features: Feature[];
12
+ supportCodeLibrary: import("@cucumber/cucumber/lib/support_code_library_builder/types").SupportCodeLibrary;
13
+ };
14
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadFeatures = loadFeatures;
4
+ exports.loadStepDefinitions = loadStepDefinitions;
5
+ exports.load = load;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ const node_crypto_1 = require("node:crypto");
9
+ const gherkin_1 = require("@cucumber/gherkin");
10
+ const cucumber_1 = require("@cucumber/cucumber");
11
+ const glob_1 = require("glob");
12
+ const PlaywrightWorld_1 = require("./PlaywrightWorld");
13
+ const uuidFn = () => (0, node_crypto_1.randomUUID)();
14
+ const builder = new gherkin_1.AstBuilder(uuidFn);
15
+ const matcher = new gherkin_1.GherkinClassicTokenMatcher();
16
+ const parser = new gherkin_1.Parser(builder, matcher);
17
+ function duplicates(tests) {
18
+ const counts = {};
19
+ return tests.map(item => {
20
+ const name = item.name;
21
+ if (!(name in counts)) {
22
+ counts[name] = 1;
23
+ }
24
+ if (counts[name] > 1) {
25
+ item.name = `${item.name} ${counts[name]}`;
26
+ }
27
+ counts[name] = counts[name] + 1;
28
+ return item;
29
+ });
30
+ }
31
+ function loadFeatures(globPattern) {
32
+ const files = (0, glob_1.globSync)(globPattern);
33
+ return files.map(file => {
34
+ const filePath = (0, node_path_1.resolve)(file);
35
+ const gherkinDocument = parser.parse((0, node_fs_1.readFileSync)(filePath, 'utf-8'));
36
+ return {
37
+ feature: gherkinDocument.feature?.name,
38
+ uri: filePath,
39
+ gherkinDocument,
40
+ tests: duplicates((0, gherkin_1.compile)(gherkinDocument, file, uuidFn))
41
+ };
42
+ });
43
+ }
44
+ function loadStepDefinitions(globPattern) {
45
+ cucumber_1.supportCodeLibraryBuilder.reset(process.cwd(), uuidFn);
46
+ cucumber_1.supportCodeLibraryBuilder.methods.setWorldConstructor(PlaywrightWorld_1.PlaywrightWorld);
47
+ const files = (0, glob_1.globSync)(globPattern);
48
+ for (const file of files) {
49
+ const filePath = (0, node_path_1.resolve)(file);
50
+ require(filePath);
51
+ }
52
+ return cucumber_1.supportCodeLibraryBuilder.finalize();
53
+ }
54
+ function load(config) {
55
+ const features = loadFeatures(config.paths);
56
+ const supportCodeLibrary = loadStepDefinitions([
57
+ ...(config.requireModules ?? []),
58
+ ...(config.require ?? []),
59
+ ...(config.import ?? [])
60
+ ]);
61
+ return { features, supportCodeLibrary };
62
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@qavajs/playwright-runner-adapter",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "adapter for playwright test runner",
5
5
  "main": "adapter/index.js",
6
6
  "types": "adapter/index.d.ts",
7
7
  "scripts": {
8
- "build": "tsc",
8
+ "build": "tsc && tsc --project tsconfig.js.json",
9
9
  "test:ui": "playwright test --config test/playwright.config.ts --ui",
10
- "test:e2e": "playwright test --config test/playwright-e2e.config.ts"
10
+ "test:e2e": "playwright test --config test/playwright.config.ts --project parallel --project serial"
11
11
  },
12
12
  "author": "Alexandr Galichenko",
13
13
  "license": "MIT",
@@ -21,12 +21,12 @@
21
21
  "dependencies": {
22
22
  "@cucumber/gherkin": "^38.0.0",
23
23
  "@cucumber/tag-expressions": "^9.0.0",
24
- "glob": "^13.0.1",
24
+ "glob": "^13.0.3",
25
25
  "@cucumber/cucumber": "^12.6.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@playwright/test": "^1.58.1",
29
- "@types/node": "^25.2.1",
28
+ "@playwright/test": "^1.58.2",
29
+ "@types/node": "^25.2.3",
30
30
  "typescript": "^5.9.3"
31
31
  },
32
32
  "keywords": [
@@ -5,7 +5,7 @@ import { APIRequestContext, Browser, BrowserContext, Page, test, expect } from '
5
5
  */
6
6
  export class TestWorld {
7
7
  log!: (data: any) => void;
8
- attach!: (data: any, details?: { fileName?: string, mediaType: string }) => void;
8
+ attach!: (data: any, details?: { fileName?: string, mediaType: string }) => Promise<void>;
9
9
  parameters!: string;
10
10
  config: any;
11
11
  supportCodeLibrary!: any;
@@ -17,6 +17,7 @@ export class TestWorld {
17
17
  this.attach = options.attach;
18
18
  this.parameters = options.parameters;
19
19
  this.supportCodeLibrary = options.supportCodeLibrary;
20
+ this.config = options.config;
20
21
  }
21
22
 
22
23
  async executeStep(this: any, text: string, extraParam?: any) {
@@ -43,9 +44,10 @@ export class PlaywrightWorld extends TestWorld {
43
44
  browserName!: string;
44
45
  request!: APIRequestContext;
45
46
 
46
- init: ({ [fixture: string]: any }) = ({ browser, context, page }: { browser: Browser, context: BrowserContext, page: Page }) => {
47
+ init: ({ [fixture: string]: any }) = ({ browser, context, page, request }: { browser: Browser, context: BrowserContext, page: Page, request: APIRequestContext }) => {
47
48
  this.browser = browser;
48
49
  this.context = context;
49
50
  this.page = page;
51
+ this.request = request;
50
52
  }
51
53
  }