@serenity-js/playwright-test 3.36.2 → 3.37.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,4 +1,6 @@
1
1
  import type {
2
+ Ability,
3
+ Actor,
2
4
  Cast,
3
5
  ClassDescription,
4
6
  Duration,
@@ -6,8 +8,9 @@ import type {
6
8
  StageCrewMember,
7
9
  StageCrewMemberBuilder
8
10
  } from '@serenity-js/core';
9
- import type { Actor } from '@serenity-js/core';
10
11
  import type { ExtraBrowserContextOptions } from '@serenity-js/playwright';
12
+ import type { AxiosRequestConfigDefaults } from '@serenity-js/rest';
13
+ import { type AxiosInstance } from 'axios';
11
14
 
12
15
  /**
13
16
  * Serenity/JS-specific [Playwright Test fixtures](https://playwright.dev/docs/test-fixtures)
@@ -241,31 +244,92 @@ export interface SerenityFixtures {
241
244
  */
242
245
  extraContextOptions: Partial<ExtraBrowserContextOptions>;
243
246
 
247
+ /**
248
+ * Extra abilities given to the actors on top of the default ones provided by the [`actors`](https://serenity-js.org/api/playwright-test/interface/SerenityFixtures/#actors) fixture.
249
+ *
250
+ * #### Extra abilities for all actors
251
+ *
252
+ * To give the same set of extra abilities to all the actors, make your `extraAbilities` fixture
253
+ * return an array of [`Ability`](https://serenity-js.org/api/core/class/Ability/) objects.
254
+ *
255
+ * ```typescript
256
+ * import { type Ability } from '@serenity-js/core';
257
+ * import { describe, it, test } from '@serenity-js/playwright-test';
258
+ * import { MyAbility } from './MyAbility';
259
+ *
260
+ * describe(`My feature`, () => {
261
+ *
262
+ * test.use({
263
+ * extraAbilities: [ new MyAbility() ]
264
+ * });
265
+ *
266
+ * it(`...`, async({ actor }) => {
267
+ * // ...
268
+ * });
269
+ * });
270
+ * ```
271
+ *
272
+ * #### Extra abilities for selected actors
273
+ *
274
+ * To give extra abilities only to selected actors, make your `extraAbilities` fixture return a `(actorName: string) => Ability[]` function that maps
275
+ * the actor name to an array of [`Ability`](https://serenity-js.org/api/core/class/Ability/) objects.
276
+ *
277
+ * ```typescript
278
+ * import { describe, it, test } from '@serenity-js/playwright-test';
279
+ * import { MyAbility } from './MyAbility';
280
+ *
281
+ * describe(`My feature`, () => {
282
+ *
283
+ * test.use({
284
+ * extraAbilities: async ({ }, use) => {
285
+ * await use((actorName: string) => {
286
+ * // Alice gets the extra abilities, but others don't
287
+ * return actorName === 'Alice'
288
+ * ? [ new MyAbility() ]
289
+ * : [];
290
+ * })
291
+ * }
292
+ * });
293
+ *
294
+ * it(`...`, async({ actor }) => {
295
+ * // ...
296
+ * });
297
+ * });
298
+ * ```
299
+ */
300
+ extraAbilities: ((actorName: string) => Ability[]) | Ability[];
301
+
244
302
  /**
245
303
  * A cast of Serenity/JS actors to be used instead of the default cast
246
304
  * when instantiating [`actor`](https://serenity-js.org/api/playwright-test/interface/SerenityFixtures/#actor)
247
305
  * and invoking [`actorCalled`](https://serenity-js.org/api/playwright-test/interface/SerenityWorkerFixtures/#actorCalled).
248
306
  *
249
307
  * :::info Did you know?
250
- * When you use `@serenity-js/playwright-test` [test APIs](https://serenity-js.org/api/playwright-test/function/it/), Serenity/JS already provides a default cast of actors for you.
251
- * Each one of the default actors receives [abilities](https://serenity-js.org/api/core/class/Ability/) to [`BrowseTheWebWithPlaywright`](https://serenity-js.org/api/playwright/class/BrowseTheWebWithPlaywright/) and [`TakeNotes.usingAnEmptyNotepad`](https://serenity-js.org/api/core/class/TakeNotes/#usingAnEmptyNotepad).
252
- *
253
- * The default abilities should be sufficient in most web testing scenarios. However, you might want to override this default configuration
254
- * when you need your actors to [interact with REST APIs](https://serenity-js.org/api/rest/class/CallAnApi/),
255
- * [manage local servers](https://serenity-js.org/api/local-server/class/ManageALocalServer/),
256
- * start with a notepad that has some [initial state](https://serenity-js.org/api/core/class/TakeNotes/#using),
257
- * or receive [custom abilities](https://serenity-js.org/api/core/class/Ability/).
258
- * :::
308
+ * Serenity/JS [test APIs](https://serenity-js.org/api/playwright-test/function/it/) offer fixtures that set up the default cast of actors for you,
309
+ * which should be sufficient in most web and HTTP API testing scenarios.
310
+ *
311
+ * Each one of the default actors receives the following [abilities](https://serenity-js.org/api/core/class/Ability/):
312
+ * - [`BrowseTheWebWithPlaywright`](https://serenity-js.org/api/playwright/class/BrowseTheWebWithPlaywright/), connected to Playwright [`page`](https://playwright.dev/docs/test-fixtures) fixture
313
+ * - [`TakeNotes`](https://serenity-js.org/api/core/class/TakeNotes/#usingAnEmptyNotepad)
314
+ * - [`CallAnApi`](https://serenity-js.org/api/rest/class/CallAnApi/), pointing at the [`baseURL`](https://playwright.dev/docs/test-use-options#basic-options)
315
+ * and using any `proxy` settings in your Playwright config file.
259
316
  *
317
+ * The easiest way to give your actors additional abilities is to use the [`extraAbilities`](https://serenity-js.org/api/playwright-test/interface/SerenityFixtures/#extraAbilities) fixture.
318
+ * :::
260
319
  *
261
320
  * #### Overriding the default cast of Serenity/JS actors
262
321
  *
263
322
  * ```typescript
264
- * import { Cast, TakeNotes } from '@serenity-js/core'
265
323
  * import { Ensure, equals } from '@serenity-js/assertions'
324
+ * import { Cast, TakeNotes } from '@serenity-js/core'
266
325
  * import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright'
326
+ * import { CallAnApi } from '@serenity-js/rest'
267
327
  * import { describe, it, test } from '@serenity-js/playwright-test'
268
328
  *
329
+ * interface MyNotes {
330
+ * username: string;
331
+ * }
332
+ *
269
333
  * describe(`Recording items`, () => {
270
334
  *
271
335
  * test.use({
@@ -273,17 +337,24 @@ export interface SerenityFixtures {
273
337
  * defaultNavigationTimeout: 30_000,
274
338
  * },
275
339
  *
276
- * defaultActorName: 'Serena',
277
- * actors: async ({ browser, contextOptions, extraContextOptions }, use) => {
278
- * const cast = Cast.where(actor =>
279
- * actor.whoCan(
280
- * BrowseTheWebWithPlaywright.using(browser, contextOptions, extraContextOptions),
281
- * TakeNotes.usingAnEmptyNotepad(),
340
+ * actors: async ({ axios, extraAbilities, extraContextOptions, extraHTTPHeaders, page }, use) => {
341
+ * const cast = Cast.where(actor => {
342
+ * const abilities = Array.isArray(extraAbilities)
343
+ * ? extraAbilities
344
+ * : extraAbilities(actor.name);
345
+ *
346
+ * return actor.whoCan(
347
+ * BrowseTheWebWithPlaywright.usingPage(page, extraContextOptions),
348
+ * TakeNotes.using<MyNotes>(Notepad.with({
349
+ * username: 'example.username'
350
+ * }),
351
+ * CallAnApi.using(axios),
352
+ * ...abilities,
282
353
  * )
283
- * )
354
+ * })
284
355
  *
285
356
  * // Make sure to pass your custom cast to Playwright `use` callback
286
- * await use(cast)
357
+ * await use(cast);
287
358
  * },
288
359
  * })
289
360
  *
@@ -302,7 +373,7 @@ export interface SerenityFixtures {
302
373
  * })
303
374
  * })
304
375
  * })
305
- *
376
+ * ```
306
377
  *
307
378
  * #### Learn more
308
379
  * - Declaring a Serenity/JS [test scenario](https://serenity-js.org/api/playwright-test/function/it/)
@@ -322,6 +393,18 @@ export interface SerenityFixtures {
322
393
  * - Declaring a Serenity/JS [test scenario](https://serenity-js.org/api/playwright-test/function/it/)
323
394
  */
324
395
  actor: Actor;
396
+
397
+ /**
398
+ * An instance of the Axios HTTP client, or default Axios request configurations,
399
+ * to be used by the [`CallAnApi`](https://serenity-js.org/api/rest/class/CallAnApi/) ability,
400
+ * provided to the actors via the [`actors`](https://serenity-js.org/api/playwright-test/interface/SerenityFixtures/#actors) fixture.
401
+ *
402
+ * By default, Serenity/JS configures Axios to use the following settings from your Playwright configuration file:
403
+ * - [`baseURL`](https://playwright.dev/docs/api/class-testoptions#test-options-base-url)
404
+ * - [`proxy`](https://playwright.dev/docs/api/class-testoptions#test-options-proxy)
405
+ * - [`extraHTTPHeaders`](https://playwright.dev/docs/api/class-testoptions#test-options-extra-http-headers)
406
+ */
407
+ axios: AxiosInstance | AxiosRequestConfigDefaults;
325
408
  }
326
409
 
327
410
  /**
@@ -370,7 +453,9 @@ export interface SerenityWorkerFixtures {
370
453
  serenity: Serenity;
371
454
 
372
455
  /**
373
- * Uses the provided [cast](https://serenity-js.org/api/core/class/Cast/) of [`actors`](https://serenity-js.org/api/playwright-test/interface/SerenityFixtures/#actors) to instantiate an [`Actor`](https://serenity-js.org/api/core/class/Actor/) called `name`
456
+ * Uses the provided [cast](https://serenity-js.org/api/core/class/Cast/) of
457
+ * [`actors`](https://serenity-js.org/api/playwright-test/interface/SerenityFixtures/#actors) to instantiate
458
+ * an [`Actor`](https://serenity-js.org/api/core/class/Actor/) called `name`
374
459
  * and inject it into a [test scenario](https://serenity-js.org/api/playwright-test/function/it/).
375
460
  *
376
461
  * Retrieves an existing actor if one has already been instantiated.
@@ -13,7 +13,7 @@ import type {
13
13
  TestType,
14
14
  WorkerInfo,
15
15
  } from '@playwright/test';
16
- import { test as playwrightBaseTest } from '@playwright/test';
16
+ import { mergeTests, test as playwrightBaseTest } from '@playwright/test';
17
17
  import type { DiffFormatter } from '@serenity-js/core';
18
18
  import { AnsiDiffFormatter, Cast, Clock, Duration, Serenity, TakeNotes } from '@serenity-js/core';
19
19
  import { SceneFinishes, SceneTagged } from '@serenity-js/core/lib/events';
@@ -68,20 +68,32 @@ export const fixtures: Fixtures<SerenityFixtures & SerenityInternalFixtures, Ser
68
68
  { option: true },
69
69
  ],
70
70
 
71
+ axios: async ({ baseURL, extraHTTPHeaders, proxy }, use) => {
72
+ await use({
73
+ baseURL: baseURL,
74
+ headers: extraHTTPHeaders,
75
+ proxy: proxy && proxy?.server
76
+ ? asProxyConfig(proxy)
77
+ : undefined,
78
+ })
79
+ },
80
+
71
81
  actors: [
72
82
  // 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
- )));
83
+ async ({ axios, extraAbilities, extraContextOptions, page }, use): Promise<void> => {
84
+ await use(Cast.where(actor => {
85
+
86
+ const abilities = Array.isArray(extraAbilities)
87
+ ? extraAbilities
88
+ : extraAbilities(actor.name);
89
+
90
+ return actor.whoCan(
91
+ BrowseTheWebWithPlaywright.usingPage(page, extraContextOptions),
92
+ TakeNotes.usingAnEmptyNotepad(),
93
+ CallAnApi.using(axios),
94
+ ...abilities,
95
+ );
96
+ }));
85
97
  },
86
98
  { option: true },
87
99
  ],
@@ -109,7 +121,7 @@ export const fixtures: Fixtures<SerenityFixtures & SerenityInternalFixtures, Ser
109
121
 
110
122
  sceneIdFactoryInternal: [
111
123
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,no-empty-pattern
112
- async ({ }, use) => {
124
+ async ({}, use) => {
113
125
  await use(new PlaywrightTestSceneIdFactory());
114
126
  },
115
127
  { scope: 'worker', box: true },
@@ -132,7 +144,7 @@ export const fixtures: Fixtures<SerenityFixtures & SerenityInternalFixtures, Ser
132
144
 
133
145
  eventStreamWriterInternal: [
134
146
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,no-empty-pattern
135
- async ({ }, use, workerInfo) => {
147
+ async ({}, use, workerInfo) => {
136
148
 
137
149
  const serenityOutputDirectory = path.join(workerInfo.project.outputDir, 'serenity');
138
150
 
@@ -225,12 +237,18 @@ export const fixtures: Fixtures<SerenityFixtures & SerenityInternalFixtures, Ser
225
237
  { auto: true, box: true, }
226
238
  ],
227
239
 
240
+ extraAbilities: [
241
+ [],
242
+ { option: true },
243
+ ],
244
+
228
245
  actorCalled: [
229
246
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
230
247
  async ({ serenity }, use) => {
231
248
 
232
249
  const actorCalled = (name: string) => {
233
250
  const actor = serenity.theActorCalled(name);
251
+
234
252
  return actor.whoCan(new PerformActivitiesAsPlaywrightSteps(actor, serenity, it));
235
253
  };
236
254
 
@@ -247,7 +265,7 @@ export const fixtures: Fixtures<SerenityFixtures & SerenityInternalFixtures, Ser
247
265
  /**
248
266
  * Serenity/JS BDD-style test API created by [`useBase`](https://serenity-js.org/api/playwright-test/function/useBase/).
249
267
  */
250
- export type TestApi<TestArgs extends Record<string, any>, WorkerArgs extends Record<string, any>> =
268
+ export type TestApi<TestArgs extends object, WorkerArgs extends object> =
251
269
  Pick<TestType<TestArgs, WorkerArgs>, 'describe' | 'beforeAll' | 'beforeEach' | 'afterEach' | 'afterAll' | 'expect'> &
252
270
  {
253
271
  /**
@@ -274,7 +292,7 @@ export type TestApi<TestArgs extends Record<string, any>, WorkerArgs extends Rec
274
292
  *
275
293
  * Shorthand for [`useBase`](https://serenity-js.org/api/playwright-test/function/useBase/)
276
294
  */
277
- useFixtures: <T extends Record<string, any>, W extends Record<string, any> = object>(
295
+ useFixtures: <T extends object, W extends object = object>(
278
296
  customFixtures: Fixtures<T, W, TestArgs, WorkerArgs>
279
297
  ) => TestApi<TestArgs & T, WorkerArgs & W>,
280
298
 
@@ -282,7 +300,7 @@ export type TestApi<TestArgs extends Record<string, any>, WorkerArgs extends Rec
282
300
  test: TestType<TestArgs, WorkerArgs>,
283
301
  }
284
302
 
285
- function createTestApi<BaseTestFixtures extends (PlaywrightTestArgs & PlaywrightTestOptions), BaseWorkerFixtures extends (PlaywrightWorkerArgs & PlaywrightWorkerOptions)>(baseTest: TestType<BaseTestFixtures, BaseWorkerFixtures>): TestApi<BaseTestFixtures, BaseWorkerFixtures> {
303
+ function createTestApi<BaseTestFixtures extends object, BaseWorkerFixtures extends object>(baseTest: TestType<BaseTestFixtures, BaseWorkerFixtures>): TestApi<BaseTestFixtures, BaseWorkerFixtures> {
286
304
  return {
287
305
  useFixtures<T extends Record<string, any>, W extends Record<string, any> = object>(customFixtures: Fixtures<T, W, BaseTestFixtures, BaseWorkerFixtures>): TestApi<BaseTestFixtures & T, BaseWorkerFixtures & W> {
288
306
  return createTestApi(baseTest.extend(customFixtures));
@@ -426,6 +444,9 @@ export const expect: Expect = api.expect;
426
444
 
427
445
  export const useFixtures = api.useFixtures;
428
446
 
447
+ type MergedT<List> = List extends [ TestType<infer T, any>, ...(infer Rest) ] ? T & MergedT<Rest> : object;
448
+ type MergedW<List> = List extends [ TestType<any, infer W>, ...(infer Rest) ] ? W & MergedW<Rest> : object;
449
+
429
450
  /**
430
451
  * Creates a Serenity/JS BDD-style test API around the given Playwright [base test](https://playwright.dev/docs/test-fixtures).
431
452
  *
@@ -562,15 +583,45 @@ export const useFixtures = api.useFixtures;
562
583
  * })
563
584
  * ```
564
585
  *
565
- * @param baseTest
586
+ * ## Merging multiple base tests
587
+ *
588
+ * To merge fixtures from multiple files or modules, pass them to `useBase`.
589
+ *
590
+ * ```tsx
591
+ * import { test as componentTest } from '@playwright/experimental-ct-react'
592
+ * import { test as a11yTest } from 'my-a11y-test-utils';
593
+ * import { Ensure, contain } from '@serenity-js/assertions'
594
+ * import { useBase } from '@serenity-js/playwright-test'
595
+ * import { Enter, PageElement, CssClasses } from '@serenity-js/web'
596
+ *
597
+ * import EmailInput from './EmailInput';
598
+ *
599
+ * const { it, describe } = useBase(componentTest, a11yTest).useFixtures<{ emailAddress: string }>({
600
+ * emailAddress: ({ actor }, use) => {
601
+ * use(`${ actor.name }@example.org`)
602
+ * }
603
+ * })
604
+ *
605
+ * describe('EmailInput', () => {
606
+ *
607
+ * it('allows valid email addresses', async ({ actor, mount, emailAddress }) => {
608
+ * const nativeComponent = await mount(<EmailInput/>);
609
+ *
610
+ * const component = PageElement.from(nativeComponent);
611
+ *
612
+ * await actor.attemptsTo(
613
+ * Enter.theValue(emailAddress).into(component),
614
+ * Ensure.that(CssClasses.of(component), contain('valid')),
615
+ * )
616
+ * })
617
+ * })
618
+ * ```
619
+ *
620
+ * @param baseTests
566
621
  */
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
- );
622
+ export function useBase<List extends any[]>(...baseTests: List): TestApi<MergedT<List> & SerenityFixtures, MergedW<List> & SerenityWorkerFixtures> {
623
+ return createTestApi<MergedT<List>, MergedW<List>>(mergeTests(...baseTests))
624
+ .useFixtures(fixtures as Fixtures<SerenityFixtures, SerenityWorkerFixtures, MergedT<List>, MergedW<List>>);
574
625
  }
575
626
 
576
627
  /**
@@ -602,13 +653,14 @@ function asProxyConfig(proxy: PlaywrightTestOptions['proxy']): {
602
653
  auth?: { username: string, password: string }
603
654
  bypass?: string;
604
655
  } {
656
+ const proxyServer = proxy.server.trim();
605
657
 
606
658
  // Playwright defaults to http when proxy.server does not define the protocol
607
659
  // See https://playwright.dev/docs/api/class-testoptions#test-options-proxy
608
- const hasProtocol = /[\dA-Za-z]+:\/\//.test(proxy.server);
660
+ const hasProtocol = /^[\dA-Za-z]+:\/\//.test(proxyServer);
609
661
  const proxyUrl = hasProtocol
610
- ? new URL(proxy.server)
611
- : new URL(`http://${ proxy.server }`);
662
+ ? new URL(proxyServer)
663
+ : new URL(`http://${ proxyServer }`);
612
664
 
613
665
  const host = proxyUrl.hostname;
614
666
  const port = proxyUrl.port