@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.
@@ -1,10 +1,9 @@
1
1
  import {load} from './loader';
2
2
  import type {TestInfo, TestType} from '@playwright/test';
3
- import type {ITestCaseHookParameter} from '@cucumber/cucumber';
3
+ import type {ITestCaseHookParameter, ITestStepHookParameter} from '@cucumber/cucumber';
4
4
 
5
5
  // Constants
6
6
  const ANNOTATION_TYPES = {
7
- TEST_ID: 'testId',
8
7
  NAME: 'name',
9
8
  URI: 'uri',
10
9
  TAGS: 'tags'
@@ -52,41 +51,17 @@ interface TestResult {
52
51
  error?: Error;
53
52
  }
54
53
 
55
- interface HookResult {
56
- duration: number | { nanos: number; seconds: number };
57
- message?: string;
58
- status: string;
59
- exception?: Error;
54
+ interface TestExecutionResult extends TestResult {
55
+ start: number;
60
56
  }
61
57
 
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;
58
+ interface FeatureContext {
59
+ gherkinDocument: unknown;
74
60
  }
75
61
 
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
- };
62
+ interface PickleStep {
63
+ text: string;
64
+ argument?: { docString?: unknown };
90
65
  }
91
66
 
92
67
  /**
@@ -116,10 +91,10 @@ function attach(
116
91
  this: { test: { info: () => TestInfo } },
117
92
  body: string | Buffer,
118
93
  details?: { fileName?: string; mediaType?: string }
119
- ): void {
94
+ ): Promise<void> {
120
95
  const fileName = details?.fileName ?? DEFAULT_VALUES.ATTACHMENT_FILENAME;
121
96
  const contentType = details?.mediaType ?? DEFAULT_VALUES.CONTENT_TYPE;
122
- this.test.info().attach(fileName, { body, contentType });
97
+ return this.test.info().attach(fileName, { body, contentType });
123
98
  }
124
99
 
125
100
  /**
@@ -140,6 +115,134 @@ function stepName(pickleStep: {
140
115
  return step;
141
116
  }
142
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
+
143
246
  /**
144
247
  * Executes before all hooks
145
248
  */
@@ -180,20 +283,6 @@ function executeAfterAllHooks(
180
283
  });
181
284
  }
182
285
 
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.TEST_ID, description: testCase.id },
193
- { type: ANNOTATION_TYPES.TAGS, description: JSON.stringify(tags) }
194
- ];
195
- }
196
-
197
286
  /**
198
287
  * Main configuration function that sets up Cucumber adapter for Playwright
199
288
  */
@@ -202,178 +291,95 @@ export function defineConfig(config: any): any {
202
291
 
203
292
  const fixture = new supportCodeLibrary.World({config});
204
293
  const test: TestType<any, any> = fixture.test;
205
-
206
294
  executeBeforeAllHooks(test, supportCodeLibrary.beforeTestRunHookDefinitions);
207
295
 
208
- const worlds = new Map<string, any>();
209
-
210
296
  for (const feature of features) {
211
297
  const tests = feature.tests;
212
-
213
- test.describe(feature.feature as string, async () => {
214
- /**
215
- * Factory function to create and register world instances
216
- */
217
- const worldFactory = (fixtures: any): void => {
218
- const world = new supportCodeLibrary.World({
219
- log,
220
- attach,
221
- supportCodeLibrary,
222
- config
223
- });
224
- world.init(fixtures);
225
- worlds.set(getTestId(test.info()), world);
226
- };
227
-
228
- worldFactory.toString = (): string => fixture.init.toString();
229
-
230
- test.beforeEach(HOOK_NAMES.WORLD, worldFactory);
231
-
232
- test.beforeEach(HOOK_NAMES.BEFORE_HOOKS, async () => {
233
- const testId = getTestId(test.info());
234
- const world = worlds.get(testId);
235
- const testCase = tests.find((t: any) => t.id === testId);
236
-
237
- if (!testCase) {
238
- throw new Error(`Test case with ID ${testId} not found`);
239
- }
240
-
241
- for (const beforeHook of supportCodeLibrary.beforeTestCaseHookDefinitions) {
242
- if (beforeHook.appliesToTestCase(testCase)) {
243
- const hookName = beforeHook.name ?? HOOK_NAMES.BEFORE;
244
- const location = getLine(beforeHook);
245
- await test.step(
246
- hookName,
247
- () => beforeHook.code.apply(world, [{
248
- gherkinDocument: feature.gherkinDocument,
249
- pickle: testCase
250
- } as ITestCaseHookParameter]),
251
- location
252
- );
253
- }
254
- }
255
- });
298
+ test.describe(feature.feature as string, () => {
256
299
 
257
300
  for (const testCase of tests) {
258
- const tag = [...new Set(testCase.tags.map((tag: { name: string }) => tag.name))];
259
- const annotation = createTestAnnotations(testCase);
260
-
261
- test(testCase.name, {tag, annotation}, async () => {
262
- const world = worlds.get(testCase.id);
263
- const result: TestResult = { status: 'passed' };
264
-
265
- for (const pickleStep of testCase.steps) {
266
- if (result.status !== 'passed') {
267
- break;
268
- }
269
-
270
- const matchingSteps = supportCodeLibrary.stepDefinitions
271
- .filter((stepDef: any) => stepDef.matchesStepName(pickleStep.text));
272
-
273
- if (matchingSteps.length === 0) {
274
- throw new Error(
275
- `Step definition not found for: "${pickleStep.text}"`
276
- );
277
- }
278
-
279
- if (matchingSteps.length > 1) {
280
- throw new Error(
281
- `Ambiguous step "${pickleStep.text}" matches ${matchingSteps.length} definitions`
282
- );
283
- }
284
-
285
- const [step] = matchingSteps;
286
- const location = getLine(step);
287
- await test.step(stepName(pickleStep), async () => {
288
- for (const beforeStep of supportCodeLibrary.beforeTestStepHookDefinitions) {
289
- if (beforeStep.appliesToTestCase(testCase)) {
290
- const location = getLine(beforeStep);
291
- await test.step(
292
- HOOK_NAMES.BEFORE_STEP,
293
- () => beforeStep.code.apply(world, [{
294
- gherkinDocument: feature.gherkinDocument,
295
- pickle: testCase,
296
- pickleStep
297
- }]),
298
- location
299
- );
300
- }
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;
301
327
  }
302
- const {parameters} = await step.getInvocationParameters({
303
- step: {
304
- text: pickleStep.text,
305
- argument: pickleStep.argument
306
- },
307
- world
308
- } as any);
309
-
310
- try {
311
- await step.code.apply(world, parameters);
312
- } catch (err) {
313
- result.status = 'failed';
314
- result.error = err as Error;
315
- throw err;
316
- } finally {
317
- for (const afterStep of supportCodeLibrary.afterTestStepHookDefinitions) {
318
- if (afterStep.appliesToTestCase(testCase)) {
319
- const location = getLine(afterStep);
320
- await test.step(
321
- HOOK_NAMES.AFTER_STEP,
322
- () => afterStep.code.apply(world, [{
323
- gherkinDocument: feature.gherkinDocument,
324
- testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID,
325
- testStepId: DEFAULT_VALUES.TEST_STEP_ID,
326
- pickle: testCase,
327
- pickleStep,
328
- result: {
329
- duration: 0 as any,
330
- message: result.error?.message,
331
- status: result.status.toUpperCase(),
332
- exception: result.error
333
- }
334
- } as any]),
335
- location
336
- );
337
- }
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
+ );
338
365
  }
339
- }
340
- }, location);
341
- }
342
- })
343
- }
344
-
345
- test.afterEach(HOOK_NAMES.AFTER_HOOKS, async () => {
346
- const testInfo = test.info();
347
- const testId = getTestId(testInfo);
348
- const world = worlds.get(testId);
349
- const testCase = tests.find((t) => t.id === testId);
350
-
351
- if (!testCase) {
352
- throw new Error(`Test case with ID ${testId} not found`);
353
- }
354
-
355
- for (const afterHook of supportCodeLibrary.afterTestCaseHookDefinitions) {
356
- if (afterHook.appliesToTestCase(testCase)) {
357
- const hookName = afterHook.name ?? HOOK_NAMES.AFTER;
358
- const location = getLine(afterHook);
359
- await test.step(
360
- hookName,
361
- () => afterHook.code.apply(world, [{
362
- gherkinDocument: feature.gherkinDocument,
363
- testCaseStartedId: DEFAULT_VALUES.TEST_CASE_ID,
364
- pickle: testCase,
365
- result: getResult(testInfo)
366
- } as any]),
367
- location
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)
368
376
  );
369
377
  }
370
- }
371
- });
372
378
 
373
- test.afterEach(HOOK_NAMES.WORLD, async () => {
374
- const testId = getTestId(test.info());
375
- worlds.delete(testId);
376
- });
379
+ }
380
+ testBody.toString = () => fixture.init.toString();
381
+ test(testCase.name, {tag, annotation}, testBody);
382
+ }
377
383
 
378
384
  });
379
385
  }
@@ -0,0 +1,23 @@
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
+ }