/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications 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.
*/
const path = require('path');
const util = require('util');
const vm = require('vm');
const {FFOX, CHROMIUM, WEBKIT} = require('./utils').testOptions(browserType);
describe('Page.close', function() {
it('should reject all promises when page is closed', async({context}) => {
const newPage = await context.newPage();
let error = null;
await Promise.all([
newPage.evaluate(() => new Promise(r => {})).catch(e => error = e),
newPage.close(),
]);
expect(error.message).toContain('Protocol error');
});
it('should not be visible in context.pages', async({context}) => {
const newPage = await context.newPage();
expect(context.pages()).toContain(newPage);
await newPage.close();
expect(context.pages()).not.toContain(newPage);
});
it('should run beforeunload if asked for', async({context, server}) => {
const newPage = await context.newPage();
await newPage.goto(server.PREFIX + '/beforeunload.html');
// We have to interact with a page so that 'beforeunload' handlers
// fire.
await newPage.click('body');
const pageClosingPromise = newPage.close({ runBeforeUnload: true });
const dialog = await newPage.waitForEvent('dialog');
expect(dialog.type()).toBe('beforeunload');
expect(dialog.defaultValue()).toBe('');
if (CHROMIUM)
expect(dialog.message()).toBe('');
else if (WEBKIT)
expect(dialog.message()).toBe('Leave?');
else
expect(dialog.message()).toBe('This page is asking you to confirm that you want to leave - data you have entered may not be saved.');
await dialog.accept();
await pageClosingPromise;
});
it('should *not* run beforeunload by default', async({context, server}) => {
const newPage = await context.newPage();
await newPage.goto(server.PREFIX + '/beforeunload.html');
// We have to interact with a page so that 'beforeunload' handlers
// fire.
await newPage.click('body');
await newPage.close();
});
it('should set the page close state', async({context}) => {
const newPage = await context.newPage();
expect(newPage.isClosed()).toBe(false);
await newPage.close();
expect(newPage.isClosed()).toBe(true);
});
it('should terminate network waiters', async({context, server}) => {
const newPage = await context.newPage();
const results = await Promise.all([
newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e),
newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e),
newPage.close()
]);
for (let i = 0; i < 2; i++) {
const message = results[i].message;
expect(message).toContain('Target closed');
expect(message).not.toContain('Timeout');
}
});
});
describe('Page.Events.Load', function() {
it('should fire when expected', async({page, server}) => {
await Promise.all([
page.goto('about:blank'),
page.waitForEvent('load'),
]);
});
});
describe('Async stacks', () => {
it('should work', async({page, server}) => {
server.setRoute('/empty.html', (req, res) => {
req.socket.end();
});
let error = null;
await page.goto(server.EMPTY_PAGE).catch(e => error = e);
expect(error).not.toBe(null);
expect(error.stack).toContain(__filename);
});
});
describe('Page.Events.Crash', function() {
function crash(page) {
if (CHROMIUM)
page.goto('chrome://crash').catch(e => {});
else if (WEBKIT)
page._delegate._session.send('Page.crash', {}).catch(e => {});
else if (FFOX)
page._delegate._session.send('Page.crash', {}).catch(e => {});
}
it('should emit crash event when page crashes', async({page}) => {
await page.setContent(`
This page should crash
`);
crash(page);
await new Promise(f => page.on('crash', f));
});
it('should throw on any action after page crashes', async({page}) => {
await page.setContent(`
This page should crash
`);
crash(page);
await page.waitForEvent('crash');
const err = await page.evaluate(() => {}).then(() => null, e => e);
expect(err).toBeTruthy();
expect(err.message).toContain('crash');
});
});
describe('Page.opener', function() {
it('should provide access to the opener page', async({page}) => {
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(() => window.open('about:blank')),
]);
const opener = await popup.opener();
expect(opener).toBe(page);
});
it('should return null if parent page has been closed', async({page}) => {
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(() => window.open('about:blank')),
]);
await page.close();
const opener = await popup.opener();
expect(opener).toBe(null);
});
});
describe('Page.Events.Console', function() {
it('should work', async({page, server}) => {
let message = null;
page.once('console', m => message = m);
await Promise.all([
page.evaluate(() => console.log('hello', 5, {foo: 'bar'})),
page.waitForEvent('console')
]);
expect(message.text()).toEqual('hello 5 JSHandle@object');
expect(message.type()).toEqual('log');
expect(await message.args()[0].jsonValue()).toEqual('hello');
expect(await message.args()[1].jsonValue()).toEqual(5);
expect(await message.args()[2].jsonValue()).toEqual({foo: 'bar'});
});
it('should emit same log twice', async({page, server}) => {
const messages = [];
page.on('console', m => messages.push(m.text()));
await page.evaluate(() => { for (let i = 0; i < 2; ++i ) console.log('hello'); } );
expect(messages).toEqual(['hello', 'hello']);
});
it('should use text() for inspection', async({page}) => {
let text;
const inspect = value => {
text = util.inspect(value);
}
page.on('console', inspect);
await page.evaluate(() => console.log('Hello world'));
expect(text).toEqual('Hello world');
});
it('should work for different console API calls', async({page, server}) => {
const messages = [];
page.on('console', msg => messages.push(msg));
// All console events will be reported before `page.evaluate` is finished.
await page.evaluate(() => {
// A pair of time/timeEnd generates only one Console API call.
console.time('calling console.time');
console.timeEnd('calling console.time');
console.trace('calling console.trace');
console.dir('calling console.dir');
console.warn('calling console.warn');
console.error('calling console.error');
console.log(Promise.resolve('should not wait until resolved!'));
});
expect(messages.map(msg => msg.type())).toEqual([
'timeEnd', 'trace', 'dir', 'warning', 'error', 'log'
]);
expect(messages[0].text()).toContain('calling console.time');
expect(messages.slice(1).map(msg => msg.text())).toEqual([
'calling console.trace',
'calling console.dir',
'calling console.warn',
'calling console.error',
'JSHandle@promise',
]);
});
it('should not fail for window object', async({page, server}) => {
let message = null;
page.once('console', msg => message = msg);
await Promise.all([
page.evaluate(() => console.error(window)),
page.waitForEvent('console')
]);
expect(message.text()).toBe('JSHandle@object');
});
it('should trigger correct Log', async({page, server}) => {
await page.goto('about:blank');
const [message] = await Promise.all([
page.waitForEvent('console'),
page.evaluate(async url => fetch(url).catch(e => {}), server.EMPTY_PAGE)
]);
expect(message.text()).toContain('Access-Control-Allow-Origin');
expect(message.type()).toEqual('error');
});
it('should have location for console API calls', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [message] = await Promise.all([
page.waitForEvent('console'),
page.goto(server.PREFIX + '/consolelog.html'),
]);
expect(message.text()).toBe('yellow');
expect(message.type()).toBe('log');
const location = message.location();
// Engines have different column notion.
delete location.columnNumber;
expect(location).toEqual({
url: server.PREFIX + '/consolelog.html',
lineNumber: 7,
});
});
// @see https://github.com/GoogleChrome/puppeteer/issues/3865
it('should not throw when there are console messages in detached iframes', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(async() => {
// 1. Create a popup that Playwright is not connected to.
const win = window.open('');
window._popup = win;
if (window.document.readyState !== 'complete')
await new Promise(f => window.addEventListener('load', f));
// 2. In this popup, create an iframe that console.logs a message.
win.document.body.innerHTML = ``;
const frame = win.document.querySelector('iframe');
if (!frame.contentDocument || frame.contentDocument.readyState !== 'complete')
await new Promise(f => frame.addEventListener('load', f));
// 3. After that, remove the iframe.
frame.remove();
}),
]);
// 4. Connect to the popup and make sure it doesn't throw.
expect(await popup.evaluate('1 + 1')).toBe(2);
});
});
describe('Page.Events.DOMContentLoaded', function() {
it('should fire when expected', async({page, server}) => {
const navigatedPromise = page.goto('about:blank');
await page.waitForEvent('domcontentloaded');
await navigatedPromise;
});
});
describe('Page.waitForRequest', function() {
it('should work', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest(server.PREFIX + '/digits/2.png'),
page.evaluate(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
});
it('should work with predicate', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForEvent('request', request => request.url() === server.PREFIX + '/digits/2.png'),
page.evaluate(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
});
it('should respect timeout', async({page, server}) => {
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}) => {
let error = null;
page.setDefaultTimeout(1);
await page.waitForEvent('request', () => false).catch(e => error = e);
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should work with no timeout', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest(server.PREFIX + '/digits/2.png', {timeout: 0}),
page.evaluate(() => setTimeout(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
}, 50))
]);
expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
});
it('should work with url match', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest(/digits\/\d\.png/),
page.evaluate(() => {
fetch('/digits/1.png');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/1.png');
});
it('should work with url match regular expression from a different context', async({page, server}) => {
const ctx = vm.createContext();
const regexp = vm.runInContext('new RegExp(/digits\\/\\d\\.png/)', ctx);
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
page.waitForRequest(regexp),
page.evaluate(() => {
fetch('/digits/1.png');
})
]);
expect(request.url()).toBe(server.PREFIX + '/digits/1.png');
});
});
describe('Page.waitForEvent', function() {
it('should fail with error upon disconnect', async({page, server}) => {
let error;
const waitForPromise = page.waitForEvent('download').catch(e => error = e);
await page.close();
await waitForPromise;
expect(error.message).toContain('Target closed');
});
});
describe('Page.waitForResponse', function() {
it('should work', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([
page.waitForResponse(server.PREFIX + '/digits/2.png'),
page.evaluate(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
})
]);
expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
});
it('should respect timeout', async({page, server}) => {
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}) => {
let error = null;
page.setDefaultTimeout(1);
await page.waitForEvent('response', () => false).catch(e => error = e);
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should work with predicate', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([
page.waitForEvent('response', response => response.url() === server.PREFIX + '/digits/2.png'),
page.evaluate(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
})
]);
expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
});
it('should work with no timeout', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([
page.waitForResponse(server.PREFIX + '/digits/2.png', { timeout: 0 }),
page.evaluate(() => setTimeout(() => {
fetch('/digits/1.png');
fetch('/digits/2.png');
fetch('/digits/3.png');
}, 50))
]);
expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
});
});
describe('Page.exposeBinding', () => {
it('should work', async({browser}) => {
const context = await browser.newContext();
const page = await context.newPage();
let bindingSource;
await page.exposeBinding('add', (source, a, b) => {
bindingSource = source;
return a + b;
});
const result = await page.evaluate(async function() {
return add(5, 6);
});
expect(bindingSource.context).toBe(context);
expect(bindingSource.page).toBe(page);
expect(bindingSource.frame).toBe(page.mainFrame());
expect(result).toEqual(11);
await context.close();
});
});
describe('Page.exposeFunction', function() {
it('should work', async({page, server}) => {
await page.exposeFunction('compute', function(a, b) {
return a * b;
});
const result = await page.evaluate(async function() {
return await compute(9, 4);
});
expect(result).toBe(36);
});
it('should throw exception in page context', async({page, server}) => {
await page.exposeFunction('woof', function() {
throw new Error('WOOF WOOF');
});
const {message, stack} = await page.evaluate(async() => {
try {
await woof();
} catch (e) {
return {message: e.message, stack: e.stack};
}
});
expect(message).toBe('WOOF WOOF');
expect(stack).toContain(__filename);
});
it('should support throwing "null"', async({page, server}) => {
await page.exposeFunction('woof', function() {
throw null;
});
const thrown = await page.evaluate(async() => {
try {
await woof();
} catch (e) {
return e;
}
});
expect(thrown).toBe(null);
});
it('should be callable from-inside addInitScript', async({page, server}) => {
let called = false;
await page.exposeFunction('woof', function() {
called = true;
});
await page.addInitScript(() => woof());
await page.reload();
expect(called).toBe(true);
});
it('should survive navigation', async({page, server}) => {
await page.exposeFunction('compute', function(a, b) {
return a * b;
});
await page.goto(server.EMPTY_PAGE);
const result = await page.evaluate(async function() {
return await compute(9, 4);
});
expect(result).toBe(36);
});
it('should await returned promise', async({page, server}) => {
await page.exposeFunction('compute', function(a, b) {
return Promise.resolve(a * b);
});
const result = await page.evaluate(async function() {
return await compute(3, 5);
});
expect(result).toBe(15);
});
it('should work on frames', async({page, server}) => {
await page.exposeFunction('compute', function(a, b) {
return Promise.resolve(a * b);
});
await page.goto(server.PREFIX + '/frames/nested-frames.html');
const frame = page.frames()[1];
const result = await frame.evaluate(async function() {
return await compute(3, 5);
});
expect(result).toBe(15);
});
it('should work on frames before navigation', async({page, server}) => {
await page.goto(server.PREFIX + '/frames/nested-frames.html');
await page.exposeFunction('compute', function(a, b) {
return Promise.resolve(a * b);
});
const frame = page.frames()[1];
const result = await frame.evaluate(async function() {
return await compute(3, 5);
});
expect(result).toBe(15);
});
it('should work after cross origin navigation', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
await page.exposeFunction('compute', function(a, b) {
return a * b;
});
await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
const result = await page.evaluate(async function() {
return await compute(9, 4);
});
expect(result).toBe(36);
});
it('should work with complex objects', async({page, server}) => {
await page.exposeFunction('complexObject', function(a, b) {
return {x: a.x + b.x};
});
const result = await page.evaluate(async() => complexObject({x: 5}, {x: 2}));
expect(result.x).toBe(7);
});
});
describe('Page.Events.PageError', function() {
it('should fire', async({page, server}) => {
const [error] = await Promise.all([
page.waitForEvent('pageerror'),
page.goto(server.PREFIX + '/error.html'),
]);
expect(error.name).toBe('Error');
expect(error.message).toBe('Fancy error!');
let stack = await page.evaluate(() => window.e.stack);
// Note that WebKit reports the stack of the 'throw' statement instead of the Error constructor call.
if (WEBKIT)
stack = stack.replace('14:25', '15:19');
expect(error.stack).toBe(stack);
});
it.fail(WEBKIT)('should contain sourceURL', async({page, server}) => {
const [error] = await Promise.all([
page.waitForEvent('pageerror'),
page.goto(server.PREFIX + '/error.html'),
]);
expect(error.stack).toContain('myscript.js');
});
});
describe('Page.setContent', function() {
const expectedOutput = '