diff --git a/e2e/stub.test.ts b/e2e/stub.test.ts index be88bbda..10e44acb 100644 --- a/e2e/stub.test.ts +++ b/e2e/stub.test.ts @@ -1,9 +1,11 @@ import path from 'path' +jest.setTimeout(10000) + describe('Example HTML file', () => { it('should detect the heading "Example" on page', async () => { await page.goto(`file:${path.join(__dirname, 'example.html')}`) const browser = await page.$eval('h1', (el) => el.textContent) - expect(browser).toBe('Example') + expectAllBrowsers(browser).toBe('Example') }) }) diff --git a/jest-playwright.config.js b/jest-playwright.config.js new file mode 100644 index 00000000..17ed7004 --- /dev/null +++ b/jest-playwright.config.js @@ -0,0 +1,4 @@ +module.exports = { + USE_NEW_API: true, + browsers: ['chromium', 'firefox'], +} diff --git a/package.json b/package.json index 4db98cd4..57592049 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "prepublishOnly": "npm run build", "test": "npm run test:src && npm run test:e2e", "test:src": "jest", - "test:e2e": "jest -c jest.config.e2e.js", + "test:e2e": "jest -c jest.config.e2e.js --forceExit --detectOpenHandles", "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls" }, "husky": { diff --git a/src/PlaywrightEnvironment.ts b/src/PlaywrightEnvironment.ts index c3ae82ab..e3109afe 100644 --- a/src/PlaywrightEnvironment.ts +++ b/src/PlaywrightEnvironment.ts @@ -1,6 +1,8 @@ /* eslint-disable no-console */ import NodeEnvironment from 'jest-environment-node' import { Config as JestConfig } from '@jest/types' +import playwright, { Browser, BrowserContext, Page } from 'playwright-core' + import { checkBrowserEnv, checkDeviceEnv, @@ -8,9 +10,19 @@ import { getDeviceType, getPlaywrightInstance, readConfig, + readPackage, } from './utils' -import { Config, CHROMIUM, GenericBrowser } from './constants' -import playwright, { Browser } from 'playwright-core' +import { + Config, + CHROMIUM, + BrowserType, + Initializer, + InitializerProps, + Args, + RootProxy, + GenericBrowser, + IMPORT_KIND_PLAYWRIGHT, +} from './constants' const handleError = (error: Error): void => { process.emit('uncaughtException', error) @@ -67,7 +79,7 @@ const getBrowserPerProcess = async ( // https://github.com/mmarkelov/jest-playwright/issues/42#issuecomment-589170220 if (browserType !== CHROMIUM && launchBrowserApp && launchBrowserApp.args) { launchBrowserApp.args = launchBrowserApp.args.filter( - (item) => item !== '--no-sandbox', + (item: string) => item !== '--no-sandbox', ) } @@ -77,7 +89,7 @@ const getBrowserPerProcess = async ( browserPerProcess = await playwrightInstance.launch(launchBrowserApp) } } - return browserPerProcess + return browserPerProcess as Browser } class PlaywrightEnvironment extends NodeEnvironment { @@ -90,15 +102,231 @@ class PlaywrightEnvironment extends NodeEnvironment { async setup(): Promise { resetBrowserCloseWatchdog() const config = await readConfig(this._config.rootDir) - const browserType = getBrowserType(config) - checkBrowserEnv(browserType) - const { context, exitOnPageError, server, selectors } = config - const device = getDeviceType(config) - const playwrightInstance = await getPlaywrightInstance( - browserType, + const { + context, + server, selectors, - ) - let contextOptions = context + browsers, + devices, + exitOnPageError, + } = config + const playwrightPackage = await readPackage() + if (playwrightPackage === IMPORT_KIND_PLAYWRIGHT) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const playwright = require('playwright') + if (selectors) { + await Promise.all( + selectors.map(({ name, script }) => { + return playwright.selectors.register(name, script) + }), + ) + } + } + // Two possible cases + // browsers are defined + if (config.USE_NEW_API && browsers && browsers.length) { + // Playwright instances for each browser + const playwrightInstances = await Promise.all( + browsers.map((browser) => + getPlaywrightInstance(playwrightPackage, browser), + ), + ) + + // Helpers + const getResult = ( + data: T[], + instances: BrowserType[] | Array, + ) => { + const result: any = {} + data.forEach((item: T, index: number) => { + result[instances[index]] = item + }) + return result + } + + const initialize = async ( + browser: BrowserType, + initializer: Initializer, + ): Promise => { + if (devices && devices.length) { + return await Promise.all( + devices.map((device) => initializer({ browser, device })), + ).then((data) => getResult(data, devices)) + } else { + return initializer({ browser }) + } + } + + // Browsers + const playwrightBrowsers = await Promise.all( + browsers.map((browser, index) => + getBrowserPerProcess(playwrightInstances[index], { + ...config, + browser, + }), + ), + ).then((data) => getResult(data, browsers)) + + // Contexts + const contextInitializer = ({ + browser, + device, + }: InitializerProps): Promise => { + let contextOptions = {} + if (device) { + const { viewport, userAgent } = playwright.devices[device] + contextOptions = { viewport, userAgent } + } + return playwrightBrowsers[browser].newContext(contextOptions) + } + + const contexts = await Promise.all( + browsers.map((browser) => initialize(browser, contextInitializer)), + ).then((data) => getResult(data, browsers)) + + // Pages + const pageInitializer = ({ + browser, + device, + }: InitializerProps): Promise => { + const instance = contexts[browser] + return device ? instance[device].newPage() : instance.newPage() + } + + const pages = await Promise.all( + browsers.map((browser) => initialize(browser, pageInitializer)), + ).then((data) => getResult(data, browsers)) + + const checker = ({ + instance, + key, + args, + }: { + instance: any + key: keyof T + args: Args + }) => { + if (typeof instance[key] === 'function') { + return (instance[key] as Function).call(instance, ...args) + } else { + return instance[key] + } + } + + // TODO Improve types + const callAsync = async ( + instances: RootProxy, + key: keyof T, + ...args: Args + ) => + await Promise.all( + browsers.map(async (browser) => { + const browserInstance: { + [key: string]: T + } = instances[browser] + if (devices && devices.length) { + return await Promise.all( + devices.map((device) => { + const instance = browserInstance[device] + return checker({ instance, key, args }) + }), + ).then((data) => getResult(data, devices)) + } else { + return checker({ instance: browserInstance, key, args }) + } + }), + ).then((data) => getResult(data, browsers)) + + const proxyWrapper = (instances: RootProxy) => + new Proxy( + {}, + { + get: (obj, key) => { + const browser = browsers.find((item) => item === key) + if (browser) { + return instances[browser] + } else { + return (...args: Args) => + callAsync(instances, key as keyof T, ...args) + } + }, + }, + ) + + const testRunner = ({ + expectFunction, + errorMessage, + args, + }: { + expectFunction: Function + errorMessage: string + args: any[] + }) => { + try { + return expectFunction(...args) + } catch (e) { + // TODO Think about error message + console.log(errorMessage) + return expectFunction(...args) + } + } + + this.global.browser = proxyWrapper(playwrightBrowsers) + this.global.context = proxyWrapper(contexts) + this.global.page = proxyWrapper(pages) + // TODO Types + // TODO Add expectWebkit, expectFirefox? + this.global.expectAllBrowsers = (input: any) => + new Proxy( + {}, + { + get: (obj, key) => { + const { expect } = this.global + return (...args: Args) => { + browsers.forEach((browser) => { + if (devices && devices.length) { + devices.forEach((device) => { + const expectFunction = expect(input[browser][device])[key] + const errorMessage = `Failed test for ${browser}, ${device}` + testRunner({ expectFunction, errorMessage, args }) + }) + } else { + const expectFunction = expect(input[browser])[key] + const errorMessage = `Failed test for ${browser}` + testRunner({ expectFunction, errorMessage, args }) + } + }) + } + }, + }, + ) + } else { + // Browsers are not defined + const browserType = getBrowserType(config) + checkBrowserEnv(browserType) + const device = getDeviceType(config) + const playwrightInstance = await getPlaywrightInstance( + playwrightPackage, + browserType, + ) + let contextOptions = context + const availableDevices = Object.keys(playwright.devices) + if (device) { + checkDeviceEnv(device, availableDevices) + const { viewport, userAgent } = playwright.devices[device] + contextOptions = { viewport, userAgent, ...contextOptions } + } + this.global.browser = await getBrowserPerProcess( + playwrightInstance, + config, + ) + this.global.context = await this.global.browser.newContext(contextOptions) + this.global.page = await this.global.context.newPage() + } + + if (exitOnPageError) { + this.global.page.on('pageerror', handleError) + } if (server) { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -124,18 +352,6 @@ class PlaywrightEnvironment extends NodeEnvironment { } } - const availableDevices = Object.keys(playwright.devices) - if (device) { - checkDeviceEnv(device, availableDevices) - const { viewport, userAgent } = playwright.devices[device] - contextOptions = { viewport, userAgent, ...contextOptions } - } - this.global.browser = await getBrowserPerProcess(playwrightInstance, config) - this.global.context = await this.global.browser.newContext(contextOptions) - this.global.page = await this.global.context.newPage() - if (exitOnPageError) { - this.global.page.on('pageerror', handleError) - } this.global.jestPlaywright = { debug: async (): Promise => { // Run a debugger (in case Playwright has been launched with `{ devtools: true }`) @@ -183,9 +399,10 @@ class PlaywrightEnvironment extends NodeEnvironment { if (!jestConfig.watch && !jestConfig.watchAll && teardownServer) { await teardownServer() } - if (this.global.page) { - this.global.page.removeListener('pageerror', handleError) - await this.global.page.close() + const { page } = this.global + if (page) { + page.removeListener('pageerror', handleError) + await page.close() } startBrowserCloseWatchdog() } diff --git a/src/constants.ts b/src/constants.ts index 672fe5c8..2fb091ac 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -39,6 +39,7 @@ export interface Config { server?: JestDevServerOptions selectors?: SelectorType[] connectBrowserApp?: BrowserTypeConnectOptions + USE_NEW_API?: boolean } export const DEFAULT_CONFIG: Config = { @@ -47,3 +48,16 @@ export const DEFAULT_CONFIG: Config = { browser: CHROMIUM, exitOnPageError: true, } + +// Utils +export type InitializerProps = { + browser: BrowserType + device?: string +} + +export type RootProxy = { + [key: string]: any +} + +export type Initializer = (args: InitializerProps) => Promise +export type Args = (string | Function)[] diff --git a/src/utils.test.ts b/src/utils.test.ts index c2105e02..0a768fe2 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -218,24 +218,7 @@ describe('getPlaywrightInstance', () => { firefox: 'firefox', })) - const instance = await getPlaywrightInstance('firefox') - expect(instance).toEqual('firefox') - }) - - it('should register passed selectors for playwright package', async () => { - spy.mockResolvedValue('playwright') - - const register = jest.fn().mockResolvedValue('registered') - - jest.doMock('playwright', () => ({ - firefox: 'firefox', - selectors: { _engines: new Map(), register }, - })) - - const selectors = [{ name: 'test', script: 'test' }] - - const instance = await getPlaywrightInstance('firefox', selectors) - expect(register).toHaveBeenLastCalledWith('test', 'test') + const instance = await getPlaywrightInstance('playwright', 'firefox') expect(instance).toEqual('firefox') }) @@ -246,7 +229,7 @@ describe('getPlaywrightInstance', () => { chromium: 'chromium', })) - const instance = await getPlaywrightInstance('chromium') + const instance = await getPlaywrightInstance('chromium', 'chromium') expect(instance).toEqual('chromium') }) }) diff --git a/src/utils.ts b/src/utils.ts index d17e02bb..b5f487c8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -80,23 +80,11 @@ export const readPackage = async (): Promise => { } export const getPlaywrightInstance = async ( + playwrightPackage: PlaywrightRequireType, browserType: BrowserType, - selectors?: SelectorType[], ): Promise => { - const playwrightPackage = await readPackage() if (playwrightPackage === IMPORT_KIND_PLAYWRIGHT) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const playwright = require('playwright') - if (selectors) { - await Promise.all( - selectors.map(({ name, script }) => { - if (!playwright.selectors._engines.get(name)) { - return playwright.selectors.register(name, script) - } - }), - ) - } - return playwright[browserType] + return require('playwright')[browserType] } return require(`playwright-${playwrightPackage}`)[playwrightPackage] } diff --git a/types/global.d.ts b/types/global.d.ts index b65db54c..7eee31cc 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -20,4 +20,5 @@ declare global { const browser: Browser const context: BrowserContext const jestPlaywright: JestPlaywright + const expectAllBrowsers: jest.Expect }