UNPKG

@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
"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