@serenity-js/playwright-test
Version:
Serenity/JS test runner adapter for Playwright Test, combining Playwright's developer experience with the advanced reporting and automation capabilities of Serenity/JS
480 lines • 18.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useFixtures = exports.expect = exports.afterAll = exports.afterEach = exports.beforeEach = exports.beforeAll = exports.describe = exports.test = exports.it = exports.fixtures = void 0;
exports.useBase = useBase;
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const node_process_1 = __importDefault(require("node:process"));
const test_1 = require("@playwright/test");
const core_1 = require("@serenity-js/core");
const events_1 = require("@serenity-js/core/lib/events");
const model_1 = require("@serenity-js/core/lib/model");
const playwright_1 = require("@serenity-js/playwright");
const rest_1 = require("@serenity-js/rest");
const web_1 = require("@serenity-js/web");
const tiny_types_1 = require("tiny-types");
const events_2 = require("../events");
const reporter_1 = require("../reporter");
const PlaywrightTestSceneIdFactory_1 = require("../reporter/PlaywrightTestSceneIdFactory");
const PerformActivitiesAsPlaywrightSteps_1 = require("./PerformActivitiesAsPlaywrightSteps");
const WorkerEventStreamWriter_1 = require("./WorkerEventStreamWriter");
exports.fixtures = {
extraContextOptions: [
{ defaultNavigationWaitUntil: 'load' },
{ option: true }
],
defaultActorName: [
'Serena',
{ option: true },
],
cueTimeout: [
core_1.Duration.ofSeconds(5),
{ option: true },
],
interactionTimeout: [
core_1.Duration.ofSeconds(5),
{ option: true },
],
crew: [
[web_1.Photographer.whoWill(web_1.TakePhotosOfFailures)],
{ option: true },
],
actors: [
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async ({ extraContextOptions, baseURL, extraHTTPHeaders, page, proxy }, use) => {
await use(core_1.Cast.where(actor => actor.whoCan(playwright_1.BrowseTheWebWithPlaywright.usingPage(page, extraContextOptions), core_1.TakeNotes.usingAnEmptyNotepad(), rest_1.CallAnApi.using({
baseURL: baseURL,
headers: extraHTTPHeaders,
proxy: proxy && proxy?.server
? asProxyConfig(proxy)
: undefined,
}))));
},
{ option: true },
],
// eslint-disable-next-line no-empty-pattern,@typescript-eslint/explicit-module-boundary-types
platform: [async ({}, use) => {
const platform = node_os_1.default.platform();
// https://nodejs.org/api/process.html#process_process_platform
const name = platform === 'win32'
? 'Windows'
: (platform === 'darwin' ? 'macOS' : 'Linux');
await use({ name, version: node_os_1.default.release() });
}, { scope: 'worker' }],
diffFormatterInternal: [
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,no-empty-pattern
async ({}, use) => {
const diffFormatter = new core_1.AnsiDiffFormatter();
await use(diffFormatter);
},
{ scope: 'worker', box: true }
],
sceneIdFactoryInternal: [
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,no-empty-pattern
async ({}, use) => {
await use(new PlaywrightTestSceneIdFactory_1.PlaywrightTestSceneIdFactory());
},
{ scope: 'worker', box: true },
],
serenity: [
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async ({ playwright, sceneIdFactoryInternal }, use, workerInfo) => {
const clock = new core_1.Clock();
const cwd = node_process_1.default.cwd();
const serenity = new core_1.Serenity(clock, cwd, sceneIdFactoryInternal);
const serenitySelectorEngines = new playwright_1.SerenitySelectorEngines();
await serenitySelectorEngines.ensureRegisteredWith(playwright.selectors);
await use(serenity);
},
{ scope: 'worker', box: true }
],
eventStreamWriterInternal: [
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,no-empty-pattern
async ({}, use, workerInfo) => {
const serenityOutputDirectory = node_path_1.default.join(workerInfo.project.outputDir, 'serenity');
const eventStreamWriter = new WorkerEventStreamWriter_1.WorkerEventStreamWriter(serenityOutputDirectory, workerInfo);
await use(eventStreamWriter);
},
{ scope: 'worker', box: true },
],
configureWorkerInternal: [
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async ({ diffFormatterInternal, eventStreamWriterInternal, sceneIdFactoryInternal, serenity, browser }, use, info) => {
serenity.configure({
actors: core_1.Cast.where(actor => actor.whoCan(playwright_1.BrowseTheWebWithPlaywright.using(browser), core_1.TakeNotes.usingAnEmptyNotepad())),
crew: [
eventStreamWriterInternal,
],
diffFormatter: diffFormatterInternal,
});
sceneIdFactoryInternal.setTestId(`worker-${info.workerIndex}`);
const workerBeforeAllSceneId = serenity.assignNewSceneId();
await use(void 0);
await eventStreamWriterInternal.persistAll(workerBeforeAllSceneId);
},
{ scope: 'worker', auto: true, box: true },
],
configureScenarioInternal: [
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async ({ actors, browser, browserName, crew, cueTimeout, diffFormatterInternal, eventStreamWriterInternal, interactionTimeout, platform, sceneIdFactoryInternal, serenity }, use, info) => {
serenity.configure({
actors: asCast(actors),
diffFormatter: diffFormatterInternal,
cueTimeout: asDuration(cueTimeout),
interactionTimeout: asDuration(interactionTimeout),
crew: [
...crew,
new reporter_1.PlaywrightStepReporter(info),
],
});
const playwrightSceneId = events_2.PlaywrightSceneId.from(info.project.name, { id: info.testId, repeatEachIndex: info.repeatEachIndex }, { retry: info.retry });
sceneIdFactoryInternal.setTestId(playwrightSceneId.value);
const sceneId = serenity.assignNewSceneId();
serenity.announce(new events_1.SceneTagged(sceneId, new model_1.PlatformTag(platform.name, platform.version), serenity.currentTime()), new events_1.SceneTagged(sceneId, new model_1.BrowserTag(browserName, browser.version()), serenity.currentTime()));
await use(void 0);
try {
serenity.announce(new events_1.SceneFinishes(sceneId, serenity.currentTime()));
await serenity.waitForNextCue();
}
finally {
await eventStreamWriterInternal.persist(playwrightSceneId.value);
}
},
{ auto: true, box: true, }
],
actorCalled: [
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async ({ serenity }, use) => {
const actorCalled = (name) => {
const actor = serenity.theActorCalled(name);
return actor.whoCan(new PerformActivitiesAsPlaywrightSteps_1.PerformActivitiesAsPlaywrightSteps(actor, serenity, exports.it));
};
await use(actorCalled);
},
{ scope: 'worker' },
],
actor: async ({ actorCalled, defaultActorName }, use) => {
await use(actorCalled(defaultActorName));
},
};
function createTestApi(baseTest) {
return {
useFixtures(customFixtures) {
return createTestApi(baseTest.extend(customFixtures));
},
beforeAll: baseTest.beforeAll,
beforeEach: baseTest.beforeEach,
afterEach: baseTest.afterEach,
afterAll: baseTest.afterAll,
describe: baseTest.describe,
expect: baseTest.expect,
it: baseTest,
test: baseTest,
};
}
const api = createTestApi(test_1.test).useFixtures(exports.fixtures);
/**
* Declares a single test scenario.
*
* ## Example
*
* ```typescript
* import { Ensure, equals } from '@serenity-js/assertions'
* import { describe, it } from '@serenity-js/playwright-test'
*
* describe(`Todo List App`, () => {
*
* it(`should allow me to add a todo item`, async ({ actor }) => {
* await actor.attemptsTo(
* startWithAnEmptyList(),
*
* recordItem('Buy some milk'),
*
* Ensure.that(itemNames(), equals([
* 'Buy some milk',
* ])),
* )
* })
*
* it('supports multiple actors using separate browsers', async ({ actorCalled }) => {
* await actorCalled('Alice').attemptsTo(
* startWithAListContaining(
* 'Feed the cat'
* ),
* )
*
* await actorCalled('Bob').attemptsTo(
* startWithAListContaining(
* 'Walk the dog'
* ),
* )
*
* await actorCalled('Alice').attemptsTo(
* Ensure.that(itemNames(), equals([
* 'Feed the cat'
* ])),
* )
*
* await actorCalled('Bob').attemptsTo(
* Ensure.that(itemNames(), equals([
* 'Walk the dog'
* ])),
* )
* })
* })
* ```
*
* ## Learn more
* - [Grouping test scenarios](https://serenity-js.org/api/playwright-test/function/describe/)
* - [`SerenityFixtures`](https://serenity-js.org/api/playwright-test/interface/SerenityFixtures/)
* - [Playwright Test `test` function](https://playwright.dev/docs/api/class-test#test-call)
* - [Serenity/JS + Playwright Test project template](https://github.com/serenity-js/serenity-js-playwright-test-template/)
*/
exports.it = api.it;
/**
* Declares a single test scenario. Alias for [`it`](https://serenity-js.org/api/playwright-test/function/it/).
*/
exports.test = api.test;
/**
* Declares a group of test scenarios.
*
* ## Example
*
* ```typescript
* import { Ensure, equals } from '@serenity-js/assertions'
* import { describe, it, test } from '@serenity-js/playwright-test'
* import { Photographer, TakePhotosOfFailures, Value } from '@serenity-js/web'
*
* describe(`Todo List App`, () => {
*
* test.use({
* defaultActorName: 'Serena',
* crew: [
* Photographer.whoWill(TakePhotosOfFailures),
* ],
* })
*
* it(`should allow me to add a todo item`, async ({ actor }) => {
* await actor.attemptsTo(
* startWithAnEmptyList(),
*
* recordItem('Buy some milk'),
*
* Ensure.that(itemNames(), equals([
* 'Buy some milk',
* ])),
* )
* })
*
* it('should clear text input field when an item is added', async ({ actor }) => {
* await actor.attemptsTo(
* startWithAnEmptyList(),
*
* recordItem('Buy some milk'),
*
* Ensure.that(Value.of(newTodoInput()), equals('')),
* )
* })
* })
* ```
*
* ## Learn more
* - Declaring a Serenity/JS [test scenario](https://serenity-js.org/api/playwright-test/function/it/)
* - [Playwright Test `describe` function](https://playwright.dev/docs/api/class-test#test-describe-1)
* - [Serenity/JS + Playwright Test project template](https://github.com/serenity-js/serenity-js-playwright-test-template/)
*/
exports.describe = api.describe;
exports.beforeAll = api.beforeAll;
exports.beforeEach = api.beforeEach;
exports.afterEach = api.afterEach;
exports.afterAll = api.afterAll;
exports.expect = api.expect;
exports.useFixtures = api.useFixtures;
/**
* Creates a Serenity/JS BDD-style test API around the given Playwright [base test](https://playwright.dev/docs/test-fixtures).
*
* ## Using default configuration
*
* When your test scenario doesn't require [custom test fixtures](https://playwright.dev/docs/test-fixtures),
* and you're happy with the default [base test](https://playwright.dev/docs/api/class-test#test-call) offered by Playwright,
* you can import test API functions such as [`describe`](https://serenity-js.org/api/playwright-test/function/describe/) and [`it`](https://serenity-js.org/api/playwright-test/function/describe/) directly from `@serenity-js/playwright-test`.
*
* ```typescript
* import { describe, it, test } from '@serenity-js/playwright-test'
* import { Log } from '@serenity-js/core'
*
* // override default fixtures if needed
* test.use({
* defaultActorName: 'Alice'
* })
*
* describe('Serenity/JS default test API', () => {
*
* it('enables easy access to actors and standard Playwright fixtures', async ({ actor, browserName }) => {
* await actor.attemptsTo(
* Log.the(browserName),
* )
* })
* })
* ```
*
* In the above example, importing test API functions directly from `@serenity-js/playwright-test` is the equivalent of the following setup:
*
* ```typescript
* import { test as playwrightBaseTest } from '@playwright/test'
* import { useBase } from '@serenity-js/playwright-test'
*
* const { describe, it, test, beforeEach, afterEach } = useBase(playwrightBaseTest)
* ```
*
* ## Using custom fixtures
*
* When your test scenario requires [custom test fixtures](https://playwright.dev/docs/test-fixtures),
* but you're still happy with the default [base test](https://playwright.dev/docs/api/class-test#test-call) offered by Playwright,
* you can create fixture-aware test API functions such as [`describe`](https://serenity-js.org/api/playwright-test/function/describe/) and [`it`](https://serenity-js.org/api/playwright-test/function/describe/)
* by calling [`useFixtures`](https://serenity-js.org/api/playwright-test/function/useFixtures/).
*
* For example, you can create a test scenario using a static `message` fixture as follows:
*
* ```typescript
* import { useFixtures } from '@serenity-js/playwright-test'
* import { Log } from '@serenity-js/core'
*
* const { describe, it } = useFixtures<{ message: string }>({
* message: 'Hello world!'
* })
*
* describe('Serenity/JS useFixtures', () => {
*
* it('enables injecting custom test fixtures into test scenarios', async ({ actor, message }) => {
* await actor.attemptsTo(
* Log.the(message),
* )
* })
* })
* ```
*
* The value of your test fixtures can be either static or dynamic and based on the value of other fixtures.
*
* To create a dynamic test fixture use the [function syntax](https://playwright.dev/docs/test-fixtures):
*
* ```typescript
* import { Log } from '@serenity-js/core'
* import { useFixtures } from '@serenity-js/playwright-test'
*
* const { describe, it } = useFixtures<{ message: string }>({
* message: async ({ actor }, use) => {
* await use(`Hello, ${ actor.name }`);
* }
* })
*
* describe('Serenity/JS useFixtures', () => {
*
* it('enables injecting custom test fixtures into test scenarios', async ({ actor, message }) => {
* await actor.attemptsTo(
* Log.the(message),
* )
* })
* })
* ```
*
* In the above example, creating test API functions via `useFixtures` is the equivalent of the following setup:
*
* ```typescript
* import { test as playwrightBaseTest } from '@playwright/test'
* import { useBase } from '@serenity-js/playwright-test'
*
* const { describe, it, test, beforeEach, afterEach } = useBase(playwrightBaseTest)
* .useFixtures<{ message: string }>({
* message: async ({ actor }, use) => {
* await use(`Hello, ${ actor.name }`);
* }
* })
* ```
*
* ## Using custom base test
*
* In cases where you need to use a non-default base test, for example when doing [UI component testing](https://playwright.dev/docs/test-components),
* you can create Serenity/JS test API functions around your preferred base test.
*
* ```tsx
* import { test as componentTest } from '@playwright/experimental-ct-react'
* import { Ensure, contain } from '@serenity-js/assertions'
* import { useBase } from '@serenity-js/playwright-test'
* import { Enter, PageElement, CssClasses } from '@serenity-js/web'
*
* import EmailInput from './EmailInput';
*
* const { it, describe } = useBase(componentTest).useFixtures<{ emailAddress: string }>({
* emailAddress: ({ actor }, use) => {
* use(`${ actor.name }@example.org`)
* }
* })
*
* describe('EmailInput', () => {
*
* it('allows valid email addresses', async ({ actor, mount, emailAddress }) => {
* const nativeComponent = await mount(<EmailInput/>);
*
* const component = PageElement.from(nativeComponent);
*
* await actor.attemptsTo(
* Enter.theValue(emailAddress).into(component),
* Ensure.that(CssClasses.of(component), contain('valid')),
* )
* })
* })
* ```
*
* @param baseTest
*/
function useBase(baseTest) {
return createTestApi(baseTest).useFixtures(exports.fixtures);
}
/**
* @private
* @param maybeDuration
*/
function asDuration(maybeDuration) {
return maybeDuration instanceof core_1.Duration
? maybeDuration
: core_1.Duration.ofMilliseconds(maybeDuration);
}
/**
* @private
* @param maybeCast
*/
function asCast(maybeCast) {
return (0, tiny_types_1.ensure)('actors', maybeCast, (0, tiny_types_1.property)('prepare', (0, tiny_types_1.isFunction)()));
}
/**
* @private
* @param proxy
*/
function asProxyConfig(proxy) {
// Playwright defaults to http when proxy.server does not define the protocol
// See https://playwright.dev/docs/api/class-testoptions#test-options-proxy
const hasProtocol = /[\dA-Za-z]+:\/\//.test(proxy.server);
const proxyUrl = hasProtocol
? new URL(proxy.server)
: new URL(`http://${proxy.server}`);
const host = proxyUrl.hostname;
const port = proxyUrl.port
? Number(proxyUrl.port)
: undefined;
const auth = proxy.username
? { username: proxy.username, password: proxy.password || '' }
: undefined;
const bypass = proxy.bypass;
return {
protocol: proxyUrl.protocol,
host,
port,
auth,
bypass,
};
}
//# sourceMappingURL=test-api.js.map