@qavajs/playwright-runner-adapter 2.3.0 → 2.3.2

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/package.json CHANGED
@@ -1,13 +1,34 @@
1
1
  {
2
2
  "name": "@qavajs/playwright-runner-adapter",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "adapter for playwright test runner",
5
5
  "main": "adapter/index.js",
6
6
  "types": "adapter/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./adapter/index.d.ts",
10
+ "require": "./adapter/index.js",
11
+ "import": "./adapter/index.js",
12
+ "default": "./adapter/index.js"
13
+ },
14
+ "./js": {
15
+ "types": "./adapter/js/index.d.ts",
16
+ "require": "./adapter/js/index.js",
17
+ "import": "./adapter/js/index.js",
18
+ "default": "./adapter/js/index.js"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "adapter/**/*",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
7
28
  "scripts": {
8
29
  "build": "tsc && tsc --project tsconfig.js.json",
9
30
  "test:ui": "playwright test --config test/playwright.config.ts --ui",
10
- "test:e2e": "playwright test --config test/playwright.config.ts --project parallel --project serial"
31
+ "test:e2e": "playwright test --config test/playwright.config.ts --project parallel --project serial --project unit"
11
32
  },
12
33
  "author": "Alexandr Galichenko",
13
34
  "license": "MIT",
@@ -19,15 +40,15 @@
19
40
  "url": "https://github.com/qavajs/playwright-runner-adapter/issues"
20
41
  },
21
42
  "dependencies": {
22
- "@cucumber/gherkin": "^38.0.0",
23
- "@cucumber/tag-expressions": "^9.0.0",
24
- "glob": "^13.0.3",
25
- "@cucumber/cucumber": "^12.6.0"
43
+ "@cucumber/gherkin": "^39.0.0",
44
+ "@cucumber/tag-expressions": "^9.1.0",
45
+ "glob": "^13.0.6",
46
+ "@cucumber/cucumber": "^12.8.1"
26
47
  },
27
48
  "devDependencies": {
28
- "@playwright/test": "^1.58.2",
29
- "@types/node": "^25.2.3",
30
- "typescript": "^5.9.3"
49
+ "@playwright/test": "^1.59.1",
50
+ "@types/node": "^25.6.0",
51
+ "typescript": "^6.0.3"
31
52
  },
32
53
  "keywords": [
33
54
  "test",
@@ -1,53 +0,0 @@
1
- import { APIRequestContext, Browser, BrowserContext, Page, test, expect } from '@playwright/test';
2
-
3
- /**
4
- * Cucumber world for playwright adapter
5
- */
6
- export class TestWorld {
7
- log!: (data: any) => void;
8
- attach!: (data: any, details?: { fileName?: string, mediaType: string }) => Promise<void>;
9
- parameters!: string;
10
- config: any;
11
- supportCodeLibrary!: any;
12
- test = test;
13
- expect = expect;
14
-
15
- constructor(options: any) {
16
- this.log = options.log;
17
- this.attach = options.attach;
18
- this.parameters = options.parameters;
19
- this.supportCodeLibrary = options.supportCodeLibrary;
20
- this.config = options.config;
21
- }
22
-
23
- async executeStep(this: any, text: string, extraParam?: any) {
24
- const steps = this.supportCodeLibrary.stepDefinitions.filter((s: any) => s.matchesStepName(text));
25
- if (steps.length === 0) throw new Error(`Step "${text}" is not defined`);
26
- if (steps.length > 1) throw new Error(`Step "${text}" matches multiple step definitions`);
27
- const step = steps.pop() as any;
28
- const { parameters } = await step.getInvocationParameters({ step: { text }, world: this } as any);
29
- try {
30
- await step.code.apply(this, [...parameters, extraParam]);
31
- } catch (err) {
32
- throw new Error(`${text}\n${err}`);
33
- }
34
- }
35
- }
36
-
37
- /**
38
- * Cucumber Playwright world for playwright adapter
39
- */
40
- export class PlaywrightWorld extends TestWorld {
41
- page!: Page;
42
- context!: BrowserContext;
43
- browser!: Browser;
44
- browserName!: string;
45
- request!: APIRequestContext;
46
-
47
- init: ({ [fixture: string]: any }) = ({ browser, context, page, request }: { browser: Browser, context: BrowserContext, page: Page, request: APIRequestContext }) => {
48
- this.browser = browser;
49
- this.context = context;
50
- this.page = page;
51
- this.request = request;
52
- }
53
- }
package/src/Template.ts DELETED
@@ -1,23 +0,0 @@
1
- /**
2
- * Define template step
3
- * @param {string} scenario - multiline string with steps
4
- * @example
5
- * When('I click {string} and verify {string}', Template((locator, expected) => `
6
- * I click '${locator}'
7
- * I expect '${locator} > Value' to equal '${expected}'
8
- * `));
9
- */
10
- export function Template(scenario: (...args: any[]) => string) {
11
- return new Proxy(scenario, {
12
- apply: async function (template: (...args: string[]) => string, world: { executeStep: (step: string) => Promise<void> }, args: any[]) {
13
- const scenario = template(...args) ;
14
- const steps = scenario
15
- .split('\n')
16
- .map(step => step.trim())
17
- .filter(Boolean);
18
- for (const step of steps) {
19
- await world.executeStep(step);
20
- }
21
- },
22
- })
23
- }
@@ -1,388 +0,0 @@
1
- import {load} from './loader';
2
- import type {TestInfo, TestType} from '@playwright/test';
3
- import type {ITestCaseHookParameter, ITestStepHookParameter} from '@cucumber/cucumber';
4
-
5
- // Constants
6
- const ANNOTATION_TYPES = {
7
- NAME: 'name',
8
- URI: 'uri',
9
- TAGS: 'tags'
10
- } as const;
11
-
12
- const HOOK_NAMES = {
13
- BEFORE_ALL: 'Before All',
14
- AFTER_ALL: 'After All',
15
- BEFORE: 'Before',
16
- AFTER: 'After',
17
- BEFORE_STEP: 'Before Step',
18
- AFTER_STEP: 'After Step',
19
- WORLD: 'World',
20
- BEFORE_HOOKS: 'Before Hooks',
21
- AFTER_HOOKS: 'After Hooks'
22
- } as const;
23
-
24
- const DEFAULT_VALUES = {
25
- ATTACHMENT_FILENAME: 'attachment',
26
- CONTENT_TYPE: 'text/plain',
27
- COLUMN: 1,
28
- TEST_CASE_ID: '0',
29
- TEST_STEP_ID: '0'
30
- } as const;
31
-
32
- const STEP_ARGUMENT_LABELS = {
33
- DOC_STRING: ' [MultiLine]',
34
- DATA_TABLE: ' [DataTable]'
35
- } as const;
36
-
37
- // Type definitions
38
- interface Location {
39
- file: string;
40
- line: number;
41
- column: number;
42
- }
43
-
44
- interface StepDefinition {
45
- uri: string;
46
- line: number;
47
- }
48
-
49
- interface TestResult {
50
- status: string;
51
- error?: Error;
52
- }
53
-
54
- interface TestExecutionResult extends TestResult {
55
- start: number;
56
- }
57
-
58
- interface FeatureContext {
59
- gherkinDocument: unknown;
60
- }
61
-
62
- interface PickleStep {
63
- text: string;
64
- argument?: { docString?: unknown };
65
- }
66
-
67
- /**
68
- * Creates a location object from step definition
69
- */
70
- function getLine(step: StepDefinition): { location: Location } {
71
- return {
72
- location: {
73
- column: DEFAULT_VALUES.COLUMN,
74
- file: step.uri,
75
- line: step.line
76
- }
77
- };
78
- }
79
-
80
- /**
81
- * Logs data to console
82
- */
83
- function log(data: unknown): void {
84
- console.log(data);
85
- }
86
-
87
- /**
88
- * Attaches content to test report
89
- */
90
- function attach(
91
- this: { test: { info: () => TestInfo } },
92
- body: string | Buffer,
93
- details?: { fileName?: string; mediaType?: string }
94
- ): Promise<void> {
95
- const fileName = details?.fileName ?? DEFAULT_VALUES.ATTACHMENT_FILENAME;
96
- const contentType = details?.mediaType ?? DEFAULT_VALUES.CONTENT_TYPE;
97
- return this.test.info().attach(fileName, { body, contentType });
98
- }
99
-
100
- /**
101
- * Generates a human-readable step name with argument type indicator
102
- */
103
- function stepName(pickleStep: {
104
- text: string;
105
- argument?: { docString?: unknown }
106
- }): string {
107
- let step = pickleStep.text;
108
-
109
- if (pickleStep.argument) {
110
- step += pickleStep.argument.docString
111
- ? STEP_ARGUMENT_LABELS.DOC_STRING
112
- : STEP_ARGUMENT_LABELS.DATA_TABLE;
113
- }
114
-
115
- return step;
116
- }
117
-
118
- function createTestExecutionResult(): TestExecutionResult {
119
- return {
120
- status: 'passed',
121
- start: Date.now()
122
- };
123
- }
124
-
125
- function createHookResult(result: TestExecutionResult): any {
126
- return {
127
- duration: (Date.now() - result.start) as any,
128
- message: result.error?.message,
129
- status: result.status.toUpperCase(),
130
- exception: result.error
131
- };
132
- }
133
-
134
- function createScenarioAnnotation(testCase: any): { type: string; description: string }[] {
135
- const tag = [...new Set(testCase.tags.map((item: { name: string }) => item.name))];
136
- return [
137
- {type: ANNOTATION_TYPES.NAME, description: testCase.name},
138
- {type: ANNOTATION_TYPES.URI, description: testCase.uri},
139
- {type: ANNOTATION_TYPES.TAGS, description: JSON.stringify(tag)}
140
- ];
141
- }
142
-
143
- function createScenarioTags(testCase: any): string[] {
144
- return [...new Set<string>(testCase.tags.map((item: { name: string }) => item.name))];
145
- }
146
-
147
- function createCaseHookPayload(feature: FeatureContext, testCase: any): ITestCaseHookParameter {
148
- return {
149
- gherkinDocument: feature.gherkinDocument,
150
- pickle: testCase
151
- } as ITestCaseHookParameter;
152
- }
153
-
154
- async function executeCaseHooks(
155
- test: TestType<any, any>,
156
- hooks: any[],
157
- world: any,
158
- testCase: any,
159
- defaultHookName: string,
160
- createPayload: (hook: any) => any
161
- ): Promise<void> {
162
- for (const hook of hooks) {
163
- if (!hook.appliesToTestCase(testCase)) {
164
- continue;
165
- }
166
-
167
- const hookName = hook.name ?? defaultHookName;
168
- const location = getLine(hook);
169
- await test.step(
170
- hookName,
171
- () => hook.code.apply(world, [createPayload(hook)]),
172
- location
173
- );
174
- }
175
- }
176
-
177
- async function executeStepHooks(
178
- test: TestType<any, any>,
179
- hooks: any[],
180
- world: any,
181
- testCase: any,
182
- hookName: string,
183
- createPayload: (hook: any) => any
184
- ): Promise<void> {
185
- for (const hook of hooks) {
186
- if (!hook.appliesToTestCase(testCase)) {
187
- continue;
188
- }
189
-
190
- const location = getLine(hook);
191
- await test.step(
192
- hookName,
193
- () => hook.code.apply(world, [createPayload(hook)]),
194
- location
195
- );
196
- }
197
- }
198
-
199
- function createStepHookParameter(
200
- feature: any,
201
- testCase: any,
202
- pickleStep: any,
203
- result: TestExecutionResult
204
- ): ITestStepHookParameter {
205
- return {
206
- gherkinDocument: feature.gherkinDocument,
207
- testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID,
208
- testStepId: DEFAULT_VALUES.TEST_STEP_ID,
209
- pickle: testCase,
210
- pickleStep,
211
- result: createHookResult(result)
212
- } as any;
213
- }
214
-
215
- function createCaseHookParameter(
216
- feature: any,
217
- testCase: any,
218
- result: TestExecutionResult
219
- ): ITestCaseHookParameter {
220
- return {
221
- gherkinDocument: feature.gherkinDocument,
222
- testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID,
223
- pickle: testCase,
224
- result: createHookResult(result)
225
- } as any;
226
- }
227
-
228
- function findSingleStepDefinition(
229
- supportCodeLibrary: any,
230
- pickleStep: PickleStep
231
- ): any {
232
- const matchingSteps = supportCodeLibrary.stepDefinitions
233
- .filter((stepDefinition: any) => stepDefinition.matchesStepName(pickleStep.text));
234
-
235
- if (matchingSteps.length === 0) {
236
- throw new Error(`Step '${pickleStep.text}' is not defined`);
237
- }
238
-
239
- if (matchingSteps.length > 1) {
240
- throw new Error(`Step '${pickleStep.text}' matches multiple step definitions`);
241
- }
242
-
243
- return matchingSteps[0];
244
- }
245
-
246
- /**
247
- * Executes before all hooks
248
- */
249
- function executeBeforeAllHooks(
250
- test: TestType<any, any>,
251
- hooks: any[]
252
- ): void {
253
- if (hooks.length === 0) return;
254
- test.beforeAll(async () => {
255
- for (const hook of hooks) {
256
- const location = getLine(hook);
257
- await test.step(
258
- HOOK_NAMES.BEFORE_ALL,
259
- () => hook.code.apply({}),
260
- location
261
- );
262
- }
263
- })
264
- }
265
-
266
- /**
267
- * Executes after all hooks
268
- */
269
- function executeAfterAllHooks(
270
- test: TestType<any, any>,
271
- hooks: any[]
272
- ): void {
273
- if (hooks.length === 0) return;
274
- test.afterAll(async () => {
275
- for (const hook of hooks) {
276
- const location = getLine(hook);
277
- await test.step(
278
- HOOK_NAMES.AFTER_ALL,
279
- () => hook.code.apply({}),
280
- location
281
- );
282
- }
283
- });
284
- }
285
-
286
- /**
287
- * Main configuration function that sets up Cucumber adapter for Playwright
288
- */
289
- export function defineConfig(config: any): any {
290
- const {features, supportCodeLibrary} = load(config);
291
-
292
- const fixture = new supportCodeLibrary.World({config});
293
- const test: TestType<any, any> = fixture.test;
294
- executeBeforeAllHooks(test, supportCodeLibrary.beforeTestRunHookDefinitions);
295
-
296
- for (const feature of features) {
297
- const tests = feature.tests;
298
- test.describe(feature.feature as string, () => {
299
-
300
- for (const testCase of tests) {
301
- const tag = createScenarioTags(testCase);
302
- const annotation = createScenarioAnnotation(testCase);
303
- const testBody = async (fixtures: any) => {
304
- const world = new supportCodeLibrary.World({
305
- log,
306
- attach,
307
- supportCodeLibrary,
308
- config
309
- });
310
- world.init(fixtures);
311
-
312
- const result = createTestExecutionResult();
313
-
314
- await executeCaseHooks(
315
- test,
316
- supportCodeLibrary.beforeTestCaseHookDefinitions,
317
- world,
318
- testCase,
319
- HOOK_NAMES.BEFORE,
320
- () => createCaseHookPayload(feature, testCase)
321
- );
322
-
323
- try {
324
- for (const pickleStep of testCase.steps) {
325
- if (result.status !== 'passed') {
326
- break;
327
- }
328
- const step = findSingleStepDefinition(supportCodeLibrary, pickleStep);
329
- const location = getLine(step);
330
- await test.step(stepName(pickleStep), async () => {
331
- await executeStepHooks(
332
- test,
333
- supportCodeLibrary.beforeTestStepHookDefinitions,
334
- world,
335
- testCase,
336
- HOOK_NAMES.BEFORE_STEP,
337
- () => ({
338
- gherkinDocument: feature.gherkinDocument,
339
- pickle: testCase,
340
- pickleStep
341
- })
342
- );
343
- const {parameters} = await step.getInvocationParameters({
344
- step: {
345
- text: pickleStep.text,
346
- argument: pickleStep.argument
347
- },
348
- world
349
- } as any);
350
- try {
351
- await step.code.apply(world, parameters);
352
- } catch (err: any) {
353
- result.status = 'failed';
354
- result.error = err;
355
- throw err;
356
- } finally {
357
- await executeStepHooks(
358
- test,
359
- supportCodeLibrary.afterTestStepHookDefinitions,
360
- world,
361
- testCase,
362
- HOOK_NAMES.AFTER_STEP,
363
- () => createStepHookParameter(feature, testCase, pickleStep, result)
364
- );
365
- }
366
- }, location);
367
- }
368
- } finally {
369
- await executeCaseHooks(
370
- test,
371
- supportCodeLibrary.afterTestCaseHookDefinitions,
372
- world,
373
- testCase,
374
- HOOK_NAMES.AFTER,
375
- () => createCaseHookParameter(feature, testCase, result)
376
- );
377
- }
378
-
379
- }
380
- testBody.toString = () => fixture.init.toString();
381
- test(testCase.name, {tag, annotation}, testBody);
382
- }
383
-
384
- });
385
- }
386
-
387
- executeAfterAllHooks(test, supportCodeLibrary.afterTestRunHookDefinitions);
388
- }
package/src/filter.ts DELETED
@@ -1,53 +0,0 @@
1
- import parse from '@cucumber/tag-expressions';
2
-
3
- class TagExpression extends RegExp {
4
- private expressionNode: {
5
- evaluate(variables: string[]): boolean;
6
- };
7
-
8
- constructor(private tagExpression: string) {
9
- super('');
10
- this.expressionNode = parse(this.tagExpression);
11
- }
12
-
13
- test(testName: string) {
14
- const tokens = testName.split(/\s+/);
15
- return this.expressionNode.evaluate(tokens);
16
- }
17
- }
18
-
19
- /**
20
- * Translate cucumber tag expression to playwright grep
21
- * @param {string} tagExpression
22
- * @example
23
- * import { tags } from '@qavajs/playwright-runner-adapter';
24
- * export default defineConfig({
25
- * grep: tags('@oneTag and @anotherTag')
26
- * });
27
- */
28
- export function tags(tagExpression: string): RegExp {
29
- return new TagExpression(tagExpression);
30
- }
31
-
32
- class Filter extends RegExp {
33
- constructor(private predicate: (testName: string) => boolean) {
34
- super('');
35
- this.predicate = predicate;
36
- }
37
-
38
- test(testName: string) {
39
- return this.predicate(testName);
40
- }
41
- }
42
-
43
- /**
44
- * Filter test cases by name
45
- * @param {(testName: string) => boolean} predicate
46
- * import { filter } from '@qavajs/playwright-runner-adapter';
47
- * export default defineConfig({
48
- * grep: filter(name => name.includes('login test'))
49
- * });
50
- */
51
- export function filter(predicate: (testName: string) => boolean): RegExp {
52
- return new Filter(predicate);
53
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from './PlaywrightWorld';
2
- export * from './filter';
3
- export * from './Template';
4
- export * from '@cucumber/cucumber';
5
- export * from './cucumberAdapter';
package/src/loader.ts DELETED
@@ -1,70 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { AstBuilder, compile, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin';
5
- import { supportCodeLibraryBuilder } from '@cucumber/cucumber';
6
- import type { Pickle, GherkinDocument } from '@cucumber/messages';
7
- import { globSync } from 'glob';
8
- import { PlaywrightWorld } from './PlaywrightWorld';
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 duplicates(tests: readonly Pickle[]) {
16
- const counts: Record<string, number> = {};
17
- return tests.map(item => {
18
- const name = item.name;
19
- if (!(name in counts)) {
20
- counts[name] = 1;
21
- }
22
- if (counts[name] > 1) {
23
- item.name = `${item.name} ${counts[name]}`;
24
- }
25
- counts[name] = counts[name] + 1;
26
- return item;
27
- });
28
- }
29
-
30
- type Feature = {
31
- feature?: string;
32
- gherkinDocument: GherkinDocument;
33
- tests: readonly Pickle[];
34
- uri: string;
35
- }
36
-
37
- export function loadFeatures(globPattern: string[]): Feature[] {
38
- const files = globSync(globPattern);
39
- return files.map(file => {
40
- const filePath = resolve(file);
41
- const gherkinDocument = parser.parse(readFileSync(filePath, 'utf-8'));
42
- return {
43
- feature: gherkinDocument.feature?.name,
44
- uri: filePath,
45
- gherkinDocument,
46
- tests: duplicates(compile(gherkinDocument, file, uuidFn))
47
- }
48
- });
49
- }
50
-
51
- export function loadStepDefinitions(globPattern: string[]) {
52
- supportCodeLibraryBuilder.reset(process.cwd(), uuidFn);
53
- supportCodeLibraryBuilder.methods.setWorldConstructor(PlaywrightWorld);
54
- const files = globSync(globPattern);
55
- for (const file of files) {
56
- const filePath = resolve(file);
57
- require(filePath);
58
- }
59
- return supportCodeLibraryBuilder.finalize();
60
- }
61
-
62
- export function load(config: any) {
63
- const features = loadFeatures(config.paths);
64
- const supportCodeLibrary = loadStepDefinitions([
65
- ...(config.requireModules ?? []),
66
- ...(config.require ?? []),
67
- ...(config.import ?? [])
68
- ]);
69
- return { features, supportCodeLibrary }
70
- }
package/tsconfig.js.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "include": [
3
- "src/**/*.ts"
4
- ],
5
- "exclude": [
6
- "test/**/*.spec.ts"
7
- ],
8
- "compilerOptions": {
9
- "target": "es2023",
10
- "module": "node16",
11
- "moduleResolution": "node16",
12
- "outDir": "./adapter/js",
13
- "esModuleInterop": true,
14
- "sourceMap": false,
15
- "forceConsistentCasingInFileNames": true,
16
- "strict": true,
17
- "skipLibCheck": true,
18
- "declaration": true,
19
- "types": [
20
- "node"
21
- ]
22
- }
23
- }
package/tsconfig.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "include": [
3
- "src/**/*.ts"
4
- ],
5
- "exclude": [
6
- "test/**/*.spec.ts"
7
- ],
8
- "compilerOptions": {
9
- "target": "es2023",
10
- "module": "node16",
11
- "moduleResolution": "node16",
12
- "outDir": "./adapter",
13
- "esModuleInterop": true,
14
- "sourceMap": true,
15
- "forceConsistentCasingInFileNames": true,
16
- "strict": true,
17
- "skipLibCheck": true,
18
- "declaration": true,
19
- "types": [
20
- "node"
21
- ]
22
- }
23
- }