diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad1b0c5ac7..abf08d35d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: # XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR # Wrap `npm run` in a subshell to redirect STDERR to file. # Enable core dumps in the subshell. - - run: xvfb-run --auto-servernum -- bash -c "ulimit -c unlimited && npm run coverage 2>./testrun.log" + - run: xvfb-run --auto-servernum -- bash -c "ulimit -c unlimited && npm run test 2>./testrun.log" env: BROWSER: ${{ matrix.browser }} DEBUG: "*,-pw:wrapped*" diff --git a/src/common/utilityScriptSerializers.ts b/src/common/utilityScriptSerializers.ts index aa6cbcdd10..8aba6ec70d 100644 --- a/src/common/utilityScriptSerializers.ts +++ b/src/common/utilityScriptSerializers.ts @@ -22,6 +22,10 @@ function isDate(obj: any): obj is Date { return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]'; } +function isError(obj: any): obj is Error { + return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error'); +} + export function parseEvaluationResultValue(value: any, handles: any[] = []): any { if (value === undefined) return undefined; @@ -85,7 +89,7 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough if (isPrimitiveValue(value)) return value; - if (value instanceof Error) { + if (isError(value)) { const error = value; if ('captureStackTrace' in global.Error) { // v8 diff --git a/src/helper.ts b/src/helper.ts index 1bcb8a6011..13ea6741ac 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -132,6 +132,10 @@ class Helper { return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; } + static isError(obj: any): obj is Error { + return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error'); + } + static isObject(obj: any): obj is NonNullable { return typeof obj === 'object' && obj !== null; } diff --git a/src/page.ts b/src/page.ts index 9b3645599d..aa476a76db 100644 --- a/src/page.ts +++ b/src/page.ts @@ -693,7 +693,7 @@ export class PageBinding { const result = await binding!.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args); context.evaluateInternal(deliverResult, { name, seq, result }).catch(logError(page._logger)); } catch (error) { - if (error instanceof Error) + if (helper.isError(error)) context.evaluateInternal(deliverError, { name, seq, message: error.message, stack: error.stack }).catch(logError(page._logger)); else context.evaluateInternal(deliverErrorValue, { name, seq, error }).catch(logError(page._logger)); diff --git a/src/rpc/serializers.ts b/src/rpc/serializers.ts index 8425f4ec67..eb74fa1bbf 100644 --- a/src/rpc/serializers.ts +++ b/src/rpc/serializers.ts @@ -24,7 +24,7 @@ import { helper, assert } from '../helper'; export function serializeError(e: any): types.Error { - if (e instanceof Error) + if (helper.isError(e)) return { message: e.message, stack: e.stack, name: e.name }; return { value: e }; } diff --git a/test/__snapshots__/coverage.jest.js.snap b/test/__snapshots__/coverage.jest.js.snap new file mode 100644 index 0000000000..5fd3ae9f8b --- /dev/null +++ b/test/__snapshots__/coverage.jest.js.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CSSCoverage should work with complicated usecases 1`] = ` +"[ + { + \\"url\\": \\"http://localhost:/csscoverage/involved.html\\", + \\"ranges\\": [ + { + \\"start\\": 149, + \\"end\\": 297 + }, + { + \\"start\\": 327, + \\"end\\": 433 + } + ], + \\"text\\": \\"\\\\n@charset \\\\\\"utf-8\\\\\\";\\\\n@namespace svg url(http://www.w3.org/2000/svg);\\\\n@font-face {\\\\n font-family: \\\\\\"Example Font\\\\\\";\\\\n src: url(\\\\\\"./Dosis-Regular.ttf\\\\\\");\\\\n}\\\\n\\\\n#fluffy {\\\\n border: 1px solid black;\\\\n z-index: 1;\\\\n /* -webkit-disabled-property: rgb(1, 2, 3) */\\\\n -lol-cats: \\\\\\"dogs\\\\\\" /* non-existing property */\\\\n}\\\\n\\\\n@media (min-width: 1px) {\\\\n span {\\\\n -webkit-border-radius: 10px;\\\\n font-family: \\\\\\"Example Font\\\\\\";\\\\n animation: 1s identifier;\\\\n }\\\\n}\\\\n\\" + } +]" +`; diff --git a/test/accessibility.spec.js b/test/accessibility.jest.js similarity index 99% rename from test/accessibility.spec.js rename to test/accessibility.jest.js index 9556793a46..508a4250ff 100644 --- a/test/accessibility.spec.js +++ b/test/accessibility.jest.js @@ -15,7 +15,7 @@ * limitations under the License. */ -const {FFOX, CHROMIUM, WEBKIT} = require('./utils').testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT} = testOptions; describe('Accessibility', function() { it('should work', async function({page}) { diff --git a/test/autowaiting.spec.js b/test/autowaiting.jest.js similarity index 99% rename from test/autowaiting.spec.js rename to test/autowaiting.jest.js index 827e7ee79d..5b27c4d6ad 100644 --- a/test/autowaiting.spec.js +++ b/test/autowaiting.jest.js @@ -15,7 +15,7 @@ * limitations under the License. */ -const {FFOX, CHROMIUM, WEBKIT, USES_HOOKS} = require('./utils').testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT, USES_HOOKS} = testOptions; describe('Auto waiting', () => { it('should await navigation when clicking anchor', async({page, server}) => { diff --git a/test/click.spec.js b/test/click.jest.js similarity index 99% rename from test/click.spec.js rename to test/click.jest.js index 562213643b..c624023443 100644 --- a/test/click.spec.js +++ b/test/click.jest.js @@ -16,7 +16,7 @@ */ const utils = require('./utils'); -const {FFOX, CHROMIUM, WEBKIT, WIN, HEADLESS, USES_HOOKS} = utils.testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT, HEADLESS, USES_HOOKS} = testOptions; async function giveItAChanceToClick(page) { for (let i = 0; i < 5; i++) @@ -251,7 +251,7 @@ describe('Page.click', function() { await page.click('label[for="agree"]'); expect(await page.evaluate(() => result.check)).toBe(false); }); - it('should not hang with touch-enabled viewports', async({server, browser}) => { + it('should not hang with touch-enabled viewports', async({browser, playwright}) => { // @see https://github.com/GoogleChrome/puppeteer/issues/161 const { viewport, hasTouch } = playwright.devices['iPhone 6']; const context = await browser.newContext({ viewport, hasTouch }); diff --git a/test/cookies.spec.js b/test/cookies.jest.js similarity index 99% rename from test/cookies.spec.js rename to test/cookies.jest.js index b898dbd709..aaa8e90aef 100644 --- a/test/cookies.spec.js +++ b/test/cookies.jest.js @@ -15,7 +15,7 @@ * limitations under the License. */ -const {FFOX, CHROMIUM, WEBKIT, WIN} = require('./utils').testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT, WIN} = testOptions; describe('BrowserContext.cookies', function() { it('should return no cookies in pristine browser context', async({context, page, server}) => { diff --git a/test/coverage.spec.js b/test/coverage.jest.js similarity index 98% rename from test/coverage.spec.js rename to test/coverage.jest.js index bc1b1335d7..e4b51d74a2 100644 --- a/test/coverage.spec.js +++ b/test/coverage.jest.js @@ -14,7 +14,7 @@ * limitations under the License. */ -const {FFOX, CHROMIUM, WEBKIT} = require('./utils').testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT} = testOptions; describe.skip(CHROMIUM)('Page.coverage missing', function() { it('should work', async function({page, server}) { @@ -141,11 +141,11 @@ describe.skip(!CHROMIUM)('CSSCoverage', function() { {start: 17, end: 38} ]); }); - it('should work with complicated usecases', async function({page, server, golden}) { + it('should work with complicated usecases', async function({page, server}) { await page.coverage.startCSSCoverage(); await page.goto(server.PREFIX + '/csscoverage/involved.html'); const coverage = await page.coverage.stopCSSCoverage(); - expect(JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':/')).toBeGolden(golden('csscoverage-involved.txt')); + expect(JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':/')).toMatchSnapshot(); }); it('should ignore injected stylesheets', async function({page, server}) { await page.coverage.startCSSCoverage(); diff --git a/test/evaluation.jest.js b/test/evaluation.jest.js index 0ba74cc75f..f4787f50fb 100644 --- a/test/evaluation.jest.js +++ b/test/evaluation.jest.js @@ -17,6 +17,7 @@ const utils = require('./utils'); const path = require('path'); +const {FFOX, CHROMIUM, WEBKIT, USES_HOOKS, CHANNEL} = testOptions; describe('Page.evaluate', function() { it('should work', async({page, server}) => { @@ -356,7 +357,7 @@ describe('Page.evaluate', function() { }); expect(result).toEqual([42]); }); - (WEBKIT ? it.skip : it)('should not throw an error when evaluation does a synchronous navigation and returns an object', async({page, server}) => { + it.fail(WEBKIT)('should not throw an error when evaluation does a synchronous navigation and returns an object', async({page, server}) => { // It is imporant to be on about:blank for sync reload. const result = await page.evaluate(() => { window.location.reload(); @@ -372,7 +373,7 @@ describe('Page.evaluate', function() { }); expect(result).toBe(undefined); }); - (CHANNEL ? it.skip : it)('should transfer 100Mb of data from page to node.js', async({page}) => { + it.skip(CHANNEL)('should transfer 100Mb of data from page to node.js', async({page}) => { const a = await page.evaluate(() => Array(100 * 1024 * 1024 + 1).join('a')); expect(a.length).toBe(100 * 1024 * 1024); }); @@ -388,7 +389,7 @@ describe('Page.evaluate', function() { const result = await page.evaluate(() => ({abc: 123})); expect(result).toEqual({abc: 123}); }); - (FFOX ? it.skip : it)('should await promise from popup', async ({page, server}) => { + it.fail(FFOX)('should await promise from popup', async ({page, server}) => { // Something is wrong about the way Firefox waits for the chained promise await page.goto(server.EMPTY_PAGE); const result = await page.evaluate(() => { @@ -573,14 +574,14 @@ describe('Frame.evaluate', function() { else expect(pageImpl._delegate._contextIdToContext.size).toBe(count); } - (USES_HOOKS ? it.skip : it)('should dispose context on navigation', async({page, server, toImpl}) => { + it.skip(USES_HOOKS)('should dispose context on navigation', async({page, server, toImpl}) => { await page.goto(server.PREFIX + '/frames/one-frame.html'); expect(page.frames().length).toBe(2); expectContexts(toImpl(page), 4); await page.goto(server.EMPTY_PAGE); expectContexts(toImpl(page), 2); }); - (USES_HOOKS ? it.skip : it)('should dispose context on cross-origin navigation', async({page, server, toImpl}) => { + it.skip(USES_HOOKS)('should dispose context on cross-origin navigation', async({page, server, toImpl}) => { await page.goto(server.PREFIX + '/frames/one-frame.html'); expect(page.frames().length).toBe(2); expectContexts(toImpl(page), 4); diff --git a/test/jest/fixtures.js b/test/jest/fixtures.js index fdc808cd13..1014e29598 100644 --- a/test/jest/fixtures.js +++ b/test/jest/fixtures.js @@ -27,7 +27,7 @@ const { setUseApiName } = require('../../lib/progress'); module.exports = function registerFixtures(global) { - global.registerWorkerFixture('server', async ({}, test) => { + global.registerWorkerFixture('http_server', async ({}, test) => { const assetsPath = path.join(__dirname, '..', 'assets'); const cachedPath = path.join(__dirname, '..', 'assets', 'cached'); @@ -40,14 +40,14 @@ module.exports = function registerFixtures(global) { server.EMPTY_PAGE = `http://localhost:${port}/empty.html`; const httpsPort = port + 1; - httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort); + const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort); httpsServer.enableHTTPCache(cachedPath); httpsServer.PORT = httpsPort; httpsServer.PREFIX = `https://localhost:${httpsPort}`; httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`; httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`; - await test(server); + await test({server, httpsServer}); await Promise.all([ server.stop(), @@ -55,6 +55,14 @@ module.exports = function registerFixtures(global) { ]); }); + global.registerWorkerFixture('defaultBrowserOptions', async({}, test) => { + await test({ + handleSIGINT: false, + slowMo: valueFromEnv('SLOW_MO', 0), + headless: !!valueFromEnv('HEADLESS', true), + }); + }); + global.registerWorkerFixture('playwright', async({}, test) => { if (process.env.PWCHANNEL) { setUseApiName(false); @@ -113,8 +121,8 @@ module.exports = function registerFixtures(global) { await test(playwright[process.env.BROWSER || 'chromium']); }); - global.registerWorkerFixture('browser', async ({browserType}, test) => { - const browser = await browserType.launch({ headless: !!global.HEADLESS }); + global.registerWorkerFixture('browser', async ({browserType, defaultBrowserOptions}, test) => { + const browser = await browserType.launch(defaultBrowserOptions); await test(browser); await browser.close(); }); @@ -129,4 +137,20 @@ module.exports = function registerFixtures(global) { const page = await context.newPage(); await test(page); }); + + global.registerFixture('server', async ({http_server}, test) => { + http_server.server.reset(); + await test(http_server.server); + }); + + global.registerFixture('httpsServer', async ({http_server}, test) => { + http_server.httpsServer.reset(); + await test(http_server.httpsServer); + }); +} + +function valueFromEnv(name, defaultValue) { + if (!(name in process.env)) + return defaultValue; + return JSON.parse(process.env[name]); } diff --git a/test/jest/playwrightEnvironment.js b/test/jest/playwrightEnvironment.js index 889e2d8633..a0052be227 100644 --- a/test/jest/playwrightEnvironment.js +++ b/test/jest/playwrightEnvironment.js @@ -16,17 +16,25 @@ const NodeEnvironment = require('jest-environment-node'); const registerFixtures = require('./fixtures'); +const os = require('os'); + +const platform = os.platform(); class PlaywrightEnvironment extends NodeEnvironment { constructor(config, context) { super(config, context); this.fixturePool = new FixturePool(); - this.global.CHROMIUM = process.env.BROWSER === 'chromium' || !process.env.BROWSER; - this.global.FFOX = process.env.BROWSER === 'firefox'; - this.global.WEBKIT = process.env.BROWSER === 'webkit'; - this.global.USES_HOOKS = process.env.PWCHANNEL === 'wire'; - this.global.CHANNEL = !!process.env.PWCHANNEL; - this.global.HEADLESS = !!valueFromEnv('HEADLESS', true); + const testOptions = {}; + testOptions.MAC = platform === 'darwin'; + testOptions.LINUX = platform === 'linux'; + testOptions.WIN = platform === 'win32'; + testOptions.CHROMIUM = process.env.BROWSER === 'chromium' || !process.env.BROWSER; + testOptions.FFOX = process.env.BROWSER === 'firefox'; + testOptions.WEBKIT = process.env.BROWSER === 'webkit'; + testOptions.USES_HOOKS = process.env.PWCHANNEL === 'wire'; + testOptions.CHANNEL = !!process.env.PWCHANNEL; + testOptions.HEADLESS = !!valueFromEnv('HEADLESS', true); + this.global.testOptions = testOptions; this.global.registerFixture = (name, fn) => { this.fixturePool.registerFixture(name, 'test', fn); @@ -51,6 +59,24 @@ class PlaywrightEnvironment extends NodeEnvironment { } async handleTestEvent(event, state) { + if (event.name === 'setup') { + const describeSkip = this.global.describe.skip; + this.global.describe.skip = (...args) => { + if (args.length = 1) + return args[0] ? describeSkip : this.global.describe; + return describeSkip(...args); + }; + this.global.describe.fail = this.global.describe.skip; + + const itSkip = this.global.it.skip; + this.global.it.skip = (...args) => { + if (args.length = 1) + return args[0] ? itSkip : this.global.it; + return itSkip(...args); + }; + this.global.it.fail = this.global.it.skip; + this.global.it.slow = () => this.global.it; + } if (event.name === 'test_start') { const fn = event.test.fn; event.test.fn = async () => { @@ -161,7 +187,7 @@ exports.default = exports.getPlaywrightEnv(); function fixtureParameterNames(fn) { const text = fn.toString(); - const match = text.match(/async\s*\(\s*{\s*([^}]*)\s*}/); + const match = text.match(/async(?:\s+function)?\s*\(\s*{\s*([^}]*)\s*}/); if (!match || !match[1].trim()) return []; let signature = match[1]; diff --git a/test/network.spec.js b/test/network.jest.js similarity index 99% rename from test/network.spec.js rename to test/network.jest.js index f726647b65..9adc548c83 100644 --- a/test/network.spec.js +++ b/test/network.jest.js @@ -18,7 +18,7 @@ const fs = require('fs'); const path = require('path'); const utils = require('./utils'); -const {FFOX, CHROMIUM, WEBKIT, MAC, WIN} = utils.testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT, MAC, WIN} = testOptions; describe('Page.Events.Request', function() { it('should fire for navigation requests', async({page, server}) => { diff --git a/test/page.spec.js b/test/page.jest.js similarity index 99% rename from test/page.spec.js rename to test/page.jest.js index 58187085f1..a94ee48cea 100644 --- a/test/page.spec.js +++ b/test/page.jest.js @@ -18,7 +18,7 @@ const path = require('path'); const util = require('util'); const vm = require('vm'); -const {FFOX, CHROMIUM, WEBKIT, WIN, USES_HOOKS, CHANNEL} = require('./utils').testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT, WIN, USES_HOOKS, CHANNEL} = testOptions; describe('Page.close', function() { it('should reject all promises when page is closed', async({context}) => { @@ -113,7 +113,7 @@ describe('Async stacks', () => { }); }); -describe.fail(FFOX && WIN).skip(USES_HOOKS)('Page.Events.Crash', function() { +describe.fail(FFOX && WIN || USES_HOOKS)('Page.Events.Crash', function() { // Firefox Win: it just doesn't crash sometimes. function crash(pageImpl) { @@ -329,12 +329,12 @@ describe('Page.waitForRequest', function() { ]); expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); }); - it('should respect timeout', async({page, server}) => { + it('should respect timeout', async({page, playwright}) => { let error = null; await page.waitForEvent('request', { predicate: () => false, timeout: 1 }).catch(e => error = e); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); - it('should respect default timeout', async({page, server}) => { + it('should respect default timeout', async({page, playwright}) => { let error = null; page.setDefaultTimeout(1); await page.waitForEvent('request', () => false).catch(e => error = e); @@ -400,12 +400,12 @@ describe('Page.waitForResponse', function() { ]); expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); }); - it('should respect timeout', async({page, server}) => { + it('should respect timeout', async({page, playwright}) => { let error = null; await page.waitForEvent('response', { predicate: () => false, timeout: 1 }).catch(e => error = e); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); - it('should respect default timeout', async({page, server}) => { + it('should respect default timeout', async({page, playwright}) => { let error = null; page.setDefaultTimeout(1); await page.waitForEvent('response', () => false).catch(e => error = e); @@ -663,7 +663,7 @@ describe('Page.setContent', function() { const result = await page.content(); expect(result).toBe(`${doctype}${expectedOutput}`); }); - it('should respect timeout', async({page, server}) => { + it('should respect timeout', async({page, server, playwright}) => { const imgPath = '/img.png'; // stall for image server.setRoute(imgPath, (req, res) => {}); @@ -671,7 +671,7 @@ describe('Page.setContent', function() { await page.setContent(``, {timeout: 1}).catch(e => error = e); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); - it('should respect default navigation timeout', async({page, server}) => { + it('should respect default navigation timeout', async({page, server, playwright}) => { page.setDefaultNavigationTimeout(1); const imgPath = '/img.png'; // stall for image diff --git a/test/test.config.js b/test/test.config.js index 73bcee3c25..d7a860b82f 100644 --- a/test/test.config.js +++ b/test/test.config.js @@ -71,11 +71,6 @@ module.exports = { specs: [ { files: [ - './accessibility.spec.js', - './autowaiting.spec.js', - './click.spec.js', - './cookies.spec.js', - './coverage.spec.js', './dialog.spec.js', './dispatchevent.spec.js', './download.spec.js', @@ -88,8 +83,6 @@ module.exports = { './keyboard.spec.ts', './mouse.spec.js', './navigation.spec.js', - './network.spec.js', - './page.spec.js', './pdf.spec.js', './queryselector.spec.js', './screenshot.spec.js',