diff --git a/packages/build_package.js b/packages/build_package.js index 555bcf0c40..b6bd3d2f44 100755 --- a/packages/build_package.js +++ b/packages/build_package.js @@ -42,6 +42,12 @@ const PACKAGES = { browsers: [], files: PLAYWRIGHT_CORE_FILES, }, + 'playwright-test': { + description: 'Playwright Test Runner', + browsers: ['chromium', 'firefox', 'webkit', 'ffmpeg'], + files: PLAYWRIGHT_CORE_FILES, + name: '@playwright/test', + }, 'playwright-webkit': { description: 'A high-level API to automate WebKit', browsers: ['webkit'], @@ -115,9 +121,12 @@ if (!args.some(arg => arg === '--no-cleanup')) { // 4. Generate package.json const pwInternalJSON = require(path.join(ROOT_PATH, 'package.json')); + const dependencies = { ...pwInternalJSON.dependencies }; + if (packageName === 'playwright-test') + dependencies.folio = pwInternalJSON.devDependencies.folio; await writeToPackage('package.json', JSON.stringify({ - name: packageName, - version: package.version || pwInternalJSON.version, + name: package.name || packageName, + version: pwInternalJSON.version, description: package.description, repository: pwInternalJSON.repository, engines: pwInternalJSON.engines, @@ -126,9 +135,6 @@ if (!args.some(arg => arg === '--no-cleanup')) { bin: { playwright: './lib/cli/cli.js', }, - engines: { - node: '>=12', - }, exports: { // Root import: we have a wrapper ES Module to support the following syntax. // const { chromium } = require('playwright'); @@ -145,7 +151,7 @@ if (!args.some(arg => arg === '--no-cleanup')) { }, author: pwInternalJSON.author, license: pwInternalJSON.license, - dependencies: pwInternalJSON.dependencies + dependencies, }, null, 2)); // 5. Generate browsers.json diff --git a/packages/installation-tests/esm-playwright-test.mjs b/packages/installation-tests/esm-playwright-test.mjs new file mode 100644 index 0000000000..0384c70341 --- /dev/null +++ b/packages/installation-tests/esm-playwright-test.mjs @@ -0,0 +1,22 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { chromium, firefox, webkit, selectors, devices, errors } from '@playwright/test'; +import playwright from '@playwright/test'; +import errorsFile from '@playwright/test/lib/utils/errors.js'; + +import testESM from './esm.mjs'; +testESM({ chromium, firefox, webkit, selectors, devices, errors, playwright, errorsFile }, [chromium, firefox, webkit]); diff --git a/packages/installation-tests/installation-tests.sh b/packages/installation-tests/installation-tests.sh index 74275f90a6..e86e7d9baa 100755 --- a/packages/installation-tests/installation-tests.sh +++ b/packages/installation-tests/installation-tests.sh @@ -27,6 +27,8 @@ PLAYWRIGHT_WEBKIT_TGZ="$(node ${PACKAGE_BUILDER} playwright-webkit ./playwright- echo "playwright-webkit built" PLAYWRIGHT_FIREFOX_TGZ="$(node ${PACKAGE_BUILDER} playwright-firefox ./playwright-firefox.tgz)" echo "playwright-firefox built" +PLAYWRIGHT_TEST_TGZ="$(node ${PACKAGE_BUILDER} playwright-test ./playwright-test.tgz)" +echo "playwright-test built" SCRIPTS_PATH="$(pwd -P)/.." TEST_ROOT="/tmp/playwright-installation-tests" @@ -45,12 +47,16 @@ function copy_test_scripts { cp "${SCRIPTS_PATH}/esm-playwright-chromium.mjs" . cp "${SCRIPTS_PATH}/esm-playwright-firefox.mjs" . cp "${SCRIPTS_PATH}/esm-playwright-webkit.mjs" . + cp "${SCRIPTS_PATH}/esm-playwright-test.mjs" . cp "${SCRIPTS_PATH}/sanity-electron.js" . cp "${SCRIPTS_PATH}/electron-app.js" . cp "${SCRIPTS_PATH}/driver-client.js" . + cp "${SCRIPTS_PATH}/sample.spec.js" . + cp "${SCRIPTS_PATH}/read-json-report.js" . } function run_tests { + test_playwright_test_should_work test_screencast test_typescript_types test_playwright_global_installation_subsequent_installs @@ -252,6 +258,12 @@ function test_playwright_should_work { node esm-playwright.mjs fi + echo "Running playwright test" + if npx playwright test -c .; then + echo "ERROR: should not be able to run tests with just playwright package" + exit 1 + fi + echo "${FUNCNAME[0]} success" } @@ -555,6 +567,37 @@ function test_playwright_driver_should_work { echo "${FUNCNAME[0]} success" } +function test_playwright_test_should_work { + initialize_test "${FUNCNAME[0]}" + + npm install ${PLAYWRIGHT_TEST_TGZ} + copy_test_scripts + + echo "Running playwright test without install" + if npx playwright test -c .; then + echo "ERROR: should not be able to run tests without installing browsers" + exit 1 + fi + + echo "Running playwright install" + PLAYWRIGHT_BROWSERS_PATH="0" npx playwright install + + echo "Running playwright test" + PLAYWRIGHT_JSON_OUTPUT_NAME=report.json PLAYWRIGHT_BROWSERS_PATH="0" npx playwright test -c . --browser=all --reporter=list,json + + echo "Checking the report" + node ./read-json-report.js ./report.json + + echo "Running sanity.js" + node sanity.js "@playwright/test" + if [[ "${NODE_VERSION}" == *"v14."* ]]; then + echo "Running esm.js" + node esm-playwright-test.mjs + fi + + echo "${FUNCNAME[0]} success" +} + function initialize_test { cd ${TEST_ROOT} local TEST_NAME="./$1" diff --git a/packages/installation-tests/read-json-report.js b/packages/installation-tests/read-json-report.js new file mode 100644 index 0000000000..e5bcb3d18a --- /dev/null +++ b/packages/installation-tests/read-json-report.js @@ -0,0 +1,18 @@ +const report = require(process.argv[2]); +if (report.suites[0].specs[0].title !== 'sample test') { + console.log(`Wrong spec title`); + process.exit(1); +} +const projects = report.suites[0].specs[0].tests.map(t => t.projectName).sort(); +if (projects.length !== 3 || projects[0] !== 'chromium' || projects[1] !== 'firefox' || projects[2] !== 'webkit') { + console.log(`Wrong browsers`); + process.exit(1); +} +for (const test of report.suites[0].specs[0].tests) { + if (test.results[0].status !== 'passed') { + console.log(`Test did not pass`); + process.exit(1); + } +} +console.log('Report check SUCCESS'); +process.exit(0); diff --git a/packages/installation-tests/sample.spec.js b/packages/installation-tests/sample.spec.js new file mode 100644 index 0000000000..d1baa902e9 --- /dev/null +++ b/packages/installation-tests/sample.spec.js @@ -0,0 +1,6 @@ +const { test, expect } = require('@playwright/test'); + +test('sample test', async ({ page }) => { + await page.setContent(`
hello
world`); + expect(await page.textContent('span')).toBe('world'); +}); diff --git a/packages/installation-tests/sanity.js b/packages/installation-tests/sanity.js index 42138e120a..6cc6386e8f 100644 --- a/packages/installation-tests/sanity.js +++ b/packages/installation-tests/sanity.js @@ -20,6 +20,7 @@ let success = { 'playwright-chromium': ['chromium'], 'playwright-firefox': ['firefox'], 'playwright-webkit': ['webkit'], + '@playwright/test': ['chromium', 'firefox', 'webkit'], }[requireName]; if (process.argv[3] === 'none') success = []; diff --git a/packages/playwright-test/README.md b/packages/playwright-test/README.md new file mode 100644 index 0000000000..c9f2d8d314 --- /dev/null +++ b/packages/playwright-test/README.md @@ -0,0 +1,3 @@ +# @playwright/test + +This package contains [Playwright Test Runner](https://playwright.dev/docs/test-intro). diff --git a/packages/playwright-test/index.d.ts b/packages/playwright-test/index.d.ts new file mode 100644 index 0000000000..ccae162bae --- /dev/null +++ b/packages/playwright-test/index.d.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as types from './types/types'; + +export * from './types/types'; +export const chromium: types.BrowserType; +export const firefox: types.BrowserType; +export const webkit: types.BrowserType; +export const _electron: types.Electron; +export const _android: types.Android; +export * from './types/test'; diff --git a/packages/playwright-test/index.js b/packages/playwright-test/index.js new file mode 100644 index 0000000000..dd7ff39165 --- /dev/null +++ b/packages/playwright-test/index.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + ...require('./lib/inprocess'), + ...require('./lib/cli/fixtures') +}; diff --git a/packages/playwright-test/index.mjs b/packages/playwright-test/index.mjs new file mode 100644 index 0000000000..c54a917122 --- /dev/null +++ b/packages/playwright-test/index.mjs @@ -0,0 +1,28 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import playwright from './index.js'; + +export const chromium = playwright.chromium; +export const firefox = playwright.firefox; +export const webkit = playwright.webkit; +export const selectors = playwright.selectors; +export const devices = playwright.devices; +export const errors = playwright.errors; +export const _electron = playwright._electron; +export const _android = playwright._android; +export const test = playwright.test; +export default playwright; diff --git a/packages/playwright-test/install.js b/packages/playwright-test/install.js new file mode 100644 index 0000000000..045b2ac8f9 --- /dev/null +++ b/packages/playwright-test/install.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Explicitly empty install.js to avoid touching browser registry at all. diff --git a/src/cli/cli.ts b/src/cli/cli.ts index f02afd2f1b..3f9fb5038c 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -35,6 +35,7 @@ import { BrowserContextOptions, LaunchOptions } from '../client/types'; import { spawn } from 'child_process'; import { installDeps } from '../install/installDeps'; import { allBrowserNames, BrowserName } from '../utils/registry'; +import { addTestCommand } from './testRunner'; import * as utils from '../utils/utils'; const SCRIPTS_DIRECTORY = path.join(__dirname, '..', '..', 'bin'); @@ -225,6 +226,9 @@ program console.log(' $ show-trace trace/directory'); }); +if (!process.env.PW_CLI_TARGET_LANG) + addTestCommand(program); + if (process.argv[2] === 'run-driver') runDriver(); else if (process.argv[2] === 'run-server') diff --git a/src/cli/fixtures.ts b/src/cli/fixtures.ts new file mode 100644 index 0000000000..fa1d8f5a07 --- /dev/null +++ b/src/cli/fixtures.ts @@ -0,0 +1,151 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as util from 'util'; +import * as folio from 'folio'; +import type { LaunchOptions, BrowserContextOptions, Page } from '../../types/types'; +import type { PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '../../types/test'; + +export * from 'folio'; +export const test = folio.test.extend({ + browserName: [ 'chromium', { scope: 'worker' } ], + playwright: [ require('../inprocess'), { scope: 'worker' } ], + headless: [ undefined, { scope: 'worker' } ], + channel: [ undefined, { scope: 'worker' } ], + launchOptions: [ {}, { scope: 'worker' } ], + + browser: [ async ({ playwright, browserName, headless, channel, launchOptions }, use) => { + if (!['chromium', 'firefox', 'webkit'].includes(browserName)) + throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); + const options: LaunchOptions = { + handleSIGINT: false, + ...launchOptions, + }; + if (headless !== undefined) + options.headless = headless; + if (channel !== undefined) + options.channel = channel; + const browser = await playwright[browserName].launch(options); + await use(browser); + await browser.close(); + }, { scope: 'worker' } ], + + screenshot: 'off', + video: 'off', + acceptDownloads: undefined, + bypassCSP: undefined, + colorScheme: undefined, + deviceScaleFactor: undefined, + extraHTTPHeaders: undefined, + geolocation: undefined, + hasTouch: undefined, + httpCredentials: undefined, + ignoreHTTPSErrors: undefined, + isMobile: undefined, + javaScriptEnabled: undefined, + locale: undefined, + offline: undefined, + permissions: undefined, + proxy: undefined, + storageState: undefined, + timezoneId: undefined, + userAgent: undefined, + viewport: undefined, + contextOptions: {}, + + context: async ({ browserName, browser, screenshot, video, acceptDownloads, bypassCSP, colorScheme, deviceScaleFactor, extraHTTPHeaders, hasTouch, geolocation, httpCredentials, ignoreHTTPSErrors, isMobile, javaScriptEnabled, locale, offline, permissions, proxy, storageState, viewport, timezoneId, userAgent, contextOptions }, use, testInfo) => { + testInfo.snapshotSuffix = browserName + '-' + process.platform; + if (process.env.PWDEBUG) + testInfo.setTimeout(0); + + const recordVideo = video === 'on' || video === 'retain-on-failure' || + (video === 'retry-with-video' && !!testInfo.retry); + const options: BrowserContextOptions = { + recordVideo: recordVideo ? { dir: testInfo.outputPath('') } : undefined, + ...contextOptions, + }; + if (acceptDownloads !== undefined) + options.acceptDownloads = acceptDownloads; + if (bypassCSP !== undefined) + options.bypassCSP = bypassCSP; + if (colorScheme !== undefined) + options.colorScheme = colorScheme; + if (deviceScaleFactor !== undefined) + options.deviceScaleFactor = deviceScaleFactor; + if (extraHTTPHeaders !== undefined) + options.extraHTTPHeaders = extraHTTPHeaders; + if (geolocation !== undefined) + options.geolocation = geolocation; + if (hasTouch !== undefined) + options.hasTouch = hasTouch; + if (httpCredentials !== undefined) + options.httpCredentials = httpCredentials; + if (ignoreHTTPSErrors !== undefined) + options.ignoreHTTPSErrors = ignoreHTTPSErrors; + if (isMobile !== undefined) + options.isMobile = isMobile; + if (javaScriptEnabled !== undefined) + options.javaScriptEnabled = javaScriptEnabled; + if (locale !== undefined) + options.locale = locale; + if (offline !== undefined) + options.offline = offline; + if (permissions !== undefined) + options.permissions = permissions; + if (proxy !== undefined) + options.proxy = proxy; + if (storageState !== undefined) + options.storageState = storageState; + if (timezoneId !== undefined) + options.timezoneId = timezoneId; + if (userAgent !== undefined) + options.userAgent = userAgent; + if (viewport !== undefined) + options.viewport = viewport; + + const context = await browser.newContext(options); + const allPages: Page[] = []; + context.on('page', page => allPages.push(page)); + + await use(context); + + const testFailed = testInfo.status !== testInfo.expectedStatus; + if (screenshot === 'on' || (screenshot === 'only-on-failure' && testFailed)) { + await Promise.all(allPages.map((page, index) => { + const screenshotPath = testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${++index}.png`); + return page.screenshot({ timeout: 5000, path: screenshotPath }).catch(e => {}); + })); + } + await context.close(); + + const deleteVideos = video === 'retain-on-failure' && !testFailed; + if (deleteVideos) { + await Promise.all(allPages.map(async page => { + const video = page.video(); + if (!video) + return; + const videoPath = await video.path(); + await util.promisify(fs.unlink)(videoPath).catch(e => {}); + })); + } + }, + + page: async ({ context }, use) => { + await use(await context.newPage()); + }, +}); +export default test; diff --git a/src/cli/testRunner.ts b/src/cli/testRunner.ts new file mode 100644 index 0000000000..7836830c72 --- /dev/null +++ b/src/cli/testRunner.ts @@ -0,0 +1,188 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable no-console */ + +import * as commander from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; +import type { Config } from 'folio'; + +type RunnerType = typeof import('folio/out/runner').Runner; + +const defaultTimeout = 30000; +const defaultReporter = process.env.CI ? 'dot' : 'list'; +const builtinReporters = ['list', 'line', 'dot', 'json', 'junit', 'null']; +const tsConfig = 'playwright.config.ts'; +const jsConfig = 'playwright.config.js'; +const defaultConfig: Config = { + preserveOutput: process.env.CI ? 'failures-only' : 'always', + reporter: [defaultReporter], + timeout: defaultTimeout, + updateSnapshots: process.env.CI ? 'none' : 'missing', + workers: Math.ceil(require('os').cpus().length / 2), +}; + +export function addTestCommand(program: commander.CommanderStatic) { + let Runner: RunnerType; + try { + Runner = require('folio/out/runner').Runner as RunnerType; + } catch (e) { + addStubTestCommand(program); + return; + } + + const command = program.command('test [test-filter...]'); + command.description('Run tests with Playwright Test'); + command.option('--browser ', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`); + command.option('--headed', `Run tests in headed browsers (default: headless)`); + command.option('-c, --config ', `Configuration file, or a test directory with optional "${tsConfig}"/"${jsConfig}"`); + command.option('--forbid-only', `Fail if test.only is called (default: false)`); + command.option('-g, --grep ', `Only run tests matching this regular expression (default: ".*")`); + command.option('--global-timeout ', `Maximum time this test suite can run in milliseconds (default: unlimited)`); + command.option('-j, --workers ', `Number of concurrent workers, use 1 to run in a single worker (default: number of CPU cores / 2)`); + command.option('--list', `Collect all the tests and report them, but do not run`); + command.option('--max-failures ', `Stop after the first N failures`); + command.option('--output ', `Folder for output artifacts (default: "test-results")`); + command.option('--quiet', `Suppress stdio`); + command.option('--repeat-each ', `Run each test N times (default: 1)`); + command.option('--reporter ', `Reporter to use, comma-separated, can be ${builtinReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`); + command.option('--retries ', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`); + command.option('--shard ', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`); + command.option('--project ', `Only run tests from the specified project (default: run all projects)`); + command.option('--timeout ', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${defaultTimeout})`); + command.option('-u, --update-snapshots', `Update snapshots with actual results (default: only create missing snapshots)`); + command.option('-x', `Stop after the first failure`); + command.action(async (args, opts) => { + try { + await runTests(Runner, args, opts); + } catch (e) { + console.error(e.toString()); + process.exit(1); + } + }); + command.on('--help', () => { + console.log(''); + console.log('Arguments [test-filter...]:'); + console.log(' Pass arguments to filter test files. Each argument is treated as a regular expression.'); + console.log(''); + console.log('Examples:'); + console.log(' $ test my.spec.ts'); + console.log(' $ test -c tests/ --headed'); + console.log(' $ test --browser=webkit'); + }); +} + +async function runTests(Runner: RunnerType, args: string[], opts: { [key: string]: any }) { + if (opts.browser) { + const browserOpt = opts.browser.toLowerCase(); + if (!['all', 'chromium', 'firefox', 'webkit'].includes(browserOpt)) + throw new Error(`Unsupported browser "${opts.browser}", must be one of "all", "chromium", "firefox" or "webkit"`); + const browserNames = browserOpt === 'all' ? ['chromium', 'firefox', 'webkit'] : [browserOpt]; + defaultConfig.projects = browserNames.map(browserName => { + return { + name: browserName, + use: { browserName }, + }; + }); + } + + const overrides = overridesFromOptions(opts); + if (opts.headed) + overrides.use = { headless: false }; + const runner = new Runner(defaultConfig, overrides); + + function loadConfig(configFile: string) { + if (fs.existsSync(configFile)) { + if (process.stdout.isTTY) + console.log(`Using config at ` + configFile); + const loadedConfig = runner.loadConfigFile(configFile); + if (('projects' in loadedConfig) && opts.browser) + throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); + return true; + } + return false; + } + + if (opts.config) { + const configFile = path.resolve(process.cwd(), opts.config); + if (!fs.existsSync(configFile)) + throw new Error(`${opts.config} does not exist`); + if (fs.statSync(configFile).isDirectory()) { + // When passed a directory, look for a config file inside. + if (!loadConfig(path.join(configFile, tsConfig)) && !loadConfig(path.join(configFile, jsConfig))) { + // If there is no config, assume this as a root testing directory. + runner.loadEmptyConfig(configFile); + } + } else { + // When passed a file, it must be a config file. + loadConfig(configFile); + } + } else if (!loadConfig(path.resolve(process.cwd(), tsConfig)) && !loadConfig(path.resolve(process.cwd(), jsConfig))) { + // No --config option, let's look for the config file in the current directory. + // If not, do not assume that current directory is a root testing directory, to avoid scanning the world. + throw new Error(`Configuration file not found. Run "npx playwright test --help" for more information.`); + } + + process.env.FOLIO_JUNIT_OUTPUT_NAME = process.env.PLAYWRIGHT_JUNIT_OUTPUT_NAME; + process.env.FOLIO_JUNIT_SUITE_ID = process.env.PLAYWRIGHT_JUNIT_SUITE_ID; + process.env.FOLIO_JUNIT_SUITE_NAME = process.env.PLAYWRIGHT_JUNIT_SUITE_NAME; + process.env.FOLIO_JSON_OUTPUT_NAME = process.env.PLAYWRIGHT_JSON_OUTPUT_NAME; + + const result = await runner.run(!!opts.list, args.map(forceRegExp), opts.project || undefined); + if (result === 'sigint') + process.exit(130); + process.exit(result === 'passed' ? 0 : 1); +} + +function forceRegExp(pattern: string): RegExp { + const match = pattern.match(/^\/(.*)\/([gi]*)$/); + if (match) + return new RegExp(match[1], match[2]); + return new RegExp(pattern, 'g'); +} + +function overridesFromOptions(options: { [key: string]: any }): Config { + const isDebuggerAttached = !!require('inspector').url(); + const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined; + return { + forbidOnly: options.forbidOnly ? true : undefined, + globalTimeout: isDebuggerAttached ? 0 : (options.globalTimeout ? parseInt(options.globalTimeout, 10) : undefined), + grep: options.grep ? forceRegExp(options.grep) : undefined, + maxFailures: options.x ? 1 : (options.maxFailures ? parseInt(options.maxFailures, 10) : undefined), + outputDir: options.output ? path.resolve(process.cwd(), options.output) : undefined, + quiet: options.quiet ? options.quiet : undefined, + repeatEach: options.repeatEach ? parseInt(options.repeatEach, 10) : undefined, + retries: options.retries ? parseInt(options.retries, 10) : undefined, + reporter: (options.reporter && options.reporter.length) ? options.reporter.split(',').map((r: string) => { + return builtinReporters.includes(r) ? r : { require: r }; + }) : undefined, + shard: shardPair ? { current: shardPair[0] - 1, total: shardPair[1] } : undefined, + timeout: isDebuggerAttached ? 0 : (options.timeout ? parseInt(options.timeout, 10) : undefined), + updateSnapshots: options.updateSnapshots ? 'all' as const : undefined, + workers: options.workers ? parseInt(options.workers, 10) : undefined, + }; +} + +function addStubTestCommand(program: commander.CommanderStatic) { + const command = program.command('test'); + command.description('Run tests with Playwright Test. Available in @playwright/test package.'); + command.action(async (args, opts) => { + console.error('Please install @playwright/test package to use Playwright Test.'); + console.error(' npm install -D @playwright/test'); + process.exit(1); + }); +} diff --git a/types/test.d.ts b/types/test.d.ts new file mode 100644 index 0000000000..e33850e240 --- /dev/null +++ b/types/test.d.ts @@ -0,0 +1,290 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials } from './types'; +import type { Project, Config } from 'folio'; + +/** + * The name of the browser supported by Playwright. + */ +type BrowserName = 'chromium' | 'firefox' | 'webkit'; + +/** + * Browser channel name. Used to run tests in different browser flavors, + * for example Google Chrome Beta, or Microsoft Edge Stable. + * @see BrowserContextOptions + */ +type BrowserChannel = Exclude; + +/** + * Emulates `'prefers-colors-scheme'` media feature, + * supported values are `'light'`, `'dark'`, `'no-preference'`. + * @see BrowserContextOptions + */ +type ColorScheme = Exclude; + +/** + * An object containing additional HTTP headers to be sent with every request. All header values must be strings. + * @see BrowserContextOptions + */ +type ExtraHTTPHeaders = Exclude; + +/** + * Proxy settings available for all tests, or individually per test. + * @see BrowserContextOptions + */ +type Proxy = Exclude; + +/** + * Storage state for the test. + * @see BrowserContextOptions + */ +type StorageState = Exclude; + +/** + * Options available to configure browser launch. + * - Set options in config: + * ```js + * use: { browserName: 'webkit' } + * ``` + * - Set options in test file: + * ```js + * test.use({ browserName: 'webkit' }) + * ``` + * + * Available as arguments to the test function and all hooks (beforeEach, afterEach, beforeAll, afterAll). + */ +export type PlaywrightWorkerOptions = { + /** + * Name of the browser (`chromium`, `firefox`, `webkit`) that runs tests. + */ + browserName: BrowserName; + + /** + * Whether to run browser in headless mode. Takes priority over `launchOptions`. + * @see LaunchOptions + */ + headless: boolean | undefined; + + /** + * Browser distribution channel. Takes priority over `launchOptions`. + * @see LaunchOptions + */ + channel: BrowserChannel | undefined; + + /** + * Options used to launch the browser. Other options above (e.g. `headless`) take priority. + * @see LaunchOptions + */ + launchOptions: LaunchOptions; +}; + +/** + * Options available to configure each test. + * - Set options in config: + * ```js + * use: { video: 'on' } + * ``` + * - Set options in test file: + * ```js + * test.use({ video: 'on' }) + * ``` + * + * Available as arguments to the test function and beforeEach/afterEach hooks. + */ +export type PlaywrightTestOptions = { + /** + * Whether to capture a screenshot after each test, off by default. + * - `off`: Do not capture screenshots. + * - `on`: Capture screenshot after each test. + * - `only-on-failure`: Capture screenshot after each test failure. + */ + screenshot: 'off' | 'on' | 'only-on-failure'; + + /** + * Whether to record video for each test, off by default. + * - `off`: Do not record video. + * - `on`: Record video for each test. + * - `retain-on-failure`: Record video for each test, but remove all videos from successful test runs. + * - `retry-with-video`: Record video only when retrying a test. + */ + video: 'off' | 'on' | 'retain-on-failure' | 'retry-with-video'; + + /** + * Whether to automatically download all the attachments. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + acceptDownloads: boolean | undefined; + + /** + * Toggles bypassing page's Content-Security-Policy. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + bypassCSP: boolean | undefined; + + /** + * Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. + * @see BrowserContextOptions + */ + colorScheme: ColorScheme | undefined; + + /** + * Specify device scale factor (can be thought of as dpr). Defaults to `1`. + * @see BrowserContextOptions + */ + deviceScaleFactor: number | undefined; + + /** + * An object containing additional HTTP headers to be sent with every request. All header values must be strings. + * @see BrowserContextOptions + */ + extraHTTPHeaders: ExtraHTTPHeaders | undefined; + + /** + * Context geolocation. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + geolocation: Geolocation | undefined; + + /** + * Specifies if viewport supports touch events. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + hasTouch: boolean | undefined; + + /** + * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). + * @see BrowserContextOptions + */ + httpCredentials: HTTPCredentials | undefined; + + /** + * Whether to ignore HTTPS errors during navigation. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + ignoreHTTPSErrors: boolean | undefined; + + /** + * Whether the `meta viewport` tag is taken into account and touch events are enabled. Not supported in Firefox. + * @see BrowserContextOptions + */ + isMobile: boolean | undefined; + + /** + * Whether or not to enable JavaScript in the context. Defaults to `true`. + * @see BrowserContextOptions + */ + javaScriptEnabled: boolean | undefined; + + /** + * User locale, for example `en-GB`, `de-DE`, etc. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + locale: string | undefined; + + /** + * Whether to emulate network being offline. + * @see BrowserContextOptions + */ + offline: boolean | undefined; + + /** + * A list of permissions to grant to all pages in this context. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + permissions: string[] | undefined; + + /** + * Proxy setting used for all pages in the test. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + proxy: Proxy | undefined; + + /** + * Populates context with given storage state. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + storageState: StorageState | undefined; + + /** + * Changes the timezone of the context. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + timezoneId: string | undefined; + + /** + * Specific user agent to use in this context. + * @see BrowserContextOptions + */ + userAgent: string | undefined; + + /** + * Viewport used for all pages in the test. Takes priority over `contextOptions`. + * @see BrowserContextOptions + */ + viewport: ViewportSize | undefined; + + /** + * Options used to create the context. Other options above (e.g. `viewport`) take priority. + * @see BrowserContextOptions + */ + contextOptions: BrowserContextOptions; +}; + + +/** + * Arguments available to the test function and all hooks (beforeEach, afterEach, beforeAll, afterAll). + */ +export type PlaywrightWorkerArgs = { + /** + * The Playwright instance. + */ + playwright: typeof import('..'); + + /** + * Browser instance, shared between multiple tests. + */ + browser: Browser; +}; + +/** + * Arguments available to the test function and beforeEach/afterEach hooks. + */ +export type PlaywrightTestArgs = { + /** + * BrowserContext instance, created fresh for each test. + */ + context: BrowserContext; + + /** + * Page instance, created fresh for each test. + */ + page: Page; +}; + +export type PlaywrightTestProject = Project; +export type PlaywrightTestConfig = Config; + +export * from 'folio'; + +import type { TestType } from 'folio'; + +/** + * These tests are executed in Playwright environment that launches the browser + * and provides a fresh page to each test. + */ +export const test: TestType; +export default test; diff --git a/utils/publish_all_packages.sh b/utils/publish_all_packages.sh index 92bf3cfb59..eb42231614 100755 --- a/utils/publish_all_packages.sh +++ b/utils/publish_all_packages.sh @@ -87,11 +87,13 @@ PLAYWRIGHT_CORE_TGZ="$PWD/playwright-core.tgz" PLAYWRIGHT_WEBKIT_TGZ="$PWD/playwright-webkit.tgz" PLAYWRIGHT_FIREFOX_TGZ="$PWD/playwright-firefox.tgz" PLAYWRIGHT_CHROMIUM_TGZ="$PWD/playwright-chromium.tgz" +PLAYWRIGHT_TEST_TGZ="$PWD/playwright-test.tgz" node ./packages/build_package.js playwright "${PLAYWRIGHT_TGZ}" node ./packages/build_package.js playwright-core "${PLAYWRIGHT_CORE_TGZ}" node ./packages/build_package.js playwright-webkit "${PLAYWRIGHT_WEBKIT_TGZ}" node ./packages/build_package.js playwright-firefox "${PLAYWRIGHT_FIREFOX_TGZ}" node ./packages/build_package.js playwright-chromium "${PLAYWRIGHT_CHROMIUM_TGZ}" +node ./packages/build_package.js playwright-test "${PLAYWRIGHT_TEST_TGZ}" echo "==================== Publishing version ${VERSION} ================" @@ -100,5 +102,6 @@ npm publish ${PLAYWRIGHT_CORE_TGZ} --tag="${NPM_PUBLISH_TAG}" npm publish ${PLAYWRIGHT_WEBKIT_TGZ} --tag="${NPM_PUBLISH_TAG}" npm publish ${PLAYWRIGHT_FIREFOX_TGZ} --tag="${NPM_PUBLISH_TAG}" npm publish ${PLAYWRIGHT_CHROMIUM_TGZ} --tag="${NPM_PUBLISH_TAG}" +npm publish ${PLAYWRIGHT_TEST_TGZ} --tag="${NPM_PUBLISH_TAG}" echo "Done."