@qavajs/playwright-runner-adapter 2.2.0 → 2.3.1

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.
@@ -1,52 +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
- }
21
-
22
- async executeStep(this: any, text: string, extraParam?: any) {
23
- const steps = this.supportCodeLibrary.stepDefinitions.filter((s: any) => s.matchesStepName(text));
24
- if (steps.length === 0) throw new Error(`Step "${text}" is not defined`);
25
- if (steps.length > 1) throw new Error(`Step "${text}" matches multiple step definitions`);
26
- const step = steps.pop() as any;
27
- const { parameters } = await step.getInvocationParameters({ step: { text }, world: this } as any);
28
- try {
29
- await step.code.apply(this, [...parameters, extraParam]);
30
- } catch (err) {
31
- throw new Error(`${text}\n${err}`);
32
- }
33
- }
34
- }
35
-
36
- /**
37
- * Cucumber Playwright world for playwright adapter
38
- */
39
- export class PlaywrightWorld extends TestWorld {
40
- page!: Page;
41
- context!: BrowserContext;
42
- browser!: Browser;
43
- browserName!: string;
44
- request!: APIRequestContext;
45
-
46
- init: ({ [fixture: string]: any }) = ({ browser, context, page, request }: { browser: Browser, context: BrowserContext, page: Page, request: APIRequestContext }) => {
47
- this.browser = browser;
48
- this.context = context;
49
- this.page = page;
50
- this.request = request;
51
- }
52
- }
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,381 +0,0 @@
1
- import {load} from './loader';
2
- import type {TestInfo, TestType} from '@playwright/test';
3
- import type {ITestCaseHookParameter} from '@cucumber/cucumber';
4
-
5
- // Constants
6
- const ANNOTATION_TYPES = {
7
- TEST_ID: '_testId',
8
- NAME: 'name',
9
- URI: 'uri',
10
- TAGS: 'tags'
11
- } as const;
12
-
13
- const HOOK_NAMES = {
14
- BEFORE_ALL: 'Before All',
15
- AFTER_ALL: 'After All',
16
- BEFORE: 'Before',
17
- AFTER: 'After',
18
- BEFORE_STEP: 'Before Step',
19
- AFTER_STEP: 'After Step',
20
- WORLD: 'World',
21
- BEFORE_HOOKS: 'Before Hooks',
22
- AFTER_HOOKS: 'After Hooks'
23
- } as const;
24
-
25
- const DEFAULT_VALUES = {
26
- ATTACHMENT_FILENAME: 'attachment',
27
- CONTENT_TYPE: 'text/plain',
28
- COLUMN: 1,
29
- TEST_CASE_ID: '0',
30
- TEST_STEP_ID: '0'
31
- } as const;
32
-
33
- const STEP_ARGUMENT_LABELS = {
34
- DOC_STRING: ' [MultiLine]',
35
- DATA_TABLE: ' [DataTable]'
36
- } as const;
37
-
38
- // Type definitions
39
- interface Location {
40
- file: string;
41
- line: number;
42
- column: number;
43
- }
44
-
45
- interface StepDefinition {
46
- uri: string;
47
- line: number;
48
- }
49
-
50
- interface TestResult {
51
- status: string;
52
- error?: Error;
53
- }
54
-
55
- interface HookResult {
56
- duration: number | { nanos: number; seconds: number };
57
- message?: string;
58
- status: string;
59
- exception?: Error;
60
- }
61
-
62
- /**
63
- * Extracts the test ID from TestInfo annotations
64
- */
65
- function getTestId(testInfo: TestInfo): string {
66
- const annotation = testInfo.annotations
67
- .find((annotation) => annotation.type === ANNOTATION_TYPES.TEST_ID);
68
-
69
- if (!annotation?.description) {
70
- throw new Error('Test ID annotation not found');
71
- }
72
-
73
- return annotation.description;
74
- }
75
-
76
- /**
77
- * Converts TestInfo to HookResult format
78
- */
79
- function getResult(testInfo: TestInfo): HookResult {
80
- const message = testInfo.errors.length > 0
81
- ? testInfo.errors.map((err) => err.message).join('\n')
82
- : undefined;
83
-
84
- return {
85
- duration: testInfo.duration,
86
- message,
87
- status: testInfo.status?.toUpperCase() ?? 'UNKNOWN',
88
- exception: testInfo.error as any
89
- };
90
- }
91
-
92
- /**
93
- * Creates a location object from step definition
94
- */
95
- function getLine(step: StepDefinition): { location: Location } {
96
- return {
97
- location: {
98
- column: DEFAULT_VALUES.COLUMN,
99
- file: step.uri,
100
- line: step.line
101
- }
102
- };
103
- }
104
-
105
- /**
106
- * Logs data to console
107
- */
108
- function log(data: unknown): void {
109
- console.log(data);
110
- }
111
-
112
- /**
113
- * Attaches content to test report
114
- */
115
- function attach(
116
- this: { test: { info: () => TestInfo } },
117
- body: string | Buffer,
118
- details?: { fileName?: string; mediaType?: string }
119
- ): Promise<void> {
120
- const fileName = details?.fileName ?? DEFAULT_VALUES.ATTACHMENT_FILENAME;
121
- const contentType = details?.mediaType ?? DEFAULT_VALUES.CONTENT_TYPE;
122
- return this.test.info().attach(fileName, { body, contentType });
123
- }
124
-
125
- /**
126
- * Generates a human-readable step name with argument type indicator
127
- */
128
- function stepName(pickleStep: {
129
- text: string;
130
- argument?: { docString?: unknown }
131
- }): string {
132
- let step = pickleStep.text;
133
-
134
- if (pickleStep.argument) {
135
- step += pickleStep.argument.docString
136
- ? STEP_ARGUMENT_LABELS.DOC_STRING
137
- : STEP_ARGUMENT_LABELS.DATA_TABLE;
138
- }
139
-
140
- return step;
141
- }
142
-
143
- /**
144
- * Executes before all hooks
145
- */
146
- function executeBeforeAllHooks(
147
- test: TestType<any, any>,
148
- hooks: any[]
149
- ): void {
150
- if (hooks.length === 0) return;
151
- test.beforeAll(async () => {
152
- for (const hook of hooks) {
153
- const location = getLine(hook);
154
- await test.step(
155
- HOOK_NAMES.BEFORE_ALL,
156
- () => hook.code.apply({}),
157
- location
158
- );
159
- }
160
- })
161
- }
162
-
163
- /**
164
- * Executes after all hooks
165
- */
166
- function executeAfterAllHooks(
167
- test: TestType<any, any>,
168
- hooks: any[]
169
- ): void {
170
- if (hooks.length === 0) return;
171
- test.afterAll(async () => {
172
- for (const hook of hooks) {
173
- const location = getLine(hook);
174
- await test.step(
175
- HOOK_NAMES.AFTER_ALL,
176
- () => hook.code.apply({}),
177
- location
178
- );
179
- }
180
- });
181
- }
182
-
183
- /**
184
- * Creates test annotations from test case data
185
- */
186
- function createTestAnnotations(testCase: any): any[] {
187
- const tags = [...new Set(testCase.tags.map((tag: { name: string }) => tag.name))];
188
-
189
- return [
190
- { type: ANNOTATION_TYPES.NAME, description: testCase.name },
191
- { type: ANNOTATION_TYPES.URI, description: testCase.uri },
192
- { type: ANNOTATION_TYPES.TAGS, description: JSON.stringify(tags) },
193
- { type: ANNOTATION_TYPES.TEST_ID, description: testCase.id }
194
- ];
195
- }
196
-
197
- /**
198
- * Main configuration function that sets up Cucumber adapter for Playwright
199
- */
200
- export function defineConfig(config: any): any {
201
- const {features, supportCodeLibrary} = load(config);
202
-
203
- const fixture = new supportCodeLibrary.World({config});
204
- const test: TestType<any, any> = fixture.test;
205
- executeBeforeAllHooks(test, supportCodeLibrary.beforeTestRunHookDefinitions);
206
-
207
- const worlds = new Map<string, any>();
208
-
209
- for (const feature of features) {
210
- const tests = feature.tests;
211
-
212
- test.describe(feature.feature as string, async () => {
213
- /**
214
- * Factory function to create and register world instances
215
- */
216
- const worldFactory = (fixtures: any): void => {
217
- const world = new supportCodeLibrary.World({
218
- log,
219
- attach,
220
- supportCodeLibrary,
221
- config
222
- });
223
- world.init(fixtures);
224
- worlds.set(getTestId(test.info()), world);
225
- };
226
-
227
- worldFactory.toString = (): string => fixture.init.toString();
228
-
229
- test.beforeEach(HOOK_NAMES.WORLD, worldFactory);
230
-
231
- test.beforeEach(HOOK_NAMES.BEFORE_HOOKS, async () => {
232
- const testId = getTestId(test.info());
233
- const world = worlds.get(testId);
234
- const testCase = tests.find((t: any) => t.id === testId);
235
-
236
- if (!testCase) {
237
- throw new Error(`Test case with ID ${testId} not found`);
238
- }
239
-
240
- for (const beforeHook of supportCodeLibrary.beforeTestCaseHookDefinitions) {
241
- if (beforeHook.appliesToTestCase(testCase)) {
242
- const hookName = beforeHook.name ?? HOOK_NAMES.BEFORE;
243
- const location = getLine(beforeHook);
244
- await test.step(
245
- hookName,
246
- () => beforeHook.code.apply(world, [{
247
- gherkinDocument: feature.gherkinDocument,
248
- pickle: testCase
249
- } as ITestCaseHookParameter]),
250
- location
251
- );
252
- }
253
- }
254
- });
255
-
256
- for (const testCase of tests) {
257
- const tag = [...new Set(testCase.tags.map((tag: { name: string }) => tag.name))];
258
- const annotation = createTestAnnotations(testCase);
259
-
260
- test(testCase.name, {tag, annotation}, async () => {
261
- const world = worlds.get(testCase.id);
262
- const result: TestResult = { status: 'passed' };
263
-
264
- for (const pickleStep of testCase.steps) {
265
- if (result.status !== 'passed') {
266
- break;
267
- }
268
-
269
- const matchingSteps = supportCodeLibrary.stepDefinitions
270
- .filter((stepDef: any) => stepDef.matchesStepName(pickleStep.text));
271
-
272
- if (matchingSteps.length === 0) {
273
- throw new Error(
274
- `Step definition not found for: "${pickleStep.text}"`
275
- );
276
- }
277
-
278
- if (matchingSteps.length > 1) {
279
- throw new Error(
280
- `Ambiguous step "${pickleStep.text}" matches ${matchingSteps.length} definitions`
281
- );
282
- }
283
-
284
- const [step] = matchingSteps;
285
- const location = getLine(step);
286
- await test.step(stepName(pickleStep), async () => {
287
- for (const beforeStep of supportCodeLibrary.beforeTestStepHookDefinitions) {
288
- if (beforeStep.appliesToTestCase(testCase)) {
289
- const location = getLine(beforeStep);
290
- await test.step(
291
- HOOK_NAMES.BEFORE_STEP,
292
- () => beforeStep.code.apply(world, [{
293
- gherkinDocument: feature.gherkinDocument,
294
- pickle: testCase,
295
- pickleStep
296
- }]),
297
- location
298
- );
299
- }
300
- }
301
- const {parameters} = await step.getInvocationParameters({
302
- step: {
303
- text: pickleStep.text,
304
- argument: pickleStep.argument
305
- },
306
- world
307
- } as any);
308
-
309
- try {
310
- await step.code.apply(world, parameters);
311
- } catch (err) {
312
- result.status = 'failed';
313
- result.error = err as Error;
314
- throw err;
315
- } finally {
316
- for (const afterStep of supportCodeLibrary.afterTestStepHookDefinitions) {
317
- if (afterStep.appliesToTestCase(testCase)) {
318
- const location = getLine(afterStep);
319
- await test.step(
320
- HOOK_NAMES.AFTER_STEP,
321
- () => afterStep.code.apply(world, [{
322
- gherkinDocument: feature.gherkinDocument,
323
- testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID,
324
- testStepId: DEFAULT_VALUES.TEST_STEP_ID,
325
- pickle: testCase,
326
- pickleStep,
327
- result: {
328
- duration: 0 as any,
329
- message: result.error?.message,
330
- status: result.status.toUpperCase(),
331
- exception: result.error
332
- }
333
- } as any]),
334
- location
335
- );
336
- }
337
- }
338
- }
339
- }, location);
340
- }
341
- })
342
- }
343
-
344
- test.afterEach(HOOK_NAMES.AFTER_HOOKS, async () => {
345
- const testInfo = test.info();
346
- const testId = getTestId(testInfo);
347
- const world = worlds.get(testId);
348
- const testCase = tests.find((t) => t.id === testId);
349
-
350
- if (!testCase) {
351
- throw new Error(`Test case with ID ${testId} not found`);
352
- }
353
-
354
- for (const afterHook of supportCodeLibrary.afterTestCaseHookDefinitions) {
355
- if (afterHook.appliesToTestCase(testCase)) {
356
- const hookName = afterHook.name ?? HOOK_NAMES.AFTER;
357
- const location = getLine(afterHook);
358
- await test.step(
359
- hookName,
360
- () => afterHook.code.apply(world, [{
361
- gherkinDocument: feature.gherkinDocument,
362
- testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID,
363
- pickle: testCase,
364
- result: getResult(testInfo)
365
- } as any]),
366
- location
367
- );
368
- }
369
- }
370
- });
371
-
372
- test.afterEach(HOOK_NAMES.WORLD, async () => {
373
- const testId = getTestId(test.info());
374
- worlds.delete(testId);
375
- });
376
-
377
- });
378
- }
379
-
380
- executeAfterAllHooks(test, supportCodeLibrary.afterTestRunHookDefinitions);
381
- }
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
- }