@serenity-js/playwright-test 3.31.17 → 3.32.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -5
  3. package/lib/api/PlaywrightTestConfig.d.ts +7 -6
  4. package/lib/api/PlaywrightTestConfig.d.ts.map +1 -1
  5. package/lib/api/WorkerEventStreamReader.d.ts +13 -0
  6. package/lib/api/WorkerEventStreamReader.d.ts.map +1 -0
  7. package/lib/api/WorkerEventStreamReader.js +58 -0
  8. package/lib/api/WorkerEventStreamReader.js.map +1 -0
  9. package/lib/api/WorkerEventStreamWriter.d.ts +24 -0
  10. package/lib/api/WorkerEventStreamWriter.d.ts.map +1 -0
  11. package/lib/api/WorkerEventStreamWriter.js +86 -0
  12. package/lib/api/WorkerEventStreamWriter.js.map +1 -0
  13. package/lib/api/index.d.ts +1 -2
  14. package/lib/api/index.d.ts.map +1 -1
  15. package/lib/api/index.js +1 -2
  16. package/lib/api/index.js.map +1 -1
  17. package/lib/api/serenity-fixtures.d.ts +377 -0
  18. package/lib/api/serenity-fixtures.d.ts.map +1 -0
  19. package/lib/api/{SerenityOptions.js → serenity-fixtures.js} +1 -1
  20. package/lib/api/serenity-fixtures.js.map +1 -0
  21. package/lib/api/test-api.d.ts +27 -15
  22. package/lib/api/test-api.d.ts.map +1 -1
  23. package/lib/api/test-api.js +126 -104
  24. package/lib/api/test-api.js.map +1 -1
  25. package/lib/events/EventFactory.d.ts +16 -0
  26. package/lib/events/EventFactory.d.ts.map +1 -0
  27. package/lib/events/EventFactory.js +94 -0
  28. package/lib/events/EventFactory.js.map +1 -0
  29. package/lib/events/PlaywrightSceneId.d.ts +7 -0
  30. package/lib/events/PlaywrightSceneId.d.ts.map +1 -0
  31. package/lib/events/PlaywrightSceneId.js +19 -0
  32. package/lib/events/PlaywrightSceneId.js.map +1 -0
  33. package/lib/events/index.d.ts +3 -0
  34. package/lib/events/index.d.ts.map +1 -0
  35. package/lib/events/index.js +19 -0
  36. package/lib/events/index.js.map +1 -0
  37. package/lib/reporter/PlaywrightErrorParser.d.ts +7 -0
  38. package/lib/reporter/PlaywrightErrorParser.d.ts.map +1 -0
  39. package/lib/reporter/PlaywrightErrorParser.js +28 -0
  40. package/lib/reporter/PlaywrightErrorParser.js.map +1 -0
  41. package/lib/reporter/PlaywrightEventBuffer.d.ts +25 -0
  42. package/lib/reporter/PlaywrightEventBuffer.d.ts.map +1 -0
  43. package/lib/reporter/PlaywrightEventBuffer.js +147 -0
  44. package/lib/reporter/PlaywrightEventBuffer.js.map +1 -0
  45. package/lib/reporter/PlaywrightTestSceneIdFactory.d.ts +8 -0
  46. package/lib/reporter/PlaywrightTestSceneIdFactory.d.ts.map +1 -0
  47. package/lib/reporter/PlaywrightTestSceneIdFactory.js +15 -0
  48. package/lib/reporter/PlaywrightTestSceneIdFactory.js.map +1 -0
  49. package/lib/reporter/SerenityReporterForPlaywrightTest.d.ts +15 -22
  50. package/lib/reporter/SerenityReporterForPlaywrightTest.d.ts.map +1 -1
  51. package/lib/reporter/SerenityReporterForPlaywrightTest.js +62 -163
  52. package/lib/reporter/SerenityReporterForPlaywrightTest.js.map +1 -1
  53. package/lib/reporter/index.d.ts +0 -2
  54. package/lib/reporter/index.d.ts.map +1 -1
  55. package/lib/reporter/index.js +0 -2
  56. package/lib/reporter/index.js.map +1 -1
  57. package/package.json +9 -9
  58. package/src/api/PlaywrightTestConfig.ts +7 -6
  59. package/src/api/WorkerEventStreamReader.ts +27 -0
  60. package/src/api/WorkerEventStreamWriter.ts +117 -0
  61. package/src/api/index.ts +1 -2
  62. package/src/api/serenity-fixtures.ts +392 -0
  63. package/src/api/test-api.ts +187 -99
  64. package/src/events/EventFactory.ts +204 -0
  65. package/src/events/PlaywrightSceneId.ts +20 -0
  66. package/src/events/index.ts +2 -0
  67. package/src/reporter/PlaywrightErrorParser.ts +35 -0
  68. package/src/reporter/PlaywrightEventBuffer.ts +251 -0
  69. package/src/reporter/PlaywrightTestSceneIdFactory.ts +14 -0
  70. package/src/reporter/SerenityReporterForPlaywrightTest.ts +89 -250
  71. package/src/reporter/index.ts +0 -2
  72. package/lib/api/SerenityFixtures.d.ts +0 -130
  73. package/lib/api/SerenityFixtures.d.ts.map +0 -1
  74. package/lib/api/SerenityFixtures.js +0 -3
  75. package/lib/api/SerenityFixtures.js.map +0 -1
  76. package/lib/api/SerenityOptions.d.ts +0 -271
  77. package/lib/api/SerenityOptions.d.ts.map +0 -1
  78. package/lib/api/SerenityOptions.js.map +0 -1
  79. package/lib/reporter/DomainEventBuffer.d.ts +0 -11
  80. package/lib/reporter/DomainEventBuffer.d.ts.map +0 -1
  81. package/lib/reporter/DomainEventBuffer.js +0 -24
  82. package/lib/reporter/DomainEventBuffer.js.map +0 -1
  83. package/lib/reporter/PlaywrightAttachments.d.ts +0 -2
  84. package/lib/reporter/PlaywrightAttachments.d.ts.map +0 -1
  85. package/lib/reporter/PlaywrightAttachments.js +0 -5
  86. package/lib/reporter/PlaywrightAttachments.js.map +0 -1
  87. package/src/api/SerenityFixtures.ts +0 -132
  88. package/src/api/SerenityOptions.ts +0 -277
  89. package/src/reporter/DomainEventBuffer.ts +0 -28
  90. package/src/reporter/PlaywrightAttachments.ts +0 -1
@@ -1,3 +1,7 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+
1
5
  import type {
2
6
  Expect,
3
7
  Fixtures,
@@ -7,52 +11,42 @@ import type {
7
11
  PlaywrightWorkerOptions,
8
12
  TestInfo,
9
13
  TestType,
14
+ WorkerInfo,
10
15
  } from '@playwright/test';
11
16
  import { test as playwrightBaseTest } from '@playwright/test';
12
- import { AnsiDiffFormatter, Cast, Duration, serenity as serenityInstance, TakeNotes } from '@serenity-js/core';
17
+ import type { DiffFormatter } from '@serenity-js/core';
18
+ import { AnsiDiffFormatter, Cast, Clock, Duration, Serenity, TakeNotes } from '@serenity-js/core';
13
19
  import { SceneFinishes, SceneTagged } from '@serenity-js/core/lib/events';
14
20
  import { BrowserTag, PlatformTag } from '@serenity-js/core/lib/model';
15
21
  import { BrowseTheWebWithPlaywright, SerenitySelectorEngines } from '@serenity-js/playwright';
16
22
  import { CallAnApi } from '@serenity-js/rest';
17
23
  import { Photographer, TakePhotosOfFailures } from '@serenity-js/web';
18
- import * as os from 'os';
19
- import type { JSONValue } from 'tiny-types';
20
24
  import { ensure, isFunction, property } from 'tiny-types';
21
25
 
22
- import {
23
- DomainEventBuffer,
24
- PlaywrightStepReporter,
25
- SERENITY_JS_DOMAIN_EVENTS_ATTACHMENT_CONTENT_TYPE
26
- } from '../reporter';
26
+ import { PlaywrightSceneId } from '../events';
27
+ import { PlaywrightStepReporter, } from '../reporter';
28
+ import { PlaywrightTestSceneIdFactory } from '../reporter/PlaywrightTestSceneIdFactory';
27
29
  import { PerformActivitiesAsPlaywrightSteps } from './PerformActivitiesAsPlaywrightSteps';
28
- import type { SerenityFixtures } from './SerenityFixtures';
29
- import type { SerenityOptions } from './SerenityOptions';
30
+ import type { SerenityFixtures, SerenityWorkerFixtures } from './serenity-fixtures';
31
+ import { WorkerEventStreamWriter } from './WorkerEventStreamWriter';
30
32
 
31
- const serenitySelectorEngines = new SerenitySelectorEngines();
33
+ interface SerenityInternalFixtures {
34
+ configureScenarioInternal: void;
35
+ }
32
36
 
33
- export const fixtures: Fixtures<Omit<SerenityOptions, 'actors'> & SerenityFixtures, object, PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions> = {
34
- actors: [
35
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
36
- async ({ contextOptions, baseURL, extraHTTPHeaders, page, proxy }, use): Promise<void> => {
37
- await use(Cast.where(actor => actor.whoCan(
38
- BrowseTheWebWithPlaywright.usingPage(page, contextOptions),
39
- TakeNotes.usingAnEmptyNotepad(),
40
- CallAnApi.using({
41
- baseURL: baseURL,
42
- headers: extraHTTPHeaders,
43
- proxy: proxy && proxy?.server
44
- ? asProxyConfig(proxy)
45
- : undefined,
46
- }),
47
- )));
48
- },
49
- { option: true },
50
- ],
37
+ interface SerenityInternalWorkerFixtures {
38
+ configureWorkerInternal: void;
39
+ sceneIdFactoryInternal: PlaywrightTestSceneIdFactory;
40
+ diffFormatterInternal: DiffFormatter;
41
+ eventStreamWriterInternal: WorkerEventStreamWriter;
42
+ }
51
43
 
52
- playwright: async ({ playwright }, use) => {
53
- await serenitySelectorEngines.ensureRegisteredWith(playwright.selectors);
54
- await use(playwright);
55
- },
44
+ export const fixtures: Fixtures<SerenityFixtures & SerenityInternalFixtures, SerenityWorkerFixtures & SerenityInternalWorkerFixtures, PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions> = {
45
+
46
+ extraContextOptions: [
47
+ { defaultNavigationWaitUntil: 'load' },
48
+ { option: true }
49
+ ],
56
50
 
57
51
  defaultActorName: [
58
52
  'Serena',
@@ -70,14 +64,30 @@ export const fixtures: Fixtures<Omit<SerenityOptions, 'actors'> & SerenityFixtur
70
64
  ],
71
65
 
72
66
  crew: [
73
- [
74
- Photographer.whoWill(TakePhotosOfFailures)
75
- ],
67
+ [ Photographer.whoWill(TakePhotosOfFailures) ],
68
+ { option: true },
69
+ ],
70
+
71
+ actors: [
72
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
73
+ async ({ extraContextOptions, baseURL, extraHTTPHeaders, page, proxy }, use): Promise<void> => {
74
+ await use(Cast.where(actor => actor.whoCan(
75
+ BrowseTheWebWithPlaywright.usingPage(page, extraContextOptions),
76
+ TakeNotes.usingAnEmptyNotepad(),
77
+ CallAnApi.using({
78
+ baseURL: baseURL,
79
+ headers: extraHTTPHeaders,
80
+ proxy: proxy && proxy?.server
81
+ ? asProxyConfig(proxy)
82
+ : undefined,
83
+ }),
84
+ )));
85
+ },
76
86
  { option: true },
77
87
  ],
78
88
 
79
- // eslint-disable-next-line no-empty-pattern
80
- platform: async ({}, use) => {
89
+ // eslint-disable-next-line no-empty-pattern,@typescript-eslint/explicit-module-boundary-types
90
+ platform: [ async ({}, use) => {
81
91
  const platform = os.platform();
82
92
 
83
93
  // https://nodejs.org/api/process.html#process_process_platform
@@ -86,75 +96,148 @@ export const fixtures: Fixtures<Omit<SerenityOptions, 'actors'> & SerenityFixtur
86
96
  : (platform === 'darwin' ? 'macOS' : 'Linux');
87
97
 
88
98
  await use({ name, version: os.release() });
89
- },
99
+ }, { scope: 'worker' } ],
90
100
 
91
- serenity: async ({ crew, cueTimeout, interactionTimeout, platform }, use, info: TestInfo) => {
101
+ diffFormatterInternal: [
102
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,no-empty-pattern
103
+ async ({}, use) => {
104
+ const diffFormatter = new AnsiDiffFormatter();
105
+ await use(diffFormatter);
106
+ },
107
+ { scope: 'worker', box: true }
108
+ ],
92
109
 
93
- const domainEventBuffer = new DomainEventBuffer();
110
+ sceneIdFactoryInternal: [
111
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,no-empty-pattern
112
+ async ({ }, use) => {
113
+ await use(new PlaywrightTestSceneIdFactory());
114
+ },
115
+ { scope: 'worker', box: true },
116
+ ],
94
117
 
95
- serenityInstance.configure({
96
- diffFormatter: new AnsiDiffFormatter(),
97
- cueTimeout: asDuration(cueTimeout),
98
- interactionTimeout: asDuration(interactionTimeout),
99
- crew: [
100
- ...crew,
101
- domainEventBuffer,
102
- new PlaywrightStepReporter(info),
103
- ],
104
- });
118
+ serenity: [
119
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
120
+ async ({ playwright, sceneIdFactoryInternal }, use, workerInfo) => {
121
+ const clock = new Clock();
122
+ const cwd = process.cwd();
123
+ const serenity = new Serenity(clock, cwd, sceneIdFactoryInternal);
105
124
 
106
- serenityInstance.announce(new SceneTagged(
107
- serenityInstance.currentSceneId(),
108
- new PlatformTag(platform.name, platform.version),
109
- serenityInstance.currentTime(),
110
- ));
125
+ const serenitySelectorEngines = new SerenitySelectorEngines();
126
+ await serenitySelectorEngines.ensureRegisteredWith(playwright.selectors);
111
127
 
112
- await use(serenityInstance);
128
+ await use(serenity);
129
+ },
130
+ { scope: 'worker', box: true }
131
+ ],
113
132
 
114
- const serialisedEvents: Array<{ type: string, value: JSONValue }> = [];
133
+ eventStreamWriterInternal: [
134
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,no-empty-pattern
135
+ async ({ }, use, workerInfo) => {
115
136
 
116
- for (const event of domainEventBuffer.flush()) {
117
- serialisedEvents.push({
118
- type: event.constructor.name,
119
- value: event.toJSON(),
120
- });
137
+ const serenityOutputDirectory = path.join(workerInfo.project.outputDir, 'serenity');
121
138
 
122
- if (event instanceof SceneTagged) {
123
- test.info().annotations.push({ type: event.tag.type, description: event.tag.name });
124
- }
125
- }
139
+ const eventStreamWriter = new WorkerEventStreamWriter(
140
+ serenityOutputDirectory,
141
+ workerInfo,
142
+ );
126
143
 
127
- info.attach('serenity-js-events.json', {
128
- contentType: SERENITY_JS_DOMAIN_EVENTS_ATTACHMENT_CONTENT_TYPE,
129
- body: Buffer.from(JSON.stringify(serialisedEvents), 'utf8'),
130
- });
131
- },
144
+ await use(eventStreamWriter);
145
+ },
146
+ { scope: 'worker', box: true },
147
+ ],
132
148
 
133
- actorCalled: async ({ serenity, actors, browser, browserName, contextOptions }, use) => {
149
+ configureWorkerInternal: [
150
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
151
+ async ({ diffFormatterInternal, eventStreamWriterInternal, sceneIdFactoryInternal, serenity, browser }, use, info: WorkerInfo) => {
152
+
153
+ serenity.configure({
154
+ actors: Cast.where(actor => actor.whoCan(
155
+ BrowseTheWebWithPlaywright.using(browser),
156
+ TakeNotes.usingAnEmptyNotepad(),
157
+ // todo: consider making `axios` a fixture and injecting an ability to CallAnApi
158
+ )),
159
+ crew: [
160
+ eventStreamWriterInternal,
161
+ ],
162
+ diffFormatter: diffFormatterInternal,
163
+ });
134
164
 
135
- const sceneId = serenity.currentSceneId();
165
+ sceneIdFactoryInternal.setTestId(`worker-${ info.workerIndex }`);
166
+ const workerBeforeAllSceneId = serenity.assignNewSceneId();
136
167
 
137
- serenity.engage(asCast(actors));
168
+ await use(void 0);
169
+
170
+ await eventStreamWriterInternal.persistAll(workerBeforeAllSceneId);
171
+ },
172
+ { scope: 'worker', auto: true, box: true },
173
+ ],
138
174
 
139
- const actorCalled = (name: string) => {
140
- const actor = serenity.theActorCalled(name);
141
- return actor.whoCan(new PerformActivitiesAsPlaywrightSteps(actor, serenity, it));
142
- };
175
+ configureScenarioInternal: [
176
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
177
+ async ({ actors, browser, browserName, crew, cueTimeout, diffFormatterInternal, eventStreamWriterInternal, interactionTimeout, platform, sceneIdFactoryInternal, serenity }, use, info: TestInfo) => {
178
+
179
+ serenity.configure({
180
+ actors: asCast(actors),
181
+ diffFormatter: diffFormatterInternal,
182
+ cueTimeout: asDuration(cueTimeout),
183
+ interactionTimeout: asDuration(interactionTimeout),
184
+ crew: [
185
+ ...crew,
186
+ new PlaywrightStepReporter(info),
187
+ ],
188
+ });
143
189
 
144
- serenity.announce(new SceneTagged(
145
- sceneId,
146
- new BrowserTag(browserName, browser.version()),
147
- serenity.currentTime(),
148
- ));
190
+ const playwrightSceneId = PlaywrightSceneId.from(
191
+ info.project.name,
192
+ { id: info.testId, repeatEachIndex: info.repeatEachIndex },
193
+ { retry: info.retry }
194
+ );
195
+
196
+ sceneIdFactoryInternal.setTestId(playwrightSceneId.value);
197
+ const sceneId = serenity.assignNewSceneId();
198
+
199
+ serenity.announce(
200
+ new SceneTagged(
201
+ sceneId,
202
+ new PlatformTag(platform.name, platform.version),
203
+ serenity.currentTime(),
204
+ ),
205
+ new SceneTagged(
206
+ sceneId,
207
+ new BrowserTag(browserName, browser.version()),
208
+ serenity.currentTime(),
209
+ )
210
+ );
211
+
212
+ await use(void 0);
213
+
214
+ try {
215
+ serenity.announce(
216
+ new SceneFinishes(sceneId, serenity.currentTime()),
217
+ );
218
+
219
+ await serenity.waitForNextCue();
220
+ }
221
+ finally {
222
+ await eventStreamWriterInternal.persist(playwrightSceneId.value);
223
+ }
224
+ },
225
+ { auto: true, box: true, }
226
+ ],
149
227
 
150
- await use(actorCalled);
228
+ actorCalled: [
229
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
230
+ async ({ serenity }, use) => {
151
231
 
152
- serenity.announce(
153
- new SceneFinishes(sceneId, serenity.currentTime()),
154
- );
232
+ const actorCalled = (name: string) => {
233
+ const actor = serenity.theActorCalled(name);
234
+ return actor.whoCan(new PerformActivitiesAsPlaywrightSteps(actor, serenity, it));
235
+ };
155
236
 
156
- await serenityInstance.waitForNextCue();
157
- },
237
+ await use(actorCalled);
238
+ },
239
+ { scope: 'worker' },
240
+ ],
158
241
 
159
242
  actor: async ({ actorCalled, defaultActorName }, use) => {
160
243
  await use(actorCalled(defaultActorName));
@@ -191,14 +274,17 @@ export type TestApi<TestArgs extends Record<string, any>, WorkerArgs extends Rec
191
274
  *
192
275
  * Shorthand for [`useBase`](https://serenity-js.org/api/playwright-test/function/useBase/)
193
276
  */
194
- useFixtures: <T extends Record<string, any>, W extends Record<string, any> = object>(customFixtures: Fixtures<T, W, TestArgs, WorkerArgs>) => TestApi<TestArgs & T, WorkerArgs & W>,
277
+ useFixtures: <T extends Record<string, any>, W extends Record<string, any> = object>(
278
+ customFixtures: Fixtures<T, W, TestArgs, WorkerArgs>
279
+ ) => TestApi<TestArgs & T, WorkerArgs & W>,
280
+
195
281
  it: TestType<TestArgs, WorkerArgs>,
196
282
  test: TestType<TestArgs, WorkerArgs>,
197
283
  }
198
284
 
199
- function createTestApi<TestArgs extends Record<string, any>, WorkerArgs extends Record<string, any> = object>(baseTest: TestType<TestArgs, WorkerArgs>): TestApi<TestArgs, WorkerArgs> {
285
+ function createTestApi<BaseTestFixtures extends (PlaywrightTestArgs & PlaywrightTestOptions), BaseWorkerFixtures extends (PlaywrightWorkerArgs & PlaywrightWorkerOptions)>(baseTest: TestType<BaseTestFixtures, BaseWorkerFixtures>): TestApi<BaseTestFixtures, BaseWorkerFixtures> {
200
286
  return {
201
- useFixtures<T extends Record<string, any>, W extends Record<string, any> = object>(customFixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestApi<TestArgs & T, WorkerArgs & W> {
287
+ useFixtures<T extends Record<string, any>, W extends Record<string, any> = object>(customFixtures: Fixtures<T, W, BaseTestFixtures, BaseWorkerFixtures>): TestApi<BaseTestFixtures & T, BaseWorkerFixtures & W> {
202
288
  return createTestApi(baseTest.extend(customFixtures));
203
289
  },
204
290
  beforeAll: baseTest.beforeAll,
@@ -212,7 +298,7 @@ function createTestApi<TestArgs extends Record<string, any>, WorkerArgs extends
212
298
  };
213
299
  }
214
300
 
215
- const api = createTestApi(playwrightBaseTest).useFixtures(fixtures);
301
+ const api = createTestApi(playwrightBaseTest).useFixtures<SerenityFixtures, SerenityWorkerFixtures>(fixtures);
216
302
 
217
303
  /**
218
304
  * Declares a single test scenario.
@@ -478,11 +564,13 @@ export const useFixtures = api.useFixtures;
478
564
  *
479
565
  * @param baseTest
480
566
  */
481
- export function useBase<TestArgs extends Record<string, any>, WorkerArgs extends Record<string, any> = object>(
482
- baseTest: TestType<TestArgs, WorkerArgs>
483
- ): TestApi<Omit<SerenityOptions, 'actors'> & SerenityFixtures & TestArgs, WorkerArgs> {
484
- return createTestApi(baseTest)
485
- .useFixtures<TestArgs & Omit<SerenityOptions, 'actors'> & SerenityFixtures, WorkerArgs>(fixtures);
567
+ export function useBase<
568
+ BaseTestFixtures extends (PlaywrightTestArgs & PlaywrightTestOptions),
569
+ BaseWorkerFixtures extends (PlaywrightWorkerArgs & PlaywrightWorkerOptions)
570
+ > (baseTest: TestType<BaseTestFixtures, BaseWorkerFixtures>): TestApi<BaseTestFixtures & SerenityFixtures, BaseWorkerFixtures & SerenityWorkerFixtures> {
571
+ return createTestApi<BaseTestFixtures, BaseWorkerFixtures>(baseTest).useFixtures(
572
+ fixtures as Fixtures<SerenityFixtures, SerenityWorkerFixtures, BaseTestFixtures, BaseWorkerFixtures>
573
+ );
486
574
  }
487
575
 
488
576
  /**
@@ -0,0 +1,204 @@
1
+ import type { FullProject } from '@playwright/test/reporter';
2
+ import { type TestCase, type TestResult } from '@playwright/test/reporter';
3
+ import { Duration, Timestamp } from '@serenity-js/core';
4
+ import {
5
+ type DomainEvent,
6
+ SceneFinished,
7
+ SceneParametersDetected,
8
+ SceneSequenceDetected,
9
+ SceneStarts,
10
+ SceneTagged,
11
+ SceneTemplateDetected,
12
+ TestRunnerDetected
13
+ } from '@serenity-js/core/lib/events';
14
+ import { FileSystem, FileSystemLocation, Path, RequirementsHierarchy } from '@serenity-js/core/lib/io';
15
+ import type { Outcome, Tag } from '@serenity-js/core/lib/model';
16
+ import {
17
+ Category,
18
+ Description,
19
+ Name,
20
+ ProjectTag,
21
+ ScenarioDetails,
22
+ ScenarioParameters,
23
+ Tags
24
+ } from '@serenity-js/core/lib/model';
25
+
26
+ import { PlaywrightSceneId } from './PlaywrightSceneId';
27
+
28
+ export class EventFactory {
29
+ private requirementsHierarchy: RequirementsHierarchy;
30
+
31
+ constructor(rootDirectory: Path) {
32
+ this.requirementsHierarchy = new RequirementsHierarchy(
33
+ new FileSystem(rootDirectory),
34
+ );
35
+ }
36
+
37
+ createSceneStartEvents(test: TestCase, result: TestResult): DomainEvent[] {
38
+ const sceneId = PlaywrightSceneId.from(test.parent.project()?.name, test, result);
39
+ const startTime = new Timestamp(result.startTime);
40
+
41
+ const project: FullProject | undefined = test.parent.project();
42
+ const projectName = project?.name ?? '';
43
+
44
+ const scenarioDetails = this.scenarioDetailsFrom(test);
45
+
46
+ const allTags = this.tagsFrom(
47
+ scenarioDetails,
48
+ test.tags,
49
+ );
50
+
51
+ if (projectName) {
52
+ allTags.push(new ProjectTag(projectName));
53
+ }
54
+
55
+ const events: DomainEvent[] = [];
56
+
57
+ if (test.retries > 0) {
58
+ events.push(
59
+ ...this.createSceneSequenceEvents(
60
+ sceneId,
61
+ startTime,
62
+ scenarioDetails,
63
+ test,
64
+ result
65
+ )
66
+ );
67
+ }
68
+
69
+ events.push(
70
+ new SceneStarts(sceneId,
71
+ scenarioDetails,
72
+ startTime
73
+ ),
74
+ new TestRunnerDetected(sceneId, new Name('Playwright'), startTime),
75
+ ...allTags.map(tag => new SceneTagged(sceneId, tag, startTime))
76
+ )
77
+
78
+ return events;
79
+ }
80
+
81
+ private createSceneSequenceEvents(
82
+ sceneId: PlaywrightSceneId,
83
+ startTime: Timestamp,
84
+ scenarioDetails: ScenarioDetails,
85
+ test: TestCase,
86
+ result: TestResult
87
+ ): DomainEvent[] {
88
+
89
+ const attempt = result.retry + 1;
90
+ const parameters = {
91
+ Retries: `Attempt #${ attempt }`
92
+ };
93
+
94
+ return [
95
+ new SceneSequenceDetected(sceneId, scenarioDetails, startTime),
96
+ new SceneTemplateDetected(
97
+ sceneId,
98
+ new Description(''),
99
+ startTime,
100
+ ),
101
+ new SceneParametersDetected(
102
+ sceneId,
103
+ scenarioDetails,
104
+ new ScenarioParameters(
105
+ new Name(''),
106
+ new Description(`Max retries: ${ test.retries }`),
107
+ parameters,
108
+ )
109
+ ),
110
+ ];
111
+ }
112
+
113
+ createSceneFinishedEvent(test: TestCase, result: TestResult, scenarioOutcome: Outcome): SceneFinished {
114
+ const sceneId = PlaywrightSceneId.from(test.parent.project()?.name, test, result);
115
+ const duration = Duration.ofMilliseconds(result.duration);
116
+ const sceneEndTime = new Timestamp(result.startTime).plus(duration);
117
+
118
+ const scenarioDetails = this.scenarioDetailsFrom(test);
119
+
120
+ return new SceneFinished(
121
+ sceneId,
122
+ scenarioDetails,
123
+ scenarioOutcome,
124
+ sceneEndTime,
125
+ );
126
+ }
127
+
128
+ private uniqueTags(...tags: Tag[]) {
129
+ const uniqueTags: Record<string, Tag> = { };
130
+
131
+ for (const tag of tags) {
132
+ const { name, type } = tag.toJSON();
133
+ const key = `${ name } ${ type }`;
134
+ uniqueTags[key] = tag;
135
+ }
136
+
137
+ return Object.values(uniqueTags);
138
+ }
139
+
140
+ private scenarioDetailsFrom(test: Pick<TestCase, 'titlePath' | 'location' | 'parent' | 'repeatEachIndex'>): ScenarioDetails {
141
+
142
+ const { featureName, name } = this.scenarioMetadataFrom(test);
143
+ const { file, line, column } = test.location;
144
+
145
+ const nameWithoutTags = Tags.stripFrom(name);
146
+
147
+ const repetitionSuffix = test.repeatEachIndex
148
+ ? ` - Repetition ${ test.repeatEachIndex }`
149
+ : '';
150
+
151
+ const scenarioName = `${ nameWithoutTags }${ repetitionSuffix }`;
152
+
153
+ return new ScenarioDetails(
154
+ new Name(scenarioName),
155
+ new Category(Tags.stripFrom(featureName)),
156
+ new FileSystemLocation(Path.from(file), line, column),
157
+ );
158
+ }
159
+
160
+ private scenarioMetadataFrom(test: Pick<TestCase, 'titlePath' | 'location'>): { featureName: string, name: string } {
161
+ const [
162
+ root_,
163
+ browserName_,
164
+ fileName,
165
+ describeOrItBlockTitle,
166
+ ...nestedTitles
167
+ ] = test.titlePath();
168
+
169
+ const scenarioName = nestedTitles.join(' ').trim();
170
+
171
+ const name = scenarioName || describeOrItBlockTitle;
172
+ const featureName = scenarioName ? describeOrItBlockTitle : fileName;
173
+
174
+ return {
175
+ featureName,
176
+ name,
177
+ };
178
+ }
179
+
180
+ private tagsFrom(scenarioDetails: ScenarioDetails, extraTagValues: string[]): Tag[] {
181
+
182
+ const tagsFromRequirementsHierarchy = this.requirementsHierarchy.requirementTagsFor(
183
+ scenarioDetails.location.path,
184
+ scenarioDetails.category.value,
185
+ );
186
+
187
+ const tagsFromTitle = Tags.from([
188
+ scenarioDetails.category.value,
189
+ scenarioDetails.name.value,
190
+ ].join(' '));
191
+
192
+ const extraTags = extraTagValues
193
+ .filter(Boolean)
194
+ .flatMap(tagValue => Tags.from(tagValue));
195
+
196
+ return [
197
+ ...tagsFromRequirementsHierarchy,
198
+ ...this.uniqueTags(
199
+ ...tagsFromTitle,
200
+ ...extraTags,
201
+ )
202
+ ]
203
+ }
204
+ }
@@ -0,0 +1,20 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import type { FullProject, TestCase, TestResult } from '@playwright/test/reporter';
4
+ import { CorrelationId } from '@serenity-js/core/lib/model';
5
+
6
+ export class PlaywrightSceneId extends CorrelationId {
7
+
8
+ static override fromJSON(v: string): CorrelationId {
9
+ return new PlaywrightSceneId(v);
10
+ }
11
+
12
+ static from(projectName: FullProject['name'], test: Pick<TestCase, 'id' | 'repeatEachIndex'>, result: Pick<TestResult, 'retry'>): CorrelationId {
13
+ const projectId = createHash('sha1')
14
+ .update(projectName)
15
+ .digest('hex')
16
+ .slice(0, 10);
17
+
18
+ return new PlaywrightSceneId(`${ test.id }-${ projectId }-${ test.repeatEachIndex }-${ result.retry }`);
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ export * from './EventFactory';
2
+ export * from './PlaywrightSceneId';
@@ -0,0 +1,35 @@
1
+ import type { TestError } from '@playwright/test/reporter';
2
+
3
+ export class PlaywrightErrorParser {
4
+ private static ascii = new RegExp(
5
+ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', // eslint-disable-line no-control-regex
6
+ 'g',
7
+ );
8
+
9
+ public errorFrom(testError: TestError): Error {
10
+ const message = testError.message && PlaywrightErrorParser.stripAsciiFrom(testError.message);
11
+
12
+ let stack = testError.stack && PlaywrightErrorParser.stripAsciiFrom(testError.stack);
13
+
14
+ // TODO: Do I need to process it?
15
+ // const value = testError.value;
16
+
17
+ const prologue = `Error: ${ message }`;
18
+ if (stack && message && stack.startsWith(prologue)) {
19
+ stack = stack.slice(prologue.length);
20
+ }
21
+
22
+ if (testError.cause) {
23
+ stack += `\nCaused by: ${ this.errorFrom(testError.cause).stack }`;
24
+ }
25
+
26
+ const error = new Error(message);
27
+ error.stack = stack;
28
+
29
+ return error;
30
+ }
31
+
32
+ private static stripAsciiFrom(text: string): string {
33
+ return text.replace(this.ascii, '');
34
+ }
35
+ }