chore: add Prettier (#5825)

This commit is contained in:
Jack Franklin 2020-05-07 11:54:55 +01:00 committed by GitHub
parent ae576aff61
commit 4fdb1e3cab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 12166 additions and 7718 deletions

View File

@ -13,33 +13,29 @@ module.exports = {
"unicorn"
],
"extends": [
"plugin:prettier/recommended"
],
"rules": {
// Error if files are not formatted with Prettier correctly.
"prettier/prettier": 2,
// syntax preferences
"quotes": [2, "single", {
"avoidEscape": true,
"allowTemplateLiterals": true
}],
"semi": 2,
"no-extra-semi": 2,
"comma-style": [2, "last"],
"wrap-iife": [2, "inside"],
"spaced-comment": [2, "always", {
"markers": ["*"]
}],
"eqeqeq": [2],
"arrow-body-style": [2, "as-needed"],
"accessor-pairs": [2, {
"getWithoutSet": false,
"setWithoutGet": false
}],
"brace-style": [2, "1tbs", {"allowSingleLine": true}],
"curly": [2, "multi-or-nest", "consistent"],
"object-curly-spacing": [2, "never"],
"new-parens": 2,
"func-call-spacing": 2,
"arrow-parens": [2, "as-needed"],
"prefer-const": 2,
"quote-props": [2, "consistent"],
// anti-patterns
"no-var": 2,
@ -68,37 +64,6 @@ module.exports = {
"require-yield": 2,
"template-curly-spacing": [2, "never"],
// spacing details
"space-infix-ops": 2,
"space-in-parens": [2, "never"],
"space-before-function-paren": [2, "never"],
"no-whitespace-before-property": 2,
"keyword-spacing": [2, {
"overrides": {
"if": {"after": true},
"else": {"after": true},
"for": {"after": true},
"while": {"after": true},
"do": {"after": true},
"switch": {"after": true},
"return": {"after": true}
}
}],
"arrow-spacing": [2, {
"after": true,
"before": true
}],
// file whitespace
"no-multiple-empty-lines": [2, {"max": 2}],
"no-mixed-spaces-and-tabs": 2,
"no-trailing-spaces": 2,
"linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ],
"indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }],
"key-spacing": [2, {
"beforeColon": false
}],
// ensure we don't have any it.only or describe.only in prod
"mocha/no-exclusive-tests": "error",

View File

@ -18,19 +18,16 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', request => {
if (request.resourceType() === 'image')
request.abort();
else
request.continue();
page.on('request', (request) => {
if (request.resourceType() === 'image') request.abort();
else request.continue();
});
await page.goto('https://news.google.com/news/');
await page.screenshot({path: 'news.png', fullPage: true});
await page.screenshot({ path: 'news.png', fullPage: true });
await browser.close();
})();

View File

@ -25,7 +25,7 @@ const firefoxOptions = {
dumpio: true,
};
(async() => {
(async () => {
const browser = await puppeteer.launch(firefoxOptions);
const page = await browser.newPage();
@ -35,9 +35,9 @@ const firefoxOptions = {
// Extract articles from the page.
const resultsSelector = '.storylink';
const links = await page.evaluate(resultsSelector => {
const links = await page.evaluate((resultsSelector) => {
const anchors = Array.from(document.querySelectorAll(resultsSelector));
return anchors.map(anchor => {
return anchors.map((anchor) => {
const title = anchor.textContent.trim();
return `${title} - ${anchor.href}`;
});

View File

@ -18,12 +18,12 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Define a window.onCustomEvent function on the page.
await page.exposeFunction('onCustomEvent', e => {
await page.exposeFunction('onCustomEvent', (e) => {
console.log(`${e.type} fired`, e.detail || '');
});
@ -33,16 +33,18 @@ const puppeteer = require('puppeteer');
* @return {!Promise}
*/
function listenFor(type) {
return page.evaluateOnNewDocument(type => {
document.addEventListener(type, e => {
window.onCustomEvent({type, detail: e.detail});
return page.evaluateOnNewDocument((type) => {
document.addEventListener(type, (e) => {
window.onCustomEvent({ type, detail: e.detail });
});
}, type);
}
await listenFor('app-ready'); // Listen for "app-ready" custom event on page load.
await page.goto('https://www.chromestatus.com/features', {waitUntil: 'networkidle0'});
await page.goto('https://www.chromestatus.com/features', {
waitUntil: 'networkidle0',
});
await browser.close();
})();

View File

@ -22,22 +22,22 @@ function sniffDetector() {
const userAgent = window.navigator.userAgent;
const platform = window.navigator.platform;
window.navigator.__defineGetter__('userAgent', function() {
window.navigator.__defineGetter__('userAgent', function () {
window.navigator.sniffed = true;
return userAgent;
});
window.navigator.__defineGetter__('platform', function() {
window.navigator.__defineGetter__('platform', function () {
window.navigator.sniffed = true;
return platform;
});
}
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.evaluateOnNewDocument(sniffDetector);
await page.goto('https://www.google.com', {waitUntil: 'networkidle2'});
await page.goto('https://www.google.com', { waitUntil: 'networkidle2' });
console.log('Sniffed: ' + (await page.evaluate(() => !!navigator.sniffed)));
await browser.close();

View File

@ -18,15 +18,17 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'});
await page.goto('https://news.ycombinator.com', {
waitUntil: 'networkidle2',
});
// page.pdf() is currently supported only in headless mode.
// @see https://bugs.chromium.org/p/chromium/issues/detail?id=753118
await page.pdf({
path: 'hn.pdf',
format: 'letter'
format: 'letter',
});
await browser.close();

View File

@ -18,7 +18,7 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch({
// Launch chromium using a proxy server on port 9876.
// More on proxying:
@ -27,7 +27,7 @@ const puppeteer = require('puppeteer');
'--proxy-server=127.0.0.1:9876',
// Use proxy for localhost URLs
'--proxy-bypass-list=<-loopback>',
]
],
});
const page = await browser.newPage();
await page.goto('https://google.com');

View File

@ -19,11 +19,11 @@
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.emulate(devices['iPhone 6']);
await page.goto('https://www.nytimes.com/');
await page.screenshot({path: 'full.png', fullPage: true});
await page.screenshot({ path: 'full.png', fullPage: true });
await browser.close();
})();

View File

@ -18,10 +18,10 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://example.com');
await page.screenshot({path: 'example.png'});
await page.screenshot({ path: 'example.png' });
await browser.close();
})();

View File

@ -23,7 +23,7 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
@ -42,9 +42,9 @@ const puppeteer = require('puppeteer');
await page.waitForSelector(resultsSelector);
// Extract the results from the page.
const links = await page.evaluate(resultsSelector => {
const links = await page.evaluate((resultsSelector) => {
const anchors = Array.from(document.querySelectorAll(resultsSelector));
return anchors.map(anchor => {
return anchors.map((anchor) => {
const title = anchor.textContent.split('|')[0].trim();
return `${title} - ${anchor.href}`;
});

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
const {helper} = require('./lib/helper');
const { helper } = require('./lib/helper');
const api = require('./lib/api');
const {Page} = require('./lib/Page');
const { Page } = require('./lib/Page');
for (const className in api) {
if (typeof api[className] === 'function')
helper.installAsyncStackHooks(api[className]);
@ -25,16 +25,25 @@ for (const className in api) {
// Expose alias for deprecated method.
Page.prototype.emulateMedia = Page.prototype.emulateMediaType;
const {Puppeteer} = require('./lib/Puppeteer');
const { Puppeteer } = require('./lib/Puppeteer');
const packageJson = require('./package.json');
let preferredRevision = packageJson.puppeteer.chromium_revision;
const isPuppeteerCore = packageJson.name === 'puppeteer-core';
// puppeteer-core ignores environment variables
const product = isPuppeteerCore ? undefined : process.env.PUPPETEER_PRODUCT || process.env.npm_config_puppeteer_product || process.env.npm_package_config_puppeteer_product;
const product = isPuppeteerCore
? undefined
: process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product;
if (!isPuppeteerCore && product === 'firefox')
preferredRevision = packageJson.puppeteer.firefox_revision;
const puppeteer = new Puppeteer(__dirname, preferredRevision, isPuppeteerCore, product);
const puppeteer = new Puppeteer(
__dirname,
preferredRevision,
isPuppeteerCore,
product
);
// The introspection in `Helper.installAsyncStackHooks` references `Puppeteer._launcher`
// before the Puppeteer ctor is called, such that an invalid Launcher is selected at import,

View File

@ -27,27 +27,44 @@
const compileTypeScriptIfRequired = require('./typescript-if-required');
const supportedProducts = {
'chrome': 'Chromium',
'firefox': 'Firefox Nightly'
chrome: 'Chromium',
firefox: 'Firefox Nightly',
};
async function download() {
await compileTypeScriptIfRequired();
const downloadHost = process.env.PUPPETEER_DOWNLOAD_HOST || process.env.npm_config_puppeteer_download_host || process.env.npm_package_config_puppeteer_download_host;
const downloadHost =
process.env.PUPPETEER_DOWNLOAD_HOST ||
process.env.npm_config_puppeteer_download_host ||
process.env.npm_package_config_puppeteer_download_host;
const puppeteer = require('./index');
const product = process.env.PUPPETEER_PRODUCT || process.env.npm_config_puppeteer_product || process.env.npm_package_config_puppeteer_product || 'chrome';
const browserFetcher = puppeteer.createBrowserFetcher({product, host: downloadHost});
const product =
process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product ||
'chrome';
const browserFetcher = puppeteer.createBrowserFetcher({
product,
host: downloadHost,
});
const revision = await getRevision();
await fetchBinary(revision);
function getRevision() {
if (product === 'chrome') {
return process.env.PUPPETEER_CHROMIUM_REVISION || process.env.npm_config_puppeteer_chromium_revision || process.env.npm_package_config_puppeteer_chromium_revision
|| require('./package.json').puppeteer.chromium_revision;
return (
process.env.PUPPETEER_CHROMIUM_REVISION ||
process.env.npm_config_puppeteer_chromium_revision ||
process.env.npm_package_config_puppeteer_chromium_revision ||
require('./package.json').puppeteer.chromium_revision
);
} else if (product === 'firefox') {
puppeteer._preferredRevision = require('./package.json').puppeteer.firefox_revision;
return getFirefoxNightlyVersion(browserFetcher.host()).catch(error => { console.error(error); process.exit(1); });
return getFirefoxNightlyVersion(browserFetcher.host()).catch((error) => {
console.error(error);
process.exit(1);
});
} else {
throw new Error(`Unsupported product ${product}`);
}
@ -58,30 +75,37 @@ async function download() {
// Do nothing if the revision is already downloaded.
if (revisionInfo.local) {
logPolitely(`${supportedProducts[product]} is already in ${revisionInfo.folderPath}; skipping download.`);
logPolitely(
`${supportedProducts[product]} is already in ${revisionInfo.folderPath}; skipping download.`
);
return;
}
// Override current environment proxy settings with npm configuration, if any.
const NPM_HTTPS_PROXY = process.env.npm_config_https_proxy || process.env.npm_config_proxy;
const NPM_HTTP_PROXY = process.env.npm_config_http_proxy || process.env.npm_config_proxy;
const NPM_HTTPS_PROXY =
process.env.npm_config_https_proxy || process.env.npm_config_proxy;
const NPM_HTTP_PROXY =
process.env.npm_config_http_proxy || process.env.npm_config_proxy;
const NPM_NO_PROXY = process.env.npm_config_no_proxy;
if (NPM_HTTPS_PROXY)
process.env.HTTPS_PROXY = NPM_HTTPS_PROXY;
if (NPM_HTTP_PROXY)
process.env.HTTP_PROXY = NPM_HTTP_PROXY;
if (NPM_NO_PROXY)
process.env.NO_PROXY = NPM_NO_PROXY;
if (NPM_HTTPS_PROXY) process.env.HTTPS_PROXY = NPM_HTTPS_PROXY;
if (NPM_HTTP_PROXY) process.env.HTTP_PROXY = NPM_HTTP_PROXY;
if (NPM_NO_PROXY) process.env.NO_PROXY = NPM_NO_PROXY;
/**
* @param {!Array<string>}
* @return {!Promise}
*/
function onSuccess(localRevisions) {
logPolitely(`${supportedProducts[product]} (${revisionInfo.revision}) downloaded to ${revisionInfo.folderPath}`);
localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));
logPolitely(
`${supportedProducts[product]} (${revisionInfo.revision}) downloaded to ${revisionInfo.folderPath}`
);
localRevisions = localRevisions.filter(
(revision) => revision !== revisionInfo.revision
);
const cleanupOldVersions = localRevisions.map((revision) =>
browserFetcher.remove(revision)
);
Promise.all([...cleanupOldVersions]);
}
@ -89,7 +113,9 @@ async function download() {
* @param {!Error} error
*/
function onError(error) {
console.error(`ERROR: Failed to set up ${supportedProducts[product]} r${revision}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.`);
console.error(
`ERROR: Failed to set up ${supportedProducts[product]} r${revision}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.`
);
console.error(error);
process.exit(1);
}
@ -99,22 +125,28 @@ async function download() {
function onProgress(downloadedBytes, totalBytes) {
if (!progressBar) {
const ProgressBar = require('progress');
progressBar = new ProgressBar(`Downloading ${supportedProducts[product]} r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
});
progressBar = new ProgressBar(
`Downloading ${
supportedProducts[product]
} r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `,
{
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
}
);
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
}
return browserFetcher.download(revisionInfo.revision, onProgress)
.then(() => browserFetcher.localRevisions())
.then(onSuccess)
.catch(onError);
return browserFetcher
.download(revisionInfo.revision, onProgress)
.then(() => browserFetcher.localRevisions())
.then(onSuccess)
.catch(onError);
}
function toMegabytes(bytes) {
@ -127,14 +159,16 @@ async function download() {
const promise = new Promise((resolve, reject) => {
let data = '';
logPolitely(`Requesting latest Firefox Nightly version from ${host}`);
https.get(host + '/', r => {
if (r.statusCode >= 400)
return reject(new Error(`Got status code ${r.statusCode}`));
r.on('data', chunk => {
data += chunk;
});
r.on('end', parseVersion);
}).on('error', reject);
https
.get(host + '/', (r) => {
if (r.statusCode >= 400)
return reject(new Error(`Got status code ${r.statusCode}`));
r.on('data', (chunk) => {
data += chunk;
});
r.on('end', parseVersion);
})
.on('error', reject);
function parseVersion() {
const regex = /firefox\-(?<version>\d\d)\..*/gm;
@ -142,11 +176,9 @@ async function download() {
let match;
while ((match = regex.exec(data)) !== null) {
const version = parseInt(match.groups.version, 10);
if (version > result)
result = version;
if (version > result) result = version;
}
if (result)
resolve(result.toString());
if (result) resolve(result.toString());
else reject(new Error('Firefox version not found'));
}
});
@ -158,34 +190,56 @@ function logPolitely(toBeLogged) {
const logLevel = process.env.npm_config_loglevel;
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1;
if (!logLevelDisplay)
console.log(toBeLogged);
if (!logLevelDisplay) console.log(toBeLogged);
}
if (process.env.PUPPETEER_SKIP_DOWNLOAD) {
logPolitely('**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.');
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.'
);
return;
}
if (process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD || process.env.npm_config_puppeteer_skip_download) {
logPolitely('**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.');
if (
process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD ||
process.env.npm_config_puppeteer_skip_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.'
);
return;
}
if (process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD || process.env.npm_package_config_puppeteer_skip_download) {
logPolitely('**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.');
if (
process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD ||
process.env.npm_package_config_puppeteer_skip_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.'
);
return;
}
if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) {
logPolitely('**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.');
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.'
);
return;
}
if (process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || process.env.npm_config_puppeteer_skip_chromium_download) {
logPolitely('**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.');
if (
process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD ||
process.env.npm_config_puppeteer_skip_chromium_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.'
);
return;
}
if (process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || process.env.npm_package_config_puppeteer_skip_chromium_download) {
logPolitely('**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.');
if (
process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD ||
process.env.npm_package_config_puppeteer_skip_chromium_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.'
);
return;
}
download();

View File

@ -67,7 +67,9 @@
"commonmark": "^0.28.1",
"cross-env": "^5.0.5",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-mocha": "^6.3.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-unicorn": "^19.0.1",
"esprima": "^4.0.0",
"expect": "^25.2.7",
@ -77,6 +79,7 @@
"ncp": "^2.0.0",
"pixelmatch": "^4.0.2",
"pngjs": "^5.0.0",
"prettier": "^2.0.5",
"text-diff": "^1.0.1",
"typescript": "3.8.3"
},

5
prettier.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
};

View File

@ -16,16 +16,15 @@
// Used as a TypeDef
// eslint-disable-next-line no-unused-vars
import {CDPSession} from './Connection';
import { CDPSession } from './Connection';
// Used as a TypeDef
// eslint-disable-next-line no-unused-vars
import {ElementHandle} from './JSHandle';
import { ElementHandle } from './JSHandle';
interface SerializedAXNode {
role: string;
name?: string;
value?: string|number;
value?: string | number;
description?: string;
keyshortcuts?: string;
roledescription?: string;
@ -39,8 +38,8 @@ interface SerializedAXNode {
readonly?: boolean;
required?: boolean;
selected?: boolean;
checked?: boolean|'mixed';
pressed?: boolean|'mixed';
checked?: boolean | 'mixed';
pressed?: boolean | 'mixed';
level?: number;
valuemin?: number;
valuemax?: number;
@ -58,31 +57,31 @@ export class Accessibility {
this._client = client;
}
async snapshot(options: {interestingOnly?: boolean; root?: ElementHandle} = {}): Promise<SerializedAXNode> {
const {
interestingOnly = true,
root = null,
} = options;
const {nodes} = await this._client.send('Accessibility.getFullAXTree');
async snapshot(
options: { interestingOnly?: boolean; root?: ElementHandle } = {}
): Promise<SerializedAXNode> {
const { interestingOnly = true, root = null } = options;
const { nodes } = await this._client.send('Accessibility.getFullAXTree');
let backendNodeId = null;
if (root) {
const {node} = await this._client.send('DOM.describeNode', {objectId: root._remoteObject.objectId});
const { node } = await this._client.send('DOM.describeNode', {
objectId: root._remoteObject.objectId,
});
backendNodeId = node.backendNodeId;
}
const defaultRoot = AXNode.createTree(nodes);
let needle = defaultRoot;
if (backendNodeId) {
needle = defaultRoot.find(node => node._payload.backendDOMNodeId === backendNodeId);
if (!needle)
return null;
needle = defaultRoot.find(
(node) => node._payload.backendDOMNodeId === backendNodeId
);
if (!needle) return null;
}
if (!interestingOnly)
return serializeTree(needle)[0];
if (!interestingOnly) return serializeTree(needle)[0];
const interestingNodes = new Set<AXNode>();
collectInterestingNodes(interestingNodes, defaultRoot, false);
if (!interestingNodes.has(needle))
return null;
if (!interestingNodes.has(needle)) return null;
return serializeTree(needle, interestingNodes)[0];
}
}
@ -92,31 +91,33 @@ export class Accessibility {
* @param {!AXNode} node
* @param {boolean} insideControl
*/
function collectInterestingNodes(collection: Set<AXNode>, node: AXNode, insideControl: boolean): void {
if (node.isInteresting(insideControl))
collection.add(node);
if (node.isLeafNode())
return;
function collectInterestingNodes(
collection: Set<AXNode>,
node: AXNode,
insideControl: boolean
): void {
if (node.isInteresting(insideControl)) collection.add(node);
if (node.isLeafNode()) return;
insideControl = insideControl || node.isControl();
for (const child of node._children)
collectInterestingNodes(collection, child, insideControl);
}
function serializeTree(node: AXNode, whitelistedNodes?: Set<AXNode>): SerializedAXNode[] {
function serializeTree(
node: AXNode,
whitelistedNodes?: Set<AXNode>
): SerializedAXNode[] {
const children: SerializedAXNode[] = [];
for (const child of node._children)
children.push(...serializeTree(child, whitelistedNodes));
if (whitelistedNodes && !whitelistedNodes.has(node))
return children;
if (whitelistedNodes && !whitelistedNodes.has(node)) return children;
const serializedNode = node.serialize();
if (children.length)
serializedNode.children = children;
if (children.length) serializedNode.children = children;
return [serializedNode];
}
class AXNode {
_payload: Protocol.Accessibility.AXNode;
_children: AXNode[] = [];
@ -139,27 +140,25 @@ class AXNode {
this._richlyEditable = property.value.value === 'richtext';
this._editable = true;
}
if (property.name === 'focusable')
this._focusable = property.value.value;
if (property.name === 'expanded')
this._expanded = property.value.value;
if (property.name === 'hidden')
this._hidden = property.value.value;
if (property.name === 'focusable') this._focusable = property.value.value;
if (property.name === 'expanded') this._expanded = property.value.value;
if (property.name === 'hidden') this._hidden = property.value.value;
}
}
_isPlainTextField(): boolean {
if (this._richlyEditable)
return false;
if (this._editable)
return true;
return this._role === 'textbox' || this._role === 'ComboBox' || this._role === 'searchbox';
if (this._richlyEditable) return false;
if (this._editable) return true;
return (
this._role === 'textbox' ||
this._role === 'ComboBox' ||
this._role === 'searchbox'
);
}
_isTextOnlyObject(): boolean {
const role = this._role;
return (role === 'LineBreak' || role === 'text' ||
role === 'InlineTextBox');
return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox';
}
_hasFocusableChild(): boolean {
@ -176,26 +175,22 @@ class AXNode {
}
find(predicate: (x: AXNode) => boolean): AXNode | null {
if (predicate(this))
return this;
if (predicate(this)) return this;
for (const child of this._children) {
const result = child.find(predicate);
if (result)
return result;
if (result) return result;
}
return null;
}
isLeafNode(): boolean {
if (!this._children.length)
return true;
if (!this._children.length) return true;
// These types of objects may have children that we use as internal
// implementation details, but we want to expose them as leaves to platform
// accessibility APIs because screen readers might be confused if they find
// any children.
if (this._isPlainTextField() || this._isTextOnlyObject())
return true;
if (this._isPlainTextField() || this._isTextOnlyObject()) return true;
// Roles whose children are only presentational according to the ARIA and
// HTML5 Specs should be hidden from screen readers.
@ -216,12 +211,9 @@ class AXNode {
}
// Here and below: Android heuristics
if (this._hasFocusableChild())
return false;
if (this._focusable && this._name)
return true;
if (this._role === 'heading' && this._name)
return true;
if (this._hasFocusableChild()) return false;
if (this._focusable && this._name) return true;
if (this._role === 'heading' && this._name) return true;
return false;
}
@ -259,39 +251,39 @@ class AXNode {
*/
isInteresting(insideControl: boolean): boolean {
const role = this._role;
if (role === 'Ignored' || this._hidden)
return false;
if (role === 'Ignored' || this._hidden) return false;
if (this._focusable || this._richlyEditable)
return true;
if (this._focusable || this._richlyEditable) return true;
// If it's not focusable but has a control role, then it's interesting.
if (this.isControl())
return true;
if (this.isControl()) return true;
// A non focusable child of a control is not interesting
if (insideControl)
return false;
if (insideControl) return false;
return this.isLeafNode() && !!this._name;
}
serialize(): SerializedAXNode {
const properties = new Map<string, number|string|boolean>();
const properties = new Map<string, number | string | boolean>();
for (const property of this._payload.properties || [])
properties.set(property.name.toLowerCase(), property.value.value);
if (this._payload.name)
properties.set('name', this._payload.name.value);
if (this._payload.value)
properties.set('value', this._payload.value.value);
if (this._payload.name) properties.set('name', this._payload.name.value);
if (this._payload.value) properties.set('value', this._payload.value.value);
if (this._payload.description)
properties.set('description', this._payload.description.value);
const node: SerializedAXNode = {
role: this._role
role: this._role,
};
type UserStringProperty = 'name'|'value'|'description'|'keyshortcuts'|'roledescription'|'valuetext';
type UserStringProperty =
| 'name'
| 'value'
| 'description'
| 'keyshortcuts'
| 'roledescription'
| 'valuetext';
const userStringProperties: UserStringProperty[] = [
'name',
@ -301,16 +293,25 @@ class AXNode {
'roledescription',
'valuetext',
];
const getUserStringPropertyValue = (key: UserStringProperty): string => properties.get(key) as string;
const getUserStringPropertyValue = (key: UserStringProperty): string =>
properties.get(key) as string;
for (const userStringProperty of userStringProperties) {
if (!properties.has(userStringProperty))
continue;
if (!properties.has(userStringProperty)) continue;
node[userStringProperty] = getUserStringPropertyValue(userStringProperty);
}
type BooleanProperty = 'disabled'|'expanded'|'focused'|'modal'|'multiline'|'multiselectable'|'readonly'|'required'|'selected';
type BooleanProperty =
| 'disabled'
| 'expanded'
| 'focused'
| 'modal'
| 'multiline'
| 'multiselectable'
| 'readonly'
| 'required'
| 'selected';
const booleanProperties: BooleanProperty[] = [
'disabled',
'expanded',
@ -322,58 +323,56 @@ class AXNode {
'required',
'selected',
];
const getBooleanPropertyValue = (key: BooleanProperty): boolean => properties.get(key) as boolean;
const getBooleanPropertyValue = (key: BooleanProperty): boolean =>
properties.get(key) as boolean;
for (const booleanProperty of booleanProperties) {
// WebArea's treat focus differently than other nodes. They report whether their frame has focus,
// not whether focus is specifically on the root node.
if (booleanProperty === 'focused' && this._role === 'WebArea')
continue;
if (booleanProperty === 'focused' && this._role === 'WebArea') continue;
const value = getBooleanPropertyValue(booleanProperty);
if (!value)
continue;
if (!value) continue;
node[booleanProperty] = getBooleanPropertyValue(booleanProperty);
}
type TristateProperty = 'checked'|'pressed';
const tristateProperties: TristateProperty[] = [
'checked',
'pressed',
];
type TristateProperty = 'checked' | 'pressed';
const tristateProperties: TristateProperty[] = ['checked', 'pressed'];
for (const tristateProperty of tristateProperties) {
if (!properties.has(tristateProperty))
continue;
if (!properties.has(tristateProperty)) continue;
const value = properties.get(tristateProperty);
node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
node[tristateProperty] =
value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
}
type NumbericalProperty = 'level'|'valuemax'|'valuemin';
type NumbericalProperty = 'level' | 'valuemax' | 'valuemin';
const numericalProperties: NumbericalProperty[] = [
'level',
'valuemax',
'valuemin',
];
const getNumericalPropertyValue = (key: NumbericalProperty): number => properties.get(key) as number;
const getNumericalPropertyValue = (key: NumbericalProperty): number =>
properties.get(key) as number;
for (const numericalProperty of numericalProperties) {
if (!properties.has(numericalProperty))
continue;
if (!properties.has(numericalProperty)) continue;
node[numericalProperty] = getNumericalPropertyValue(numericalProperty);
}
type TokenProperty = 'autocomplete'|'haspopup'|'invalid'|'orientation';
type TokenProperty =
| 'autocomplete'
| 'haspopup'
| 'invalid'
| 'orientation';
const tokenProperties: TokenProperty[] = [
'autocomplete',
'haspopup',
'invalid',
'orientation',
];
const getTokenPropertyValue = (key: TokenProperty): string => properties.get(key) as string;
const getTokenPropertyValue = (key: TokenProperty): string =>
properties.get(key) as string;
for (const tokenProperty of tokenProperties) {
const value = getTokenPropertyValue(tokenProperty);
if (!value || value === 'false')
continue;
if (!value || value === 'false') continue;
node[tokenProperty] = getTokenPropertyValue(tokenProperty);
}
return node;

View File

@ -14,214 +14,283 @@
* limitations under the License.
*/
import {helper, assert} from './helper';
import {Target} from './Target';
import { helper, assert } from './helper';
import { Target } from './Target';
import * as EventEmitter from 'events';
import {TaskQueue} from './TaskQueue';
import {Events} from './Events';
import {Connection} from './Connection';
import {Page} from './Page';
import {ChildProcess} from 'child_process';
import type {Viewport} from './PuppeteerViewport';
import { TaskQueue } from './TaskQueue';
import { Events } from './Events';
import { Connection } from './Connection';
import { Page } from './Page';
import { ChildProcess } from 'child_process';
import type { Viewport } from './PuppeteerViewport';
type BrowserCloseCallback = () => Promise<void> | void;
export class Browser extends EventEmitter {
static async create(connection: Connection, contextIds: string[], ignoreHTTPSErrors: boolean, defaultViewport?: Viewport, process?: ChildProcess, closeCallback?: BrowserCloseCallback): Promise<Browser> {
const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback);
await connection.send('Target.setDiscoverTargets', {discover: true});
static async create(
connection: Connection,
contextIds: string[],
ignoreHTTPSErrors: boolean,
defaultViewport?: Viewport,
process?: ChildProcess,
closeCallback?: BrowserCloseCallback
): Promise<Browser> {
const browser = new Browser(
connection,
contextIds,
ignoreHTTPSErrors,
defaultViewport,
process,
closeCallback
);
await connection.send('Target.setDiscoverTargets', { discover: true });
return browser;
}
_ignoreHTTPSErrors: boolean;
_defaultViewport?: Viewport;
_process?: ChildProcess;
_screenshotTaskQueue = new TaskQueue();
_connection: Connection;
_closeCallback: BrowserCloseCallback;
_defaultContext: BrowserContext;
_contexts: Map<string, BrowserContext>;
// TODO: once Target is in TypeScript we can type this properly.
_targets: Map<string, Target>;
_ignoreHTTPSErrors: boolean;
_defaultViewport?: Viewport;
_process?: ChildProcess;
_screenshotTaskQueue = new TaskQueue();
_connection: Connection;
_closeCallback: BrowserCloseCallback;
_defaultContext: BrowserContext;
_contexts: Map<string, BrowserContext>;
// TODO: once Target is in TypeScript we can type this properly.
_targets: Map<string, Target>;
constructor(connection: Connection, contextIds: string[], ignoreHTTPSErrors: boolean, defaultViewport?: Viewport, process?: ChildProcess, closeCallback?: BrowserCloseCallback) {
super();
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport;
this._process = process;
this._screenshotTaskQueue = new TaskQueue();
this._connection = connection;
this._closeCallback = closeCallback || function(): void {};
constructor(
connection: Connection,
contextIds: string[],
ignoreHTTPSErrors: boolean,
defaultViewport?: Viewport,
process?: ChildProcess,
closeCallback?: BrowserCloseCallback
) {
super();
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport;
this._process = process;
this._screenshotTaskQueue = new TaskQueue();
this._connection = connection;
this._closeCallback = closeCallback || function (): void {};
this._defaultContext = new BrowserContext(this._connection, this, null);
this._contexts = new Map();
for (const contextId of contextIds)
this._contexts.set(contextId, new BrowserContext(this._connection, this, contextId));
this._defaultContext = new BrowserContext(this._connection, this, null);
this._contexts = new Map();
for (const contextId of contextIds)
this._contexts.set(
contextId,
new BrowserContext(this._connection, this, contextId)
);
this._targets = new Map();
this._connection.on(Events.Connection.Disconnected, () => this.emit(Events.Browser.Disconnected));
this._connection.on('Target.targetCreated', this._targetCreated.bind(this));
this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
this._connection.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
}
this._targets = new Map();
this._connection.on(Events.Connection.Disconnected, () =>
this.emit(Events.Browser.Disconnected)
);
this._connection.on('Target.targetCreated', this._targetCreated.bind(this));
this._connection.on(
'Target.targetDestroyed',
this._targetDestroyed.bind(this)
);
this._connection.on(
'Target.targetInfoChanged',
this._targetInfoChanged.bind(this)
);
}
process(): ChildProcess | null {
return this._process;
}
process(): ChildProcess | null {
return this._process;
}
async createIncognitoBrowserContext(): Promise<BrowserContext> {
const {browserContextId} = await this._connection.send('Target.createBrowserContext');
const context = new BrowserContext(this._connection, this, browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
async createIncognitoBrowserContext(): Promise<BrowserContext> {
const { browserContextId } = await this._connection.send(
'Target.createBrowserContext'
);
const context = new BrowserContext(
this._connection,
this,
browserContextId
);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts(): BrowserContext[] {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
browserContexts(): BrowserContext[] {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
defaultBrowserContext(): BrowserContext {
return this._defaultContext;
}
defaultBrowserContext(): BrowserContext {
return this._defaultContext;
}
/**
/**
* @param {?string} contextId
*/
async _disposeContext(contextId?: string): Promise<void> {
await this._connection.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined});
this._contexts.delete(contextId);
}
async _disposeContext(contextId?: string): Promise<void> {
await this._connection.send('Target.disposeBrowserContext', {
browserContextId: contextId || undefined,
});
this._contexts.delete(contextId);
}
async _targetCreated(event: Protocol.Target.targetCreatedPayload): Promise<void> {
const targetInfo = event.targetInfo;
const {browserContextId} = targetInfo;
const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext;
async _targetCreated(
event: Protocol.Target.targetCreatedPayload
): Promise<void> {
const targetInfo = event.targetInfo;
const { browserContextId } = targetInfo;
const context =
browserContextId && this._contexts.has(browserContextId)
? this._contexts.get(browserContextId)
: this._defaultContext;
const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotTaskQueue);
assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
this._targets.set(event.targetInfo.targetId, target);
const target = new Target(
targetInfo,
context,
() => this._connection.createSession(targetInfo),
this._ignoreHTTPSErrors,
this._defaultViewport,
this._screenshotTaskQueue
);
assert(
!this._targets.has(event.targetInfo.targetId),
'Target should not exist before targetCreated'
);
this._targets.set(event.targetInfo.targetId, target);
if (await target._initializedPromise) {
this.emit(Events.Browser.TargetCreated, target);
context.emit(Events.BrowserContext.TargetCreated, target);
}
}
if (await target._initializedPromise) {
this.emit(Events.Browser.TargetCreated, target);
context.emit(Events.BrowserContext.TargetCreated, target);
}
}
/**
/**
* @param {{targetId: string}} event
*/
async _targetDestroyed(event: { targetId: string}): Promise<void> {
const target = this._targets.get(event.targetId);
target._initializedCallback(false);
this._targets.delete(event.targetId);
target._closedCallback();
if (await target._initializedPromise) {
this.emit(Events.Browser.TargetDestroyed, target);
target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target);
}
}
async _targetDestroyed(event: { targetId: string }): Promise<void> {
const target = this._targets.get(event.targetId);
target._initializedCallback(false);
this._targets.delete(event.targetId);
target._closedCallback();
if (await target._initializedPromise) {
this.emit(Events.Browser.TargetDestroyed, target);
target
.browserContext()
.emit(Events.BrowserContext.TargetDestroyed, target);
}
}
/**
/**
* @param {!Protocol.Target.targetInfoChangedPayload} event
*/
_targetInfoChanged(event: Protocol.Target.targetInfoChangedPayload): void {
const target = this._targets.get(event.targetInfo.targetId);
assert(target, 'target should exist before targetInfoChanged');
const previousURL = target.url();
const wasInitialized = target._isInitialized;
target._targetInfoChanged(event.targetInfo);
if (wasInitialized && previousURL !== target.url()) {
this.emit(Events.Browser.TargetChanged, target);
target.browserContext().emit(Events.BrowserContext.TargetChanged, target);
}
}
_targetInfoChanged(event: Protocol.Target.targetInfoChangedPayload): void {
const target = this._targets.get(event.targetInfo.targetId);
assert(target, 'target should exist before targetInfoChanged');
const previousURL = target.url();
const wasInitialized = target._isInitialized;
target._targetInfoChanged(event.targetInfo);
if (wasInitialized && previousURL !== target.url()) {
this.emit(Events.Browser.TargetChanged, target);
target.browserContext().emit(Events.BrowserContext.TargetChanged, target);
}
}
wsEndpoint(): string {
return this._connection.url();
}
wsEndpoint(): string {
return this._connection.url();
}
async newPage(): Promise<Page> {
return this._defaultContext.newPage();
}
async newPage(): Promise<Page> {
return this._defaultContext.newPage();
}
async _createPageInContext(contextId?: string): Promise<Page> {
const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank', browserContextId: contextId || undefined});
const target = await this._targets.get(targetId);
assert(await target._initializedPromise, 'Failed to create target for page');
const page = await target.page();
return page;
}
async _createPageInContext(contextId?: string): Promise<Page> {
const { targetId } = await this._connection.send('Target.createTarget', {
url: 'about:blank',
browserContextId: contextId || undefined,
});
const target = await this._targets.get(targetId);
assert(
await target._initializedPromise,
'Failed to create target for page'
);
const page = await target.page();
return page;
}
targets(): Target[] {
return Array.from(this._targets.values()).filter(target => target._isInitialized);
}
targets(): Target[] {
return Array.from(this._targets.values()).filter(
(target) => target._isInitialized
);
}
target(): Target {
return this.targets().find(target => target.type() === 'browser');
}
target(): Target {
return this.targets().find((target) => target.type() === 'browser');
}
/**
/**
* @param {function(!Target):boolean} predicate
* @param {{timeout?: number}=} options
* @return {!Promise<!Target>}
*/
async waitForTarget(predicate: (x: Target) => boolean, options: { timeout?: number} = {}): Promise<Target> {
const {
timeout = 30000
} = options;
const existingTarget = this.targets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve;
const targetPromise = new Promise<Target>(x => resolve = x);
this.on(Events.Browser.TargetCreated, check);
this.on(Events.Browser.TargetChanged, check);
try {
if (!timeout)
return await targetPromise;
return await helper.waitWithTimeout<Target>(targetPromise, 'target', timeout);
} finally {
this.removeListener(Events.Browser.TargetCreated, check);
this.removeListener(Events.Browser.TargetChanged, check);
}
async waitForTarget(
predicate: (x: Target) => boolean,
options: { timeout?: number } = {}
): Promise<Target> {
const { timeout = 30000 } = options;
const existingTarget = this.targets().find(predicate);
if (existingTarget) return existingTarget;
let resolve;
const targetPromise = new Promise<Target>((x) => (resolve = x));
this.on(Events.Browser.TargetCreated, check);
this.on(Events.Browser.TargetChanged, check);
try {
if (!timeout) return await targetPromise;
return await helper.waitWithTimeout<Target>(
targetPromise,
'target',
timeout
);
} finally {
this.removeListener(Events.Browser.TargetCreated, check);
this.removeListener(Events.Browser.TargetChanged, check);
}
function check(target: Target): void {
if (predicate(target))
resolve(target);
}
}
function check(target: Target): void {
if (predicate(target)) resolve(target);
}
}
async pages(): Promise<Page[]> {
const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);
}
async pages(): Promise<Page[]> {
const contextPages = await Promise.all(
this.browserContexts().map((context) => context.pages())
);
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);
}
async version(): Promise<string> {
const version = await this._getVersion();
return version.product;
}
async version(): Promise<string> {
const version = await this._getVersion();
return version.product;
}
async userAgent(): Promise<string> {
const version = await this._getVersion();
return version.userAgent;
}
async userAgent(): Promise<string> {
const version = await this._getVersion();
return version.userAgent;
}
async close(): Promise<void> {
await this._closeCallback.call(null);
this.disconnect();
}
async close(): Promise<void> {
await this._closeCallback.call(null);
this.disconnect();
}
disconnect(): void {
this._connection.dispose();
}
disconnect(): void {
this._connection.dispose();
}
isConnected(): boolean {
return !this._connection._closed;
}
isConnected(): boolean {
return !this._connection._closed;
}
_getVersion(): Promise<Protocol.Browser.getVersionReturnValue> {
return this._connection.send('Browser.getVersion');
}
_getVersion(): Promise<Protocol.Browser.getVersionReturnValue> {
return this._connection.send('Browser.getVersion');
}
}
export class BrowserContext extends EventEmitter {
@ -237,28 +306,42 @@ export class BrowserContext extends EventEmitter {
}
targets(): Target[] {
return this._browser.targets().filter(target => target.browserContext() === this);
return this._browser
.targets()
.filter((target) => target.browserContext() === this);
}
waitForTarget(predicate: (x: Target) => boolean, options: { timeout?: number}): Promise<Target> {
return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options);
waitForTarget(
predicate: (x: Target) => boolean,
options: { timeout?: number }
): Promise<Target> {
return this._browser.waitForTarget(
(target) => target.browserContext() === this && predicate(target),
options
);
}
async pages(): Promise<Page[]> {
const pages = await Promise.all(
this.targets()
.filter(target => target.type() === 'page')
.map(target => target.page())
this.targets()
.filter((target) => target.type() === 'page')
.map((target) => target.page())
);
return pages.filter(page => !!page);
return pages.filter((page) => !!page);
}
isIncognito(): boolean {
return !!this._id;
}
async overridePermissions(origin: string, permissions: Protocol.Browser.PermissionType[]): Promise<void> {
const webPermissionToProtocol = new Map<string, Protocol.Browser.PermissionType>([
async overridePermissions(
origin: string,
permissions: Protocol.Browser.PermissionType[]
): Promise<void> {
const webPermissionToProtocol = new Map<
string,
Protocol.Browser.PermissionType
>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
@ -278,17 +361,23 @@ export class BrowserContext extends EventEmitter {
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
permissions = permissions.map(permission => {
permissions = permissions.map((permission) => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._connection.send('Browser.grantPermissions', {origin, browserContextId: this._id || undefined, permissions});
await this._connection.send('Browser.grantPermissions', {
origin,
browserContextId: this._id || undefined,
permissions,
});
}
async clearPermissionOverrides(): Promise<void> {
await this._connection.send('Browser.resetPermissions', {browserContextId: this._id || undefined});
await this._connection.send('Browser.resetPermissions', {
browserContextId: this._id || undefined,
});
}
newPage(): Promise<Page> {

View File

@ -27,9 +27,9 @@ import * as debug from 'debug';
import * as removeRecursive from 'rimraf';
import * as URL from 'url';
import * as ProxyAgent from 'https-proxy-agent';
import {getProxyForUrl} from 'proxy-from-env';
import { getProxyForUrl } from 'proxy-from-env';
import {helper, assert} from './helper';
import { helper, assert } from './helper';
const debugFetcher = debug(`puppeteer:fetcher`);
const downloadURLs = {
@ -53,20 +53,23 @@ const browserConfig = {
destination: '.local-chromium',
},
firefox: {
host: 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central',
host:
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central',
destination: '.local-firefox',
}
},
} as const;
type Platform = 'linux' | 'mac' | 'win32' | 'win64';
type Product = 'chrome' | 'firefox';
function archiveName(product: Product, platform: Platform, revision: string): string {
function archiveName(
product: Product,
platform: Platform,
revision: string
): string {
if (product === 'chrome') {
if (platform === 'linux')
return 'chrome-linux';
if (platform === 'mac')
return 'chrome-mac';
if (platform === 'linux') return 'chrome-linux';
if (platform === 'mac') return 'chrome-mac';
if (platform === 'win32' || platform === 'win64') {
// Windows archive name changed at r591479.
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
@ -83,8 +86,18 @@ function archiveName(product: Product, platform: Platform, revision: string): st
* @param {string} revision
* @return {string}
*/
function downloadURL(product: Product, platform: Platform, host: string, revision: string): string {
const url = util.format(downloadURLs[product][platform], host, revision, archiveName(product, platform, revision));
function downloadURL(
product: Product,
platform: Platform,
host: string,
revision: string
): string {
const url = util.format(
downloadURLs[product][platform],
host,
revision,
archiveName(product, platform, revision)
);
return url;
}
@ -94,8 +107,8 @@ const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
const chmodAsync = helper.promisify(fs.chmod.bind(fs));
function existsAsync(filePath: string): Promise<boolean> {
return new Promise(resolve => {
fs.access(filePath, err => resolve(!err));
return new Promise((resolve) => {
fs.access(filePath, (err) => resolve(!err));
});
}
@ -125,12 +138,20 @@ export class BrowserFetcher {
constructor(projectRoot: string, options: BrowserFetcherOptions = {}) {
this._product = (options.product || 'chrome').toLowerCase() as Product;
assert(this._product === 'chrome' || this._product === 'firefox', `Unknown product: "${options.product}"`);
assert(
this._product === 'chrome' || this._product === 'firefox',
`Unknown product: "${options.product}"`
);
this._downloadsFolder = options.path || path.join(projectRoot, browserConfig[this._product].destination);
this._downloadsFolder =
options.path ||
path.join(projectRoot, browserConfig[this._product].destination);
this._downloadHost = options.host || browserConfig[this._product].host;
this.setPlatform(options.platform);
assert(downloadURLs[this._product][this._platform], 'Unsupported platform: ' + this._platform);
assert(
downloadURLs[this._product][this._platform],
'Unsupported platform: ' + this._platform
);
}
private setPlatform(platformFromOptions?: Platform): void {
@ -140,14 +161,11 @@ export class BrowserFetcher {
}
const platform = os.platform();
if (platform === 'darwin')
this._platform = 'mac';
else if (platform === 'linux')
this._platform = 'linux';
if (platform === 'darwin') this._platform = 'mac';
else if (platform === 'linux') this._platform = 'linux';
else if (platform === 'win32')
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
else
assert(this._platform, 'Unsupported platform: ' + os.platform());
else assert(this._platform, 'Unsupported platform: ' + os.platform());
}
platform(): string {
@ -163,12 +181,17 @@ export class BrowserFetcher {
}
canDownload(revision: string): Promise<boolean> {
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
return new Promise(resolve => {
const request = httpRequest(url, 'HEAD', response => {
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
revision
);
return new Promise((resolve) => {
const request = httpRequest(url, 'HEAD', (response) => {
resolve(response.statusCode === 200);
});
request.on('error', error => {
request.on('error', (error) => {
console.error(error);
resolve(false);
});
@ -180,39 +203,49 @@ export class BrowserFetcher {
* @param {?function(number, number):void} progressCallback
* @return {!Promise<!BrowserFetcher.RevisionInfo>}
*/
async download(revision: string, progressCallback: (x: number, y: number) => void): Promise<BrowserFetcherRevisionInfo> {
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
async download(
revision: string,
progressCallback: (x: number, y: number) => void
): Promise<BrowserFetcherRevisionInfo> {
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
revision
);
const fileName = url.split('/').pop();
const archivePath = path.join(this._downloadsFolder, fileName);
const outputPath = this._getFolderPath(revision);
if (await existsAsync(outputPath))
return this.revisionInfo(revision);
if (await existsAsync(outputPath)) return this.revisionInfo(revision);
if (!(await existsAsync(this._downloadsFolder)))
await mkdirAsync(this._downloadsFolder);
try {
await downloadFile(url, archivePath, progressCallback);
await install(archivePath, outputPath);
} finally {
if (await existsAsync(archivePath))
await unlinkAsync(archivePath);
if (await existsAsync(archivePath)) await unlinkAsync(archivePath);
}
const revisionInfo = this.revisionInfo(revision);
if (revisionInfo)
await chmodAsync(revisionInfo.executablePath, 0o755);
if (revisionInfo) await chmodAsync(revisionInfo.executablePath, 0o755);
return revisionInfo;
}
async localRevisions(): Promise<string[]> {
if (!await existsAsync(this._downloadsFolder))
return [];
if (!(await existsAsync(this._downloadsFolder))) return [];
const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames.map(fileName => parseFolderPath(this._product, fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
return fileNames
.map((fileName) => parseFolderPath(this._product, fileName))
.filter((entry) => entry && entry.platform === this._platform)
.map((entry) => entry.revision);
}
async remove(revision: string): Promise<void> {
const folderPath = this._getFolderPath(revision);
assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
await new Promise(fulfill => removeRecursive(folderPath, fulfill));
assert(
await existsAsync(folderPath),
`Failed to remove: revision ${revision} is not downloaded`
);
await new Promise((fulfill) => removeRecursive(folderPath, fulfill));
}
revisionInfo(revision: string): BrowserFetcherRevisionInfo {
@ -220,29 +253,67 @@ export class BrowserFetcher {
let executablePath = '';
if (this._product === 'chrome') {
if (this._platform === 'mac')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
'Chromium.app',
'Contents',
'MacOS',
'Chromium'
);
else if (this._platform === 'linux')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome');
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
'chrome'
);
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome.exe');
else
throw new Error('Unsupported platform: ' + this._platform);
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
'chrome.exe'
);
else throw new Error('Unsupported platform: ' + this._platform);
} else if (this._product === 'firefox') {
if (this._platform === 'mac')
executablePath = path.join(folderPath, 'Firefox Nightly.app', 'Contents', 'MacOS', 'firefox');
executablePath = path.join(
folderPath,
'Firefox Nightly.app',
'Contents',
'MacOS',
'firefox'
);
else if (this._platform === 'linux')
executablePath = path.join(folderPath, 'firefox', 'firefox');
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
else
throw new Error('Unsupported platform: ' + this._platform);
else throw new Error('Unsupported platform: ' + this._platform);
} else {
throw new Error('Unsupported product: ' + this._product);
}
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
revision
);
const local = fs.existsSync(folderPath);
debugFetcher({revision, executablePath, folderPath, local, url, product: this._product});
return {revision, executablePath, folderPath, local, url, product: this._product};
debugFetcher({
revision,
executablePath,
folderPath,
local,
url,
product: this._product,
});
return {
revision,
executablePath,
folderPath,
local,
url,
product: this._product,
};
}
/**
@ -254,15 +325,16 @@ export class BrowserFetcher {
}
}
function parseFolderPath(product: Product, folderPath: string): {product: string; platform: string; revision: string} | null {
function parseFolderPath(
product: Product,
folderPath: string
): { product: string; platform: string; revision: string } | null {
const name = path.basename(folderPath);
const splits = name.split('-');
if (splits.length !== 2)
return null;
if (splits.length !== 2) return null;
const [platform, revision] = splits;
if (!downloadURLs[product][platform])
return null;
return {product, platform, revision};
if (!downloadURLs[product][platform]) return null;
return { product, platform, revision };
}
/**
@ -271,17 +343,26 @@ function parseFolderPath(product: Product, folderPath: string): {product: string
* @param {?function(number, number):void} progressCallback
* @return {!Promise}
*/
function downloadFile(url: string, destinationPath: string, progressCallback: (x: number, y: number) => void): Promise<void> {
function downloadFile(
url: string,
destinationPath: string,
progressCallback: (x: number, y: number) => void
): Promise<void> {
debugFetcher(`Downloading binary from ${url}`);
let fulfill, reject;
let downloadedBytes = 0;
let totalBytes = 0;
const promise = new Promise<void>((x, y) => { fulfill = x; reject = y; });
const promise = new Promise<void>((x, y) => {
fulfill = x;
reject = y;
});
const request = httpRequest(url, 'GET', response => {
const request = httpRequest(url, 'GET', (response) => {
if (response.statusCode !== 200) {
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
const error = new Error(
`Download failed: server returned code ${response.statusCode}. URL: ${url}`
);
// consume response data to free up memory
response.resume();
reject(error);
@ -289,13 +370,15 @@ function downloadFile(url: string, destinationPath: string, progressCallback: (x
}
const file = fs.createWriteStream(destinationPath);
file.on('finish', () => fulfill());
file.on('error', error => reject(error));
file.on('error', (error) => reject(error));
response.pipe(file);
totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
if (progressCallback)
response.on('data', onData);
totalBytes = parseInt(
/** @type {string} */ response.headers['content-length'],
10
);
if (progressCallback) response.on('data', onData);
});
request.on('error', error => reject(error));
request.on('error', (error) => reject(error));
return promise;
function onData(chunk: string): void {
@ -307,13 +390,14 @@ function downloadFile(url: string, destinationPath: string, progressCallback: (x
function install(archivePath: string, folderPath: string): Promise<unknown> {
debugFetcher(`Installing ${archivePath} to ${folderPath}`);
if (archivePath.endsWith('.zip'))
return extractZip(archivePath, {dir: folderPath});
return extractZip(archivePath, { dir: folderPath });
else if (archivePath.endsWith('.tar.bz2'))
return extractTar(archivePath, folderPath);
else if (archivePath.endsWith('.dmg'))
return mkdirAsync(folderPath).then(() => installDMG(archivePath, folderPath));
else
throw new Error(`Unsupported archive format: ${archivePath}`);
return mkdirAsync(folderPath).then(() =>
installDMG(archivePath, folderPath)
);
else throw new Error(`Unsupported archive format: ${archivePath}`);
}
/**
@ -331,7 +415,9 @@ function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
tarStream.on('error', reject);
tarStream.on('finish', fulfill);
const readStream = fs.createReadStream(tarPath);
readStream.on('data', () => { process.stdout.write('\rExtracting...'); });
readStream.on('data', () => {
process.stdout.write('\rExtracting...');
});
readStream.pipe(bzip()).pipe(tarStream);
});
}
@ -349,88 +435,94 @@ function installDMG(dmgPath: string, folderPath: string): Promise<void> {
function mountAndCopy(fulfill: () => void, reject: (Error) => void): void {
const mountCommand = `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`;
childProcess.exec(mountCommand, (err, stdout) => {
if (err)
return reject(err);
if (err) return reject(err);
const volumes = stdout.match(/\/Volumes\/(.*)/m);
if (!volumes)
return reject(new Error(`Could not find volume path in ${stdout}`));
mountPath = volumes[0];
readdirAsync(mountPath).then(fileNames => {
const appName = fileNames.filter(item => typeof item === 'string' && item.endsWith('.app'))[0];
if (!appName)
return reject(new Error(`Cannot find app in ${mountPath}`));
const copyPath = path.join(mountPath, appName);
debugFetcher(`Copying ${copyPath} to ${folderPath}`);
childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, err => {
if (err)
reject(err);
else
fulfill();
});
}).catch(reject);
readdirAsync(mountPath)
.then((fileNames) => {
const appName = fileNames.filter(
(item) => typeof item === 'string' && item.endsWith('.app')
)[0];
if (!appName)
return reject(new Error(`Cannot find app in ${mountPath}`));
const copyPath = path.join(mountPath, appName);
debugFetcher(`Copying ${copyPath} to ${folderPath}`);
childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, (err) => {
if (err) reject(err);
else fulfill();
});
})
.catch(reject);
});
}
function unmount(): void {
if (!mountPath)
return;
if (!mountPath) return;
const unmountCommand = `hdiutil detach "${mountPath}" -quiet`;
debugFetcher(`Unmounting ${mountPath}`);
childProcess.exec(unmountCommand, err => {
if (err)
console.error(`Error unmounting dmg: ${err}`);
childProcess.exec(unmountCommand, (err) => {
if (err) console.error(`Error unmounting dmg: ${err}`);
});
}
return new Promise<void>(mountAndCopy).catch(error => { console.error(error); }).finally(unmount);
return new Promise<void>(mountAndCopy)
.catch((error) => {
console.error(error);
})
.finally(unmount);
}
function httpRequest(url: string, method: string, response: (x: http.IncomingMessage) => void): http.ClientRequest {
function httpRequest(
url: string,
method: string,
response: (x: http.IncomingMessage) => void
): http.ClientRequest {
const urlParsed = URL.parse(url);
type Options = Partial<URL.UrlWithStringQuery> & {
method?: string;
agent?: ProxyAgent;
rejectUnauthorized?: boolean;
};
type Options = Partial<URL.UrlWithStringQuery> & {
method?: string;
agent?: ProxyAgent;
rejectUnauthorized?: boolean;
};
let options: Options = {
...urlParsed,
method,
};
let options: Options = {
...urlParsed,
method,
};
const proxyURL = getProxyForUrl(url);
if (proxyURL) {
if (url.startsWith('http:')) {
const proxy = URL.parse(proxyURL);
options = {
path: options.href,
host: proxy.hostname,
port: proxy.port,
};
} else {
const parsedProxyURL = URL.parse(proxyURL);
const proxyURL = getProxyForUrl(url);
if (proxyURL) {
if (url.startsWith('http:')) {
const proxy = URL.parse(proxyURL);
options = {
path: options.href,
host: proxy.hostname,
port: proxy.port,
};
} else {
const parsedProxyURL = URL.parse(proxyURL);
const proxyOptions = {
...parsedProxyURL,
secureProxy: parsedProxyURL.protocol === 'https:',
} as ProxyAgent.HttpsProxyAgentOptions;
const proxyOptions = {
...parsedProxyURL,
secureProxy: parsedProxyURL.protocol === 'https:',
} as ProxyAgent.HttpsProxyAgentOptions;
options.agent = new ProxyAgent(proxyOptions);
options.rejectUnauthorized = false;
}
}
options.agent = new ProxyAgent(proxyOptions);
options.rejectUnauthorized = false;
}
}
const requestCallback = (res: http.IncomingMessage): void => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
httpRequest(res.headers.location, method, response);
else
response(res);
};
const request = options.protocol === 'https:' ?
https.request(options, requestCallback) :
http.request(options, requestCallback);
request.end();
return request;
const requestCallback = (res: http.IncomingMessage): void => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
httpRequest(res.headers.location, method, response);
else response(res);
};
const request =
options.protocol === 'https:'
? https.request(options, requestCallback)
: http.request(options, requestCallback);
request.end();
return request;
}

View File

@ -13,20 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {assert} from './helper';
import {Events} from './Events';
import { assert } from './helper';
import { Events } from './Events';
import * as debug from 'debug';
const debugProtocol = debug('puppeteer:protocol');
import type {ConnectionTransport} from './ConnectionTransport';
import type { ConnectionTransport } from './ConnectionTransport';
import * as EventEmitter from 'events';
interface ConnectionCallback {
resolve: Function;
reject: Function;
error: Error;
method: string;
resolve: Function;
reject: Function;
error: Error;
method: string;
}
export class Connection extends EventEmitter {
@ -65,29 +64,35 @@ export class Connection extends EventEmitter {
return this._url;
}
send<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T]> {
const id = this._rawSend({method, params});
send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
const id = this._rawSend({ method, params });
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
this._callbacks.set(id, { resolve, reject, error: new Error(), method });
});
}
_rawSend(message: {}): number {
const id = ++this._lastId;
message = JSON.stringify(Object.assign({}, message, {id}));
message = JSON.stringify(Object.assign({}, message, { id }));
debugProtocol('SEND ► ' + message);
this._transport.send(message);
return id;
}
async _onMessage(message: string): Promise<void> {
if (this._delay)
await new Promise(f => setTimeout(f, this._delay));
if (this._delay) await new Promise((f) => setTimeout(f, this._delay));
debugProtocol('◀ RECV ' + message);
const object = JSON.parse(message);
if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId;
const session = new CDPSession(this, object.params.targetInfo.type, sessionId);
const session = new CDPSession(
this,
object.params.targetInfo.type,
sessionId
);
this._sessions.set(sessionId, session);
} else if (object.method === 'Target.detachedFromTarget') {
const session = this._sessions.get(object.params.sessionId);
@ -98,17 +103,17 @@ export class Connection extends EventEmitter {
}
if (object.sessionId) {
const session = this._sessions.get(object.sessionId);
if (session)
session._onMessage(object);
if (session) session._onMessage(object);
} else if (object.id) {
const callback = this._callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`.
if (callback) {
this._callbacks.delete(object.id);
if (object.error)
callback.reject(createProtocolError(callback.error, callback.method, object));
else
callback.resolve(object.result);
callback.reject(
createProtocolError(callback.error, callback.method, object)
);
else callback.resolve(object.result);
}
} else {
this.emit(object.method, object.params);
@ -116,16 +121,19 @@ export class Connection extends EventEmitter {
}
_onClose(): void {
if (this._closed)
return;
if (this._closed) return;
this._closed = true;
this._transport.onmessage = null;
this._transport.onclose = null;
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
callback.reject(
rewriteError(
callback.error,
`Protocol error (${callback.method}): Target closed.`
)
);
this._callbacks.clear();
for (const session of this._sessions.values())
session._onClosed();
for (const session of this._sessions.values()) session._onClosed();
this._sessions.clear();
this.emit(Events.Connection.Disconnected);
}
@ -139,19 +147,23 @@ export class Connection extends EventEmitter {
* @param {Protocol.Target.TargetInfo} targetInfo
* @return {!Promise<!CDPSession>}
*/
async createSession(targetInfo: Protocol.Target.TargetInfo): Promise<CDPSession> {
const {sessionId} = await this.send('Target.attachToTarget', {targetId: targetInfo.targetId, flatten: true});
async createSession(
targetInfo: Protocol.Target.TargetInfo
): Promise<CDPSession> {
const { sessionId } = await this.send('Target.attachToTarget', {
targetId: targetInfo.targetId,
flatten: true,
});
return this._sessions.get(sessionId);
}
}
interface CDPSessionOnMessageObject {
id?: number;
method: string;
params: {};
error: {message: string; data: any};
result?: any;
id?: number;
method: string;
params: {};
error: { message: string; data: any };
result?: any;
}
export class CDPSession extends EventEmitter {
_connection: Connection;
@ -166,9 +178,16 @@ export class CDPSession extends EventEmitter {
this._sessionId = sessionId;
}
send<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T]> {
send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (!this._connection)
return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
return Promise.reject(
new Error(
`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`
)
);
const id = this._connection._rawSend({
sessionId: this._sessionId,
@ -177,11 +196,11 @@ export class CDPSession extends EventEmitter {
* we no longer need the `|| {}` check
* https://bugzilla.mozilla.org/show_bug.cgi?id=1631570
*/
params: params || {}
params: params || {},
});
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
this._callbacks.set(id, { resolve, reject, error: new Error(), method });
});
}
@ -190,9 +209,10 @@ export class CDPSession extends EventEmitter {
const callback = this._callbacks.get(object.id);
this._callbacks.delete(object.id);
if (object.error)
callback.reject(createProtocolError(callback.error, callback.method, object));
else
callback.resolve(object.result);
callback.reject(
createProtocolError(callback.error, callback.method, object)
);
else callback.resolve(object.result);
} else {
assert(!object.id);
this.emit(object.method, object.params);
@ -201,13 +221,22 @@ export class CDPSession extends EventEmitter {
async detach(): Promise<void> {
if (!this._connection)
throw new Error(`Session already detached. Most likely the ${this._targetType} has been closed.`);
await this._connection.send('Target.detachFromTarget', {sessionId: this._sessionId});
throw new Error(
`Session already detached. Most likely the ${this._targetType} has been closed.`
);
await this._connection.send('Target.detachFromTarget', {
sessionId: this._sessionId,
});
}
_onClosed(): void {
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
callback.reject(
rewriteError(
callback.error,
`Protocol error (${callback.method}): Target closed.`
)
);
this._callbacks.clear();
this._connection = null;
this.emit(Events.CDPSession.Disconnected);
@ -220,10 +249,13 @@ export class CDPSession extends EventEmitter {
* @param {{error: {message: string, data: any}}} object
* @return {!Error}
*/
function createProtocolError(error: Error, method: string, object: { error: { message: string; data: any}}): Error {
function createProtocolError(
error: Error,
method: string,
object: { error: { message: string; data: any } }
): Error {
let message = `Protocol error (${method}): ${object.error.message}`;
if ('data' in object.error)
message += ` ${object.error.data}`;
if ('data' in object.error) message += ` ${object.error.data}`;
return rewriteError(error, message);
}

View File

@ -14,15 +14,15 @@
* limitations under the License.
*/
import {helper, debugError, assert, PuppeteerEventListener} from './helper';
import {CDPSession} from './Connection';
import { helper, debugError, assert, PuppeteerEventListener } from './helper';
import { CDPSession } from './Connection';
import {EVALUATION_SCRIPT_URL} from './ExecutionContext';
import { EVALUATION_SCRIPT_URL } from './ExecutionContext';
interface CoverageEntry {
url: string;
text: string;
ranges: Array<{start: number; end: number}>;
ranges: Array<{ start: number; end: number }>;
}
export class Coverage {
@ -34,7 +34,10 @@ export class Coverage {
this._cssCoverage = new CSSCoverage(client);
}
async startJSCoverage(options: {resetOnNavigation?: boolean; reportAnonymousScripts?: boolean}): Promise<void> {
async startJSCoverage(options: {
resetOnNavigation?: boolean;
reportAnonymousScripts?: boolean;
}): Promise<void> {
return await this._jsCoverage.start(options);
}
@ -42,7 +45,9 @@ export class Coverage {
return await this._jsCoverage.stop();
}
async startCSSCoverage(options: {resetOnNavigation?: boolean}): Promise<void> {
async startCSSCoverage(options: {
resetOnNavigation?: boolean;
}): Promise<void> {
return await this._cssCoverage.start(options);
}
@ -64,11 +69,16 @@ class JSCoverage {
this._client = client;
}
async start(options: {resetOnNavigation?: boolean; reportAnonymousScripts?: boolean} = {}): Promise<void> {
async start(
options: {
resetOnNavigation?: boolean;
reportAnonymousScripts?: boolean;
} = {}
): Promise<void> {
assert(!this._enabled, 'JSCoverage is already enabled');
const {
resetOnNavigation = true,
reportAnonymousScripts = false
reportAnonymousScripts = false,
} = options;
this._resetOnNavigation = resetOnNavigation;
this._reportAnonymousScripts = reportAnonymousScripts;
@ -76,33 +86,45 @@ class JSCoverage {
this._scriptURLs.clear();
this._scriptSources.clear();
this._eventListeners = [
helper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
helper.addEventListener(
this._client,
'Debugger.scriptParsed',
this._onScriptParsed.bind(this)
),
helper.addEventListener(
this._client,
'Runtime.executionContextsCleared',
this._onExecutionContextsCleared.bind(this)
),
];
await Promise.all([
this._client.send('Profiler.enable'),
this._client.send('Profiler.startPreciseCoverage', {callCount: false, detailed: true}),
this._client.send('Profiler.startPreciseCoverage', {
callCount: false,
detailed: true,
}),
this._client.send('Debugger.enable'),
this._client.send('Debugger.setSkipAllPauses', {skip: true})
this._client.send('Debugger.setSkipAllPauses', { skip: true }),
]);
}
_onExecutionContextsCleared(): void {
if (!this._resetOnNavigation)
return;
if (!this._resetOnNavigation) return;
this._scriptURLs.clear();
this._scriptSources.clear();
}
async _onScriptParsed(event: Protocol.Debugger.scriptParsedPayload): Promise<void> {
async _onScriptParsed(
event: Protocol.Debugger.scriptParsedPayload
): Promise<void> {
// Ignore puppeteer-injected scripts
if (event.url === EVALUATION_SCRIPT_URL)
return;
if (event.url === EVALUATION_SCRIPT_URL) return;
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
if (!event.url && !this._reportAnonymousScripts)
return;
if (!event.url && !this._reportAnonymousScripts) return;
try {
const response = await this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId});
const response = await this._client.send('Debugger.getScriptSource', {
scriptId: event.scriptId,
});
this._scriptURLs.set(event.scriptId, event.url);
this._scriptSources.set(event.scriptId, response.scriptSource);
} catch (error) {
@ -137,13 +159,11 @@ class JSCoverage {
if (!url && this._reportAnonymousScripts)
url = 'debugger://VM' + entry.scriptId;
const text = this._scriptSources.get(entry.scriptId);
if (text === undefined || url === undefined)
continue;
if (text === undefined || url === undefined) continue;
const flattenRanges = [];
for (const func of entry.functions)
flattenRanges.push(...func.ranges);
for (const func of entry.functions) flattenRanges.push(...func.ranges);
const ranges = convertToDisjointRanges(flattenRanges);
coverage.push({url, ranges, text});
coverage.push({ url, ranges, text });
}
return coverage;
}
@ -162,16 +182,24 @@ class CSSCoverage {
this._client = client;
}
async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> {
async start(options: { resetOnNavigation?: boolean } = {}): Promise<void> {
assert(!this._enabled, 'CSSCoverage is already enabled');
const {resetOnNavigation = true} = options;
const { resetOnNavigation = true } = options;
this._resetOnNavigation = resetOnNavigation;
this._enabled = true;
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
this._eventListeners = [
helper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
helper.addEventListener(
this._client,
'CSS.styleSheetAdded',
this._onStyleSheet.bind(this)
),
helper.addEventListener(
this._client,
'Runtime.executionContextsCleared',
this._onExecutionContextsCleared.bind(this)
),
];
await Promise.all([
this._client.send('DOM.enable'),
@ -181,19 +209,21 @@ class CSSCoverage {
}
_onExecutionContextsCleared(): void {
if (!this._resetOnNavigation)
return;
if (!this._resetOnNavigation) return;
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
}
async _onStyleSheet(event: Protocol.CSS.styleSheetAddedPayload): Promise<void> {
async _onStyleSheet(
event: Protocol.CSS.styleSheetAddedPayload
): Promise<void> {
const header = event.header;
// Ignore anonymous scripts
if (!header.sourceURL)
return;
if (!header.sourceURL) return;
try {
const response = await this._client.send('CSS.getStyleSheetText', {styleSheetId: header.styleSheetId});
const response = await this._client.send('CSS.getStyleSheetText', {
styleSheetId: header.styleSheetId,
});
this._stylesheetURLs.set(header.styleSheetId, header.sourceURL);
this._stylesheetSources.set(header.styleSheetId, response.text);
} catch (error) {
@ -205,7 +235,9 @@ class CSSCoverage {
async stop(): Promise<CoverageEntry[]> {
assert(this._enabled, 'CSSCoverage is not enabled');
this._enabled = false;
const ruleTrackingResponse = await this._client.send('CSS.stopRuleUsageTracking');
const ruleTrackingResponse = await this._client.send(
'CSS.stopRuleUsageTracking'
);
await Promise.all([
this._client.send('CSS.disable'),
this._client.send('DOM.disable'),
@ -231,33 +263,34 @@ class CSSCoverage {
for (const styleSheetId of this._stylesheetURLs.keys()) {
const url = this._stylesheetURLs.get(styleSheetId);
const text = this._stylesheetSources.get(styleSheetId);
const ranges = convertToDisjointRanges(styleSheetIdToCoverage.get(styleSheetId) || []);
coverage.push({url, ranges, text});
const ranges = convertToDisjointRanges(
styleSheetIdToCoverage.get(styleSheetId) || []
);
coverage.push({ url, ranges, text });
}
return coverage;
}
}
function convertToDisjointRanges(nestedRanges: Array<{startOffset: number; endOffset: number; count: number}>): Array<{start: number; end: number}> {
function convertToDisjointRanges(
nestedRanges: Array<{ startOffset: number; endOffset: number; count: number }>
): Array<{ start: number; end: number }> {
const points = [];
for (const range of nestedRanges) {
points.push({offset: range.startOffset, type: 0, range});
points.push({offset: range.endOffset, type: 1, range});
points.push({ offset: range.startOffset, type: 0, range });
points.push({ offset: range.endOffset, type: 1, range });
}
// Sort points to form a valid parenthesis sequence.
points.sort((a, b) => {
// Sort with increasing offsets.
if (a.offset !== b.offset)
return a.offset - b.offset;
if (a.offset !== b.offset) return a.offset - b.offset;
// All "end" points should go before "start" points.
if (a.type !== b.type)
return b.type - a.type;
if (a.type !== b.type) return b.type - a.type;
const aLength = a.range.endOffset - a.range.startOffset;
const bLength = b.range.endOffset - b.range.startOffset;
// For two "start" points, the one with longer range goes first.
if (a.type === 0)
return bLength - aLength;
if (a.type === 0) return bLength - aLength;
// For two "end" points, the one with shorter range goes first.
return aLength - bLength;
});
@ -267,20 +300,20 @@ function convertToDisjointRanges(nestedRanges: Array<{startOffset: number; endOf
let lastOffset = 0;
// Run scanning line to intersect all ranges.
for (const point of points) {
if (hitCountStack.length && lastOffset < point.offset && hitCountStack[hitCountStack.length - 1] > 0) {
if (
hitCountStack.length &&
lastOffset < point.offset &&
hitCountStack[hitCountStack.length - 1] > 0
) {
const lastResult = results.length ? results[results.length - 1] : null;
if (lastResult && lastResult.end === lastOffset)
lastResult.end = point.offset;
else
results.push({start: lastOffset, end: point.offset});
else results.push({ start: lastOffset, end: point.offset });
}
lastOffset = point.offset;
if (point.type === 0)
hitCountStack.push(point.range.count);
else
hitCountStack.pop();
if (point.type === 0) hitCountStack.push(point.range.count);
else hitCountStack.pop();
}
// Filter out empty ranges.
return results.filter(range => range.end - range.start > 1);
return results.filter((range) => range.end - range.start > 1);
}

View File

@ -15,15 +15,15 @@
*/
import * as fs from 'fs';
import {helper, assert} from './helper';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher';
import {TimeoutError} from './Errors';
import {JSHandle, ElementHandle} from './JSHandle';
import {ExecutionContext} from './ExecutionContext';
import {TimeoutSettings} from './TimeoutSettings';
import {MouseButtonInput} from './Input';
import {FrameManager, Frame} from './FrameManager';
import {getQueryHandlerAndSelector, QueryHandler} from './QueryHandler';
import { helper, assert } from './helper';
import { LifecycleWatcher, PuppeteerLifeCycleEvent } from './LifecycleWatcher';
import { TimeoutError } from './Errors';
import { JSHandle, ElementHandle } from './JSHandle';
import { ExecutionContext } from './ExecutionContext';
import { TimeoutSettings } from './TimeoutSettings';
import { MouseButtonInput } from './Input';
import { FrameManager, Frame } from './FrameManager';
import { getQueryHandlerAndSelector, QueryHandler } from './QueryHandler';
// This predicateQueryHandler is declared here so that TypeScript knows about it
// when it is used in the predicate function below.
@ -49,7 +49,11 @@ export class DOMWorld {
_detached = false;
_waitTasks = new Set<WaitTask>();
constructor(frameManager: FrameManager, frame: Frame, timeoutSettings: TimeoutSettings) {
constructor(
frameManager: FrameManager,
frame: Frame,
timeoutSettings: TimeoutSettings
) {
this._frameManager = frameManager;
this._frame = frame;
this._timeoutSettings = timeoutSettings;
@ -67,11 +71,10 @@ export class DOMWorld {
if (context) {
this._contextResolveCallback.call(null, context);
this._contextResolveCallback = null;
for (const waitTask of this._waitTasks)
waitTask.rerun();
for (const waitTask of this._waitTasks) waitTask.rerun();
} else {
this._documentPromise = null;
this._contextPromise = new Promise(fulfill => {
this._contextPromise = new Promise((fulfill) => {
this._contextResolveCallback = fulfill;
});
}
@ -84,7 +87,9 @@ export class DOMWorld {
_detach(): void {
this._detached = true;
for (const waitTask of this._waitTasks)
waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
waitTask.terminate(
new Error('waitForFunction failed: frame got detached.')
);
}
/**
@ -92,7 +97,9 @@ export class DOMWorld {
*/
executionContext(): Promise<ExecutionContext> {
if (this._detached)
throw new Error(`Execution Context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`);
throw new Error(
`Execution Context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`
);
return this._contextPromise;
}
@ -101,7 +108,10 @@ export class DOMWorld {
* @param {!Array<*>} args
* @return {!Promise<!JSHandle>}
*/
async evaluateHandle(pageFunction: Function | string, ...args: unknown[]): Promise<JSHandle> {
async evaluateHandle(
pageFunction: Function | string,
...args: unknown[]
): Promise<JSHandle> {
const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args);
}
@ -111,7 +121,10 @@ export class DOMWorld {
* @param {!Array<*>} args
* @return {!Promise<*>}
*/
async evaluate<ReturnType extends any>(pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const context = await this.executionContext();
return context.evaluate<ReturnType>(pageFunction, ...args);
}
@ -127,9 +140,8 @@ export class DOMWorld {
}
async _document(): Promise<ElementHandle> {
if (this._documentPromise)
return this._documentPromise;
this._documentPromise = this.executionContext().then(async context => {
if (this._documentPromise) return this._documentPromise;
this._documentPromise = this.executionContext().then(async (context) => {
const document = await context.evaluateHandle('document');
return document.asElement();
});
@ -142,14 +154,26 @@ export class DOMWorld {
return value;
}
async $eval<ReturnType extends any>(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const document = await this._document();
return document.$eval<ReturnType>(selector, pageFunction, ...args);
}
async $$eval<ReturnType extends any>(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const document = await this._document();
const value = await document.$$eval<ReturnType>(selector, pageFunction, ...args);
const value = await document.$$eval<ReturnType>(
selector,
pageFunction,
...args
);
return value;
}
@ -174,43 +198,55 @@ export class DOMWorld {
});
}
async setContent(html: string, options: {timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]} = {}): Promise<void> {
async setContent(
html: string,
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {}
): Promise<void> {
const {
waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(),
} = options;
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
await this.evaluate(html => {
await this.evaluate((html) => {
document.open();
document.write(html);
document.close();
}, html);
const watcher = new LifecycleWatcher(this._frameManager, this._frame, waitUntil, timeout);
const watcher = new LifecycleWatcher(
this._frameManager,
this._frame,
waitUntil,
timeout
);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
watcher.lifecyclePromise(),
]);
watcher.dispose();
if (error)
throw error;
if (error) throw error;
}
/**
* @param {!{url?: string, path?: string, content?: string, type?: string}} options
* @return {!Promise<!ElementHandle>}
*/
async addScriptTag(options: {url?: string; path?: string; content?: string; type?: string}): Promise<ElementHandle> {
const {
url = null,
path = null,
content = null,
type = ''
} = options;
async addScriptTag(options: {
url?: string;
path?: string;
content?: string;
type?: string;
}): Promise<ElementHandle> {
const { url = null, path = null, content = null, type = '' } = options;
if (url !== null) {
try {
const context = await this.executionContext();
return (await context.evaluateHandle(addScriptUrl, url, type)).asElement();
return (
await context.evaluateHandle(addScriptUrl, url, type)
).asElement();
} catch (error) {
throw new Error(`Loading script from ${url} failed`);
}
@ -220,21 +256,29 @@ export class DOMWorld {
let contents = await readFileAsync(path, 'utf8');
contents += '//# sourceURL=' + path.replace(/\n/g, '');
const context = await this.executionContext();
return (await context.evaluateHandle(addScriptContent, contents, type)).asElement();
return (
await context.evaluateHandle(addScriptContent, contents, type)
).asElement();
}
if (content !== null) {
const context = await this.executionContext();
return (await context.evaluateHandle(addScriptContent, content, type)).asElement();
return (
await context.evaluateHandle(addScriptContent, content, type)
).asElement();
}
throw new Error('Provide an object with a `url`, `path` or `content` property');
throw new Error(
'Provide an object with a `url`, `path` or `content` property'
);
async function addScriptUrl(url: string, type: string): Promise<HTMLElement> {
async function addScriptUrl(
url: string,
type: string
): Promise<HTMLElement> {
const script = document.createElement('script');
script.src = url;
if (type)
script.type = type;
if (type) script.type = type;
const promise = new Promise((res, rej) => {
script.onload = res;
script.onerror = rej;
@ -244,25 +288,27 @@ export class DOMWorld {
return script;
}
function addScriptContent(content: string, type = 'text/javascript'): HTMLElement {
function addScriptContent(
content: string,
type = 'text/javascript'
): HTMLElement {
const script = document.createElement('script');
script.type = type;
script.text = content;
let error = null;
script.onerror = e => error = e;
script.onerror = (e) => (error = e);
document.head.appendChild(script);
if (error)
throw error;
if (error) throw error;
return script;
}
}
async addStyleTag(options: {url?: string; path?: string; content?: string}): Promise<ElementHandle> {
const {
url = null,
path = null,
content = null
} = options;
async addStyleTag(options: {
url?: string;
path?: string;
content?: string;
}): Promise<ElementHandle> {
const { url = null, path = null, content = null } = options;
if (url !== null) {
try {
const context = await this.executionContext();
@ -276,15 +322,21 @@ export class DOMWorld {
let contents = await readFileAsync(path, 'utf8');
contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
const context = await this.executionContext();
return (await context.evaluateHandle(addStyleContent, contents)).asElement();
return (
await context.evaluateHandle(addStyleContent, contents)
).asElement();
}
if (content !== null) {
const context = await this.executionContext();
return (await context.evaluateHandle(addStyleContent, content)).asElement();
return (
await context.evaluateHandle(addStyleContent, content)
).asElement();
}
throw new Error('Provide an object with a `url`, `path` or `content` property');
throw new Error(
'Provide an object with a `url`, `path` or `content` property'
);
async function addStyleUrl(url: string): Promise<HTMLElement> {
const link = document.createElement('link');
@ -313,7 +365,10 @@ export class DOMWorld {
}
}
async click(selector: string, options: {delay?: number; button?: MouseButtonInput; clickCount?: number}): Promise<void> {
async click(
selector: string,
options: { delay?: number; button?: MouseButtonInput; clickCount?: number }
): Promise<void> {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.click(options);
@ -349,43 +404,87 @@ export class DOMWorld {
await handle.dispose();
}
async type(selector: string, text: string, options?: {delay: number}): Promise<void> {
async type(
selector: string,
text: string,
options?: { delay: number }
): Promise<void> {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.type(text, options);
await handle.dispose();
}
waitForSelector(selector: string, options: WaitForSelectorOptions): Promise<ElementHandle | null> {
waitForSelector(
selector: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
return this._waitForSelectorOrXPath(selector, false, options);
}
waitForXPath(xpath: string, options: WaitForSelectorOptions): Promise<ElementHandle | null> {
waitForXPath(
xpath: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
return this._waitForSelectorOrXPath(xpath, true, options);
}
waitForFunction(pageFunction: Function | string, options: {polling?: string | number; timeout?: number} = {}, ...args: unknown[]): Promise<JSHandle> {
waitForFunction(
pageFunction: Function | string,
options: { polling?: string | number; timeout?: number } = {},
...args: unknown[]
): Promise<JSHandle> {
const {
polling = 'raf',
timeout = this._timeoutSettings.timeout(),
} = options;
return new WaitTask(this, pageFunction, undefined, 'function', polling, timeout, ...args).promise;
return new WaitTask(
this,
pageFunction,
undefined,
'function',
polling,
timeout,
...args
).promise;
}
async title(): Promise<string> {
return this.evaluate(() => document.title);
}
private async _waitForSelectorOrXPath(selectorOrXPath: string, isXPath: boolean, options: WaitForSelectorOptions = {}): Promise<ElementHandle | null> {
private async _waitForSelectorOrXPath(
selectorOrXPath: string,
isXPath: boolean,
options: WaitForSelectorOptions = {}
): Promise<ElementHandle | null> {
const {
visible: waitForVisible = false,
hidden: waitForHidden = false,
timeout = this._timeoutSettings.timeout(),
} = options;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
const {updatedSelector, queryHandler} = getQueryHandlerAndSelector(selectorOrXPath, (element, selector) => document.querySelector(selector));
const waitTask = new WaitTask(this, predicate, queryHandler, title, polling, timeout, updatedSelector, isXPath, waitForVisible, waitForHidden);
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${
waitForHidden ? ' to be hidden' : ''
}`;
const {
updatedSelector,
queryHandler,
} = getQueryHandlerAndSelector(selectorOrXPath, (element, selector) =>
document.querySelector(selector)
);
const waitTask = new WaitTask(
this,
predicate,
queryHandler,
title,
polling,
timeout,
updatedSelector,
isXPath,
waitForVisible,
waitForHidden
);
const handle = await waitTask.promise;
if (!handle.asElement()) {
await handle.dispose();
@ -400,19 +499,35 @@ export class DOMWorld {
* @param {boolean} waitForHidden
* @return {?Node|boolean}
*/
function predicate(selectorOrXPath: string, isXPath: boolean, waitForVisible: boolean, waitForHidden: boolean): Node | null | boolean {
function predicate(
selectorOrXPath: string,
isXPath: boolean,
waitForVisible: boolean,
waitForHidden: boolean
): Node | null | boolean {
const node = isXPath
? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
: predicateQueryHandler ? predicateQueryHandler(document, selectorOrXPath) as Element : document.querySelector(selectorOrXPath);
if (!node)
return waitForHidden;
if (!waitForVisible && !waitForHidden)
return node;
const element = (node.nodeType === Node.TEXT_NODE ? node.parentElement : node as Element);
? document.evaluate(
selectorOrXPath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue
: predicateQueryHandler
? (predicateQueryHandler(document, selectorOrXPath) as Element)
: document.querySelector(selectorOrXPath);
if (!node) return waitForHidden;
if (!waitForVisible && !waitForHidden) return node;
const element =
node.nodeType === Node.TEXT_NODE
? node.parentElement
: (node as Element);
const style = window.getComputedStyle(element);
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
const isVisible =
style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success =
waitForVisible === isVisible || waitForHidden === !isVisible;
return success ? node : null;
function hasVisibleBoundingBox(): boolean {
@ -436,17 +551,29 @@ class WaitTask {
_timeoutTimer?: NodeJS.Timeout;
_terminated = false;
constructor(domWorld: DOMWorld, predicateBody: Function | string, predicateQueryHandlerBody: Function | string | undefined, title: string, polling: string | number, timeout: number, ...args: unknown[]) {
constructor(
domWorld: DOMWorld,
predicateBody: Function | string,
predicateQueryHandlerBody: Function | string | undefined,
title: string,
polling: string | number,
timeout: number,
...args: unknown[]
) {
if (helper.isString(polling))
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
assert(
polling === 'raf' || polling === 'mutation',
'Unknown polling option: ' + polling
);
else if (helper.isNumber(polling))
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
else
throw new Error('Unknown polling options: ' + polling);
else throw new Error('Unknown polling options: ' + polling);
function getPredicateBody(predicateBody: Function | string, predicateQueryHandlerBody: Function | string) {
if (helper.isString(predicateBody))
return `return (${predicateBody});`;
function getPredicateBody(
predicateBody: Function | string,
predicateQueryHandlerBody: Function | string
) {
if (helper.isString(predicateBody)) return `return (${predicateBody});`;
if (predicateQueryHandlerBody) {
return `
return (function wrapper(args) {
@ -460,7 +587,10 @@ class WaitTask {
this._domWorld = domWorld;
this._polling = polling;
this._timeout = timeout;
this._predicateBody = getPredicateBody(predicateBody, predicateQueryHandlerBody);
this._predicateBody = getPredicateBody(
predicateBody,
predicateQueryHandlerBody
);
this._args = args;
this._runCount = 0;
domWorld._waitTasks.add(this);
@ -471,8 +601,13 @@ class WaitTask {
// Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end.
if (timeout) {
const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
const timeoutError = new TimeoutError(
`waiting for ${title} failed: timeout ${timeout}ms exceeded`
);
this._timeoutTimer = setTimeout(
() => this.terminate(timeoutError),
timeout
);
}
this.rerun();
}
@ -489,21 +624,29 @@ class WaitTask {
let success = null;
let error = null;
try {
success = await (await this._domWorld.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
success = await (await this._domWorld.executionContext()).evaluateHandle(
waitForPredicatePageFunction,
this._predicateBody,
this._polling,
this._timeout,
...this._args
);
} catch (error_) {
error = error_;
}
if (this._terminated || runCount !== this._runCount) {
if (success)
await success.dispose();
if (success) await success.dispose();
return;
}
// Ignore timeouts in pageScript - we track timeouts ourselves.
// If the frame's execution context has already changed, `frame.evaluate` will
// throw an error - ignore this predicate run altogether.
if (!error && await this._domWorld.evaluate(s => !s, success).catch(() => true)) {
if (
!error &&
(await this._domWorld.evaluate((s) => !s, success).catch(() => true))
) {
await success.dispose();
return;
}
@ -515,13 +658,14 @@ class WaitTask {
// We could have tried to evaluate in a context which was already
// destroyed.
if (error && error.message.includes('Cannot find context with specified id'))
if (
error &&
error.message.includes('Cannot find context with specified id')
)
return;
if (error)
this._reject(error);
else
this._resolve(success);
if (error) this._reject(error);
else this._resolve(success);
this._cleanup();
}
@ -532,28 +676,28 @@ class WaitTask {
}
}
async function waitForPredicatePageFunction(predicateBody: string, polling: string, timeout: number, ...args: unknown[]): Promise<unknown> {
async function waitForPredicatePageFunction(
predicateBody: string,
polling: string,
timeout: number,
...args: unknown[]
): Promise<unknown> {
const predicate = new Function('...args', predicateBody);
let timedOut = false;
if (timeout)
setTimeout(() => timedOut = true, timeout);
if (polling === 'raf')
return await pollRaf();
if (polling === 'mutation')
return await pollMutation();
if (typeof polling === 'number')
return await pollInterval(polling);
if (timeout) setTimeout(() => (timedOut = true), timeout);
if (polling === 'raf') return await pollRaf();
if (polling === 'mutation') return await pollMutation();
if (typeof polling === 'number') return await pollInterval(polling);
/**
* @return {!Promise<*>}
*/
function pollMutation(): Promise<unknown> {
const success = predicate(...args);
if (success)
return Promise.resolve(success);
if (success) return Promise.resolve(success);
let fulfill;
const result = new Promise(x => fulfill = x);
const result = new Promise((x) => (fulfill = x));
const observer = new MutationObserver(() => {
if (timedOut) {
observer.disconnect();
@ -568,14 +712,14 @@ async function waitForPredicatePageFunction(predicateBody: string, polling: stri
observer.observe(document, {
childList: true,
subtree: true,
attributes: true
attributes: true,
});
return result;
}
function pollRaf(): Promise<unknown> {
let fulfill;
const result = new Promise(x => fulfill = x);
const result = new Promise((x) => (fulfill = x));
onRaf();
return result;
@ -585,16 +729,14 @@ async function waitForPredicatePageFunction(predicateBody: string, polling: stri
return;
}
const success = predicate(...args);
if (success)
fulfill(success);
else
requestAnimationFrame(onRaf);
if (success) fulfill(success);
else requestAnimationFrame(onRaf);
}
}
function pollInterval(pollInterval: number): Promise<unknown> {
let fulfill;
const result = new Promise(x => fulfill = x);
const result = new Promise((x) => (fulfill = x));
onTimeout();
return result;
@ -604,10 +746,8 @@ async function waitForPredicatePageFunction(predicateBody: string, polling: stri
return;
}
const success = predicate(...args);
if (success)
fulfill(success);
else
setTimeout(onTimeout, pollInterval);
if (success) fulfill(success);
else setTimeout(onTimeout, pollInterval);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,8 @@
* limitations under the License.
*/
import {assert} from './helper';
import {CDPSession} from './Connection';
import { assert } from './helper';
import { CDPSession } from './Connection';
/* TODO(jacktfranklin): protocol.d.ts defines this
* so let's ditch this and avoid the duplication
@ -24,7 +24,7 @@ export enum DialogType {
Alert = 'alert',
BeforeUnload = 'beforeunload',
Confirm = 'confirm',
Prompt = 'prompt'
Prompt = 'prompt',
}
export class Dialog {
@ -36,7 +36,12 @@ export class Dialog {
private _defaultValue: string;
private _handled = false;
constructor(client: CDPSession, type: DialogType, message: string, defaultValue = '') {
constructor(
client: CDPSession,
type: DialogType,
message: string,
defaultValue = ''
) {
this._client = client;
this._type = type;
this._message = message;
@ -60,7 +65,7 @@ export class Dialog {
this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', {
accept: true,
promptText: promptText
promptText: promptText,
});
}
@ -68,7 +73,7 @@ export class Dialog {
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', {
accept: false
accept: false,
});
}
}

View File

@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {CDPSession} from './Connection';
import type {Viewport} from './PuppeteerViewport';
import { CDPSession } from './Connection';
import type { Viewport } from './PuppeteerViewport';
export class EmulationManager {
_client: CDPSession;
@ -30,17 +30,26 @@ export class EmulationManager {
const width = viewport.width;
const height = viewport.height;
const deviceScaleFactor = viewport.deviceScaleFactor || 1;
const screenOrientation: Protocol.Emulation.ScreenOrientation = viewport.isLandscape ? {angle: 90, type: 'landscapePrimary'} : {angle: 0, type: 'portraitPrimary'};
const screenOrientation: Protocol.Emulation.ScreenOrientation = viewport.isLandscape
? { angle: 90, type: 'landscapePrimary' }
: { angle: 0, type: 'portraitPrimary' };
const hasTouch = viewport.hasTouch || false;
await Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', {mobile, width, height, deviceScaleFactor, screenOrientation}),
this._client.send('Emulation.setDeviceMetricsOverride', {
mobile,
width,
height,
deviceScaleFactor,
screenOrientation,
}),
this._client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch
})
enabled: hasTouch,
}),
]);
const reloadNeeded = this._emulatingMobile !== mobile || this._hasTouch !== hasTouch;
const reloadNeeded =
this._emulatingMobile !== mobile || this._hasTouch !== hasTouch;
this._emulatingMobile = mobile;
this._hasTouch = hasTouch;
return reloadNeeded;

View File

@ -42,7 +42,7 @@ export const Events = {
TargetCreated: 'targetcreated',
TargetDestroyed: 'targetdestroyed',
TargetChanged: 'targetchanged',
Disconnected: 'disconnected'
Disconnected: 'disconnected',
},
BrowserContext: {
@ -63,9 +63,15 @@ export const Events = {
FrameNavigated: Symbol('Events.FrameManager.FrameNavigated'),
FrameDetached: Symbol('Events.FrameManager.FrameDetached'),
LifecycleEvent: Symbol('Events.FrameManager.LifecycleEvent'),
FrameNavigatedWithinDocument: Symbol('Events.FrameManager.FrameNavigatedWithinDocument'),
ExecutionContextCreated: Symbol('Events.FrameManager.ExecutionContextCreated'),
ExecutionContextDestroyed: Symbol('Events.FrameManager.ExecutionContextDestroyed'),
FrameNavigatedWithinDocument: Symbol(
'Events.FrameManager.FrameNavigatedWithinDocument'
),
ExecutionContextCreated: Symbol(
'Events.FrameManager.ExecutionContextCreated'
),
ExecutionContextDestroyed: Symbol(
'Events.FrameManager.ExecutionContextDestroyed'
),
},
Connection: {

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
import {helper, assert} from './helper';
import {createJSHandle, JSHandle, ElementHandle} from './JSHandle';
import {CDPSession} from './Connection';
import {DOMWorld} from './DOMWorld';
import {Frame} from './FrameManager';
import { helper, assert } from './helper';
import { createJSHandle, JSHandle, ElementHandle } from './JSHandle';
import { CDPSession } from './Connection';
import { DOMWorld } from './DOMWorld';
import { Frame } from './FrameManager';
export const EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
@ -28,7 +28,11 @@ export class ExecutionContext {
_world: DOMWorld;
_contextId: number;
constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: DOMWorld) {
constructor(
client: CDPSession,
contextPayload: Protocol.Runtime.ExecutionContextDescription,
world: DOMWorld
) {
this._client = client;
this._world = world;
this._contextId = contextPayload.id;
@ -38,38 +42,62 @@ export class ExecutionContext {
return this._world ? this._world.frame() : null;
}
async evaluate<ReturnType extends any>(pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
return await this._evaluateInternal<ReturnType>(true, pageFunction, ...args);
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return await this._evaluateInternal<ReturnType>(
true,
pageFunction,
...args
);
}
async evaluateHandle(pageFunction: Function | string, ...args: unknown[]): Promise<JSHandle> {
async evaluateHandle(
pageFunction: Function | string,
...args: unknown[]
): Promise<JSHandle> {
return this._evaluateInternal<JSHandle>(false, pageFunction, ...args);
}
private async _evaluateInternal<ReturnType>(returnByValue, pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
private async _evaluateInternal<ReturnType>(
returnByValue,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
if (helper.isString(pageFunction)) {
const contextId = this._contextId;
const expression = pageFunction;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) ? expression : expression + '\n' + suffix;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
? expression
: expression + '\n' + suffix;
const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', {
expression: expressionWithSourceUrl,
contextId,
returnByValue,
awaitPromise: true,
userGesture: true
}).catch(rewriteError);
const { exceptionDetails, result: remoteObject } = await this._client
.send('Runtime.evaluate', {
expression: expressionWithSourceUrl,
contextId,
returnByValue,
awaitPromise: true,
userGesture: true,
})
.catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
throw new Error(
'Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)
);
return returnByValue ? helper.valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject);
return returnByValue
? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject);
}
if (typeof pageFunction !== 'function')
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
throw new Error(
`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`
);
let functionText = pageFunction.toString();
try {
@ -78,11 +106,11 @@ export class ExecutionContext {
// This means we might have a function shorthand. Try another
// time prefixing 'function '.
if (functionText.startsWith('async '))
functionText = 'async function ' + functionText.substring('async '.length);
else
functionText = 'function ' + functionText;
functionText =
'async function ' + functionText.substring('async '.length);
else functionText = 'function ' + functionText;
try {
new Function('(' + functionText + ')');
new Function('(' + functionText + ')');
} catch (error) {
// We tried hard to serialize, but there's a weird beast here.
throw new Error('Passed function is not well-serializable!');
@ -96,17 +124,27 @@ export class ExecutionContext {
arguments: args.map(convertArgument.bind(this)),
returnByValue,
awaitPromise: true,
userGesture: true
userGesture: true,
});
} catch (error) {
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
if (
error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON')
)
error.message += ' Are you passing a nested JSHandle?';
throw error;
}
const {exceptionDetails, result: remoteObject} = await callFunctionOnPromise.catch(rewriteError);
const {
exceptionDetails,
result: remoteObject,
} = await callFunctionOnPromise.catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return returnByValue ? helper.valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject);
throw new Error(
'Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)
);
return returnByValue
? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject);
/**
* @param {*} arg
@ -114,62 +152,78 @@ export class ExecutionContext {
* @this {ExecutionContext}
*/
function convertArgument(this: ExecutionContext, arg: unknown): unknown {
if (typeof arg === 'bigint') // eslint-disable-line valid-typeof
return {unserializableValue: `${arg.toString()}n`};
if (Object.is(arg, -0))
return {unserializableValue: '-0'};
if (Object.is(arg, Infinity))
return {unserializableValue: 'Infinity'};
if (typeof arg === 'bigint')
// eslint-disable-line valid-typeof
return { unserializableValue: `${arg.toString()}n` };
if (Object.is(arg, -0)) return { unserializableValue: '-0' };
if (Object.is(arg, Infinity)) return { unserializableValue: 'Infinity' };
if (Object.is(arg, -Infinity))
return {unserializableValue: '-Infinity'};
if (Object.is(arg, NaN))
return {unserializableValue: 'NaN'};
const objectHandle = arg && (arg instanceof JSHandle) ? arg : null;
return { unserializableValue: '-Infinity' };
if (Object.is(arg, NaN)) return { unserializableValue: 'NaN' };
const objectHandle = arg && arg instanceof JSHandle ? arg : null;
if (objectHandle) {
if (objectHandle._context !== this)
throw new Error('JSHandles can be evaluated only in the context they were created!');
if (objectHandle._disposed)
throw new Error('JSHandle is disposed!');
throw new Error(
'JSHandles can be evaluated only in the context they were created!'
);
if (objectHandle._disposed) throw new Error('JSHandle is disposed!');
if (objectHandle._remoteObject.unserializableValue)
return {unserializableValue: objectHandle._remoteObject.unserializableValue};
return {
unserializableValue: objectHandle._remoteObject.unserializableValue,
};
if (!objectHandle._remoteObject.objectId)
return {value: objectHandle._remoteObject.value};
return {objectId: objectHandle._remoteObject.objectId};
return { value: objectHandle._remoteObject.value };
return { objectId: objectHandle._remoteObject.objectId };
}
return {value: arg};
return { value: arg };
}
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
if (error.message.includes('Object reference chain is too long'))
return {result: {type: 'undefined'}};
if (error.message.includes('Object couldn\'t be returned by value'))
return {result: {type: 'undefined'}};
return { result: { type: 'undefined' } };
if (error.message.includes("Object couldn't be returned by value"))
return { result: { type: 'undefined' } };
if (error.message.endsWith('Cannot find context with specified id') || error.message.endsWith('Inspected target navigated or closed'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
if (
error.message.endsWith('Cannot find context with specified id') ||
error.message.endsWith('Inspected target navigated or closed')
)
throw new Error(
'Execution context was destroyed, most likely because of a navigation.'
);
throw error;
}
}
async queryObjects(prototypeHandle: JSHandle): Promise<JSHandle> {
assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!');
assert(prototypeHandle._remoteObject.objectId, 'Prototype JSHandle must not be referencing primitive value');
assert(
prototypeHandle._remoteObject.objectId,
'Prototype JSHandle must not be referencing primitive value'
);
const response = await this._client.send('Runtime.queryObjects', {
prototypeObjectId: prototypeHandle._remoteObject.objectId
prototypeObjectId: prototypeHandle._remoteObject.objectId,
});
return createJSHandle(this, response.objects);
}
async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId): Promise<ElementHandle> {
const {object} = await this._client.send('DOM.resolveNode', {
async _adoptBackendNodeId(
backendNodeId: Protocol.DOM.BackendNodeId
): Promise<ElementHandle> {
const { object } = await this._client.send('DOM.resolveNode', {
backendNodeId: backendNodeId,
executionContextId: this._contextId,
});
return createJSHandle(this, object) as ElementHandle;
}
async _adoptElementHandle(elementHandle: ElementHandle): Promise<ElementHandle> {
assert(elementHandle.executionContext() !== this, 'Cannot adopt handle that already belongs to this execution context');
async _adoptElementHandle(
elementHandle: ElementHandle
): Promise<ElementHandle> {
assert(
elementHandle.executionContext() !== this,
'Cannot adopt handle that already belongs to this execution context'
);
assert(this._world, 'Cannot adopt handle without DOMWorld');
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: elementHandle._remoteObject.objectId,

View File

@ -15,17 +15,17 @@
*/
import * as EventEmitter from 'events';
import {helper, assert, debugError} from './helper';
import {Events} from './Events';
import {ExecutionContext, EVALUATION_SCRIPT_URL} from './ExecutionContext';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher';
import {DOMWorld, WaitForSelectorOptions} from './DOMWorld';
import {NetworkManager, Response} from './NetworkManager';
import {TimeoutSettings} from './TimeoutSettings';
import {CDPSession} from './Connection';
import {JSHandle, ElementHandle} from './JSHandle';
import {MouseButtonInput} from './Input';
import {Page} from './Page';
import { helper, assert, debugError } from './helper';
import { Events } from './Events';
import { ExecutionContext, EVALUATION_SCRIPT_URL } from './ExecutionContext';
import { LifecycleWatcher, PuppeteerLifeCycleEvent } from './LifecycleWatcher';
import { DOMWorld, WaitForSelectorOptions } from './DOMWorld';
import { NetworkManager, Response } from './NetworkManager';
import { TimeoutSettings } from './TimeoutSettings';
import { CDPSession } from './Connection';
import { JSHandle, ElementHandle } from './JSHandle';
import { MouseButtonInput } from './Input';
import { Page } from './Page';
const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
@ -39,34 +39,62 @@ export class FrameManager extends EventEmitter {
_isolatedWorlds = new Set<string>();
_mainFrame: Frame;
constructor(client: CDPSession, page: Page, ignoreHTTPSErrors: boolean, timeoutSettings: TimeoutSettings) {
constructor(
client: CDPSession,
page: Page,
ignoreHTTPSErrors: boolean,
timeoutSettings: TimeoutSettings
) {
super();
this._client = client;
this._page = page;
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this._timeoutSettings = timeoutSettings;
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId));
this._client.on('Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId));
this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context));
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
this._client.on('Runtime.executionContextsCleared', () => this._onExecutionContextsCleared());
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
this._client.on('Page.frameAttached', (event) =>
this._onFrameAttached(event.frameId, event.parentFrameId)
);
this._client.on('Page.frameNavigated', (event) =>
this._onFrameNavigated(event.frame)
);
this._client.on('Page.navigatedWithinDocument', (event) =>
this._onFrameNavigatedWithinDocument(event.frameId, event.url)
);
this._client.on('Page.frameDetached', (event) =>
this._onFrameDetached(event.frameId)
);
this._client.on('Page.frameStoppedLoading', (event) =>
this._onFrameStoppedLoading(event.frameId)
);
this._client.on('Runtime.executionContextCreated', (event) =>
this._onExecutionContextCreated(event.context)
);
this._client.on('Runtime.executionContextDestroyed', (event) =>
this._onExecutionContextDestroyed(event.executionContextId)
);
this._client.on('Runtime.executionContextsCleared', () =>
this._onExecutionContextsCleared()
);
this._client.on('Page.lifecycleEvent', (event) =>
this._onLifecycleEvent(event)
);
}
async initialize(): Promise<void> {
const result = await Promise.all<Protocol.Page.enableReturnValue, Protocol.Page.getFrameTreeReturnValue>([
const result = await Promise.all<
Protocol.Page.enableReturnValue,
Protocol.Page.getFrameTreeReturnValue
>([
this._client.send('Page.enable'),
this._client.send('Page.getFrameTree'),
]);
const {frameTree} = result[1];
const { frameTree } = result[1];
this._handleFrameTree(frameTree);
await Promise.all([
this._client.send('Page.setLifecycleEventsEnabled', {enabled: true}),
this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
this._client
.send('Runtime.enable', {})
.then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
this._networkManager.initialize(),
]);
}
@ -75,7 +103,15 @@ export class FrameManager extends EventEmitter {
return this._networkManager;
}
async navigateFrame(frame: Frame, url: string, options: {referer?: string; timeout?: number; waitUntil?: PuppeteerLifeCycleEvent|PuppeteerLifeCycleEvent[]} = {}): Promise<Response | null> {
async navigateFrame(
frame: Frame,
url: string,
options: {
referer?: string;
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {}
): Promise<Response | null> {
assertNoLegacyNavigationOptions(options);
const {
referer = this._networkManager.extraHTTPHeaders()['referer'],
@ -92,26 +128,44 @@ export class FrameManager extends EventEmitter {
if (!error) {
error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise() : watcher.sameDocumentNavigationPromise(),
ensureNewDocumentNavigation
? watcher.newDocumentNavigationPromise()
: watcher.sameDocumentNavigationPromise(),
]);
}
watcher.dispose();
if (error)
throw error;
if (error) throw error;
return watcher.navigationResponse();
async function navigate(client: CDPSession, url: string, referrer: string, frameId: string): Promise<Error | null> {
async function navigate(
client: CDPSession,
url: string,
referrer: string,
frameId: string
): Promise<Error | null> {
try {
const response = await client.send('Page.navigate', {url, referrer, frameId});
const response = await client.send('Page.navigate', {
url,
referrer,
frameId,
});
ensureNewDocumentNavigation = !!response.loaderId;
return response.errorText ? new Error(`${response.errorText} at ${url}`) : null;
return response.errorText
? new Error(`${response.errorText} at ${url}`)
: null;
} catch (error) {
return error;
}
}
}
async waitForFrameNavigation(frame: Frame, options: {timeout?: number; waitUntil?: PuppeteerLifeCycleEvent|PuppeteerLifeCycleEvent[]} = {}): Promise<Response | null> {
async waitForFrameNavigation(
frame: Frame,
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {}
): Promise<Response | null> {
assertNoLegacyNavigationOptions(options);
const {
waitUntil = ['load'],
@ -121,26 +175,23 @@ export class FrameManager extends EventEmitter {
const error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
watcher.sameDocumentNavigationPromise(),
watcher.newDocumentNavigationPromise()
watcher.newDocumentNavigationPromise(),
]);
watcher.dispose();
if (error)
throw error;
if (error) throw error;
return watcher.navigationResponse();
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload): void {
const frame = this._frames.get(event.frameId);
if (!frame)
return;
if (!frame) return;
frame._onLifecycleEvent(event.loaderId, event.name);
this.emit(Events.FrameManager.LifecycleEvent, frame);
}
_onFrameStoppedLoading(frameId: string): void {
const frame = this._frames.get(frameId);
if (!frame)
return;
if (!frame) return;
frame._onLoadingStopped();
this.emit(Events.FrameManager.LifecycleEvent, frame);
}
@ -149,11 +200,9 @@ export class FrameManager extends EventEmitter {
if (frameTree.frame.parentId)
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId);
this._onFrameNavigated(frameTree.frame);
if (!frameTree.childFrames)
return;
if (!frameTree.childFrames) return;
for (const child of frameTree.childFrames)
this._handleFrameTree(child);
for (const child of frameTree.childFrames) this._handleFrameTree(child);
}
page(): Page {
@ -173,8 +222,7 @@ export class FrameManager extends EventEmitter {
}
_onFrameAttached(frameId: string, parentFrameId?: string): void {
if (this._frames.has(frameId))
return;
if (this._frames.has(frameId)) return;
assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId);
const frame = new Frame(this, this._client, parentFrame, frameId);
@ -184,8 +232,13 @@ export class FrameManager extends EventEmitter {
_onFrameNavigated(framePayload: Protocol.Page.Frame): void {
const isMainFrame = !framePayload.parentId;
let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id);
assert(isMainFrame || frame, 'We either navigate top level or have old version of the navigated frame');
let frame = isMainFrame
? this._mainFrame
: this._frames.get(framePayload.id);
assert(
isMainFrame || frame,
'We either navigate top level or have old version of the navigated frame'
);
// Detach all child frames first.
if (frame) {
@ -214,24 +267,28 @@ export class FrameManager extends EventEmitter {
}
async _ensureIsolatedWorld(name: string): Promise<void> {
if (this._isolatedWorlds.has(name))
return;
if (this._isolatedWorlds.has(name)) return;
this._isolatedWorlds.add(name);
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {
source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`,
worldName: name,
}),
await Promise.all(this.frames().map(frame => this._client.send('Page.createIsolatedWorld', {
frameId: frame._id,
grantUniveralAccess: true,
worldName: name,
}).catch(debugError))); // frames might be removed before we send this
await Promise.all(
this.frames().map((frame) =>
this._client
.send('Page.createIsolatedWorld', {
frameId: frame._id,
grantUniveralAccess: true,
worldName: name,
})
.catch(debugError)
)
); // frames might be removed before we send this
}
_onFrameNavigatedWithinDocument(frameId: string, url: string): void {
const frame = this._frames.get(frameId);
if (!frame)
return;
if (!frame) return;
frame._navigatedWithinDocument(url);
this.emit(Events.FrameManager.FrameNavigatedWithinDocument, frame);
this.emit(Events.FrameManager.FrameNavigated, frame);
@ -239,19 +296,23 @@ export class FrameManager extends EventEmitter {
_onFrameDetached(frameId: string): void {
const frame = this._frames.get(frameId);
if (frame)
this._removeFramesRecursively(frame);
if (frame) this._removeFramesRecursively(frame);
}
_onExecutionContextCreated(contextPayload: Protocol.Runtime.ExecutionContextDescription): void {
const auxData = contextPayload.auxData as { frameId?: string};
_onExecutionContextCreated(
contextPayload: Protocol.Runtime.ExecutionContextDescription
): void {
const auxData = contextPayload.auxData as { frameId?: string };
const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null;
let world = null;
if (frame) {
if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
world = frame._mainWorld;
} else if (contextPayload.name === UTILITY_WORLD_NAME && !frame._secondaryWorld._hasContext()) {
} else if (
contextPayload.name === UTILITY_WORLD_NAME &&
!frame._secondaryWorld._hasContext()
) {
// In case of multiple sessions to the same target, there's a race between
// connections so we might end up creating multiple isolated worlds.
// We can use either.
@ -261,8 +322,7 @@ export class FrameManager extends EventEmitter {
if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated')
this._isolatedWorlds.add(contextPayload.name);
const context = new ExecutionContext(this._client, contextPayload, world);
if (world)
world._setContext(context);
if (world) world._setContext(context);
this._contextIdToContext.set(contextPayload.id, context);
}
@ -271,17 +331,14 @@ export class FrameManager extends EventEmitter {
*/
_onExecutionContextDestroyed(executionContextId: number): void {
const context = this._contextIdToContext.get(executionContextId);
if (!context)
return;
if (!context) return;
this._contextIdToContext.delete(executionContextId);
if (context._world)
context._world._setContext(null);
if (context._world) context._world._setContext(null);
}
_onExecutionContextsCleared(): void {
for (const context of this._contextIdToContext.values()) {
if (context._world)
context._world._setContext(null);
if (context._world) context._world._setContext(null);
}
this._contextIdToContext.clear();
}
@ -317,7 +374,12 @@ export class Frame {
_secondaryWorld: DOMWorld;
_childFrames: Set<Frame>;
constructor(frameManager: FrameManager, client: CDPSession, parentFrame: Frame | null, frameId: string) {
constructor(
frameManager: FrameManager,
client: CDPSession,
parentFrame: Frame | null,
frameId: string
) {
this._frameManager = frameManager;
this._client = client;
this._parentFrame = parentFrame;
@ -326,19 +388,36 @@ export class Frame {
this._detached = false;
this._loaderId = '';
this._mainWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings);
this._secondaryWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings);
this._mainWorld = new DOMWorld(
frameManager,
this,
frameManager._timeoutSettings
);
this._secondaryWorld = new DOMWorld(
frameManager,
this,
frameManager._timeoutSettings
);
this._childFrames = new Set();
if (this._parentFrame)
this._parentFrame._childFrames.add(this);
if (this._parentFrame) this._parentFrame._childFrames.add(this);
}
async goto(url: string, options: {referer?: string; timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]}): Promise<Response | null> {
async goto(
url: string,
options: {
referer?: string;
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
): Promise<Response | null> {
return await this._frameManager.navigateFrame(this, url, options);
}
async waitForNavigation(options: {timeout?: number; waitUntil?: PuppeteerLifeCycleEvent|PuppeteerLifeCycleEvent[]}): Promise<Response | null> {
async waitForNavigation(options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}): Promise<Response | null> {
return await this._frameManager.waitForFrameNavigation(this, options);
}
@ -346,11 +425,17 @@ export class Frame {
return this._mainWorld.executionContext();
}
async evaluateHandle(pageFunction: Function|string, ...args: unknown[]): Promise<JSHandle> {
async evaluateHandle(
pageFunction: Function | string,
...args: unknown[]
): Promise<JSHandle> {
return this._mainWorld.evaluateHandle(pageFunction, ...args);
}
async evaluate<ReturnType extends any>(pageFunction: Function|string, ...args: unknown[]): Promise<ReturnType> {
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return this._mainWorld.evaluate<ReturnType>(pageFunction, ...args);
}
@ -362,11 +447,19 @@ export class Frame {
return this._mainWorld.$x(expression);
}
async $eval<ReturnType extends any>(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return this._mainWorld.$eval<ReturnType>(selector, pageFunction, ...args);
}
async $$eval<ReturnType extends any>(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return this._mainWorld.$$eval<ReturnType>(selector, pageFunction, ...args);
}
@ -378,7 +471,13 @@ export class Frame {
return this._secondaryWorld.content();
}
async setContent(html: string, options: {timeout?: number; waitUntil?: PuppeteerLifeCycleEvent|PuppeteerLifeCycleEvent[]} = {}): Promise<void> {
async setContent(
html: string,
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {}
): Promise<void> {
return this._secondaryWorld.setContent(html, options);
}
@ -402,15 +501,27 @@ export class Frame {
return this._detached;
}
async addScriptTag(options: {url?: string; path?: string; content?: string; type?: string}): Promise<ElementHandle> {
async addScriptTag(options: {
url?: string;
path?: string;
content?: string;
type?: string;
}): Promise<ElementHandle> {
return this._mainWorld.addScriptTag(options);
}
async addStyleTag(options: {url?: string; path?: string; content?: string}): Promise<ElementHandle> {
async addStyleTag(options: {
url?: string;
path?: string;
content?: string;
}): Promise<ElementHandle> {
return this._mainWorld.addStyleTag(options);
}
async click(selector: string, options: {delay?: number; button?: MouseButtonInput; clickCount?: number}): Promise<void> {
async click(
selector: string,
options: { delay?: number; button?: MouseButtonInput; clickCount?: number }
): Promise<void> {
return this._secondaryWorld.click(selector, options);
}
@ -430,11 +541,19 @@ export class Frame {
return this._secondaryWorld.tap(selector);
}
async type(selector: string, text: string, options?: {delay: number}): Promise<void> {
async type(
selector: string,
text: string,
options?: { delay: number }
): Promise<void> {
return this._mainWorld.type(selector, text, options);
}
waitFor(selectorOrFunctionOrTimeout: string|number|Function, options: {} = {}, ...args: unknown[]): Promise<JSHandle | null> {
waitFor(
selectorOrFunctionOrTimeout: string | number | Function,
options: {} = {},
...args: unknown[]
): Promise<JSHandle | null> {
const xPathPattern = '//';
if (helper.isString(selectorOrFunctionOrTimeout)) {
@ -444,33 +563,54 @@ export class Frame {
return this.waitForSelector(string, options);
}
if (helper.isNumber(selectorOrFunctionOrTimeout))
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
return new Promise((fulfill) =>
setTimeout(fulfill, selectorOrFunctionOrTimeout)
);
if (typeof selectorOrFunctionOrTimeout === 'function')
return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
return this.waitForFunction(
selectorOrFunctionOrTimeout,
options,
...args
);
return Promise.reject(
new Error(
'Unsupported target type: ' + typeof selectorOrFunctionOrTimeout
)
);
}
async waitForSelector(selector: string, options: WaitForSelectorOptions): Promise<ElementHandle | null> {
const handle = await this._secondaryWorld.waitForSelector(selector, options);
if (!handle)
return null;
async waitForSelector(
selector: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
const handle = await this._secondaryWorld.waitForSelector(
selector,
options
);
if (!handle) return null;
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}
async waitForXPath(xpath: string, options: WaitForSelectorOptions): Promise<ElementHandle | null> {
async waitForXPath(
xpath: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
const handle = await this._secondaryWorld.waitForXPath(xpath, options);
if (!handle)
return null;
if (!handle) return null;
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}
waitForFunction(pageFunction: Function|string, options: {polling?: string|number; timeout?: number} = {}, ...args: unknown[]): Promise<JSHandle> {
waitForFunction(
pageFunction: Function | string,
options: { polling?: string | number; timeout?: number } = {},
...args: unknown[]
): Promise<JSHandle> {
return this._mainWorld.waitForFunction(pageFunction, options, ...args);
}
@ -504,14 +644,24 @@ export class Frame {
this._detached = true;
this._mainWorld._detach();
this._secondaryWorld._detach();
if (this._parentFrame)
this._parentFrame._childFrames.delete(this);
if (this._parentFrame) this._parentFrame._childFrames.delete(this);
this._parentFrame = null;
}
}
function assertNoLegacyNavigationOptions(options: {[optionName: string]: unknown}): void {
assert(options['networkIdleTimeout'] === undefined, 'ERROR: networkIdleTimeout option is no longer supported.');
assert(options['networkIdleInflight'] === undefined, 'ERROR: networkIdleInflight option is no longer supported.');
assert(options.waitUntil !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead');
function assertNoLegacyNavigationOptions(options: {
[optionName: string]: unknown;
}): void {
assert(
options['networkIdleTimeout'] === undefined,
'ERROR: networkIdleTimeout option is no longer supported.'
);
assert(
options['networkIdleInflight'] === undefined,
'ERROR: networkIdleInflight option is no longer supported.'
);
assert(
options.waitUntil !== 'networkidle',
'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead'
);
}

View File

@ -14,11 +14,13 @@
* limitations under the License.
*/
import {assert} from './helper';
import {CDPSession} from './Connection';
import {keyDefinitions, KeyDefinition, KeyInput} from './USKeyboardLayout';
import { assert } from './helper';
import { CDPSession } from './Connection';
import { keyDefinitions, KeyDefinition, KeyInput } from './USKeyboardLayout';
type KeyDescription = Required<Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>>;
type KeyDescription = Required<
Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>
>;
export class Keyboard {
_client: CDPSession;
@ -29,7 +31,10 @@ export class Keyboard {
this._client = client;
}
async down(key: KeyInput, options: { text?: string } = {text: undefined}): Promise<void> {
async down(
key: KeyInput,
options: { text?: string } = { text: undefined }
): Promise<void> {
const description = this._keyDescriptionForString(key);
const autoRepeat = this._pressedKeys.has(description.code);
@ -47,19 +52,15 @@ export class Keyboard {
unmodifiedText: text,
autoRepeat,
location: description.location,
isKeypad: description.location === 3
isKeypad: description.location === 3,
});
}
private _modifierBit(key: string): number {
if (key === 'Alt')
return 1;
if (key === 'Control')
return 2;
if (key === 'Meta')
return 4;
if (key === 'Shift')
return 8;
if (key === 'Alt') return 1;
if (key === 'Control') return 2;
if (key === 'Meta') return 4;
if (key === 'Shift') return 8;
return 0;
}
@ -70,39 +71,30 @@ export class Keyboard {
keyCode: 0,
code: '',
text: '',
location: 0
location: 0,
};
const definition = keyDefinitions[keyString];
assert(definition, `Unknown key: "${keyString}"`);
if (definition.key)
description.key = definition.key;
if (shift && definition.shiftKey)
description.key = definition.shiftKey;
if (definition.key) description.key = definition.key;
if (shift && definition.shiftKey) description.key = definition.shiftKey;
if (definition.keyCode)
description.keyCode = definition.keyCode;
if (definition.keyCode) description.keyCode = definition.keyCode;
if (shift && definition.shiftKeyCode)
description.keyCode = definition.shiftKeyCode;
if (definition.code)
description.code = definition.code;
if (definition.code) description.code = definition.code;
if (definition.location)
description.location = definition.location;
if (definition.location) description.location = definition.location;
if (description.key.length === 1)
description.text = description.key;
if (description.key.length === 1) description.text = description.key;
if (definition.text)
description.text = definition.text;
if (shift && definition.shiftText)
description.text = definition.shiftText;
if (definition.text) description.text = definition.text;
if (shift && definition.shiftText) description.text = definition.shiftText;
// if any modifiers besides shift are pressed, no text should be sent
if (this._modifiers & ~8)
description.text = '';
if (this._modifiers & ~8) description.text = '';
return description;
}
@ -118,36 +110,37 @@ export class Keyboard {
key: description.key,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
location: description.location
location: description.location,
});
}
async sendCharacter(char: string): Promise<void> {
await this._client.send('Input.insertText', {text: char});
await this._client.send('Input.insertText', { text: char });
}
private charIsKey(char: string): char is KeyInput {
return !!keyDefinitions[char];
}
async type(text: string, options: {delay?: number}): Promise<void> {
async type(text: string, options: { delay?: number }): Promise<void> {
const delay = (options && options.delay) || null;
for (const char of text) {
if (this.charIsKey(char)) {
await this.press(char, {delay});
await this.press(char, { delay });
} else {
if (delay)
await new Promise(f => setTimeout(f, delay));
if (delay) await new Promise((f) => setTimeout(f, delay));
await this.sendCharacter(char);
}
}
}
async press(key: KeyInput, options: {delay?: number; text?: string} = {}): Promise<void> {
const {delay = null} = options;
async press(
key: KeyInput,
options: { delay?: number; text?: string } = {}
): Promise<void> {
const { delay = null } = options;
await this.down(key, options);
if (delay)
await new Promise(f => setTimeout(f, options.delay));
if (delay) await new Promise((f) => setTimeout(f, options.delay));
await this.up(key);
}
}
@ -175,9 +168,14 @@ export class Mouse {
this._keyboard = keyboard;
}
async move(x: number, y: number, options: {steps?: number} = {}): Promise<void> {
const {steps = 1} = options;
const fromX = this._x, fromY = this._y;
async move(
x: number,
y: number,
options: { steps?: number } = {}
): Promise<void> {
const { steps = 1 } = options;
const fromX = this._x,
fromY = this._y;
this._x = x;
this._y = y;
for (let i = 1; i <= steps; i++) {
@ -186,19 +184,20 @@ export class Mouse {
button: this._button,
x: fromX + (this._x - fromX) * (i / steps),
y: fromY + (this._y - fromY) * (i / steps),
modifiers: this._keyboard._modifiers
modifiers: this._keyboard._modifiers,
});
}
}
async click(x: number, y: number, options: MouseOptions & {delay?: number} = {}): Promise<void> {
const {delay = null} = options;
async click(
x: number,
y: number,
options: MouseOptions & { delay?: number } = {}
): Promise<void> {
const { delay = null } = options;
if (delay !== null) {
await Promise.all([
this.move(x, y),
this.down(options),
]);
await new Promise(f => setTimeout(f, delay));
await Promise.all([this.move(x, y), this.down(options)]);
await new Promise((f) => setTimeout(f, delay));
await this.up(options);
} else {
await Promise.all([
@ -210,7 +209,7 @@ export class Mouse {
}
async down(options: MouseOptions = {}): Promise<void> {
const {button = 'left', clickCount = 1} = options;
const { button = 'left', clickCount = 1 } = options;
this._button = button;
await this._client.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
@ -218,7 +217,7 @@ export class Mouse {
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount
clickCount,
});
}
@ -226,7 +225,7 @@ export class Mouse {
* @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
*/
async up(options: MouseOptions = {}): Promise<void> {
const {button = 'left', clickCount = 1} = options;
const { button = 'left', clickCount = 1 } = options;
this._button = 'none';
await this._client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
@ -234,7 +233,7 @@ export class Mouse {
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount
clickCount,
});
}
}
@ -257,20 +256,21 @@ export class Touchscreen {
// This waits a frame before sending the tap.
// @see https://crbug.com/613219
await this._client.send('Runtime.evaluate', {
expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true
expression:
'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true,
});
const touchPoints = [{x: Math.round(x), y: Math.round(y)}];
const touchPoints = [{ x: Math.round(x), y: Math.round(y) }];
await this._client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints,
modifiers: this._keyboard._modifiers
modifiers: this._keyboard._modifiers,
});
await this._client.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
modifiers: this._keyboard._modifiers
modifiers: this._keyboard._modifiers,
});
}
}

View File

@ -14,28 +14,37 @@
* limitations under the License.
*/
import {helper, assert, debugError} from './helper';
import {ExecutionContext} from './ExecutionContext';
import {Page} from './Page';
import {CDPSession} from './Connection';
import {KeyInput} from './USKeyboardLayout';
import {FrameManager, Frame} from './FrameManager';
import {getQueryHandlerAndSelector} from './QueryHandler';
import { helper, assert, debugError } from './helper';
import { ExecutionContext } from './ExecutionContext';
import { Page } from './Page';
import { CDPSession } from './Connection';
import { KeyInput } from './USKeyboardLayout';
import { FrameManager, Frame } from './FrameManager';
import { getQueryHandlerAndSelector } from './QueryHandler';
interface BoxModel {
content: Array<{x: number; y: number}>;
padding: Array<{x: number; y: number}>;
border: Array<{x: number; y: number}>;
margin: Array<{x: number; y: number}>;
content: Array<{ x: number; y: number }>;
padding: Array<{ x: number; y: number }>;
border: Array<{ x: number; y: number }>;
margin: Array<{ x: number; y: number }>;
width: number;
height: number;
}
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): JSHandle {
export function createJSHandle(
context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject
): JSHandle {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._frameManager;
return new ElementHandle(context, context._client, remoteObject, frameManager.page(), frameManager);
return new ElementHandle(
context,
context._client,
remoteObject,
frameManager.page(),
frameManager
);
}
return new JSHandle(context, context._client, remoteObject);
}
@ -46,7 +55,11 @@ export class JSHandle {
_remoteObject: Protocol.Runtime.RemoteObject;
_disposed = false;
constructor(context: ExecutionContext, client: CDPSession, remoteObject: Protocol.Runtime.RemoteObject) {
constructor(
context: ExecutionContext,
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject
) {
this._context = context;
this._client = client;
this._remoteObject = remoteObject;
@ -56,20 +69,37 @@ export class JSHandle {
return this._context;
}
async evaluate<ReturnType extends any>(pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
return await this.executionContext().evaluate<ReturnType>(pageFunction, this, ...args);
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return await this.executionContext().evaluate<ReturnType>(
pageFunction,
this,
...args
);
}
async evaluateHandle(pageFunction: Function | string, ...args: unknown[]): Promise<JSHandle> {
return await this.executionContext().evaluateHandle(pageFunction, this, ...args);
async evaluateHandle(
pageFunction: Function | string,
...args: unknown[]
): Promise<JSHandle> {
return await this.executionContext().evaluateHandle(
pageFunction,
this,
...args
);
}
async getProperty(propertyName: string): Promise<JSHandle | undefined> {
const objectHandle = await this.evaluateHandle((object: HTMLElement, propertyName: string) => {
const result = {__proto__: null};
result[propertyName] = object[propertyName];
return result;
}, propertyName);
const objectHandle = await this.evaluateHandle(
(object: HTMLElement, propertyName: string) => {
const result = { __proto__: null };
result[propertyName] = object[propertyName];
return result;
},
propertyName
);
const properties = await objectHandle.getProperties();
const result = properties.get(propertyName) || null;
await objectHandle.dispose();
@ -79,12 +109,11 @@ export class JSHandle {
async getProperties(): Promise<Map<string, JSHandle>> {
const response = await this._client.send('Runtime.getProperties', {
objectId: this._remoteObject.objectId,
ownProperties: true
ownProperties: true,
});
const result = new Map<string, JSHandle>();
for (const property of response.result) {
if (!property.enumerable)
continue;
if (!property.enumerable) continue;
result.set(property.name, createJSHandle(this._context, property.value));
}
return result;
@ -109,15 +138,14 @@ export class JSHandle {
}
async dispose(): Promise<void> {
if (this._disposed)
return;
if (this._disposed) return;
this._disposed = true;
await helper.releaseObject(this._client, this._remoteObject);
}
toString(): string {
if (this._remoteObject.objectId) {
const type = this._remoteObject.subtype || this._remoteObject.type;
const type = this._remoteObject.subtype || this._remoteObject.type;
return 'JSHandle@' + type;
}
return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
@ -127,7 +155,13 @@ export class JSHandle {
export class ElementHandle extends JSHandle {
_page: Page;
_frameManager: FrameManager;
constructor(context: ExecutionContext, client: CDPSession, remoteObject: Protocol.Runtime.RemoteObject, page: Page, frameManager: FrameManager) {
constructor(
context: ExecutionContext,
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject,
page: Page,
frameManager: FrameManager
) {
super(context, client, remoteObject);
this._client = client;
this._remoteObject = remoteObject;
@ -141,59 +175,74 @@ export class ElementHandle extends JSHandle {
async contentFrame(): Promise<Frame | null> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: this._remoteObject.objectId
objectId: this._remoteObject.objectId,
});
if (typeof nodeInfo.node.frameId !== 'string')
return null;
if (typeof nodeInfo.node.frameId !== 'string') return null;
return this._frameManager.frame(nodeInfo.node.frameId);
}
async _scrollIntoViewIfNeeded(): Promise<void> {
const error = await this.evaluate<Promise<string | false>>(async(element: HTMLElement, pageJavascriptEnabled: boolean) => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
// force-scroll if page's javascript is disabled.
if (!pageJavascriptEnabled) {
// Chrome still supports behavior: instant but it's not in the spec so TS shouts
// We don't want to make this breaking change in Puppeteer yet so we'll ignore the line.
// @ts-ignore
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
const error = await this.evaluate<Promise<string | false>>(
async (element: HTMLElement, pageJavascriptEnabled: boolean) => {
if (!element.isConnected) return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
// force-scroll if page's javascript is disabled.
if (!pageJavascriptEnabled) {
element.scrollIntoView({
block: 'center',
inline: 'center',
// Chrome still supports behavior: instant but it's not in the spec so TS shouts
// We don't want to make this breaking change in Puppeteer yet so we'll ignore the line.
// @ts-ignore
behavior: 'instant',
});
return false;
}
const visibleRatio = await new Promise((resolve) => {
const observer = new IntersectionObserver((entries) => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
observer.observe(element);
});
if (visibleRatio !== 1.0) {
// Chrome still supports behavior: instant but it's not in the spec so TS shouts
// We don't want to make this breaking change in Puppeteer yet so we'll ignore the line.
// @ts-ignore
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
}
return false;
}, this._page._javascriptEnabled);
if (visibleRatio !== 1.0) {
element.scrollIntoView({
block: 'center',
inline: 'center',
// Chrome still supports behavior: instant but it's not in the spec so TS shouts
// We don't want to make this breaking change in Puppeteer yet so we'll ignore the line.
// @ts-ignore
behavior: 'instant',
});
}
return false;
},
this._page._javascriptEnabled
);
if (error)
throw new Error(error);
if (error) throw new Error(error);
}
async _clickablePoint(): Promise<{x: number; y: number}> {
async _clickablePoint(): Promise<{ x: number; y: number }> {
const [result, layoutMetrics] = await Promise.all([
this._client.send('DOM.getContentQuads', {
objectId: this._remoteObject.objectId
}).catch(debugError),
this._client
.send('DOM.getContentQuads', {
objectId: this._remoteObject.objectId,
})
.catch(debugError),
this._client.send('Page.getLayoutMetrics'),
]);
if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Filter out quads that have too small area to click into.
const {clientWidth, clientHeight} = layoutMetrics.layoutViewport;
const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).map(quad => this._intersectQuadWithViewport(quad, clientWidth, clientHeight)).filter(quad => computeQuadArea(quad) > 1);
const { clientWidth, clientHeight } = layoutMetrics.layoutViewport;
const quads = result.quads
.map((quad) => this._fromProtocolQuad(quad))
.map((quad) =>
this._intersectQuadWithViewport(quad, clientWidth, clientHeight)
)
.filter((quad) => computeQuadArea(quad) > 1);
if (!quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Return the middle point of the first quad.
@ -206,27 +255,33 @@ export class ElementHandle extends JSHandle {
}
return {
x: x / 4,
y: y / 4
y: y / 4,
};
}
_getBoxModel(): Promise<void | Protocol.DOM.getBoxModelReturnValue> {
return this._client.send('DOM.getBoxModel', {
objectId: this._remoteObject.objectId
}).catch(error => debugError(error));
return this._client
.send('DOM.getBoxModel', {
objectId: this._remoteObject.objectId,
})
.catch((error) => debugError(error));
}
_fromProtocolQuad(quad: number[]): Array<{x: number; y: number}> {
_fromProtocolQuad(quad: number[]): Array<{ x: number; y: number }> {
return [
{x: quad[0], y: quad[1]},
{x: quad[2], y: quad[3]},
{x: quad[4], y: quad[5]},
{x: quad[6], y: quad[7]}
{ x: quad[0], y: quad[1] },
{ x: quad[2], y: quad[3] },
{ x: quad[4], y: quad[5] },
{ x: quad[6], y: quad[7] },
];
}
_intersectQuadWithViewport(quad: Array<{x: number; y: number}>, width: number, height: number): Array<{x: number; y: number}> {
return quad.map(point => ({
_intersectQuadWithViewport(
quad: Array<{ x: number; y: number }>,
width: number,
height: number
): Array<{ x: number; y: number }> {
return quad.map((point) => ({
x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height),
}));
@ -234,24 +289,35 @@ export class ElementHandle extends JSHandle {
async hover(): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
const { x, y } = await this._clickablePoint();
await this._page.mouse.move(x, y);
}
async click(options: {delay?: number; button?: 'left'|'right'|'middle'; clickCount?: number}): Promise<void> {
async click(options: {
delay?: number;
button?: 'left' | 'right' | 'middle';
clickCount?: number;
}): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
const { x, y } = await this._clickablePoint();
await this._page.mouse.click(x, y, options);
}
async select(...values: string[]): Promise<string[]> {
for (const value of values)
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
assert(
helper.isString(value),
'Values must be strings. Found value "' +
value +
'" of type "' +
typeof value +
'"'
);
/* TODO(jacktfranklin@): once ExecutionContext is TypeScript, and
* its evaluate function is properly typed with generics we can
* return here and remove the typecasting
*/
* its evaluate function is properly typed with generics we can
* return here and remove the typecasting
*/
return this.evaluate((element: HTMLSelectElement, values: string[]) => {
if (element.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
@ -260,18 +326,24 @@ export class ElementHandle extends JSHandle {
element.value = undefined;
for (const option of options) {
option.selected = values.includes(option.value);
if (option.selected && !element.multiple)
break;
if (option.selected && !element.multiple) break;
}
element.dispatchEvent(new Event('input', {bubbles: true}));
element.dispatchEvent(new Event('change', {bubbles: true}));
return options.filter(option => option.selected).map(option => option.value);
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
return options
.filter((option) => option.selected)
.map((option) => option.value);
}, values);
}
async uploadFile(...filePaths: string[]): Promise<void> {
const isMultiple = await this.evaluate<boolean>((element: HTMLInputElement) => element.multiple);
assert(filePaths.length <= 1 || isMultiple, 'Multiple file uploads only work with <input type=file multiple>');
const isMultiple = await this.evaluate<boolean>(
(element: HTMLInputElement) => element.multiple
);
assert(
filePaths.length <= 1 || isMultiple,
'Multiple file uploads only work with <input type=file multiple>'
);
// This import is only needed for `uploadFile`, so keep it scoped here to avoid paying
// the cost unnecessarily.
@ -280,24 +352,26 @@ export class ElementHandle extends JSHandle {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {promisify} = require('util');
const { promisify } = require('util');
const access = promisify(fs.access);
// Locate all files and confirm that they exist.
const files = await Promise.all(filePaths.map(async filePath => {
const resolvedPath: string = path.resolve(filePath);
try {
await access(resolvedPath, fs.constants.R_OK);
} catch (error) {
if (error.code === 'ENOENT')
throw new Error(`${filePath} does not exist or is not readable`);
}
const files = await Promise.all(
filePaths.map(async (filePath) => {
const resolvedPath: string = path.resolve(filePath);
try {
await access(resolvedPath, fs.constants.R_OK);
} catch (error) {
if (error.code === 'ENOENT')
throw new Error(`${filePath} does not exist or is not readable`);
}
return resolvedPath;
}));
const {objectId} = this._remoteObject;
const {node} = await this._client.send('DOM.describeNode', {objectId});
const {backendNodeId} = node;
return resolvedPath;
})
);
const { objectId } = this._remoteObject;
const { node } = await this._client.send('DOM.describeNode', { objectId });
const { backendNodeId } = node;
// The zero-length array is a special case, it seems that DOM.setFileInputFiles does
// not actually update the files in that case, so the solution is to eval the element
@ -307,39 +381,50 @@ export class ElementHandle extends JSHandle {
element.files = new DataTransfer().files;
// Dispatch events for this case because it should behave akin to a user action.
element.dispatchEvent(new Event('input', {bubbles: true}));
element.dispatchEvent(new Event('change', {bubbles: true}));
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
});
} else {
await this._client.send('DOM.setFileInputFiles', {objectId, files, backendNodeId});
await this._client.send('DOM.setFileInputFiles', {
objectId,
files,
backendNodeId,
});
}
}
async tap(): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
const { x, y } = await this._clickablePoint();
await this._page.touchscreen.tap(x, y);
}
async focus(): Promise<void> {
await this.evaluate(element => element.focus());
await this.evaluate((element) => element.focus());
}
async type(text: string, options?: {delay: number}): Promise<void> {
async type(text: string, options?: { delay: number }): Promise<void> {
await this.focus();
await this._page.keyboard.type(text, options);
}
async press(key: KeyInput, options?: {delay?: number; text?: string}): Promise<void> {
async press(
key: KeyInput,
options?: { delay?: number; text?: string }
): Promise<void> {
await this.focus();
await this._page.keyboard.press(key, options);
}
async boundingBox(): Promise<{x: number; y: number; width: number; height: number}> {
async boundingBox(): Promise<{
x: number;
y: number;
width: number;
height: number;
}> {
const result = await this._getBoxModel();
if (!result)
return null;
if (!result) return null;
const quad = result.model.border;
const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
@ -347,7 +432,7 @@ export class ElementHandle extends JSHandle {
const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
return {x, y, width, height};
return { x, y, width, height };
}
/**
@ -356,21 +441,20 @@ export class ElementHandle extends JSHandle {
async boxModel(): Promise<BoxModel | null> {
const result = await this._getBoxModel();
if (!result)
return null;
if (!result) return null;
const {content, padding, border, margin, width, height} = result.model;
const { content, padding, border, margin, width, height } = result.model;
return {
content: this._fromProtocolQuad(content),
padding: this._fromProtocolQuad(padding),
border: this._fromProtocolQuad(border),
margin: this._fromProtocolQuad(margin),
width,
height
height,
};
}
async screenshot(options = {}): Promise<string|Buffer> {
async screenshot(options = {}): Promise<string | Buffer> {
let needsViewportReset = false;
let boundingBox = await this.boundingBox();
@ -378,7 +462,11 @@ export class ElementHandle extends JSHandle {
const viewport = this._page.viewport();
if (viewport && (boundingBox.width > viewport.width || boundingBox.height > viewport.height)) {
if (
viewport &&
(boundingBox.width > viewport.width ||
boundingBox.height > viewport.height)
) {
const newViewport = {
width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
@ -395,97 +483,136 @@ export class ElementHandle extends JSHandle {
assert(boundingBox.width !== 0, 'Node has 0 width.');
assert(boundingBox.height !== 0, 'Node has 0 height.');
const {layoutViewport: {pageX, pageY}} = await this._client.send('Page.getLayoutMetrics');
const {
layoutViewport: { pageX, pageY },
} = await this._client.send('Page.getLayoutMetrics');
const clip = Object.assign({}, boundingBox);
clip.x += pageX;
clip.y += pageY;
const imageData = await this._page.screenshot(Object.assign({}, {
clip
}, options));
const imageData = await this._page.screenshot(
Object.assign(
{},
{
clip,
},
options
)
);
if (needsViewportReset)
await this._page.setViewport(viewport);
if (needsViewportReset) await this._page.setViewport(viewport);
return imageData;
}
async $(selector: string): Promise<ElementHandle | null> {
const defaultHandler = (element: Element, selector: string) => element.querySelector(selector);
const {updatedSelector, queryHandler} = getQueryHandlerAndSelector(selector, defaultHandler);
const defaultHandler = (element: Element, selector: string) =>
element.querySelector(selector);
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
);
const handle = await this.evaluateHandle(queryHandler, updatedSelector);
const element = handle.asElement();
if (element)
return element;
if (element) return element;
await handle.dispose();
return null;
}
async $$(selector: string): Promise<ElementHandle[]> {
const defaultHandler = (element: Element, selector: string) => element.querySelectorAll(selector);
const {updatedSelector, queryHandler} = getQueryHandlerAndSelector(selector, defaultHandler);
const defaultHandler = (element: Element, selector: string) =>
element.querySelectorAll(selector);
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
);
const arrayHandle = await this.evaluateHandle(queryHandler, updatedSelector);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
async $eval<ReturnType extends any>(selector: string, pageFunction: Function|string, ...args: unknown[]): Promise<ReturnType> {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await elementHandle.evaluate<ReturnType>(pageFunction, ...args);
await elementHandle.dispose();
return result;
}
async $$eval<ReturnType extends any>(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
const defaultHandler = (element: Element, selector: string) => Array.from(element.querySelectorAll(selector));
const {updatedSelector, queryHandler} = getQueryHandlerAndSelector(selector, defaultHandler);
const arrayHandle = await this.evaluateHandle(queryHandler, updatedSelector);
const result = await arrayHandle.evaluate<ReturnType>(pageFunction, ...args);
await arrayHandle.dispose();
return result;
}
async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(element, expression) => {
const document = element.ownerDocument || element;
const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
const array = [];
let item;
while ((item = iterator.iterateNext()))
array.push(item);
return array;
},
expression
queryHandler,
updatedSelector
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
if (elementHandle) result.push(elementHandle);
}
return result;
}
async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(
`Error: failed to find element matching selector "${selector}"`
);
const result = await elementHandle.evaluate<ReturnType>(
pageFunction,
...args
);
await elementHandle.dispose();
return result;
}
async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const defaultHandler = (element: Element, selector: string) =>
Array.from(element.querySelectorAll(selector));
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
);
const arrayHandle = await this.evaluateHandle(
queryHandler,
updatedSelector
);
const result = await arrayHandle.evaluate<ReturnType>(
pageFunction,
...args
);
await arrayHandle.dispose();
return result;
}
async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle((element, expression) => {
const document = element.ownerDocument || element;
const iterator = document.evaluate(
expression,
element,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
const array = [];
let item;
while ((item = iterator.iterateNext())) array.push(item);
return array;
}, expression);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle) result.push(elementHandle);
}
return result;
}
async isIntersectingViewport(): Promise<boolean> {
return await this.evaluate<Promise<boolean>>(async element => {
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
return await this.evaluate<Promise<boolean>>(async (element) => {
const visibleRatio = await new Promise((resolve) => {
const observer = new IntersectionObserver((entries) => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
@ -496,7 +623,7 @@ export class ElementHandle extends JSHandle {
}
}
function computeQuadArea(quad: Array<{x: number; y: number}>): number {
function computeQuadArea(quad: Array<{ x: number; y: number }>): number {
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0;

View File

@ -25,15 +25,15 @@ import * as debug from 'debug';
import * as removeFolder from 'rimraf';
import * as childProcess from 'child_process';
import {BrowserFetcher} from './BrowserFetcher';
import {Connection} from './Connection';
import {Browser} from './Browser';
import {helper, assert, debugError} from './helper';
import {TimeoutError} from './Errors';
import type {ConnectionTransport} from './ConnectionTransport';
import {WebSocketTransport} from './WebSocketTransport';
import {PipeTransport} from './PipeTransport';
import type {Viewport} from './PuppeteerViewport';
import { BrowserFetcher } from './BrowserFetcher';
import { Connection } from './Connection';
import { Browser } from './Browser';
import { helper, assert, debugError } from './helper';
import { TimeoutError } from './Errors';
import type { ConnectionTransport } from './ConnectionTransport';
import { WebSocketTransport } from './WebSocketTransport';
import { PipeTransport } from './PipeTransport';
import type { Viewport } from './PuppeteerViewport';
const mkdtempAsync = helper.promisify(fs.mkdtemp);
const removeFolderAsync = helper.promisify(removeFolder);
@ -49,22 +49,22 @@ export interface ProductLauncher {
}
export interface ChromeArgOptions {
headless?: boolean;
args?: string[];
userDataDir?: string;
devtools?: boolean;
headless?: boolean;
args?: string[];
userDataDir?: string;
devtools?: boolean;
}
export interface LaunchOptions {
executablePath?: string;
ignoreDefaultArgs?: boolean|string[];
handleSIGINT?: boolean;
handleSIGTERM?: boolean;
handleSIGHUP?: boolean;
timeout?: number;
dumpio?: boolean;
env?: Record<string, string | undefined>;
pipe?: boolean;
executablePath?: string;
ignoreDefaultArgs?: boolean | string[];
handleSIGINT?: boolean;
handleSIGTERM?: boolean;
handleSIGHUP?: boolean;
timeout?: number;
dumpio?: boolean;
env?: Record<string, string | undefined>;
pipe?: boolean;
}
export interface BrowserOptions {
@ -85,7 +85,11 @@ class BrowserRunner {
_listeners = [];
_processClosing: Promise<void>;
constructor(executablePath: string, processArguments: string[], tempDirectory?: string) {
constructor(
executablePath: string,
processArguments: string[],
tempDirectory?: string
) {
this._executablePath = executablePath;
this._processArguments = processArguments;
this._tempDirectory = tempDirectory;
@ -98,65 +102,75 @@ class BrowserRunner {
handleSIGHUP,
dumpio,
env,
pipe
pipe,
} = options;
let stdio: Array<'ignore'|'pipe'> = ['pipe', 'pipe', 'pipe'];
let stdio: Array<'ignore' | 'pipe'> = ['pipe', 'pipe', 'pipe'];
if (pipe) {
if (dumpio)
stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
else
stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
if (dumpio) stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
else stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
}
assert(!this.proc, 'This process has previously been started.');
debugLauncher(`Calling ${this._executablePath} ${this._processArguments.join(' ')}`);
debugLauncher(
`Calling ${this._executablePath} ${this._processArguments.join(' ')}`
);
this.proc = childProcess.spawn(
this._executablePath,
this._processArguments,
{
// On non-windows platforms, `detached: true` makes child process a leader of a new
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
detached: process.platform !== 'win32',
env,
stdio
}
this._executablePath,
this._processArguments,
{
// On non-windows platforms, `detached: true` makes child process a leader of a new
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
detached: process.platform !== 'win32',
env,
stdio,
}
);
if (dumpio) {
this.proc.stderr.pipe(process.stderr);
this.proc.stdout.pipe(process.stdout);
}
this._closed = false;
this._processClosing = new Promise(fulfill => {
this._processClosing = new Promise((fulfill) => {
this.proc.once('exit', () => {
this._closed = true;
// Cleanup as processes exit.
if (this._tempDirectory) {
removeFolderAsync(this._tempDirectory)
.then(() => fulfill())
.catch(error => console.error(error));
.then(() => fulfill())
.catch((error) => console.error(error));
} else {
fulfill();
}
});
});
this._listeners = [ helper.addEventListener(process, 'exit', this.kill.bind(this)) ];
this._listeners = [
helper.addEventListener(process, 'exit', this.kill.bind(this)),
];
if (handleSIGINT)
this._listeners.push(helper.addEventListener(process, 'SIGINT', () => { this.kill(); process.exit(130); }));
this._listeners.push(
helper.addEventListener(process, 'SIGINT', () => {
this.kill();
process.exit(130);
})
);
if (handleSIGTERM)
this._listeners.push(helper.addEventListener(process, 'SIGTERM', this.close.bind(this)));
this._listeners.push(
helper.addEventListener(process, 'SIGTERM', this.close.bind(this))
);
if (handleSIGHUP)
this._listeners.push(helper.addEventListener(process, 'SIGHUP', this.close.bind(this)));
this._listeners.push(
helper.addEventListener(process, 'SIGHUP', this.close.bind(this))
);
}
close(): Promise<void> {
if (this._closed)
return Promise.resolve();
if (this._closed) return Promise.resolve();
helper.removeEventListeners(this._listeners);
if (this._tempDirectory) {
this.kill();
} else if (this.connection) {
// Attempt to close the browser gracefully
this.connection.send('Browser.close').catch(error => {
this.connection.send('Browser.close').catch((error) => {
debugError(error);
this.kill();
});
@ -170,8 +184,7 @@ class BrowserRunner {
try {
if (process.platform === 'win32')
childProcess.execSync(`taskkill /pid ${this.proc.pid} /T /F`);
else
process.kill(-this.proc.pid, 'SIGKILL');
else process.kill(-this.proc.pid, 'SIGKILL');
} catch (error) {
// the process might have already stopped
}
@ -179,7 +192,7 @@ class BrowserRunner {
// Attempt to remove temporary profile directory to avoid littering.
try {
removeFolder.sync(this._tempDirectory);
} catch (error) { }
} catch (error) {}
}
/**
@ -193,20 +206,22 @@ class BrowserRunner {
slowMo: number;
preferredRevision: string;
}): Promise<Connection> {
const {
usePipe,
timeout,
slowMo,
preferredRevision
} = options;
const { usePipe, timeout, slowMo, preferredRevision } = options;
if (!usePipe) {
const browserWSEndpoint = await waitForWSEndpoint(this.proc, timeout, preferredRevision);
const browserWSEndpoint = await waitForWSEndpoint(
this.proc,
timeout,
preferredRevision
);
const transport = await WebSocketTransport.create(browserWSEndpoint);
this.connection = new Connection(browserWSEndpoint, transport, slowMo);
} else {
// stdio was assigned during start(), and the 'pipe' option there adds the 4th and 5th items to stdio array
const {3: pipeWrite, 4: pipeRead} = this.proc.stdio;
const transport = new PipeTransport(pipeWrite as NodeJS.WritableStream, pipeRead as NodeJS.ReadableStream);
const { 3: pipeWrite, 4: pipeRead } = this.proc.stdio;
const transport = new PipeTransport(
pipeWrite as NodeJS.WritableStream,
pipeRead as NodeJS.ReadableStream
);
this.connection = new Connection('', transport, slowMo);
}
return this.connection;
@ -218,13 +233,19 @@ class ChromeLauncher implements ProductLauncher {
_preferredRevision: string;
_isPuppeteerCore: boolean;
constructor(projectRoot: string, preferredRevision: string, isPuppeteerCore: boolean) {
constructor(
projectRoot: string,
preferredRevision: string,
isPuppeteerCore: boolean
) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
async launch(options: LaunchOptions & ChromeArgOptions & BrowserOptions = {}): Promise<Browser> {
async launch(
options: LaunchOptions & ChromeArgOptions & BrowserOptions = {}
): Promise<Browser> {
const {
ignoreDefaultArgs = false,
args = [],
@ -236,45 +257,75 @@ class ChromeLauncher implements ProductLauncher {
handleSIGTERM = true,
handleSIGHUP = true,
ignoreHTTPSErrors = false,
defaultViewport = {width: 800, height: 600},
defaultViewport = { width: 800, height: 600 },
slowMo = 0,
timeout = 30000
timeout = 30000,
} = options;
const profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-');
const chromeArguments = [];
if (!ignoreDefaultArgs)
chromeArguments.push(...this.defaultArgs(options));
if (!ignoreDefaultArgs) chromeArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
chromeArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg)));
else
chromeArguments.push(...args);
chromeArguments.push(
...this.defaultArgs(options).filter(
(arg) => !ignoreDefaultArgs.includes(arg)
)
);
else chromeArguments.push(...args);
let temporaryUserDataDir = null;
if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) {
if (
!chromeArguments.some((argument) =>
argument.startsWith('--remote-debugging-')
)
)
chromeArguments.push(
pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0'
);
if (!chromeArguments.some((arg) => arg.startsWith('--user-data-dir'))) {
temporaryUserDataDir = await mkdtempAsync(profilePath);
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
}
let chromeExecutable = executablePath;
if (!executablePath) {
const {missingText, executablePath} = resolveExecutablePath(this);
if (missingText)
throw new Error(missingText);
const { missingText, executablePath } = resolveExecutablePath(this);
if (missingText) throw new Error(missingText);
chromeExecutable = executablePath;
}
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
const runner = new BrowserRunner(chromeExecutable, chromeArguments, temporaryUserDataDir);
runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe: usePipe});
const runner = new BrowserRunner(
chromeExecutable,
chromeArguments,
temporaryUserDataDir
);
runner.start({
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe: usePipe,
});
try {
const connection = await runner.setupConnection({usePipe, timeout, slowMo, preferredRevision: this._preferredRevision});
const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
await browser.waitForTarget(t => t.type() === 'page');
const connection = await runner.setupConnection({
usePipe,
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
const browser = await Browser.create(
connection,
[],
ignoreHTTPSErrors,
defaultViewport,
runner.proc,
runner.close.bind(runner)
);
await browser.waitForTarget((t) => t.type() === 'page');
return browser;
} catch (error) {
runner.kill();
@ -316,20 +367,14 @@ class ChromeLauncher implements ProductLauncher {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null
userDataDir = null,
} = options;
if (userDataDir)
chromeArguments.push(`--user-data-dir=${userDataDir}`);
if (devtools)
chromeArguments.push('--auto-open-devtools-for-tabs');
if (userDataDir) chromeArguments.push(`--user-data-dir=${userDataDir}`);
if (devtools) chromeArguments.push('--auto-open-devtools-for-tabs');
if (headless) {
chromeArguments.push(
'--headless',
'--hide-scrollbars',
'--mute-audio'
);
chromeArguments.push('--headless', '--hide-scrollbars', '--mute-audio');
}
if (args.every(arg => arg.startsWith('-')))
if (args.every((arg) => arg.startsWith('-')))
chromeArguments.push('about:blank');
chromeArguments.push(...args);
return chromeArguments;
@ -343,38 +388,62 @@ class ChromeLauncher implements ProductLauncher {
return 'chrome';
}
async connect(options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}): Promise<Browser> {
async connect(
options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}
): Promise<Browser> {
const {
browserWSEndpoint,
browserURL,
ignoreHTTPSErrors = false,
defaultViewport = {width: 800, height: 600},
defaultViewport = { width: 800, height: 600 },
transport,
slowMo = 0,
} = options;
assert(Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect');
assert(
Number(!!browserWSEndpoint) +
Number(!!browserURL) +
Number(!!transport) ===
1,
'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect'
);
let connection = null;
if (transport) {
connection = new Connection('', transport, slowMo);
} else if (browserWSEndpoint) {
const connectionTransport = await WebSocketTransport.create(browserWSEndpoint);
connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
const connectionTransport = await WebSocketTransport.create(
browserWSEndpoint
);
connection = new Connection(
browserWSEndpoint,
connectionTransport,
slowMo
);
} else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL);
const connectionTransport = await WebSocketTransport.create(connectionURL);
const connectionTransport = await WebSocketTransport.create(
connectionURL
);
connection = new Connection(connectionURL, connectionTransport, slowMo);
}
const {browserContextIds} = await connection.send('Target.getBrowserContexts');
return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
const { browserContextIds } = await connection.send(
'Target.getBrowserContexts'
);
return Browser.create(
connection,
browserContextIds,
ignoreHTTPSErrors,
defaultViewport,
null,
() => connection.send('Browser.close').catch(debugError)
);
}
}
class FirefoxLauncher implements ProductLauncher {
@ -382,15 +451,23 @@ class FirefoxLauncher implements ProductLauncher {
_preferredRevision: string;
_isPuppeteerCore: boolean;
constructor(projectRoot: string, preferredRevision: string, isPuppeteerCore: boolean) {
constructor(
projectRoot: string,
preferredRevision: string,
isPuppeteerCore: boolean
) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
async launch(options: LaunchOptions & ChromeArgOptions & BrowserOptions & {
extraPrefsFirefox?: {[x: string]: unknown};
} = {}): Promise<Browser> {
async launch(
options: LaunchOptions &
ChromeArgOptions &
BrowserOptions & {
extraPrefsFirefox?: { [x: string]: unknown };
} = {}
): Promise<Browser> {
const {
ignoreDefaultArgs = false,
args = [],
@ -402,26 +479,35 @@ class FirefoxLauncher implements ProductLauncher {
handleSIGTERM = true,
handleSIGHUP = true,
ignoreHTTPSErrors = false,
defaultViewport = {width: 800, height: 600},
defaultViewport = { width: 800, height: 600 },
slowMo = 0,
timeout = 30000,
extraPrefsFirefox = {}
extraPrefsFirefox = {},
} = options;
const firefoxArguments = [];
if (!ignoreDefaultArgs)
firefoxArguments.push(...this.defaultArgs(options));
if (!ignoreDefaultArgs) firefoxArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
firefoxArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg)));
else
firefoxArguments.push(...args);
firefoxArguments.push(
...this.defaultArgs(options).filter(
(arg) => !ignoreDefaultArgs.includes(arg)
)
);
else firefoxArguments.push(...args);
if (!firefoxArguments.some(argument => argument.startsWith('--remote-debugging-')))
if (
!firefoxArguments.some((argument) =>
argument.startsWith('--remote-debugging-')
)
)
firefoxArguments.push('--remote-debugging-port=0');
let temporaryUserDataDir = null;
if (!firefoxArguments.includes('-profile') && !firefoxArguments.includes('--profile')) {
if (
!firefoxArguments.includes('-profile') &&
!firefoxArguments.includes('--profile')
) {
temporaryUserDataDir = await this._createProfile(extraPrefsFirefox);
firefoxArguments.push('--profile');
firefoxArguments.push(temporaryUserDataDir);
@ -430,19 +516,41 @@ class FirefoxLauncher implements ProductLauncher {
await this._updateRevision();
let firefoxExecutable = executablePath;
if (!executablePath) {
const {missingText, executablePath} = resolveExecutablePath(this);
if (missingText)
throw new Error(missingText);
const { missingText, executablePath } = resolveExecutablePath(this);
if (missingText) throw new Error(missingText);
firefoxExecutable = executablePath;
}
const runner = new BrowserRunner(firefoxExecutable, firefoxArguments, temporaryUserDataDir);
runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe});
const runner = new BrowserRunner(
firefoxExecutable,
firefoxArguments,
temporaryUserDataDir
);
runner.start({
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe,
});
try {
const connection = await runner.setupConnection({usePipe: pipe, timeout, slowMo, preferredRevision: this._preferredRevision});
const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
await browser.waitForTarget(t => t.type() === 'page');
const connection = await runner.setupConnection({
usePipe: pipe,
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
const browser = await Browser.create(
connection,
[],
ignoreHTTPSErrors,
defaultViewport,
runner.proc,
runner.close.bind(runner)
);
await browser.waitForTarget((t) => t.type() === 'page');
return browser;
} catch (error) {
runner.kill();
@ -450,36 +558,61 @@ class FirefoxLauncher implements ProductLauncher {
}
}
async connect(options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}): Promise<Browser> {
async connect(
options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}
): Promise<Browser> {
const {
browserWSEndpoint,
browserURL,
ignoreHTTPSErrors = false,
defaultViewport = {width: 800, height: 600},
defaultViewport = { width: 800, height: 600 },
transport,
slowMo = 0,
} = options;
assert(Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect');
assert(
Number(!!browserWSEndpoint) +
Number(!!browserURL) +
Number(!!transport) ===
1,
'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect'
);
let connection = null;
if (transport) {
connection = new Connection('', transport, slowMo);
} else if (browserWSEndpoint) {
const connectionTransport = await WebSocketTransport.create(browserWSEndpoint);
connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
const connectionTransport = await WebSocketTransport.create(
browserWSEndpoint
);
connection = new Connection(
browserWSEndpoint,
connectionTransport,
slowMo
);
} else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL);
const connectionTransport = await WebSocketTransport.create(connectionURL);
const connectionTransport = await WebSocketTransport.create(
connectionURL
);
connection = new Connection(connectionURL, connectionTransport, slowMo);
}
const {browserContextIds} = await connection.send('Target.getBrowserContexts');
return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
const { browserContextIds } = await connection.send(
'Target.getBrowserContexts'
);
return Browser.create(
connection,
browserContextIds,
ignoreHTTPSErrors,
defaultViewport,
null,
() => connection.send('Browser.close').catch(debugError)
);
}
executablePath(): string {
@ -489,10 +622,11 @@ class FirefoxLauncher implements ProductLauncher {
async _updateRevision(): Promise<void> {
// replace 'latest' placeholder with actual downloaded revision
if (this._preferredRevision === 'latest') {
const browserFetcher = new BrowserFetcher(this._projectRoot, {product: this.product});
const browserFetcher = new BrowserFetcher(this._projectRoot, {
product: this.product,
});
const localRevisions = await browserFetcher.localRevisions();
if (localRevisions[0])
this._preferredRevision = localRevisions[0];
if (localRevisions[0]) this._preferredRevision = localRevisions[0];
}
}
@ -501,32 +635,29 @@ class FirefoxLauncher implements ProductLauncher {
}
defaultArgs(options: ChromeArgOptions = {}): string[] {
const firefoxArguments = [
'--no-remote',
'--foreground',
];
const firefoxArguments = ['--no-remote', '--foreground'];
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null
userDataDir = null,
} = options;
if (userDataDir) {
firefoxArguments.push('--profile');
firefoxArguments.push(userDataDir);
}
if (headless)
firefoxArguments.push('--headless');
if (devtools)
firefoxArguments.push('--devtools');
if (args.every(arg => arg.startsWith('-')))
if (headless) firefoxArguments.push('--headless');
if (devtools) firefoxArguments.push('--devtools');
if (args.every((arg) => arg.startsWith('-')))
firefoxArguments.push('about:blank');
firefoxArguments.push(...args);
return firefoxArguments;
}
async _createProfile(extraPrefs: { [x: string]: unknown}): Promise<string> {
const profilePath = await mkdtempAsync(path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-'));
async _createProfile(extraPrefs: { [x: string]: unknown }): Promise<string> {
const profilePath = await mkdtempAsync(
path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-')
);
const prefsJS = [];
const userJS = [];
const server = 'dummy.test';
@ -543,8 +674,8 @@ class FirefoxLauncher implements ProductLauncher {
// Prevent various error message on the console
// jest-puppeteer asserts that no error message is emitted by the console
'browser.contentblocking.features.standard': '-tp,tpPrivate,cookieBehavior0,-cm,-fp',
'browser.contentblocking.features.standard':
'-tp,tpPrivate,cookieBehavior0,-cm,-fp',
// Enable the dump function: which sends messages to the system
// console
@ -724,28 +855,37 @@ class FirefoxLauncher implements ProductLauncher {
'toolkit.telemetry.server': `https://${server}/dummy/telemetry/`,
// Prevent starting into safe mode after application crashes
'toolkit.startup.max_resumed_crashes': -1,
};
Object.assign(defaultPreferences, extraPrefs);
for (const [key, value] of Object.entries(defaultPreferences))
userJS.push(`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`);
userJS.push(
`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`
);
await writeFileAsync(path.join(profilePath, 'user.js'), userJS.join('\n'));
await writeFileAsync(path.join(profilePath, 'prefs.js'), prefsJS.join('\n'));
await writeFileAsync(
path.join(profilePath, 'prefs.js'),
prefsJS.join('\n')
);
return profilePath;
}
}
function waitForWSEndpoint(browserProcess: childProcess.ChildProcess, timeout: number, preferredRevision: string): Promise<string> {
function waitForWSEndpoint(
browserProcess: childProcess.ChildProcess,
timeout: number,
preferredRevision: string
): Promise<string> {
return new Promise((resolve, reject) => {
const rl = readline.createInterface({input: browserProcess.stderr});
const rl = readline.createInterface({ input: browserProcess.stderr });
let stderr = '';
const listeners = [
helper.addEventListener(rl, 'line', onLine),
helper.addEventListener(rl, 'close', () => onClose()),
helper.addEventListener(browserProcess, 'exit', () => onClose()),
helper.addEventListener(browserProcess, 'error', error => onClose(error))
helper.addEventListener(browserProcess, 'error', (error) =>
onClose(error)
),
];
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
@ -754,32 +894,39 @@ function waitForWSEndpoint(browserProcess: childProcess.ChildProcess, timeout: n
*/
function onClose(error?: Error): void {
cleanup();
reject(new Error([
'Failed to launch the browser process!' + (error ? ' ' + error.message : ''),
stderr,
'',
'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md',
'',
].join('\n')));
reject(
new Error(
[
'Failed to launch the browser process!' +
(error ? ' ' + error.message : ''),
stderr,
'',
'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md',
'',
].join('\n')
)
);
}
function onTimeout(): void {
cleanup();
reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.`));
reject(
new TimeoutError(
`Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.`
)
);
}
function onLine(line: string): void {
stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match)
return;
if (!match) return;
cleanup();
resolve(match[1]);
}
function cleanup(): void {
if (timeoutId)
clearTimeout(timeoutId);
if (timeoutId) clearTimeout(timeoutId);
helper.removeEventListeners(listeners);
}
});
@ -787,12 +934,17 @@ function waitForWSEndpoint(browserProcess: childProcess.ChildProcess, timeout: n
function getWSEndpoint(browserURL: string): Promise<string> {
let resolve, reject;
const promise = new Promise<string>((res, rej) => { resolve = res; reject = rej; });
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
const endpointURL = URL.resolve(browserURL, '/json/version');
const protocol = endpointURL.startsWith('https') ? https : http;
const requestOptions = Object.assign(URL.parse(endpointURL), {method: 'GET'});
const request = protocol.request(requestOptions, res => {
const requestOptions = Object.assign(URL.parse(endpointURL), {
method: 'GET',
});
const request = protocol.request(requestOptions, (res) => {
let data = '';
if (res.statusCode !== 200) {
// Consume response data to free up memory.
@ -801,52 +953,85 @@ function getWSEndpoint(browserURL: string): Promise<string> {
return;
}
res.setEncoding('utf8');
res.on('data', chunk => data += chunk);
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(JSON.parse(data).webSocketDebuggerUrl));
});
request.on('error', reject);
request.end();
return promise.catch(error => {
error.message = `Failed to fetch browser webSocket url from ${endpointURL}: ` + error.message;
return promise.catch((error) => {
error.message =
`Failed to fetch browser webSocket url from ${endpointURL}: ` +
error.message;
throw error;
});
}
function resolveExecutablePath(launcher: ChromeLauncher | FirefoxLauncher): {executablePath: string; missingText?: string} {
function resolveExecutablePath(
launcher: ChromeLauncher | FirefoxLauncher
): { executablePath: string; missingText?: string } {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!launcher._isPuppeteerCore) {
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.npm_config_puppeteer_executable_path || process.env.npm_package_config_puppeteer_executable_path;
const executablePath =
process.env.PUPPETEER_EXECUTABLE_PATH ||
process.env.npm_config_puppeteer_executable_path ||
process.env.npm_package_config_puppeteer_executable_path;
if (executablePath) {
const missingText = !fs.existsSync(executablePath) ? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' + executablePath : null;
return {executablePath, missingText};
const missingText = !fs.existsSync(executablePath)
? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' +
executablePath
: null;
return { executablePath, missingText };
}
}
const browserFetcher = new BrowserFetcher(launcher._projectRoot, {product: launcher.product});
const browserFetcher = new BrowserFetcher(launcher._projectRoot, {
product: launcher.product,
});
if (!launcher._isPuppeteerCore && launcher.product === 'chrome') {
const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
if (revision) {
const revisionInfo = browserFetcher.revisionInfo(revision);
const missingText = !revisionInfo.local ? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' + revisionInfo.executablePath : null;
return {executablePath: revisionInfo.executablePath, missingText};
const missingText = !revisionInfo.local
? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' +
revisionInfo.executablePath
: null;
return { executablePath: revisionInfo.executablePath, missingText };
}
}
const revisionInfo = browserFetcher.revisionInfo(launcher._preferredRevision);
const missingText = !revisionInfo.local ? `Could not find browser revision ${launcher._preferredRevision}. Run "npm install" or "yarn install" to download a browser binary.` : null;
return {executablePath: revisionInfo.executablePath, missingText};
const missingText = !revisionInfo.local
? `Could not find browser revision ${launcher._preferredRevision}. Run "npm install" or "yarn install" to download a browser binary.`
: null;
return { executablePath: revisionInfo.executablePath, missingText };
}
function Launcher(projectRoot: string, preferredRevision: string, isPuppeteerCore: boolean, product?: string): ProductLauncher {
function Launcher(
projectRoot: string,
preferredRevision: string,
isPuppeteerCore: boolean,
product?: string
): ProductLauncher {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!product && !isPuppeteerCore)
product = process.env.PUPPETEER_PRODUCT || process.env.npm_config_puppeteer_product || process.env.npm_package_config_puppeteer_product;
product =
process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product;
switch (product) {
case 'firefox':
return new FirefoxLauncher(projectRoot, preferredRevision, isPuppeteerCore);
return new FirefoxLauncher(
projectRoot,
preferredRevision,
isPuppeteerCore
);
case 'chrome':
default:
return new ChromeLauncher(projectRoot, preferredRevision, isPuppeteerCore);
return new ChromeLauncher(
projectRoot,
preferredRevision,
isPuppeteerCore
);
}
}

View File

@ -14,23 +14,33 @@
* limitations under the License.
*/
import {helper, assert, PuppeteerEventListener} from './helper';
import {Events} from './Events';
import {TimeoutError} from './Errors';
import {FrameManager, Frame} from './FrameManager';
import {Request, Response} from './NetworkManager';
import { helper, assert, PuppeteerEventListener } from './helper';
import { Events } from './Events';
import { TimeoutError } from './Errors';
import { FrameManager, Frame } from './FrameManager';
import { Request, Response } from './NetworkManager';
export type PuppeteerLifeCycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
type ProtocolLifeCycleEvent = 'load' | 'DOMContentLoaded' | 'networkIdle' | 'networkAlmostIdle';
export type PuppeteerLifeCycleEvent =
| 'load'
| 'domcontentloaded'
| 'networkidle0'
| 'networkidle2';
type ProtocolLifeCycleEvent =
| 'load'
| 'DOMContentLoaded'
| 'networkIdle'
| 'networkAlmostIdle';
const puppeteerToProtocolLifecycle = new Map<PuppeteerLifeCycleEvent, ProtocolLifeCycleEvent>([
const puppeteerToProtocolLifecycle = new Map<
PuppeteerLifeCycleEvent,
ProtocolLifeCycleEvent
>([
['load', 'load'],
['domcontentloaded', 'DOMContentLoaded'],
['networkidle0', 'networkIdle'],
['networkidle2', 'networkAlmostIdle'],
]);
export class LifecycleWatcher {
_expectedLifecycle: ProtocolLifeCycleEvent[];
_frameManager: FrameManager;
@ -40,7 +50,6 @@ export class LifecycleWatcher {
_eventListeners: PuppeteerEventListener[];
_initialLoaderId: string;
_sameDocumentNavigationPromise: Promise<Error | null>;
_sameDocumentNavigationCompleteCallback: (x?: Error) => void;
@ -58,12 +67,15 @@ export class LifecycleWatcher {
_maximumTimer?: NodeJS.Timeout;
_hasSameDocumentNavigation?: boolean;
constructor(frameManager: FrameManager, frame: Frame, waitUntil: PuppeteerLifeCycleEvent|PuppeteerLifeCycleEvent[], timeout: number) {
if (Array.isArray(waitUntil))
waitUntil = waitUntil.slice();
else if (typeof waitUntil === 'string')
waitUntil = [waitUntil];
this._expectedLifecycle = waitUntil.map(value => {
constructor(
frameManager: FrameManager,
frame: Frame,
waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[],
timeout: number
) {
if (Array.isArray(waitUntil)) waitUntil = waitUntil.slice();
else if (typeof waitUntil === 'string') waitUntil = [waitUntil];
this._expectedLifecycle = waitUntil.map((value) => {
const protocolEvent = puppeteerToProtocolLifecycle.get(value);
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
return protocolEvent;
@ -75,27 +87,52 @@ export class LifecycleWatcher {
this._timeout = timeout;
this._navigationRequest = null;
this._eventListeners = [
helper.addEventListener(frameManager._client, Events.CDPSession.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))),
helper.addEventListener(this._frameManager, Events.FrameManager.LifecycleEvent, this._checkLifecycleComplete.bind(this)),
helper.addEventListener(this._frameManager, Events.FrameManager.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)),
helper.addEventListener(this._frameManager, Events.FrameManager.FrameDetached, this._onFrameDetached.bind(this)),
helper.addEventListener(this._frameManager.networkManager(), Events.NetworkManager.Request, this._onRequest.bind(this)),
helper.addEventListener(
frameManager._client,
Events.CDPSession.Disconnected,
() =>
this._terminate(
new Error('Navigation failed because browser has disconnected!')
)
),
helper.addEventListener(
this._frameManager,
Events.FrameManager.LifecycleEvent,
this._checkLifecycleComplete.bind(this)
),
helper.addEventListener(
this._frameManager,
Events.FrameManager.FrameNavigatedWithinDocument,
this._navigatedWithinDocument.bind(this)
),
helper.addEventListener(
this._frameManager,
Events.FrameManager.FrameDetached,
this._onFrameDetached.bind(this)
),
helper.addEventListener(
this._frameManager.networkManager(),
Events.NetworkManager.Request,
this._onRequest.bind(this)
),
];
this._sameDocumentNavigationPromise = new Promise<Error | null>(fulfill => {
this._sameDocumentNavigationCompleteCallback = fulfill;
});
this._sameDocumentNavigationPromise = new Promise<Error | null>(
(fulfill) => {
this._sameDocumentNavigationCompleteCallback = fulfill;
}
);
this._lifecyclePromise = new Promise(fulfill => {
this._lifecyclePromise = new Promise((fulfill) => {
this._lifecycleCallback = fulfill;
});
this._newDocumentNavigationPromise = new Promise(fulfill => {
this._newDocumentNavigationPromise = new Promise((fulfill) => {
this._newDocumentNavigationCompleteCallback = fulfill;
});
this._timeoutPromise = this._createTimeoutPromise();
this._terminationPromise = new Promise(fulfill => {
this._terminationPromise = new Promise((fulfill) => {
this._terminationCallback = fulfill;
});
this._checkLifecycleComplete();
@ -109,7 +146,10 @@ export class LifecycleWatcher {
_onFrameDetached(frame: Frame): void {
if (this._frame === frame) {
this._terminationCallback.call(null, new Error('Navigating frame was detached'));
this._terminationCallback.call(
null,
new Error('Navigating frame was detached')
);
return;
}
this._checkLifecycleComplete();
@ -140,26 +180,28 @@ export class LifecycleWatcher {
}
_createTimeoutPromise(): Promise<TimeoutError | null> {
if (!this._timeout)
return new Promise(() => {});
const errorMessage = 'Navigation timeout of ' + this._timeout + ' ms exceeded';
return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout))
.then(() => new TimeoutError(errorMessage));
if (!this._timeout) return new Promise(() => {});
const errorMessage =
'Navigation timeout of ' + this._timeout + ' ms exceeded';
return new Promise(
(fulfill) => (this._maximumTimer = setTimeout(fulfill, this._timeout))
).then(() => new TimeoutError(errorMessage));
}
_navigatedWithinDocument(frame: Frame): void {
if (frame !== this._frame)
return;
if (frame !== this._frame) return;
this._hasSameDocumentNavigation = true;
this._checkLifecycleComplete();
}
_checkLifecycleComplete(): void {
// We expect navigation to commit.
if (!checkLifecycle(this._frame, this._expectedLifecycle))
return;
if (!checkLifecycle(this._frame, this._expectedLifecycle)) return;
this._lifecycleCallback();
if (this._frame._loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation)
if (
this._frame._loaderId === this._initialLoaderId &&
!this._hasSameDocumentNavigation
)
return;
if (this._hasSameDocumentNavigation)
this._sameDocumentNavigationCompleteCallback();
@ -171,14 +213,15 @@ export class LifecycleWatcher {
* @param {!Array<string>} expectedLifecycle
* @return {boolean}
*/
function checkLifecycle(frame: Frame, expectedLifecycle: ProtocolLifeCycleEvent[]): boolean {
function checkLifecycle(
frame: Frame,
expectedLifecycle: ProtocolLifeCycleEvent[]
): boolean {
for (const event of expectedLifecycle) {
if (!frame._lifecycleEvents.has(event))
return false;
if (!frame._lifecycleEvents.has(event)) return false;
}
for (const child of frame.childFrames()) {
if (!checkLifecycle(child, expectedLifecycle))
return false;
if (!checkLifecycle(child, expectedLifecycle)) return false;
}
return true;
}

View File

@ -14,10 +14,10 @@
* limitations under the License.
*/
import * as EventEmitter from 'events';
import {helper, assert, debugError} from './helper';
import {Events} from './Events';
import {CDPSession} from './Connection';
import {FrameManager, Frame} from './FrameManager';
import { helper, assert, debugError } from './helper';
import { Events } from './Events';
import { CDPSession } from './Connection';
import { FrameManager, Frame } from './FrameManager';
export interface Credentials {
username: string;
@ -29,7 +29,10 @@ export class NetworkManager extends EventEmitter {
_ignoreHTTPSErrors: boolean;
_frameManager: FrameManager;
_requestIdToRequest = new Map<string, Request>();
_requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
_requestIdToRequestWillBeSentEvent = new Map<
string,
Protocol.Network.requestWillBeSentPayload
>();
_extraHTTPHeaders: Record<string, string> = {};
_offline = false;
_credentials?: Credentials = null;
@ -39,7 +42,11 @@ export class NetworkManager extends EventEmitter {
_userCacheDisabled = false;
_requestIdToInterceptionId = new Map<string, string>();
constructor(client: CDPSession, ignoreHTTPSErrors: boolean, frameManager: FrameManager) {
constructor(
client: CDPSession,
ignoreHTTPSErrors: boolean,
frameManager: FrameManager
) {
super();
this._client = client;
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
@ -47,17 +54,31 @@ export class NetworkManager extends EventEmitter {
this._client.on('Fetch.requestPaused', this._onRequestPaused.bind(this));
this._client.on('Fetch.authRequired', this._onAuthRequired.bind(this));
this._client.on('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this));
this._client.on('Network.requestServedFromCache', this._onRequestServedFromCache.bind(this));
this._client.on('Network.responseReceived', this._onResponseReceived.bind(this));
this._client.on('Network.loadingFinished', this._onLoadingFinished.bind(this));
this._client.on(
'Network.requestWillBeSent',
this._onRequestWillBeSent.bind(this)
);
this._client.on(
'Network.requestServedFromCache',
this._onRequestServedFromCache.bind(this)
);
this._client.on(
'Network.responseReceived',
this._onResponseReceived.bind(this)
);
this._client.on(
'Network.loadingFinished',
this._onLoadingFinished.bind(this)
);
this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
}
async initialize(): Promise<void> {
await this._client.send('Network.enable');
if (this._ignoreHTTPSErrors)
await this._client.send('Security.setIgnoreCertificateErrors', {ignore: true});
await this._client.send('Security.setIgnoreCertificateErrors', {
ignore: true,
});
}
async authenticate(credentials?: Credentials): Promise<void> {
@ -65,14 +86,21 @@ export class NetworkManager extends EventEmitter {
await this._updateProtocolRequestInterception();
}
async setExtraHTTPHeaders(extraHTTPHeaders: Record<string, string>): Promise<void> {
async setExtraHTTPHeaders(
extraHTTPHeaders: Record<string, string>
): Promise<void> {
this._extraHTTPHeaders = {};
for (const key of Object.keys(extraHTTPHeaders)) {
const value = extraHTTPHeaders[key];
assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
assert(
helper.isString(value),
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`
);
this._extraHTTPHeaders[key.toLowerCase()] = value;
}
await this._client.send('Network.setExtraHTTPHeaders', {headers: this._extraHTTPHeaders});
await this._client.send('Network.setExtraHTTPHeaders', {
headers: this._extraHTTPHeaders,
});
}
extraHTTPHeaders(): Record<string, string> {
@ -80,20 +108,19 @@ export class NetworkManager extends EventEmitter {
}
async setOfflineMode(value: boolean): Promise<void> {
if (this._offline === value)
return;
if (this._offline === value) return;
this._offline = value;
await this._client.send('Network.emulateNetworkConditions', {
offline: this._offline,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1
uploadThroughput: -1,
});
}
async setUserAgent(userAgent: string): Promise<void> {
await this._client.send('Network.setUserAgentOverride', {userAgent});
await this._client.send('Network.setUserAgentOverride', { userAgent });
}
async setCacheEnabled(enabled: boolean): Promise<void> {
@ -108,34 +135,37 @@ export class NetworkManager extends EventEmitter {
async _updateProtocolRequestInterception(): Promise<void> {
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
if (enabled === this._protocolRequestInterceptionEnabled)
return;
if (enabled === this._protocolRequestInterceptionEnabled) return;
this._protocolRequestInterceptionEnabled = enabled;
if (enabled) {
await Promise.all([
this._updateProtocolCacheDisabled(),
this._client.send('Fetch.enable', {
handleAuthRequests: true,
patterns: [{urlPattern: '*'}],
patterns: [{ urlPattern: '*' }],
}),
]);
} else {
await Promise.all([
this._updateProtocolCacheDisabled(),
this._client.send('Fetch.disable')
this._client.send('Fetch.disable'),
]);
}
}
async _updateProtocolCacheDisabled(): Promise<void> {
await this._client.send('Network.setCacheDisabled', {
cacheDisabled: this._userCacheDisabled || this._protocolRequestInterceptionEnabled
cacheDisabled:
this._userCacheDisabled || this._protocolRequestInterceptionEnabled,
});
}
_onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload): void {
// Request interception doesn't happen for data URLs with Network Service.
if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) {
if (
this._protocolRequestInterceptionEnabled &&
!event.request.url.startsWith('data:')
) {
const requestId = event.requestId;
const interceptionId = this._requestIdToInterceptionId.get(requestId);
if (interceptionId) {
@ -154,9 +184,9 @@ export class NetworkManager extends EventEmitter {
*/
_onAuthRequired(event: Protocol.Fetch.authRequiredPayload): void {
/* TODO(jacktfranklin): This is defined in protocol.d.ts but not
* in an easily referrable way - we should look at exposing it.
*/
type AuthResponse = 'Default'|'CancelAuth'|'ProvideCredentials';
* in an easily referrable way - we should look at exposing it.
*/
type AuthResponse = 'Default' | 'CancelAuth' | 'ProvideCredentials';
let response: AuthResponse = 'Default';
if (this._attemptedAuthentications.has(event.requestId)) {
response = 'CancelAuth';
@ -164,24 +194,36 @@ export class NetworkManager extends EventEmitter {
response = 'ProvideCredentials';
this._attemptedAuthentications.add(event.requestId);
}
const {username, password} = this._credentials || {username: undefined, password: undefined};
this._client.send('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: {response, username, password},
}).catch(debugError);
const { username, password } = this._credentials || {
username: undefined,
password: undefined,
};
this._client
.send('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: { response, username, password },
})
.catch(debugError);
}
_onRequestPaused(event: Protocol.Fetch.requestPausedPayload): void {
if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
this._client.send('Fetch.continueRequest', {
requestId: event.requestId
}).catch(debugError);
if (
!this._userRequestInterceptionEnabled &&
this._protocolRequestInterceptionEnabled
) {
this._client
.send('Fetch.continueRequest', {
requestId: event.requestId,
})
.catch(debugError);
}
const requestId = event.networkId;
const interceptionId = event.requestId;
if (requestId && this._requestIdToRequestWillBeSentEvent.has(requestId)) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(
requestId
);
this._onRequest(requestWillBeSentEvent, interceptionId);
this._requestIdToRequestWillBeSentEvent.delete(requestId);
} else {
@ -189,7 +231,10 @@ export class NetworkManager extends EventEmitter {
}
}
_onRequest(event: Protocol.Network.requestWillBeSentPayload, interceptionId?: string): void {
_onRequest(
event: Protocol.Network.requestWillBeSentPayload,
interceptionId?: string
): void {
let redirectChain = [];
if (event.redirectResponse) {
const request = this._requestIdToRequest.get(event.requestId);
@ -199,23 +244,39 @@ export class NetworkManager extends EventEmitter {
redirectChain = request._redirectChain;
}
}
const frame = event.frameId ? this._frameManager.frame(event.frameId) : null;
const request = new Request(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain);
const frame = event.frameId
? this._frameManager.frame(event.frameId)
: null;
const request = new Request(
this._client,
frame,
interceptionId,
this._userRequestInterceptionEnabled,
event,
redirectChain
);
this._requestIdToRequest.set(event.requestId, request);
this.emit(Events.NetworkManager.Request, request);
}
_onRequestServedFromCache(event: Protocol.Network.requestServedFromCachePayload): void {
_onRequestServedFromCache(
event: Protocol.Network.requestServedFromCachePayload
): void {
const request = this._requestIdToRequest.get(event.requestId);
if (request)
request._fromMemoryCache = true;
if (request) request._fromMemoryCache = true;
}
_handleRequestRedirect(request: Request, responsePayload: Protocol.Network.Response): void {
_handleRequestRedirect(
request: Request,
responsePayload: Protocol.Network.Response
): void {
const response = new Response(this._client, request, responsePayload);
request._response = response;
request._redirectChain.push(request);
response._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
response._bodyLoadedPromiseFulfill.call(
null,
new Error('Response body is unavailable for redirect responses')
);
this._requestIdToRequest.delete(request._requestId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(Events.NetworkManager.Response, response);
@ -225,8 +286,7 @@ export class NetworkManager extends EventEmitter {
_onResponseReceived(event: Protocol.Network.responseReceivedPayload): void {
const request = this._requestIdToRequest.get(event.requestId);
// FileUpload sends a response without a matching request.
if (!request)
return;
if (!request) return;
const response = new Response(this._client, request, event.response);
request._response = response;
this.emit(Events.NetworkManager.Response, response);
@ -236,8 +296,7 @@ export class NetworkManager extends EventEmitter {
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
return;
if (!request) return;
// Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475
@ -252,12 +311,10 @@ export class NetworkManager extends EventEmitter {
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
return;
if (!request) return;
request._failureText = event.errorText;
const response = request.response();
if (response)
response._bodyLoadedPromiseFulfill.call(null);
if (response) response._bodyLoadedPromiseFulfill.call(null);
this._requestIdToRequest.delete(request._requestId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(Events.NetworkManager.RequestFailed, request);
@ -282,12 +339,20 @@ export class Request {
_frame: Frame;
_redirectChain: Request[];
_fromMemoryCache = false;
_fromMemoryCache = false;
constructor(client: CDPSession, frame: Frame, interceptionId: string, allowInterception: boolean, event: Protocol.Network.requestWillBeSentPayload, redirectChain: Request[]) {
constructor(
client: CDPSession,
frame: Frame,
interceptionId: string,
allowInterception: boolean,
event: Protocol.Network.requestWillBeSentPayload,
redirectChain: Request[]
) {
this._client = client;
this._requestId = event.requestId;
this._isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document';
this._isNavigationRequest =
event.requestId === event.loaderId && event.type === 'Document';
this._interceptionId = interceptionId;
this._allowInterception = allowInterception;
this._url = event.request.url;
@ -340,54 +405,58 @@ export class Request {
/**
* @return {?{errorText: string}}
*/
failure(): {errorText: string} | null {
if (!this._failureText)
return null;
failure(): { errorText: string } | null {
if (!this._failureText) return null;
return {
errorText: this._failureText
errorText: this._failureText,
};
}
async continue(overrides: {url?: string; method?: string; postData?: string; headers?: Record<string, string>} = {}): Promise<void> {
async continue(
overrides: {
url?: string;
method?: string;
postData?: string;
headers?: Record<string, string>;
} = {}
): Promise<void> {
// Request interception is not supported for data: urls.
if (this._url.startsWith('data:'))
return;
if (this._url.startsWith('data:')) return;
assert(this._allowInterception, 'Request Interception is not enabled!');
assert(!this._interceptionHandled, 'Request is already handled!');
const {
url,
method,
postData,
headers
} = overrides;
const { url, method, postData, headers } = overrides;
this._interceptionHandled = true;
await this._client.send('Fetch.continueRequest', {
requestId: this._interceptionId,
url,
method,
postData,
headers: headers ? headersArray(headers) : undefined,
}).catch(error => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
});
await this._client
.send('Fetch.continueRequest', {
requestId: this._interceptionId,
url,
method,
postData,
headers: headers ? headersArray(headers) : undefined,
})
.catch((error) => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
});
}
async respond(response: {
status: number;
headers: Record<string, string>;
contentType: string;
body: string|Buffer;
body: string | Buffer;
}): Promise<void> {
// Mocking responses for dataURL requests is not currently supported.
if (this._url.startsWith('data:'))
return;
if (this._url.startsWith('data:')) return;
assert(this._allowInterception, 'Request Interception is not enabled!');
assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true;
const responseBody: Buffer | null = response.body && helper.isString(response.body) ? Buffer.from(response.body) : response.body as Buffer || null;
const responseBody: Buffer | null =
response.body && helper.isString(response.body)
? Buffer.from(response.body)
: (response.body as Buffer) || null;
const responseHeaders: Record<string, string> = {};
if (response.headers) {
@ -397,63 +466,82 @@ export class Request {
if (response.contentType)
responseHeaders['content-type'] = response.contentType;
if (responseBody && !('content-length' in responseHeaders))
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
responseHeaders['content-length'] = String(
Buffer.byteLength(responseBody)
);
await this._client.send('Fetch.fulfillRequest', {
requestId: this._interceptionId,
responseCode: response.status || 200,
responsePhrase: STATUS_TEXTS[response.status || 200],
responseHeaders: headersArray(responseHeaders),
body: responseBody ? responseBody.toString('base64') : undefined,
}).catch(error => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
});
await this._client
.send('Fetch.fulfillRequest', {
requestId: this._interceptionId,
responseCode: response.status || 200,
responsePhrase: STATUS_TEXTS[response.status || 200],
responseHeaders: headersArray(responseHeaders),
body: responseBody ? responseBody.toString('base64') : undefined,
})
.catch((error) => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
});
}
async abort(errorCode: ErrorCode = 'failed'): Promise<void> {
// Request interception is not supported for data: urls.
if (this._url.startsWith('data:'))
return;
if (this._url.startsWith('data:')) return;
const errorReason = errorReasons[errorCode];
assert(errorReason, 'Unknown error code: ' + errorCode);
assert(this._allowInterception, 'Request Interception is not enabled!');
assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true;
await this._client.send('Fetch.failRequest', {
requestId: this._interceptionId,
errorReason
}).catch(error => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
});
await this._client
.send('Fetch.failRequest', {
requestId: this._interceptionId,
errorReason,
})
.catch((error) => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
});
}
}
type ErrorCode = 'aborted' | 'accessdenied' | 'addressunreachable' | 'blockedbyclient' | 'blockedbyresponse' | 'connectionaborted' | 'connectionclosed' | 'connectionfailed' | 'connectionrefused' | 'connectionreset' | 'internetdisconnected' | 'namenotresolved' | 'timedout' | 'failed';
type ErrorCode =
| 'aborted'
| 'accessdenied'
| 'addressunreachable'
| 'blockedbyclient'
| 'blockedbyresponse'
| 'connectionaborted'
| 'connectionclosed'
| 'connectionfailed'
| 'connectionrefused'
| 'connectionreset'
| 'internetdisconnected'
| 'namenotresolved'
| 'timedout'
| 'failed';
const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = {
'aborted': 'Aborted',
'accessdenied': 'AccessDenied',
'addressunreachable': 'AddressUnreachable',
'blockedbyclient': 'BlockedByClient',
'blockedbyresponse': 'BlockedByResponse',
'connectionaborted': 'ConnectionAborted',
'connectionclosed': 'ConnectionClosed',
'connectionfailed': 'ConnectionFailed',
'connectionrefused': 'ConnectionRefused',
'connectionreset': 'ConnectionReset',
'internetdisconnected': 'InternetDisconnected',
'namenotresolved': 'NameNotResolved',
'timedout': 'TimedOut',
'failed': 'Failed',
aborted: 'Aborted',
accessdenied: 'AccessDenied',
addressunreachable: 'AddressUnreachable',
blockedbyclient: 'BlockedByClient',
blockedbyresponse: 'BlockedByResponse',
connectionaborted: 'ConnectionAborted',
connectionclosed: 'ConnectionClosed',
connectionfailed: 'ConnectionFailed',
connectionrefused: 'ConnectionRefused',
connectionreset: 'ConnectionReset',
internetdisconnected: 'InternetDisconnected',
namenotresolved: 'NameNotResolved',
timedout: 'TimedOut',
failed: 'Failed',
} as const;
interface RemoteAddress {
ip: string;
port: number;
ip: string;
port: number;
}
export class Response {
@ -471,11 +559,15 @@ export class Response {
_headers: Record<string, string> = {};
_securityDetails: SecurityDetails | null;
constructor(client: CDPSession, request: Request, responsePayload: Protocol.Network.Response) {
constructor(
client: CDPSession,
request: Request,
responsePayload: Protocol.Network.Response
) {
this._client = client;
this._request = request;
this._bodyLoadedPromise = new Promise(fulfill => {
this._bodyLoadedPromise = new Promise((fulfill) => {
this._bodyLoadedPromiseFulfill = fulfill;
});
@ -490,7 +582,9 @@ export class Response {
this._fromServiceWorker = !!responsePayload.fromServiceWorker;
for (const key of Object.keys(responsePayload.headers))
this._headers[key.toLowerCase()] = responsePayload.headers[key];
this._securityDetails = responsePayload.securityDetails ? new SecurityDetails(responsePayload.securityDetails) : null;
this._securityDetails = responsePayload.securityDetails
? new SecurityDetails(responsePayload.securityDetails)
: null;
}
remoteAddress(): RemoteAddress {
@ -523,13 +617,15 @@ export class Response {
buffer(): Promise<Buffer> {
if (!this._contentPromise) {
this._contentPromise = this._bodyLoadedPromise.then(async error => {
if (error)
throw error;
this._contentPromise = this._bodyLoadedPromise.then(async (error) => {
if (error) throw error;
const response = await this._client.send('Network.getResponseBody', {
requestId: this._request._requestId
requestId: this._request._requestId,
});
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
return Buffer.from(
response.body,
response.base64Encoded ? 'base64' : 'utf8'
);
});
}
return this._contentPromise;
@ -598,11 +694,13 @@ export class SecurityDetails {
}
}
function headersArray(headers: Record<string, string>): Array<{name: string; value: string}> {
function headersArray(
headers: Record<string, string>
): Array<{ name: string; value: string }> {
const result = [];
for (const name in headers) {
if (!Object.is(headers[name], undefined))
result.push({name, value: headers[name] + ''});
result.push({ name, value: headers[name] + '' });
}
return result;
}
@ -650,7 +748,7 @@ const STATUS_TEXTS = {
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I\'m a teapot',
'418': "I'm a teapot",
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',

File diff suppressed because it is too large Load Diff

View File

@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {helper, debugError, PuppeteerEventListener} from './helper';
import type {ConnectionTransport} from './ConnectionTransport';
import { helper, debugError, PuppeteerEventListener } from './helper';
import type { ConnectionTransport } from './ConnectionTransport';
export class PipeTransport implements ConnectionTransport {
_pipeWrite: NodeJS.WritableStream;
@ -24,14 +24,18 @@ export class PipeTransport implements ConnectionTransport {
onclose?: () => void;
onmessage?: () => void;
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) {
constructor(
pipeWrite: NodeJS.WritableStream,
pipeRead: NodeJS.ReadableStream
) {
this._pipeWrite = pipeWrite;
this._pendingMessage = '';
this._eventListeners = [
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
helper.addEventListener(pipeRead, 'data', (buffer) =>
this._dispatch(buffer)
),
helper.addEventListener(pipeRead, 'close', () => {
if (this.onclose)
this.onclose.call(null);
if (this.onclose) this.onclose.call(null);
}),
helper.addEventListener(pipeRead, 'error', debugError),
helper.addEventListener(pipeWrite, 'error', debugError),
@ -52,8 +56,7 @@ export class PipeTransport implements ConnectionTransport {
return;
}
const message = this._pendingMessage + buffer.toString(undefined, 0, end);
if (this.onmessage)
this.onmessage.call(null, message);
if (this.onmessage) this.onmessage.call(null, message);
let start = end + 1;
end = buffer.indexOf('\0', start);

View File

@ -14,14 +14,19 @@
* limitations under the License.
*/
import Launcher from './Launcher';
import type {LaunchOptions, ChromeArgOptions, BrowserOptions, ProductLauncher} from './Launcher';
import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher';
import {puppeteerErrors, PuppeteerErrors} from './Errors';
import type {ConnectionTransport} from './ConnectionTransport';
import type {
LaunchOptions,
ChromeArgOptions,
BrowserOptions,
ProductLauncher,
} from './Launcher';
import { BrowserFetcher, BrowserFetcherOptions } from './BrowserFetcher';
import { puppeteerErrors, PuppeteerErrors } from './Errors';
import type { ConnectionTransport } from './ConnectionTransport';
import {devicesMap} from './DeviceDescriptors';
import type {DevicesMap} from './/DeviceDescriptors';
import {Browser} from './Browser';
import { devicesMap } from './DeviceDescriptors';
import type { DevicesMap } from './/DeviceDescriptors';
import { Browser } from './Browser';
import * as QueryHandler from './QueryHandler';
export class Puppeteer {
@ -32,7 +37,12 @@ export class Puppeteer {
__productName: string;
_lazyLauncher: ProductLauncher;
constructor(projectRoot: string, preferredRevision: string, isPuppeteerCore: boolean, productName: string) {
constructor(
projectRoot: string,
preferredRevision: string,
isPuppeteerCore: boolean,
productName: string
) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
@ -40,26 +50,29 @@ export class Puppeteer {
this.__productName = productName;
}
launch(options: LaunchOptions & ChromeArgOptions & BrowserOptions & {product?: string; extraPrefsFirefox?: {}} = {}): Promise<Browser> {
if (options.product)
this._productName = options.product;
launch(
options: LaunchOptions &
ChromeArgOptions &
BrowserOptions & { product?: string; extraPrefsFirefox?: {} } = {}
): Promise<Browser> {
if (options.product) this._productName = options.product;
return this._launcher.launch(options);
}
connect(options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
product?: string;
}): Promise<Browser> {
if (options.product)
this._productName = options.product;
connect(
options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
product?: string;
}
): Promise<Browser> {
if (options.product) this._productName = options.product;
return this._launcher.connect(options);
}
set _productName(name: string) {
if (this.__productName !== name)
this._changedProduct = true;
if (this.__productName !== name) this._changedProduct = true;
this.__productName = name;
}
@ -72,7 +85,11 @@ export class Puppeteer {
}
get _launcher(): ProductLauncher {
if (!this._lazyLauncher || this._lazyLauncher.product !== this._productName || this._changedProduct) {
if (
!this._lazyLauncher ||
this._lazyLauncher.product !== this._productName ||
this._changedProduct
) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require('../package.json');
@ -85,7 +102,12 @@ export class Puppeteer {
this._preferredRevision = packageJson.puppeteer.chromium_revision;
}
this._changedProduct = false;
this._lazyLauncher = Launcher(this._projectRoot, this._preferredRevision, this._isPuppeteerCore, this._productName);
this._lazyLauncher = Launcher(
this._projectRoot,
this._preferredRevision,
this._isPuppeteerCore,
this._productName
);
}
return this._lazyLauncher;
}
@ -111,7 +133,10 @@ export class Puppeteer {
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_registerCustomQueryHandler(name: string, queryHandler: QueryHandler.QueryHandler): void {
__experimental_registerCustomQueryHandler(
name: string,
queryHandler: QueryHandler.QueryHandler
): void {
QueryHandler.registerCustomQueryHandler(name, queryHandler);
}

View File

@ -15,12 +15,18 @@
*/
export interface QueryHandler {
(element: Element | Document, selector: string): Element | Element[] | NodeListOf<Element>;
(element: Element | Document, selector: string):
| Element
| Element[]
| NodeListOf<Element>;
}
const _customQueryHandlers = new Map<string, QueryHandler>();
export function registerCustomQueryHandler(name: string, handler: Function): void {
export function registerCustomQueryHandler(
name: string,
handler: Function
): void {
if (_customQueryHandlers.get(name))
throw new Error(`A custom query handler named "${name}" already exists`);
@ -46,22 +52,26 @@ export function clearQueryHandlers(): void {
_customQueryHandlers.clear();
}
export function getQueryHandlerAndSelector(selector: string, defaultQueryHandler: QueryHandler):
{ updatedSelector: string; queryHandler: QueryHandler} {
export function getQueryHandlerAndSelector(
selector: string,
defaultQueryHandler: QueryHandler
): { updatedSelector: string; queryHandler: QueryHandler } {
const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector);
if (!hasCustomQueryHandler)
return {updatedSelector: selector, queryHandler: defaultQueryHandler};
return { updatedSelector: selector, queryHandler: defaultQueryHandler };
const index = selector.indexOf('/');
const name = selector.slice(0, index);
const updatedSelector = selector.slice(index + 1);
const queryHandler = customQueryHandlers().get(name);
if (!queryHandler)
throw new Error(`Query set to use "${name}", but no query handler of that name was found`);
throw new Error(
`Query set to use "${name}", but no query handler of that name was found`
);
return {
updatedSelector,
queryHandler
queryHandler,
};
}
@ -70,5 +80,5 @@ module.exports = {
unregisterCustomQueryHandler,
customQueryHandlers,
getQueryHandlerAndSelector,
clearQueryHandlers
clearQueryHandlers,
};

View File

@ -14,13 +14,13 @@
* limitations under the License.
*/
import {Events} from './Events';
import {Page} from './Page';
import {Worker as PuppeteerWorker} from './Worker';
import {CDPSession} from './Connection';
import {TaskQueue} from './TaskQueue';
import {Browser, BrowserContext} from './Browser';
import type {Viewport} from './PuppeteerViewport';
import { Events } from './Events';
import { Page } from './Page';
import { Worker as PuppeteerWorker } from './Worker';
import { CDPSession } from './Connection';
import { TaskQueue } from './TaskQueue';
import { Browser, BrowserContext } from './Browser';
import type { Viewport } from './PuppeteerViewport';
export class Target {
_targetInfo: Protocol.Target.TargetInfo;
@ -38,7 +38,14 @@ export class Target {
_closedCallback: () => void;
_isInitialized: boolean;
constructor(targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext, sessionFactory: () => Promise<CDPSession>, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue) {
constructor(
targetInfo: Protocol.Target.TargetInfo,
browserContext: BrowserContext,
sessionFactory: () => Promise<CDPSession>,
ignoreHTTPSErrors: boolean,
defaultViewport: Viewport | null,
screenshotTaskQueue: TaskQueue
) {
this._targetInfo = targetInfo;
this._browserContext = browserContext;
this._targetId = targetInfo.targetId;
@ -50,23 +57,25 @@ export class Target {
this._pagePromise = null;
/** @type {?Promise<!PuppeteerWorker>} */
this._workerPromise = null;
this._initializedPromise = new Promise<boolean>(fulfill => this._initializedCallback = fulfill).then(async success => {
if (!success)
return false;
this._initializedPromise = new Promise<boolean>(
(fulfill) => (this._initializedCallback = fulfill)
).then(async (success) => {
if (!success) return false;
const opener = this.opener();
if (!opener || !opener._pagePromise || this.type() !== 'page')
return true;
const openerPage = await opener._pagePromise;
if (!openerPage.listenerCount(Events.Page.Popup))
return true;
if (!openerPage.listenerCount(Events.Page.Popup)) return true;
const popupPage = await this.page();
openerPage.emit(Events.Page.Popup, popupPage);
return true;
});
this._isClosedPromise = new Promise<boolean>(fulfill => this._closedCallback = fulfill);
this._isInitialized = this._targetInfo.type !== 'page' || this._targetInfo.url !== '';
if (this._isInitialized)
this._initializedCallback(true);
this._isClosedPromise = new Promise<boolean>(
(fulfill) => (this._closedCallback = fulfill)
);
this._isInitialized =
this._targetInfo.type !== 'page' || this._targetInfo.url !== '';
if (this._isInitialized) this._initializedCallback(true);
}
createCDPSession(): Promise<CDPSession> {
@ -74,20 +83,41 @@ export class Target {
}
async page(): Promise<Page | null> {
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
this._pagePromise = this._sessionFactory()
.then(client => Page.create(client, this, this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotTaskQueue));
if (
(this._targetInfo.type === 'page' ||
this._targetInfo.type === 'background_page') &&
!this._pagePromise
) {
this._pagePromise = this._sessionFactory().then((client) =>
Page.create(
client,
this,
this._ignoreHTTPSErrors,
this._defaultViewport,
this._screenshotTaskQueue
)
);
}
return this._pagePromise;
}
async worker(): Promise<PuppeteerWorker | null> {
if (this._targetInfo.type !== 'service_worker' && this._targetInfo.type !== 'shared_worker')
if (
this._targetInfo.type !== 'service_worker' &&
this._targetInfo.type !== 'shared_worker'
)
return null;
if (!this._workerPromise) {
// TODO(einbinder): Make workers send their console logs.
this._workerPromise = this._sessionFactory()
.then(client => new PuppeteerWorker(client, this._targetInfo.url, () => {} /* consoleAPICalled */, () => {} /* exceptionThrown */));
this._workerPromise = this._sessionFactory().then(
(client) =>
new PuppeteerWorker(
client,
this._targetInfo.url,
() => {} /* consoleAPICalled */,
() => {} /* exceptionThrown */
)
);
}
return this._workerPromise;
}
@ -96,9 +126,21 @@ export class Target {
return this._targetInfo.url;
}
type(): 'page'|'background_page'|'service_worker'|'shared_worker'|'other'|'browser'{
type():
| 'page'
| 'background_page'
| 'service_worker'
| 'shared_worker'
| 'other'
| 'browser' {
const type = this._targetInfo.type;
if (type === 'page' || type === 'background_page' || type === 'service_worker' || type === 'shared_worker' || type === 'browser')
if (
type === 'page' ||
type === 'background_page' ||
type === 'service_worker' ||
type === 'shared_worker' ||
type === 'browser'
)
return type;
return 'other';
}
@ -112,16 +154,18 @@ export class Target {
}
opener(): Target | null {
const {openerId} = this._targetInfo;
if (!openerId)
return null;
const { openerId } = this._targetInfo;
if (!openerId) return null;
return this.browser()._targets.get(openerId);
}
_targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void {
this._targetInfo = targetInfo;
if (!this._isInitialized && (this._targetInfo.type !== 'page' || this._targetInfo.url !== '')) {
if (
!this._isInitialized &&
(this._targetInfo.type !== 'page' || this._targetInfo.url !== '')
) {
this._isInitialized = true;
this._initializedCallback(true);
return;

View File

@ -39,14 +39,12 @@ export class TimeoutSettings {
navigationTimeout(): number {
if (this._defaultNavigationTimeout !== null)
return this._defaultNavigationTimeout;
if (this._defaultTimeout !== null)
return this._defaultTimeout;
if (this._defaultTimeout !== null) return this._defaultTimeout;
return DEFAULT_TIMEOUT;
}
timeout(): number {
if (this._defaultTimeout !== null)
return this._defaultTimeout;
if (this._defaultTimeout !== null) return this._defaultTimeout;
return DEFAULT_TIMEOUT;
}
}

View File

@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {helper, assert} from './helper';
import {CDPSession} from './Connection';
import { helper, assert } from './helper';
import { CDPSession } from './Connection';
interface TracingOptions {
path?: string;
@ -25,20 +25,31 @@ interface TracingOptions {
export class Tracing {
_client: CDPSession;
_recording = false;
_path = '';
_path = '';
constructor(client: CDPSession) {
this._client = client;
}
async start(options: TracingOptions = {}): Promise<void> {
assert(!this._recording, 'Cannot start recording trace while already recording trace.');
assert(
!this._recording,
'Cannot start recording trace while already recording trace.'
);
const defaultCategories = [
'-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.frame', 'toplevel',
'blink.console', 'blink.user_timing', 'latencyInfo', 'disabled-by-default-devtools.timeline.stack',
'disabled-by-default-v8.cpu_profiler', 'disabled-by-default-v8.cpu_profiler.hires'
'-*',
'devtools.timeline',
'v8.execute',
'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.frame',
'toplevel',
'blink.console',
'blink.user_timing',
'latencyInfo',
'disabled-by-default-devtools.timeline.stack',
'disabled-by-default-v8.cpu_profiler',
'disabled-by-default-v8.cpu_profiler.hires',
];
const {
path = null,
@ -46,22 +57,23 @@ export class Tracing {
categories = defaultCategories,
} = options;
if (screenshots)
categories.push('disabled-by-default-devtools.screenshot');
if (screenshots) categories.push('disabled-by-default-devtools.screenshot');
this._path = path;
this._recording = true;
await this._client.send('Tracing.start', {
transferMode: 'ReturnAsStream',
categories: categories.join(',')
categories: categories.join(','),
});
}
async stop(): Promise<Buffer> {
let fulfill: (value: Buffer) => void;
const contentPromise = new Promise<Buffer>(x => fulfill = x);
this._client.once('Tracing.tracingComplete', event => {
helper.readProtocolStream(this._client, event.stream, this._path).then(fulfill);
const contentPromise = new Promise<Buffer>((x) => (fulfill = x));
this._client.once('Tracing.tracingComplete', (event) => {
helper
.readProtocolStream(this._client, event.stream, this._path)
.then(fulfill);
});
await this._client.send('Tracing.end');
this._recording = false;

View File

@ -14,274 +14,656 @@
* limitations under the License.
*/
export interface KeyDefinition {
keyCode?: number;
shiftKeyCode?: number;
key?: string;
shiftKey?: string;
code?: string;
text?: string;
shiftText?: string;
location?: number;
keyCode?: number;
shiftKeyCode?: number;
key?: string;
shiftKey?: string;
code?: string;
text?: string;
shiftText?: string;
location?: number;
}
export type KeyInput = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'Power'|'Eject'|'Abort'|'Help'|'Backspace'|'Tab'|'Numpad5'|'NumpadEnter'|'Enter'|'\r'|'\n'|'ShiftLeft'|'ShiftRight'|'ControlLeft'|'ControlRight'|'AltLeft'|'AltRight'|'Pause'|'CapsLock'|'Escape'|'Convert'|'NonConvert'|'Space'|'Numpad9'|'PageUp'|'Numpad3'|'PageDown'|'End'|'Numpad1'|'Home'|'Numpad7'|'ArrowLeft'|'Numpad4'|'Numpad8'|'ArrowUp'|'ArrowRight'|'Numpad6'|'Numpad2'|'ArrowDown'|'Select'|'Open'|'PrintScreen'|'Insert'|'Numpad0'|'Delete'|'NumpadDecimal'|'Digit0'|'Digit1'|'Digit2'|'Digit3'|'Digit4'|'Digit5'|'Digit6'|'Digit7'|'Digit8'|'Digit9'|'KeyA'|'KeyB'|'KeyC'|'KeyD'|'KeyE'|'KeyF'|'KeyG'|'KeyH'|'KeyI'|'KeyJ'|'KeyK'|'KeyL'|'KeyM'|'KeyN'|'KeyO'|'KeyP'|'KeyQ'|'KeyR'|'KeyS'|'KeyT'|'KeyU'|'KeyV'|'KeyW'|'KeyX'|'KeyY'|'KeyZ'|'MetaLeft'|'MetaRight'|'ContextMenu'|'NumpadMultiply'|'NumpadAdd'|'NumpadSubtract'|'NumpadDivide'|'F1'|'F2'|'F3'|'F4'|'F5'|'F6'|'F7'|'F8'|'F9'|'F10'|'F11'|'F12'|'F13'|'F14'|'F15'|'F16'|'F17'|'F18'|'F19'|'F20'|'F21'|'F22'|'F23'|'F24'|'NumLock'|'ScrollLock'|'AudioVolumeMute'|'AudioVolumeDown'|'AudioVolumeUp'|'MediaTrackNext'|'MediaTrackPrevious'|'MediaStop'|'MediaPlayPause'|'Semicolon'|'Equal'|'NumpadEqual'|'Comma'|'Minus'|'Period'|'Slash'|'Backquote'|'BracketLeft'|'Backslash'|'BracketRight'|'Quote'|'AltGraph'|'Props'|'Cancel'|'Clear'|'Shift'|'Control'|'Alt'|'Accept'|'ModeChange'|' '|'Print'|'Execute'|'\u0000'|'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z'|'Meta'|'*'|'+'|'-'|'/'|';'|'='|','|'.'|'`'|'['|'\\'|']'|'\''|'Attn'|'CrSel'|'ExSel'|'EraseEof'|'Play'|'ZoomOut'|')'|'!'|'@'|'#'|'$'|'%'|'^'|'&'|'('|'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z'|':'|'<'|'_'|'>'|'?'|'~'|'{'|'|'|'}'|'"'|'SoftLeft'|'SoftRight'|'Camera'|'Call'|'EndCall'|'VolumeDown'|'VolumeUp';
export type KeyInput =
| '0'
| '1'
| '2'
| '3'
| '4'
| '5'
| '6'
| '7'
| '8'
| '9'
| 'Power'
| 'Eject'
| 'Abort'
| 'Help'
| 'Backspace'
| 'Tab'
| 'Numpad5'
| 'NumpadEnter'
| 'Enter'
| '\r'
| '\n'
| 'ShiftLeft'
| 'ShiftRight'
| 'ControlLeft'
| 'ControlRight'
| 'AltLeft'
| 'AltRight'
| 'Pause'
| 'CapsLock'
| 'Escape'
| 'Convert'
| 'NonConvert'
| 'Space'
| 'Numpad9'
| 'PageUp'
| 'Numpad3'
| 'PageDown'
| 'End'
| 'Numpad1'
| 'Home'
| 'Numpad7'
| 'ArrowLeft'
| 'Numpad4'
| 'Numpad8'
| 'ArrowUp'
| 'ArrowRight'
| 'Numpad6'
| 'Numpad2'
| 'ArrowDown'
| 'Select'
| 'Open'
| 'PrintScreen'
| 'Insert'
| 'Numpad0'
| 'Delete'
| 'NumpadDecimal'
| 'Digit0'
| 'Digit1'
| 'Digit2'
| 'Digit3'
| 'Digit4'
| 'Digit5'
| 'Digit6'
| 'Digit7'
| 'Digit8'
| 'Digit9'
| 'KeyA'
| 'KeyB'
| 'KeyC'
| 'KeyD'
| 'KeyE'
| 'KeyF'
| 'KeyG'
| 'KeyH'
| 'KeyI'
| 'KeyJ'
| 'KeyK'
| 'KeyL'
| 'KeyM'
| 'KeyN'
| 'KeyO'
| 'KeyP'
| 'KeyQ'
| 'KeyR'
| 'KeyS'
| 'KeyT'
| 'KeyU'
| 'KeyV'
| 'KeyW'
| 'KeyX'
| 'KeyY'
| 'KeyZ'
| 'MetaLeft'
| 'MetaRight'
| 'ContextMenu'
| 'NumpadMultiply'
| 'NumpadAdd'
| 'NumpadSubtract'
| 'NumpadDivide'
| 'F1'
| 'F2'
| 'F3'
| 'F4'
| 'F5'
| 'F6'
| 'F7'
| 'F8'
| 'F9'
| 'F10'
| 'F11'
| 'F12'
| 'F13'
| 'F14'
| 'F15'
| 'F16'
| 'F17'
| 'F18'
| 'F19'
| 'F20'
| 'F21'
| 'F22'
| 'F23'
| 'F24'
| 'NumLock'
| 'ScrollLock'
| 'AudioVolumeMute'
| 'AudioVolumeDown'
| 'AudioVolumeUp'
| 'MediaTrackNext'
| 'MediaTrackPrevious'
| 'MediaStop'
| 'MediaPlayPause'
| 'Semicolon'
| 'Equal'
| 'NumpadEqual'
| 'Comma'
| 'Minus'
| 'Period'
| 'Slash'
| 'Backquote'
| 'BracketLeft'
| 'Backslash'
| 'BracketRight'
| 'Quote'
| 'AltGraph'
| 'Props'
| 'Cancel'
| 'Clear'
| 'Shift'
| 'Control'
| 'Alt'
| 'Accept'
| 'ModeChange'
| ' '
| 'Print'
| 'Execute'
| '\u0000'
| 'a'
| 'b'
| 'c'
| 'd'
| 'e'
| 'f'
| 'g'
| 'h'
| 'i'
| 'j'
| 'k'
| 'l'
| 'm'
| 'n'
| 'o'
| 'p'
| 'q'
| 'r'
| 's'
| 't'
| 'u'
| 'v'
| 'w'
| 'x'
| 'y'
| 'z'
| 'Meta'
| '*'
| '+'
| '-'
| '/'
| ';'
| '='
| ','
| '.'
| '`'
| '['
| '\\'
| ']'
| "'"
| 'Attn'
| 'CrSel'
| 'ExSel'
| 'EraseEof'
| 'Play'
| 'ZoomOut'
| ')'
| '!'
| '@'
| '#'
| '$'
| '%'
| '^'
| '&'
| '('
| 'A'
| 'B'
| 'C'
| 'D'
| 'E'
| 'F'
| 'G'
| 'H'
| 'I'
| 'J'
| 'K'
| 'L'
| 'M'
| 'N'
| 'O'
| 'P'
| 'Q'
| 'R'
| 'S'
| 'T'
| 'U'
| 'V'
| 'W'
| 'X'
| 'Y'
| 'Z'
| ':'
| '<'
| '_'
| '>'
| '?'
| '~'
| '{'
| '|'
| '}'
| '"'
| 'SoftLeft'
| 'SoftRight'
| 'Camera'
| 'Call'
| 'EndCall'
| 'VolumeDown'
| 'VolumeUp';
export const keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
'0': {'keyCode': 48, 'key': '0', 'code': 'Digit0'},
'1': {'keyCode': 49, 'key': '1', 'code': 'Digit1'},
'2': {'keyCode': 50, 'key': '2', 'code': 'Digit2'},
'3': {'keyCode': 51, 'key': '3', 'code': 'Digit3'},
'4': {'keyCode': 52, 'key': '4', 'code': 'Digit4'},
'5': {'keyCode': 53, 'key': '5', 'code': 'Digit5'},
'6': {'keyCode': 54, 'key': '6', 'code': 'Digit6'},
'7': {'keyCode': 55, 'key': '7', 'code': 'Digit7'},
'8': {'keyCode': 56, 'key': '8', 'code': 'Digit8'},
'9': {'keyCode': 57, 'key': '9', 'code': 'Digit9'},
'Power': {'key': 'Power', 'code': 'Power'},
'Eject': {'key': 'Eject', 'code': 'Eject'},
'Abort': {'keyCode': 3, 'code': 'Abort', 'key': 'Cancel'},
'Help': {'keyCode': 6, 'code': 'Help', 'key': 'Help'},
'Backspace': {'keyCode': 8, 'code': 'Backspace', 'key': 'Backspace'},
'Tab': {'keyCode': 9, 'code': 'Tab', 'key': 'Tab'},
'Numpad5': {'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3},
'NumpadEnter': {'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3},
'Enter': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'\r': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'\n': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'ShiftLeft': {'keyCode': 16, 'code': 'ShiftLeft', 'key': 'Shift', 'location': 1},
'ShiftRight': {'keyCode': 16, 'code': 'ShiftRight', 'key': 'Shift', 'location': 2},
'ControlLeft': {'keyCode': 17, 'code': 'ControlLeft', 'key': 'Control', 'location': 1},
'ControlRight': {'keyCode': 17, 'code': 'ControlRight', 'key': 'Control', 'location': 2},
'AltLeft': {'keyCode': 18, 'code': 'AltLeft', 'key': 'Alt', 'location': 1},
'AltRight': {'keyCode': 18, 'code': 'AltRight', 'key': 'Alt', 'location': 2},
'Pause': {'keyCode': 19, 'code': 'Pause', 'key': 'Pause'},
'CapsLock': {'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock'},
'Escape': {'keyCode': 27, 'code': 'Escape', 'key': 'Escape'},
'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'},
'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'},
'Space': {'keyCode': 32, 'code': 'Space', 'key': ' '},
'Numpad9': {'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'code': 'Numpad9', 'shiftKey': '9', 'location': 3},
'PageUp': {'keyCode': 33, 'code': 'PageUp', 'key': 'PageUp'},
'Numpad3': {'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'code': 'Numpad3', 'shiftKey': '3', 'location': 3},
'PageDown': {'keyCode': 34, 'code': 'PageDown', 'key': 'PageDown'},
'End': {'keyCode': 35, 'code': 'End', 'key': 'End'},
'Numpad1': {'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3},
'Home': {'keyCode': 36, 'code': 'Home', 'key': 'Home'},
'Numpad7': {'keyCode': 36, 'shiftKeyCode': 103, 'key': 'Home', 'code': 'Numpad7', 'shiftKey': '7', 'location': 3},
'ArrowLeft': {'keyCode': 37, 'code': 'ArrowLeft', 'key': 'ArrowLeft'},
'Numpad4': {'keyCode': 37, 'shiftKeyCode': 100, 'key': 'ArrowLeft', 'code': 'Numpad4', 'shiftKey': '4', 'location': 3},
'Numpad8': {'keyCode': 38, 'shiftKeyCode': 104, 'key': 'ArrowUp', 'code': 'Numpad8', 'shiftKey': '8', 'location': 3},
'ArrowUp': {'keyCode': 38, 'code': 'ArrowUp', 'key': 'ArrowUp'},
'ArrowRight': {'keyCode': 39, 'code': 'ArrowRight', 'key': 'ArrowRight'},
'Numpad6': {'keyCode': 39, 'shiftKeyCode': 102, 'key': 'ArrowRight', 'code': 'Numpad6', 'shiftKey': '6', 'location': 3},
'Numpad2': {'keyCode': 40, 'shiftKeyCode': 98, 'key': 'ArrowDown', 'code': 'Numpad2', 'shiftKey': '2', 'location': 3},
'ArrowDown': {'keyCode': 40, 'code': 'ArrowDown', 'key': 'ArrowDown'},
'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'},
'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'},
'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'},
'Insert': {'keyCode': 45, 'code': 'Insert', 'key': 'Insert'},
'Numpad0': {'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3},
'Delete': {'keyCode': 46, 'code': 'Delete', 'key': 'Delete'},
'NumpadDecimal': {'keyCode': 46, 'shiftKeyCode': 110, 'code': 'NumpadDecimal', 'key': '\u0000', 'shiftKey': '.', 'location': 3},
'Digit0': {'keyCode': 48, 'code': 'Digit0', 'shiftKey': ')', 'key': '0'},
'Digit1': {'keyCode': 49, 'code': 'Digit1', 'shiftKey': '!', 'key': '1'},
'Digit2': {'keyCode': 50, 'code': 'Digit2', 'shiftKey': '@', 'key': '2'},
'Digit3': {'keyCode': 51, 'code': 'Digit3', 'shiftKey': '#', 'key': '3'},
'Digit4': {'keyCode': 52, 'code': 'Digit4', 'shiftKey': '$', 'key': '4'},
'Digit5': {'keyCode': 53, 'code': 'Digit5', 'shiftKey': '%', 'key': '5'},
'Digit6': {'keyCode': 54, 'code': 'Digit6', 'shiftKey': '^', 'key': '6'},
'Digit7': {'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7'},
'Digit8': {'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8'},
'Digit9': {'keyCode': 57, 'code': 'Digit9', 'shiftKey': '\(', 'key': '9'},
'KeyA': {'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a'},
'KeyB': {'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b'},
'KeyC': {'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c'},
'KeyD': {'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd'},
'KeyE': {'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e'},
'KeyF': {'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f'},
'KeyG': {'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g'},
'KeyH': {'keyCode': 72, 'code': 'KeyH', 'shiftKey': 'H', 'key': 'h'},
'KeyI': {'keyCode': 73, 'code': 'KeyI', 'shiftKey': 'I', 'key': 'i'},
'KeyJ': {'keyCode': 74, 'code': 'KeyJ', 'shiftKey': 'J', 'key': 'j'},
'KeyK': {'keyCode': 75, 'code': 'KeyK', 'shiftKey': 'K', 'key': 'k'},
'KeyL': {'keyCode': 76, 'code': 'KeyL', 'shiftKey': 'L', 'key': 'l'},
'KeyM': {'keyCode': 77, 'code': 'KeyM', 'shiftKey': 'M', 'key': 'm'},
'KeyN': {'keyCode': 78, 'code': 'KeyN', 'shiftKey': 'N', 'key': 'n'},
'KeyO': {'keyCode': 79, 'code': 'KeyO', 'shiftKey': 'O', 'key': 'o'},
'KeyP': {'keyCode': 80, 'code': 'KeyP', 'shiftKey': 'P', 'key': 'p'},
'KeyQ': {'keyCode': 81, 'code': 'KeyQ', 'shiftKey': 'Q', 'key': 'q'},
'KeyR': {'keyCode': 82, 'code': 'KeyR', 'shiftKey': 'R', 'key': 'r'},
'KeyS': {'keyCode': 83, 'code': 'KeyS', 'shiftKey': 'S', 'key': 's'},
'KeyT': {'keyCode': 84, 'code': 'KeyT', 'shiftKey': 'T', 'key': 't'},
'KeyU': {'keyCode': 85, 'code': 'KeyU', 'shiftKey': 'U', 'key': 'u'},
'KeyV': {'keyCode': 86, 'code': 'KeyV', 'shiftKey': 'V', 'key': 'v'},
'KeyW': {'keyCode': 87, 'code': 'KeyW', 'shiftKey': 'W', 'key': 'w'},
'KeyX': {'keyCode': 88, 'code': 'KeyX', 'shiftKey': 'X', 'key': 'x'},
'KeyY': {'keyCode': 89, 'code': 'KeyY', 'shiftKey': 'Y', 'key': 'y'},
'KeyZ': {'keyCode': 90, 'code': 'KeyZ', 'shiftKey': 'Z', 'key': 'z'},
'MetaLeft': {'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta', 'location': 1},
'MetaRight': {'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta', 'location': 2},
'ContextMenu': {'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu'},
'NumpadMultiply': {'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3},
'NumpadAdd': {'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3},
'NumpadSubtract': {'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3},
'NumpadDivide': {'keyCode': 111, 'code': 'NumpadDivide', 'key': '/', 'location': 3},
'F1': {'keyCode': 112, 'code': 'F1', 'key': 'F1'},
'F2': {'keyCode': 113, 'code': 'F2', 'key': 'F2'},
'F3': {'keyCode': 114, 'code': 'F3', 'key': 'F3'},
'F4': {'keyCode': 115, 'code': 'F4', 'key': 'F4'},
'F5': {'keyCode': 116, 'code': 'F5', 'key': 'F5'},
'F6': {'keyCode': 117, 'code': 'F6', 'key': 'F6'},
'F7': {'keyCode': 118, 'code': 'F7', 'key': 'F7'},
'F8': {'keyCode': 119, 'code': 'F8', 'key': 'F8'},
'F9': {'keyCode': 120, 'code': 'F9', 'key': 'F9'},
'F10': {'keyCode': 121, 'code': 'F10', 'key': 'F10'},
'F11': {'keyCode': 122, 'code': 'F11', 'key': 'F11'},
'F12': {'keyCode': 123, 'code': 'F12', 'key': 'F12'},
'F13': {'keyCode': 124, 'code': 'F13', 'key': 'F13'},
'F14': {'keyCode': 125, 'code': 'F14', 'key': 'F14'},
'F15': {'keyCode': 126, 'code': 'F15', 'key': 'F15'},
'F16': {'keyCode': 127, 'code': 'F16', 'key': 'F16'},
'F17': {'keyCode': 128, 'code': 'F17', 'key': 'F17'},
'F18': {'keyCode': 129, 'code': 'F18', 'key': 'F18'},
'F19': {'keyCode': 130, 'code': 'F19', 'key': 'F19'},
'F20': {'keyCode': 131, 'code': 'F20', 'key': 'F20'},
'F21': {'keyCode': 132, 'code': 'F21', 'key': 'F21'},
'F22': {'keyCode': 133, 'code': 'F22', 'key': 'F22'},
'F23': {'keyCode': 134, 'code': 'F23', 'key': 'F23'},
'F24': {'keyCode': 135, 'code': 'F24', 'key': 'F24'},
'NumLock': {'keyCode': 144, 'code': 'NumLock', 'key': 'NumLock'},
'ScrollLock': {'keyCode': 145, 'code': 'ScrollLock', 'key': 'ScrollLock'},
'AudioVolumeMute': {'keyCode': 173, 'code': 'AudioVolumeMute', 'key': 'AudioVolumeMute'},
'AudioVolumeDown': {'keyCode': 174, 'code': 'AudioVolumeDown', 'key': 'AudioVolumeDown'},
'AudioVolumeUp': {'keyCode': 175, 'code': 'AudioVolumeUp', 'key': 'AudioVolumeUp'},
'MediaTrackNext': {'keyCode': 176, 'code': 'MediaTrackNext', 'key': 'MediaTrackNext'},
'MediaTrackPrevious': {'keyCode': 177, 'code': 'MediaTrackPrevious', 'key': 'MediaTrackPrevious'},
'MediaStop': {'keyCode': 178, 'code': 'MediaStop', 'key': 'MediaStop'},
'MediaPlayPause': {'keyCode': 179, 'code': 'MediaPlayPause', 'key': 'MediaPlayPause'},
'Semicolon': {'keyCode': 186, 'code': 'Semicolon', 'shiftKey': ':', 'key': ';'},
'Equal': {'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '='},
'NumpadEqual': {'keyCode': 187, 'code': 'NumpadEqual', 'key': '=', 'location': 3},
'Comma': {'keyCode': 188, 'code': 'Comma', 'shiftKey': '\<', 'key': ','},
'Minus': {'keyCode': 189, 'code': 'Minus', 'shiftKey': '_', 'key': '-'},
'Period': {'keyCode': 190, 'code': 'Period', 'shiftKey': '>', 'key': '.'},
'Slash': {'keyCode': 191, 'code': 'Slash', 'shiftKey': '?', 'key': '/'},
'Backquote': {'keyCode': 192, 'code': 'Backquote', 'shiftKey': '~', 'key': '`'},
'BracketLeft': {'keyCode': 219, 'code': 'BracketLeft', 'shiftKey': '{', 'key': '['},
'Backslash': {'keyCode': 220, 'code': 'Backslash', 'shiftKey': '|', 'key': '\\'},
'BracketRight': {'keyCode': 221, 'code': 'BracketRight', 'shiftKey': '}', 'key': ']'},
'Quote': {'keyCode': 222, 'code': 'Quote', 'shiftKey': '"', 'key': '\''},
'AltGraph': {'keyCode': 225, 'code': 'AltGraph', 'key': 'AltGraph'},
'Props': {'keyCode': 247, 'code': 'Props', 'key': 'CrSel'},
'Cancel': {'keyCode': 3, 'key': 'Cancel', 'code': 'Abort'},
'Clear': {'keyCode': 12, 'key': 'Clear', 'code': 'Numpad5', 'location': 3},
'Shift': {'keyCode': 16, 'key': 'Shift', 'code': 'ShiftLeft', 'location': 1},
'Control': {'keyCode': 17, 'key': 'Control', 'code': 'ControlLeft', 'location': 1},
'Alt': {'keyCode': 18, 'key': 'Alt', 'code': 'AltLeft', 'location': 1},
'Accept': {'keyCode': 30, 'key': 'Accept'},
'ModeChange': {'keyCode': 31, 'key': 'ModeChange'},
' ': {'keyCode': 32, 'key': ' ', 'code': 'Space'},
'Print': {'keyCode': 42, 'key': 'Print'},
'Execute': {'keyCode': 43, 'key': 'Execute', 'code': 'Open'},
'\u0000': {'keyCode': 46, 'key': '\u0000', 'code': 'NumpadDecimal', 'location': 3},
'a': {'keyCode': 65, 'key': 'a', 'code': 'KeyA'},
'b': {'keyCode': 66, 'key': 'b', 'code': 'KeyB'},
'c': {'keyCode': 67, 'key': 'c', 'code': 'KeyC'},
'd': {'keyCode': 68, 'key': 'd', 'code': 'KeyD'},
'e': {'keyCode': 69, 'key': 'e', 'code': 'KeyE'},
'f': {'keyCode': 70, 'key': 'f', 'code': 'KeyF'},
'g': {'keyCode': 71, 'key': 'g', 'code': 'KeyG'},
'h': {'keyCode': 72, 'key': 'h', 'code': 'KeyH'},
'i': {'keyCode': 73, 'key': 'i', 'code': 'KeyI'},
'j': {'keyCode': 74, 'key': 'j', 'code': 'KeyJ'},
'k': {'keyCode': 75, 'key': 'k', 'code': 'KeyK'},
'l': {'keyCode': 76, 'key': 'l', 'code': 'KeyL'},
'm': {'keyCode': 77, 'key': 'm', 'code': 'KeyM'},
'n': {'keyCode': 78, 'key': 'n', 'code': 'KeyN'},
'o': {'keyCode': 79, 'key': 'o', 'code': 'KeyO'},
'p': {'keyCode': 80, 'key': 'p', 'code': 'KeyP'},
'q': {'keyCode': 81, 'key': 'q', 'code': 'KeyQ'},
'r': {'keyCode': 82, 'key': 'r', 'code': 'KeyR'},
's': {'keyCode': 83, 'key': 's', 'code': 'KeyS'},
't': {'keyCode': 84, 'key': 't', 'code': 'KeyT'},
'u': {'keyCode': 85, 'key': 'u', 'code': 'KeyU'},
'v': {'keyCode': 86, 'key': 'v', 'code': 'KeyV'},
'w': {'keyCode': 87, 'key': 'w', 'code': 'KeyW'},
'x': {'keyCode': 88, 'key': 'x', 'code': 'KeyX'},
'y': {'keyCode': 89, 'key': 'y', 'code': 'KeyY'},
'z': {'keyCode': 90, 'key': 'z', 'code': 'KeyZ'},
'Meta': {'keyCode': 91, 'key': 'Meta', 'code': 'MetaLeft', 'location': 1},
'*': {'keyCode': 106, 'key': '*', 'code': 'NumpadMultiply', 'location': 3},
'+': {'keyCode': 107, 'key': '+', 'code': 'NumpadAdd', 'location': 3},
'-': {'keyCode': 109, 'key': '-', 'code': 'NumpadSubtract', 'location': 3},
'/': {'keyCode': 111, 'key': '/', 'code': 'NumpadDivide', 'location': 3},
';': {'keyCode': 186, 'key': ';', 'code': 'Semicolon'},
'=': {'keyCode': 187, 'key': '=', 'code': 'Equal'},
',': {'keyCode': 188, 'key': ',', 'code': 'Comma'},
'.': {'keyCode': 190, 'key': '.', 'code': 'Period'},
'`': {'keyCode': 192, 'key': '`', 'code': 'Backquote'},
'[': {'keyCode': 219, 'key': '[', 'code': 'BracketLeft'},
'\\': {'keyCode': 220, 'key': '\\', 'code': 'Backslash'},
']': {'keyCode': 221, 'key': ']', 'code': 'BracketRight'},
'\'': {'keyCode': 222, 'key': '\'', 'code': 'Quote'},
'Attn': {'keyCode': 246, 'key': 'Attn'},
'CrSel': {'keyCode': 247, 'key': 'CrSel', 'code': 'Props'},
'ExSel': {'keyCode': 248, 'key': 'ExSel'},
'EraseEof': {'keyCode': 249, 'key': 'EraseEof'},
'Play': {'keyCode': 250, 'key': 'Play'},
'ZoomOut': {'keyCode': 251, 'key': 'ZoomOut'},
')': {'keyCode': 48, 'key': ')', 'code': 'Digit0'},
'!': {'keyCode': 49, 'key': '!', 'code': 'Digit1'},
'@': {'keyCode': 50, 'key': '@', 'code': 'Digit2'},
'#': {'keyCode': 51, 'key': '#', 'code': 'Digit3'},
'$': {'keyCode': 52, 'key': '$', 'code': 'Digit4'},
'%': {'keyCode': 53, 'key': '%', 'code': 'Digit5'},
'^': {'keyCode': 54, 'key': '^', 'code': 'Digit6'},
'&': {'keyCode': 55, 'key': '&', 'code': 'Digit7'},
'(': {'keyCode': 57, 'key': '\(', 'code': 'Digit9'},
'A': {'keyCode': 65, 'key': 'A', 'code': 'KeyA'},
'B': {'keyCode': 66, 'key': 'B', 'code': 'KeyB'},
'C': {'keyCode': 67, 'key': 'C', 'code': 'KeyC'},
'D': {'keyCode': 68, 'key': 'D', 'code': 'KeyD'},
'E': {'keyCode': 69, 'key': 'E', 'code': 'KeyE'},
'F': {'keyCode': 70, 'key': 'F', 'code': 'KeyF'},
'G': {'keyCode': 71, 'key': 'G', 'code': 'KeyG'},
'H': {'keyCode': 72, 'key': 'H', 'code': 'KeyH'},
'I': {'keyCode': 73, 'key': 'I', 'code': 'KeyI'},
'J': {'keyCode': 74, 'key': 'J', 'code': 'KeyJ'},
'K': {'keyCode': 75, 'key': 'K', 'code': 'KeyK'},
'L': {'keyCode': 76, 'key': 'L', 'code': 'KeyL'},
'M': {'keyCode': 77, 'key': 'M', 'code': 'KeyM'},
'N': {'keyCode': 78, 'key': 'N', 'code': 'KeyN'},
'O': {'keyCode': 79, 'key': 'O', 'code': 'KeyO'},
'P': {'keyCode': 80, 'key': 'P', 'code': 'KeyP'},
'Q': {'keyCode': 81, 'key': 'Q', 'code': 'KeyQ'},
'R': {'keyCode': 82, 'key': 'R', 'code': 'KeyR'},
'S': {'keyCode': 83, 'key': 'S', 'code': 'KeyS'},
'T': {'keyCode': 84, 'key': 'T', 'code': 'KeyT'},
'U': {'keyCode': 85, 'key': 'U', 'code': 'KeyU'},
'V': {'keyCode': 86, 'key': 'V', 'code': 'KeyV'},
'W': {'keyCode': 87, 'key': 'W', 'code': 'KeyW'},
'X': {'keyCode': 88, 'key': 'X', 'code': 'KeyX'},
'Y': {'keyCode': 89, 'key': 'Y', 'code': 'KeyY'},
'Z': {'keyCode': 90, 'key': 'Z', 'code': 'KeyZ'},
':': {'keyCode': 186, 'key': ':', 'code': 'Semicolon'},
'<': {'keyCode': 188, 'key': '\<', 'code': 'Comma'},
'_': {'keyCode': 189, 'key': '_', 'code': 'Minus'},
'>': {'keyCode': 190, 'key': '>', 'code': 'Period'},
'?': {'keyCode': 191, 'key': '?', 'code': 'Slash'},
'~': {'keyCode': 192, 'key': '~', 'code': 'Backquote'},
'{': {'keyCode': 219, 'key': '{', 'code': 'BracketLeft'},
'|': {'keyCode': 220, 'key': '|', 'code': 'Backslash'},
'}': {'keyCode': 221, 'key': '}', 'code': 'BracketRight'},
'"': {'keyCode': 222, 'key': '"', 'code': 'Quote'},
'SoftLeft': {'key': 'SoftLeft', 'code': 'SoftLeft', 'location': 4},
'SoftRight': {'key': 'SoftRight', 'code': 'SoftRight', 'location': 4},
'Camera': {'keyCode': 44, 'key': 'Camera', 'code': 'Camera', 'location': 4},
'Call': {'key': 'Call', 'code': 'Call', 'location': 4},
'EndCall': {'keyCode': 95, 'key': 'EndCall', 'code': 'EndCall', 'location': 4},
'VolumeDown': {'keyCode': 182, 'key': 'VolumeDown', 'code': 'VolumeDown', 'location': 4},
'VolumeUp': {'keyCode': 183, 'key': 'VolumeUp', 'code': 'VolumeUp', 'location': 4},
'0': { keyCode: 48, key: '0', code: 'Digit0' },
'1': { keyCode: 49, key: '1', code: 'Digit1' },
'2': { keyCode: 50, key: '2', code: 'Digit2' },
'3': { keyCode: 51, key: '3', code: 'Digit3' },
'4': { keyCode: 52, key: '4', code: 'Digit4' },
'5': { keyCode: 53, key: '5', code: 'Digit5' },
'6': { keyCode: 54, key: '6', code: 'Digit6' },
'7': { keyCode: 55, key: '7', code: 'Digit7' },
'8': { keyCode: 56, key: '8', code: 'Digit8' },
'9': { keyCode: 57, key: '9', code: 'Digit9' },
Power: { key: 'Power', code: 'Power' },
Eject: { key: 'Eject', code: 'Eject' },
Abort: { keyCode: 3, code: 'Abort', key: 'Cancel' },
Help: { keyCode: 6, code: 'Help', key: 'Help' },
Backspace: { keyCode: 8, code: 'Backspace', key: 'Backspace' },
Tab: { keyCode: 9, code: 'Tab', key: 'Tab' },
Numpad5: {
keyCode: 12,
shiftKeyCode: 101,
key: 'Clear',
code: 'Numpad5',
shiftKey: '5',
location: 3,
},
NumpadEnter: {
keyCode: 13,
code: 'NumpadEnter',
key: 'Enter',
text: '\r',
location: 3,
},
Enter: { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
'\r': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
'\n': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
ShiftLeft: { keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1 },
ShiftRight: { keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2 },
ControlLeft: {
keyCode: 17,
code: 'ControlLeft',
key: 'Control',
location: 1,
},
ControlRight: {
keyCode: 17,
code: 'ControlRight',
key: 'Control',
location: 2,
},
AltLeft: { keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1 },
AltRight: { keyCode: 18, code: 'AltRight', key: 'Alt', location: 2 },
Pause: { keyCode: 19, code: 'Pause', key: 'Pause' },
CapsLock: { keyCode: 20, code: 'CapsLock', key: 'CapsLock' },
Escape: { keyCode: 27, code: 'Escape', key: 'Escape' },
Convert: { keyCode: 28, code: 'Convert', key: 'Convert' },
NonConvert: { keyCode: 29, code: 'NonConvert', key: 'NonConvert' },
Space: { keyCode: 32, code: 'Space', key: ' ' },
Numpad9: {
keyCode: 33,
shiftKeyCode: 105,
key: 'PageUp',
code: 'Numpad9',
shiftKey: '9',
location: 3,
},
PageUp: { keyCode: 33, code: 'PageUp', key: 'PageUp' },
Numpad3: {
keyCode: 34,
shiftKeyCode: 99,
key: 'PageDown',
code: 'Numpad3',
shiftKey: '3',
location: 3,
},
PageDown: { keyCode: 34, code: 'PageDown', key: 'PageDown' },
End: { keyCode: 35, code: 'End', key: 'End' },
Numpad1: {
keyCode: 35,
shiftKeyCode: 97,
key: 'End',
code: 'Numpad1',
shiftKey: '1',
location: 3,
},
Home: { keyCode: 36, code: 'Home', key: 'Home' },
Numpad7: {
keyCode: 36,
shiftKeyCode: 103,
key: 'Home',
code: 'Numpad7',
shiftKey: '7',
location: 3,
},
ArrowLeft: { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' },
Numpad4: {
keyCode: 37,
shiftKeyCode: 100,
key: 'ArrowLeft',
code: 'Numpad4',
shiftKey: '4',
location: 3,
},
Numpad8: {
keyCode: 38,
shiftKeyCode: 104,
key: 'ArrowUp',
code: 'Numpad8',
shiftKey: '8',
location: 3,
},
ArrowUp: { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' },
ArrowRight: { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' },
Numpad6: {
keyCode: 39,
shiftKeyCode: 102,
key: 'ArrowRight',
code: 'Numpad6',
shiftKey: '6',
location: 3,
},
Numpad2: {
keyCode: 40,
shiftKeyCode: 98,
key: 'ArrowDown',
code: 'Numpad2',
shiftKey: '2',
location: 3,
},
ArrowDown: { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' },
Select: { keyCode: 41, code: 'Select', key: 'Select' },
Open: { keyCode: 43, code: 'Open', key: 'Execute' },
PrintScreen: { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' },
Insert: { keyCode: 45, code: 'Insert', key: 'Insert' },
Numpad0: {
keyCode: 45,
shiftKeyCode: 96,
key: 'Insert',
code: 'Numpad0',
shiftKey: '0',
location: 3,
},
Delete: { keyCode: 46, code: 'Delete', key: 'Delete' },
NumpadDecimal: {
keyCode: 46,
shiftKeyCode: 110,
code: 'NumpadDecimal',
key: '\u0000',
shiftKey: '.',
location: 3,
},
Digit0: { keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0' },
Digit1: { keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1' },
Digit2: { keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2' },
Digit3: { keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3' },
Digit4: { keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4' },
Digit5: { keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5' },
Digit6: { keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6' },
Digit7: { keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7' },
Digit8: { keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8' },
Digit9: { keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9' },
KeyA: { keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a' },
KeyB: { keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b' },
KeyC: { keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c' },
KeyD: { keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd' },
KeyE: { keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e' },
KeyF: { keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f' },
KeyG: { keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g' },
KeyH: { keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h' },
KeyI: { keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i' },
KeyJ: { keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j' },
KeyK: { keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k' },
KeyL: { keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l' },
KeyM: { keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm' },
KeyN: { keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n' },
KeyO: { keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o' },
KeyP: { keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p' },
KeyQ: { keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q' },
KeyR: { keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r' },
KeyS: { keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's' },
KeyT: { keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't' },
KeyU: { keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u' },
KeyV: { keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v' },
KeyW: { keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w' },
KeyX: { keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x' },
KeyY: { keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y' },
KeyZ: { keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z' },
MetaLeft: { keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1 },
MetaRight: { keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2 },
ContextMenu: { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' },
NumpadMultiply: {
keyCode: 106,
code: 'NumpadMultiply',
key: '*',
location: 3,
},
NumpadAdd: { keyCode: 107, code: 'NumpadAdd', key: '+', location: 3 },
NumpadSubtract: {
keyCode: 109,
code: 'NumpadSubtract',
key: '-',
location: 3,
},
NumpadDivide: { keyCode: 111, code: 'NumpadDivide', key: '/', location: 3 },
F1: { keyCode: 112, code: 'F1', key: 'F1' },
F2: { keyCode: 113, code: 'F2', key: 'F2' },
F3: { keyCode: 114, code: 'F3', key: 'F3' },
F4: { keyCode: 115, code: 'F4', key: 'F4' },
F5: { keyCode: 116, code: 'F5', key: 'F5' },
F6: { keyCode: 117, code: 'F6', key: 'F6' },
F7: { keyCode: 118, code: 'F7', key: 'F7' },
F8: { keyCode: 119, code: 'F8', key: 'F8' },
F9: { keyCode: 120, code: 'F9', key: 'F9' },
F10: { keyCode: 121, code: 'F10', key: 'F10' },
F11: { keyCode: 122, code: 'F11', key: 'F11' },
F12: { keyCode: 123, code: 'F12', key: 'F12' },
F13: { keyCode: 124, code: 'F13', key: 'F13' },
F14: { keyCode: 125, code: 'F14', key: 'F14' },
F15: { keyCode: 126, code: 'F15', key: 'F15' },
F16: { keyCode: 127, code: 'F16', key: 'F16' },
F17: { keyCode: 128, code: 'F17', key: 'F17' },
F18: { keyCode: 129, code: 'F18', key: 'F18' },
F19: { keyCode: 130, code: 'F19', key: 'F19' },
F20: { keyCode: 131, code: 'F20', key: 'F20' },
F21: { keyCode: 132, code: 'F21', key: 'F21' },
F22: { keyCode: 133, code: 'F22', key: 'F22' },
F23: { keyCode: 134, code: 'F23', key: 'F23' },
F24: { keyCode: 135, code: 'F24', key: 'F24' },
NumLock: { keyCode: 144, code: 'NumLock', key: 'NumLock' },
ScrollLock: { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' },
AudioVolumeMute: {
keyCode: 173,
code: 'AudioVolumeMute',
key: 'AudioVolumeMute',
},
AudioVolumeDown: {
keyCode: 174,
code: 'AudioVolumeDown',
key: 'AudioVolumeDown',
},
AudioVolumeUp: { keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp' },
MediaTrackNext: {
keyCode: 176,
code: 'MediaTrackNext',
key: 'MediaTrackNext',
},
MediaTrackPrevious: {
keyCode: 177,
code: 'MediaTrackPrevious',
key: 'MediaTrackPrevious',
},
MediaStop: { keyCode: 178, code: 'MediaStop', key: 'MediaStop' },
MediaPlayPause: {
keyCode: 179,
code: 'MediaPlayPause',
key: 'MediaPlayPause',
},
Semicolon: { keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';' },
Equal: { keyCode: 187, code: 'Equal', shiftKey: '+', key: '=' },
NumpadEqual: { keyCode: 187, code: 'NumpadEqual', key: '=', location: 3 },
Comma: { keyCode: 188, code: 'Comma', shiftKey: '<', key: ',' },
Minus: { keyCode: 189, code: 'Minus', shiftKey: '_', key: '-' },
Period: { keyCode: 190, code: 'Period', shiftKey: '>', key: '.' },
Slash: { keyCode: 191, code: 'Slash', shiftKey: '?', key: '/' },
Backquote: { keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`' },
BracketLeft: { keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '[' },
Backslash: { keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\' },
BracketRight: { keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']' },
Quote: { keyCode: 222, code: 'Quote', shiftKey: '"', key: "'" },
AltGraph: { keyCode: 225, code: 'AltGraph', key: 'AltGraph' },
Props: { keyCode: 247, code: 'Props', key: 'CrSel' },
Cancel: { keyCode: 3, key: 'Cancel', code: 'Abort' },
Clear: { keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3 },
Shift: { keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1 },
Control: { keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1 },
Alt: { keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1 },
Accept: { keyCode: 30, key: 'Accept' },
ModeChange: { keyCode: 31, key: 'ModeChange' },
' ': { keyCode: 32, key: ' ', code: 'Space' },
Print: { keyCode: 42, key: 'Print' },
Execute: { keyCode: 43, key: 'Execute', code: 'Open' },
'\u0000': { keyCode: 46, key: '\u0000', code: 'NumpadDecimal', location: 3 },
a: { keyCode: 65, key: 'a', code: 'KeyA' },
b: { keyCode: 66, key: 'b', code: 'KeyB' },
c: { keyCode: 67, key: 'c', code: 'KeyC' },
d: { keyCode: 68, key: 'd', code: 'KeyD' },
e: { keyCode: 69, key: 'e', code: 'KeyE' },
f: { keyCode: 70, key: 'f', code: 'KeyF' },
g: { keyCode: 71, key: 'g', code: 'KeyG' },
h: { keyCode: 72, key: 'h', code: 'KeyH' },
i: { keyCode: 73, key: 'i', code: 'KeyI' },
j: { keyCode: 74, key: 'j', code: 'KeyJ' },
k: { keyCode: 75, key: 'k', code: 'KeyK' },
l: { keyCode: 76, key: 'l', code: 'KeyL' },
m: { keyCode: 77, key: 'm', code: 'KeyM' },
n: { keyCode: 78, key: 'n', code: 'KeyN' },
o: { keyCode: 79, key: 'o', code: 'KeyO' },
p: { keyCode: 80, key: 'p', code: 'KeyP' },
q: { keyCode: 81, key: 'q', code: 'KeyQ' },
r: { keyCode: 82, key: 'r', code: 'KeyR' },
s: { keyCode: 83, key: 's', code: 'KeyS' },
t: { keyCode: 84, key: 't', code: 'KeyT' },
u: { keyCode: 85, key: 'u', code: 'KeyU' },
v: { keyCode: 86, key: 'v', code: 'KeyV' },
w: { keyCode: 87, key: 'w', code: 'KeyW' },
x: { keyCode: 88, key: 'x', code: 'KeyX' },
y: { keyCode: 89, key: 'y', code: 'KeyY' },
z: { keyCode: 90, key: 'z', code: 'KeyZ' },
Meta: { keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1 },
'*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 },
'+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 },
'-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 },
'/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 },
';': { keyCode: 186, key: ';', code: 'Semicolon' },
'=': { keyCode: 187, key: '=', code: 'Equal' },
',': { keyCode: 188, key: ',', code: 'Comma' },
'.': { keyCode: 190, key: '.', code: 'Period' },
'`': { keyCode: 192, key: '`', code: 'Backquote' },
'[': { keyCode: 219, key: '[', code: 'BracketLeft' },
'\\': { keyCode: 220, key: '\\', code: 'Backslash' },
']': { keyCode: 221, key: ']', code: 'BracketRight' },
"'": { keyCode: 222, key: "'", code: 'Quote' },
Attn: { keyCode: 246, key: 'Attn' },
CrSel: { keyCode: 247, key: 'CrSel', code: 'Props' },
ExSel: { keyCode: 248, key: 'ExSel' },
EraseEof: { keyCode: 249, key: 'EraseEof' },
Play: { keyCode: 250, key: 'Play' },
ZoomOut: { keyCode: 251, key: 'ZoomOut' },
')': { keyCode: 48, key: ')', code: 'Digit0' },
'!': { keyCode: 49, key: '!', code: 'Digit1' },
'@': { keyCode: 50, key: '@', code: 'Digit2' },
'#': { keyCode: 51, key: '#', code: 'Digit3' },
$: { keyCode: 52, key: '$', code: 'Digit4' },
'%': { keyCode: 53, key: '%', code: 'Digit5' },
'^': { keyCode: 54, key: '^', code: 'Digit6' },
'&': { keyCode: 55, key: '&', code: 'Digit7' },
'(': { keyCode: 57, key: '(', code: 'Digit9' },
A: { keyCode: 65, key: 'A', code: 'KeyA' },
B: { keyCode: 66, key: 'B', code: 'KeyB' },
C: { keyCode: 67, key: 'C', code: 'KeyC' },
D: { keyCode: 68, key: 'D', code: 'KeyD' },
E: { keyCode: 69, key: 'E', code: 'KeyE' },
F: { keyCode: 70, key: 'F', code: 'KeyF' },
G: { keyCode: 71, key: 'G', code: 'KeyG' },
H: { keyCode: 72, key: 'H', code: 'KeyH' },
I: { keyCode: 73, key: 'I', code: 'KeyI' },
J: { keyCode: 74, key: 'J', code: 'KeyJ' },
K: { keyCode: 75, key: 'K', code: 'KeyK' },
L: { keyCode: 76, key: 'L', code: 'KeyL' },
M: { keyCode: 77, key: 'M', code: 'KeyM' },
N: { keyCode: 78, key: 'N', code: 'KeyN' },
O: { keyCode: 79, key: 'O', code: 'KeyO' },
P: { keyCode: 80, key: 'P', code: 'KeyP' },
Q: { keyCode: 81, key: 'Q', code: 'KeyQ' },
R: { keyCode: 82, key: 'R', code: 'KeyR' },
S: { keyCode: 83, key: 'S', code: 'KeyS' },
T: { keyCode: 84, key: 'T', code: 'KeyT' },
U: { keyCode: 85, key: 'U', code: 'KeyU' },
V: { keyCode: 86, key: 'V', code: 'KeyV' },
W: { keyCode: 87, key: 'W', code: 'KeyW' },
X: { keyCode: 88, key: 'X', code: 'KeyX' },
Y: { keyCode: 89, key: 'Y', code: 'KeyY' },
Z: { keyCode: 90, key: 'Z', code: 'KeyZ' },
':': { keyCode: 186, key: ':', code: 'Semicolon' },
'<': { keyCode: 188, key: '<', code: 'Comma' },
_: { keyCode: 189, key: '_', code: 'Minus' },
'>': { keyCode: 190, key: '>', code: 'Period' },
'?': { keyCode: 191, key: '?', code: 'Slash' },
'~': { keyCode: 192, key: '~', code: 'Backquote' },
'{': { keyCode: 219, key: '{', code: 'BracketLeft' },
'|': { keyCode: 220, key: '|', code: 'Backslash' },
'}': { keyCode: 221, key: '}', code: 'BracketRight' },
'"': { keyCode: 222, key: '"', code: 'Quote' },
SoftLeft: { key: 'SoftLeft', code: 'SoftLeft', location: 4 },
SoftRight: { key: 'SoftRight', code: 'SoftRight', location: 4 },
Camera: { keyCode: 44, key: 'Camera', code: 'Camera', location: 4 },
Call: { key: 'Call', code: 'Call', location: 4 },
EndCall: { keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4 },
VolumeDown: {
keyCode: 182,
key: 'VolumeDown',
code: 'VolumeDown',
location: 4,
},
VolumeUp: { keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4 },
};

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import * as NodeWebSocket from 'ws';
import type {ConnectionTransport} from './ConnectionTransport';
import type { ConnectionTransport } from './ConnectionTransport';
export class WebSocketTransport implements ConnectionTransport {
static create(url: string): Promise<WebSocketTransport> {
@ -35,13 +35,11 @@ export class WebSocketTransport implements ConnectionTransport {
constructor(ws: NodeWebSocket) {
this._ws = ws;
this._ws.addEventListener('message', event => {
if (this.onmessage)
this.onmessage.call(null, event.data);
this._ws.addEventListener('message', (event) => {
if (this.onmessage) this.onmessage.call(null, event.data);
});
this._ws.addEventListener('close', () => {
if (this.onclose)
this.onclose.call(null);
if (this.onclose) this.onclose.call(null);
});
// Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {});
@ -57,4 +55,3 @@ export class WebSocketTransport implements ConnectionTransport {
this._ws.close();
}
}

View File

@ -13,14 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {EventEmitter} from 'events';
import {debugError} from './helper';
import {ExecutionContext} from './ExecutionContext';
import {JSHandle} from './JSHandle';
import {CDPSession} from './Connection';
import { EventEmitter } from 'events';
import { debugError } from './helper';
import { ExecutionContext } from './ExecutionContext';
import { JSHandle } from './JSHandle';
import { CDPSession } from './Connection';
type ConsoleAPICalledCallback = (eventType: string, handles: JSHandle[], trace: Protocol.Runtime.StackTrace) => void;
type ExceptionThrownCallback = (details: Protocol.Runtime.ExceptionDetails) => void;
type ConsoleAPICalledCallback = (
eventType: string,
handles: JSHandle[],
trace: Protocol.Runtime.StackTrace
) => void;
type ExceptionThrownCallback = (
details: Protocol.Runtime.ExceptionDetails
) => void;
type JSHandleFactory = (obj: Protocol.Runtime.RemoteObject) => JSHandle;
export class Worker extends EventEmitter {
@ -29,24 +35,44 @@ export class Worker extends EventEmitter {
_executionContextPromise: Promise<ExecutionContext>;
_executionContextCallback: (value: ExecutionContext) => void;
constructor(client: CDPSession, url: string, consoleAPICalled: ConsoleAPICalledCallback, exceptionThrown: ExceptionThrownCallback) {
constructor(
client: CDPSession,
url: string,
consoleAPICalled: ConsoleAPICalledCallback,
exceptionThrown: ExceptionThrownCallback
) {
super();
this._client = client;
this._url = url;
this._executionContextPromise = new Promise<ExecutionContext>(x => this._executionContextCallback = x);
this._executionContextPromise = new Promise<ExecutionContext>(
(x) => (this._executionContextCallback = x)
);
let jsHandleFactory: JSHandleFactory;
this._client.once('Runtime.executionContextCreated', async event => {
this._client.once('Runtime.executionContextCreated', async (event) => {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
jsHandleFactory = remoteObject => new JSHandle(executionContext, client, remoteObject);
const executionContext = new ExecutionContext(client, event.context, null);
jsHandleFactory = (remoteObject) =>
new JSHandle(executionContext, client, remoteObject);
const executionContext = new ExecutionContext(
client,
event.context,
null
);
this._executionContextCallback(executionContext);
});
// This might fail if the target is closed before we recieve all execution contexts.
this._client.send('Runtime.enable', {}).catch(debugError);
this._client.on('Runtime.consoleAPICalled', event => consoleAPICalled(event.type, event.args.map(jsHandleFactory), event.stackTrace));
this._client.on('Runtime.exceptionThrown', exception => exceptionThrown(exception.exceptionDetails));
this._client.on('Runtime.consoleAPICalled', (event) =>
consoleAPICalled(
event.type,
event.args.map(jsHandleFactory),
event.stackTrace
)
);
this._client.on('Runtime.exceptionThrown', (exception) =>
exceptionThrown(exception.exceptionDetails)
);
}
url(): string {
@ -57,11 +83,23 @@ export class Worker extends EventEmitter {
return this._executionContextPromise;
}
async evaluate<ReturnType extends any>(pageFunction: Function|string, ...args: any[]): Promise<ReturnType> {
return (await this._executionContextPromise).evaluate<ReturnType>(pageFunction, ...args);
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: any[]
): Promise<ReturnType> {
return (await this._executionContextPromise).evaluate<ReturnType>(
pageFunction,
...args
);
}
async evaluateHandle(pageFunction: Function|string, ...args: any[]): Promise<JSHandle> {
return (await this._executionContextPromise).evaluateHandle(pageFunction, ...args);
async evaluateHandle(
pageFunction: Function | string,
...args: any[]
): Promise<JSHandle> {
return (await this._executionContextPromise).evaluateHandle(
pageFunction,
...args
);
}
}

View File

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TimeoutError} from './Errors';
import { TimeoutError } from './Errors';
import * as debug from 'debug';
import * as fs from 'fs';
import {CDPSession} from './Connection';
import {promisify} from 'util';
import { CDPSession } from './Connection';
import { promisify } from 'util';
const openAsync = promisify(fs.open);
const writeAsync = promisify(fs.write);
@ -26,22 +26,29 @@ const closeAsync = promisify(fs.close);
export const debugError = debug('puppeteer:error');
export function assert(value: unknown, message?: string): void {
if (!value)
throw new Error(message);
if (!value) throw new Error(message);
}
interface AnyClass {
prototype: object;
}
function getExceptionMessage(exceptionDetails: Protocol.Runtime.ExceptionDetails): string {
function getExceptionMessage(
exceptionDetails: Protocol.Runtime.ExceptionDetails
): string {
if (exceptionDetails.exception)
return exceptionDetails.exception.description || exceptionDetails.exception.value;
return (
exceptionDetails.exception.description || exceptionDetails.exception.value
);
let message = exceptionDetails.text;
if (exceptionDetails.stackTrace) {
for (const callframe of exceptionDetails.stackTrace.callFrames) {
const location = callframe.url + ':' + callframe.lineNumber + ':' + callframe.columnNumber;
const location =
callframe.url +
':' +
callframe.lineNumber +
':' +
callframe.columnNumber;
const functionName = callframe.functionName || '<anonymous>';
message += `\n at ${functionName} (${location})`;
}
@ -49,7 +56,9 @@ function getExceptionMessage(exceptionDetails: Protocol.Runtime.ExceptionDetails
return message;
}
function valueFromRemoteObject(remoteObject: Protocol.Runtime.RemoteObject): any {
function valueFromRemoteObject(
remoteObject: Protocol.Runtime.RemoteObject
): any {
assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
if (remoteObject.unserializableValue) {
if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined')
@ -64,36 +73,55 @@ function valueFromRemoteObject(remoteObject: Protocol.Runtime.RemoteObject): any
case '-Infinity':
return -Infinity;
default:
throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue);
throw new Error(
'Unsupported unserializable value: ' +
remoteObject.unserializableValue
);
}
}
return remoteObject.value;
}
async function releaseObject(client: CDPSession, remoteObject: Protocol.Runtime.RemoteObject): Promise<void> {
if (!remoteObject.objectId)
return;
await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
async function releaseObject(
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject
): Promise<void> {
if (!remoteObject.objectId) return;
await client
.send('Runtime.releaseObject', { objectId: remoteObject.objectId })
.catch((error) => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
}
function installAsyncStackHooks(classType: AnyClass): void {
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function' || method.constructor.name !== 'AsyncFunction')
if (
methodName === 'constructor' ||
typeof methodName !== 'string' ||
methodName.startsWith('_') ||
typeof method !== 'function' ||
method.constructor.name !== 'AsyncFunction'
)
continue;
Reflect.set(classType.prototype, methodName, function(...args) {
Reflect.set(classType.prototype, methodName, function (...args) {
const syncStack = {
stack: ''
stack: '',
};
Error.captureStackTrace(syncStack);
return method.call(this, ...args).catch(error => {
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
return method.call(this, ...args).catch((error) => {
const stack = syncStack.stack.substring(
syncStack.stack.indexOf('\n') + 1
);
const clientStack = stack.substring(stack.indexOf('\n'));
if (error instanceof Error && error.stack && !error.stack.includes(clientStack))
if (
error instanceof Error &&
error.stack &&
!error.stack.includes(clientStack)
)
error.stack += '\n -- ASYNC --\n' + stack;
throw error;
});
@ -107,12 +135,22 @@ export interface PuppeteerEventListener {
handler: (...args: any[]) => void;
}
function addEventListener(emitter: NodeJS.EventEmitter, eventName: string|symbol, handler: (...args: any[]) => void): PuppeteerEventListener {
function addEventListener(
emitter: NodeJS.EventEmitter,
eventName: string | symbol,
handler: (...args: any[]) => void
): PuppeteerEventListener {
emitter.on(eventName, handler);
return {emitter, eventName, handler};
return { emitter, eventName, handler };
}
function removeEventListeners(listeners: Array<{emitter: NodeJS.EventEmitter; eventName: string|symbol; handler: (...args: any[]) => void}>): void {
function removeEventListeners(
listeners: Array<{
emitter: NodeJS.EventEmitter;
eventName: string | symbol;
handler: (...args: any[]) => void;
}>
): void {
for (const listener of listeners)
listener.emitter.removeListener(listener.eventName, listener.handler);
listeners.length = 0;
@ -126,35 +164,44 @@ function isNumber(obj: unknown): obj is number {
return typeof obj === 'number' || obj instanceof Number;
}
async function waitForEvent<T extends any>(emitter: NodeJS.EventEmitter, eventName: string|symbol, predicate: (event: T) => boolean, timeout: number, abortPromise: Promise<Error>): Promise<T> {
async function waitForEvent<T extends any>(
emitter: NodeJS.EventEmitter,
eventName: string | symbol,
predicate: (event: T) => boolean,
timeout: number,
abortPromise: Promise<Error>
): Promise<T> {
let eventTimeout, resolveCallback, rejectCallback;
const promise = new Promise<T>((resolve, reject) => {
resolveCallback = resolve;
rejectCallback = reject;
});
const listener = addEventListener(emitter, eventName, event => {
if (!predicate(event))
return;
const listener = addEventListener(emitter, eventName, (event) => {
if (!predicate(event)) return;
resolveCallback(event);
});
if (timeout) {
eventTimeout = setTimeout(() => {
rejectCallback(new TimeoutError('Timeout exceeded while waiting for event'));
rejectCallback(
new TimeoutError('Timeout exceeded while waiting for event')
);
}, timeout);
}
function cleanup(): void {
removeEventListeners([listener]);
clearTimeout(eventTimeout);
}
const result = await Promise.race([promise, abortPromise]).then(r => {
cleanup();
return r;
}, error => {
cleanup();
throw error;
});
if (result instanceof Error)
throw result;
const result = await Promise.race([promise, abortPromise]).then(
(r) => {
cleanup();
return r;
},
(error) => {
cleanup();
throw error;
}
);
if (result instanceof Error) throw result;
return result;
}
@ -166,46 +213,53 @@ function evaluationString(fun: Function | string, ...args: unknown[]): string {
}
function serializeArgument(arg: unknown): string {
if (Object.is(arg, undefined))
return 'undefined';
if (Object.is(arg, undefined)) return 'undefined';
return JSON.stringify(arg);
}
return `(${fun})(${args.map(serializeArgument).join(',')})`;
}
async function waitWithTimeout<T extends any>(promise: Promise<T>, taskName: string, timeout: number): Promise<T> {
async function waitWithTimeout<T extends any>(
promise: Promise<T>,
taskName: string,
timeout: number
): Promise<T> {
let reject;
const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`);
const timeoutPromise = new Promise<T>((resolve, x) => reject = x);
const timeoutError = new TimeoutError(
`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`
);
const timeoutPromise = new Promise<T>((resolve, x) => (reject = x));
let timeoutTimer = null;
if (timeout)
timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
if (timeout) timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeoutTimer)
clearTimeout(timeoutTimer);
if (timeoutTimer) clearTimeout(timeoutTimer);
}
}
async function readProtocolStream(client: CDPSession, handle: string, path?: string): Promise<Buffer> {
async function readProtocolStream(
client: CDPSession,
handle: string,
path?: string
): Promise<Buffer> {
let eof = false;
let file;
if (path)
file = await openAsync(path, 'w');
if (path) file = await openAsync(path, 'w');
const bufs = [];
while (!eof) {
const response = await client.send('IO.read', {handle});
const response = await client.send('IO.read', { handle });
eof = response.eof;
const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined);
const buf = Buffer.from(
response.data,
response.base64Encoded ? 'base64' : undefined
);
bufs.push(buf);
if (path)
await writeAsync(file, buf);
if (path) await writeAsync(file, buf);
}
if (path)
await closeAsync(file);
await client.send('IO.close', {handle});
if (path) await closeAsync(file);
await client.send('IO.close', { handle });
let resultBuffer = null;
try {
resultBuffer = Buffer.concat(bufs);
@ -214,7 +268,6 @@ async function readProtocolStream(client: CDPSession, handle: string, path?: str
}
}
export const helper = {
promisify,
evaluationString,

View File

@ -14,38 +14,42 @@
* limitations under the License.
*/
const {waitEvent} = require('./utils');
const { waitEvent } = require('./utils');
const expect = require('expect');
const {getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describeChromeOnly('Target.createCDPSession', function() {
describeChromeOnly('Target.createCDPSession', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should work', async() => {
const {page} = getTestState();
it('should work', async () => {
const { page } = getTestState();
const client = await page.target().createCDPSession();
await Promise.all([
client.send('Runtime.enable'),
client.send('Runtime.evaluate', {expression: 'window.foo = "bar"'})
client.send('Runtime.evaluate', { expression: 'window.foo = "bar"' }),
]);
const foo = await page.evaluate(() => window.foo);
expect(foo).toBe('bar');
});
it('should send events', async() => {
const {page, server} = getTestState();
it('should send events', async () => {
const { page, server } = getTestState();
const client = await page.target().createCDPSession();
await client.send('Network.enable');
const events = [];
client.on('Network.requestWillBeSent', event => events.push(event));
client.on('Network.requestWillBeSent', (event) => events.push(event));
await page.goto(server.EMPTY_PAGE);
expect(events.length).toBe(1);
});
it('should enable and disable domains independently', async() => {
const {page} = getTestState();
it('should enable and disable domains independently', async () => {
const { page } = getTestState();
const client = await page.target().createCDPSession();
await client.send('Runtime.enable');
@ -56,32 +60,38 @@ describeChromeOnly('Target.createCDPSession', function() {
// generate a script in page and wait for the event.
const [event] = await Promise.all([
waitEvent(client, 'Debugger.scriptParsed'),
page.evaluate('//# sourceURL=foo.js')
page.evaluate('//# sourceURL=foo.js'),
]);
// expect events to be dispatched.
// expect events to be dispatched.
expect(event.url).toBe('foo.js');
});
it('should be able to detach session', async() => {
const {page} = getTestState();
it('should be able to detach session', async () => {
const { page } = getTestState();
const client = await page.target().createCDPSession();
await client.send('Runtime.enable');
const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true});
const evalResponse = await client.send('Runtime.evaluate', {
expression: '1 + 2',
returnByValue: true,
});
expect(evalResponse.result.value).toBe(3);
await client.detach();
let error = null;
try {
await client.send('Runtime.evaluate', {expression: '3 + 1', returnByValue: true});
await client.send('Runtime.evaluate', {
expression: '3 + 1',
returnByValue: true,
});
} catch (error_) {
error = error_;
}
expect(error.message).toContain('Session closed.');
});
it('should throw nice errors', async() => {
const {page} = getTestState();
it('should throw nice errors', async () => {
const { page } = getTestState();
const client = await page.target().createCDPSession();
const error = await theSourceOfTheProblems().catch(error => error);
const error = await theSourceOfTheProblems().catch((error) => error);
expect(error.stack).toContain('theSourceOfTheProblems');
expect(error.message).toContain('ThisCommand.DoesNotExist');

View File

@ -15,14 +15,18 @@
*/
const expect = require('expect');
const {getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describeFailsFirefox('Accessibility', function() {
describeFailsFirefox('Accessibility', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should work', async() => {
const {page, isFirefox} = getTestState();
it('should work', async () => {
const { page, isFirefox } = getTestState();
await page.setContent(`
<head>
@ -46,210 +50,283 @@ describeFailsFirefox('Accessibility', function() {
</body>`);
await page.focus('[placeholder="Empty input"]');
const golden = isFirefox ? {
role: 'document',
name: 'Accessibility Test',
children: [
{role: 'text leaf', name: 'Hello World'},
{role: 'heading', name: 'Inputs', level: 1},
{role: 'entry', name: 'Empty input', focused: true},
{role: 'entry', name: 'readonly input', readonly: true},
{role: 'entry', name: 'disabled input', disabled: true},
{role: 'entry', name: 'Input with whitespace', value: ' '},
{role: 'entry', name: '', value: 'value only'},
{role: 'entry', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name
{role: 'entry', name: '', value: 'and a value', description: 'This is a description!'}, // and here
{role: 'combobox', name: '', value: 'First Option', haspopup: true, children: [
{role: 'combobox option', name: 'First Option', selected: true},
{role: 'combobox option', name: 'Second Option'}]
}]
} : {
role: 'WebArea',
name: 'Accessibility Test',
children: [
{role: 'text', name: 'Hello World'},
{role: 'heading', name: 'Inputs', level: 1},
{role: 'textbox', name: 'Empty input', focused: true},
{role: 'textbox', name: 'readonly input', readonly: true},
{role: 'textbox', name: 'disabled input', disabled: true},
{role: 'textbox', name: 'Input with whitespace', value: ' '},
{role: 'textbox', name: '', value: 'value only'},
{role: 'textbox', name: 'placeholder', value: 'and a value'},
{role: 'textbox', name: 'placeholder', value: 'and a value', description: 'This is a description!'},
{role: 'combobox', name: '', value: 'First Option', children: [
{role: 'menuitem', name: 'First Option', selected: true},
{role: 'menuitem', name: 'Second Option'}]
}]
};
const golden = isFirefox
? {
role: 'document',
name: 'Accessibility Test',
children: [
{ role: 'text leaf', name: 'Hello World' },
{ role: 'heading', name: 'Inputs', level: 1 },
{ role: 'entry', name: 'Empty input', focused: true },
{ role: 'entry', name: 'readonly input', readonly: true },
{ role: 'entry', name: 'disabled input', disabled: true },
{ role: 'entry', name: 'Input with whitespace', value: ' ' },
{ role: 'entry', name: '', value: 'value only' },
{ role: 'entry', name: '', value: 'and a value' }, // firefox doesn't use aria-placeholder for the name
{
role: 'entry',
name: '',
value: 'and a value',
description: 'This is a description!',
}, // and here
{
role: 'combobox',
name: '',
value: 'First Option',
haspopup: true,
children: [
{
role: 'combobox option',
name: 'First Option',
selected: true,
},
{ role: 'combobox option', name: 'Second Option' },
],
},
],
}
: {
role: 'WebArea',
name: 'Accessibility Test',
children: [
{ role: 'text', name: 'Hello World' },
{ role: 'heading', name: 'Inputs', level: 1 },
{ role: 'textbox', name: 'Empty input', focused: true },
{ role: 'textbox', name: 'readonly input', readonly: true },
{ role: 'textbox', name: 'disabled input', disabled: true },
{ role: 'textbox', name: 'Input with whitespace', value: ' ' },
{ role: 'textbox', name: '', value: 'value only' },
{ role: 'textbox', name: 'placeholder', value: 'and a value' },
{
role: 'textbox',
name: 'placeholder',
value: 'and a value',
description: 'This is a description!',
},
{
role: 'combobox',
name: '',
value: 'First Option',
children: [
{ role: 'menuitem', name: 'First Option', selected: true },
{ role: 'menuitem', name: 'Second Option' },
],
},
],
};
expect(await page.accessibility.snapshot()).toEqual(golden);
});
it('should report uninteresting nodes', async() => {
const {page, isFirefox} = getTestState();
it('should report uninteresting nodes', async () => {
const { page, isFirefox } = getTestState();
await page.setContent(`<textarea>hi</textarea>`);
await page.focus('textarea');
const golden = isFirefox ? {
role: 'entry',
name: '',
value: 'hi',
focused: true,
multiline: true,
children: [{
role: 'text leaf',
name: 'hi'
}]
} : {
role: 'textbox',
name: '',
value: 'hi',
focused: true,
multiline: true,
children: [{
role: 'generic',
name: '',
children: [{
role: 'text', name: 'hi'
}]
}]
};
expect(findFocusedNode(await page.accessibility.snapshot({interestingOnly: false}))).toEqual(golden);
const golden = isFirefox
? {
role: 'entry',
name: '',
value: 'hi',
focused: true,
multiline: true,
children: [
{
role: 'text leaf',
name: 'hi',
},
],
}
: {
role: 'textbox',
name: '',
value: 'hi',
focused: true,
multiline: true,
children: [
{
role: 'generic',
name: '',
children: [
{
role: 'text',
name: 'hi',
},
],
},
],
};
expect(
findFocusedNode(
await page.accessibility.snapshot({ interestingOnly: false })
)
).toEqual(golden);
});
it('roledescription', async() => {
const {page} = getTestState();
it('roledescription', async () => {
const { page } = getTestState();
await page.setContent('<div tabIndex=-1 aria-roledescription="foo">Hi</div>');
await page.setContent(
'<div tabIndex=-1 aria-roledescription="foo">Hi</div>'
);
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].roledescription).toEqual('foo');
});
it('orientation', async() => {
const {page} = getTestState();
it('orientation', async () => {
const { page } = getTestState();
await page.setContent('<a href="" role="slider" aria-orientation="vertical">11</a>');
await page.setContent(
'<a href="" role="slider" aria-orientation="vertical">11</a>'
);
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].orientation).toEqual('vertical');
});
it('autocomplete', async() => {
const {page} = getTestState();
it('autocomplete', async () => {
const { page } = getTestState();
await page.setContent('<input type="number" aria-autocomplete="list" />');
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].autocomplete).toEqual('list');
});
it('multiselectable', async() => {
const {page} = getTestState();
it('multiselectable', async () => {
const { page } = getTestState();
await page.setContent('<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>');
await page.setContent(
'<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>'
);
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].multiselectable).toEqual(true);
});
it('keyshortcuts', async() => {
const {page} = getTestState();
it('keyshortcuts', async () => {
const { page } = getTestState();
await page.setContent('<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>');
await page.setContent(
'<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>'
);
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].keyshortcuts).toEqual('foo');
});
describe('filtering children of leaf nodes', function() {
it('should not report text nodes inside controls', async() => {
const {page, isFirefox} = getTestState();
describe('filtering children of leaf nodes', function () {
it('should not report text nodes inside controls', async () => {
const { page, isFirefox } = getTestState();
await page.setContent(`
<div role="tablist">
<div role="tab" aria-selected="true"><b>Tab1</b></div>
<div role="tab">Tab2</div>
</div>`);
const golden = isFirefox ? {
role: 'document',
name: '',
children: [{
role: 'pagetab',
name: 'Tab1',
selected: true
}, {
role: 'pagetab',
name: 'Tab2'
}]
} : {
role: 'WebArea',
name: '',
children: [{
role: 'tab',
name: 'Tab1',
selected: true
}, {
role: 'tab',
name: 'Tab2'
}]
};
const golden = isFirefox
? {
role: 'document',
name: '',
children: [
{
role: 'pagetab',
name: 'Tab1',
selected: true,
},
{
role: 'pagetab',
name: 'Tab2',
},
],
}
: {
role: 'WebArea',
name: '',
children: [
{
role: 'tab',
name: 'Tab1',
selected: true,
},
{
role: 'tab',
name: 'Tab2',
},
],
};
expect(await page.accessibility.snapshot()).toEqual(golden);
});
it('rich text editable fields should have children', async() => {
const {page, isFirefox} = getTestState();
it('rich text editable fields should have children', async () => {
const { page, isFirefox } = getTestState();
await page.setContent(`
<div contenteditable="true">
Edit this image: <img src="fakeimage.png" alt="my fake image">
</div>`);
const golden = isFirefox ? {
role: 'section',
name: '',
children: [{
role: 'text leaf',
name: 'Edit this image: '
}, {
role: 'text',
name: 'my fake image'
}]
} : {
role: 'generic',
name: '',
value: 'Edit this image: ',
children: [{
role: 'text',
name: 'Edit this image:'
}, {
role: 'img',
name: 'my fake image'
}]
};
const golden = isFirefox
? {
role: 'section',
name: '',
children: [
{
role: 'text leaf',
name: 'Edit this image: ',
},
{
role: 'text',
name: 'my fake image',
},
],
}
: {
role: 'generic',
name: '',
value: 'Edit this image: ',
children: [
{
role: 'text',
name: 'Edit this image:',
},
{
role: 'img',
name: 'my fake image',
},
],
};
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden);
});
it('rich text editable fields with role should have children', async() => {
const {page, isFirefox} = getTestState();
it('rich text editable fields with role should have children', async () => {
const { page, isFirefox } = getTestState();
await page.setContent(`
<div contenteditable="true" role='textbox'>
Edit this image: <img src="fakeimage.png" alt="my fake image">
</div>`);
const golden = isFirefox ? {
role: 'entry',
name: '',
value: 'Edit this image: my fake image',
children: [{
role: 'text',
name: 'my fake image'
}]
} : {
role: 'textbox',
name: '',
value: 'Edit this image: ',
children: [{
role: 'text',
name: 'Edit this image:'
}, {
role: 'img',
name: 'my fake image'
}]
};
const golden = isFirefox
? {
role: 'entry',
name: '',
value: 'Edit this image: my fake image',
children: [
{
role: 'text',
name: 'my fake image',
},
],
}
: {
role: 'textbox',
name: '',
value: 'Edit this image: ',
children: [
{
role: 'text',
name: 'Edit this image:',
},
{
role: 'img',
name: 'my fake image',
},
],
};
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden);
});
// Firefox does not support contenteditable="plaintext-only".
describeFailsFirefox('plaintext contenteditable', function() {
it('plain text field with role should not have children', async() => {
const {page} = getTestState();
describeFailsFirefox('plaintext contenteditable', function () {
it('plain text field with role should not have children', async () => {
const { page } = getTestState();
await page.setContent(`
<div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
@ -257,119 +334,125 @@ describeFailsFirefox('Accessibility', function() {
expect(snapshot.children[0]).toEqual({
role: 'textbox',
name: '',
value: 'Edit this image:'
value: 'Edit this image:',
});
});
it('plain text field without role should not have content', async() => {
const {page} = getTestState();
it('plain text field without role should not have content', async () => {
const { page } = getTestState();
await page.setContent(`
<div contenteditable="plaintext-only">Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual({
role: 'generic',
name: ''
name: '',
});
});
it('plain text field with tabindex and without role should not have content', async() => {
const {page} = getTestState();
it('plain text field with tabindex and without role should not have content', async () => {
const { page } = getTestState();
await page.setContent(`
<div contenteditable="plaintext-only" tabIndex=0>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual({
role: 'generic',
name: ''
name: '',
});
});
});
it('non editable textbox with role and tabIndex and label should not have children', async() => {
const {page, isFirefox} = getTestState();
it('non editable textbox with role and tabIndex and label should not have children', async () => {
const { page, isFirefox } = getTestState();
await page.setContent(`
<div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox">
this is the inner content
<img alt="yo" src="fakeimg.png">
</div>`);
const golden = isFirefox ? {
role: 'entry',
name: 'my favorite textbox',
value: 'this is the inner content yo'
} : {
role: 'textbox',
name: 'my favorite textbox',
value: 'this is the inner content '
};
const golden = isFirefox
? {
role: 'entry',
name: 'my favorite textbox',
value: 'this is the inner content yo',
}
: {
role: 'textbox',
name: 'my favorite textbox',
value: 'this is the inner content ',
};
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden);
});
it('checkbox with and tabIndex and label should not have children', async() => {
const {page, isFirefox} = getTestState();
it('checkbox with and tabIndex and label should not have children', async () => {
const { page, isFirefox } = getTestState();
await page.setContent(`
<div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox">
this is the inner content
<img alt="yo" src="fakeimg.png">
</div>`);
const golden = isFirefox ? {
role: 'checkbutton',
name: 'my favorite checkbox',
checked: true
} : {
role: 'checkbox',
name: 'my favorite checkbox',
checked: true
};
const golden = isFirefox
? {
role: 'checkbutton',
name: 'my favorite checkbox',
checked: true,
}
: {
role: 'checkbox',
name: 'my favorite checkbox',
checked: true,
};
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden);
});
it('checkbox without label should not have children', async() => {
const {page, isFirefox} = getTestState();
it('checkbox without label should not have children', async () => {
const { page, isFirefox } = getTestState();
await page.setContent(`
<div role="checkbox" aria-checked="true">
this is the inner content
<img alt="yo" src="fakeimg.png">
</div>`);
const golden = isFirefox ? {
role: 'checkbutton',
name: 'this is the inner content yo',
checked: true
} : {
role: 'checkbox',
name: 'this is the inner content yo',
checked: true
};
const golden = isFirefox
? {
role: 'checkbutton',
name: 'this is the inner content yo',
checked: true,
}
: {
role: 'checkbox',
name: 'this is the inner content yo',
checked: true,
};
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden);
});
describe('root option', function() {
it('should work a button', async() => {
const {page} = getTestState();
describe('root option', function () {
it('should work a button', async () => {
const { page } = getTestState();
await page.setContent(`<button>My Button</button>`);
const button = await page.$('button');
expect(await page.accessibility.snapshot({root: button})).toEqual({
expect(await page.accessibility.snapshot({ root: button })).toEqual({
role: 'button',
name: 'My Button'
name: 'My Button',
});
});
it('should work an input', async() => {
const {page} = getTestState();
it('should work an input', async () => {
const { page } = getTestState();
await page.setContent(`<input title="My Input" value="My Value">`);
const input = await page.$('input');
expect(await page.accessibility.snapshot({root: input})).toEqual({
expect(await page.accessibility.snapshot({ root: input })).toEqual({
role: 'textbox',
name: 'My Input',
value: 'My Value'
value: 'My Value',
});
});
it('should work a menu', async() => {
const {page} = getTestState();
it('should work a menu', async () => {
const { page } = getTestState();
await page.setContent(`
<div role="menu" title="My Menu">
@ -380,39 +463,45 @@ describeFailsFirefox('Accessibility', function() {
`);
const menu = await page.$('div[role="menu"]');
expect(await page.accessibility.snapshot({root: menu})).toEqual({
expect(await page.accessibility.snapshot({ root: menu })).toEqual({
role: 'menu',
name: 'My Menu',
children:
[ {role: 'menuitem', name: 'First Item'},
{role: 'menuitem', name: 'Second Item'},
{role: 'menuitem', name: 'Third Item'} ]
children: [
{ role: 'menuitem', name: 'First Item' },
{ role: 'menuitem', name: 'Second Item' },
{ role: 'menuitem', name: 'Third Item' },
],
});
});
it('should return null when the element is no longer in DOM', async() => {
const {page} = getTestState();
it('should return null when the element is no longer in DOM', async () => {
const { page } = getTestState();
await page.setContent(`<button>My Button</button>`);
const button = await page.$('button');
await page.$eval('button', button => button.remove());
expect(await page.accessibility.snapshot({root: button})).toEqual(null);
await page.$eval('button', (button) => button.remove());
expect(await page.accessibility.snapshot({ root: button })).toEqual(
null
);
});
it('should support the interestingOnly option', async() => {
const {page} = getTestState();
it('should support the interestingOnly option', async () => {
const { page } = getTestState();
await page.setContent(`<div><button>My Button</button></div>`);
const div = await page.$('div');
expect(await page.accessibility.snapshot({root: div})).toEqual(null);
expect(await page.accessibility.snapshot({root: div, interestingOnly: false})).toEqual({
expect(await page.accessibility.snapshot({ root: div })).toEqual(null);
expect(
await page.accessibility.snapshot({
root: div,
interestingOnly: false,
})
).toEqual({
role: 'generic',
name: '',
children: [
{
role: 'button',
name: 'My Button',
children: [
{role: 'text', name: 'My Button'},
],
children: [{ role: 'text', name: 'My Button' }],
},
],
});
@ -420,12 +509,10 @@ describeFailsFirefox('Accessibility', function() {
});
});
function findFocusedNode(node) {
if (node.focused)
return node;
if (node.focused) return node;
for (const child of node.children || []) {
const focusedChild = findFocusedNode(child);
if (focusedChild)
return focusedChild;
if (focusedChild) return focusedChild;
}
return null;
}

View File

@ -1,5 +1,5 @@
const {describe, it} = require('mocha');
const {getCoverageResults} = require('./coverage-utils');
const { describe, it } = require('mocha');
const { getCoverageResults } = require('./coverage-utils');
const expect = require('expect');
describe('API coverage test', () => {
@ -9,11 +9,12 @@ describe('API coverage test', () => {
const coverageMap = getCoverageResults();
const missingMethods = [];
for (const method of coverageMap.keys()) {
if (!coverageMap.get(method))
missingMethods.push(method);
if (!coverageMap.get(method)) missingMethods.push(method);
}
if (missingMethods.length) {
console.error('\nCoverage check failed: not all API methods called. See above output for list of missing methods.');
console.error(
'\nCoverage check failed: not all API methods called. See above output for list of missing methods.'
);
console.error(missingMethods.join('\n'));
}

View File

@ -1,2 +1,2 @@
import num from './es6module.js';
window.__es6injected = num;
window.__es6injected = num;

View File

@ -1 +1 @@
export default 42;
export default 42;

View File

@ -1,2 +1,2 @@
import num from './es6/es6module.js';
window.__es6injected = num;
window.__es6injected = num;

View File

@ -1,2 +1,2 @@
window.__injected = 42;
window.__injectedError = new Error('hi');
window.__injectedError = new Error('hi');

View File

@ -1,6 +1,6 @@
// This injects a box into the page that moves with the mouse;
// Useful for debugging
(function(){
(function () {
const box = document.createElement('div');
box.classList.add('mouse-helper');
const styleElement = document.createElement('style');
@ -42,19 +42,31 @@
`;
document.head.appendChild(styleElement);
document.body.appendChild(box);
document.addEventListener('mousemove', event => {
box.style.left = event.pageX + 'px';
box.style.top = event.pageY + 'px';
updateButtons(event.buttons);
}, true);
document.addEventListener('mousedown', event => {
updateButtons(event.buttons);
box.classList.add('button-' + event.which);
}, true);
document.addEventListener('mouseup', event => {
updateButtons(event.buttons);
box.classList.remove('button-' + event.which);
}, true);
document.addEventListener(
'mousemove',
(event) => {
box.style.left = event.pageX + 'px';
box.style.top = event.pageY + 'px';
updateButtons(event.buttons);
},
true
);
document.addEventListener(
'mousedown',
(event) => {
updateButtons(event.buttons);
box.classList.add('button-' + event.which);
},
true
);
document.addEventListener(
'mouseup',
(event) => {
updateButtons(event.buttons);
box.classList.remove('button-' + event.which);
},
true
);
function updateButtons(buttons) {
for (let i = 0; i < 5; i++)
box.classList.toggle('button-' + i, buttons & (1 << i));

View File

@ -1,7 +1,7 @@
self.addEventListener('fetch', event => {
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request));
});
self.addEventListener('activate', event => {
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});

View File

@ -1,3 +1,2 @@
console.log('hey from the content-script');
self.thisIsTheContentScript = true;

View File

@ -4,13 +4,13 @@ function workerFunction() {
return 'worker function result';
}
self.addEventListener('message', event => {
self.addEventListener('message', (event) => {
console.log('got this data: ' + event.data);
});
(async function() {
(async function () {
while (true) {
self.postMessage(workerFunction.toString());
await new Promise(x => setTimeout(x, 100));
await new Promise((x) => setTimeout(x, 100));
}
})();
})();

View File

@ -15,14 +15,14 @@
*/
const expect = require('expect');
const {getTestState, setupTestBrowserHooks} = require('./mocha-utils');
const { getTestState, setupTestBrowserHooks } = require('./mocha-utils');
describe('Browser specs', function() {
describe('Browser specs', function () {
setupTestBrowserHooks();
describe('Browser.version', function() {
it('should return whether we are in headless', async() => {
const {browser, isHeadless} = getTestState();
describe('Browser.version', function () {
it('should return whether we are in headless', async () => {
const { browser, isHeadless } = getTestState();
const version = await browser.version();
expect(version.length).toBeGreaterThan(0);
@ -30,51 +30,49 @@ describe('Browser specs', function() {
});
});
describe('Browser.userAgent', function() {
it('should include WebKit', async() => {
const {browser, isChrome} = getTestState();
describe('Browser.userAgent', function () {
it('should include WebKit', async () => {
const { browser, isChrome } = getTestState();
const userAgent = await browser.userAgent();
expect(userAgent.length).toBeGreaterThan(0);
if (isChrome)
expect(userAgent).toContain('WebKit');
else
expect(userAgent).toContain('Gecko');
if (isChrome) expect(userAgent).toContain('WebKit');
else expect(userAgent).toContain('Gecko');
});
});
describe('Browser.target', function() {
it('should return browser target', async() => {
const {browser} = getTestState();
describe('Browser.target', function () {
it('should return browser target', async () => {
const { browser } = getTestState();
const target = browser.target();
expect(target.type()).toBe('browser');
});
});
describe('Browser.process', function() {
it('should return child_process instance', async() => {
const {browser} = getTestState();
describe('Browser.process', function () {
it('should return child_process instance', async () => {
const { browser } = getTestState();
const process = await browser.process();
expect(process.pid).toBeGreaterThan(0);
});
it('should not return child_process for remote browser', async() => {
const {browser, puppeteer} = getTestState();
it('should not return child_process for remote browser', async () => {
const { browser, puppeteer } = getTestState();
const browserWSEndpoint = browser.wsEndpoint();
const remoteBrowser = await puppeteer.connect({browserWSEndpoint});
const remoteBrowser = await puppeteer.connect({ browserWSEndpoint });
expect(remoteBrowser.process()).toBe(null);
remoteBrowser.disconnect();
});
});
describe('Browser.isConnected', () => {
it('should set the browser connected state', async() => {
const {browser, puppeteer} = getTestState();
it('should set the browser connected state', async () => {
const { browser, puppeteer } = getTestState();
const browserWSEndpoint = browser.wsEndpoint();
const newBrowser = await puppeteer.connect({browserWSEndpoint});
const newBrowser = await puppeteer.connect({ browserWSEndpoint });
expect(newBrowser.isConnected()).toBe(true);
newBrowser.disconnect();
expect(newBrowser.isConnected()).toBe(false);

View File

@ -15,23 +15,23 @@
*/
const expect = require('expect');
const {getTestState, setupTestBrowserHooks} = require('./mocha-utils');
const { getTestState, setupTestBrowserHooks } = require('./mocha-utils');
const utils = require('./utils');
describe('BrowserContext', function() {
describe('BrowserContext', function () {
setupTestBrowserHooks();
itFailsFirefox('should have default context', async() => {
const {browser} = getTestState();
itFailsFirefox('should have default context', async () => {
const { browser } = getTestState();
expect(browser.browserContexts().length).toEqual(1);
const defaultContext = browser.browserContexts()[0];
expect(defaultContext.isIncognito()).toBe(false);
let error = null;
await defaultContext.close().catch(error_ => error = error_);
await defaultContext.close().catch((error_) => (error = error_));
expect(browser.defaultBrowserContext()).toBe(defaultContext);
expect(error.message).toContain('cannot be closed');
});
itFailsFirefox('should create new incognito context', async() => {
const {browser} = getTestState();
itFailsFirefox('should create new incognito context', async () => {
const { browser } = getTestState();
expect(browser.browserContexts().length).toBe(1);
const context = await browser.createIncognitoBrowserContext();
@ -41,57 +41,68 @@ describe('BrowserContext', function() {
await context.close();
expect(browser.browserContexts().length).toBe(1);
});
itFailsFirefox('should close all belonging targets once closing context', async() => {
const {browser} = getTestState();
itFailsFirefox(
'should close all belonging targets once closing context',
async () => {
const { browser } = getTestState();
expect((await browser.pages()).length).toBe(1);
expect((await browser.pages()).length).toBe(1);
const context = await browser.createIncognitoBrowserContext();
await context.newPage();
expect((await browser.pages()).length).toBe(2);
expect((await context.pages()).length).toBe(1);
const context = await browser.createIncognitoBrowserContext();
await context.newPage();
expect((await browser.pages()).length).toBe(2);
expect((await context.pages()).length).toBe(1);
await context.close();
expect((await browser.pages()).length).toBe(1);
});
itFailsFirefox('window.open should use parent tab context', async() => {
const {browser, server} = getTestState();
await context.close();
expect((await browser.pages()).length).toBe(1);
}
);
itFailsFirefox('window.open should use parent tab context', async () => {
const { browser, server } = getTestState();
const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const [popupTarget] = await Promise.all([
utils.waitEvent(browser, 'targetcreated'),
page.evaluate(url => window.open(url), server.EMPTY_PAGE)
page.evaluate((url) => window.open(url), server.EMPTY_PAGE),
]);
expect(popupTarget.browserContext()).toBe(context);
await context.close();
});
itFailsFirefox('should fire target events', async() => {
const {browser, server} = getTestState();
itFailsFirefox('should fire target events', async () => {
const { browser, server } = getTestState();
const context = await browser.createIncognitoBrowserContext();
const events = [];
context.on('targetcreated', target => events.push('CREATED: ' + target.url()));
context.on('targetchanged', target => events.push('CHANGED: ' + target.url()));
context.on('targetdestroyed', target => events.push('DESTROYED: ' + target.url()));
context.on('targetcreated', (target) =>
events.push('CREATED: ' + target.url())
);
context.on('targetchanged', (target) =>
events.push('CHANGED: ' + target.url())
);
context.on('targetdestroyed', (target) =>
events.push('DESTROYED: ' + target.url())
);
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await page.close();
expect(events).toEqual([
'CREATED: about:blank',
`CHANGED: ${server.EMPTY_PAGE}`,
`DESTROYED: ${server.EMPTY_PAGE}`
`DESTROYED: ${server.EMPTY_PAGE}`,
]);
await context.close();
});
itFailsFirefox('should wait for a target', async() => {
const {browser, server} = getTestState();
itFailsFirefox('should wait for a target', async () => {
const { browser, server } = getTestState();
const context = await browser.createIncognitoBrowserContext();
let resolved = false;
const targetPromise = context.waitForTarget(target => target.url() === server.EMPTY_PAGE);
targetPromise.then(() => resolved = true);
const targetPromise = context.waitForTarget(
(target) => target.url() === server.EMPTY_PAGE
);
targetPromise.then(() => (resolved = true));
const page = await context.newPage();
expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE);
@ -100,17 +111,21 @@ describe('BrowserContext', function() {
await context.close();
});
it('should timeout waiting for a non-existent target', async() => {
const {browser, server, puppeteer} = getTestState();
it('should timeout waiting for a non-existent target', async () => {
const { browser, server, puppeteer } = getTestState();
const context = await browser.createIncognitoBrowserContext();
const error = await context.waitForTarget(target => target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(error_ => error_);
const error = await context
.waitForTarget((target) => target.url() === server.EMPTY_PAGE, {
timeout: 1,
})
.catch((error_) => error_);
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
await context.close();
});
itFailsFirefox('should isolate localStorage and cookies', async() => {
const {browser, server} = getTestState();
itFailsFirefox('should isolate localStorage and cookies', async () => {
const { browser, server } = getTestState();
// Create two incognito contexts.
const context1 = await browser.createIncognitoBrowserContext();
@ -143,27 +158,28 @@ describe('BrowserContext', function() {
expect(context2.targets()[0]).toBe(page2.target());
// Make sure pages don't share localstorage or cookies.
expect(await page1.evaluate(() => localStorage.getItem('name'))).toBe('page1');
expect(await page1.evaluate(() => localStorage.getItem('name'))).toBe(
'page1'
);
expect(await page1.evaluate(() => document.cookie)).toBe('name=page1');
expect(await page2.evaluate(() => localStorage.getItem('name'))).toBe('page2');
expect(await page2.evaluate(() => localStorage.getItem('name'))).toBe(
'page2'
);
expect(await page2.evaluate(() => document.cookie)).toBe('name=page2');
// Cleanup contexts.
await Promise.all([
context1.close(),
context2.close()
]);
await Promise.all([context1.close(), context2.close()]);
expect(browser.browserContexts().length).toBe(1);
});
itFailsFirefox('should work across sessions', async() => {
const {browser, puppeteer} = getTestState();
itFailsFirefox('should work across sessions', async () => {
const { browser, puppeteer } = getTestState();
expect(browser.browserContexts().length).toBe(1);
const context = await browser.createIncognitoBrowserContext();
expect(browser.browserContexts().length).toBe(2);
const remoteBrowser = await puppeteer.connect({
browserWSEndpoint: browser.wsEndpoint()
browserWSEndpoint: browser.wsEndpoint(),
});
const contexts = remoteBrowser.browserContexts();
expect(contexts.length).toBe(2);

View File

@ -14,62 +14,85 @@
* limitations under the License.
*/
const expect = require('expect');
const {getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describeChromeOnly('Chromium-Specific Launcher tests', function() {
describe('Puppeteer.launch |browserURL| option', function() {
it('should be able to connect using browserUrl, with and without trailing slash', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
describeChromeOnly('Chromium-Specific Launcher tests', function () {
describe('Puppeteer.launch |browserURL| option', function () {
it('should be able to connect using browserUrl, with and without trailing slash', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const originalBrowser = await puppeteer.launch(Object.assign({}, defaultBrowserOptions, {
args: ['--remote-debugging-port=21222']
}));
const originalBrowser = await puppeteer.launch(
Object.assign({}, defaultBrowserOptions, {
args: ['--remote-debugging-port=21222'],
})
);
const browserURL = 'http://127.0.0.1:21222';
const browser1 = await puppeteer.connect({browserURL});
const browser1 = await puppeteer.connect({ browserURL });
const page1 = await browser1.newPage();
expect(await page1.evaluate(() => 7 * 8)).toBe(56);
browser1.disconnect();
const browser2 = await puppeteer.connect({browserURL: browserURL + '/'});
const browser2 = await puppeteer.connect({
browserURL: browserURL + '/',
});
const page2 = await browser2.newPage();
expect(await page2.evaluate(() => 8 * 7)).toBe(56);
browser2.disconnect();
originalBrowser.close();
});
it('should throw when using both browserWSEndpoint and browserURL', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
it('should throw when using both browserWSEndpoint and browserURL', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const originalBrowser = await puppeteer.launch(Object.assign({}, defaultBrowserOptions, {
args: ['--remote-debugging-port=21222']
}));
const originalBrowser = await puppeteer.launch(
Object.assign({}, defaultBrowserOptions, {
args: ['--remote-debugging-port=21222'],
})
);
const browserURL = 'http://127.0.0.1:21222';
let error = null;
await puppeteer.connect({browserURL, browserWSEndpoint: originalBrowser.wsEndpoint()}).catch(error_ => error = error_);
expect(error.message).toContain('Exactly one of browserWSEndpoint, browserURL or transport');
await puppeteer
.connect({
browserURL,
browserWSEndpoint: originalBrowser.wsEndpoint(),
})
.catch((error_) => (error = error_));
expect(error.message).toContain(
'Exactly one of browserWSEndpoint, browserURL or transport'
);
originalBrowser.close();
});
it('should throw when trying to connect to non-existing browser', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
it('should throw when trying to connect to non-existing browser', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const originalBrowser = await puppeteer.launch(Object.assign({}, defaultBrowserOptions, {
args: ['--remote-debugging-port=21222']
}));
const originalBrowser = await puppeteer.launch(
Object.assign({}, defaultBrowserOptions, {
args: ['--remote-debugging-port=21222'],
})
);
const browserURL = 'http://127.0.0.1:32333';
let error = null;
await puppeteer.connect({browserURL}).catch(error_ => error = error_);
expect(error.message).toContain('Failed to fetch browser webSocket url from');
await puppeteer
.connect({ browserURL })
.catch((error_) => (error = error_));
expect(error.message).toContain(
'Failed to fetch browser webSocket url from'
);
originalBrowser.close();
});
});
describe('Puppeteer.launch |pipe| option', function() {
it('should support the pipe option', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
const options = Object.assign({pipe: true}, defaultBrowserOptions);
describe('Puppeteer.launch |pipe| option', function () {
it('should support the pipe option', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const options = Object.assign({ pipe: true }, defaultBrowserOptions);
const browser = await puppeteer.launch(options);
expect((await browser.pages()).length).toBe(1);
expect(browser.wsEndpoint()).toBe('');
@ -78,8 +101,8 @@ describeChromeOnly('Chromium-Specific Launcher tests', function() {
await page.close();
await browser.close();
});
it('should support the pipe argument', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
it('should support the pipe argument', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const options = Object.assign({}, defaultBrowserOptions);
options.args = ['--remote-debugging-pipe'].concat(options.args || []);
const browser = await puppeteer.launch(options);
@ -89,30 +112,33 @@ describeChromeOnly('Chromium-Specific Launcher tests', function() {
await page.close();
await browser.close();
});
it('should fire "disconnected" when closing with pipe', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
const options = Object.assign({pipe: true}, defaultBrowserOptions);
it('should fire "disconnected" when closing with pipe', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const options = Object.assign({ pipe: true }, defaultBrowserOptions);
const browser = await puppeteer.launch(options);
const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve));
const disconnectedEventPromise = new Promise((resolve) =>
browser.once('disconnected', resolve)
);
// Emulate user exiting browser.
browser.process().kill();
await disconnectedEventPromise;
});
});
});
describeChromeOnly('Chromium-Specific Page Tests', function() {
describeChromeOnly('Chromium-Specific Page Tests', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('Page.setRequestInterception should work with intervention headers', async() => {
const {server, page} = getTestState();
it('Page.setRequestInterception should work with intervention headers', async () => {
const { server, page } = getTestState();
server.setRoute('/intervention', (req, res) => res.end(`
server.setRoute('/intervention', (req, res) =>
res.end(`
<script>
document.write('<script src="${server.CROSS_PROCESS_PREFIX}/intervention.js">' + '</scr' + 'ipt>');
</script>
`));
`)
);
server.setRedirect('/intervention.js', '/redirect.js');
let serverRequest = null;
server.setRoute('/redirect.js', (req, res) => {
@ -121,10 +147,12 @@ describeChromeOnly('Chromium-Specific Page Tests', function() {
});
await page.setRequestInterception(true);
page.on('request', request => request.continue());
page.on('request', (request) => request.continue());
await page.goto(server.PREFIX + '/intervention');
// Check for feature URL substring rather than https://www.chromestatus.com to
// make it work with Edgium.
expect(serverRequest.headers.intervention).toContain('feature/5718547946799104');
expect(serverRequest.headers.intervention).toContain(
'feature/5718547946799104'
);
});
});

View File

@ -15,21 +15,25 @@
*/
const expect = require('expect');
const {getTestState,setupTestPageAndContextHooks, setupTestBrowserHooks} = require('./mocha-utils');
const {
getTestState,
setupTestPageAndContextHooks,
setupTestBrowserHooks,
} = require('./mocha-utils');
const utils = require('./utils');
describe('Page.click', function() {
describe('Page.click', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should click the button', async() => {
const {page, server} = getTestState();
it('should click the button', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
await page.click('button');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
itFailsFirefox('should click svg', async() => {
const {page} = getTestState();
itFailsFirefox('should click svg', async () => {
const { page } = getTestState();
await page.setContent(`
<svg height="100" width="100">
@ -39,19 +43,24 @@ describe('Page.click', function() {
await page.click('circle');
expect(await page.evaluate(() => window.__CLICKED)).toBe(42);
});
itFailsFirefox('should click the button if window.Node is removed', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should click the button if window.Node is removed',
async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
await page.evaluate(() => delete window.Node);
await page.click('button');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
await page.goto(server.PREFIX + '/input/button.html');
await page.evaluate(() => delete window.Node);
await page.click('button');
expect(await page.evaluate(() => result)).toBe('Clicked');
}
);
// @see https://github.com/puppeteer/puppeteer/issues/4281
itFailsFirefox('should click on a span with an inline element inside', async() => {
const {page} = getTestState();
itFailsFirefox(
'should click on a span with an inline element inside',
async () => {
const { page } = getTestState();
await page.setContent(`
await page.setContent(`
<style>
span::before {
content: 'q';
@ -59,20 +68,21 @@ describe('Page.click', function() {
</style>
<span onclick='javascript:window.CLICKED=42'></span>
`);
await page.click('span');
expect(await page.evaluate(() => window.CLICKED)).toBe(42);
});
it('should not throw UnhandledPromiseRejection when page closes', async() => {
const {page} = getTestState();
await page.click('span');
expect(await page.evaluate(() => window.CLICKED)).toBe(42);
}
);
it('should not throw UnhandledPromiseRejection when page closes', async () => {
const { page } = getTestState();
const newPage = await page.browser().newPage();
await Promise.all([
newPage.close(),
newPage.mouse.click(1, 2),
]).catch(error => {});
]).catch((error) => {});
});
it('should click the button after navigation ', async() => {
const {page, server} = getTestState();
it('should click the button after navigation ', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
await page.click('button');
@ -80,21 +90,20 @@ describe('Page.click', function() {
await page.click('button');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
itFailsFirefox('should click with disabled javascript', async() => {
const {page, server} = getTestState();
itFailsFirefox('should click with disabled javascript', async () => {
const { page, server } = getTestState();
await page.setJavaScriptEnabled(false);
await page.goto(server.PREFIX + '/wrappedlink.html');
await Promise.all([
page.click('a'),
page.waitForNavigation()
]);
await Promise.all([page.click('a'), page.waitForNavigation()]);
expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
});
itFailsFirefox('should click when one of inline box children is outside of viewport', async() => {
const {page} = getTestState();
itFailsFirefox(
'should click when one of inline box children is outside of viewport',
async () => {
const { page } = getTestState();
await page.setContent(`
await page.setContent(`
<style>
i {
position: absolute;
@ -103,30 +112,37 @@ describe('Page.click', function() {
</style>
<span onclick='javascript:window.CLICKED = 42;'><i>woof</i><b>doggo</b></span>
`);
await page.click('span');
expect(await page.evaluate(() => window.CLICKED)).toBe(42);
});
it('should select the text by triple clicking', async() => {
const {page, server} = getTestState();
await page.click('span');
expect(await page.evaluate(() => window.CLICKED)).toBe(42);
}
);
it('should select the text by triple clicking', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
const text = 'This is the text that we are going to try to select. Let\'s see how it goes.';
const text =
"This is the text that we are going to try to select. Let's see how it goes.";
await page.keyboard.type(text);
await page.click('textarea');
await page.click('textarea', {clickCount: 2});
await page.click('textarea', {clickCount: 3});
expect(await page.evaluate(() => {
const textarea = document.querySelector('textarea');
return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
})).toBe(text);
await page.click('textarea', { clickCount: 2 });
await page.click('textarea', { clickCount: 3 });
expect(
await page.evaluate(() => {
const textarea = document.querySelector('textarea');
return textarea.value.substring(
textarea.selectionStart,
textarea.selectionEnd
);
})
).toBe(text);
});
itFailsFirefox('should click offscreen buttons', async() => {
const {page, server} = getTestState();
itFailsFirefox('should click offscreen buttons', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/offscreenbuttons.html');
const messages = [];
page.on('console', msg => messages.push(msg.text()));
page.on('console', (msg) => messages.push(msg.text()));
for (let i = 0; i < 11; ++i) {
// We might've scrolled to click a button - reset to (0, 0).
await page.evaluate(() => window.scrollTo(0, 0));
@ -143,20 +159,20 @@ describe('Page.click', function() {
'button #7 clicked',
'button #8 clicked',
'button #9 clicked',
'button #10 clicked'
'button #10 clicked',
]);
});
it('should click wrapped links', async() => {
const {page, server} = getTestState();
it('should click wrapped links', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/wrappedlink.html');
await page.click('a');
expect(await page.evaluate(() => window.__clicked)).toBe(true);
});
it('should click on checkbox input and toggle', async() => {
const {page, server} = getTestState();
it('should click on checkbox input and toggle', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/checkbox.html');
expect(await page.evaluate(() => result.check)).toBe(null);
@ -176,8 +192,8 @@ describe('Page.click', function() {
expect(await page.evaluate(() => result.check)).toBe(false);
});
itFailsFirefox('should click on checkbox label and toggle', async() => {
const {page, server} = getTestState();
itFailsFirefox('should click on checkbox label and toggle', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/checkbox.html');
expect(await page.evaluate(() => result.check)).toBe(null);
@ -192,50 +208,60 @@ describe('Page.click', function() {
expect(await page.evaluate(() => result.check)).toBe(false);
});
it('should fail to click a missing button', async() => {
const {page, server} = getTestState();
it('should fail to click a missing button', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
let error = null;
await page.click('button.does-not-exist').catch(error_ => error = error_);
expect(error.message).toBe('No node found for selector: button.does-not-exist');
await page
.click('button.does-not-exist')
.catch((error_) => (error = error_));
expect(error.message).toBe(
'No node found for selector: button.does-not-exist'
);
});
// @see https://github.com/puppeteer/puppeteer/issues/161
it('should not hang with touch-enabled viewports', async() => {
const {page, puppeteer} = getTestState();
it('should not hang with touch-enabled viewports', async () => {
const { page, puppeteer } = getTestState();
await page.setViewport(puppeteer.devices['iPhone 6'].viewport);
await page.mouse.down();
await page.mouse.move(100, 10);
await page.mouse.up();
});
it('should scroll and click the button', async() => {
const {page, server} = getTestState();
it('should scroll and click the button', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/scrollable.html');
await page.click('#button-5');
expect(await page.evaluate(() => document.querySelector('#button-5').textContent)).toBe('clicked');
expect(
await page.evaluate(() => document.querySelector('#button-5').textContent)
).toBe('clicked');
await page.click('#button-80');
expect(await page.evaluate(() => document.querySelector('#button-80').textContent)).toBe('clicked');
expect(
await page.evaluate(
() => document.querySelector('#button-80').textContent
)
).toBe('clicked');
});
itFailsFirefox('should double click the button', async() => {
const {page, server} = getTestState();
itFailsFirefox('should double click the button', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
await page.evaluate(() => {
window.double = false;
const button = document.querySelector('button');
button.addEventListener('dblclick', event => {
button.addEventListener('dblclick', (event) => {
window.double = true;
});
});
const button = await page.$('button');
await button.click({clickCount: 2});
await button.click({ clickCount: 2 });
expect(await page.evaluate('double')).toBe(true);
expect(await page.evaluate('result')).toBe('Clicked');
});
it('should click a partially obscured button', async() => {
const {page, server} = getTestState();
it('should click a partially obscured button', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
await page.evaluate(() => {
@ -247,62 +273,85 @@ describe('Page.click', function() {
await page.click('button');
expect(await page.evaluate(() => window.result)).toBe('Clicked');
});
it('should click a rotated button', async() => {
const {page, server} = getTestState();
it('should click a rotated button', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/rotatedButton.html');
await page.click('button');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should fire contextmenu event on right click', async() => {
const {page, server} = getTestState();
it('should fire contextmenu event on right click', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/scrollable.html');
await page.click('#button-8', {button: 'right'});
expect(await page.evaluate(() => document.querySelector('#button-8').textContent)).toBe('context menu');
await page.click('#button-8', { button: 'right' });
expect(
await page.evaluate(() => document.querySelector('#button-8').textContent)
).toBe('context menu');
});
// @see https://github.com/puppeteer/puppeteer/issues/206
itFailsFirefox('should click links which cause navigation', async() => {
const {page, server} = getTestState();
itFailsFirefox('should click links which cause navigation', async () => {
const { page, server } = getTestState();
await page.setContent(`<a href="${server.EMPTY_PAGE}">empty.html</a>`);
// This await should not hang.
await page.click('a');
});
itFailsFirefox('should click the button inside an iframe', async() => {
const {page, server} = getTestState();
itFailsFirefox('should click the button inside an iframe', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setContent('<div style="width:100px;height:100px">spacer</div>');
await utils.attachFrame(page, 'button-test', server.PREFIX + '/input/button.html');
await utils.attachFrame(
page,
'button-test',
server.PREFIX + '/input/button.html'
);
const frame = page.frames()[1];
const button = await frame.$('button');
await button.click();
expect(await frame.evaluate(() => window.result)).toBe('Clicked');
});
// @see https://github.com/puppeteer/puppeteer/issues/4110
xit('should click the button with fixed position inside an iframe', async() => {
const {page, server} = getTestState();
xit('should click the button with fixed position inside an iframe', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setViewport({width: 500, height: 500});
await page.setContent('<div style="width:100px;height:2000px">spacer</div>');
await utils.attachFrame(page, 'button-test', server.CROSS_PROCESS_PREFIX + '/input/button.html');
await page.setViewport({ width: 500, height: 500 });
await page.setContent(
'<div style="width:100px;height:2000px">spacer</div>'
);
await utils.attachFrame(
page,
'button-test',
server.CROSS_PROCESS_PREFIX + '/input/button.html'
);
const frame = page.frames()[1];
await frame.$eval('button', button => button.style.setProperty('position', 'fixed'));
await frame.$eval('button', (button) =>
button.style.setProperty('position', 'fixed')
);
await frame.click('button');
expect(await frame.evaluate(() => window.result)).toBe('Clicked');
});
itFailsFirefox('should click the button with deviceScaleFactor set', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should click the button with deviceScaleFactor set',
async () => {
const { page, server } = getTestState();
await page.setViewport({width: 400, height: 400, deviceScaleFactor: 5});
expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5);
await page.setContent('<div style="width:100px;height:100px">spacer</div>');
await utils.attachFrame(page, 'button-test', server.PREFIX + '/input/button.html');
const frame = page.frames()[1];
const button = await frame.$('button');
await button.click();
expect(await frame.evaluate(() => window.result)).toBe('Clicked');
});
await page.setViewport({ width: 400, height: 400, deviceScaleFactor: 5 });
expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5);
await page.setContent(
'<div style="width:100px;height:100px">spacer</div>'
);
await utils.attachFrame(
page,
'button-test',
server.PREFIX + '/input/button.html'
);
const frame = page.frames()[1];
const button = await frame.$('button');
await button.click();
expect(await frame.evaluate(() => window.result)).toBe('Clicked');
}
);
});

View File

@ -14,38 +14,44 @@
* limitations under the License.
*/
const expect = require('expect');
const {getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('Cookie specs', () => {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describe('Page.cookies', function() {
it('should return no cookies in pristine browser context', async() => {
const {page, server} = getTestState();
describe('Page.cookies', function () {
it('should return no cookies in pristine browser context', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
expect(await page.cookies()).toEqual([]);
});
itFailsFirefox('should get a cookie', async() => {
const {page, server} = getTestState();
itFailsFirefox('should get a cookie', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
document.cookie = 'username=John Doe';
});
expect(await page.cookies()).toEqual([{
name: 'username',
value: 'John Doe',
domain: 'localhost',
path: '/',
expires: -1,
size: 16,
httpOnly: false,
secure: false,
session: true,
}]);
expect(await page.cookies()).toEqual([
{
name: 'username',
value: 'John Doe',
domain: 'localhost',
path: '/',
expires: -1,
size: 16,
httpOnly: false,
secure: false,
session: true,
},
]);
});
it('should properly report httpOnly cookie', async() => {
const {page, server} = getTestState();
it('should properly report httpOnly cookie', async () => {
const { page, server } = getTestState();
server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', 'a=b; HttpOnly; Path=/');
res.end();
@ -55,8 +61,8 @@ describe('Cookie specs', () => {
expect(cookies.length).toBe(1);
expect(cookies[0].httpOnly).toBe(true);
});
it('should properly report "Strict" sameSite cookie', async() => {
const {page, server} = getTestState();
it('should properly report "Strict" sameSite cookie', async () => {
const { page, server } = getTestState();
server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', 'a=b; SameSite=Strict');
res.end();
@ -66,8 +72,8 @@ describe('Cookie specs', () => {
expect(cookies.length).toBe(1);
expect(cookies[0].sameSite).toBe('Strict');
});
it('should properly report "Lax" sameSite cookie', async() => {
const {page, server} = getTestState();
it('should properly report "Lax" sameSite cookie', async () => {
const { page, server } = getTestState();
server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', 'a=b; SameSite=Lax');
res.end();
@ -77,8 +83,8 @@ describe('Cookie specs', () => {
expect(cookies.length).toBe(1);
expect(cookies[0].sameSite).toBe('Lax');
});
itFailsFirefox('should get multiple cookies', async() => {
const {page, server} = getTestState();
itFailsFirefox('should get multiple cookies', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
document.cookie = 'username=John Doe';
@ -111,59 +117,68 @@ describe('Cookie specs', () => {
},
]);
});
itFailsFirefox('should get cookies from multiple urls', async() => {
const {page} = getTestState();
await page.setCookie({
url: 'https://foo.com',
name: 'doggo',
value: 'woofs',
}, {
url: 'https://bar.com',
name: 'catto',
value: 'purrs',
}, {
url: 'https://baz.com',
name: 'birdo',
value: 'tweets',
});
itFailsFirefox('should get cookies from multiple urls', async () => {
const { page } = getTestState();
await page.setCookie(
{
url: 'https://foo.com',
name: 'doggo',
value: 'woofs',
},
{
url: 'https://bar.com',
name: 'catto',
value: 'purrs',
},
{
url: 'https://baz.com',
name: 'birdo',
value: 'tweets',
}
);
const cookies = await page.cookies('https://foo.com', 'https://baz.com');
cookies.sort((a, b) => a.name.localeCompare(b.name));
expect(cookies).toEqual([{
name: 'birdo',
value: 'tweets',
domain: 'baz.com',
path: '/',
expires: -1,
size: 11,
httpOnly: false,
secure: true,
session: true,
}, {
name: 'doggo',
value: 'woofs',
domain: 'foo.com',
path: '/',
expires: -1,
size: 10,
httpOnly: false,
secure: true,
session: true,
}]);
expect(cookies).toEqual([
{
name: 'birdo',
value: 'tweets',
domain: 'baz.com',
path: '/',
expires: -1,
size: 11,
httpOnly: false,
secure: true,
session: true,
},
{
name: 'doggo',
value: 'woofs',
domain: 'foo.com',
path: '/',
expires: -1,
size: 10,
httpOnly: false,
secure: true,
session: true,
},
]);
});
});
describe('Page.setCookie', function() {
itFailsFirefox('should work', async() => {
const {page, server} = getTestState();
describe('Page.setCookie', function () {
itFailsFirefox('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
name: 'password',
value: '123456'
value: '123456',
});
expect(await page.evaluate(() => document.cookie)).toEqual('password=123456');
expect(await page.evaluate(() => document.cookie)).toEqual(
'password=123456'
);
});
itFailsFirefox('should isolate cookies in browser contexts', async() => {
const {page, server, browser} = getTestState();
itFailsFirefox('should isolate cookies in browser contexts', async () => {
const { page, server, browser } = getTestState();
const anotherContext = await browser.createIncognitoBrowserContext();
const anotherPage = await anotherContext.newPage();
@ -171,8 +186,8 @@ describe('Cookie specs', () => {
await page.goto(server.EMPTY_PAGE);
await anotherPage.goto(server.EMPTY_PAGE);
await page.setCookie({name: 'page1cookie', value: 'page1value'});
await anotherPage.setCookie({name: 'page2cookie', value: 'page2value'});
await page.setCookie({ name: 'page1cookie', value: 'page1value' });
await anotherPage.setCookie({ name: 'page2cookie', value: 'page2value' });
const cookies1 = await page.cookies();
const cookies2 = await anotherPage.cookies();
@ -184,78 +199,87 @@ describe('Cookie specs', () => {
expect(cookies2[0].value).toBe('page2value');
await anotherContext.close();
});
itFailsFirefox('should set multiple cookies', async() => {
const {page, server} = getTestState();
itFailsFirefox('should set multiple cookies', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
name: 'password',
value: '123456'
}, {
name: 'foo',
value: 'bar'
});
expect(await page.evaluate(() => {
const cookies = document.cookie.split(';');
return cookies.map(cookie => cookie.trim()).sort();
})).toEqual([
'foo=bar',
'password=123456',
]);
await page.setCookie(
{
name: 'password',
value: '123456',
},
{
name: 'foo',
value: 'bar',
}
);
expect(
await page.evaluate(() => {
const cookies = document.cookie.split(';');
return cookies.map((cookie) => cookie.trim()).sort();
})
).toEqual(['foo=bar', 'password=123456']);
});
itFailsFirefox('should have |expires| set to |-1| for session cookies', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should have |expires| set to |-1| for session cookies',
async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
name: 'password',
value: '123456',
});
const cookies = await page.cookies();
expect(cookies[0].session).toBe(true);
expect(cookies[0].expires).toBe(-1);
}
);
itFailsFirefox('should set cookie with reasonable defaults', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
name: 'password',
value: '123456'
});
const cookies = await page.cookies();
expect(cookies[0].session).toBe(true);
expect(cookies[0].expires).toBe(-1);
});
itFailsFirefox('should set cookie with reasonable defaults', async() => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
name: 'password',
value: '123456'
});
const cookies = await page.cookies();
expect(cookies.sort((a, b) => a.name.localeCompare(b.name))).toEqual([{
name: 'password',
value: '123456',
domain: 'localhost',
path: '/',
expires: -1,
size: 14,
httpOnly: false,
secure: false,
session: true,
}]);
});
const cookies = await page.cookies();
expect(cookies.sort((a, b) => a.name.localeCompare(b.name))).toEqual([
{
name: 'password',
value: '123456',
domain: 'localhost',
path: '/',
expires: -1,
size: 14,
httpOnly: false,
secure: false,
session: true,
},
]);
});
itFailsFirefox('should set a cookie with a path', async() => {
const {page, server} = getTestState();
itFailsFirefox('should set a cookie with a path', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/grid.html');
await page.setCookie({
name: 'gridcookie',
value: 'GRID',
path: '/grid.html'
});
expect(await page.cookies()).toEqual([{
name: 'gridcookie',
value: 'GRID',
domain: 'localhost',
path: '/grid.html',
expires: -1,
size: 14,
httpOnly: false,
secure: false,
session: true,
}]);
});
expect(await page.cookies()).toEqual([
{
name: 'gridcookie',
value: 'GRID',
domain: 'localhost',
path: '/grid.html',
expires: -1,
size: 14,
httpOnly: false,
secure: false,
session: true,
},
]);
expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID');
await page.goto(server.EMPTY_PAGE);
expect(await page.cookies()).toEqual([]);
@ -263,75 +287,85 @@ describe('Cookie specs', () => {
await page.goto(server.PREFIX + '/grid.html');
expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID');
});
it('should not set a cookie on a blank page', async() => {
const {page} = getTestState();
it('should not set a cookie on a blank page', async () => {
const { page } = getTestState();
await page.goto('about:blank');
let error = null;
try {
await page.setCookie({name: 'example-cookie', value: 'best'});
await page.setCookie({ name: 'example-cookie', value: 'best' });
} catch (error_) {
error = error_;
}
expect(error.message).toContain('At least one of the url and domain needs to be specified');
expect(error.message).toContain(
'At least one of the url and domain needs to be specified'
);
});
it('should not set a cookie with blank page URL', async() => {
const {page, server} = getTestState();
it('should not set a cookie with blank page URL', async () => {
const { page, server } = getTestState();
let error = null;
await page.goto(server.EMPTY_PAGE);
try {
await page.setCookie(
{name: 'example-cookie', value: 'best'},
{url: 'about:blank', name: 'example-cookie-blank', value: 'best'}
{ name: 'example-cookie', value: 'best' },
{ url: 'about:blank', name: 'example-cookie-blank', value: 'best' }
);
} catch (error_) {
error = error_;
}
expect(error.message).toEqual(
`Blank page can not have cookie "example-cookie-blank"`
`Blank page can not have cookie "example-cookie-blank"`
);
});
it('should not set a cookie on a data URL page', async() => {
const {page} = getTestState();
it('should not set a cookie on a data URL page', async () => {
const { page } = getTestState();
let error = null;
await page.goto('data:,Hello%2C%20World!');
try {
await page.setCookie({name: 'example-cookie', value: 'best'});
await page.setCookie({ name: 'example-cookie', value: 'best' });
} catch (error_) {
error = error_;
}
expect(error.message).toContain('At least one of the url and domain needs to be specified');
expect(error.message).toContain(
'At least one of the url and domain needs to be specified'
);
});
itFailsFirefox('should default to setting secure cookie for HTTPS websites', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should default to setting secure cookie for HTTPS websites',
async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const SECURE_URL = 'https://example.com';
await page.setCookie({
url: SECURE_URL,
name: 'foo',
value: 'bar',
});
const [cookie] = await page.cookies(SECURE_URL);
expect(cookie.secure).toBe(true);
});
itFailsFirefox('should be able to set unsecure cookie for HTTP website', async() => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE);
const SECURE_URL = 'https://example.com';
await page.setCookie({
url: SECURE_URL,
name: 'foo',
value: 'bar',
});
const [cookie] = await page.cookies(SECURE_URL);
expect(cookie.secure).toBe(true);
}
);
itFailsFirefox(
'should be able to set unsecure cookie for HTTP website',
async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const HTTP_URL = 'http://example.com';
await page.setCookie({
url: HTTP_URL,
name: 'foo',
value: 'bar',
});
const [cookie] = await page.cookies(HTTP_URL);
expect(cookie.secure).toBe(false);
});
itFailsFirefox('should set a cookie on a different domain', async() => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE);
const HTTP_URL = 'http://example.com';
await page.setCookie({
url: HTTP_URL,
name: 'foo',
value: 'bar',
});
const [cookie] = await page.cookies(HTTP_URL);
expect(cookie.secure).toBe(false);
}
);
itFailsFirefox('should set a cookie on a different domain', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
@ -341,80 +375,102 @@ describe('Cookie specs', () => {
});
expect(await page.evaluate('document.cookie')).toBe('');
expect(await page.cookies()).toEqual([]);
expect(await page.cookies('https://www.example.com')).toEqual([{
name: 'example-cookie',
value: 'best',
domain: 'www.example.com',
path: '/',
expires: -1,
size: 18,
httpOnly: false,
secure: true,
session: true,
}]);
expect(await page.cookies('https://www.example.com')).toEqual([
{
name: 'example-cookie',
value: 'best',
domain: 'www.example.com',
path: '/',
expires: -1,
size: 18,
httpOnly: false,
secure: true,
session: true,
},
]);
});
itFailsFirefox('should set cookies from a frame', async() => {
const {page, server} = getTestState();
itFailsFirefox('should set cookies from a frame', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/grid.html');
await page.setCookie({name: 'localhost-cookie', value: 'best'});
await page.evaluate(src => {
await page.setCookie({ name: 'localhost-cookie', value: 'best' });
await page.evaluate((src) => {
let fulfill;
const promise = new Promise(x => fulfill = x);
const promise = new Promise((x) => (fulfill = x));
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.onload = fulfill;
iframe.src = src;
return promise;
}, server.CROSS_PROCESS_PREFIX);
await page.setCookie({name: '127-cookie', value: 'worst', url: server.CROSS_PROCESS_PREFIX});
expect(await page.evaluate('document.cookie')).toBe('localhost-cookie=best');
expect(await page.frames()[1].evaluate('document.cookie')).toBe('127-cookie=worst');
expect(await page.cookies()).toEqual([{
name: 'localhost-cookie',
value: 'best',
domain: 'localhost',
path: '/',
expires: -1,
size: 20,
httpOnly: false,
secure: false,
session: true,
}]);
expect(await page.cookies(server.CROSS_PROCESS_PREFIX)).toEqual([{
await page.setCookie({
name: '127-cookie',
value: 'worst',
domain: '127.0.0.1',
path: '/',
expires: -1,
size: 15,
httpOnly: false,
secure: false,
session: true,
}]);
url: server.CROSS_PROCESS_PREFIX,
});
expect(await page.evaluate('document.cookie')).toBe(
'localhost-cookie=best'
);
expect(await page.frames()[1].evaluate('document.cookie')).toBe(
'127-cookie=worst'
);
expect(await page.cookies()).toEqual([
{
name: 'localhost-cookie',
value: 'best',
domain: 'localhost',
path: '/',
expires: -1,
size: 20,
httpOnly: false,
secure: false,
session: true,
},
]);
expect(await page.cookies(server.CROSS_PROCESS_PREFIX)).toEqual([
{
name: '127-cookie',
value: 'worst',
domain: '127.0.0.1',
path: '/',
expires: -1,
size: 15,
httpOnly: false,
secure: false,
session: true,
},
]);
});
});
describe('Page.deleteCookie', function() {
itFailsFirefox('should work', async() => {
const {page, server} = getTestState();
describe('Page.deleteCookie', function () {
itFailsFirefox('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
name: 'cookie1',
value: '1'
}, {
name: 'cookie2',
value: '2'
}, {
name: 'cookie3',
value: '3'
});
expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2; cookie3=3');
await page.deleteCookie({name: 'cookie2'});
expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie3=3');
await page.setCookie(
{
name: 'cookie1',
value: '1',
},
{
name: 'cookie2',
value: '2',
},
{
name: 'cookie3',
value: '3',
}
);
expect(await page.evaluate('document.cookie')).toBe(
'cookie1=1; cookie2=2; cookie3=3'
);
await page.deleteCookie({ name: 'cookie2' });
expect(await page.evaluate('document.cookie')).toBe(
'cookie1=1; cookie3=3'
);
});
});
});

View File

@ -39,10 +39,15 @@ function traceAPICoverage(apiCoverage, events, className, classType) {
className = className.substring(0, 1).toLowerCase() + className.substring(1);
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
if (
methodName === 'constructor' ||
typeof methodName !== 'string' ||
methodName.startsWith('_') ||
typeof method !== 'function'
)
continue;
apiCoverage.set(`${className}.${methodName}`, false);
Reflect.set(classType.prototype, methodName, function(...args) {
Reflect.set(classType.prototype, methodName, function (...args) {
apiCoverage.set(`${className}.${methodName}`, true);
return method.call(this, ...args);
});
@ -54,7 +59,7 @@ function traceAPICoverage(apiCoverage, events, className, classType) {
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
}
const method = Reflect.get(classType.prototype, 'emit');
Reflect.set(classType.prototype, 'emit', function(event, ...args) {
Reflect.set(classType.prototype, 'emit', function (event, ...args) {
if (typeof event !== 'symbol' && this.listenerCount(event))
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
return method.call(this, event, ...args);
@ -71,14 +76,14 @@ const clearOldCoverage = () => {
// do nothing, the file didn't exist
}
};
const writeCoverage = coverage => {
const writeCoverage = (coverage) => {
fs.writeFileSync(coverageLocation, JSON.stringify([...coverage.entries()]));
};
const getCoverageResults = () => {
let contents;
try {
contents = fs.readFileSync(coverageLocation, {encoding: 'utf8'});
contents = fs.readFileSync(coverageLocation, { encoding: 'utf8' });
} catch (error) {
console.error('Warning: coverage file does not exist or is not readable.');
}
@ -92,7 +97,6 @@ const trackCoverage = () => {
const coverageMap = new Map();
before(() => {
const api = require('../lib/api');
const events = require('../lib/Events');
for (const [className, classType] of Object.entries(api))
@ -106,5 +110,5 @@ const trackCoverage = () => {
module.exports = {
trackCoverage,
getCoverageResults
getCoverageResults,
};

View File

@ -15,27 +15,33 @@
*/
const expect = require('expect');
const {getTestState, setupTestPageAndContextHooks, setupTestBrowserHooks} = require('./mocha-utils');
const {
getTestState,
setupTestPageAndContextHooks,
setupTestBrowserHooks,
} = require('./mocha-utils');
describe('Coverage specs', function() {
describeChromeOnly('JSCoverage', function() {
describe('Coverage specs', function () {
describeChromeOnly('JSCoverage', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should work', async() => {
const {page, server} = getTestState();
it('should work', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage();
await page.goto(server.PREFIX + '/jscoverage/simple.html', {waitUntil: 'networkidle0'});
await page.goto(server.PREFIX + '/jscoverage/simple.html', {
waitUntil: 'networkidle0',
});
const coverage = await page.coverage.stopJSCoverage();
expect(coverage.length).toBe(1);
expect(coverage[0].url).toContain('/jscoverage/simple.html');
expect(coverage[0].ranges).toEqual([
{start: 0, end: 17},
{start: 35, end: 61},
{ start: 0, end: 17 },
{ start: 35, end: 61 },
]);
});
it('should report sourceURLs', async() => {
const {page, server} = getTestState();
it('should report sourceURLs', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage();
await page.goto(server.PREFIX + '/jscoverage/sourceurl.html');
@ -43,35 +49,37 @@ describe('Coverage specs', function() {
expect(coverage.length).toBe(1);
expect(coverage[0].url).toBe('nicename.js');
});
it('should ignore eval() scripts by default', async() => {
const {page, server} = getTestState();
it('should ignore eval() scripts by default', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage();
await page.goto(server.PREFIX + '/jscoverage/eval.html');
const coverage = await page.coverage.stopJSCoverage();
expect(coverage.length).toBe(1);
});
it('shouldn\'t ignore eval() scripts if reportAnonymousScripts is true', async() => {
const {page, server} = getTestState();
it("shouldn't ignore eval() scripts if reportAnonymousScripts is true", async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage({reportAnonymousScripts: true});
await page.coverage.startJSCoverage({ reportAnonymousScripts: true });
await page.goto(server.PREFIX + '/jscoverage/eval.html');
const coverage = await page.coverage.stopJSCoverage();
expect(coverage.find(entry => entry.url.startsWith('debugger://'))).not.toBe(null);
expect(
coverage.find((entry) => entry.url.startsWith('debugger://'))
).not.toBe(null);
expect(coverage.length).toBe(2);
});
it('should ignore pptr internal scripts if reportAnonymousScripts is true', async() => {
const {page, server} = getTestState();
it('should ignore pptr internal scripts if reportAnonymousScripts is true', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage({reportAnonymousScripts: true});
await page.coverage.startJSCoverage({ reportAnonymousScripts: true });
await page.goto(server.EMPTY_PAGE);
await page.evaluate('console.log("foo")');
await page.evaluate(() => console.log('bar'));
const coverage = await page.coverage.stopJSCoverage();
expect(coverage.length).toBe(0);
});
it('should report multiple scripts', async() => {
const {page, server} = getTestState();
it('should report multiple scripts', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage();
await page.goto(server.PREFIX + '/jscoverage/multiple.html');
@ -81,8 +89,8 @@ describe('Coverage specs', function() {
expect(coverage[0].url).toContain('/jscoverage/script1.js');
expect(coverage[1].url).toContain('/jscoverage/script2.js');
});
it('should report right ranges', async() => {
const {page, server} = getTestState();
it('should report right ranges', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage();
await page.goto(server.PREFIX + '/jscoverage/ranges.html');
@ -91,10 +99,12 @@ describe('Coverage specs', function() {
const entry = coverage[0];
expect(entry.ranges.length).toBe(1);
const range = entry.ranges[0];
expect(entry.text.substring(range.start, range.end)).toBe(`console.log('used!');`);
expect(entry.text.substring(range.start, range.end)).toBe(
`console.log('used!');`
);
});
it('should report scripts that have no coverage', async() => {
const {page, server} = getTestState();
it('should report scripts that have no coverage', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage();
await page.goto(server.PREFIX + '/jscoverage/unused.html');
@ -104,27 +114,29 @@ describe('Coverage specs', function() {
expect(entry.url).toContain('unused.html');
expect(entry.ranges.length).toBe(0);
});
it('should work with conditionals', async() => {
const {page, server} = getTestState();
it('should work with conditionals', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage();
await page.goto(server.PREFIX + '/jscoverage/involved.html');
const coverage = await page.coverage.stopJSCoverage();
expect(JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':<PORT>/')).toBeGolden('jscoverage-involved.txt');
expect(
JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':<PORT>/')
).toBeGolden('jscoverage-involved.txt');
});
describe('resetOnNavigation', function() {
it('should report scripts across navigations when disabled', async() => {
const {page, server} = getTestState();
describe('resetOnNavigation', function () {
it('should report scripts across navigations when disabled', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage({resetOnNavigation: false});
await page.coverage.startJSCoverage({ resetOnNavigation: false });
await page.goto(server.PREFIX + '/jscoverage/multiple.html');
await page.goto(server.EMPTY_PAGE);
const coverage = await page.coverage.stopJSCoverage();
expect(coverage.length).toBe(2);
});
it('should NOT report scripts across navigations when enabled', async() => {
const {page, server} = getTestState();
it('should NOT report scripts across navigations when enabled', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage(); // Enabled by default.
await page.goto(server.PREFIX + '/jscoverage/multiple.html');
@ -134,8 +146,8 @@ describe('Coverage specs', function() {
});
});
// @see https://crbug.com/990945
xit('should not hang when there is a debugger statement', async() => {
const {page, server} = getTestState();
xit('should not hang when there is a debugger statement', async () => {
const { page, server } = getTestState();
await page.coverage.startJSCoverage();
await page.goto(server.EMPTY_PAGE);
@ -146,26 +158,26 @@ describe('Coverage specs', function() {
});
});
describeChromeOnly('CSSCoverage', function() {
describeChromeOnly('CSSCoverage', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should work', async() => {
const {page, server} = getTestState();
it('should work', async () => {
const { page, server } = getTestState();
await page.coverage.startCSSCoverage();
await page.goto(server.PREFIX + '/csscoverage/simple.html');
const coverage = await page.coverage.stopCSSCoverage();
expect(coverage.length).toBe(1);
expect(coverage[0].url).toContain('/csscoverage/simple.html');
expect(coverage[0].ranges).toEqual([
{start: 1, end: 22}
]);
expect(coverage[0].ranges).toEqual([{ start: 1, end: 22 }]);
const range = coverage[0].ranges[0];
expect(coverage[0].text.substring(range.start, range.end)).toBe('div { color: green; }');
expect(coverage[0].text.substring(range.start, range.end)).toBe(
'div { color: green; }'
);
});
it('should report sourceURLs', async() => {
const {page, server} = getTestState();
it('should report sourceURLs', async () => {
const { page, server } = getTestState();
await page.coverage.startCSSCoverage();
await page.goto(server.PREFIX + '/csscoverage/sourceurl.html');
@ -173,8 +185,8 @@ describe('Coverage specs', function() {
expect(coverage.length).toBe(1);
expect(coverage[0].url).toBe('nicename.css');
});
it('should report multiple stylesheets', async() => {
const {page, server} = getTestState();
it('should report multiple stylesheets', async () => {
const { page, server } = getTestState();
await page.coverage.startCSSCoverage();
await page.goto(server.PREFIX + '/csscoverage/multiple.html');
@ -184,8 +196,8 @@ describe('Coverage specs', function() {
expect(coverage[0].url).toContain('/csscoverage/stylesheet1.css');
expect(coverage[1].url).toContain('/csscoverage/stylesheet2.css');
});
it('should report stylesheets that have no coverage', async() => {
const {page, server} = getTestState();
it('should report stylesheets that have no coverage', async () => {
const { page, server } = getTestState();
await page.coverage.startCSSCoverage();
await page.goto(server.PREFIX + '/csscoverage/unused.html');
@ -194,49 +206,51 @@ describe('Coverage specs', function() {
expect(coverage[0].url).toBe('unused.css');
expect(coverage[0].ranges.length).toBe(0);
});
it('should work with media queries', async() => {
const {page, server} = getTestState();
it('should work with media queries', async () => {
const { page, server } = getTestState();
await page.coverage.startCSSCoverage();
await page.goto(server.PREFIX + '/csscoverage/media.html');
const coverage = await page.coverage.stopCSSCoverage();
expect(coverage.length).toBe(1);
expect(coverage[0].url).toContain('/csscoverage/media.html');
expect(coverage[0].ranges).toEqual([
{start: 17, end: 38}
]);
expect(coverage[0].ranges).toEqual([{ start: 17, end: 38 }]);
});
it('should work with complicated usecases', async() => {
const {page, server} = getTestState();
it('should work with complicated usecases', async () => {
const { page, server } = getTestState();
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, ':<PORT>/')).toBeGolden('csscoverage-involved.txt');
expect(
JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':<PORT>/')
).toBeGolden('csscoverage-involved.txt');
});
it('should ignore injected stylesheets', async() => {
const {page} = getTestState();
it('should ignore injected stylesheets', async () => {
const { page } = getTestState();
await page.coverage.startCSSCoverage();
await page.addStyleTag({content: 'body { margin: 10px;}'});
await page.addStyleTag({ content: 'body { margin: 10px;}' });
// trigger style recalc
const margin = await page.evaluate(() => window.getComputedStyle(document.body).margin);
const margin = await page.evaluate(
() => window.getComputedStyle(document.body).margin
);
expect(margin).toBe('10px');
const coverage = await page.coverage.stopCSSCoverage();
expect(coverage.length).toBe(0);
});
describe('resetOnNavigation', function() {
it('should report stylesheets across navigations', async() => {
const {page, server} = getTestState();
describe('resetOnNavigation', function () {
it('should report stylesheets across navigations', async () => {
const { page, server } = getTestState();
await page.coverage.startCSSCoverage({resetOnNavigation: false});
await page.coverage.startCSSCoverage({ resetOnNavigation: false });
await page.goto(server.PREFIX + '/csscoverage/multiple.html');
await page.goto(server.EMPTY_PAGE);
const coverage = await page.coverage.stopCSSCoverage();
expect(coverage.length).toBe(2);
});
it('should NOT report scripts across navigations', async() => {
const {page, server} = getTestState();
it('should NOT report scripts across navigations', async () => {
const { page, server } = getTestState();
await page.coverage.startCSSCoverage(); // Enabled by default.
await page.goto(server.PREFIX + '/csscoverage/multiple.html');
@ -245,18 +259,18 @@ describe('Coverage specs', function() {
expect(coverage.length).toBe(0);
});
});
it('should work with a recently loaded stylesheet', async() => {
const {page, server} = getTestState();
it('should work with a recently loaded stylesheet', async () => {
const { page, server } = getTestState();
await page.coverage.startCSSCoverage();
await page.evaluate(async url => {
await page.evaluate(async (url) => {
document.body.textContent = 'hello, world';
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
await new Promise(x => link.onload = x);
await new Promise((x) => (link.onload = x));
}, server.PREFIX + '/csscoverage/stylesheet1.css');
const coverage = await page.coverage.stopCSSCoverage();
expect(coverage.length).toBe(1);

View File

@ -14,75 +14,90 @@
* limitations under the License.
*/
const expect = require('expect');
const {getTestState,setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('DefaultBrowserContext', function() {
describe('DefaultBrowserContext', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
itFailsFirefox('page.cookies() should work', async() => {
const {page, server} = getTestState();
itFailsFirefox('page.cookies() should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
document.cookie = 'username=John Doe';
});
expect(await page.cookies()).toEqual([{
name: 'username',
value: 'John Doe',
domain: 'localhost',
path: '/',
expires: -1,
size: 16,
httpOnly: false,
secure: false,
session: true,
}]);
expect(await page.cookies()).toEqual([
{
name: 'username',
value: 'John Doe',
domain: 'localhost',
path: '/',
expires: -1,
size: 16,
httpOnly: false,
secure: false,
session: true,
},
]);
});
itFailsFirefox('page.setCookie() should work', async() => {
const {page, server} = getTestState();
itFailsFirefox('page.setCookie() should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
name: 'username',
value: 'John Doe'
});
expect(await page.evaluate(() => document.cookie)).toBe('username=John Doe');
expect(await page.cookies()).toEqual([{
name: 'username',
value: 'John Doe',
domain: 'localhost',
path: '/',
expires: -1,
size: 16,
httpOnly: false,
secure: false,
session: true,
}]);
});
expect(await page.evaluate(() => document.cookie)).toBe(
'username=John Doe'
);
expect(await page.cookies()).toEqual([
{
name: 'username',
value: 'John Doe',
domain: 'localhost',
path: '/',
expires: -1,
size: 16,
httpOnly: false,
secure: false,
session: true,
},
]);
});
itFailsFirefox('page.deleteCookie() should work', async() => {
const {page, server} = getTestState();
itFailsFirefox('page.deleteCookie() should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
name: 'cookie1',
value: '1'
}, {
name: 'cookie2',
value: '2'
});
await page.setCookie(
{
name: 'cookie1',
value: '1',
},
{
name: 'cookie2',
value: '2',
}
);
expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2');
await page.deleteCookie({name: 'cookie2'});
await page.deleteCookie({ name: 'cookie2' });
expect(await page.evaluate('document.cookie')).toBe('cookie1=1');
expect(await page.cookies()).toEqual([{
name: 'cookie1',
value: '1',
domain: 'localhost',
path: '/',
expires: -1,
size: 8,
httpOnly: false,
secure: false,
session: true,
}]);
expect(await page.cookies()).toEqual([
{
name: 'cookie1',
value: '1',
domain: 'localhost',
path: '/',
expires: -1,
size: 8,
httpOnly: false,
secure: false,
session: true,
},
]);
});
});

View File

@ -14,15 +14,19 @@
* limitations under the License.
*/
const expect = require('expect');
const {getTestState,setupTestPageAndContextHooks, setupTestBrowserHooks} = require('./mocha-utils');
const {
getTestState,
setupTestPageAndContextHooks,
setupTestBrowserHooks,
} = require('./mocha-utils');
describe('Page.Events.Dialog', function() {
describe('Page.Events.Dialog', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should fire', async() => {
const {page} = getTestState();
it('should fire', async () => {
const { page } = getTestState();
page.on('dialog', dialog => {
page.on('dialog', (dialog) => {
expect(dialog.type()).toBe('alert');
expect(dialog.defaultValue()).toBe('');
expect(dialog.message()).toBe('yo');
@ -30,10 +34,10 @@ describe('Page.Events.Dialog', function() {
});
await page.evaluate(() => alert('yo'));
});
itFailsFirefox('should allow accepting prompts', async() => {
const {page} = getTestState();
itFailsFirefox('should allow accepting prompts', async () => {
const { page } = getTestState();
page.on('dialog', dialog => {
page.on('dialog', (dialog) => {
expect(dialog.type()).toBe('prompt');
expect(dialog.defaultValue()).toBe('yes.');
expect(dialog.message()).toBe('question?');
@ -42,10 +46,10 @@ describe('Page.Events.Dialog', function() {
const result = await page.evaluate(() => prompt('question?', 'yes.'));
expect(result).toBe('answer!');
});
it('should dismiss the prompt', async() => {
const {page} = getTestState();
it('should dismiss the prompt', async () => {
const { page } = getTestState();
page.on('dialog', dialog => {
page.on('dialog', (dialog) => {
dialog.dismiss();
});
const result = await page.evaluate(() => prompt('question?'));

View File

@ -15,56 +15,64 @@
*/
const expect = require('expect');
const {getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
const utils = require('./utils');
describe('ElementHandle specs', function() {
describe('ElementHandle specs', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describeFailsFirefox('ElementHandle.boundingBox', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('ElementHandle.boundingBox', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const elementHandle = await page.$('.box:nth-of-type(13)');
const box = await elementHandle.boundingBox();
expect(box).toEqual({x: 100, y: 50, width: 50, height: 50});
expect(box).toEqual({ x: 100, y: 50, width: 50, height: 50 });
});
it('should handle nested frames', async() => {
const {page, server, isChrome} = getTestState();
it('should handle nested frames', async () => {
const { page, server, isChrome } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/frames/nested-frames.html');
const nestedFrame = page.frames()[1].childFrames()[1];
const elementHandle = await nestedFrame.$('div');
const box = await elementHandle.boundingBox();
if (isChrome)
expect(box).toEqual({x: 28, y: 260, width: 264, height: 18});
else
expect(box).toEqual({x: 28, y: 182, width: 254, height: 18});
expect(box).toEqual({ x: 28, y: 260, width: 264, height: 18 });
else expect(box).toEqual({ x: 28, y: 182, width: 254, height: 18 });
});
it('should return null for invisible elements', async() => {
const {page} = getTestState();
it('should return null for invisible elements', async () => {
const { page } = getTestState();
await page.setContent('<div style="display:none">hi</div>');
const element = await page.$('div');
expect(await element.boundingBox()).toBe(null);
});
it('should force a layout', async() => {
const {page} = getTestState();
it('should force a layout', async () => {
const { page } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setContent('<div style="width: 100px; height: 100px">hello</div>');
await page.setViewport({ width: 500, height: 500 });
await page.setContent(
'<div style="width: 100px; height: 100px">hello</div>'
);
const elementHandle = await page.$('div');
await page.evaluate(element => element.style.height = '200px', elementHandle);
await page.evaluate(
(element) => (element.style.height = '200px'),
elementHandle
);
const box = await elementHandle.boundingBox();
expect(box).toEqual({x: 8, y: 8, width: 100, height: 200});
expect(box).toEqual({ x: 8, y: 8, width: 100, height: 200 });
});
it('should work with SVG nodes', async() => {
const {page} = getTestState();
it('should work with SVG nodes', async () => {
const { page } = getTestState();
await page.setContent(`
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
@ -73,17 +81,17 @@ describe('ElementHandle specs', function() {
`);
const element = await page.$('#therect');
const pptrBoundingBox = await element.boundingBox();
const webBoundingBox = await page.evaluate(e => {
const webBoundingBox = await page.evaluate((e) => {
const rect = e.getBoundingClientRect();
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
}, element);
expect(pptrBoundingBox).toEqual(webBoundingBox);
});
});
describeFailsFirefox('ElementHandle.boxModel', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('ElementHandle.boxModel', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/resetcss.html');
@ -100,10 +108,11 @@ describe('ElementHandle specs', function() {
// Step 2: Add div and position it absolutely inside frame.
const frame = page.frames()[1];
const divHandle = (await frame.evaluateHandle(() => {
const div = document.createElement('div');
document.body.appendChild(div);
div.style = `
const divHandle = (
await frame.evaluateHandle(() => {
const div = document.createElement('div');
document.body.appendChild(div);
div.style = `
box-sizing: border-box;
position: absolute;
border-left: 1px solid black;
@ -114,8 +123,9 @@ describe('ElementHandle specs', function() {
width: 6px;
height: 7px;
`;
return div;
})).asElement();
return div;
})
).asElement();
// Step 3: query div's boxModel and assert box values.
const box = await divHandle.boxModel();
@ -139,8 +149,8 @@ describe('ElementHandle specs', function() {
});
});
it('should return null for invisible elements', async() => {
const {page} = getTestState();
it('should return null for invisible elements', async () => {
const { page } = getTestState();
await page.setContent('<div style="display:none">hi</div>');
const element = await page.$('div');
@ -148,9 +158,9 @@ describe('ElementHandle specs', function() {
});
});
describe('ElementHandle.contentFrame', function() {
itFailsFirefox('should work', async() => {
const {page,server} = getTestState();
describe('ElementHandle.contentFrame', function () {
itFailsFirefox('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
@ -160,84 +170,97 @@ describe('ElementHandle specs', function() {
});
});
describe('ElementHandle.click', function() {
it('should work', async() => {
const {page, server} = getTestState();
describe('ElementHandle.click', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await button.click();
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should work for Shadow DOM v1', async() => {
const {page, server} = getTestState();
it('should work for Shadow DOM v1', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/shadow.html');
const buttonHandle = await page.evaluateHandle(() => button);
await buttonHandle.click();
expect(await page.evaluate(() => clicked)).toBe(true);
});
it('should work for TextNodes', async() => {
const {page, server} = getTestState();
it('should work for TextNodes', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
const buttonTextNode = await page.evaluateHandle(() => document.querySelector('button').firstChild);
const buttonTextNode = await page.evaluateHandle(
() => document.querySelector('button').firstChild
);
let error = null;
await buttonTextNode.click().catch(error_ => error = error_);
await buttonTextNode.click().catch((error_) => (error = error_));
expect(error.message).toBe('Node is not of type HTMLElement');
});
it('should throw for detached nodes', async() => {
const {page, server} = getTestState();
it('should throw for detached nodes', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.remove(), button);
await page.evaluate((button) => button.remove(), button);
let error = null;
await button.click().catch(error_ => error = error_);
await button.click().catch((error_) => (error = error_));
expect(error.message).toBe('Node is detached from document');
});
it('should throw for hidden nodes', async() => {
const {page, server} = getTestState();
it('should throw for hidden nodes', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.style.display = 'none', button);
const error = await button.click().catch(error_ => error_);
expect(error.message).toBe('Node is either not visible or not an HTMLElement');
await page.evaluate((button) => (button.style.display = 'none'), button);
const error = await button.click().catch((error_) => error_);
expect(error.message).toBe(
'Node is either not visible or not an HTMLElement'
);
});
it('should throw for recursively hidden nodes', async() => {
const {page, server} = getTestState();
it('should throw for recursively hidden nodes', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.parentElement.style.display = 'none', button);
const error = await button.click().catch(error_ => error_);
expect(error.message).toBe('Node is either not visible or not an HTMLElement');
await page.evaluate(
(button) => (button.parentElement.style.display = 'none'),
button
);
const error = await button.click().catch((error_) => error_);
expect(error.message).toBe(
'Node is either not visible or not an HTMLElement'
);
});
itFailsFirefox('should throw for <br> elements', async() => {
const {page} = getTestState();
itFailsFirefox('should throw for <br> elements', async () => {
const { page } = getTestState();
await page.setContent('hello<br>goodbye');
const br = await page.$('br');
const error = await br.click().catch(error_ => error_);
expect(error.message).toBe('Node is either not visible or not an HTMLElement');
const error = await br.click().catch((error_) => error_);
expect(error.message).toBe(
'Node is either not visible or not an HTMLElement'
);
});
});
describe('ElementHandle.hover', function() {
itFailsFirefox('should work', async() => {
const {page, server} = getTestState();
describe('ElementHandle.hover', function () {
itFailsFirefox('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/scrollable.html');
const button = await page.$('#button-6');
await button.hover();
expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6');
expect(
await page.evaluate(() => document.querySelector('button:hover').id)
).toBe('button-6');
});
});
describe('ElementHandle.isIntersectingViewport', function() {
it('should work', async() => {
const {page, server} = getTestState();
describe('ElementHandle.isIntersectingViewport', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/offscreenbuttons.html');
for (let i = 0; i < 11; ++i) {
@ -249,19 +272,22 @@ describe('ElementHandle specs', function() {
});
});
describe('Custom queries', function() {
describe('Custom queries', function () {
this.afterEach(() => {
const {puppeteer} = getTestState();
const { puppeteer } = getTestState();
puppeteer.__experimental_clearQueryHandlers();
});
it('should register and unregister', async() => {
const {page, puppeteer} = getTestState();
it('should register and unregister', async () => {
const { page, puppeteer } = getTestState();
await page.setContent('<div id="not-foo"></div><div id="foo"></div>');
// Register.
puppeteer.__experimental_registerCustomQueryHandler('getById', (element, selector) => document.querySelector(`[id="${selector}"]`));
puppeteer.__experimental_registerCustomQueryHandler(
'getById',
(element, selector) => document.querySelector(`[id="${selector}"]`)
);
const element = await page.$('getById/foo');
expect(await page.evaluate(element => element.id, element)).toBe('foo');
expect(await page.evaluate((element) => element.id, element)).toBe('foo');
// Unregister.
puppeteer.__experimental_unregisterCustomQueryHandler('getById');
@ -269,53 +295,90 @@ describe('ElementHandle specs', function() {
await page.$('getById/foo');
expect.fail('Custom query handler not set - throw expected');
} catch (error) {
expect(error).toStrictEqual(new Error('Query set to use "getById", but no query handler of that name was found'));
expect(error).toStrictEqual(
new Error(
'Query set to use "getById", but no query handler of that name was found'
)
);
}
});
it('should throw with invalid query names', () => {
try {
const {puppeteer} = getTestState();
puppeteer.__experimental_registerCustomQueryHandler('1/2/3', (element, selector) => {});
const { puppeteer } = getTestState();
puppeteer.__experimental_registerCustomQueryHandler(
'1/2/3',
(element, selector) => {}
);
expect.fail('Custom query handler name was invalid - throw expected');
} catch (error) {
expect(error).toStrictEqual(new Error('Custom query handler names may only contain [a-zA-Z]'));
expect(error).toStrictEqual(
new Error('Custom query handler names may only contain [a-zA-Z]')
);
}
});
it('should work for multiple elements', async() => {
const {page, puppeteer} = getTestState();
await page.setContent('<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>');
puppeteer.__experimental_registerCustomQueryHandler('getByClass', (element, selector) => document.querySelectorAll(`.${selector}`));
it('should work for multiple elements', async () => {
const { page, puppeteer } = getTestState();
await page.setContent(
'<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
);
puppeteer.__experimental_registerCustomQueryHandler(
'getByClass',
(element, selector) => document.querySelectorAll(`.${selector}`)
);
const elements = await page.$$('getByClass/foo');
const classNames = await Promise.all(elements.map(async element => await page.evaluate(element => element.className, element)));
const classNames = await Promise.all(
elements.map(
async (element) =>
await page.evaluate((element) => element.className, element)
)
);
expect(classNames).toStrictEqual(['foo', 'foo baz']);
});
it('should eval correctly', async() => {
const {page, puppeteer} = getTestState();
await page.setContent('<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>');
puppeteer.__experimental_registerCustomQueryHandler('getByClass', (element, selector) => document.querySelectorAll(`.${selector}`));
const elements = await page.$$eval('getByClass/foo', divs => divs.length);
it('should eval correctly', async () => {
const { page, puppeteer } = getTestState();
await page.setContent(
'<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
);
puppeteer.__experimental_registerCustomQueryHandler(
'getByClass',
(element, selector) => document.querySelectorAll(`.${selector}`)
);
const elements = await page.$$eval(
'getByClass/foo',
(divs) => divs.length
);
expect(elements).toBe(2);
});
it('should wait correctly with waitForSelector', async() => {
const {page, puppeteer} = getTestState();
puppeteer.__experimental_registerCustomQueryHandler('getByClass', (element, selector) => element.querySelector(`.${selector}`));
it('should wait correctly with waitForSelector', async () => {
const { page, puppeteer } = getTestState();
puppeteer.__experimental_registerCustomQueryHandler(
'getByClass',
(element, selector) => element.querySelector(`.${selector}`)
);
const waitFor = page.waitForSelector('getByClass/foo');
// Set the page content after the waitFor has been started.
await page.setContent('<div id="not-foo"></div><div class="foo">Foo1</div>');
await page.setContent(
'<div id="not-foo"></div><div class="foo">Foo1</div>'
);
const element = await waitFor;
expect(element).toBeDefined();
});
it('should wait correctly with waitFor', async() => {
const {page, puppeteer} = getTestState();
puppeteer.__experimental_registerCustomQueryHandler('getByClass', (element, selector) => element.querySelector(`.${selector}`));
it('should wait correctly with waitFor', async () => {
const { page, puppeteer } = getTestState();
puppeteer.__experimental_registerCustomQueryHandler(
'getByClass',
(element, selector) => element.querySelector(`.${selector}`)
);
const waitFor = page.waitFor('getByClass/foo');
// Set the page content after the waitFor has been started.
await page.setContent('<div id="not-foo"></div><div class="foo">Foo1</div>');
await page.setContent(
'<div id="not-foo"></div><div class="foo">Foo1</div>'
);
const element = await waitFor;
expect(element).toBeDefined();

View File

@ -15,7 +15,11 @@
*/
const expect = require('expect');
const {getTestState,setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('Emulation', () => {
setupTestBrowserHooks();
@ -24,45 +28,44 @@ describe('Emulation', () => {
let iPhoneLandscape;
before(() => {
const {puppeteer} = getTestState();
const { puppeteer } = getTestState();
iPhone = puppeteer.devices['iPhone 6'];
iPhoneLandscape = puppeteer.devices['iPhone 6 landscape'];
});
describe('Page.viewport', function() {
describe('Page.viewport', function () {
it('should get the proper viewport size', async () => {
const { page } = getTestState();
it('should get the proper viewport size', async() => {
const {page} = getTestState();
expect(page.viewport()).toEqual({width: 800, height: 600});
await page.setViewport({width: 123, height: 456});
expect(page.viewport()).toEqual({width: 123, height: 456});
expect(page.viewport()).toEqual({ width: 800, height: 600 });
await page.setViewport({ width: 123, height: 456 });
expect(page.viewport()).toEqual({ width: 123, height: 456 });
});
it('should support mobile emulation', async() => {
const {page, server} = getTestState();
it('should support mobile emulation', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(800);
await page.setViewport(iPhone.viewport);
expect(await page.evaluate(() => window.innerWidth)).toBe(375);
await page.setViewport({width: 400, height: 300});
await page.setViewport({ width: 400, height: 300 });
expect(await page.evaluate(() => window.innerWidth)).toBe(400);
});
itFailsFirefox('should support touch emulation', async() => {
const {page, server} = getTestState();
itFailsFirefox('should support touch emulation', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
await page.setViewport(iPhone.viewport);
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true);
expect(await page.evaluate(dispatchTouch)).toBe('Received touch');
await page.setViewport({width: 100, height: 100});
await page.setViewport({ width: 100, height: 100 });
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
function dispatchTouch() {
let fulfill;
const promise = new Promise(x => fulfill = x);
window.ontouchstart = function(e) {
const promise = new Promise((x) => (fulfill = x));
window.ontouchstart = function (e) {
fulfill('Received touch');
};
window.dispatchEvent(new Event('touchstart'));
@ -72,57 +75,75 @@ describe('Emulation', () => {
return promise;
}
});
itFailsFirefox('should be detectable by Modernizr', async() => {
const {page, server} = getTestState();
itFailsFirefox('should be detectable by Modernizr', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/detect-touch.html');
expect(await page.evaluate(() => document.body.textContent.trim())).toBe('NO');
expect(await page.evaluate(() => document.body.textContent.trim())).toBe(
'NO'
);
await page.setViewport(iPhone.viewport);
await page.goto(server.PREFIX + '/detect-touch.html');
expect(await page.evaluate(() => document.body.textContent.trim())).toBe('YES');
expect(await page.evaluate(() => document.body.textContent.trim())).toBe(
'YES'
);
});
itFailsFirefox('should detect touch when applying viewport with touches', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should detect touch when applying viewport with touches',
async () => {
const { page, server } = getTestState();
await page.setViewport({width: 800, height: 600, hasTouch: true});
await page.addScriptTag({url: server.PREFIX + '/modernizr.js'});
expect(await page.evaluate(() => Modernizr.touchevents)).toBe(true);
});
itFailsFirefox('should support landscape emulation', async() => {
const {page, server} = getTestState();
await page.setViewport({ width: 800, height: 600, hasTouch: true });
await page.addScriptTag({ url: server.PREFIX + '/modernizr.js' });
expect(await page.evaluate(() => Modernizr.touchevents)).toBe(true);
}
);
itFailsFirefox('should support landscape emulation', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => screen.orientation.type)).toBe('portrait-primary');
expect(await page.evaluate(() => screen.orientation.type)).toBe(
'portrait-primary'
);
await page.setViewport(iPhoneLandscape.viewport);
expect(await page.evaluate(() => screen.orientation.type)).toBe('landscape-primary');
await page.setViewport({width: 100, height: 100});
expect(await page.evaluate(() => screen.orientation.type)).toBe('portrait-primary');
expect(await page.evaluate(() => screen.orientation.type)).toBe(
'landscape-primary'
);
await page.setViewport({ width: 100, height: 100 });
expect(await page.evaluate(() => screen.orientation.type)).toBe(
'portrait-primary'
);
});
});
describe('Page.emulate', function() {
it('should work', async() => {
const {page, server} = getTestState();
describe('Page.emulate', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/mobile.html');
await page.emulate(iPhone);
expect(await page.evaluate(() => window.innerWidth)).toBe(375);
expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone');
expect(await page.evaluate(() => navigator.userAgent)).toContain(
'iPhone'
);
});
it('should support clicking', async() => {
const {page, server} = getTestState();
it('should support clicking', async () => {
const { page, server } = getTestState();
await page.emulate(iPhone);
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.style.marginTop = '200px', button);
await page.evaluate(
(button) => (button.style.marginTop = '200px'),
button
);
await button.click();
expect(await page.evaluate(() => result)).toBe('Clicked');
});
});
describe('Page.emulateMedia [deprecated]', function() {
/* emulateMedia is deprecated in favour of emulateMediaType but we
describe('Page.emulateMedia [deprecated]', function () {
/* emulateMedia is deprecated in favour of emulateMediaType but we
* don't want to remove it from Puppeteer just yet. We can't check
* that emulateMedia === emulateMediaType because when running tests
* with COVERAGE=1 the methods get rewritten. So instead we
@ -132,121 +153,202 @@ describe('Emulation', () => {
* If you update these tests, you should update emulateMediaType's
* tests, and vice-versa.
*/
itFailsFirefox('should work', async() => {
const {page} = getTestState();
itFailsFirefox('should work', async () => {
const { page } = getTestState();
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
true
);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(
false
);
await page.emulateMedia('print');
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
false
);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true);
await page.emulateMedia(null);
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
true
);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(
false
);
});
it('should throw in case of bad argument', async() => {
const {page} = getTestState();
it('should throw in case of bad argument', async () => {
const { page } = getTestState();
let error = null;
await page.emulateMedia('bad').catch(error_ => error = error_);
await page.emulateMedia('bad').catch((error_) => (error = error_));
expect(error.message).toBe('Unsupported media type: bad');
});
});
describe('Page.emulateMediaType', function() {
/* NOTE! Updating these tests? Update the emulateMedia tests above
describe('Page.emulateMediaType', function () {
/* NOTE! Updating these tests? Update the emulateMedia tests above
* too (and see the big comment for why we have these duplicated).
*/
itFailsFirefox('should work', async() => {
const {page} = getTestState();
itFailsFirefox('should work', async () => {
const { page } = getTestState();
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
true
);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(
false
);
await page.emulateMediaType('print');
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
false
);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true);
await page.emulateMediaType(null);
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
true
);
expect(await page.evaluate(() => matchMedia('print').matches)).toBe(
false
);
});
it('should throw in case of bad argument', async() => {
const {page} = getTestState();
it('should throw in case of bad argument', async () => {
const { page } = getTestState();
let error = null;
await page.emulateMediaType('bad').catch(error_ => error = error_);
await page.emulateMediaType('bad').catch((error_) => (error = error_));
expect(error.message).toBe('Unsupported media type: bad');
});
});
describe('Page.emulateMediaFeatures', function() {
itFailsFirefox('should work', async() => {
const {page} = getTestState();
describe('Page.emulateMediaFeatures', function () {
itFailsFirefox('should work', async () => {
const { page } = getTestState();
await page.emulateMediaFeatures([
{name: 'prefers-reduced-motion', value: 'reduce'},
{ name: 'prefers-reduced-motion', value: 'reduce' },
]);
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(false);
expect(
await page.evaluate(
() => matchMedia('(prefers-reduced-motion: reduce)').matches
)
).toBe(true);
expect(
await page.evaluate(
() => matchMedia('(prefers-reduced-motion: no-preference)').matches
)
).toBe(false);
await page.emulateMediaFeatures([
{name: 'prefers-color-scheme', value: 'light'},
{ name: 'prefers-color-scheme', value: 'light' },
]);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: light)').matches
)
).toBe(true);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: dark)').matches
)
).toBe(false);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: no-preference)').matches
)
).toBe(false);
await page.emulateMediaFeatures([
{name: 'prefers-color-scheme', value: 'dark'},
{ name: 'prefers-color-scheme', value: 'dark' },
]);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: dark)').matches
)
).toBe(true);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: light)').matches
)
).toBe(false);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: no-preference)').matches
)
).toBe(false);
await page.emulateMediaFeatures([
{name: 'prefers-reduced-motion', value: 'reduce'},
{name: 'prefers-color-scheme', value: 'light'},
{ name: 'prefers-reduced-motion', value: 'reduce' },
{ name: 'prefers-color-scheme', value: 'light' },
]);
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false);
expect(
await page.evaluate(
() => matchMedia('(prefers-reduced-motion: reduce)').matches
)
).toBe(true);
expect(
await page.evaluate(
() => matchMedia('(prefers-reduced-motion: no-preference)').matches
)
).toBe(false);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: light)').matches
)
).toBe(true);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: dark)').matches
)
).toBe(false);
expect(
await page.evaluate(
() => matchMedia('(prefers-color-scheme: no-preference)').matches
)
).toBe(false);
});
it('should throw in case of bad argument', async() => {
const {page} = getTestState();
it('should throw in case of bad argument', async () => {
const { page } = getTestState();
let error = null;
await page.emulateMediaFeatures([{name: 'bad', value: ''}]).catch(error_ => error = error_);
await page
.emulateMediaFeatures([{ name: 'bad', value: '' }])
.catch((error_) => (error = error_));
expect(error.message).toBe('Unsupported media feature: bad');
});
});
describeFailsFirefox('Page.emulateTimezone', function() {
it('should work', async() => {
const {page} = getTestState();
describeFailsFirefox('Page.emulateTimezone', function () {
it('should work', async () => {
const { page } = getTestState();
page.evaluate(() => {
globalThis.date = new Date(1479579154987);
});
await page.emulateTimezone('America/Jamaica');
expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)');
expect(await page.evaluate(() => date.toString())).toBe(
'Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'
);
await page.emulateTimezone('Pacific/Honolulu');
expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)');
expect(await page.evaluate(() => date.toString())).toBe(
'Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'
);
await page.emulateTimezone('America/Buenos_Aires');
expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)');
expect(await page.evaluate(() => date.toString())).toBe(
'Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'
);
await page.emulateTimezone('Europe/Berlin');
expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)');
expect(await page.evaluate(() => date.toString())).toBe(
'Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'
);
});
it('should throw for invalid timezone IDs', async() => {
const {page} = getTestState();
it('should throw for invalid timezone IDs', async () => {
const { page } = getTestState();
let error = null;
await page.emulateTimezone('Foo/Bar').catch(error_ => error = error_);
await page.emulateTimezone('Foo/Bar').catch((error_) => (error = error_));
expect(error.message).toBe('Invalid timezone ID: Foo/Bar');
await page.emulateTimezone('Baz/Qux').catch(error_ => error = error_);
await page.emulateTimezone('Baz/Qux').catch((error_) => (error = error_));
expect(error.message).toBe('Invalid timezone ID: Baz/Qux');
});
});
});

View File

@ -16,284 +16,331 @@
const utils = require('./utils');
const expect = require('expect');
const {getTestState,setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
const bigint = typeof BigInt !== 'undefined';
describe('Evaluation specs', function() {
describe('Evaluation specs', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describe('Page.evaluate', function() {
it('should work', async() => {
const {page} = getTestState();
describe('Page.evaluate', function () {
it('should work', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => 7 * 3);
expect(result).toBe(21);
});
(bigint ? itFailsFirefox : xit)('should transfer BigInt', async() => {
const {page} = getTestState();
(bigint ? itFailsFirefox : xit)('should transfer BigInt', async () => {
const { page } = getTestState();
const result = await page.evaluate(a => a, BigInt(42));
const result = await page.evaluate((a) => a, BigInt(42));
expect(result).toBe(BigInt(42));
});
itFailsFirefox('should transfer NaN', async() => {
const {page} = getTestState();
itFailsFirefox('should transfer NaN', async () => {
const { page } = getTestState();
const result = await page.evaluate(a => a, NaN);
const result = await page.evaluate((a) => a, NaN);
expect(Object.is(result, NaN)).toBe(true);
});
itFailsFirefox('should transfer -0', async() => {
const {page} = getTestState();
itFailsFirefox('should transfer -0', async () => {
const { page } = getTestState();
const result = await page.evaluate(a => a, -0);
const result = await page.evaluate((a) => a, -0);
expect(Object.is(result, -0)).toBe(true);
});
itFailsFirefox('should transfer Infinity', async() => {
const {page} = getTestState();
itFailsFirefox('should transfer Infinity', async () => {
const { page } = getTestState();
const result = await page.evaluate(a => a, Infinity);
const result = await page.evaluate((a) => a, Infinity);
expect(Object.is(result, Infinity)).toBe(true);
});
itFailsFirefox('should transfer -Infinity', async() => {
const {page} = getTestState();
itFailsFirefox('should transfer -Infinity', async () => {
const { page } = getTestState();
const result = await page.evaluate(a => a, -Infinity);
const result = await page.evaluate((a) => a, -Infinity);
expect(Object.is(result, -Infinity)).toBe(true);
});
it('should transfer arrays', async() => {
const {page} = getTestState();
it('should transfer arrays', async () => {
const { page } = getTestState();
const result = await page.evaluate(a => a, [1, 2, 3]);
expect(result).toEqual([1,2,3]);
const result = await page.evaluate((a) => a, [1, 2, 3]);
expect(result).toEqual([1, 2, 3]);
});
it('should transfer arrays as arrays, not objects', async() => {
const {page} = getTestState();
it('should transfer arrays as arrays, not objects', async () => {
const { page } = getTestState();
const result = await page.evaluate(a => Array.isArray(a), [1, 2, 3]);
const result = await page.evaluate((a) => Array.isArray(a), [1, 2, 3]);
expect(result).toBe(true);
});
it('should modify global environment', async() => {
const {page} = getTestState();
it('should modify global environment', async () => {
const { page } = getTestState();
await page.evaluate(() => window.globalVar = 123);
await page.evaluate(() => (window.globalVar = 123));
expect(await page.evaluate('globalVar')).toBe(123);
});
it('should evaluate in the page context', async() => {
const {page, server} = getTestState();
it('should evaluate in the page context', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/global-var.html');
expect(await page.evaluate('globalVar')).toBe(123);
});
itFailsFirefox('should return undefined for objects with symbols', async() => {
const {page} = getTestState();
itFailsFirefox(
'should return undefined for objects with symbols',
async () => {
const { page } = getTestState();
expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined);
});
it('should work with function shorthands', async() => {
const {page} = getTestState();
expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined);
}
);
it('should work with function shorthands', async () => {
const { page } = getTestState();
const a = {
sum(a, b) { return a + b; },
sum(a, b) {
return a + b;
},
async mult(a, b) { return a * b; }
async mult(a, b) {
return a * b;
},
};
expect(await page.evaluate(a.sum, 1, 2)).toBe(3);
expect(await page.evaluate(a.mult, 2, 4)).toBe(8);
});
it('should work with unicode chars', async() => {
const {page} = getTestState();
it('should work with unicode chars', async () => {
const { page } = getTestState();
const result = await page.evaluate(a => a['中文字符'], {'中文字符': 42});
const result = await page.evaluate((a) => a['中文字符'], {
中文字符: 42,
});
expect(result).toBe(42);
});
itFailsFirefox('should throw when evaluation triggers reload', async() => {
const {page} = getTestState();
itFailsFirefox('should throw when evaluation triggers reload', async () => {
const { page } = getTestState();
let error = null;
await page.evaluate(() => {
location.reload();
return new Promise(() => {});
}).catch(error_ => error = error_);
await page
.evaluate(() => {
location.reload();
return new Promise(() => {});
})
.catch((error_) => (error = error_));
expect(error.message).toContain('Protocol error');
});
it('should await promise', async() => {
const {page} = getTestState();
it('should await promise', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => Promise.resolve(8 * 7));
expect(result).toBe(56);
});
it('should work right after framenavigated', async() => {
const {page, server} = getTestState();
it('should work right after framenavigated', async () => {
const { page, server } = getTestState();
let frameEvaluation = null;
page.on('framenavigated', async frame => {
page.on('framenavigated', async (frame) => {
frameEvaluation = frame.evaluate(() => 6 * 7);
});
await page.goto(server.EMPTY_PAGE);
expect(await frameEvaluation).toBe(42);
});
itFailsFirefox('should work from-inside an exposed function', async() => {
const {page} = getTestState();
itFailsFirefox('should work from-inside an exposed function', async () => {
const { page } = getTestState();
// Setup inpage callback, which calls Page.evaluate
await page.exposeFunction('callController', async function(a, b) {
await page.exposeFunction('callController', async function (a, b) {
return await page.evaluate((a, b) => a * b, a, b);
});
const result = await page.evaluate(async function() {
const result = await page.evaluate(async function () {
return await callController(9, 3);
});
expect(result).toBe(27);
});
it('should reject promise with exception', async() => {
const {page} = getTestState();
it('should reject promise with exception', async () => {
const { page } = getTestState();
let error = null;
await page.evaluate(() => not_existing_object.property).catch(error_ => error = error_);
await page
.evaluate(() => not_existing_object.property)
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain('not_existing_object');
});
it('should support thrown strings as error messages', async() => {
const {page} = getTestState();
it('should support thrown strings as error messages', async () => {
const { page } = getTestState();
let error = null;
await page.evaluate(() => { throw 'qwerty'; }).catch(error_ => error = error_);
await page
.evaluate(() => {
throw 'qwerty';
})
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain('qwerty');
});
it('should support thrown numbers as error messages', async() => {
const {page} = getTestState();
it('should support thrown numbers as error messages', async () => {
const { page } = getTestState();
let error = null;
await page.evaluate(() => { throw 100500; }).catch(error_ => error = error_);
await page
.evaluate(() => {
throw 100500;
})
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain('100500');
});
it('should return complex objects', async() => {
const {page} = getTestState();
it('should return complex objects', async () => {
const { page } = getTestState();
const object = {foo: 'bar!'};
const result = await page.evaluate(a => a, object);
const object = { foo: 'bar!' };
const result = await page.evaluate((a) => a, object);
expect(result).not.toBe(object);
expect(result).toEqual(object);
});
(bigint ? itFailsFirefox : xit)('should return BigInt', async() => {
const {page} = getTestState();
(bigint ? itFailsFirefox : xit)('should return BigInt', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => BigInt(42));
expect(result).toBe(BigInt(42));
});
itFailsFirefox('should return NaN', async() => {
const {page} = getTestState();
itFailsFirefox('should return NaN', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => NaN);
expect(Object.is(result, NaN)).toBe(true);
});
itFailsFirefox('should return -0', async() => {
const {page} = getTestState();
itFailsFirefox('should return -0', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => -0);
expect(Object.is(result, -0)).toBe(true);
});
itFailsFirefox('should return Infinity', async() => {
const {page} = getTestState();
itFailsFirefox('should return Infinity', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => Infinity);
expect(Object.is(result, Infinity)).toBe(true);
});
itFailsFirefox('should return -Infinity', async() => {
const {page} = getTestState();
itFailsFirefox('should return -Infinity', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => -Infinity);
expect(Object.is(result, -Infinity)).toBe(true);
});
it('should accept "undefined" as one of multiple parameters', async() => {
const {page} = getTestState();
it('should accept "undefined" as one of multiple parameters', async () => {
const { page } = getTestState();
const result = await page.evaluate((a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), undefined, 'foo');
const result = await page.evaluate(
(a, b) => Object.is(a, undefined) && Object.is(b, 'foo'),
undefined,
'foo'
);
expect(result).toBe(true);
});
it('should properly serialize null fields', async() => {
const {page} = getTestState();
it('should properly serialize null fields', async () => {
const { page } = getTestState();
expect(await page.evaluate(() => ({a: undefined}))).toEqual({});
expect(await page.evaluate(() => ({ a: undefined }))).toEqual({});
});
itFailsFirefox('should return undefined for non-serializable objects', async() => {
const {page} = getTestState();
itFailsFirefox(
'should return undefined for non-serializable objects',
async () => {
const { page } = getTestState();
expect(await page.evaluate(() => window)).toBe(undefined);
});
itFailsFirefox('should fail for circular object', async() => {
const {page} = getTestState();
expect(await page.evaluate(() => window)).toBe(undefined);
}
);
itFailsFirefox('should fail for circular object', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => {
const a = {};
const b = {a};
const b = { a };
a.b = b;
return a;
});
expect(result).toBe(undefined);
});
itFailsFirefox('should be able to throw a tricky error', async() => {
const {page} = getTestState();
itFailsFirefox('should be able to throw a tricky error', async () => {
const { page } = getTestState();
const windowHandle = await page.evaluateHandle(() => window);
const errorText = await windowHandle.jsonValue().catch(error_ => error_.message);
const error = await page.evaluate(errorText => {
throw new Error(errorText);
}, errorText).catch(error_ => error_);
const errorText = await windowHandle
.jsonValue()
.catch((error_) => error_.message);
const error = await page
.evaluate((errorText) => {
throw new Error(errorText);
}, errorText)
.catch((error_) => error_);
expect(error.message).toContain(errorText);
});
it('should accept a string', async() => {
const {page} = getTestState();
it('should accept a string', async () => {
const { page } = getTestState();
const result = await page.evaluate('1 + 2');
expect(result).toBe(3);
});
it('should accept a string with semi colons', async() => {
const {page} = getTestState();
it('should accept a string with semi colons', async () => {
const { page } = getTestState();
const result = await page.evaluate('1 + 5;');
expect(result).toBe(6);
});
it('should accept a string with comments', async() => {
const {page} = getTestState();
it('should accept a string with comments', async () => {
const { page } = getTestState();
const result = await page.evaluate('2 + 5;\n// do some math!');
expect(result).toBe(7);
});
itFailsFirefox('should accept element handle as an argument', async() => {
const {page} = getTestState();
itFailsFirefox('should accept element handle as an argument', async () => {
const { page } = getTestState();
await page.setContent('<section>42</section>');
const element = await page.$('section');
const text = await page.evaluate(e => e.textContent, element);
const text = await page.evaluate((e) => e.textContent, element);
expect(text).toBe('42');
});
itFailsFirefox('should throw if underlying element was disposed', async() => {
const {page} = getTestState();
itFailsFirefox(
'should throw if underlying element was disposed',
async () => {
const { page } = getTestState();
await page.setContent('<section>39</section>');
const element = await page.$('section');
expect(element).toBeTruthy();
await element.dispose();
let error = null;
await page.evaluate(e => e.textContent, element).catch(error_ => error = error_);
expect(error.message).toContain('JSHandle is disposed');
});
itFailsFirefox('should throw if elementHandles are from other frames', async() => {
const {page, server} = getTestState();
await page.setContent('<section>39</section>');
const element = await page.$('section');
expect(element).toBeTruthy();
await element.dispose();
let error = null;
await page
.evaluate((e) => e.textContent, element)
.catch((error_) => (error = error_));
expect(error.message).toContain('JSHandle is disposed');
}
);
itFailsFirefox(
'should throw if elementHandles are from other frames',
async () => {
const { page, server } = getTestState();
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const bodyHandle = await page.frames()[1].$('body');
let error = null;
await page.evaluate(body => body.innerHTML, bodyHandle).catch(error_ => error = error_);
expect(error).toBeTruthy();
expect(error.message).toContain('JSHandles can be evaluated only in the context they were created');
});
itFailsFirefox('should simulate a user gesture', async() => {
const {page} = getTestState();
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const bodyHandle = await page.frames()[1].$('body');
let error = null;
await page
.evaluate((body) => body.innerHTML, bodyHandle)
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain(
'JSHandles can be evaluated only in the context they were created'
);
}
);
itFailsFirefox('should simulate a user gesture', async () => {
const { page } = getTestState();
const result = await page.evaluate(() => {
document.body.appendChild(document.createTextNode('test'));
@ -302,99 +349,124 @@ describe('Evaluation specs', function() {
});
expect(result).toBe(true);
});
itFailsFirefox('should throw a nice error after a navigation', async() => {
const {page} = getTestState();
itFailsFirefox('should throw a nice error after a navigation', async () => {
const { page } = getTestState();
const executionContext = await page.mainFrame().executionContext();
await Promise.all([
page.waitForNavigation(),
executionContext.evaluate(() => window.location.reload())
executionContext.evaluate(() => window.location.reload()),
]);
const error = await executionContext.evaluate(() => null).catch(error_ => error_);
const error = await executionContext
.evaluate(() => null)
.catch((error_) => error_);
expect(error.message).toContain('navigation');
});
itFailsFirefox('should not throw an error when evaluation does a navigation', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should not throw an error when evaluation does a navigation',
async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/one-style.html');
const result = await page.evaluate(() => {
window.location = '/empty.html';
return [42];
});
expect(result).toEqual([42]);
});
itFailsFirefox('should transfer 100Mb of data from page to node.js', async function() {
const {page} = getTestState();
await page.goto(server.PREFIX + '/one-style.html');
const result = await page.evaluate(() => {
window.location = '/empty.html';
return [42];
});
expect(result).toEqual([42]);
}
);
itFailsFirefox(
'should transfer 100Mb of data from page to node.js',
async function () {
const { page } = getTestState();
const a = await page.evaluate(() => Array(100 * 1024 * 1024 + 1).join('a'));
expect(a.length).toBe(100 * 1024 * 1024);
});
it('should throw error with detailed information on exception inside promise ', async() => {
const {page} = getTestState();
const a = await page.evaluate(() =>
Array(100 * 1024 * 1024 + 1).join('a')
);
expect(a.length).toBe(100 * 1024 * 1024);
}
);
it('should throw error with detailed information on exception inside promise ', async () => {
const { page } = getTestState();
let error = null;
await page.evaluate(() => new Promise(() => {
throw new Error('Error in promise');
})).catch(error_ => error = error_);
await page
.evaluate(
() =>
new Promise(() => {
throw new Error('Error in promise');
})
)
.catch((error_) => (error = error_));
expect(error.message).toContain('Error in promise');
});
});
describeFailsFirefox('Page.evaluateOnNewDocument', function() {
it('should evaluate before anything else on the page', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Page.evaluateOnNewDocument', function () {
it('should evaluate before anything else on the page', async () => {
const { page, server } = getTestState();
await page.evaluateOnNewDocument(function(){
await page.evaluateOnNewDocument(function () {
window.injected = 123;
});
await page.goto(server.PREFIX + '/tamperable.html');
expect(await page.evaluate(() => window.result)).toBe(123);
});
it('should work with CSP', async() => {
const {page, server} = getTestState();
it('should work with CSP', async () => {
const { page, server } = getTestState();
server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
await page.evaluateOnNewDocument(function(){
await page.evaluateOnNewDocument(function () {
window.injected = 123;
});
await page.goto(server.PREFIX + '/empty.html');
expect(await page.evaluate(() => window.injected)).toBe(123);
// Make sure CSP works.
await page.addScriptTag({content: 'window.e = 10;'}).catch(error => void error);
await page
.addScriptTag({ content: 'window.e = 10;' })
.catch((error) => void error);
expect(await page.evaluate(() => window.e)).toBe(undefined);
});
});
describe('Frame.evaluate', function() {
itFailsFirefox('should have different execution contexts', async() => {
const {page, server} = getTestState();
describe('Frame.evaluate', function () {
itFailsFirefox('should have different execution contexts', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
expect(page.frames().length).toBe(2);
await page.frames()[0].evaluate(() => window.FOO = 'foo');
await page.frames()[1].evaluate(() => window.FOO = 'bar');
await page.frames()[0].evaluate(() => (window.FOO = 'foo'));
await page.frames()[1].evaluate(() => (window.FOO = 'bar'));
expect(await page.frames()[0].evaluate(() => window.FOO)).toBe('foo');
expect(await page.frames()[1].evaluate(() => window.FOO)).toBe('bar');
});
itFailsFirefox('should have correct execution contexts', async() => {
const {page, server} = getTestState();
itFailsFirefox('should have correct execution contexts', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/frames/one-frame.html');
expect(page.frames().length).toBe(2);
expect(await page.frames()[0].evaluate(() => document.body.textContent.trim())).toBe('');
expect(await page.frames()[1].evaluate(() => document.body.textContent.trim())).toBe(`Hi, I'm frame`);
expect(
await page.frames()[0].evaluate(() => document.body.textContent.trim())
).toBe('');
expect(
await page.frames()[1].evaluate(() => document.body.textContent.trim())
).toBe(`Hi, I'm frame`);
});
it('should execute after cross-site navigation', async() => {
const {page, server} = getTestState();
it('should execute after cross-site navigation', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const mainFrame = page.mainFrame();
expect(await mainFrame.evaluate(() => window.location.href)).toContain('localhost');
expect(await mainFrame.evaluate(() => window.location.href)).toContain(
'localhost'
);
await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
expect(await mainFrame.evaluate(() => window.location.href)).toContain('127');
expect(await mainFrame.evaluate(() => window.location.href)).toContain(
'127'
);
});
});
});

View File

@ -15,61 +15,75 @@
*/
const expect = require('expect');
const {getTestState} = require('./mocha-utils');
const { getTestState } = require('./mocha-utils');
const path = require('path');
describe('Fixtures', function() {
itFailsFirefox('dumpio option should work with pipe option ', async() => {
const {defaultBrowserOptions, puppeteerPath} = getTestState();
describe('Fixtures', function () {
itFailsFirefox('dumpio option should work with pipe option ', async () => {
const { defaultBrowserOptions, puppeteerPath } = getTestState();
let dumpioData = '';
const {spawn} = require('child_process');
const options = Object.assign({}, defaultBrowserOptions, {pipe: true, dumpio: true});
const res = spawn('node',
[path.join(__dirname, 'fixtures', 'dumpio.js'), puppeteerPath, JSON.stringify(options)]);
res.stderr.on('data', data => dumpioData += data.toString('utf8'));
await new Promise(resolve => res.on('close', resolve));
const { spawn } = require('child_process');
const options = Object.assign({}, defaultBrowserOptions, {
pipe: true,
dumpio: true,
});
const res = spawn('node', [
path.join(__dirname, 'fixtures', 'dumpio.js'),
puppeteerPath,
JSON.stringify(options),
]);
res.stderr.on('data', (data) => (dumpioData += data.toString('utf8')));
await new Promise((resolve) => res.on('close', resolve));
expect(dumpioData).toContain('message from dumpio');
});
it('should dump browser process stderr', async() => {
const {defaultBrowserOptions, puppeteerPath} = getTestState();
it('should dump browser process stderr', async () => {
const { defaultBrowserOptions, puppeteerPath } = getTestState();
let dumpioData = '';
const {spawn} = require('child_process');
const options = Object.assign({}, defaultBrowserOptions, {dumpio: true});
const res = spawn('node',
[path.join(__dirname, 'fixtures', 'dumpio.js'), puppeteerPath, JSON.stringify(options)]);
res.stderr.on('data', data => dumpioData += data.toString('utf8'));
await new Promise(resolve => res.on('close', resolve));
const { spawn } = require('child_process');
const options = Object.assign({}, defaultBrowserOptions, { dumpio: true });
const res = spawn('node', [
path.join(__dirname, 'fixtures', 'dumpio.js'),
puppeteerPath,
JSON.stringify(options),
]);
res.stderr.on('data', (data) => (dumpioData += data.toString('utf8')));
await new Promise((resolve) => res.on('close', resolve));
expect(dumpioData).toContain('DevTools listening on ws://');
});
it('should close the browser when the node process closes', async() => {
const {defaultBrowserOptions, puppeteerPath, puppeteer} = getTestState();
it('should close the browser when the node process closes', async () => {
const { defaultBrowserOptions, puppeteerPath, puppeteer } = getTestState();
const {spawn, execSync} = require('child_process');
const { spawn, execSync } = require('child_process');
const options = Object.assign({}, defaultBrowserOptions, {
// Disable DUMPIO to cleanly read stdout.
dumpio: false,
});
const res = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), puppeteerPath, JSON.stringify(options)]);
const res = spawn('node', [
path.join(__dirname, 'fixtures', 'closeme.js'),
puppeteerPath,
JSON.stringify(options),
]);
let wsEndPointCallback;
const wsEndPointPromise = new Promise(x => wsEndPointCallback = x);
const wsEndPointPromise = new Promise((x) => (wsEndPointCallback = x));
let output = '';
res.stdout.on('data', data => {
res.stdout.on('data', (data) => {
output += data;
if (output.indexOf('\n'))
wsEndPointCallback(output.substring(0, output.indexOf('\n')));
});
const browser = await puppeteer.connect({browserWSEndpoint: await wsEndPointPromise});
const browser = await puppeteer.connect({
browserWSEndpoint: await wsEndPointPromise,
});
const promises = [
new Promise(resolve => browser.once('disconnected', resolve)),
new Promise(resolve => res.on('close', resolve))
new Promise((resolve) => browser.once('disconnected', resolve)),
new Promise((resolve) => res.on('close', resolve)),
];
if (process.platform === 'win32')
execSync(`taskkill /pid ${res.pid} /T /F`);
else
process.kill(res.pid);
else process.kill(res.pid);
await Promise.all(promises);
});
});

View File

@ -1,4 +1,4 @@
(async() => {
(async () => {
const [, , puppeteerRoot, options] = process.argv;
const browser = await require(puppeteerRoot).launch(JSON.parse(options));
console.log(browser.wsEndpoint());

View File

@ -1,4 +1,4 @@
(async() => {
(async () => {
const [, , puppeteerRoot, options] = process.argv;
const browser = await require(puppeteerRoot).launch(JSON.parse(options));
const page = await browser.newPage();

View File

@ -16,15 +16,19 @@
const utils = require('./utils');
const expect = require('expect');
const {getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('Frame specs', function() {
describe('Frame specs', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describe('Frame.executionContext', function() {
itFailsFirefox('should work', async() => {
const {page, server} = getTestState();
describe('Frame.executionContext', function () {
itFailsFirefox('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
@ -39,21 +43,21 @@ describe('Frame specs', function() {
expect(context2.frame()).toBe(frame2);
await Promise.all([
context1.evaluate(() => window.a = 1),
context2.evaluate(() => window.a = 2)
context1.evaluate(() => (window.a = 1)),
context2.evaluate(() => (window.a = 2)),
]);
const [a1, a2] = await Promise.all([
context1.evaluate(() => window.a),
context2.evaluate(() => window.a)
context2.evaluate(() => window.a),
]);
expect(a1).toBe(1);
expect(a2).toBe(2);
});
});
describe('Frame.evaluateHandle', function() {
it('should work', async() => {
const {page, server} = getTestState();
describe('Frame.evaluateHandle', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const mainFrame = page.mainFrame();
@ -62,21 +66,23 @@ describe('Frame specs', function() {
});
});
describe('Frame.evaluate', function() {
itFailsFirefox('should throw for detached frames', async() => {
const {page, server} = getTestState();
describe('Frame.evaluate', function () {
itFailsFirefox('should throw for detached frames', async () => {
const { page, server } = getTestState();
const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
await utils.detachFrame(page, 'frame1');
let error = null;
await frame1.evaluate(() => 7 * 8).catch(error_ => error = error_);
expect(error.message).toContain('Execution Context is not available in detached frame');
await frame1.evaluate(() => 7 * 8).catch((error_) => (error = error_));
expect(error.message).toContain(
'Execution Context is not available in detached frame'
);
});
});
describe('Frame Management', function() {
itFailsFirefox('should handle nested frames', async() => {
const {page, server} = getTestState();
describe('Frame Management', function () {
itFailsFirefox('should handle nested frames', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/frames/nested-frames.html');
expect(utils.dumpFrames(page.mainFrame())).toEqual([
@ -84,70 +90,76 @@ describe('Frame specs', function() {
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
' http://localhost:<PORT>/frames/frame.html (uno)',
' http://localhost:<PORT>/frames/frame.html (dos)',
' http://localhost:<PORT>/frames/frame.html (aframe)'
' http://localhost:<PORT>/frames/frame.html (aframe)',
]);
});
itFailsFirefox('should send events when frames are manipulated dynamically', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should send events when frames are manipulated dynamically',
async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
// validate frameattached events
const attachedFrames = [];
page.on('frameattached', frame => attachedFrames.push(frame));
await utils.attachFrame(page, 'frame1', './assets/frame.html');
expect(attachedFrames.length).toBe(1);
expect(attachedFrames[0].url()).toContain('/assets/frame.html');
await page.goto(server.EMPTY_PAGE);
// validate frameattached events
const attachedFrames = [];
page.on('frameattached', (frame) => attachedFrames.push(frame));
await utils.attachFrame(page, 'frame1', './assets/frame.html');
expect(attachedFrames.length).toBe(1);
expect(attachedFrames[0].url()).toContain('/assets/frame.html');
// validate framenavigated events
const navigatedFrames = [];
page.on('framenavigated', frame => navigatedFrames.push(frame));
await utils.navigateFrame(page, 'frame1', './empty.html');
expect(navigatedFrames.length).toBe(1);
expect(navigatedFrames[0].url()).toBe(server.EMPTY_PAGE);
// validate framenavigated events
const navigatedFrames = [];
page.on('framenavigated', (frame) => navigatedFrames.push(frame));
await utils.navigateFrame(page, 'frame1', './empty.html');
expect(navigatedFrames.length).toBe(1);
expect(navigatedFrames[0].url()).toBe(server.EMPTY_PAGE);
// validate framedetached events
const detachedFrames = [];
page.on('framedetached', frame => detachedFrames.push(frame));
await utils.detachFrame(page, 'frame1');
expect(detachedFrames.length).toBe(1);
expect(detachedFrames[0].isDetached()).toBe(true);
});
itFailsFirefox('should send "framenavigated" when navigating on anchor URLs', async() => {
const {page, server} = getTestState();
// validate framedetached events
const detachedFrames = [];
page.on('framedetached', (frame) => detachedFrames.push(frame));
await utils.detachFrame(page, 'frame1');
expect(detachedFrames.length).toBe(1);
expect(detachedFrames[0].isDetached()).toBe(true);
}
);
itFailsFirefox(
'should send "framenavigated" when navigating on anchor URLs',
async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await Promise.all([
page.goto(server.EMPTY_PAGE + '#foo'),
utils.waitEvent(page, 'framenavigated')
]);
expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
});
it('should persist mainFrame on cross-process navigation', async() => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE);
await Promise.all([
page.goto(server.EMPTY_PAGE + '#foo'),
utils.waitEvent(page, 'framenavigated'),
]);
expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
}
);
it('should persist mainFrame on cross-process navigation', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const mainFrame = page.mainFrame();
await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
expect(page.mainFrame() === mainFrame).toBeTruthy();
});
it('should not send attach/detach events for main frame', async() => {
const {page, server} = getTestState();
it('should not send attach/detach events for main frame', async () => {
const { page, server } = getTestState();
let hasEvents = false;
page.on('frameattached', frame => hasEvents = true);
page.on('framedetached', frame => hasEvents = true);
page.on('frameattached', (frame) => (hasEvents = true));
page.on('framedetached', (frame) => (hasEvents = true));
await page.goto(server.EMPTY_PAGE);
expect(hasEvents).toBe(false);
});
itFailsFirefox('should detach child frames on navigation', async() => {
const {page, server} = getTestState();
itFailsFirefox('should detach child frames on navigation', async () => {
const { page, server } = getTestState();
let attachedFrames = [];
let detachedFrames = [];
let navigatedFrames = [];
page.on('frameattached', frame => attachedFrames.push(frame));
page.on('framedetached', frame => detachedFrames.push(frame));
page.on('framenavigated', frame => navigatedFrames.push(frame));
page.on('frameattached', (frame) => attachedFrames.push(frame));
page.on('framedetached', (frame) => detachedFrames.push(frame));
page.on('framenavigated', (frame) => navigatedFrames.push(frame));
await page.goto(server.PREFIX + '/frames/nested-frames.html');
expect(attachedFrames.length).toBe(4);
expect(detachedFrames.length).toBe(0);
@ -161,15 +173,15 @@ describe('Frame specs', function() {
expect(detachedFrames.length).toBe(4);
expect(navigatedFrames.length).toBe(1);
});
itFailsFirefox('should support framesets', async() => {
const {page, server} = getTestState();
itFailsFirefox('should support framesets', async () => {
const { page, server } = getTestState();
let attachedFrames = [];
let detachedFrames = [];
let navigatedFrames = [];
page.on('frameattached', frame => attachedFrames.push(frame));
page.on('framedetached', frame => detachedFrames.push(frame));
page.on('framenavigated', frame => navigatedFrames.push(frame));
page.on('frameattached', (frame) => attachedFrames.push(frame));
page.on('framedetached', (frame) => detachedFrames.push(frame));
page.on('framenavigated', (frame) => navigatedFrames.push(frame));
await page.goto(server.PREFIX + '/frames/frameset.html');
expect(attachedFrames.length).toBe(4);
expect(detachedFrames.length).toBe(0);
@ -183,36 +195,36 @@ describe('Frame specs', function() {
expect(detachedFrames.length).toBe(4);
expect(navigatedFrames.length).toBe(1);
});
itFailsFirefox('should report frame from-inside shadow DOM', async() => {
const {page, server} = getTestState();
itFailsFirefox('should report frame from-inside shadow DOM', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/shadow.html');
await page.evaluate(async url => {
await page.evaluate(async (url) => {
const frame = document.createElement('iframe');
frame.src = url;
document.body.shadowRoot.appendChild(frame);
await new Promise(x => frame.onload = x);
await new Promise((x) => (frame.onload = x));
}, server.EMPTY_PAGE);
expect(page.frames().length).toBe(2);
expect(page.frames()[1].url()).toBe(server.EMPTY_PAGE);
});
itFailsFirefox('should report frame.name()', async() => {
const {page, server} = getTestState();
itFailsFirefox('should report frame.name()', async () => {
const { page, server } = getTestState();
await utils.attachFrame(page, 'theFrameId', server.EMPTY_PAGE);
await page.evaluate(url => {
await page.evaluate((url) => {
const frame = document.createElement('iframe');
frame.name = 'theFrameName';
frame.src = url;
document.body.appendChild(frame);
return new Promise(x => frame.onload = x);
return new Promise((x) => (frame.onload = x));
}, server.EMPTY_PAGE);
expect(page.frames()[0].name()).toBe('');
expect(page.frames()[1].name()).toBe('theFrameId');
expect(page.frames()[2].name()).toBe('theFrameName');
});
itFailsFirefox('should report frame.parent()', async() => {
const {page, server} = getTestState();
itFailsFirefox('should report frame.parent()', async () => {
const { page, server } = getTestState();
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE);
@ -220,21 +232,28 @@ describe('Frame specs', function() {
expect(page.frames()[1].parentFrame()).toBe(page.mainFrame());
expect(page.frames()[2].parentFrame()).toBe(page.mainFrame());
});
itFailsFirefox('should report different frame instance when frame re-attaches', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should report different frame instance when frame re-attaches',
async () => {
const { page, server } = getTestState();
const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
await page.evaluate(() => {
window.frame = document.querySelector('#frame1');
window.frame.remove();
});
expect(frame1.isDetached()).toBe(true);
const [frame2] = await Promise.all([
utils.waitEvent(page, 'frameattached'),
page.evaluate(() => document.body.appendChild(window.frame)),
]);
expect(frame2.isDetached()).toBe(false);
expect(frame1).not.toBe(frame2);
});
const frame1 = await utils.attachFrame(
page,
'frame1',
server.EMPTY_PAGE
);
await page.evaluate(() => {
window.frame = document.querySelector('#frame1');
window.frame.remove();
});
expect(frame1.isDetached()).toBe(true);
const [frame2] = await Promise.all([
utils.waitEvent(page, 'frameattached'),
page.evaluate(() => document.body.appendChild(window.frame)),
]);
expect(frame2.isDetached()).toBe(false);
expect(frame1).not.toBe(frame2);
}
);
});
});

View File

@ -21,15 +21,14 @@ const PNG = require('pngjs').PNG;
const jpeg = require('jpeg-js');
const pixelmatch = require('pixelmatch');
module.exports = {compare};
module.exports = { compare };
const GoldenComparators = {
'image/png': compareImages,
'image/jpeg': compareImages,
'text/plain': compareText
'text/plain': compareText,
};
/**
* @param {?Object} actualBuffer
* @param {!Buffer} expectedBuffer
@ -38,18 +37,31 @@ const GoldenComparators = {
*/
function compareImages(actualBuffer, expectedBuffer, mimeType) {
if (!actualBuffer || !(actualBuffer instanceof Buffer))
return {errorMessage: 'Actual result should be Buffer.'};
return { errorMessage: 'Actual result should be Buffer.' };
const actual = mimeType === 'image/png' ? PNG.sync.read(actualBuffer) : jpeg.decode(actualBuffer);
const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer);
const actual =
mimeType === 'image/png'
? PNG.sync.read(actualBuffer)
: jpeg.decode(actualBuffer);
const expected =
mimeType === 'image/png'
? PNG.sync.read(expectedBuffer)
: jpeg.decode(expectedBuffer);
if (expected.width !== actual.width || expected.height !== actual.height) {
return {
errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. `
errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. `,
};
}
const diff = new PNG({width: expected.width, height: expected.height});
const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, {threshold: 0.1});
return count > 0 ? {diff: PNG.sync.write(diff)} : null;
const diff = new PNG({ width: expected.width, height: expected.height });
const count = pixelmatch(
expected.data,
actual.data,
diff.data,
expected.width,
expected.height,
{ threshold: 0.1 }
);
return count > 0 ? { diff: PNG.sync.write(diff) } : null;
}
/**
@ -59,10 +71,9 @@ function compareImages(actualBuffer, expectedBuffer, mimeType) {
*/
function compareText(actual, expectedBuffer) {
if (typeof actual !== 'string')
return {errorMessage: 'Actual result should be string'};
return { errorMessage: 'Actual result should be string' };
const expected = expectedBuffer.toString('utf-8');
if (expected === actual)
return null;
if (expected === actual) return null;
const diff = new Diff();
const result = diff.main(expected, actual);
diff.cleanupSemantic(result);
@ -71,7 +82,7 @@ function compareText(actual, expectedBuffer) {
html = `<link rel="stylesheet" href="file://${diffStylePath}">` + html;
return {
diff: html,
diffExtension: '.html'
diffExtension: '.html',
};
}
@ -86,14 +97,15 @@ function compare(goldenPath, outputPath, actual, goldenName) {
const expectedPath = path.join(goldenPath, goldenName);
const actualPath = path.join(outputPath, goldenName);
const messageSuffix = 'Output is saved in "' + path.basename(outputPath + '" directory');
const messageSuffix =
'Output is saved in "' + path.basename(outputPath + '" directory');
if (!fs.existsSync(expectedPath)) {
ensureOutputDir();
fs.writeFileSync(actualPath, actual);
return {
pass: false,
message: goldenName + ' is missing in golden results. ' + messageSuffix
message: goldenName + ' is missing in golden results. ' + messageSuffix,
};
}
const expected = fs.readFileSync(expectedPath);
@ -102,12 +114,12 @@ function compare(goldenPath, outputPath, actual, goldenName) {
if (!comparator) {
return {
pass: false,
message: 'Failed to find comparator with type ' + mimeType + ': ' + goldenName
message:
'Failed to find comparator with type ' + mimeType + ': ' + goldenName,
};
}
const result = comparator(actual, expected, mimeType);
if (!result)
return {pass: true};
if (!result) return { pass: true };
ensureOutputDir();
if (goldenPath === outputPath) {
fs.writeFileSync(addSuffix(actualPath, '-actual'), actual);
@ -122,16 +134,14 @@ function compare(goldenPath, outputPath, actual, goldenName) {
}
let message = goldenName + ' mismatch!';
if (result.errorMessage)
message += ' ' + result.errorMessage;
if (result.errorMessage) message += ' ' + result.errorMessage;
return {
pass: false,
message: message + ' ' + messageSuffix
message: message + ' ' + messageSuffix,
};
function ensureOutputDir() {
if (!fs.existsSync(outputPath))
fs.mkdirSync(outputPath);
if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath);
}
}

View File

@ -17,10 +17,10 @@
const path = require('path');
const os = require('os');
const fs = require('fs');
const {promisify} = require('util');
const {waitEvent} = require('./utils');
const { promisify } = require('util');
const { waitEvent } = require('./utils');
const expect = require('expect');
const {getTestState} = require('./mocha-utils');
const { getTestState } = require('./mocha-utils');
const rmAsync = promisify(require('rimraf'));
const mkdtempAsync = promisify(fs.mkdtemp);
@ -29,8 +29,7 @@ const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const extensionPath = path.join(__dirname, 'assets', 'simple-extension');
describeChromeOnly('headful tests', function() {
describeChromeOnly('headful tests', function () {
/* These tests fire up an actual browser so let's
* allow a higher timeout
*/
@ -41,12 +40,12 @@ describeChromeOnly('headful tests', function() {
let extensionOptions;
beforeEach(() => {
const {defaultBrowserOptions} = getTestState();
const { defaultBrowserOptions } = getTestState();
headfulOptions = Object.assign({}, defaultBrowserOptions, {
headless: false
headless: false,
});
headlessOptions = Object.assign({}, defaultBrowserOptions, {
headless: true
headless: true,
});
extensionOptions = Object.assign({}, defaultBrowserOptions, {
@ -56,88 +55,101 @@ describeChromeOnly('headful tests', function() {
`--load-extension=${extensionPath}`,
],
});
});
describe('HEADFUL', function() {
it('background_page target type should be available', async() => {
const {puppeteer} = getTestState();
describe('HEADFUL', function () {
it('background_page target type should be available', async () => {
const { puppeteer } = getTestState();
const browserWithExtension = await puppeteer.launch(extensionOptions);
const page = await browserWithExtension.newPage();
const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page');
const backgroundPageTarget = await browserWithExtension.waitForTarget(
(target) => target.type() === 'background_page'
);
await page.close();
await browserWithExtension.close();
expect(backgroundPageTarget).toBeTruthy();
});
it('target.page() should return a background_page', async function() {
const {puppeteer} = getTestState();
it('target.page() should return a background_page', async function () {
const { puppeteer } = getTestState();
const browserWithExtension = await puppeteer.launch(extensionOptions);
const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page');
const backgroundPageTarget = await browserWithExtension.waitForTarget(
(target) => target.type() === 'background_page'
);
const page = await backgroundPageTarget.page();
expect(await page.evaluate(() => 2 * 3)).toBe(6);
expect(await page.evaluate(() => window.MAGIC)).toBe(42);
await browserWithExtension.close();
});
it('should have default url when launching browser', async function() {
const {puppeteer} = getTestState();
it('should have default url when launching browser', async function () {
const { puppeteer } = getTestState();
const browser = await puppeteer.launch(extensionOptions);
const pages = (await browser.pages()).map(page => page.url());
const pages = (await browser.pages()).map((page) => page.url());
expect(pages).toEqual(['about:blank']);
await browser.close();
});
itFailsWindowsUntilDate(
/* We have deferred fixing this test on Windows in favour of
* getting all other Windows tests running on CI. Putting this
* date in to force us to come back and debug properly in the
* future.
*/
new Date('2020-06-01'),
'headless should be able to read cookies written by headful', async() => {
const {server, puppeteer} = getTestState();
/* We have deferred fixing this test on Windows in favour of
* getting all other Windows tests running on CI. Putting this
* date in to force us to come back and debug properly in the
* future.
*/
new Date('2020-06-01'),
'headless should be able to read cookies written by headful',
async () => {
const { server, puppeteer } = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER);
// Write a cookie in headful chrome
const headfulBrowser = await puppeteer.launch(Object.assign({userDataDir}, headfulOptions));
const headfulPage = await headfulBrowser.newPage();
await headfulPage.goto(server.EMPTY_PAGE);
await headfulPage.evaluate(() => document.cookie = 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
await headfulBrowser.close();
// Read the cookie from headless chrome
const headlessBrowser = await puppeteer.launch(Object.assign({userDataDir}, headlessOptions));
const headlessPage = await headlessBrowser.newPage();
await headlessPage.goto(server.EMPTY_PAGE);
const cookie = await headlessPage.evaluate(() => document.cookie);
await headlessBrowser.close();
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(error => {});
expect(cookie).toBe('foo=true');
});
const userDataDir = await mkdtempAsync(TMP_FOLDER);
// Write a cookie in headful chrome
const headfulBrowser = await puppeteer.launch(
Object.assign({ userDataDir }, headfulOptions)
);
const headfulPage = await headfulBrowser.newPage();
await headfulPage.goto(server.EMPTY_PAGE);
await headfulPage.evaluate(
() =>
(document.cookie =
'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT')
);
await headfulBrowser.close();
// Read the cookie from headless chrome
const headlessBrowser = await puppeteer.launch(
Object.assign({ userDataDir }, headlessOptions)
);
const headlessPage = await headlessBrowser.newPage();
await headlessPage.goto(server.EMPTY_PAGE);
const cookie = await headlessPage.evaluate(() => document.cookie);
await headlessBrowser.close();
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch((error) => {});
expect(cookie).toBe('foo=true');
}
);
// TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548
xit('OOPIF: should report google.com frame', async() => {
const {server} = getTestState();
xit('OOPIF: should report google.com frame', async () => {
const { server } = getTestState();
// https://google.com is isolated by default in Chromium embedder.
const browser = await puppeteer.launch(headfulOptions);
const page = await browser.newPage();
await page.goto(server.EMPTY_PAGE);
await page.setRequestInterception(true);
page.on('request', r => r.respond({body: 'YO, GOOGLE.COM'}));
page.on('request', (r) => r.respond({ body: 'YO, GOOGLE.COM' }));
await page.evaluate(() => {
const frame = document.createElement('iframe');
frame.setAttribute('src', 'https://google.com/');
document.body.appendChild(frame);
return new Promise(x => frame.onload = x);
return new Promise((x) => (frame.onload = x));
});
await page.waitForSelector('iframe[src="https://google.com/"]');
const urls = page.frames().map(frame => frame.url()).sort();
expect(urls).toEqual([
server.EMPTY_PAGE,
'https://google.com/'
]);
const urls = page
.frames()
.map((frame) => frame.url())
.sort();
expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']);
await browser.close();
});
it('should close browser with beforeunload page', async() => {
const {server, puppeteer} = getTestState();
it('should close browser with beforeunload page', async () => {
const { server, puppeteer } = getTestState();
const browser = await puppeteer.launch(headfulOptions);
const page = await browser.newPage();
@ -147,39 +159,47 @@ describeChromeOnly('headful tests', function() {
await page.click('body');
await browser.close();
});
it('should open devtools when "devtools: true" option is given', async() => {
const {puppeteer} = getTestState();
it('should open devtools when "devtools: true" option is given', async () => {
const { puppeteer } = getTestState();
const browser = await puppeteer.launch(Object.assign({devtools: true}, headfulOptions));
const browser = await puppeteer.launch(
Object.assign({ devtools: true }, headfulOptions)
);
const context = await browser.createIncognitoBrowserContext();
await Promise.all([
context.newPage(),
context.waitForTarget(target => target.url().includes('devtools://')),
context.waitForTarget((target) => target.url().includes('devtools://')),
]);
await browser.close();
});
});
describe('Page.bringToFront', function() {
it('should work', async() => {
const {puppeteer} = getTestState();
describe('Page.bringToFront', function () {
it('should work', async () => {
const { puppeteer } = getTestState();
const browser = await puppeteer.launch(headfulOptions);
const page1 = await browser.newPage();
const page2 = await browser.newPage();
await page1.bringToFront();
expect(await page1.evaluate(() => document.visibilityState)).toBe('visible');
expect(await page2.evaluate(() => document.visibilityState)).toBe('hidden');
expect(await page1.evaluate(() => document.visibilityState)).toBe(
'visible'
);
expect(await page2.evaluate(() => document.visibilityState)).toBe(
'hidden'
);
await page2.bringToFront();
expect(await page1.evaluate(() => document.visibilityState)).toBe('hidden');
expect(await page2.evaluate(() => document.visibilityState)).toBe('visible');
expect(await page1.evaluate(() => document.visibilityState)).toBe(
'hidden'
);
expect(await page2.evaluate(() => document.visibilityState)).toBe(
'visible'
);
await page1.close();
await page2.close();
await browser.close();
});
});
});

View File

@ -15,9 +15,9 @@
*/
const expect = require('expect');
const {getTestState} = require('./mocha-utils');
const { getTestState } = require('./mocha-utils');
describeFailsFirefox('ignoreHTTPSErrors', function() {
describeFailsFirefox('ignoreHTTPSErrors', function () {
/* Note that this test creates its own browser rather than use
* the one provided by the test set-up as we need one
* with ignoreHTTPSErrors set to true
@ -26,35 +26,38 @@ describeFailsFirefox('ignoreHTTPSErrors', function() {
let context;
let page;
before(async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
const options = Object.assign({ignoreHTTPSErrors: true}, defaultBrowserOptions);
before(async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const options = Object.assign(
{ ignoreHTTPSErrors: true },
defaultBrowserOptions
);
browser = await puppeteer.launch(options);
});
after(async() => {
after(async () => {
await browser.close();
browser = null;
});
beforeEach(async() => {
beforeEach(async () => {
context = await browser.createIncognitoBrowserContext();
page = await context.newPage();
});
afterEach(async() => {
afterEach(async () => {
await context.close();
context = null;
page = null;
});
describe('Response.securityDetails', function() {
it('should work', async() => {
const {httpsServer} = getTestState();
describe('Response.securityDetails', function () {
it('should work', async () => {
const { httpsServer } = getTestState();
const [serverRequest, response] = await Promise.all([
httpsServer.waitForRequest('/empty.html'),
page.goto(httpsServer.EMPTY_PAGE)
page.goto(httpsServer.EMPTY_PAGE),
]);
const securityDetails = response.securityDetails();
expect(securityDetails.issuer()).toBe('puppeteer-tests');
@ -64,21 +67,21 @@ describeFailsFirefox('ignoreHTTPSErrors', function() {
expect(securityDetails.validFrom()).toBe(1550084863);
expect(securityDetails.validTo()).toBe(33086084863);
});
it('should be |null| for non-secure requests', async() => {
const {server} = getTestState();
it('should be |null| for non-secure requests', async () => {
const { server } = getTestState();
const response = await page.goto(server.EMPTY_PAGE);
expect(response.securityDetails()).toBe(null);
});
it('Network redirects should report SecurityDetails', async() => {
const {httpsServer} = getTestState();
it('Network redirects should report SecurityDetails', async () => {
const { httpsServer } = getTestState();
httpsServer.setRedirect('/plzredirect', '/empty.html');
const responses = [];
page.on('response', response => responses.push(response));
const [serverRequest, ] = await Promise.all([
const responses = [];
page.on('response', (response) => responses.push(response));
const [serverRequest] = await Promise.all([
httpsServer.waitForRequest('/plzredirect'),
page.goto(httpsServer.PREFIX + '/plzredirect')
page.goto(httpsServer.PREFIX + '/plzredirect'),
]);
expect(responses.length).toBe(2);
expect(responses[0].status()).toBe(302);
@ -88,29 +91,33 @@ describeFailsFirefox('ignoreHTTPSErrors', function() {
});
});
it('should work', async() => {
const {httpsServer} = getTestState();
it('should work', async () => {
const { httpsServer } = getTestState();
let error = null;
const response = await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => error = error_);
const response = await page
.goto(httpsServer.EMPTY_PAGE)
.catch((error_) => (error = error_));
expect(error).toBe(null);
expect(response.ok()).toBe(true);
});
it('should work with request interception', async() => {
const {httpsServer} = getTestState();
it('should work with request interception', async () => {
const { httpsServer } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => request.continue());
page.on('request', (request) => request.continue());
const response = await page.goto(httpsServer.EMPTY_PAGE);
expect(response.status()).toBe(200);
});
it('should work with mixed content', async() => {
const {server, httpsServer} = getTestState();
it('should work with mixed content', async () => {
const { server, httpsServer } = getTestState();
httpsServer.setRoute('/mixedcontent.html', (req, res) => {
res.end(`<iframe src=${server.EMPTY_PAGE}></iframe>`);
});
await page.goto(httpsServer.PREFIX + '/mixedcontent.html', {waitUntil: 'load'});
await page.goto(httpsServer.PREFIX + '/mixedcontent.html', {
waitUntil: 'load',
});
expect(page.frames().length).toBe(2);
// Make sure blocked iframe has functional execution context
// @see https://github.com/puppeteer/puppeteer/issues/2709

View File

@ -16,42 +16,55 @@
const path = require('path');
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt');
describe('input tests', function() {
describe('input tests', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describeFailsFirefox('input', function() {
it('should upload the file', async() => {
const {page, server} = getTestState();
describeFailsFirefox('input', function () {
it('should upload the file', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/fileupload.html');
const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD);
const input = await page.$('input');
await page.evaluate(e => {
await page.evaluate((e) => {
window._inputEvents = [];
e.addEventListener('change', ev => window._inputEvents.push(ev.type));
e.addEventListener('input', ev => window._inputEvents.push(ev.type));
e.addEventListener('change', (ev) => window._inputEvents.push(ev.type));
e.addEventListener('input', (ev) => window._inputEvents.push(ev.type));
}, input);
await input.uploadFile(filePath);
expect(await page.evaluate(e => e.files[0].name, input)).toBe('file-to-upload.txt');
expect(await page.evaluate(e => e.files[0].type, input)).toBe('text/plain');
expect(await page.evaluate(() => window._inputEvents)).toEqual(['input', 'change']);
expect(await page.evaluate(e => {
const reader = new FileReader();
const promise = new Promise(fulfill => reader.onload = fulfill);
reader.readAsText(e.files[0]);
return promise.then(() => reader.result);
}, input)).toBe('contents of the file');
expect(await page.evaluate((e) => e.files[0].name, input)).toBe(
'file-to-upload.txt'
);
expect(await page.evaluate((e) => e.files[0].type, input)).toBe(
'text/plain'
);
expect(await page.evaluate(() => window._inputEvents)).toEqual([
'input',
'change',
]);
expect(
await page.evaluate((e) => {
const reader = new FileReader();
const promise = new Promise((fulfill) => (reader.onload = fulfill));
reader.readAsText(e.files[0]);
return promise.then(() => reader.result);
}, input)
).toBe('contents of the file');
});
});
describeFailsFirefox('Page.waitForFileChooser', function() {
it('should work when file input is attached to DOM', async() => {
const {page} = getTestState();
describeFailsFirefox('Page.waitForFileChooser', function () {
it('should work when file input is attached to DOM', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
const [chooser] = await Promise.all([
@ -60,8 +73,8 @@ describe('input tests', function() {
]);
expect(chooser).toBeTruthy();
});
it('should work when file input is not attached to DOM', async() => {
const {page} = getTestState();
it('should work when file input is not attached to DOM', async () => {
const { page } = getTestState();
const [chooser] = await Promise.all([
page.waitForFileChooser(),
@ -73,104 +86,124 @@ describe('input tests', function() {
]);
expect(chooser).toBeTruthy();
});
it('should respect timeout', async() => {
const {page, puppeteer} = getTestState();
it('should respect timeout', async () => {
const { page, puppeteer } = getTestState();
let error = null;
await page.waitForFileChooser({timeout: 1}).catch(error_ => error = error_);
await page
.waitForFileChooser({ timeout: 1 })
.catch((error_) => (error = error_));
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should respect default timeout when there is no custom timeout', async() => {
const {page, puppeteer} = getTestState();
it('should respect default timeout when there is no custom timeout', async () => {
const { page, puppeteer } = getTestState();
page.setDefaultTimeout(1);
let error = null;
await page.waitForFileChooser().catch(error_ => error = error_);
await page.waitForFileChooser().catch((error_) => (error = error_));
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should prioritize exact timeout over default timeout', async() => {
const {page, puppeteer} = getTestState();
it('should prioritize exact timeout over default timeout', async () => {
const { page, puppeteer } = getTestState();
page.setDefaultTimeout(0);
let error = null;
await page.waitForFileChooser({timeout: 1}).catch(error_ => error = error_);
await page
.waitForFileChooser({ timeout: 1 })
.catch((error_) => (error = error_));
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should work with no timeout', async() => {
const {page} = getTestState();
it('should work with no timeout', async () => {
const { page } = getTestState();
const [chooser] = await Promise.all([
page.waitForFileChooser({timeout: 0}),
page.evaluate(() => setTimeout(() => {
const el = document.createElement('input');
el.type = 'file';
el.click();
}, 50))
page.waitForFileChooser({ timeout: 0 }),
page.evaluate(() =>
setTimeout(() => {
const el = document.createElement('input');
el.type = 'file';
el.click();
}, 50)
),
]);
expect(chooser).toBeTruthy();
});
it('should return the same file chooser when there are many watchdogs simultaneously', async() => {
const {page} = getTestState();
it('should return the same file chooser when there are many watchdogs simultaneously', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
const [fileChooser1, fileChooser2] = await Promise.all([
page.waitForFileChooser(),
page.waitForFileChooser(),
page.$eval('input', input => input.click()),
page.$eval('input', (input) => input.click()),
]);
expect(fileChooser1 === fileChooser2).toBe(true);
});
});
describeFailsFirefox('FileChooser.accept', function() {
it('should accept single file', async() => {
const {page} = getTestState();
describeFailsFirefox('FileChooser.accept', function () {
it('should accept single file', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file oninput='javascript:console.timeStamp()'>`);
await page.setContent(
`<input type=file oninput='javascript:console.timeStamp()'>`
);
const [chooser] = await Promise.all([
page.waitForFileChooser(),
page.click('input'),
]);
await Promise.all([
chooser.accept([FILE_TO_UPLOAD]),
new Promise(x => page.once('metrics', x)),
new Promise((x) => page.once('metrics', x)),
]);
expect(await page.$eval('input', input => input.files.length)).toBe(1);
expect(await page.$eval('input', input => input.files[0].name)).toBe('file-to-upload.txt');
expect(await page.$eval('input', (input) => input.files.length)).toBe(1);
expect(await page.$eval('input', (input) => input.files[0].name)).toBe(
'file-to-upload.txt'
);
});
it('should be able to read selected file', async() => {
const {page} = getTestState();
it('should be able to read selected file', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD]));
expect(await page.$eval('input', async picker => {
picker.click();
await new Promise(x => picker.oninput = x);
const reader = new FileReader();
const promise = new Promise(fulfill => reader.onload = fulfill);
reader.readAsText(picker.files[0]);
return promise.then(() => reader.result);
})).toBe('contents of the file');
page
.waitForFileChooser()
.then((chooser) => chooser.accept([FILE_TO_UPLOAD]));
expect(
await page.$eval('input', async (picker) => {
picker.click();
await new Promise((x) => (picker.oninput = x));
const reader = new FileReader();
const promise = new Promise((fulfill) => (reader.onload = fulfill));
reader.readAsText(picker.files[0]);
return promise.then(() => reader.result);
})
).toBe('contents of the file');
});
it('should be able to reset selected files with empty file list', async() => {
const {page} = getTestState();
it('should be able to reset selected files with empty file list', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD]));
expect(await page.$eval('input', async picker => {
picker.click();
await new Promise(x => picker.oninput = x);
return picker.files.length;
})).toBe(1);
page.waitForFileChooser().then(chooser => chooser.accept([]));
expect(await page.$eval('input', async picker => {
picker.click();
await new Promise(x => picker.oninput = x);
return picker.files.length;
})).toBe(0);
page
.waitForFileChooser()
.then((chooser) => chooser.accept([FILE_TO_UPLOAD]));
expect(
await page.$eval('input', async (picker) => {
picker.click();
await new Promise((x) => (picker.oninput = x));
return picker.files.length;
})
).toBe(1);
page.waitForFileChooser().then((chooser) => chooser.accept([]));
expect(
await page.$eval('input', async (picker) => {
picker.click();
await new Promise((x) => (picker.oninput = x));
return picker.files.length;
})
).toBe(0);
});
it('should not accept multiple files for single-file input', async() => {
const {page} = getTestState();
it('should not accept multiple files for single-file input', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
const [chooser] = await Promise.all([
@ -178,14 +211,19 @@ describe('input tests', function() {
page.click('input'),
]);
let error = null;
await chooser.accept([
path.relative(process.cwd(), __dirname + '/assets/file-to-upload.txt'),
path.relative(process.cwd(), __dirname + '/assets/pptr.png'),
]).catch(error_ => error = error_);
await chooser
.accept([
path.relative(
process.cwd(),
__dirname + '/assets/file-to-upload.txt'
),
path.relative(process.cwd(), __dirname + '/assets/pptr.png'),
])
.catch((error_) => (error = error_));
expect(error).not.toBe(null);
});
it('should fail for non-existent files', async() => {
const {page} = getTestState();
it('should fail for non-existent files', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
const [chooser] = await Promise.all([
@ -193,27 +231,31 @@ describe('input tests', function() {
page.click('input'),
]);
let error = null;
await chooser.accept(['file-does-not-exist.txt']).catch(error_ => error = error_);
await chooser
.accept(['file-does-not-exist.txt'])
.catch((error_) => (error = error_));
expect(error).not.toBe(null);
});
it('should fail when accepting file chooser twice', async() => {
const {page} = getTestState();
it('should fail when accepting file chooser twice', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.$eval('input', input => input.click()),
page.$eval('input', (input) => input.click()),
]);
await fileChooser.accept([]);
let error = null;
await fileChooser.accept([]).catch(error_ => error = error_);
expect(error.message).toBe('Cannot accept FileChooser which is already handled!');
await fileChooser.accept([]).catch((error_) => (error = error_));
expect(error.message).toBe(
'Cannot accept FileChooser which is already handled!'
);
});
});
describeFailsFirefox('FileChooser.cancel', function() {
it('should cancel dialog', async() => {
const {page} = getTestState();
describeFailsFirefox('FileChooser.cancel', function () {
it('should cancel dialog', async () => {
const { page } = getTestState();
// Consider file chooser canceled if we can summon another one.
// There's no reliable way in WebPlatform to see that FileChooser was
@ -221,33 +263,35 @@ describe('input tests', function() {
await page.setContent(`<input type=file>`);
const [fileChooser1] = await Promise.all([
page.waitForFileChooser(),
page.$eval('input', input => input.click()),
page.$eval('input', (input) => input.click()),
]);
await fileChooser1.cancel();
// If this resolves, than we successfully canceled file chooser.
await Promise.all([
page.waitForFileChooser(),
page.$eval('input', input => input.click()),
page.$eval('input', (input) => input.click()),
]);
});
it('should fail when canceling file chooser twice', async() => {
const {page} = getTestState();
it('should fail when canceling file chooser twice', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.$eval('input', input => input.click()),
page.$eval('input', (input) => input.click()),
]);
await fileChooser.cancel();
let error = null;
await fileChooser.cancel().catch(error_ => error = error_);
expect(error.message).toBe('Cannot cancel FileChooser which is already handled!');
await fileChooser.cancel().catch((error_) => (error = error_));
expect(error.message).toBe(
'Cannot cancel FileChooser which is already handled!'
);
});
});
describeFailsFirefox('FileChooser.isMultiple', () => {
it('should work for single file pick', async() => {
const {page} = getTestState();
it('should work for single file pick', async () => {
const { page } = getTestState();
await page.setContent(`<input type=file>`);
const [chooser] = await Promise.all([
@ -256,8 +300,8 @@ describe('input tests', function() {
]);
expect(chooser.isMultiple()).toBe(false);
});
it('should work for "multiple"', async() => {
const {page} = getTestState();
it('should work for "multiple"', async () => {
const { page } = getTestState();
await page.setContent(`<input multiple type=file>`);
const [chooser] = await Promise.all([
@ -266,8 +310,8 @@ describe('input tests', function() {
]);
expect(chooser.isMultiple()).toBe(true);
});
it('should work for "webkitdirectory"', async() => {
const {page} = getTestState();
it('should work for "webkitdirectory"', async () => {
const { page } = getTestState();
await page.setContent(`<input multiple webkitdirectory type=file>`);
const [chooser] = await Promise.all([

View File

@ -15,126 +15,134 @@
*/
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('JSHandle', function() {
describe('JSHandle', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describe('Page.evaluateHandle', function() {
it('should work', async() => {
const {page} = getTestState();
describe('Page.evaluateHandle', function () {
it('should work', async () => {
const { page } = getTestState();
const windowHandle = await page.evaluateHandle(() => window);
expect(windowHandle).toBeTruthy();
});
it('should accept object handle as an argument', async() => {
const {page} = getTestState();
it('should accept object handle as an argument', async () => {
const { page } = getTestState();
const navigatorHandle = await page.evaluateHandle(() => navigator);
const text = await page.evaluate(e => e.userAgent, navigatorHandle);
const text = await page.evaluate((e) => e.userAgent, navigatorHandle);
expect(text).toContain('Mozilla');
});
it('should accept object handle to primitive types', async() => {
const {page} = getTestState();
it('should accept object handle to primitive types', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => 5);
const isFive = await page.evaluate(e => Object.is(e, 5), aHandle);
const isFive = await page.evaluate((e) => Object.is(e, 5), aHandle);
expect(isFive).toBeTruthy();
});
it('should warn on nested object handles', async() => {
const {page} = getTestState();
it('should warn on nested object handles', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => document.body);
let error = null;
await page.evaluateHandle(
opts => opts.elem.querySelector('p'),
{elem: aHandle}
).catch(error_ => error = error_);
await page
.evaluateHandle((opts) => opts.elem.querySelector('p'), {
elem: aHandle,
})
.catch((error_) => (error = error_));
expect(error.message).toContain('Are you passing a nested JSHandle?');
});
it('should accept object handle to unserializable value', async() => {
const {page} = getTestState();
it('should accept object handle to unserializable value', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => Infinity);
expect(await page.evaluate(e => Object.is(e, Infinity), aHandle)).toBe(true);
expect(await page.evaluate((e) => Object.is(e, Infinity), aHandle)).toBe(
true
);
});
it('should use the same JS wrappers', async() => {
const {page} = getTestState();
it('should use the same JS wrappers', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => {
window.FOO = 123;
return window;
});
expect(await page.evaluate(e => e.FOO, aHandle)).toBe(123);
expect(await page.evaluate((e) => e.FOO, aHandle)).toBe(123);
});
it('should work with primitives', async() => {
const {page} = getTestState();
it('should work with primitives', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => {
window.FOO = 123;
return window;
});
expect(await page.evaluate(e => e.FOO, aHandle)).toBe(123);
expect(await page.evaluate((e) => e.FOO, aHandle)).toBe(123);
});
});
describe('JSHandle.getProperty', function() {
it('should work', async() => {
const {page} = getTestState();
describe('JSHandle.getProperty', function () {
it('should work', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => ({
one: 1,
two: 2,
three: 3
three: 3,
}));
const twoHandle = await aHandle.getProperty('two');
expect(await twoHandle.jsonValue()).toEqual(2);
});
});
describe('JSHandle.jsonValue', function() {
it('should work', async() => {
const {page} = getTestState();
describe('JSHandle.jsonValue', function () {
it('should work', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => ({foo: 'bar'}));
const aHandle = await page.evaluateHandle(() => ({ foo: 'bar' }));
const json = await aHandle.jsonValue();
expect(json).toEqual({foo: 'bar'});
expect(json).toEqual({ foo: 'bar' });
});
itFailsFirefox('should not work with dates', async() => {
const {page} = getTestState();
itFailsFirefox('should not work with dates', async () => {
const { page } = getTestState();
const dateHandle = await page.evaluateHandle(() => new Date('2017-09-26T00:00:00.000Z'));
const dateHandle = await page.evaluateHandle(
() => new Date('2017-09-26T00:00:00.000Z')
);
const json = await dateHandle.jsonValue();
expect(json).toEqual({});
});
it('should throw for circular objects', async() => {
const {page, isChrome} = getTestState();
it('should throw for circular objects', async () => {
const { page, isChrome } = getTestState();
const windowHandle = await page.evaluateHandle('window');
let error = null;
await windowHandle.jsonValue().catch(error_ => error = error_);
await windowHandle.jsonValue().catch((error_) => (error = error_));
if (isChrome)
expect(error.message).toContain('Object reference chain is too long');
else
expect(error.message).toContain('Object is not serializable');
else expect(error.message).toContain('Object is not serializable');
});
});
describe('JSHandle.getProperties', function() {
it('should work', async() => {
const {page} = getTestState();
describe('JSHandle.getProperties', function () {
it('should work', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => ({
foo: 'bar'
foo: 'bar',
}));
const properties = await aHandle.getProperties();
const foo = properties.get('foo');
expect(foo).toBeTruthy();
expect(await foo.jsonValue()).toBe('bar');
});
it('should return even non-own properties', async() => {
const {page} = getTestState();
it('should return even non-own properties', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => {
class A {
@ -156,77 +164,120 @@ describe('JSHandle', function() {
});
});
describe('JSHandle.asElement', function() {
it('should work', async() => {
const {page} = getTestState();
describe('JSHandle.asElement', function () {
it('should work', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => document.body);
const element = aHandle.asElement();
expect(element).toBeTruthy();
});
it('should return null for non-elements', async() => {
const {page} = getTestState();
it('should return null for non-elements', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => 2);
const element = aHandle.asElement();
expect(element).toBeFalsy();
});
itFailsFirefox('should return ElementHandle for TextNodes', async() => {
const {page} = getTestState();
itFailsFirefox('should return ElementHandle for TextNodes', async () => {
const { page } = getTestState();
await page.setContent('<div>ee!</div>');
const aHandle = await page.evaluateHandle(() => document.querySelector('div').firstChild);
const aHandle = await page.evaluateHandle(
() => document.querySelector('div').firstChild
);
const element = aHandle.asElement();
expect(element).toBeTruthy();
expect(await page.evaluate(e => e.nodeType === HTMLElement.TEXT_NODE, element));
expect(
await page.evaluate(
(e) => e.nodeType === HTMLElement.TEXT_NODE,
element
)
);
});
itFailsFirefox('should work with nullified Node', async() => {
const {page} = getTestState();
itFailsFirefox('should work with nullified Node', async () => {
const { page } = getTestState();
await page.setContent('<section>test</section>');
await page.evaluate(() => delete Node);
const handle = await page.evaluateHandle(() => document.querySelector('section'));
const handle = await page.evaluateHandle(() =>
document.querySelector('section')
);
const element = handle.asElement();
expect(element).not.toBe(null);
});
});
describe('JSHandle.toString', function() {
it('should work for primitives', async() => {
const {page} = getTestState();
describe('JSHandle.toString', function () {
it('should work for primitives', async () => {
const { page } = getTestState();
const numberHandle = await page.evaluateHandle(() => 2);
expect(numberHandle.toString()).toBe('JSHandle:2');
const stringHandle = await page.evaluateHandle(() => 'a');
expect(stringHandle.toString()).toBe('JSHandle:a');
});
it('should work for complicated objects', async() => {
const {page} = getTestState();
it('should work for complicated objects', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => window);
expect(aHandle.toString()).toBe('JSHandle@object');
});
it('should work with different subtypes', async() => {
const {page} = getTestState();
it('should work with different subtypes', async () => {
const { page } = getTestState();
expect((await page.evaluateHandle('(function(){})')).toString()).toBe('JSHandle@function');
expect((await page.evaluateHandle('(function(){})')).toString()).toBe(
'JSHandle@function'
);
expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle:12');
expect((await page.evaluateHandle('true')).toString()).toBe('JSHandle:true');
expect((await page.evaluateHandle('undefined')).toString()).toBe('JSHandle:undefined');
expect((await page.evaluateHandle('"foo"')).toString()).toBe('JSHandle:foo');
expect((await page.evaluateHandle('Symbol()')).toString()).toBe('JSHandle@symbol');
expect((await page.evaluateHandle('new Map()')).toString()).toBe('JSHandle@map');
expect((await page.evaluateHandle('new Set()')).toString()).toBe('JSHandle@set');
expect((await page.evaluateHandle('[]')).toString()).toBe('JSHandle@array');
expect((await page.evaluateHandle('null')).toString()).toBe('JSHandle:null');
expect((await page.evaluateHandle('/foo/')).toString()).toBe('JSHandle@regexp');
expect((await page.evaluateHandle('document.body')).toString()).toBe('JSHandle@node');
expect((await page.evaluateHandle('new Date()')).toString()).toBe('JSHandle@date');
expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe('JSHandle@weakmap');
expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe('JSHandle@weakset');
expect((await page.evaluateHandle('new Error()')).toString()).toBe('JSHandle@error');
expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe('JSHandle@typedarray');
expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe('JSHandle@proxy');
expect((await page.evaluateHandle('true')).toString()).toBe(
'JSHandle:true'
);
expect((await page.evaluateHandle('undefined')).toString()).toBe(
'JSHandle:undefined'
);
expect((await page.evaluateHandle('"foo"')).toString()).toBe(
'JSHandle:foo'
);
expect((await page.evaluateHandle('Symbol()')).toString()).toBe(
'JSHandle@symbol'
);
expect((await page.evaluateHandle('new Map()')).toString()).toBe(
'JSHandle@map'
);
expect((await page.evaluateHandle('new Set()')).toString()).toBe(
'JSHandle@set'
);
expect((await page.evaluateHandle('[]')).toString()).toBe(
'JSHandle@array'
);
expect((await page.evaluateHandle('null')).toString()).toBe(
'JSHandle:null'
);
expect((await page.evaluateHandle('/foo/')).toString()).toBe(
'JSHandle@regexp'
);
expect((await page.evaluateHandle('document.body')).toString()).toBe(
'JSHandle@node'
);
expect((await page.evaluateHandle('new Date()')).toString()).toBe(
'JSHandle@date'
);
expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe(
'JSHandle@weakmap'
);
expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe(
'JSHandle@weakset'
);
expect((await page.evaluateHandle('new Error()')).toString()).toBe(
'JSHandle@error'
);
expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe(
'JSHandle@typedarray'
);
expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe(
'JSHandle@proxy'
);
});
});
});

View File

@ -17,14 +17,18 @@
const utils = require('./utils');
const os = require('os');
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('Keyboard', function() {
describe('Keyboard', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should type into a textarea', async() => {
const {page} = getTestState();
it('should type into a textarea', async () => {
const { page } = getTestState();
await page.evaluate(() => {
const textarea = document.createElement('textarea');
@ -33,159 +37,242 @@ describe('Keyboard', function() {
});
const text = 'Hello world. I am the text that was typed!';
await page.keyboard.type(text);
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe(text);
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe(text);
});
itFailsFirefox('should press the metaKey', async() => {
const {page, isFirefox} = getTestState();
itFailsFirefox('should press the metaKey', async () => {
const { page, isFirefox } = getTestState();
await page.evaluate(() => {
window.keyPromise = new Promise(resolve => document.addEventListener('keydown', event => resolve(event.key)));
window.keyPromise = new Promise((resolve) =>
document.addEventListener('keydown', (event) => resolve(event.key))
);
});
await page.keyboard.press('Meta');
expect(await page.evaluate('keyPromise')).toBe(isFirefox && os.platform() !== 'darwin' ? 'OS' : 'Meta');
expect(await page.evaluate('keyPromise')).toBe(
isFirefox && os.platform() !== 'darwin' ? 'OS' : 'Meta'
);
});
it('should move with the arrow keys', async() => {
const {page, server} = getTestState();
it('should move with the arrow keys', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.type('textarea', 'Hello World!');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello World!');
for (let i = 0; i < 'World!'.length; i++)
page.keyboard.press('ArrowLeft');
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe('Hello World!');
for (let i = 0; i < 'World!'.length; i++) page.keyboard.press('ArrowLeft');
await page.keyboard.type('inserted ');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello inserted World!');
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe('Hello inserted World!');
page.keyboard.down('Shift');
for (let i = 0; i < 'inserted '.length; i++)
page.keyboard.press('ArrowLeft');
page.keyboard.up('Shift');
await page.keyboard.press('Backspace');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello World!');
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe('Hello World!');
});
it('should send a character with ElementHandle.press', async() => {
const {page, server} = getTestState();
it('should send a character with ElementHandle.press', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
const textarea = await page.$('textarea');
await textarea.press('a');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('a');
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe('a');
await page.evaluate(() => window.addEventListener('keydown', e => e.preventDefault(), true));
await page.evaluate(() =>
window.addEventListener('keydown', (e) => e.preventDefault(), true)
);
await textarea.press('b');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('a');
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe('a');
});
itFailsFirefox('ElementHandle.press should support |text| option', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'ElementHandle.press should support |text| option',
async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
const textarea = await page.$('textarea');
await textarea.press('a', {text: 'ё'});
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('ё');
});
itFailsFirefox('should send a character with sendCharacter', async() => {
const {page, server} = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
const textarea = await page.$('textarea');
await textarea.press('a', { text: 'ё' });
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe('ё');
}
);
itFailsFirefox('should send a character with sendCharacter', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
await page.keyboard.sendCharacter('嗨');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('嗨');
await page.evaluate(() => window.addEventListener('keydown', e => e.preventDefault(), true));
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe('嗨');
await page.evaluate(() =>
window.addEventListener('keydown', (e) => e.preventDefault(), true)
);
await page.keyboard.sendCharacter('a');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('嗨a');
expect(
await page.evaluate(() => document.querySelector('textarea').value)
).toBe('嗨a');
});
itFailsFirefox('should report shiftKey', async() => {
const {page, server} = getTestState();
itFailsFirefox('should report shiftKey', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/keyboard.html');
const keyboard = page.keyboard;
const codeForKey = {'Shift': 16, 'Alt': 18, 'Control': 17};
const codeForKey = { Shift: 16, Alt: 18, Control: 17 };
for (const modifierKey in codeForKey) {
await keyboard.down(modifierKey);
expect(await page.evaluate(() => getResult())).toBe('Keydown: ' + modifierKey + ' ' + modifierKey + 'Left ' + codeForKey[modifierKey] + ' [' + modifierKey + ']');
expect(await page.evaluate(() => getResult())).toBe(
'Keydown: ' +
modifierKey +
' ' +
modifierKey +
'Left ' +
codeForKey[modifierKey] +
' [' +
modifierKey +
']'
);
await keyboard.down('!');
// Shift+! will generate a keypress
if (modifierKey === 'Shift')
expect(await page.evaluate(() => getResult())).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']\nKeypress: ! Digit1 33 33 [' + modifierKey + ']');
expect(await page.evaluate(() => getResult())).toBe(
'Keydown: ! Digit1 49 [' +
modifierKey +
']\nKeypress: ! Digit1 33 33 [' +
modifierKey +
']'
);
else
expect(await page.evaluate(() => getResult())).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']');
expect(await page.evaluate(() => getResult())).toBe(
'Keydown: ! Digit1 49 [' + modifierKey + ']'
);
await keyboard.up('!');
expect(await page.evaluate(() => getResult())).toBe('Keyup: ! Digit1 49 [' + modifierKey + ']');
expect(await page.evaluate(() => getResult())).toBe(
'Keyup: ! Digit1 49 [' + modifierKey + ']'
);
await keyboard.up(modifierKey);
expect(await page.evaluate(() => getResult())).toBe('Keyup: ' + modifierKey + ' ' + modifierKey + 'Left ' + codeForKey[modifierKey] + ' []');
expect(await page.evaluate(() => getResult())).toBe(
'Keyup: ' +
modifierKey +
' ' +
modifierKey +
'Left ' +
codeForKey[modifierKey] +
' []'
);
}
});
it('should report multiple modifiers', async() => {
const {page, server} = getTestState();
it('should report multiple modifiers', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/keyboard.html');
const keyboard = page.keyboard;
await keyboard.down('Control');
expect(await page.evaluate(() => getResult())).toBe('Keydown: Control ControlLeft 17 [Control]');
expect(await page.evaluate(() => getResult())).toBe(
'Keydown: Control ControlLeft 17 [Control]'
);
await keyboard.down('Alt');
expect(await page.evaluate(() => getResult())).toBe('Keydown: Alt AltLeft 18 [Alt Control]');
expect(await page.evaluate(() => getResult())).toBe(
'Keydown: Alt AltLeft 18 [Alt Control]'
);
await keyboard.down(';');
expect(await page.evaluate(() => getResult())).toBe('Keydown: ; Semicolon 186 [Alt Control]');
expect(await page.evaluate(() => getResult())).toBe(
'Keydown: ; Semicolon 186 [Alt Control]'
);
await keyboard.up(';');
expect(await page.evaluate(() => getResult())).toBe('Keyup: ; Semicolon 186 [Alt Control]');
expect(await page.evaluate(() => getResult())).toBe(
'Keyup: ; Semicolon 186 [Alt Control]'
);
await keyboard.up('Control');
expect(await page.evaluate(() => getResult())).toBe('Keyup: Control ControlLeft 17 [Alt]');
expect(await page.evaluate(() => getResult())).toBe(
'Keyup: Control ControlLeft 17 [Alt]'
);
await keyboard.up('Alt');
expect(await page.evaluate(() => getResult())).toBe('Keyup: Alt AltLeft 18 []');
expect(await page.evaluate(() => getResult())).toBe(
'Keyup: Alt AltLeft 18 []'
);
});
it('should send proper codes while typing', async() => {
const {page, server} = getTestState();
it('should send proper codes while typing', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/keyboard.html');
await page.keyboard.type('!');
expect(await page.evaluate(() => getResult())).toBe(
[ 'Keydown: ! Digit1 49 []',
'Keypress: ! Digit1 33 33 []',
'Keyup: ! Digit1 49 []'].join('\n'));
[
'Keydown: ! Digit1 49 []',
'Keypress: ! Digit1 33 33 []',
'Keyup: ! Digit1 49 []',
].join('\n')
);
await page.keyboard.type('^');
expect(await page.evaluate(() => getResult())).toBe(
[ 'Keydown: ^ Digit6 54 []',
'Keypress: ^ Digit6 94 94 []',
'Keyup: ^ Digit6 54 []'].join('\n'));
[
'Keydown: ^ Digit6 54 []',
'Keypress: ^ Digit6 94 94 []',
'Keyup: ^ Digit6 54 []',
].join('\n')
);
});
it('should send proper codes while typing with shift', async() => {
const {page, server} = getTestState();
it('should send proper codes while typing with shift', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/keyboard.html');
const keyboard = page.keyboard;
await keyboard.down('Shift');
await page.keyboard.type('~');
expect(await page.evaluate(() => getResult())).toBe(
[ 'Keydown: Shift ShiftLeft 16 [Shift]',
'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode
'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode
'Keyup: ~ Backquote 192 [Shift]'].join('\n'));
[
'Keydown: Shift ShiftLeft 16 [Shift]',
'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode
'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode
'Keyup: ~ Backquote 192 [Shift]',
].join('\n')
);
await keyboard.up('Shift');
});
it('should not type canceled events', async() => {
const {page, server} = getTestState();
it('should not type canceled events', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
await page.evaluate(() => {
window.addEventListener('keydown', event => {
event.stopPropagation();
event.stopImmediatePropagation();
if (event.key === 'l')
event.preventDefault();
if (event.key === 'o')
event.preventDefault();
}, false);
window.addEventListener(
'keydown',
(event) => {
event.stopPropagation();
event.stopImmediatePropagation();
if (event.key === 'l') event.preventDefault();
if (event.key === 'o') event.preventDefault();
},
false
);
});
await page.keyboard.type('Hello World!');
expect(await page.evaluate(() => textarea.value)).toBe('He Wrd!');
});
itFailsFirefox('should specify repeat property', async() => {
const {page, server} = getTestState();
itFailsFirefox('should specify repeat property', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
await page.evaluate(() => document.querySelector('textarea').addEventListener('keydown', e => window.lastEvent = e, true));
await page.evaluate(() =>
document
.querySelector('textarea')
.addEventListener('keydown', (e) => (window.lastEvent = e), true)
);
await page.keyboard.down('a');
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false);
await page.keyboard.press('a');
@ -200,8 +287,8 @@ describe('Keyboard', function() {
await page.keyboard.down('a');
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false);
});
itFailsFirefox('should type all kinds of characters', async() => {
const {page, server} = getTestState();
itFailsFirefox('should type all kinds of characters', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
@ -209,12 +296,16 @@ describe('Keyboard', function() {
await page.keyboard.type(text);
expect(await page.evaluate('result')).toBe(text);
});
itFailsFirefox('should specify location', async() => {
const {page, server} = getTestState();
itFailsFirefox('should specify location', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.evaluate(() => {
window.addEventListener('keydown', event => window.keyLocation = event.location, true);
window.addEventListener(
'keydown',
(event) => (window.keyLocation = event.location),
true
);
});
const textarea = await page.$('textarea');
@ -230,60 +321,63 @@ describe('Keyboard', function() {
await textarea.press('NumpadSubtract');
expect(await page.evaluate('keyLocation')).toBe(3);
});
it('should throw on unknown keys', async() => {
const {page} = getTestState();
it('should throw on unknown keys', async () => {
const { page } = getTestState();
let error = await page.keyboard.press('NotARealKey').catch(error_ => error_);
let error = await page.keyboard
.press('NotARealKey')
.catch((error_) => error_);
expect(error.message).toBe('Unknown key: "NotARealKey"');
error = await page.keyboard.press('ё').catch(error_ => error_);
error = await page.keyboard.press('ё').catch((error_) => error_);
expect(error && error.message).toBe('Unknown key: "ё"');
error = await page.keyboard.press('😊').catch(error_ => error_);
error = await page.keyboard.press('😊').catch((error_) => error_);
expect(error && error.message).toBe('Unknown key: "😊"');
});
itFailsFirefox('should type emoji', async() => {
const {page, server} = getTestState();
itFailsFirefox('should type emoji', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.type('textarea', '👹 Tokyo street Japan 🇯🇵');
expect(await page.$eval('textarea', textarea => textarea.value)).toBe('👹 Tokyo street Japan 🇯🇵');
expect(await page.$eval('textarea', (textarea) => textarea.value)).toBe(
'👹 Tokyo street Japan 🇯🇵'
);
});
itFailsFirefox('should type emoji into an iframe', async() => {
const {page, server} = getTestState();
itFailsFirefox('should type emoji into an iframe', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await utils.attachFrame(page, 'emoji-test', server.PREFIX + '/input/textarea.html');
await utils.attachFrame(
page,
'emoji-test',
server.PREFIX + '/input/textarea.html'
);
const frame = page.frames()[1];
const textarea = await frame.$('textarea');
await textarea.type('👹 Tokyo street Japan 🇯🇵');
expect(await frame.$eval('textarea', textarea => textarea.value)).toBe('👹 Tokyo street Japan 🇯🇵');
expect(await frame.$eval('textarea', (textarea) => textarea.value)).toBe(
'👹 Tokyo street Japan 🇯🇵'
);
});
itFailsFirefox('should press the meta key', async() => {
const {page, isFirefox} = getTestState();
itFailsFirefox('should press the meta key', async () => {
const { page, isFirefox } = getTestState();
await page.evaluate(() => {
window.result = null;
document.addEventListener('keydown', event => {
document.addEventListener('keydown', (event) => {
window.result = [event.key, event.code, event.metaKey];
});
});
await page.keyboard.press('Meta');
const [key, code, metaKey] = await page.evaluate('result');
if (isFirefox && os.platform() !== 'darwin')
expect(key).toBe('OS');
else
expect(key).toBe('Meta');
if (isFirefox && os.platform() !== 'darwin') expect(key).toBe('OS');
else expect(key).toBe('Meta');
if (isFirefox)
expect(code).toBe('OSLeft');
else
expect(code).toBe('MetaLeft');
if (isFirefox && os.platform() !== 'darwin')
expect(metaKey).toBe(false);
else
expect(metaKey).toBe(true);
if (isFirefox) expect(code).toBe('OSLeft');
else expect(code).toBe('MetaLeft');
if (isFirefox && os.platform() !== 'darwin') expect(metaKey).toBe(false);
else expect(metaKey).toBe(true);
});
});

View File

@ -16,7 +16,7 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const {helper} = require('../lib/helper');
const { helper } = require('../lib/helper');
const rmAsync = helper.promisify(require('rimraf'));
const mkdtempAsync = helper.promisify(fs.mkdtemp);
const readFileAsync = helper.promisify(fs.readFile);
@ -24,25 +24,28 @@ const statAsync = helper.promisify(fs.stat);
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const utils = require('./utils');
const expect = require('expect');
const {getTestState} = require('./mocha-utils');
const { getTestState } = require('./mocha-utils');
describe('Launcher specs', function() {
describe('Puppeteer', function() {
describe('BrowserFetcher', function() {
it('should download and extract chrome linux binary', async() => {
const {server, puppeteer} = getTestState();
describe('Launcher specs', function () {
describe('Puppeteer', function () {
describe('BrowserFetcher', function () {
it('should download and extract chrome linux binary', async () => {
const { server, puppeteer } = getTestState();
const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
const browserFetcher = puppeteer.createBrowserFetcher({
platform: 'linux',
path: downloadsFolder,
host: server.PREFIX
host: server.PREFIX,
});
const expectedRevision = '123456';
let revisionInfo = browserFetcher.revisionInfo(expectedRevision);
server.setRoute(revisionInfo.url.substring(server.PREFIX.length), (req, res) => {
server.serveFile(req, res, '/chromium-linux.zip');
});
server.setRoute(
revisionInfo.url.substring(server.PREFIX.length),
(req, res) => {
server.serveFile(req, res, '/chromium-linux.zip');
}
);
expect(revisionInfo.local).toBe(false);
expect(browserFetcher.platform()).toBe('linux');
@ -53,16 +56,22 @@ describe('Launcher specs', function() {
revisionInfo = await browserFetcher.download(expectedRevision);
expect(revisionInfo.local).toBe(true);
expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe('LINUX BINARY\n');
expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe(
'LINUX BINARY\n'
);
const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755;
expect((await statAsync(revisionInfo.executablePath)).mode & 0o777).toBe(expectedPermissions);
expect(await browserFetcher.localRevisions()).toEqual([expectedRevision]);
expect(
(await statAsync(revisionInfo.executablePath)).mode & 0o777
).toBe(expectedPermissions);
expect(await browserFetcher.localRevisions()).toEqual([
expectedRevision,
]);
await browserFetcher.remove(expectedRevision);
expect(await browserFetcher.localRevisions()).toEqual([]);
await rmAsync(downloadsFolder);
});
it('should download and extract firefox linux binary', async() => {
const {server , puppeteer} = getTestState();
it('should download and extract firefox linux binary', async () => {
const { server, puppeteer } = getTestState();
const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
const browserFetcher = puppeteer.createBrowserFetcher({
@ -73,9 +82,16 @@ describe('Launcher specs', function() {
});
const expectedVersion = '75';
let revisionInfo = browserFetcher.revisionInfo(expectedVersion);
server.setRoute(revisionInfo.url.substring(server.PREFIX.length), (req, res) => {
server.serveFile(req, res, `/firefox-${expectedVersion}.0a1.en-US.linux-x86_64.tar.bz2`);
});
server.setRoute(
revisionInfo.url.substring(server.PREFIX.length),
(req, res) => {
server.serveFile(
req,
res,
`/firefox-${expectedVersion}.0a1.en-US.linux-x86_64.tar.bz2`
);
}
);
expect(revisionInfo.local).toBe(false);
expect(browserFetcher.platform()).toBe('linux');
@ -85,55 +101,73 @@ describe('Launcher specs', function() {
revisionInfo = await browserFetcher.download(expectedVersion);
expect(revisionInfo.local).toBe(true);
expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe('FIREFOX LINUX BINARY\n');
expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe(
'FIREFOX LINUX BINARY\n'
);
const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755;
expect((await statAsync(revisionInfo.executablePath)).mode & 0o777).toBe(expectedPermissions);
expect(await browserFetcher.localRevisions()).toEqual([expectedVersion]);
expect(
(await statAsync(revisionInfo.executablePath)).mode & 0o777
).toBe(expectedPermissions);
expect(await browserFetcher.localRevisions()).toEqual([
expectedVersion,
]);
await browserFetcher.remove(expectedVersion);
expect(await browserFetcher.localRevisions()).toEqual([]);
await rmAsync(downloadsFolder);
});
});
describe('Browser.disconnect', function() {
it('should reject navigation when browser closes', async() => {
const {server, puppeteer, defaultBrowserOptions} = getTestState();
describe('Browser.disconnect', function () {
it('should reject navigation when browser closes', async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
server.setRoute('/one-style.css', () => {});
const browser = await puppeteer.launch(defaultBrowserOptions);
const remote = await puppeteer.connect({browserWSEndpoint: browser.wsEndpoint()});
const remote = await puppeteer.connect({
browserWSEndpoint: browser.wsEndpoint(),
});
const page = await remote.newPage();
const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(error_ => error_);
const navigationPromise = page
.goto(server.PREFIX + '/one-style.html', { timeout: 60000 })
.catch((error_) => error_);
await server.waitForRequest('/one-style.css');
remote.disconnect();
const error = await navigationPromise;
expect(error.message).toBe('Navigation failed because browser has disconnected!');
expect(error.message).toBe(
'Navigation failed because browser has disconnected!'
);
await browser.close();
});
it('should reject waitForSelector when browser closes', async() => {
const {server, puppeteer, defaultBrowserOptions} = getTestState();
it('should reject waitForSelector when browser closes', async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
server.setRoute('/empty.html', () => {});
const browser = await puppeteer.launch(defaultBrowserOptions);
const remote = await puppeteer.connect({browserWSEndpoint: browser.wsEndpoint()});
const remote = await puppeteer.connect({
browserWSEndpoint: browser.wsEndpoint(),
});
const page = await remote.newPage();
const watchdog = page.waitForSelector('div', {timeout: 60000}).catch(error_ => error_);
const watchdog = page
.waitForSelector('div', { timeout: 60000 })
.catch((error_) => error_);
remote.disconnect();
const error = await watchdog;
expect(error.message).toContain('Protocol error');
await browser.close();
});
});
describe('Browser.close', function() {
it('should terminate network waiters', async() => {
const {server, puppeteer, defaultBrowserOptions} = getTestState();
describe('Browser.close', function () {
it('should terminate network waiters', async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions);
const remote = await puppeteer.connect({browserWSEndpoint: browser.wsEndpoint()});
const remote = await puppeteer.connect({
browserWSEndpoint: browser.wsEndpoint(),
});
const newPage = await remote.newPage();
const results = await Promise.all([
newPage.waitForRequest(server.EMPTY_PAGE).catch(error => error),
newPage.waitForResponse(server.EMPTY_PAGE).catch(error => error),
browser.close()
newPage.waitForRequest(server.EMPTY_PAGE).catch((error) => error),
newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => error),
browser.close(),
]);
for (let i = 0; i < 2; i++) {
const message = results[i].message;
@ -143,30 +177,34 @@ describe('Launcher specs', function() {
await browser.close();
});
});
describe('Puppeteer.launch', function() {
it('should reject all promises when browser is closed', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
describe('Puppeteer.launch', function () {
it('should reject all promises when browser is closed', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions);
const page = await browser.newPage();
let error = null;
const neverResolves = page.evaluate(() => new Promise(r => {})).catch(error_ => error = error_);
const neverResolves = page
.evaluate(() => new Promise((r) => {}))
.catch((error_) => (error = error_));
await browser.close();
await neverResolves;
expect(error.message).toContain('Protocol error');
});
it('should reject if executable path is invalid', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
it('should reject if executable path is invalid', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
let waitError = null;
const options = Object.assign({}, defaultBrowserOptions, {executablePath: 'random-invalid-path'});
await puppeteer.launch(options).catch(error => waitError = error);
const options = Object.assign({}, defaultBrowserOptions, {
executablePath: 'random-invalid-path',
});
await puppeteer.launch(options).catch((error) => (waitError = error));
expect(waitError.message).toContain('Failed to launch');
});
it('userDataDir option', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
it('userDataDir option', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER);
const options = Object.assign({userDataDir}, defaultBrowserOptions);
const options = Object.assign({ userDataDir }, defaultBrowserOptions);
const browser = await puppeteer.launch(options);
// Open a page to make sure its functional.
await browser.newPage();
@ -174,17 +212,17 @@ describe('Launcher specs', function() {
await browser.close();
expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(error => {});
await rmAsync(userDataDir).catch((error) => {});
});
it('userDataDir argument', async() => {
const {isChrome, puppeteer, defaultBrowserOptions} = getTestState();
it('userDataDir argument', async () => {
const { isChrome, puppeteer, defaultBrowserOptions } = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER);
const options = Object.assign({}, defaultBrowserOptions);
if (isChrome) {
options.args = [
...(defaultBrowserOptions.args || []),
`--user-data-dir=${userDataDir}`
`--user-data-dir=${userDataDir}`,
];
} else {
options.args = [
@ -198,17 +236,17 @@ describe('Launcher specs', function() {
await browser.close();
expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(error => {});
await rmAsync(userDataDir).catch((error) => {});
});
it('userDataDir option should restore state', async() => {
const {server, puppeteer, defaultBrowserOptions} = getTestState();
it('userDataDir option should restore state', async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER);
const options = Object.assign({userDataDir}, defaultBrowserOptions);
const options = Object.assign({ userDataDir }, defaultBrowserOptions);
const browser = await puppeteer.launch(options);
const page = await browser.newPage();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => localStorage.hey = 'hello');
await page.evaluate(() => (localStorage.hey = 'hello'));
await browser.close();
const browser2 = await puppeteer.launch(options);
@ -217,59 +255,79 @@ describe('Launcher specs', function() {
expect(await page2.evaluate(() => localStorage.hey)).toBe('hello');
await browser2.close();
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(error => {});
await rmAsync(userDataDir).catch((error) => {});
});
// This mysteriously fails on Windows on AppVeyor. See https://github.com/puppeteer/puppeteer/issues/4111
xit('userDataDir option should restore cookies', async() => {
const {server , puppeteer} = getTestState();
xit('userDataDir option should restore cookies', async () => {
const { server, puppeteer } = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER);
const options = Object.assign({userDataDir}, defaultBrowserOptions);
const options = Object.assign({ userDataDir }, defaultBrowserOptions);
const browser = await puppeteer.launch(options);
const page = await browser.newPage();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
await page.evaluate(
() =>
(document.cookie =
'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT')
);
await browser.close();
const browser2 = await puppeteer.launch(options);
const page2 = await browser2.newPage();
await page2.goto(server.EMPTY_PAGE);
expect(await page2.evaluate(() => document.cookie)).toBe('doSomethingOnlyOnce=true');
expect(await page2.evaluate(() => document.cookie)).toBe(
'doSomethingOnlyOnce=true'
);
await browser2.close();
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(error => {});
await rmAsync(userDataDir).catch((error) => {});
});
it('should return the default arguments', async() => {
const {isChrome, isFirefox, puppeteer} = getTestState();
it('should return the default arguments', async () => {
const { isChrome, isFirefox, puppeteer } = getTestState();
if (isChrome) {
expect(puppeteer.defaultArgs()).toContain('--no-first-run');
expect(puppeteer.defaultArgs()).toContain('--headless');
expect(puppeteer.defaultArgs({headless: false})).not.toContain('--headless');
expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('--user-data-dir=foo');
expect(puppeteer.defaultArgs({ headless: false })).not.toContain(
'--headless'
);
expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
'--user-data-dir=foo'
);
} else if (isFirefox) {
expect(puppeteer.defaultArgs()).toContain('--headless');
expect(puppeteer.defaultArgs()).toContain('--no-remote');
expect(puppeteer.defaultArgs()).toContain('--foreground');
expect(puppeteer.defaultArgs({headless: false})).not.toContain('--headless');
expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('--profile');
expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('foo');
expect(puppeteer.defaultArgs({ headless: false })).not.toContain(
'--headless'
);
expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
'--profile'
);
expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
'foo'
);
} else {
expect(puppeteer.defaultArgs()).toContain('-headless');
expect(puppeteer.defaultArgs({headless: false})).not.toContain('-headless');
expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('-profile');
expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('foo');
expect(puppeteer.defaultArgs({ headless: false })).not.toContain(
'-headless'
);
expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
'-profile'
);
expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
'foo'
);
}
});
it('should report the correct product', async() => {
const {isChrome, isFirefox, puppeteer} = getTestState();
if (isChrome)
expect(puppeteer.product).toBe('chrome');
else if (isFirefox)
expect(puppeteer.product).toBe('firefox');
it('should report the correct product', async () => {
const { isChrome, isFirefox, puppeteer } = getTestState();
if (isChrome) expect(puppeteer.product).toBe('chrome');
else if (isFirefox) expect(puppeteer.product).toBe('firefox');
});
itFailsFirefox('should work with no default arguments', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
itFailsFirefox('should work with no default arguments', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const options = Object.assign({}, defaultBrowserOptions);
options.ignoreDefaultArgs = true;
const browser = await puppeteer.launch(options);
@ -278,48 +336,55 @@ describe('Launcher specs', function() {
await page.close();
await browser.close();
});
itFailsFirefox('should filter out ignored default arguments', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
// Make sure we launch with `--enable-automation` by default.
const defaultArgs = puppeteer.defaultArgs();
const browser = await puppeteer.launch(Object.assign({}, defaultBrowserOptions, {
// Ignore first and third default argument.
ignoreDefaultArgs: [ defaultArgs[0], defaultArgs[2] ],
}));
const spawnargs = browser.process().spawnargs;
expect(spawnargs.indexOf(defaultArgs[0])).toBe(-1);
expect(spawnargs.indexOf(defaultArgs[1])).not.toBe(-1);
expect(spawnargs.indexOf(defaultArgs[2])).toBe(-1);
await browser.close();
});
it('should have default URL when launching browser', async function() {
const {defaultBrowserOptions, puppeteer} = getTestState();
itFailsFirefox(
'should filter out ignored default arguments',
async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
// Make sure we launch with `--enable-automation` by default.
const defaultArgs = puppeteer.defaultArgs();
const browser = await puppeteer.launch(
Object.assign({}, defaultBrowserOptions, {
// Ignore first and third default argument.
ignoreDefaultArgs: [defaultArgs[0], defaultArgs[2]],
})
);
const spawnargs = browser.process().spawnargs;
expect(spawnargs.indexOf(defaultArgs[0])).toBe(-1);
expect(spawnargs.indexOf(defaultArgs[1])).not.toBe(-1);
expect(spawnargs.indexOf(defaultArgs[2])).toBe(-1);
await browser.close();
}
);
it('should have default URL when launching browser', async function () {
const { defaultBrowserOptions, puppeteer } = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions);
const pages = (await browser.pages()).map(page => page.url());
const pages = (await browser.pages()).map((page) => page.url());
expect(pages).toEqual(['about:blank']);
await browser.close();
});
itFailsFirefox('should have custom URL when launching browser', async() => {
const {server, puppeteer, defaultBrowserOptions} = getTestState();
itFailsFirefox(
'should have custom URL when launching browser',
async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
const options = Object.assign({}, defaultBrowserOptions);
options.args = [server.EMPTY_PAGE].concat(options.args || []);
const browser = await puppeteer.launch(options);
const pages = await browser.pages();
expect(pages.length).toBe(1);
const page = pages[0];
if (page.url() !== server.EMPTY_PAGE)
await page.waitForNavigation();
expect(page.url()).toBe(server.EMPTY_PAGE);
await browser.close();
});
it('should set the default viewport', async() => {
const {puppeteer, defaultBrowserOptions} = getTestState();
const options = Object.assign({}, defaultBrowserOptions);
options.args = [server.EMPTY_PAGE].concat(options.args || []);
const browser = await puppeteer.launch(options);
const pages = await browser.pages();
expect(pages.length).toBe(1);
const page = pages[0];
if (page.url() !== server.EMPTY_PAGE) await page.waitForNavigation();
expect(page.url()).toBe(server.EMPTY_PAGE);
await browser.close();
}
);
it('should set the default viewport', async () => {
const { puppeteer, defaultBrowserOptions } = getTestState();
const options = Object.assign({}, defaultBrowserOptions, {
defaultViewport: {
width: 456,
height: 789
}
height: 789,
},
});
const browser = await puppeteer.launch(options);
const page = await browser.newPage();
@ -327,50 +392,53 @@ describe('Launcher specs', function() {
expect(await page.evaluate('window.innerHeight')).toBe(789);
await browser.close();
});
it('should disable the default viewport', async() => {
const {puppeteer, defaultBrowserOptions} = getTestState();
it('should disable the default viewport', async () => {
const { puppeteer, defaultBrowserOptions } = getTestState();
const options = Object.assign({}, defaultBrowserOptions, {
defaultViewport: null
defaultViewport: null,
});
const browser = await puppeteer.launch(options);
const page = await browser.newPage();
expect(page.viewport()).toBe(null);
await browser.close();
});
itFailsFirefox('should take fullPage screenshots when defaultViewport is null', async() => {
const {server, puppeteer, defaultBrowserOptions} = getTestState();
itFailsFirefox(
'should take fullPage screenshots when defaultViewport is null',
async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
const options = Object.assign({}, defaultBrowserOptions, {
defaultViewport: null
});
const browser = await puppeteer.launch(options);
const page = await browser.newPage();
await page.goto(server.PREFIX + '/grid.html');
const screenshot = await page.screenshot({
fullPage: true
});
expect(screenshot).toBeInstanceOf(Buffer);
await browser.close();
});
const options = Object.assign({}, defaultBrowserOptions, {
defaultViewport: null,
});
const browser = await puppeteer.launch(options);
const page = await browser.newPage();
await page.goto(server.PREFIX + '/grid.html');
const screenshot = await page.screenshot({
fullPage: true,
});
expect(screenshot).toBeInstanceOf(Buffer);
await browser.close();
}
);
});
describe('Puppeteer.launch', function() {
describe('Puppeteer.launch', function () {
let productName;
before(async() => {
const {puppeteer} = getTestState();
before(async () => {
const { puppeteer } = getTestState();
productName = puppeteer._productName;
});
after(async() => {
const {puppeteer} = getTestState();
after(async () => {
const { puppeteer } = getTestState();
puppeteer._lazyLauncher = undefined;
puppeteer._productName = productName;
});
it('should be able to launch Chrome', async() => {
const {puppeteer} = getTestState();
const browser = await puppeteer.launch({product: 'chrome'});
it('should be able to launch Chrome', async () => {
const { puppeteer } = getTestState();
const browser = await puppeteer.launch({ product: 'chrome' });
const userAgent = await browser.userAgent();
await browser.close();
expect(userAgent).toContain('Chrome');
@ -381,55 +449,69 @@ describe('Launcher specs', function() {
* this so we can get Windows CI stable and then dig into this
* properly with help from the Mozilla folks.
*/
itFailsWindowsUntilDate(new Date('2020-06-01'), 'should be able to launch Firefox', async() => {
const {puppeteer} = getTestState();
const browser = await puppeteer.launch({product: 'firefox'});
const userAgent = await browser.userAgent();
await browser.close();
expect(userAgent).toContain('Firefox');
});
itFailsWindowsUntilDate(
new Date('2020-06-01'),
'should be able to launch Firefox',
async () => {
const { puppeteer } = getTestState();
const browser = await puppeteer.launch({ product: 'firefox' });
const userAgent = await browser.userAgent();
await browser.close();
expect(userAgent).toContain('Firefox');
}
);
});
describe('Puppeteer.connect', function() {
it('should be able to connect multiple times to the same browser', async() => {
const {puppeteer, defaultBrowserOptions} = getTestState();
describe('Puppeteer.connect', function () {
it('should be able to connect multiple times to the same browser', async () => {
const { puppeteer, defaultBrowserOptions } = getTestState();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const otherBrowser = await puppeteer.connect({
browserWSEndpoint: originalBrowser.wsEndpoint()
browserWSEndpoint: originalBrowser.wsEndpoint(),
});
const page = await otherBrowser.newPage();
expect(await page.evaluate(() => 7 * 8)).toBe(56);
otherBrowser.disconnect();
const secondPage = await originalBrowser.newPage();
expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work');
expect(await secondPage.evaluate(() => 7 * 6)).toBe(
42,
'original browser should still work'
);
await originalBrowser.close();
});
it('should be able to close remote browser', async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
it('should be able to close remote browser', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const remoteBrowser = await puppeteer.connect({
browserWSEndpoint: originalBrowser.wsEndpoint()
browserWSEndpoint: originalBrowser.wsEndpoint(),
});
await Promise.all([
utils.waitEvent(originalBrowser, 'disconnected'),
remoteBrowser.close(),
]);
});
itFailsFirefox('should support ignoreHTTPSErrors option', async() => {
const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState();
itFailsFirefox('should support ignoreHTTPSErrors option', async () => {
const {
httpsServer,
puppeteer,
defaultBrowserOptions,
} = getTestState();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const browserWSEndpoint = originalBrowser.wsEndpoint();
const browser = await puppeteer.connect({browserWSEndpoint, ignoreHTTPSErrors: true});
const browser = await puppeteer.connect({
browserWSEndpoint,
ignoreHTTPSErrors: true,
});
const page = await browser.newPage();
let error = null;
const [serverRequest, response] = await Promise.all([
httpsServer.waitForRequest('/empty.html'),
page.goto(httpsServer.EMPTY_PAGE).catch(error_ => error = error_)
page.goto(httpsServer.EMPTY_PAGE).catch((error_) => (error = error_)),
]);
expect(error).toBe(null);
expect(response.ok()).toBe(true);
@ -439,47 +521,59 @@ describe('Launcher specs', function() {
await page.close();
await browser.close();
});
itFailsFirefox('should be able to reconnect to a disconnected browser', async() => {
const {server , puppeteer, defaultBrowserOptions} = getTestState();
itFailsFirefox(
'should be able to reconnect to a disconnected browser',
async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const browserWSEndpoint = originalBrowser.wsEndpoint();
const page = await originalBrowser.newPage();
await page.goto(server.PREFIX + '/frames/nested-frames.html');
originalBrowser.disconnect();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const browserWSEndpoint = originalBrowser.wsEndpoint();
const page = await originalBrowser.newPage();
await page.goto(server.PREFIX + '/frames/nested-frames.html');
originalBrowser.disconnect();
const browser = await puppeteer.connect({browserWSEndpoint});
const pages = await browser.pages();
const restoredPage = pages.find(page => page.url() === server.PREFIX + '/frames/nested-frames.html');
expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([
'http://localhost:<PORT>/frames/nested-frames.html',
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
' http://localhost:<PORT>/frames/frame.html (uno)',
' http://localhost:<PORT>/frames/frame.html (dos)',
' http://localhost:<PORT>/frames/frame.html (aframe)',
]);
expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56);
await browser.close();
});
const browser = await puppeteer.connect({ browserWSEndpoint });
const pages = await browser.pages();
const restoredPage = pages.find(
(page) =>
page.url() === server.PREFIX + '/frames/nested-frames.html'
);
expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([
'http://localhost:<PORT>/frames/nested-frames.html',
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
' http://localhost:<PORT>/frames/frame.html (uno)',
' http://localhost:<PORT>/frames/frame.html (dos)',
' http://localhost:<PORT>/frames/frame.html (aframe)',
]);
expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56);
await browser.close();
}
);
// @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410
itFailsFirefox('should be able to connect to the same page simultaneously', async() => {
const {puppeteer} = getTestState();
const browserOne = await puppeteer.launch();
const browserTwo = await puppeteer.connect({browserWSEndpoint: browserOne.wsEndpoint()});
const [page1, page2] = await Promise.all([
new Promise(x => browserOne.once('targetcreated', target => x(target.page()))),
browserTwo.newPage(),
]);
expect(await page1.evaluate(() => 7 * 8)).toBe(56);
expect(await page2.evaluate(() => 7 * 6)).toBe(42);
await browserOne.close();
});
itFailsFirefox(
'should be able to connect to the same page simultaneously',
async () => {
const { puppeteer } = getTestState();
const browserOne = await puppeteer.launch();
const browserTwo = await puppeteer.connect({
browserWSEndpoint: browserOne.wsEndpoint(),
});
const [page1, page2] = await Promise.all([
new Promise((x) =>
browserOne.once('targetcreated', (target) => x(target.page()))
),
browserTwo.newPage(),
]);
expect(await page1.evaluate(() => 7 * 8)).toBe(56);
expect(await page2.evaluate(() => 7 * 6)).toBe(42);
await browserOne.close();
}
);
});
describe('Puppeteer.executablePath', function() {
itFailsFirefox('should work', async() => {
const {puppeteer} = getTestState();
describe('Puppeteer.executablePath', function () {
itFailsFirefox('should work', async () => {
const { puppeteer } = getTestState();
const executablePath = puppeteer.executablePath();
expect(fs.existsSync(executablePath)).toBe(true);
@ -488,22 +582,25 @@ describe('Launcher specs', function() {
});
});
describe('Top-level requires', function() {
it('should require top-level Errors', async() => {
const {puppeteer, puppeteerPath} = getTestState();
describe('Top-level requires', function () {
it('should require top-level Errors', async () => {
const { puppeteer, puppeteerPath } = getTestState();
const Errors = require(path.join(puppeteerPath, '/Errors'));
expect(Errors.TimeoutError).toBe(puppeteer.errors.TimeoutError);
});
it('should require top-level DeviceDescriptors', async() => {
const {puppeteer, puppeteerPath} = getTestState();
const {devicesMap} = require(path.join(puppeteerPath, '/DeviceDescriptors'));
it('should require top-level DeviceDescriptors', async () => {
const { puppeteer, puppeteerPath } = getTestState();
const { devicesMap } = require(path.join(
puppeteerPath,
'/DeviceDescriptors'
));
expect(devicesMap['iPhone 6']).toBe(puppeteer.devices['iPhone 6']);
});
});
describe('Browser target events', function() {
itFailsFirefox('should work', async() => {
const {server , puppeteer, defaultBrowserOptions} = getTestState();
describe('Browser target events', function () {
itFailsFirefox('should work', async () => {
const { server, puppeteer, defaultBrowserOptions } = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions);
const events = [];
@ -518,13 +615,13 @@ describe('Launcher specs', function() {
});
});
describe('Browser.Events.disconnected', function() {
it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => {
const {puppeteer, defaultBrowserOptions} = getTestState();
describe('Browser.Events.disconnected', function () {
it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => {
const { puppeteer, defaultBrowserOptions } = getTestState();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const browserWSEndpoint = originalBrowser.wsEndpoint();
const remoteBrowser1 = await puppeteer.connect({browserWSEndpoint});
const remoteBrowser2 = await puppeteer.connect({browserWSEndpoint});
const remoteBrowser1 = await puppeteer.connect({ browserWSEndpoint });
const remoteBrowser2 = await puppeteer.connect({ browserWSEndpoint });
let disconnectedOriginal = 0;
let disconnectedRemote1 = 0;

View File

@ -14,15 +14,15 @@
* limitations under the License.
*/
const {TestServer} = require('../utils/testserver/index');
const { TestServer } = require('../utils/testserver/index');
const path = require('path');
const fs = require('fs');
const os = require('os');
const puppeteer = require('../');
const utils = require('./utils');
const {trackCoverage} = require('./coverage-utils');
const { trackCoverage } = require('./coverage-utils');
const setupServer = async() => {
const setupServer = async () => {
const assetsPath = path.join(__dirname, 'assets');
const cachedPath = path.join(__dirname, 'assets', 'cached');
@ -42,14 +42,16 @@ const setupServer = async() => {
httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`;
httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`;
return {server, httpsServer};
return { server, httpsServer };
};
exports.getTestState = () => state;
const product = process.env.PRODUCT || process.env.PUPPETEER_PRODUCT || 'Chromium';
const product =
process.env.PRODUCT || process.env.PUPPETEER_PRODUCT || 'Chromium';
const isHeadless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true';
const isHeadless =
(process.env.HEADLESS || 'true').trim().toLowerCase() === 'true';
const isFirefox = product === 'firefox';
const isChrome = product === 'Chromium';
const defaultBrowserOptions = {
@ -60,25 +62,26 @@ const defaultBrowserOptions = {
dumpio: !!process.env.DUMPIO,
};
(async() => {
(async () => {
if (defaultBrowserOptions.executablePath) {
console.warn(`WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}`);
console.warn(
`WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}`
);
} else {
if (product === 'firefox')
await puppeteer._launcher._updateRevision();
if (product === 'firefox') await puppeteer._launcher._updateRevision();
const executablePath = puppeteer.executablePath();
if (!fs.existsSync(executablePath))
throw new Error(`Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests`);
throw new Error(
`Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests`
);
}
})();
const setupGoldenAssertions = () => {
const suffix = product.toLowerCase();
const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix);
const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix);
if (fs.existsSync(OUTPUT_DIR))
rm(OUTPUT_DIR);
if (fs.existsSync(OUTPUT_DIR)) rm(OUTPUT_DIR);
utils.extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR);
};
@ -87,10 +90,8 @@ setupGoldenAssertions();
const state = {};
global.itFailsFirefox = (...args) => {
if (isFirefox)
return xit(...args);
else
return it(...args);
if (isFirefox) return xit(...args);
else return it(...args);
};
global.itFailsWindowsUntilDate = (date, ...args) => {
@ -103,53 +104,49 @@ global.itFailsWindowsUntilDate = (date, ...args) => {
};
global.describeFailsFirefox = (...args) => {
if (isFirefox)
return xdescribe(...args);
else
return describe(...args);
if (isFirefox) return xdescribe(...args);
else return describe(...args);
};
global.describeChromeOnly = (...args) => {
if (isChrome)
return describe(...args);
if (isChrome) return describe(...args);
};
if (process.env.COVERAGE)
trackCoverage();
if (process.env.COVERAGE) trackCoverage();
console.log(
`Running unit tests with:
`Running unit tests with:
-> product: ${product}
-> binary: ${path.relative(process.cwd(), puppeteer.executablePath())}`);
-> binary: ${path.relative(process.cwd(), puppeteer.executablePath())}`
);
exports.setupTestBrowserHooks = () => {
before(async() => {
before(async () => {
const browser = await puppeteer.launch(defaultBrowserOptions);
state.browser = browser;
});
after(async() => {
after(async () => {
await state.browser.close();
state.browser = null;
});
};
exports.setupTestPageAndContextHooks = () => {
beforeEach(async() => {
beforeEach(async () => {
state.context = await state.browser.createIncognitoBrowserContext();
state.page = await state.context.newPage();
});
afterEach(async() => {
afterEach(async () => {
await state.context.close();
state.context = null;
state.page = null;
});
};
before(async() => {
const {server, httpsServer} = await setupServer();
before(async () => {
const { server, httpsServer } = await setupServer();
state.puppeteer = puppeteer;
state.defaultBrowserOptions = defaultBrowserOptions;
@ -161,12 +158,12 @@ before(async() => {
state.puppeteerPath = path.resolve(path.join(__dirname, '..'));
});
beforeEach(async() => {
beforeEach(async () => {
state.server.reset();
state.httpsServer.reset();
});
after(async() => {
after(async () => {
await state.server.stop();
state.server = null;
await state.httpsServer.stop();

View File

@ -15,7 +15,11 @@
*/
const os = require('os');
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
function dimensions() {
const rect = document.querySelector('textarea').getBoundingClientRect();
@ -23,26 +27,26 @@ function dimensions() {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height
height: rect.height,
};
}
describe('Mouse', function() {
describe('Mouse', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should click the document', async() => {
const {page} = getTestState();
it('should click the document', async () => {
const { page } = getTestState();
await page.evaluate(() => {
window.clickPromise = new Promise(resolve => {
document.addEventListener('click', event => {
window.clickPromise = new Promise((resolve) => {
document.addEventListener('click', (event) => {
resolve({
type: event.type,
detail: event.detail,
clientX: event.clientX,
clientY: event.clientY,
isTrusted: event.isTrusted,
button: event.button
button: event.button,
});
});
});
@ -56,11 +60,11 @@ describe('Mouse', function() {
expect(event.isTrusted).toBe(true);
expect(event.button).toBe(0);
});
itFailsFirefox('should resize the textarea', async() => {
const {page, server} = getTestState();
itFailsFirefox('should resize the textarea', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
const {x, y, width, height} = await page.evaluate(dimensions);
const { x, y, width, height } = await page.evaluate(dimensions);
const mouse = page.mouse;
await mouse.move(x + width - 4, y + height - 4);
await mouse.down();
@ -70,101 +74,138 @@ describe('Mouse', function() {
expect(newDimensions.width).toBe(Math.round(width + 104));
expect(newDimensions.height).toBe(Math.round(height + 104));
});
itFailsFirefox('should select the text with mouse', async() => {
const {page, server} = getTestState();
itFailsFirefox('should select the text with mouse', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea');
const text = 'This is the text that we are going to try to select. Let\'s see how it goes.';
const text =
"This is the text that we are going to try to select. Let's see how it goes.";
await page.keyboard.type(text);
// Firefox needs an extra frame here after typing or it will fail to set the scrollTop
await page.evaluate(() => new Promise(requestAnimationFrame));
await page.evaluate(() => document.querySelector('textarea').scrollTop = 0);
const {x, y} = await page.evaluate(dimensions);
await page.mouse.move(x + 2,y + 2);
await page.evaluate(
() => (document.querySelector('textarea').scrollTop = 0)
);
const { x, y } = await page.evaluate(dimensions);
await page.mouse.move(x + 2, y + 2);
await page.mouse.down();
await page.mouse.move(100,100);
await page.mouse.move(100, 100);
await page.mouse.up();
expect(await page.evaluate(() => {
const textarea = document.querySelector('textarea');
return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
})).toBe(text);
expect(
await page.evaluate(() => {
const textarea = document.querySelector('textarea');
return textarea.value.substring(
textarea.selectionStart,
textarea.selectionEnd
);
})
).toBe(text);
});
itFailsFirefox('should trigger hover state', async() => {
const {page, server} = getTestState();
itFailsFirefox('should trigger hover state', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/scrollable.html');
await page.hover('#button-6');
expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6');
expect(
await page.evaluate(() => document.querySelector('button:hover').id)
).toBe('button-6');
await page.hover('#button-2');
expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-2');
expect(
await page.evaluate(() => document.querySelector('button:hover').id)
).toBe('button-2');
await page.hover('#button-91');
expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-91');
expect(
await page.evaluate(() => document.querySelector('button:hover').id)
).toBe('button-91');
});
itFailsFirefox('should trigger hover state with removed window.Node', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should trigger hover state with removed window.Node',
async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/input/scrollable.html');
await page.evaluate(() => delete window.Node);
await page.hover('#button-6');
expect(
await page.evaluate(() => document.querySelector('button:hover').id)
).toBe('button-6');
}
);
it('should set modifier keys on click', async () => {
const { page, server, isFirefox } = getTestState();
await page.goto(server.PREFIX + '/input/scrollable.html');
await page.evaluate(() => delete window.Node);
await page.hover('#button-6');
expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6');
});
it('should set modifier keys on click', async() => {
const {page, server, isFirefox} = getTestState();
await page.goto(server.PREFIX + '/input/scrollable.html');
await page.evaluate(() => document.querySelector('#button-3').addEventListener('mousedown', e => window.lastEvent = e, true));
const modifiers = {'Shift': 'shiftKey', 'Control': 'ctrlKey', 'Alt': 'altKey', 'Meta': 'metaKey'};
await page.evaluate(() =>
document
.querySelector('#button-3')
.addEventListener('mousedown', (e) => (window.lastEvent = e), true)
);
const modifiers = {
Shift: 'shiftKey',
Control: 'ctrlKey',
Alt: 'altKey',
Meta: 'metaKey',
};
// In Firefox, the Meta modifier only exists on Mac
if (isFirefox && os.platform() !== 'darwin')
delete modifiers['Meta'];
if (isFirefox && os.platform() !== 'darwin') delete modifiers['Meta'];
for (const modifier in modifiers) {
await page.keyboard.down(modifier);
await page.click('#button-3');
if (!(await page.evaluate(mod => window.lastEvent[mod], modifiers[modifier])))
if (
!(await page.evaluate(
(mod) => window.lastEvent[mod],
modifiers[modifier]
))
)
throw new Error(modifiers[modifier] + ' should be true');
await page.keyboard.up(modifier);
}
await page.click('#button-3');
for (const modifier in modifiers) {
if ((await page.evaluate(mod => window.lastEvent[mod], modifiers[modifier])))
if (
await page.evaluate((mod) => window.lastEvent[mod], modifiers[modifier])
)
throw new Error(modifiers[modifier] + ' should be false');
}
});
itFailsFirefox('should tween mouse movement', async() => {
const {page} = getTestState();
itFailsFirefox('should tween mouse movement', async () => {
const { page } = getTestState();
await page.mouse.move(100, 100);
await page.evaluate(() => {
window.result = [];
document.addEventListener('mousemove', event => {
document.addEventListener('mousemove', (event) => {
window.result.push([event.clientX, event.clientY]);
});
});
await page.mouse.move(200, 300, {steps: 5});
await page.mouse.move(200, 300, { steps: 5 });
expect(await page.evaluate('result')).toEqual([
[120, 140],
[140, 180],
[160, 220],
[180, 260],
[200, 300]
[200, 300],
]);
});
// @see https://crbug.com/929806
itFailsFirefox('should work with mobile viewports and cross process navigations', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should work with mobile viewports and cross process navigations',
async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setViewport({width: 360, height: 640, isMobile: true});
await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html');
await page.evaluate(() => {
document.addEventListener('click', event => {
window.result = {x: event.clientX, y: event.clientY};
await page.goto(server.EMPTY_PAGE);
await page.setViewport({ width: 360, height: 640, isMobile: true });
await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html');
await page.evaluate(() => {
document.addEventListener('click', (event) => {
window.result = { x: event.clientX, y: event.clientY };
});
});
});
await page.mouse.click(30, 40);
await page.mouse.click(30, 40);
expect(await page.evaluate('result')).toEqual({x: 30, y: 40});
});
expect(await page.evaluate('result')).toEqual({ x: 30, y: 40 });
}
);
});

View File

@ -16,20 +16,24 @@
const utils = require('./utils');
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('navigation', function() {
describe('navigation', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describe('Page.goto', function() {
it('should work', async() => {
const {page, server} = getTestState();
describe('Page.goto', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
expect(page.url()).toBe(server.EMPTY_PAGE);
});
itFailsFirefox('should work with anchor navigation', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work with anchor navigation', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
expect(page.url()).toBe(server.EMPTY_PAGE);
@ -38,28 +42,31 @@ describe('navigation', function() {
await page.goto(server.EMPTY_PAGE + '#bar');
expect(page.url()).toBe(server.EMPTY_PAGE + '#bar');
});
it('should work with redirects', async() => {
const {page, server} = getTestState();
it('should work with redirects', async () => {
const { page, server } = getTestState();
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html');
await page.goto(server.PREFIX + '/redirect/1.html');
expect(page.url()).toBe(server.EMPTY_PAGE);
});
it('should navigate to about:blank', async() => {
const {page} = getTestState();
it('should navigate to about:blank', async () => {
const { page } = getTestState();
const response = await page.goto('about:blank');
expect(response).toBe(null);
});
itFailsFirefox('should return response when page changes its URL after load', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should return response when page changes its URL after load',
async () => {
const { page, server } = getTestState();
const response = await page.goto(server.PREFIX + '/historyapi.html');
expect(response.status()).toBe(200);
});
it('should work with subframes return 204', async() => {
const {page, server} = getTestState();
const response = await page.goto(server.PREFIX + '/historyapi.html');
expect(response.status()).toBe(200);
}
);
it('should work with subframes return 204', async () => {
const { page, server } = getTestState();
server.setRoute('/frames/frame.html', (req, res) => {
res.statusCode = 204;
@ -67,324 +74,406 @@ describe('navigation', function() {
});
await page.goto(server.PREFIX + '/frames/one-frame.html');
});
itFailsFirefox('should fail when server returns 204', async() => {
const {page, server, isChrome} = getTestState();
itFailsFirefox('should fail when server returns 204', async () => {
const { page, server, isChrome } = getTestState();
server.setRoute('/empty.html', (req, res) => {
res.statusCode = 204;
res.end();
});
let error = null;
await page.goto(server.EMPTY_PAGE).catch(error_ => error = error_);
await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_));
expect(error).not.toBe(null);
if (isChrome)
expect(error.message).toContain('net::ERR_ABORTED');
else
expect(error.message).toContain('NS_BINDING_ABORTED');
if (isChrome) expect(error.message).toContain('net::ERR_ABORTED');
else expect(error.message).toContain('NS_BINDING_ABORTED');
});
itFailsFirefox('should navigate to empty page with domcontentloaded', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should navigate to empty page with domcontentloaded',
async () => {
const { page, server } = getTestState();
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'});
expect(response.status()).toBe(200);
});
itFailsFirefox('should work when page calls history API in beforeunload', async() => {
const {page, server} = getTestState();
const response = await page.goto(server.EMPTY_PAGE, {
waitUntil: 'domcontentloaded',
});
expect(response.status()).toBe(200);
}
);
itFailsFirefox(
'should work when page calls history API in beforeunload',
async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
window.addEventListener('beforeunload', () => history.replaceState(null, 'initial', window.location.href), false);
});
const response = await page.goto(server.PREFIX + '/grid.html');
expect(response.status()).toBe(200);
});
itFailsFirefox('should navigate to empty page with networkidle0', async() => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
window.addEventListener(
'beforeunload',
() => history.replaceState(null, 'initial', window.location.href),
false
);
});
const response = await page.goto(server.PREFIX + '/grid.html');
expect(response.status()).toBe(200);
}
);
itFailsFirefox(
'should navigate to empty page with networkidle0',
async () => {
const { page, server } = getTestState();
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle0'});
expect(response.status()).toBe(200);
});
itFailsFirefox('should navigate to empty page with networkidle2', async() => {
const {page, server} = getTestState();
const response = await page.goto(server.EMPTY_PAGE, {
waitUntil: 'networkidle0',
});
expect(response.status()).toBe(200);
}
);
itFailsFirefox(
'should navigate to empty page with networkidle2',
async () => {
const { page, server } = getTestState();
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle2'});
expect(response.status()).toBe(200);
});
itFailsFirefox('should fail when navigating to bad url', async() => {
const {page, isChrome} = getTestState();
const response = await page.goto(server.EMPTY_PAGE, {
waitUntil: 'networkidle2',
});
expect(response.status()).toBe(200);
}
);
itFailsFirefox('should fail when navigating to bad url', async () => {
const { page, isChrome } = getTestState();
let error = null;
await page.goto('asdfasdf').catch(error_ => error = error_);
await page.goto('asdfasdf').catch((error_) => (error = error_));
if (isChrome)
expect(error.message).toContain('Cannot navigate to invalid URL');
else
expect(error.message).toContain('Invalid url');
else expect(error.message).toContain('Invalid url');
});
itFailsFirefox('should fail when navigating to bad SSL', async() => {
const {page, httpsServer, isChrome} = getTestState();
itFailsFirefox('should fail when navigating to bad SSL', async () => {
const { page, httpsServer, isChrome } = getTestState();
// Make sure that network events do not emit 'undefined'.
// @see https://crbug.com/750469
page.on('request', request => expect(request).toBeTruthy());
page.on('requestfinished', request => expect(request).toBeTruthy());
page.on('requestfailed', request => expect(request).toBeTruthy());
page.on('request', (request) => expect(request).toBeTruthy());
page.on('requestfinished', (request) => expect(request).toBeTruthy());
page.on('requestfailed', (request) => expect(request).toBeTruthy());
let error = null;
await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => error = error_);
await page
.goto(httpsServer.EMPTY_PAGE)
.catch((error_) => (error = error_));
if (isChrome)
expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID');
else
expect(error.message).toContain('SSL_ERROR_UNKNOWN');
else expect(error.message).toContain('SSL_ERROR_UNKNOWN');
});
itFailsFirefox('should fail when navigating to bad SSL after redirects', async() => {
const {page, server, httpsServer, isChrome} = getTestState();
itFailsFirefox(
'should fail when navigating to bad SSL after redirects',
async () => {
const { page, server, httpsServer, isChrome } = getTestState();
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html');
let error = null;
await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(error_ => error = error_);
if (isChrome)
expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID');
else
expect(error.message).toContain('SSL_ERROR_UNKNOWN');
});
it('should throw if networkidle is passed as an option', async() => {
const {page, server} = getTestState();
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html');
let error = null;
await page
.goto(httpsServer.PREFIX + '/redirect/1.html')
.catch((error_) => (error = error_));
if (isChrome)
expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID');
else expect(error.message).toContain('SSL_ERROR_UNKNOWN');
}
);
it('should throw if networkidle is passed as an option', async () => {
const { page, server } = getTestState();
let error = null;
await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle'}).catch(error_ => error = error_);
expect(error.message).toContain('"networkidle" option is no longer supported');
await page
.goto(server.EMPTY_PAGE, { waitUntil: 'networkidle' })
.catch((error_) => (error = error_));
expect(error.message).toContain(
'"networkidle" option is no longer supported'
);
});
itFailsFirefox('should fail when main resources failed to load', async() => {
const {page, isChrome} = getTestState();
itFailsFirefox(
'should fail when main resources failed to load',
async () => {
const { page, isChrome } = getTestState();
let error = null;
await page.goto('http://localhost:44123/non-existing-url').catch(error_ => error = error_);
if (isChrome)
expect(error.message).toContain('net::ERR_CONNECTION_REFUSED');
else
expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED');
});
it('should fail when exceeding maximum navigation timeout', async() => {
const {page, server, puppeteer} = getTestState();
let error = null;
await page
.goto('http://localhost:44123/non-existing-url')
.catch((error_) => (error = error_));
if (isChrome)
expect(error.message).toContain('net::ERR_CONNECTION_REFUSED');
else expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED');
}
);
it('should fail when exceeding maximum navigation timeout', async () => {
const { page, server, puppeteer } = getTestState();
// Hang for request to the empty.html
server.setRoute('/empty.html', (req, res) => { });
server.setRoute('/empty.html', (req, res) => {});
let error = null;
await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(error_ => error = error_);
await page
.goto(server.PREFIX + '/empty.html', { timeout: 1 })
.catch((error_) => (error = error_));
expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should fail when exceeding default maximum navigation timeout', async() => {
const {page, server, puppeteer} = getTestState();
it('should fail when exceeding default maximum navigation timeout', async () => {
const { page, server, puppeteer } = getTestState();
// Hang for request to the empty.html
server.setRoute('/empty.html', (req, res) => { });
server.setRoute('/empty.html', (req, res) => {});
let error = null;
page.setDefaultNavigationTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch(error_ => error = error_);
await page
.goto(server.PREFIX + '/empty.html')
.catch((error_) => (error = error_));
expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should fail when exceeding default maximum timeout', async() => {
const {page, server, puppeteer} = getTestState();
it('should fail when exceeding default maximum timeout', async () => {
const { page, server, puppeteer } = getTestState();
// Hang for request to the empty.html
server.setRoute('/empty.html', (req, res) => { });
server.setRoute('/empty.html', (req, res) => {});
let error = null;
page.setDefaultTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch(error_ => error = error_);
await page
.goto(server.PREFIX + '/empty.html')
.catch((error_) => (error = error_));
expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should prioritize default navigation timeout over default timeout', async() => {
const {page, server, puppeteer} = getTestState();
it('should prioritize default navigation timeout over default timeout', async () => {
const { page, server, puppeteer } = getTestState();
// Hang for request to the empty.html
server.setRoute('/empty.html', (req, res) => { });
server.setRoute('/empty.html', (req, res) => {});
let error = null;
page.setDefaultTimeout(0);
page.setDefaultNavigationTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch(error_ => error = error_);
await page
.goto(server.PREFIX + '/empty.html')
.catch((error_) => (error = error_));
expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should disable timeout when its set to 0', async() => {
const {page, server} = getTestState();
it('should disable timeout when its set to 0', async () => {
const { page, server } = getTestState();
let error = null;
let loaded = false;
page.once('load', () => loaded = true);
await page.goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']}).catch(error_ => error = error_);
page.once('load', () => (loaded = true));
await page
.goto(server.PREFIX + '/grid.html', { timeout: 0, waitUntil: ['load'] })
.catch((error_) => (error = error_));
expect(error).toBe(null);
expect(loaded).toBe(true);
});
itFailsFirefox('should work when navigating to valid url', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work when navigating to valid url', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.EMPTY_PAGE);
expect(response.ok()).toBe(true);
});
itFailsFirefox('should work when navigating to data url', async() => {
const {page} = getTestState();
itFailsFirefox('should work when navigating to data url', async () => {
const { page } = getTestState();
const response = await page.goto('data:text/html,hello');
expect(response.ok()).toBe(true);
});
itFailsFirefox('should work when navigating to 404', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work when navigating to 404', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.PREFIX + '/not-found');
expect(response.ok()).toBe(false);
expect(response.status()).toBe(404);
});
itFailsFirefox('should return last response in redirect chain', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should return last response in redirect chain',
async () => {
const { page, server } = getTestState();
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/redirect/3.html');
server.setRedirect('/redirect/3.html', server.EMPTY_PAGE);
const response = await page.goto(server.PREFIX + '/redirect/1.html');
expect(response.ok()).toBe(true);
expect(response.url()).toBe(server.EMPTY_PAGE);
});
itFailsFirefox('should wait for network idle to succeed navigation', async() => {
const {page, server} = getTestState();
let responses = [];
// Hold on to a bunch of requests without answering.
server.setRoute('/fetch-request-a.js', (req, res) => responses.push(res));
server.setRoute('/fetch-request-b.js', (req, res) => responses.push(res));
server.setRoute('/fetch-request-c.js', (req, res) => responses.push(res));
server.setRoute('/fetch-request-d.js', (req, res) => responses.push(res));
const initialFetchResourcesRequested = Promise.all([
server.waitForRequest('/fetch-request-a.js'),
server.waitForRequest('/fetch-request-b.js'),
server.waitForRequest('/fetch-request-c.js'),
]);
const secondFetchResourceRequested = server.waitForRequest('/fetch-request-d.js');
// Navigate to a page which loads immediately and then does a bunch of
// requests via javascript's fetch method.
const navigationPromise = page.goto(server.PREFIX + '/networkidle.html', {
waitUntil: 'networkidle0',
});
// Track when the navigation gets completed.
let navigationFinished = false;
navigationPromise.then(() => navigationFinished = true);
// Wait for the page's 'load' event.
await new Promise(fulfill => page.once('load', fulfill));
expect(navigationFinished).toBe(false);
// Wait for the initial three resources to be requested.
await initialFetchResourcesRequested;
// Expect navigation still to be not finished.
expect(navigationFinished).toBe(false);
// Respond to initial requests.
for (const response of responses) {
response.statusCode = 404;
response.end(`File not found`);
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/redirect/3.html');
server.setRedirect('/redirect/3.html', server.EMPTY_PAGE);
const response = await page.goto(server.PREFIX + '/redirect/1.html');
expect(response.ok()).toBe(true);
expect(response.url()).toBe(server.EMPTY_PAGE);
}
);
itFailsFirefox(
'should wait for network idle to succeed navigation',
async () => {
const { page, server } = getTestState();
// Reset responses array
responses = [];
let responses = [];
// Hold on to a bunch of requests without answering.
server.setRoute('/fetch-request-a.js', (req, res) =>
responses.push(res)
);
server.setRoute('/fetch-request-b.js', (req, res) =>
responses.push(res)
);
server.setRoute('/fetch-request-c.js', (req, res) =>
responses.push(res)
);
server.setRoute('/fetch-request-d.js', (req, res) =>
responses.push(res)
);
const initialFetchResourcesRequested = Promise.all([
server.waitForRequest('/fetch-request-a.js'),
server.waitForRequest('/fetch-request-b.js'),
server.waitForRequest('/fetch-request-c.js'),
]);
const secondFetchResourceRequested = server.waitForRequest(
'/fetch-request-d.js'
);
// Wait for the second round to be requested.
await secondFetchResourceRequested;
// Expect navigation still to be not finished.
expect(navigationFinished).toBe(false);
// Navigate to a page which loads immediately and then does a bunch of
// requests via javascript's fetch method.
const navigationPromise = page.goto(
server.PREFIX + '/networkidle.html',
{
waitUntil: 'networkidle0',
}
);
// Track when the navigation gets completed.
let navigationFinished = false;
navigationPromise.then(() => (navigationFinished = true));
// Respond to requests.
for (const response of responses) {
response.statusCode = 404;
response.end(`File not found`);
// Wait for the page's 'load' event.
await new Promise((fulfill) => page.once('load', fulfill));
expect(navigationFinished).toBe(false);
// Wait for the initial three resources to be requested.
await initialFetchResourcesRequested;
// Expect navigation still to be not finished.
expect(navigationFinished).toBe(false);
// Respond to initial requests.
for (const response of responses) {
response.statusCode = 404;
response.end(`File not found`);
}
// Reset responses array
responses = [];
// Wait for the second round to be requested.
await secondFetchResourceRequested;
// Expect navigation still to be not finished.
expect(navigationFinished).toBe(false);
// Respond to requests.
for (const response of responses) {
response.statusCode = 404;
response.end(`File not found`);
}
const response = await navigationPromise;
// Expect navigation to succeed.
expect(response.ok()).toBe(true);
}
const response = await navigationPromise;
// Expect navigation to succeed.
expect(response.ok()).toBe(true);
});
it('should not leak listeners during navigation', async() => {
const {page, server} = getTestState();
);
it('should not leak listeners during navigation', async () => {
const { page, server } = getTestState();
let warning = null;
const warningHandler = w => warning = w;
const warningHandler = (w) => (warning = w);
process.on('warning', warningHandler);
for (let i = 0; i < 20; ++i)
await page.goto(server.EMPTY_PAGE);
for (let i = 0; i < 20; ++i) await page.goto(server.EMPTY_PAGE);
process.removeListener('warning', warningHandler);
expect(warning).toBe(null);
});
itFailsFirefox('should not leak listeners during bad navigation', async() => {
const {page} = getTestState();
itFailsFirefox(
'should not leak listeners during bad navigation',
async () => {
const { page } = getTestState();
let warning = null;
const warningHandler = (w) => (warning = w);
process.on('warning', warningHandler);
for (let i = 0; i < 20; ++i)
await page.goto('asdf').catch((error) => {
/* swallow navigation error */
});
process.removeListener('warning', warningHandler);
expect(warning).toBe(null);
}
);
it('should not leak listeners during navigation of 11 pages', async () => {
const { context, server } = getTestState();
let warning = null;
const warningHandler = w => warning = w;
const warningHandler = (w) => (warning = w);
process.on('warning', warningHandler);
for (let i = 0; i < 20; ++i)
await page.goto('asdf').catch(error => {/* swallow navigation error */});
await Promise.all(
[...Array(20)].map(async () => {
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await page.close();
})
);
process.removeListener('warning', warningHandler);
expect(warning).toBe(null);
});
it('should not leak listeners during navigation of 11 pages', async() => {
const {context, server} = getTestState();
itFailsFirefox(
'should navigate to dataURL and fire dataURL requests',
async () => {
const { page } = getTestState();
let warning = null;
const warningHandler = w => warning = w;
process.on('warning', warningHandler);
await Promise.all([...Array(20)].map(async() => {
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await page.close();
}));
process.removeListener('warning', warningHandler);
expect(warning).toBe(null);
});
itFailsFirefox('should navigate to dataURL and fire dataURL requests', async() => {
const {page} = getTestState();
const requests = [];
page.on(
'request',
(request) => !utils.isFavicon(request) && requests.push(request)
);
const dataURL = 'data:text/html,<div>yo</div>';
const response = await page.goto(dataURL);
expect(response.status()).toBe(200);
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(dataURL);
}
);
itFailsFirefox(
'should navigate to URL with hash and fire requests without hash',
async () => {
const { page, server } = getTestState();
const requests = [];
page.on('request', request => !utils.isFavicon(request) && requests.push(request));
const dataURL = 'data:text/html,<div>yo</div>';
const response = await page.goto(dataURL);
expect(response.status()).toBe(200);
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(dataURL);
});
itFailsFirefox('should navigate to URL with hash and fire requests without hash', async() => {
const {page, server} = getTestState();
const requests = [];
page.on('request', request => !utils.isFavicon(request) && requests.push(request));
const response = await page.goto(server.EMPTY_PAGE + '#hash');
expect(response.status()).toBe(200);
expect(response.url()).toBe(server.EMPTY_PAGE);
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(server.EMPTY_PAGE);
});
itFailsFirefox('should work with self requesting page', async() => {
const {page, server} = getTestState();
const requests = [];
page.on(
'request',
(request) => !utils.isFavicon(request) && requests.push(request)
);
const response = await page.goto(server.EMPTY_PAGE + '#hash');
expect(response.status()).toBe(200);
expect(response.url()).toBe(server.EMPTY_PAGE);
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(server.EMPTY_PAGE);
}
);
itFailsFirefox('should work with self requesting page', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.PREFIX + '/self-request.html');
expect(response.status()).toBe(200);
expect(response.url()).toContain('self-request.html');
});
itFailsFirefox('should fail when navigating and show the url at the error message', async() => {
const {page, httpsServer} = getTestState();
itFailsFirefox(
'should fail when navigating and show the url at the error message',
async () => {
const { page, httpsServer } = getTestState();
const url = httpsServer.PREFIX + '/redirect/1.html';
let error = null;
try {
await page.goto(url);
} catch (error_) {
error = error_;
const url = httpsServer.PREFIX + '/redirect/1.html';
let error = null;
try {
await page.goto(url);
} catch (error_) {
error = error_;
}
expect(error.message).toContain(url);
}
expect(error.message).toContain(url);
});
itFailsFirefox('should send referer', async() => {
const {page, server} = getTestState();
);
itFailsFirefox('should send referer', async () => {
const { page, server } = getTestState();
const [request1, request2] = await Promise.all([
server.waitForRequest('/grid.html'),
@ -399,32 +488,37 @@ describe('navigation', function() {
});
});
describe('Page.waitForNavigation', function() {
itFailsFirefox('should work', async() => {
const {page, server} = getTestState();
describe('Page.waitForNavigation', function () {
itFailsFirefox('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([
page.waitForNavigation(),
page.evaluate(url => window.location.href = url, server.PREFIX + '/grid.html')
page.evaluate(
(url) => (window.location.href = url),
server.PREFIX + '/grid.html'
),
]);
expect(response.ok()).toBe(true);
expect(response.url()).toContain('grid.html');
});
it('should work with both domcontentloaded and load', async() => {
const {page, server} = getTestState();
it('should work with both domcontentloaded and load', async () => {
const { page, server } = getTestState();
let response = null;
server.setRoute('/one-style.css', (req, res) => response = res);
server.setRoute('/one-style.css', (req, res) => (response = res));
const navigationPromise = page.goto(server.PREFIX + '/one-style.html');
const domContentLoadedPromise = page.waitForNavigation({
waitUntil: 'domcontentloaded'
waitUntil: 'domcontentloaded',
});
let bothFired = false;
const bothFiredPromise = page.waitForNavigation({
waitUntil: ['load', 'domcontentloaded']
}).then(() => bothFired = true);
const bothFiredPromise = page
.waitForNavigation({
waitUntil: ['load', 'domcontentloaded'],
})
.then(() => (bothFired = true));
await server.waitForRequest('/one-style.css');
await domContentLoadedPromise;
@ -433,8 +527,8 @@ describe('navigation', function() {
await bothFiredPromise;
await navigationPromise;
});
itFailsFirefox('should work with clicking on anchor links', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work with clicking on anchor links', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<a href='#foobar'>foobar</a>`);
@ -445,8 +539,8 @@ describe('navigation', function() {
expect(response).toBe(null);
expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar');
});
itFailsFirefox('should work with history.pushState()', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work with history.pushState()', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setContent(`
@ -462,8 +556,8 @@ describe('navigation', function() {
expect(response).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/wow.html');
});
itFailsFirefox('should work with history.replaceState()', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work with history.replaceState()', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setContent(`
@ -479,11 +573,13 @@ describe('navigation', function() {
expect(response).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/replaced.html');
});
itFailsFirefox('should work with DOM history.back()/history.forward()', async() => {
const {page, server} = getTestState();
itFailsFirefox(
'should work with DOM history.back()/history.forward()',
async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setContent(`
await page.goto(server.EMPTY_PAGE);
await page.setContent(`
<a id=back onclick='javascript:goBack()'>back</a>
<a id=forward onclick='javascript:goForward()'>forward</a>
<script>
@ -493,42 +589,47 @@ describe('navigation', function() {
history.pushState({}, '', '/second.html');
</script>
`);
expect(page.url()).toBe(server.PREFIX + '/second.html');
const [backResponse] = await Promise.all([
page.waitForNavigation(),
page.click('a#back'),
]);
expect(backResponse).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/first.html');
const [forwardResponse] = await Promise.all([
page.waitForNavigation(),
page.click('a#forward'),
]);
expect(forwardResponse).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/second.html');
});
itFailsFirefox('should work when subframe issues window.stop()', async() => {
const {page, server} = getTestState();
expect(page.url()).toBe(server.PREFIX + '/second.html');
const [backResponse] = await Promise.all([
page.waitForNavigation(),
page.click('a#back'),
]);
expect(backResponse).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/first.html');
const [forwardResponse] = await Promise.all([
page.waitForNavigation(),
page.click('a#forward'),
]);
expect(forwardResponse).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/second.html');
}
);
itFailsFirefox(
'should work when subframe issues window.stop()',
async () => {
const { page, server } = getTestState();
server.setRoute('/frames/style.css', (req, res) => {});
const navigationPromise = page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = await utils.waitEvent(page, 'frameattached');
await new Promise(fulfill => {
page.on('framenavigated', f => {
if (f === frame)
fulfill();
server.setRoute('/frames/style.css', (req, res) => {});
const navigationPromise = page.goto(
server.PREFIX + '/frames/one-frame.html'
);
const frame = await utils.waitEvent(page, 'frameattached');
await new Promise((fulfill) => {
page.on('framenavigated', (f) => {
if (f === frame) fulfill();
});
});
});
await Promise.all([
frame.evaluate(() => window.stop()),
navigationPromise
]);
});
await Promise.all([
frame.evaluate(() => window.stop()),
navigationPromise,
]);
}
);
});
describeFailsFirefox('Page.goBack', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Page.goBack', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.goto(server.PREFIX + '/grid.html');
@ -544,8 +645,8 @@ describe('navigation', function() {
response = await page.goForward();
expect(response).toBe(null);
});
it('should work with HistoryAPI', async() => {
const {page, server} = getTestState();
it('should work with HistoryAPI', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
@ -563,9 +664,9 @@ describe('navigation', function() {
});
});
describeFailsFirefox('Frame.goto', function() {
it('should navigate subframes', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Frame.goto', function () {
it('should navigate subframes', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/frames/one-frame.html');
expect(page.frames()[0].url()).toContain('/frames/one-frame.html');
@ -575,21 +676,24 @@ describe('navigation', function() {
expect(response.ok()).toBe(true);
expect(response.frame()).toBe(page.frames()[1]);
});
it('should reject when frame detaches', async() => {
const {page, server} = getTestState();
it('should reject when frame detaches', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/frames/one-frame.html');
server.setRoute('/empty.html', () => {});
const navigationPromise = page.frames()[1].goto(server.EMPTY_PAGE).catch(error_ => error_);
const navigationPromise = page
.frames()[1]
.goto(server.EMPTY_PAGE)
.catch((error_) => error_);
await server.waitForRequest('/empty.html');
await page.$eval('iframe', frame => frame.remove());
await page.$eval('iframe', (frame) => frame.remove());
const error = await navigationPromise;
expect(error.message).toBe('Navigating frame was detached');
});
it('should return matching responses', async() => {
const {page, server} = getTestState();
it('should return matching responses', async () => {
const { page, server } = getTestState();
// Disable cache: otherwise, chromium will cache similar requests.
await page.setCacheEnabled(false);
@ -602,7 +706,9 @@ describe('navigation', function() {
]);
// Navigate all frames to the same URL.
const serverResponses = [];
server.setRoute('/one-style.html', (req, res) => serverResponses.push(res));
server.setRoute('/one-style.html', (req, res) =>
serverResponses.push(res)
);
const navigations = [];
for (let i = 0; i < 3; ++i) {
navigations.push(frames[i].goto(server.PREFIX + '/one-style.html'));
@ -619,46 +725,51 @@ describe('navigation', function() {
});
});
describeFailsFirefox('Frame.waitForNavigation', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Frame.waitForNavigation', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = page.frames()[1];
const [response] = await Promise.all([
frame.waitForNavigation(),
frame.evaluate(url => window.location.href = url, server.PREFIX + '/grid.html')
frame.evaluate(
(url) => (window.location.href = url),
server.PREFIX + '/grid.html'
),
]);
expect(response.ok()).toBe(true);
expect(response.url()).toContain('grid.html');
expect(response.frame()).toBe(frame);
expect(page.url()).toContain('/frames/one-frame.html');
});
it('should fail when frame detaches', async() => {
const {page, server} = getTestState();
it('should fail when frame detaches', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = page.frames()[1];
server.setRoute('/empty.html', () => {});
let error = null;
const navigationPromise = frame.waitForNavigation().catch(error_ => error = error_);
const navigationPromise = frame
.waitForNavigation()
.catch((error_) => (error = error_));
await Promise.all([
server.waitForRequest('/empty.html'),
frame.evaluate(() => window.location = '/empty.html')
frame.evaluate(() => (window.location = '/empty.html')),
]);
await page.$eval('iframe', frame => frame.remove());
await page.$eval('iframe', (frame) => frame.remove());
await navigationPromise;
expect(error.message).toBe('Navigating frame was detached');
});
});
describe('Page.reload', function() {
it('should work', async() => {
const {page, server} = getTestState();
describe('Page.reload', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => window._foo = 10);
await page.evaluate(() => (window._foo = 10));
await page.reload();
expect(await page.evaluate(() => window._foo)).toBe(undefined);
});

View File

@ -18,77 +18,101 @@ const fs = require('fs');
const path = require('path');
const utils = require('./utils');
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('network', function() {
describe('network', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describe('Page.Events.Request', function() {
it('should fire for navigation requests', async() => {
const {page, server} = getTestState();
describe('Page.Events.Request', function () {
it('should fire for navigation requests', async () => {
const { page, server } = getTestState();
const requests = [];
page.on('request', request => !utils.isFavicon(request) && requests.push(request));
page.on(
'request',
(request) => !utils.isFavicon(request) && requests.push(request)
);
await page.goto(server.EMPTY_PAGE);
expect(requests.length).toBe(1);
});
itFailsFirefox('should fire for iframes', async() => {
const {page, server} = getTestState();
itFailsFirefox('should fire for iframes', async () => {
const { page, server } = getTestState();
const requests = [];
page.on('request', request => !utils.isFavicon(request) && requests.push(request));
page.on(
'request',
(request) => !utils.isFavicon(request) && requests.push(request)
);
await page.goto(server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
expect(requests.length).toBe(2);
});
it('should fire for fetches', async() => {
const {page, server} = getTestState();
it('should fire for fetches', async () => {
const { page, server } = getTestState();
const requests = [];
page.on('request', request => !utils.isFavicon(request) && requests.push(request));
page.on(
'request',
(request) => !utils.isFavicon(request) && requests.push(request)
);
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => fetch('/empty.html'));
expect(requests.length).toBe(2);
});
});
describe('Request.frame', function() {
it('should work for main frame navigation request', async() => {
const {page, server} = getTestState();
describe('Request.frame', function () {
it('should work for main frame navigation request', async () => {
const { page, server } = getTestState();
const requests = [];
page.on('request', request => !utils.isFavicon(request) && requests.push(request));
page.on(
'request',
(request) => !utils.isFavicon(request) && requests.push(request)
);
await page.goto(server.EMPTY_PAGE);
expect(requests.length).toBe(1);
expect(requests[0].frame()).toBe(page.mainFrame());
});
itFailsFirefox('should work for subframe navigation request', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work for subframe navigation request', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const requests = [];
page.on('request', request => !utils.isFavicon(request) && requests.push(request));
page.on(
'request',
(request) => !utils.isFavicon(request) && requests.push(request)
);
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
expect(requests.length).toBe(1);
expect(requests[0].frame()).toBe(page.frames()[1]);
});
it('should work for fetch requests', async() => {
const {page, server} = getTestState();
it('should work for fetch requests', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
let requests = [];
page.on('request', request => !utils.isFavicon(request) && requests.push(request));
page.on(
'request',
(request) => !utils.isFavicon(request) && requests.push(request)
);
await page.evaluate(() => fetch('/digits/1.png'));
requests = requests.filter(request => !request.url().includes('favicon'));
requests = requests.filter(
(request) => !request.url().includes('favicon')
);
expect(requests.length).toBe(1);
expect(requests[0].frame()).toBe(page.mainFrame());
});
});
describeFailsFirefox('Request.headers', function() {
it('should work', async() => {
const {page, server, isChrome} = getTestState();
describeFailsFirefox('Request.headers', function () {
it('should work', async () => {
const { page, server, isChrome } = getTestState();
const response = await page.goto(server.EMPTY_PAGE);
if (isChrome)
@ -98,9 +122,9 @@ describe('network', function() {
});
});
describeFailsFirefox('Response.headers', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Response.headers', function () {
it('should work', async () => {
const { page, server } = getTestState();
server.setRoute('/empty.html', (req, res) => {
res.setHeader('foo', 'bar');
@ -111,19 +135,24 @@ describe('network', function() {
});
});
describeFailsFirefox('Response.fromCache', function() {
it('should return |false| for non-cached content', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Response.fromCache', function () {
it('should return |false| for non-cached content', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.EMPTY_PAGE);
expect(response.fromCache()).toBe(false);
});
it('should work', async() => {
const {page, server} = getTestState();
it('should work', async () => {
const { page, server } = getTestState();
const responses = new Map();
page.on('response', r => !utils.isFavicon(r.request()) && responses.set(r.url().split('/').pop(), r));
page.on(
'response',
(r) =>
!utils.isFavicon(r.request()) &&
responses.set(r.url().split('/').pop(), r)
);
// Load and re-load to make sure it's cached.
await page.goto(server.PREFIX + '/cached/one-style.html');
@ -137,23 +166,25 @@ describe('network', function() {
});
});
describeFailsFirefox('Response.fromServiceWorker', function() {
it('should return |false| for non-service-worker content', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Response.fromServiceWorker', function () {
it('should return |false| for non-service-worker content', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.EMPTY_PAGE);
expect(response.fromServiceWorker()).toBe(false);
});
it('Response.fromServiceWorker', async() => {
const {page, server} = getTestState();
it('Response.fromServiceWorker', async () => {
const { page, server } = getTestState();
const responses = new Map();
page.on('response', r => responses.set(r.url().split('/').pop(), r));
page.on('response', (r) => responses.set(r.url().split('/').pop(), r));
// Load and re-load to make sure serviceworker is installed and running.
await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {waitUntil: 'networkidle2'});
await page.evaluate(async() => await window.activationPromise);
await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {
waitUntil: 'networkidle2',
});
await page.evaluate(async () => await window.activationPromise);
await page.reload();
expect(responses.size).toBe(2);
@ -164,36 +195,41 @@ describe('network', function() {
});
});
describeFailsFirefox('Request.postData', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Request.postData', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
server.setRoute('/post', (req, res) => res.end());
let request = null;
page.on('request', r => request = r);
await page.evaluate(() => fetch('./post', {method: 'POST', body: JSON.stringify({foo: 'bar'})}));
page.on('request', (r) => (request = r));
await page.evaluate(() =>
fetch('./post', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
})
);
expect(request).toBeTruthy();
expect(request.postData()).toBe('{"foo":"bar"}');
});
it('should be |undefined| when there is no post data', async() => {
const {page, server} = getTestState();
it('should be |undefined| when there is no post data', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.EMPTY_PAGE);
expect(response.request().postData()).toBe(undefined);
});
});
describeFailsFirefox('Response.text', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Response.text', function () {
it('should work', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.PREFIX + '/simple.json');
const responseText = (await response.text()).trimEnd();
expect(responseText).toBe('{"foo": "bar"}');
});
it('should return uncompressed text', async() => {
const {page, server} = getTestState();
it('should return uncompressed text', async () => {
const { page, server } = getTestState();
server.enableGzip('/simple.json');
const response = await page.goto(server.PREFIX + '/simple.json');
@ -201,8 +237,8 @@ describe('network', function() {
const responseText = (await response.text()).trimEnd();
expect(responseText).toBe('{"foo": "bar"}');
});
it('should throw when requesting body of redirected response', async() => {
const {page, server} = getTestState();
it('should throw when requesting body of redirected response', async () => {
const { page, server } = getTestState();
server.setRedirect('/foo.html', '/empty.html');
const response = await page.goto(server.PREFIX + '/foo.html');
@ -211,11 +247,13 @@ describe('network', function() {
const redirected = redirectChain[0].response();
expect(redirected.status()).toBe(302);
let error = null;
await redirected.text().catch(error_ => error = error_);
expect(error.message).toContain('Response body is unavailable for redirect responses');
await redirected.text().catch((error_) => (error = error_));
expect(error.message).toContain(
'Response body is unavailable for redirect responses'
);
});
it('should wait until response completes', async() => {
const {page, server} = getTestState();
it('should wait until response completes', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
// Setup server to trap request.
@ -229,11 +267,14 @@ describe('network', function() {
});
// Setup page to trap response.
let requestFinished = false;
page.on('requestfinished', r => requestFinished = requestFinished || r.url().includes('/get'));
page.on(
'requestfinished',
(r) => (requestFinished = requestFinished || r.url().includes('/get'))
);
// send request and wait for server response
const [pageResponse] = await Promise.all([
page.waitForResponse(r => !utils.isFavicon(r.request())),
page.evaluate(() => fetch('./get', {method: 'GET'})),
page.waitForResponse((r) => !utils.isFavicon(r.request())),
page.evaluate(() => fetch('./get', { method: 'GET' })),
server.waitForRequest('/get'),
]);
@ -244,45 +285,49 @@ describe('network', function() {
const responseText = pageResponse.text();
// Write part of the response and wait for it to be flushed.
await new Promise(x => serverResponse.write('wor', x));
await new Promise((x) => serverResponse.write('wor', x));
// Finish response.
await new Promise(x => serverResponse.end('ld!', x));
await new Promise((x) => serverResponse.end('ld!', x));
expect(await responseText).toBe('hello world!');
});
});
describeFailsFirefox('Response.json', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Response.json', function () {
it('should work', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.PREFIX + '/simple.json');
expect(await response.json()).toEqual({foo: 'bar'});
expect(await response.json()).toEqual({ foo: 'bar' });
});
});
describeFailsFirefox('Response.buffer', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Response.buffer', function () {
it('should work', async () => {
const { page, server } = getTestState();
const response = await page.goto(server.PREFIX + '/pptr.png');
const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png'));
const imageBuffer = fs.readFileSync(
path.join(__dirname, 'assets', 'pptr.png')
);
const responseBuffer = await response.buffer();
expect(responseBuffer.equals(imageBuffer)).toBe(true);
});
it('should work with compression', async() => {
const {page, server} = getTestState();
it('should work with compression', async () => {
const { page, server } = getTestState();
server.enableGzip('/pptr.png');
const response = await page.goto(server.PREFIX + '/pptr.png');
const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png'));
const imageBuffer = fs.readFileSync(
path.join(__dirname, 'assets', 'pptr.png')
);
const responseBuffer = await response.buffer();
expect(responseBuffer.equals(imageBuffer)).toBe(true);
});
});
describeFailsFirefox('Response.statusText', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Response.statusText', function () {
it('should work', async () => {
const { page, server } = getTestState();
server.setRoute('/cool', (req, res) => {
res.writeHead(200, 'cool!');
@ -293,12 +338,12 @@ describe('network', function() {
});
});
describeFailsFirefox('Network Events', function() {
it('Page.Events.Request', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Network Events', function () {
it('Page.Events.Request', async () => {
const { page, server } = getTestState();
const requests = [];
page.on('request', request => requests.push(request));
page.on('request', (request) => requests.push(request));
await page.goto(server.EMPTY_PAGE);
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(server.EMPTY_PAGE);
@ -308,11 +353,11 @@ describe('network', function() {
expect(requests[0].frame() === page.mainFrame()).toBe(true);
expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE);
});
it('Page.Events.Response', async() => {
const {page, server} = getTestState();
it('Page.Events.Response', async () => {
const { page, server } = getTestState();
const responses = [];
page.on('response', response => responses.push(response));
page.on('response', (response) => responses.push(response));
await page.goto(server.EMPTY_PAGE);
expect(responses.length).toBe(1);
expect(responses[0].url()).toBe(server.EMPTY_PAGE);
@ -321,22 +366,22 @@ describe('network', function() {
expect(responses[0].request()).toBeTruthy();
const remoteAddress = responses[0].remoteAddress();
// Either IPv6 or IPv4, depending on environment.
expect(remoteAddress.ip.includes('::1') || remoteAddress.ip === '127.0.0.1').toBe(true);
expect(
remoteAddress.ip.includes('::1') || remoteAddress.ip === '127.0.0.1'
).toBe(true);
expect(remoteAddress.port).toBe(server.PORT);
});
it('Page.Events.RequestFailed', async() => {
const {page, server, isChrome} = getTestState();
it('Page.Events.RequestFailed', async () => {
const { page, server, isChrome } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().endsWith('css'))
request.abort();
else
request.continue();
page.on('request', (request) => {
if (request.url().endsWith('css')) request.abort();
else request.continue();
});
const failedRequests = [];
page.on('requestfailed', request => failedRequests.push(request));
page.on('requestfailed', (request) => failedRequests.push(request));
await page.goto(server.PREFIX + '/one-style.html');
expect(failedRequests.length).toBe(1);
expect(failedRequests[0].url()).toContain('one-style.css');
@ -348,11 +393,11 @@ describe('network', function() {
expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE');
expect(failedRequests[0].frame()).toBeTruthy();
});
it('Page.Events.RequestFinished', async() => {
const {page, server} = getTestState();
it('Page.Events.RequestFinished', async () => {
const { page, server } = getTestState();
const requests = [];
page.on('requestfinished', request => requests.push(request));
page.on('requestfinished', (request) => requests.push(request));
await page.goto(server.EMPTY_PAGE);
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(server.EMPTY_PAGE);
@ -360,24 +405,32 @@ describe('network', function() {
expect(requests[0].frame() === page.mainFrame()).toBe(true);
expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE);
});
it('should fire events in proper order', async() => {
const {page, server} = getTestState();
it('should fire events in proper order', async () => {
const { page, server } = getTestState();
const events = [];
page.on('request', request => events.push('request'));
page.on('response', response => events.push('response'));
page.on('requestfinished', request => events.push('requestfinished'));
page.on('request', (request) => events.push('request'));
page.on('response', (response) => events.push('response'));
page.on('requestfinished', (request) => events.push('requestfinished'));
await page.goto(server.EMPTY_PAGE);
expect(events).toEqual(['request', 'response', 'requestfinished']);
});
it('should support redirects', async() => {
const {page, server} = getTestState();
it('should support redirects', async () => {
const { page, server } = getTestState();
const events = [];
page.on('request', request => events.push(`${request.method()} ${request.url()}`));
page.on('response', response => events.push(`${response.status()} ${response.url()}`));
page.on('requestfinished', request => events.push(`DONE ${request.url()}`));
page.on('requestfailed', request => events.push(`FAIL ${request.url()}`));
page.on('request', (request) =>
events.push(`${request.method()} ${request.url()}`)
);
page.on('response', (response) =>
events.push(`${response.status()} ${response.url()}`)
);
page.on('requestfinished', (request) =>
events.push(`DONE ${request.url()}`)
);
page.on('requestfailed', (request) =>
events.push(`FAIL ${request.url()}`)
);
server.setRedirect('/foo.html', '/empty.html');
const FOO_URL = server.PREFIX + '/foo.html';
const response = await page.goto(FOO_URL);
@ -387,23 +440,27 @@ describe('network', function() {
`DONE ${FOO_URL}`,
`GET ${server.EMPTY_PAGE}`,
`200 ${server.EMPTY_PAGE}`,
`DONE ${server.EMPTY_PAGE}`
`DONE ${server.EMPTY_PAGE}`,
]);
// Check redirect chain
const redirectChain = response.request().redirectChain();
expect(redirectChain.length).toBe(1);
expect(redirectChain[0].url()).toContain('/foo.html');
expect(redirectChain[0].response().remoteAddress().port).toBe(server.PORT);
expect(redirectChain[0].response().remoteAddress().port).toBe(
server.PORT
);
});
});
describe('Request.isNavigationRequest', () => {
itFailsFirefox('should work', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work', async () => {
const { page, server } = getTestState();
const requests = new Map();
page.on('request', request => requests.set(request.url().split('/').pop(), request));
page.on('request', (request) =>
requests.set(request.url().split('/').pop(), request)
);
server.setRedirect('/rrredirect', '/frames/one-frame.html');
await page.goto(server.PREFIX + '/rrredirect');
expect(requests.get('rrredirect').isNavigationRequest()).toBe(true);
@ -412,11 +469,11 @@ describe('network', function() {
expect(requests.get('script.js').isNavigationRequest()).toBe(false);
expect(requests.get('style.css').isNavigationRequest()).toBe(false);
});
itFailsFirefox('should work with request interception', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work with request interception', async () => {
const { page, server } = getTestState();
const requests = new Map();
page.on('request', request => {
page.on('request', (request) => {
requests.set(request.url().split('/').pop(), request);
request.continue();
});
@ -429,22 +486,22 @@ describe('network', function() {
expect(requests.get('script.js').isNavigationRequest()).toBe(false);
expect(requests.get('style.css').isNavigationRequest()).toBe(false);
});
it('should work when navigating to image', async() => {
const {page, server} = getTestState();
it('should work when navigating to image', async () => {
const { page, server } = getTestState();
const requests = [];
page.on('request', request => requests.push(request));
page.on('request', (request) => requests.push(request));
await page.goto(server.PREFIX + '/pptr.png');
expect(requests[0].isNavigationRequest()).toBe(true);
});
});
describeFailsFirefox('Page.setExtraHTTPHeaders', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Page.setExtraHTTPHeaders', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.setExtraHTTPHeaders({
foo: 'bar'
foo: 'bar',
});
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
@ -452,53 +509,55 @@ describe('network', function() {
]);
expect(request.headers['foo']).toBe('bar');
});
it('should throw for non-string header values', async() => {
const {page} = getTestState();
it('should throw for non-string header values', async () => {
const { page } = getTestState();
let error = null;
try {
await page.setExtraHTTPHeaders({'foo': 1});
await page.setExtraHTTPHeaders({ foo: 1 });
} catch (error_) {
error = error_;
}
expect(error.message).toBe('Expected value of header "foo" to be String, but "number" is found.');
expect(error.message).toBe(
'Expected value of header "foo" to be String, but "number" is found.'
);
});
});
describeFailsFirefox('Page.authenticate', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Page.authenticate', function () {
it('should work', async () => {
const { page, server } = getTestState();
server.setAuth('/empty.html', 'user', 'pass');
let response = await page.goto(server.EMPTY_PAGE);
expect(response.status()).toBe(401);
await page.authenticate({
username: 'user',
password: 'pass'
password: 'pass',
});
response = await page.reload();
expect(response.status()).toBe(200);
});
it('should fail if wrong credentials', async() => {
const {page, server} = getTestState();
it('should fail if wrong credentials', async () => {
const { page, server } = getTestState();
// Use unique user/password since Chrome caches credentials per origin.
server.setAuth('/empty.html', 'user2', 'pass2');
await page.authenticate({
username: 'foo',
password: 'bar'
password: 'bar',
});
const response = await page.goto(server.EMPTY_PAGE);
expect(response.status()).toBe(401);
});
it('should allow disable authentication', async() => {
const {page, server} = getTestState();
it('should allow disable authentication', async () => {
const { page, server } = getTestState();
// Use unique user/password since Chrome caches credentials per origin.
server.setAuth('/empty.html', 'user3', 'pass3');
await page.authenticate({
username: 'user3',
password: 'pass3'
password: 'pass3',
});
let response = await page.goto(server.EMPTY_PAGE);
expect(response.status()).toBe(200);
@ -508,5 +567,4 @@ describe('network', function() {
expect(response.status()).toBe(401);
});
});
});

View File

@ -15,57 +15,60 @@
*/
const expect = require('expect');
const {getTestState} = require('./mocha-utils');
const { getTestState } = require('./mocha-utils');
describeChromeOnly('OOPIF', function() {
describeChromeOnly('OOPIF', function () {
/* We use a special browser for this test as we need the --site-per-process flag */
let browser;
let context;
let page;
before(async() => {
const {puppeteer, defaultBrowserOptions} = getTestState();
browser = await puppeteer.launch(Object.assign({}, defaultBrowserOptions, {
args: (defaultBrowserOptions.args || []).concat(['--site-per-process']),
}));
before(async () => {
const { puppeteer, defaultBrowserOptions } = getTestState();
browser = await puppeteer.launch(
Object.assign({}, defaultBrowserOptions, {
args: (defaultBrowserOptions.args || []).concat(['--site-per-process']),
})
);
});
beforeEach(async() => {
beforeEach(async () => {
context = await browser.createIncognitoBrowserContext();
page = await context.newPage();
});
afterEach(async() => {
afterEach(async () => {
await context.close();
page = null;
context = null;
});
after(async() => {
after(async () => {
await browser.close();
browser = null;
});
xit('should report oopif frames', async() => {
const {server} = getTestState();
xit('should report oopif frames', async () => {
const { server } = getTestState();
await page.goto(server.PREFIX + '/dynamic-oopif.html');
expect(oopifs(context).length).toBe(1);
expect(page.frames().length).toBe(2);
});
it('should load oopif iframes with subresources and request interception', async() => {
const {server} = getTestState();
it('should load oopif iframes with subresources and request interception', async () => {
const { server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => request.continue());
page.on('request', (request) => request.continue());
await page.goto(server.PREFIX + '/dynamic-oopif.html');
expect(oopifs(context).length).toBe(1);
});
});
/**
* @param {!BrowserContext} context
*/
function oopifs(context) {
return context.targets().filter(target => target._targetInfo.type === 'iframe');
return context
.targets()
.filter((target) => target._targetInfo.type === 'iframe');
}

File diff suppressed because it is too large Load Diff

View File

@ -14,81 +14,101 @@
* limitations under the License.
*/
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('querySelector', function() {
describe('querySelector', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describeFailsFirefox('Page.$eval', function() {
it('should work', async() => {
const {page} = getTestState();
describeFailsFirefox('Page.$eval', function () {
it('should work', async () => {
const { page } = getTestState();
await page.setContent('<section id="testAttribute">43543</section>');
const idAttribute = await page.$eval('section', e => e.id);
const idAttribute = await page.$eval('section', (e) => e.id);
expect(idAttribute).toBe('testAttribute');
});
it('should accept arguments', async() => {
const {page} = getTestState();
it('should accept arguments', async () => {
const { page } = getTestState();
await page.setContent('<section>hello</section>');
const text = await page.$eval('section', (e, suffix) => e.textContent + suffix, ' world!');
const text = await page.$eval(
'section',
(e, suffix) => e.textContent + suffix,
' world!'
);
expect(text).toBe('hello world!');
});
it('should accept ElementHandles as arguments', async() => {
const {page} = getTestState();
it('should accept ElementHandles as arguments', async () => {
const { page } = getTestState();
await page.setContent('<section>hello</section><div> world</div>');
const divHandle = await page.$('div');
const text = await page.$eval('section', (e, div) => e.textContent + div.textContent, divHandle);
const text = await page.$eval(
'section',
(e, div) => e.textContent + div.textContent,
divHandle
);
expect(text).toBe('hello world');
});
it('should throw error if no element is found', async() => {
const {page} = getTestState();
it('should throw error if no element is found', async () => {
const { page } = getTestState();
let error = null;
await page.$eval('section', e => e.id).catch(error_ => error = error_);
expect(error.message).toContain('failed to find element matching selector "section"');
await page
.$eval('section', (e) => e.id)
.catch((error_) => (error = error_));
expect(error.message).toContain(
'failed to find element matching selector "section"'
);
});
});
describeFailsFirefox('Page.$$eval', function() {
it('should work', async() => {
const {page} = getTestState();
describeFailsFirefox('Page.$$eval', function () {
it('should work', async () => {
const { page } = getTestState();
await page.setContent('<div>hello</div><div>beautiful</div><div>world!</div>');
const divsCount = await page.$$eval('div', divs => divs.length);
await page.setContent(
'<div>hello</div><div>beautiful</div><div>world!</div>'
);
const divsCount = await page.$$eval('div', (divs) => divs.length);
expect(divsCount).toBe(3);
});
});
describeFailsFirefox('Page.$', function() {
it('should query existing element', async() => {
const {page} = getTestState();
describeFailsFirefox('Page.$', function () {
it('should query existing element', async () => {
const { page } = getTestState();
await page.setContent('<section>test</section>');
const element = await page.$('section');
expect(element).toBeTruthy();
});
it('should return null for non-existing element', async() => {
const {page} = getTestState();
it('should return null for non-existing element', async () => {
const { page } = getTestState();
const element = await page.$('non-existing-element');
expect(element).toBe(null);
});
});
describe('Page.$$', function() {
itFailsFirefox('should query existing elements', async() => {
const {page} = getTestState();
describe('Page.$$', function () {
itFailsFirefox('should query existing elements', async () => {
const { page } = getTestState();
await page.setContent('<div>A</div><br/><div>B</div>');
const elements = await page.$$('div');
expect(elements.length).toBe(2);
const promises = elements.map(element => page.evaluate(e => e.textContent, element));
const promises = elements.map((element) =>
page.evaluate((e) => e.textContent, element)
);
expect(await Promise.all(promises)).toEqual(['A', 'B']);
});
it('should return empty array if nothing is found', async() => {
const {page, server} = getTestState();
it('should return empty array if nothing is found', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const elements = await page.$$('div');
@ -96,23 +116,23 @@ describe('querySelector', function() {
});
});
describeFailsFirefox('Path.$x', function() {
it('should query existing element', async() => {
const {page} = getTestState();
describeFailsFirefox('Path.$x', function () {
it('should query existing element', async () => {
const { page } = getTestState();
await page.setContent('<section>test</section>');
const elements = await page.$x('/html/body/section');
expect(elements[0]).toBeTruthy();
expect(elements.length).toBe(1);
});
it('should return empty array for non-existing element', async() => {
const {page} = getTestState();
it('should return empty array for non-existing element', async () => {
const { page } = getTestState();
const element = await page.$x('/html/body/non-existing-element');
expect(element).toEqual([]);
});
it('should return multiple elements', async() => {
const {page} = getTestState();
it('should return multiple elements', async () => {
const { page } = getTestState();
await page.setContent('<div></div><div></div>');
const elements = await page.$x('/html/body/div');
@ -120,131 +140,161 @@ describe('querySelector', function() {
});
});
describe('ElementHandle.$', function() {
it('should query existing element', async() => {
const {page, server} = getTestState();
describe('ElementHandle.$', function () {
it('should query existing element', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/playground.html');
await page.setContent('<html><body><div class="second"><div class="inner">A</div></div></body></html>');
await page.setContent(
'<html><body><div class="second"><div class="inner">A</div></div></body></html>'
);
const html = await page.$('html');
const second = await html.$('.second');
const inner = await second.$('.inner');
const content = await page.evaluate(e => e.textContent, inner);
const content = await page.evaluate((e) => e.textContent, inner);
expect(content).toBe('A');
});
itFailsFirefox('should return null for non-existing element', async() => {
const {page} = getTestState();
itFailsFirefox('should return null for non-existing element', async () => {
const { page } = getTestState();
await page.setContent('<html><body><div class="second"><div class="inner">B</div></div></body></html>');
await page.setContent(
'<html><body><div class="second"><div class="inner">B</div></div></body></html>'
);
const html = await page.$('html');
const second = await html.$('.third');
expect(second).toBe(null);
});
});
describeFailsFirefox('ElementHandle.$eval', function() {
it('should work', async() => {
const {page} = getTestState();
describeFailsFirefox('ElementHandle.$eval', function () {
it('should work', async () => {
const { page } = getTestState();
await page.setContent('<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>');
await page.setContent(
'<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>'
);
const tweet = await page.$('.tweet');
const content = await tweet.$eval('.like', node => node.innerText);
const content = await tweet.$eval('.like', (node) => node.innerText);
expect(content).toBe('100');
});
it('should retrieve content from subtree', async() => {
const {page} = getTestState();
it('should retrieve content from subtree', async () => {
const { page } = getTestState();
const htmlContent = '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>';
const htmlContent =
'<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>';
await page.setContent(htmlContent);
const elementHandle = await page.$('#myId');
const content = await elementHandle.$eval('.a', node => node.innerText);
const content = await elementHandle.$eval('.a', (node) => node.innerText);
expect(content).toBe('a-child-div');
});
it('should throw in case of missing selector', async() => {
const {page} = getTestState();
it('should throw in case of missing selector', async () => {
const { page } = getTestState();
const htmlContent = '<div class="a">not-a-child-div</div><div id="myId"></div>';
const htmlContent =
'<div class="a">not-a-child-div</div><div id="myId"></div>';
await page.setContent(htmlContent);
const elementHandle = await page.$('#myId');
const errorMessage = await elementHandle.$eval('.a', node => node.innerText).catch(error => error.message);
expect(errorMessage).toBe(`Error: failed to find element matching selector ".a"`);
const errorMessage = await elementHandle
.$eval('.a', (node) => node.innerText)
.catch((error) => error.message);
expect(errorMessage).toBe(
`Error: failed to find element matching selector ".a"`
);
});
});
describeFailsFirefox('ElementHandle.$$eval', function() {
it('should work', async() => {
const {page} = getTestState();
describeFailsFirefox('ElementHandle.$$eval', function () {
it('should work', async () => {
const { page } = getTestState();
await page.setContent('<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>');
await page.setContent(
'<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>'
);
const tweet = await page.$('.tweet');
const content = await tweet.$$eval('.like', nodes => nodes.map(n => n.innerText));
const content = await tweet.$$eval('.like', (nodes) =>
nodes.map((n) => n.innerText)
);
expect(content).toEqual(['100', '10']);
});
it('should retrieve content from subtree', async() => {
const {page} = getTestState();
it('should retrieve content from subtree', async () => {
const { page } = getTestState();
const htmlContent = '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>';
const htmlContent =
'<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>';
await page.setContent(htmlContent);
const elementHandle = await page.$('#myId');
const content = await elementHandle.$$eval('.a', nodes => nodes.map(n => n.innerText));
const content = await elementHandle.$$eval('.a', (nodes) =>
nodes.map((n) => n.innerText)
);
expect(content).toEqual(['a1-child-div', 'a2-child-div']);
});
it('should not throw in case of missing selector', async() => {
const {page} = getTestState();
it('should not throw in case of missing selector', async () => {
const { page } = getTestState();
const htmlContent = '<div class="a">not-a-child-div</div><div id="myId"></div>';
const htmlContent =
'<div class="a">not-a-child-div</div><div id="myId"></div>';
await page.setContent(htmlContent);
const elementHandle = await page.$('#myId');
const nodesLength = await elementHandle.$$eval('.a', nodes => nodes.length);
const nodesLength = await elementHandle.$$eval(
'.a',
(nodes) => nodes.length
);
expect(nodesLength).toBe(0);
});
});
describeFailsFirefox('ElementHandle.$$', function() {
it('should query existing elements', async() => {
const {page} = getTestState();
describeFailsFirefox('ElementHandle.$$', function () {
it('should query existing elements', async () => {
const { page } = getTestState();
await page.setContent('<html><body><div>A</div><br/><div>B</div></body></html>');
await page.setContent(
'<html><body><div>A</div><br/><div>B</div></body></html>'
);
const html = await page.$('html');
const elements = await html.$$('div');
expect(elements.length).toBe(2);
const promises = elements.map(element => page.evaluate(e => e.textContent, element));
const promises = elements.map((element) =>
page.evaluate((e) => e.textContent, element)
);
expect(await Promise.all(promises)).toEqual(['A', 'B']);
});
it('should return empty array for non-existing elements', async() => {
const {page} = getTestState();
it('should return empty array for non-existing elements', async () => {
const { page } = getTestState();
await page.setContent('<html><body><span>A</span><br/><span>B</span></body></html>');
await page.setContent(
'<html><body><span>A</span><br/><span>B</span></body></html>'
);
const html = await page.$('html');
const elements = await html.$$('div');
expect(elements.length).toBe(0);
});
});
describe('ElementHandle.$x', function() {
it('should query existing element', async() => {
const {page, server} = getTestState();
describe('ElementHandle.$x', function () {
it('should query existing element', async () => {
const { page, server } = getTestState();
await page.goto(server.PREFIX + '/playground.html');
await page.setContent('<html><body><div class="second"><div class="inner">A</div></div></body></html>');
await page.setContent(
'<html><body><div class="second"><div class="inner">A</div></div></body></html>'
);
const html = await page.$('html');
const second = await html.$x(`./body/div[contains(@class, 'second')]`);
const inner = await second[0].$x(`./div[contains(@class, 'inner')]`);
const content = await page.evaluate(e => e.textContent, inner[0]);
const content = await page.evaluate((e) => e.textContent, inner[0]);
expect(content).toBe('A');
});
itFailsFirefox('should return null for non-existing element', async() => {
const {page} = getTestState();
itFailsFirefox('should return null for non-existing element', async () => {
const { page } = getTestState();
await page.setContent('<html><body><div class="second"><div class="inner">B</div></div></body></html>');
await page.setContent(
'<html><body><div class="second"><div class="inner">B</div></div></body></html>'
);
const html = await page.$('html');
const second = await html.$x(`/div[contains(@class, 'third')]`);
expect(second).toEqual([]);

View File

@ -18,17 +18,21 @@ const fs = require('fs');
const path = require('path');
const utils = require('./utils');
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('request interception', function() {
describe('request interception', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describeFailsFirefox('Page.setRequestInterception', function() {
it('should intercept', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Page.setRequestInterception', function () {
it('should intercept', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
if (utils.isFavicon(request)) {
request.continue();
return;
@ -47,101 +51,100 @@ describe('request interception', function() {
expect(response.ok()).toBe(true);
expect(response.remoteAddress().port).toBe(server.PORT);
});
it('should work when POST is redirected with 302', async() => {
const {page, server} = getTestState();
it('should work when POST is redirected with 302', async () => {
const { page, server } = getTestState();
server.setRedirect('/rredirect', '/empty.html');
await page.goto(server.EMPTY_PAGE);
await page.setRequestInterception(true);
page.on('request', request => request.continue());
page.on('request', (request) => request.continue());
await page.setContent(`
<form action='/rredirect' method='post'>
<input type="hidden" id="foo" name="foo" value="FOOBAR">
</form>
`);
await Promise.all([
page.$eval('form', form => form.submit()),
page.waitForNavigation()
page.$eval('form', (form) => form.submit()),
page.waitForNavigation(),
]);
});
// @see https://github.com/puppeteer/puppeteer/issues/3973
it('should work when header manipulation headers with redirect', async() => {
const {page, server} = getTestState();
it('should work when header manipulation headers with redirect', async () => {
const { page, server } = getTestState();
server.setRedirect('/rrredirect', '/empty.html');
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
const headers = Object.assign({}, request.headers(), {
foo: 'bar'
foo: 'bar',
});
request.continue({headers});
request.continue({ headers });
});
await page.goto(server.PREFIX + '/rrredirect');
});
// @see https://github.com/puppeteer/puppeteer/issues/4743
it('should be able to remove headers', async() => {
const {page, server} = getTestState();
it('should be able to remove headers', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
const headers = Object.assign({}, request.headers(), {
foo: 'bar',
origin: undefined, // remove "origin" header
});
request.continue({headers});
request.continue({ headers });
});
const [serverRequest] = await Promise.all([
server.waitForRequest('/empty.html'),
page.goto(server.PREFIX + '/empty.html')
page.goto(server.PREFIX + '/empty.html'),
]);
expect(serverRequest.headers.origin).toBe(undefined);
});
it('should contain referer header', async() => {
const {page, server} = getTestState();
it('should contain referer header', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
const requests = [];
page.on('request', request => {
if (!utils.isFavicon(request))
requests.push(request);
page.on('request', (request) => {
if (!utils.isFavicon(request)) requests.push(request);
request.continue();
});
await page.goto(server.PREFIX + '/one-style.html');
expect(requests[1].url()).toContain('/one-style.css');
expect(requests[1].headers().referer).toContain('/one-style.html');
});
it('should properly return navigation response when URL has cookies', async() => {
const {page, server} = getTestState();
it('should properly return navigation response when URL has cookies', async () => {
const { page, server } = getTestState();
// Setup cookie.
await page.goto(server.EMPTY_PAGE);
await page.setCookie({name: 'foo', value: 'bar'});
await page.setCookie({ name: 'foo', value: 'bar' });
// Setup request interception.
await page.setRequestInterception(true);
page.on('request', request => request.continue());
page.on('request', (request) => request.continue());
const response = await page.reload();
expect(response.status()).toBe(200);
});
it('should stop intercepting', async() => {
const {page, server} = getTestState();
it('should stop intercepting', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.once('request', request => request.continue());
page.once('request', (request) => request.continue());
await page.goto(server.EMPTY_PAGE);
await page.setRequestInterception(false);
await page.goto(server.EMPTY_PAGE);
});
it('should show custom HTTP headers', async() => {
const {page, server} = getTestState();
it('should show custom HTTP headers', async () => {
const { page, server } = getTestState();
await page.setExtraHTTPHeaders({
foo: 'bar'
foo: 'bar',
});
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
expect(request.headers()['foo']).toBe('bar');
request.continue();
});
@ -149,104 +152,113 @@ describe('request interception', function() {
expect(response.ok()).toBe(true);
});
// @see https://github.com/puppeteer/puppeteer/issues/4337
it('should work with redirect inside sync XHR', async() => {
const {page, server} = getTestState();
it('should work with redirect inside sync XHR', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
server.setRedirect('/logo.png', '/pptr.png');
await page.setRequestInterception(true);
page.on('request', request => request.continue());
const status = await page.evaluate(async() => {
page.on('request', (request) => request.continue());
const status = await page.evaluate(async () => {
const request = new XMLHttpRequest();
request.open('GET', '/logo.png', false); // `false` makes the request synchronous
request.open('GET', '/logo.png', false); // `false` makes the request synchronous
request.send(null);
return request.status;
});
expect(status).toBe(200);
});
it('should work with custom referer headers', async() => {
const {page, server} = getTestState();
it('should work with custom referer headers', async () => {
const { page, server } = getTestState();
await page.setExtraHTTPHeaders({'referer': server.EMPTY_PAGE});
await page.setExtraHTTPHeaders({ referer: server.EMPTY_PAGE });
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
request.continue();
});
const response = await page.goto(server.EMPTY_PAGE);
expect(response.ok()).toBe(true);
});
it('should be abortable', async() => {
const {page, server} = getTestState();
it('should be abortable', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().endsWith('.css'))
request.abort();
else
request.continue();
page.on('request', (request) => {
if (request.url().endsWith('.css')) request.abort();
else request.continue();
});
let failedRequests = 0;
page.on('requestfailed', event => ++failedRequests);
page.on('requestfailed', (event) => ++failedRequests);
const response = await page.goto(server.PREFIX + '/one-style.html');
expect(response.ok()).toBe(true);
expect(response.request().failure()).toBe(null);
expect(failedRequests).toBe(1);
});
it('should be abortable with custom error codes', async() => {
const {page, server} = getTestState();
it('should be abortable with custom error codes', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
request.abort('internetdisconnected');
});
let failedRequest = null;
page.on('requestfailed', request => failedRequest = request);
await page.goto(server.EMPTY_PAGE).catch(error => {});
page.on('requestfailed', (request) => (failedRequest = request));
await page.goto(server.EMPTY_PAGE).catch((error) => {});
expect(failedRequest).toBeTruthy();
expect(failedRequest.failure().errorText).toBe('net::ERR_INTERNET_DISCONNECTED');
expect(failedRequest.failure().errorText).toBe(
'net::ERR_INTERNET_DISCONNECTED'
);
});
it('should send referer', async() => {
const {page, server} = getTestState();
it('should send referer', async () => {
const { page, server } = getTestState();
await page.setExtraHTTPHeaders({
referer: 'http://google.com/'
referer: 'http://google.com/',
});
await page.setRequestInterception(true);
page.on('request', request => request.continue());
page.on('request', (request) => request.continue());
const [request] = await Promise.all([
server.waitForRequest('/grid.html'),
page.goto(server.PREFIX + '/grid.html'),
]);
expect(request.headers['referer']).toBe('http://google.com/');
});
it('should fail navigation when aborting main resource', async() => {
const {page, server, isChrome} = getTestState();
it('should fail navigation when aborting main resource', async () => {
const { page, server, isChrome } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => request.abort());
page.on('request', (request) => request.abort());
let error = null;
await page.goto(server.EMPTY_PAGE).catch(error_ => error = error_);
await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_));
expect(error).toBeTruthy();
if (isChrome)
expect(error.message).toContain('net::ERR_FAILED');
else
expect(error.message).toContain('NS_ERROR_FAILURE');
if (isChrome) expect(error.message).toContain('net::ERR_FAILED');
else expect(error.message).toContain('NS_ERROR_FAILURE');
});
it('should work with redirects', async() => {
const {page, server} = getTestState();
it('should work with redirects', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
const requests = [];
page.on('request', request => {
page.on('request', (request) => {
request.continue();
requests.push(request);
});
server.setRedirect('/non-existing-page.html', '/non-existing-page-2.html');
server.setRedirect('/non-existing-page-2.html', '/non-existing-page-3.html');
server.setRedirect('/non-existing-page-3.html', '/non-existing-page-4.html');
server.setRedirect(
'/non-existing-page.html',
'/non-existing-page-2.html'
);
server.setRedirect(
'/non-existing-page-2.html',
'/non-existing-page-3.html'
);
server.setRedirect(
'/non-existing-page-3.html',
'/non-existing-page-4.html'
);
server.setRedirect('/non-existing-page-4.html', '/empty.html');
const response = await page.goto(server.PREFIX + '/non-existing-page.html');
const response = await page.goto(
server.PREFIX + '/non-existing-page.html'
);
expect(response.status()).toBe(200);
expect(response.url()).toContain('empty.html');
expect(requests.length).toBe(5);
@ -262,20 +274,21 @@ describe('request interception', function() {
expect(request.redirectChain().indexOf(request)).toBe(i);
}
});
it('should work with redirects for subresources', async() => {
const {page, server} = getTestState();
it('should work with redirects for subresources', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
const requests = [];
page.on('request', request => {
page.on('request', (request) => {
request.continue();
if (!utils.isFavicon(request))
requests.push(request);
if (!utils.isFavicon(request)) requests.push(request);
});
server.setRedirect('/one-style.css', '/two-style.css');
server.setRedirect('/two-style.css', '/three-style.css');
server.setRedirect('/three-style.css', '/four-style.css');
server.setRoute('/four-style.css', (req, res) => res.end('body {box-sizing: border-box; }'));
server.setRoute('/four-style.css', (req, res) =>
res.end('body {box-sizing: border-box; }')
);
const response = await page.goto(server.PREFIX + '/one-style.html');
expect(response.status()).toBe(200);
@ -289,42 +302,38 @@ describe('request interception', function() {
expect(redirectChain[0].url()).toContain('/one-style.css');
expect(redirectChain[2].url()).toContain('/three-style.css');
});
it('should be able to abort redirects', async() => {
const {page, server, isChrome} = getTestState();
it('should be able to abort redirects', async () => {
const { page, server, isChrome } = getTestState();
await page.setRequestInterception(true);
server.setRedirect('/non-existing.json', '/non-existing-2.json');
server.setRedirect('/non-existing-2.json', '/simple.html');
page.on('request', request => {
if (request.url().includes('non-existing-2'))
request.abort();
else
request.continue();
page.on('request', (request) => {
if (request.url().includes('non-existing-2')) request.abort();
else request.continue();
});
await page.goto(server.EMPTY_PAGE);
const result = await page.evaluate(async() => {
const result = await page.evaluate(async () => {
try {
await fetch('/non-existing.json');
} catch (error) {
return error.message;
}
});
if (isChrome)
expect(result).toContain('Failed to fetch');
else
expect(result).toContain('NetworkError');
if (isChrome) expect(result).toContain('Failed to fetch');
else expect(result).toContain('NetworkError');
});
it('should work with equal requests', async() => {
const {page, server} = getTestState();
it('should work with equal requests', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
let responseCount = 1;
server.setRoute('/zzz', (req, res) => res.end((responseCount++) * 11 + ''));
server.setRoute('/zzz', (req, res) => res.end(responseCount++ * 11 + ''));
await page.setRequestInterception(true);
let spinner = false;
// Cancel 2nd request.
page.on('request', request => {
page.on('request', (request) => {
if (utils.isFavicon(request)) {
request.continue();
return;
@ -332,19 +341,27 @@ describe('request interception', function() {
spinner ? request.abort() : request.continue();
spinner = !spinner;
});
const results = await page.evaluate(() => Promise.all([
fetch('/zzz').then(response => response.text()).catch(error => 'FAILED'),
fetch('/zzz').then(response => response.text()).catch(error => 'FAILED'),
fetch('/zzz').then(response => response.text()).catch(error => 'FAILED'),
]));
const results = await page.evaluate(() =>
Promise.all([
fetch('/zzz')
.then((response) => response.text())
.catch((error) => 'FAILED'),
fetch('/zzz')
.then((response) => response.text())
.catch((error) => 'FAILED'),
fetch('/zzz')
.then((response) => response.text())
.catch((error) => 'FAILED'),
])
);
expect(results).toEqual(['11', 'FAILED', '22']);
});
it('should navigate to dataURL and fire dataURL requests', async() => {
const {page} = getTestState();
it('should navigate to dataURL and fire dataURL requests', async () => {
const { page } = getTestState();
await page.setRequestInterception(true);
const requests = [];
page.on('request', request => {
page.on('request', (request) => {
requests.push(request);
request.continue();
});
@ -354,28 +371,31 @@ describe('request interception', function() {
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(dataURL);
});
it('should be able to fetch dataURL and fire dataURL requests', async() => {
const {page, server} = getTestState();
it('should be able to fetch dataURL and fire dataURL requests', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setRequestInterception(true);
const requests = [];
page.on('request', request => {
page.on('request', (request) => {
requests.push(request);
request.continue();
});
const dataURL = 'data:text/html,<div>yo</div>';
const text = await page.evaluate(url => fetch(url).then(r => r.text()), dataURL);
const text = await page.evaluate(
(url) => fetch(url).then((r) => r.text()),
dataURL
);
expect(text).toBe('<div>yo</div>');
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(dataURL);
});
it('should navigate to URL with hash and and fire requests without hash', async() => {
const {page, server} = getTestState();
it('should navigate to URL with hash and and fire requests without hash', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
const requests = [];
page.on('request', request => {
page.on('request', (request) => {
requests.push(request);
request.continue();
});
@ -385,62 +405,70 @@ describe('request interception', function() {
expect(requests.length).toBe(1);
expect(requests[0].url()).toBe(server.EMPTY_PAGE);
});
it('should work with encoded server', async() => {
const {page, server} = getTestState();
it('should work with encoded server', async () => {
const { page, server } = getTestState();
// The requestWillBeSent will report encoded URL, whereas interception will
// report URL as-is. @see crbug.com/759388
await page.setRequestInterception(true);
page.on('request', request => request.continue());
const response = await page.goto(server.PREFIX + '/some nonexisting page');
page.on('request', (request) => request.continue());
const response = await page.goto(
server.PREFIX + '/some nonexisting page'
);
expect(response.status()).toBe(404);
});
it('should work with badly encoded server', async() => {
const {page, server} = getTestState();
it('should work with badly encoded server', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
server.setRoute('/malformed?rnd=%911', (req, res) => res.end());
page.on('request', request => request.continue());
page.on('request', (request) => request.continue());
const response = await page.goto(server.PREFIX + '/malformed?rnd=%911');
expect(response.status()).toBe(200);
});
it('should work with encoded server - 2', async() => {
const {page, server} = getTestState();
it('should work with encoded server - 2', async () => {
const { page, server } = getTestState();
// The requestWillBeSent will report URL as-is, whereas interception will
// report encoded URL for stylesheet. @see crbug.com/759388
await page.setRequestInterception(true);
const requests = [];
page.on('request', request => {
page.on('request', (request) => {
request.continue();
requests.push(request);
});
const response = await page.goto(`data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`);
const response = await page.goto(
`data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`
);
expect(response.status()).toBe(200);
expect(requests.length).toBe(2);
expect(requests[1].response().status()).toBe(404);
});
it('should not throw "Invalid Interception Id" if the request was cancelled', async() => {
const {page, server} = getTestState();
it('should not throw "Invalid Interception Id" if the request was cancelled', async () => {
const { page, server } = getTestState();
await page.setContent('<iframe></iframe>');
await page.setRequestInterception(true);
let request = null;
page.on('request', async r => request = r);
page.$eval('iframe', (frame, url) => frame.src = url, server.EMPTY_PAGE),
// Wait for request interception.
await utils.waitEvent(page, 'request');
page.on('request', async (r) => (request = r));
page.$eval(
'iframe',
(frame, url) => (frame.src = url),
server.EMPTY_PAGE
),
// Wait for request interception.
await utils.waitEvent(page, 'request');
// Delete frame to cause request to be canceled.
await page.$eval('iframe', frame => frame.remove());
await page.$eval('iframe', (frame) => frame.remove());
let error = null;
await request.continue().catch(error_ => error = error_);
await request.continue().catch((error_) => (error = error_));
expect(error).toBe(null);
});
it('should throw if interception is not enabled', async() => {
const {page, server} = getTestState();
it('should throw if interception is not enabled', async () => {
const { page, server } = getTestState();
let error = null;
page.on('request', async request => {
page.on('request', async (request) => {
try {
await request.continue();
} catch (error_) {
@ -450,96 +478,102 @@ describe('request interception', function() {
await page.goto(server.EMPTY_PAGE);
expect(error.message).toContain('Request Interception is not enabled');
});
it('should work with file URLs', async() => {
const {page} = getTestState();
it('should work with file URLs', async () => {
const { page } = getTestState();
await page.setRequestInterception(true);
const urls = new Set();
page.on('request', request => {
page.on('request', (request) => {
urls.add(request.url().split('/').pop());
request.continue();
});
await page.goto(pathToFileURL(path.join(__dirname, 'assets', 'one-style.html')));
await page.goto(
pathToFileURL(path.join(__dirname, 'assets', 'one-style.html'))
);
expect(urls.size).toBe(2);
expect(urls.has('one-style.html')).toBe(true);
expect(urls.has('one-style.css')).toBe(true);
});
});
describeFailsFirefox('Request.continue', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Request.continue', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => request.continue());
page.on('request', (request) => request.continue());
await page.goto(server.EMPTY_PAGE);
});
it('should amend HTTP headers', async() => {
const {page, server} = getTestState();
it('should amend HTTP headers', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
const headers = Object.assign({}, request.headers());
headers['FOO'] = 'bar';
request.continue({headers});
request.continue({ headers });
});
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
server.waitForRequest('/sleep.zzz'),
page.evaluate(() => fetch('/sleep.zzz'))
page.evaluate(() => fetch('/sleep.zzz')),
]);
expect(request.headers['foo']).toBe('bar');
});
it('should redirect in a way non-observable to page', async() => {
const {page, server} = getTestState();
it('should redirect in a way non-observable to page', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
const redirectURL = request.url().includes('/empty.html') ? server.PREFIX + '/consolelog.html' : undefined;
request.continue({url: redirectURL});
page.on('request', (request) => {
const redirectURL = request.url().includes('/empty.html')
? server.PREFIX + '/consolelog.html'
: undefined;
request.continue({ url: redirectURL });
});
let consoleMessage = null;
page.on('console', msg => consoleMessage = msg);
page.on('console', (msg) => (consoleMessage = msg));
await page.goto(server.EMPTY_PAGE);
expect(page.url()).toBe(server.EMPTY_PAGE);
expect(consoleMessage.text()).toBe('yellow');
});
it('should amend method', async() => {
const {page, server} = getTestState();
it('should amend method', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setRequestInterception(true);
page.on('request', request => {
request.continue({method: 'POST'});
page.on('request', (request) => {
request.continue({ method: 'POST' });
});
const [request] = await Promise.all([
server.waitForRequest('/sleep.zzz'),
page.evaluate(() => fetch('/sleep.zzz'))
page.evaluate(() => fetch('/sleep.zzz')),
]);
expect(request.method).toBe('POST');
});
it('should amend post data', async() => {
const {page, server} = getTestState();
it('should amend post data', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.setRequestInterception(true);
page.on('request', request => {
request.continue({postData: 'doggo'});
page.on('request', (request) => {
request.continue({ postData: 'doggo' });
});
const [serverRequest] = await Promise.all([
server.waitForRequest('/sleep.zzz'),
page.evaluate(() => fetch('/sleep.zzz', {method: 'POST', body: 'birdy'}))
page.evaluate(() =>
fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })
),
]);
expect(await serverRequest.postBody).toBe('doggo');
});
it('should amend both post data and method on navigation', async() => {
const {page, server} = getTestState();
it('should amend both post data and method on navigation', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
request.continue({method: 'POST', postData: 'doggo'});
page.on('request', (request) => {
request.continue({ method: 'POST', postData: 'doggo' });
});
const [serverRequest] = await Promise.all([
server.waitForRequest('/empty.html'),
@ -550,45 +584,49 @@ describe('request interception', function() {
});
});
describeFailsFirefox('Request.respond', function() {
it('should work', async() => {
const {page, server} = getTestState();
describeFailsFirefox('Request.respond', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
request.respond({
status: 201,
headers: {
foo: 'bar'
foo: 'bar',
},
body: 'Yo, page!'
body: 'Yo, page!',
});
});
const response = await page.goto(server.EMPTY_PAGE);
expect(response.status()).toBe(201);
expect(response.headers().foo).toBe('bar');
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
expect(await page.evaluate(() => document.body.textContent)).toBe(
'Yo, page!'
);
});
it('should work with status code 422', async() => {
const {page, server} = getTestState();
it('should work with status code 422', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
request.respond({
status: 422,
body: 'Yo, page!'
body: 'Yo, page!',
});
});
const response = await page.goto(server.EMPTY_PAGE);
expect(response.status()).toBe(422);
expect(response.statusText()).toBe('Unprocessable Entity');
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
expect(await page.evaluate(() => document.body.textContent)).toBe(
'Yo, page!'
);
});
it('should redirect', async() => {
const {page, server} = getTestState();
it('should redirect', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
if (!request.url().includes('rrredirect')) {
request.continue();
return;
@ -602,47 +640,53 @@ describe('request interception', function() {
});
const response = await page.goto(server.PREFIX + '/rrredirect');
expect(response.request().redirectChain().length).toBe(1);
expect(response.request().redirectChain()[0].url()).toBe(server.PREFIX + '/rrredirect');
expect(response.request().redirectChain()[0].url()).toBe(
server.PREFIX + '/rrredirect'
);
expect(response.url()).toBe(server.EMPTY_PAGE);
});
it('should allow mocking binary responses', async() => {
const {page, server} = getTestState();
it('should allow mocking binary responses', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png'));
page.on('request', (request) => {
const imageBuffer = fs.readFileSync(
path.join(__dirname, 'assets', 'pptr.png')
);
request.respond({
contentType: 'image/png',
body: imageBuffer
body: imageBuffer,
});
});
await page.evaluate(PREFIX => {
await page.evaluate((PREFIX) => {
const img = document.createElement('img');
img.src = PREFIX + '/does-not-exist.png';
document.body.appendChild(img);
return new Promise(fulfill => img.onload = fulfill);
return new Promise((fulfill) => (img.onload = fulfill));
}, server.PREFIX);
const img = await page.$('img');
expect(await img.screenshot()).toBeGolden('mock-binary-response.png');
});
it('should stringify intercepted request response headers', async() => {
const {page, server} = getTestState();
it('should stringify intercepted request response headers', async () => {
const { page, server } = getTestState();
await page.setRequestInterception(true);
page.on('request', request => {
page.on('request', (request) => {
request.respond({
status: 200,
headers: {
'foo': true
foo: true,
},
body: 'Yo, page!'
body: 'Yo, page!',
});
});
const response = await page.goto(server.EMPTY_PAGE);
expect(response.status()).toBe(200);
const headers = response.headers();
expect(headers.foo).toBe('true');
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
expect(await page.evaluate(() => document.body.textContent)).toBe(
'Yo, page!'
);
});
});
});
@ -654,7 +698,6 @@ describe('request interception', function() {
function pathToFileURL(path) {
let pathName = path.replace(/\\/g, '/');
// Windows drive letter must be prefixed with a slash.
if (!pathName.startsWith('/'))
pathName = '/' + pathName;
if (!pathName.startsWith('/')) pathName = '/' + pathName;
return 'file://' + pathName;
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
const path = require('path');
const {TestServer} = require('../utils/testserver/');
const { TestServer } = require('../utils/testserver/');
const port = 8907;
const httpsPort = 8908;
@ -24,7 +24,7 @@ const cachedPath = path.join(__dirname, 'assets', 'cached');
Promise.all([
TestServer.create(assetsPath, port),
TestServer.createHTTPS(assetsPath, httpsPort)
TestServer.createHTTPS(assetsPath, httpsPort),
]).then(([server, httpsServer]) => {
server.enableHTTPCache(cachedPath);
httpsServer.enableHTTPCache(cachedPath);

View File

@ -15,115 +15,132 @@
*/
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('Screenshots', function() {
describe('Screenshots', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describe('Page.screenshot', function() {
itFailsFirefox('should work', async() => {
const {page, server} = getTestState();
describe('Page.screenshot', function () {
itFailsFirefox('should work', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const screenshot = await page.screenshot();
expect(screenshot).toBeGolden('screenshot-sanity.png');
});
itFailsFirefox('should clip rect', async() => {
const {page, server} = getTestState();
itFailsFirefox('should clip rect', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const screenshot = await page.screenshot({
clip: {
x: 50,
y: 100,
width: 150,
height: 100
}
height: 100,
},
});
expect(screenshot).toBeGolden('screenshot-clip-rect.png');
});
itFailsFirefox('should clip elements to the viewport', async() => {
const {page, server} = getTestState();
itFailsFirefox('should clip elements to the viewport', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const screenshot = await page.screenshot({
clip: {
x: 50,
y: 600,
width: 100,
height: 100
}
height: 100,
},
});
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
});
it('should run in parallel', async() => {
const {page, server} = getTestState();
it('should run in parallel', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const promises = [];
for (let i = 0; i < 3; ++i) {
promises.push(page.screenshot({
clip: {
x: 50 * i,
y: 0,
width: 50,
height: 50
}
}));
promises.push(
page.screenshot({
clip: {
x: 50 * i,
y: 0,
width: 50,
height: 50,
},
})
);
}
const screenshots = await Promise.all(promises);
expect(screenshots[1]).toBeGolden('grid-cell-1.png');
});
itFailsFirefox('should take fullPage screenshots', async() => {
const {page, server} = getTestState();
itFailsFirefox('should take fullPage screenshots', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const screenshot = await page.screenshot({
fullPage: true
fullPage: true,
});
expect(screenshot).toBeGolden('screenshot-grid-fullpage.png');
});
it('should run in parallel in multiple pages', async() => {
const {server, context} = getTestState();
it('should run in parallel in multiple pages', async () => {
const { server, context } = getTestState();
const N = 2;
const pages = await Promise.all(Array(N).fill(0).map(async() => {
const page = await context.newPage();
await page.goto(server.PREFIX + '/grid.html');
return page;
}));
const pages = await Promise.all(
Array(N)
.fill(0)
.map(async () => {
const page = await context.newPage();
await page.goto(server.PREFIX + '/grid.html');
return page;
})
);
const promises = [];
for (let i = 0; i < N; ++i)
promises.push(pages[i].screenshot({clip: {x: 50 * i, y: 0, width: 50, height: 50}}));
promises.push(
pages[i].screenshot({
clip: { x: 50 * i, y: 0, width: 50, height: 50 },
})
);
const screenshots = await Promise.all(promises);
for (let i = 0; i < N; ++i)
expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
await Promise.all(pages.map(page => page.close()));
await Promise.all(pages.map((page) => page.close()));
});
itFailsFirefox('should allow transparency', async() => {
const {page, server} = getTestState();
itFailsFirefox('should allow transparency', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 100, height: 100});
await page.setViewport({ width: 100, height: 100 });
await page.goto(server.EMPTY_PAGE);
const screenshot = await page.screenshot({omitBackground: true});
const screenshot = await page.screenshot({ omitBackground: true });
expect(screenshot).toBeGolden('transparent.png');
});
itFailsFirefox('should render white background on jpeg file', async() => {
const {page, server} = getTestState();
itFailsFirefox('should render white background on jpeg file', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 100, height: 100});
await page.setViewport({ width: 100, height: 100 });
await page.goto(server.EMPTY_PAGE);
const screenshot = await page.screenshot({omitBackground: true, type: 'jpeg'});
const screenshot = await page.screenshot({
omitBackground: true,
type: 'jpeg',
});
expect(screenshot).toBeGolden('white.jpg');
});
it('should work with odd clip size on Retina displays', async() => {
const {page} = getTestState();
it('should work with odd clip size on Retina displays', async () => {
const { page } = getTestState();
const screenshot = await page.screenshot({
clip: {
@ -131,37 +148,39 @@ describe('Screenshots', function() {
y: 0,
width: 11,
height: 11,
}
},
});
expect(screenshot).toBeGolden('screenshot-clip-odd-size.png');
});
itFailsFirefox('should return base64', async() => {
const {page, server} = getTestState();
itFailsFirefox('should return base64', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
const screenshot = await page.screenshot({
encoding: 'base64'
encoding: 'base64',
});
expect(Buffer.from(screenshot, 'base64')).toBeGolden('screenshot-sanity.png');
expect(Buffer.from(screenshot, 'base64')).toBeGolden(
'screenshot-sanity.png'
);
});
});
describe('ElementHandle.screenshot', function() {
it('should work', async() => {
const {page, server} = getTestState();
describe('ElementHandle.screenshot', function () {
it('should work', async () => {
const { page, server } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
await page.evaluate(() => window.scrollBy(50, 100));
const elementHandle = await page.$('.box:nth-of-type(3)');
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-bounding-box.png');
});
itFailsFirefox('should take into account padding and border', async() => {
const {page} = getTestState();
itFailsFirefox('should take into account padding and border', async () => {
const { page } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.setContent(`
something above
<style>div {
@ -177,12 +196,14 @@ describe('Screenshots', function() {
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
});
itFailsFirefox('should capture full element when larger than viewport', async() => {
const {page} = getTestState();
itFailsFirefox(
'should capture full element when larger than viewport',
async () => {
const { page } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.setContent(`
await page.setContent(`
something above
<style>
div.to-screenshot {
@ -197,16 +218,24 @@ describe('Screenshots', function() {
</style>
<div class="to-screenshot"></div>
`);
const elementHandle = await page.$('div.to-screenshot');
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-larger-than-viewport.png');
const elementHandle = await page.$('div.to-screenshot');
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden(
'screenshot-element-larger-than-viewport.png'
);
expect(await page.evaluate(() => ({w: window.innerWidth, h: window.innerHeight}))).toEqual({w: 500, h: 500});
});
itFailsFirefox('should scroll element into view', async() => {
const {page} = getTestState();
expect(
await page.evaluate(() => ({
w: window.innerWidth,
h: window.innerHeight,
}))
).toEqual({ w: 500, h: 500 });
}
);
itFailsFirefox('should scroll element into view', async () => {
const { page } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.setContent(`
something above
<style>div.above {
@ -226,12 +255,14 @@ describe('Screenshots', function() {
`);
const elementHandle = await page.$('div.to-screenshot');
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-scrolled-into-view.png');
expect(screenshot).toBeGolden(
'screenshot-element-scrolled-into-view.png'
);
});
itFailsFirefox('should work with a rotated element', async() => {
const {page} = getTestState();
itFailsFirefox('should work with a rotated element', async () => {
const { page } = getTestState();
await page.setViewport({width: 500, height: 500});
await page.setViewport({ width: 500, height: 500 });
await page.setContent(`<div style="position:absolute;
top: 100px;
left: 100px;
@ -243,35 +274,49 @@ describe('Screenshots', function() {
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-rotate.png');
});
itFailsFirefox('should fail to screenshot a detached element', async() => {
const {page} = getTestState();
itFailsFirefox('should fail to screenshot a detached element', async () => {
const { page } = getTestState();
await page.setContent('<h1>remove this</h1>');
const elementHandle = await page.$('h1');
await page.evaluate(element => element.remove(), elementHandle);
const screenshotError = await elementHandle.screenshot().catch(error => error);
expect(screenshotError.message).toBe('Node is either not visible or not an HTMLElement');
await page.evaluate((element) => element.remove(), elementHandle);
const screenshotError = await elementHandle
.screenshot()
.catch((error) => error);
expect(screenshotError.message).toBe(
'Node is either not visible or not an HTMLElement'
);
});
itFailsFirefox('should not hang with zero width/height element', async() => {
const {page} = getTestState();
itFailsFirefox(
'should not hang with zero width/height element',
async () => {
const { page } = getTestState();
await page.setContent('<div style="width: 50px; height: 0"></div>');
const div = await page.$('div');
const error = await div.screenshot().catch(error_ => error_);
expect(error.message).toBe('Node has 0 height.');
});
itFailsFirefox('should work for an element with fractional dimensions', async() => {
const {page} = getTestState();
await page.setContent('<div style="width: 50px; height: 0"></div>');
const div = await page.$('div');
const error = await div.screenshot().catch((error_) => error_);
expect(error.message).toBe('Node has 0 height.');
}
);
itFailsFirefox(
'should work for an element with fractional dimensions',
async () => {
const { page } = getTestState();
await page.setContent('<div style="width:48.51px;height:19.8px;border:1px solid black;"></div>');
const elementHandle = await page.$('div');
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-fractional.png');
});
itFailsFirefox('should work for an element with an offset', async() => {
const {page} = getTestState();
await page.setContent(
'<div style="width:48.51px;height:19.8px;border:1px solid black;"></div>'
);
const elementHandle = await page.$('div');
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-fractional.png');
}
);
itFailsFirefox('should work for an element with an offset', async () => {
const { page } = getTestState();
await page.setContent('<div style="position:absolute; top: 10.3px; left: 20.4px;width:50.3px;height:20.2px;border:1px solid black;"></div>');
await page.setContent(
'<div style="position:absolute; top: 10.3px; left: 20.4px;width:50.3px;height:20.2px;border:1px solid black;"></div>'
);
const elementHandle = await page.$('div');
const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png');

View File

@ -15,25 +15,32 @@
*/
const utils = require('./utils');
const {waitEvent} = utils;
const { waitEvent } = utils;
const expect = require('expect');
const {getTestState, setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('Target', function() {
describe('Target', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('Browser.targets should return all of the targets', async() => {
const {browser} = getTestState();
it('Browser.targets should return all of the targets', async () => {
const { browser } = getTestState();
// The pages will be the testing page and the original newtab page
const targets = browser.targets();
expect(targets.some(target => target.type() === 'page' &&
target.url() === 'about:blank')).toBeTruthy();
expect(targets.some(target => target.type() === 'browser')).toBeTruthy();
expect(
targets.some(
(target) => target.type() === 'page' && target.url() === 'about:blank'
)
).toBeTruthy();
expect(targets.some((target) => target.type() === 'browser')).toBeTruthy();
});
it('Browser.pages should return all of the pages', async() => {
const {page, context} = getTestState();
it('Browser.pages should return all of the pages', async () => {
const { page, context } = getTestState();
// The pages will be the testing page
const allPages = await context.pages();
@ -41,154 +48,219 @@ describe('Target', function() {
expect(allPages).toContain(page);
expect(allPages[0]).not.toBe(allPages[1]);
});
it('should contain browser target', async() => {
const {browser} = getTestState();
it('should contain browser target', async () => {
const { browser } = getTestState();
const targets = browser.targets();
const browserTarget = targets.find(target => target.type() === 'browser');
const browserTarget = targets.find((target) => target.type() === 'browser');
expect(browserTarget).toBeTruthy();
});
it('should be able to use the default page in the browser', async() => {
const {page, browser} = getTestState();
it('should be able to use the default page in the browser', async () => {
const { page, browser } = getTestState();
// The pages will be the testing page and the original newtab page
const allPages = await browser.pages();
const originalPage = allPages.find(p => p !== page);
expect(await originalPage.evaluate(() => ['Hello', 'world'].join(' '))).toBe('Hello world');
const originalPage = allPages.find((p) => p !== page);
expect(
await originalPage.evaluate(() => ['Hello', 'world'].join(' '))
).toBe('Hello world');
expect(await originalPage.$('body')).toBeTruthy();
});
itFailsFirefox('should report when a new page is created and closed', async() => {
const {page, server, context} = getTestState();
itFailsFirefox(
'should report when a new page is created and closed',
async () => {
const { page, server, context } = getTestState();
const [otherPage] = await Promise.all([
context.waitForTarget(target => target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html').then(target => target.page()),
page.evaluate(url => window.open(url), server.CROSS_PROCESS_PREFIX + '/empty.html'),
]);
expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX);
expect(await otherPage.evaluate(() => ['Hello', 'world'].join(' '))).toBe('Hello world');
expect(await otherPage.$('body')).toBeTruthy();
const [otherPage] = await Promise.all([
context
.waitForTarget(
(target) =>
target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'
)
.then((target) => target.page()),
page.evaluate(
(url) => window.open(url),
server.CROSS_PROCESS_PREFIX + '/empty.html'
),
]);
expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX);
expect(await otherPage.evaluate(() => ['Hello', 'world'].join(' '))).toBe(
'Hello world'
);
expect(await otherPage.$('body')).toBeTruthy();
let allPages = await context.pages();
expect(allPages).toContain(page);
expect(allPages).toContain(otherPage);
let allPages = await context.pages();
expect(allPages).toContain(page);
expect(allPages).toContain(otherPage);
const closePagePromise = new Promise(fulfill => context.once('targetdestroyed', target => fulfill(target.page())));
await otherPage.close();
expect(await closePagePromise).toBe(otherPage);
const closePagePromise = new Promise((fulfill) =>
context.once('targetdestroyed', (target) => fulfill(target.page()))
);
await otherPage.close();
expect(await closePagePromise).toBe(otherPage);
allPages = await Promise.all(context.targets().map(target => target.page()));
expect(allPages).toContain(page);
expect(allPages).not.toContain(otherPage);
});
itFailsFirefox('should report when a service worker is created and destroyed', async() => {
const {page, server, context} = getTestState();
allPages = await Promise.all(
context.targets().map((target) => target.page())
);
expect(allPages).toContain(page);
expect(allPages).not.toContain(otherPage);
}
);
itFailsFirefox(
'should report when a service worker is created and destroyed',
async () => {
const { page, server, context } = getTestState();
await page.goto(server.EMPTY_PAGE);
const createdTarget = new Promise(fulfill => context.once('targetcreated', target => fulfill(target)));
await page.goto(server.EMPTY_PAGE);
const createdTarget = new Promise((fulfill) =>
context.once('targetcreated', (target) => fulfill(target))
);
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
expect((await createdTarget).type()).toBe('service_worker');
expect((await createdTarget).url()).toBe(
server.PREFIX + '/serviceworkers/empty/sw.js'
);
const destroyedTarget = new Promise((fulfill) =>
context.once('targetdestroyed', (target) => fulfill(target))
);
await page.evaluate(() =>
window.registrationPromise.then((registration) =>
registration.unregister()
)
);
expect(await destroyedTarget).toBe(await createdTarget);
}
);
itFailsFirefox('should create a worker from a service worker', async () => {
const { page, server, context } = getTestState();
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
expect((await createdTarget).type()).toBe('service_worker');
expect((await createdTarget).url()).toBe(server.PREFIX + '/serviceworkers/empty/sw.js');
const destroyedTarget = new Promise(fulfill => context.once('targetdestroyed', target => fulfill(target)));
await page.evaluate(() => window.registrationPromise.then(registration => registration.unregister()));
expect(await destroyedTarget).toBe(await createdTarget);
});
itFailsFirefox('should create a worker from a service worker', async() => {
const {page, server, context} = getTestState();
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
const target = await context.waitForTarget(target => target.type() === 'service_worker');
const target = await context.waitForTarget(
(target) => target.type() === 'service_worker'
);
const worker = await target.worker();
expect(await worker.evaluate(() => self.toString())).toBe('[object ServiceWorkerGlobalScope]');
expect(await worker.evaluate(() => self.toString())).toBe(
'[object ServiceWorkerGlobalScope]'
);
});
itFailsFirefox('should create a worker from a shared worker', async() => {
const {page, server, context} = getTestState();
itFailsFirefox('should create a worker from a shared worker', async () => {
const { page, server, context } = getTestState();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
new SharedWorker('data:text/javascript,console.log("hi")');
});
const target = await context.waitForTarget(target => target.type() === 'shared_worker');
const target = await context.waitForTarget(
(target) => target.type() === 'shared_worker'
);
const worker = await target.worker();
expect(await worker.evaluate(() => self.toString())).toBe('[object SharedWorkerGlobalScope]');
expect(await worker.evaluate(() => self.toString())).toBe(
'[object SharedWorkerGlobalScope]'
);
});
itFailsFirefox('should report when a target url changes', async() => {
const {page, server, context} = getTestState();
itFailsFirefox('should report when a target url changes', async () => {
const { page, server, context } = getTestState();
await page.goto(server.EMPTY_PAGE);
let changedTarget = new Promise(fulfill => context.once('targetchanged', target => fulfill(target)));
let changedTarget = new Promise((fulfill) =>
context.once('targetchanged', (target) => fulfill(target))
);
await page.goto(server.CROSS_PROCESS_PREFIX + '/');
expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/');
changedTarget = new Promise(fulfill => context.once('targetchanged', target => fulfill(target)));
changedTarget = new Promise((fulfill) =>
context.once('targetchanged', (target) => fulfill(target))
);
await page.goto(server.EMPTY_PAGE);
expect((await changedTarget).url()).toBe(server.EMPTY_PAGE);
});
itFailsFirefox('should not report uninitialized pages', async() => {
const {context} = getTestState();
itFailsFirefox('should not report uninitialized pages', async () => {
const { context } = getTestState();
let targetChanged = false;
const listener = () => targetChanged = true;
const listener = () => (targetChanged = true);
context.on('targetchanged', listener);
const targetPromise = new Promise(fulfill => context.once('targetcreated', target => fulfill(target)));
const targetPromise = new Promise((fulfill) =>
context.once('targetcreated', (target) => fulfill(target))
);
const newPagePromise = context.newPage();
const target = await targetPromise;
expect(target.url()).toBe('about:blank');
const newPage = await newPagePromise;
const targetPromise2 = new Promise(fulfill => context.once('targetcreated', target => fulfill(target)));
const targetPromise2 = new Promise((fulfill) =>
context.once('targetcreated', (target) => fulfill(target))
);
const evaluatePromise = newPage.evaluate(() => window.open('about:blank'));
const target2 = await targetPromise2;
expect(target2.url()).toBe('about:blank');
await evaluatePromise;
await newPage.close();
expect(targetChanged).toBe(false, 'target should not be reported as changed');
expect(targetChanged).toBe(
false,
'target should not be reported as changed'
);
context.removeListener('targetchanged', listener);
});
itFailsFirefox('should not crash while redirecting if original request was missed', async() => {
const {page, server, context} = getTestState();
itFailsFirefox(
'should not crash while redirecting if original request was missed',
async () => {
const { page, server, context } = getTestState();
let serverResponse = null;
server.setRoute('/one-style.css', (req, res) => serverResponse = res);
// Open a new page. Use window.open to connect to the page later.
await Promise.all([
page.evaluate(url => window.open(url), server.PREFIX + '/one-style.html'),
server.waitForRequest('/one-style.css')
]);
// Connect to the opened page.
const target = await context.waitForTarget(target => target.url().includes('one-style.html'));
const newPage = await target.page();
// Issue a redirect.
serverResponse.writeHead(302, {location: '/injectedstyle.css'});
serverResponse.end();
// Wait for the new page to load.
await waitEvent(newPage, 'load');
// Cleanup.
await newPage.close();
});
itFailsFirefox('should have an opener', async() => {
const {page, server, context} = getTestState();
let serverResponse = null;
server.setRoute('/one-style.css', (req, res) => (serverResponse = res));
// Open a new page. Use window.open to connect to the page later.
await Promise.all([
page.evaluate(
(url) => window.open(url),
server.PREFIX + '/one-style.html'
),
server.waitForRequest('/one-style.css'),
]);
// Connect to the opened page.
const target = await context.waitForTarget((target) =>
target.url().includes('one-style.html')
);
const newPage = await target.page();
// Issue a redirect.
serverResponse.writeHead(302, { location: '/injectedstyle.css' });
serverResponse.end();
// Wait for the new page to load.
await waitEvent(newPage, 'load');
// Cleanup.
await newPage.close();
}
);
itFailsFirefox('should have an opener', async () => {
const { page, server, context } = getTestState();
await page.goto(server.EMPTY_PAGE);
const [createdTarget] = await Promise.all([
new Promise(fulfill => context.once('targetcreated', target => fulfill(target))),
page.goto(server.PREFIX + '/popup/window-open.html')
new Promise((fulfill) =>
context.once('targetcreated', (target) => fulfill(target))
),
page.goto(server.PREFIX + '/popup/window-open.html'),
]);
expect((await createdTarget.page()).url()).toBe(server.PREFIX + '/popup/popup.html');
expect((await createdTarget.page()).url()).toBe(
server.PREFIX + '/popup/popup.html'
);
expect(createdTarget.opener()).toBe(page.target());
expect(page.target().opener()).toBe(null);
});
describe('Browser.waitForTarget', () => {
itFailsFirefox('should wait for a target', async() => {
const {browser, server} = getTestState();
itFailsFirefox('should wait for a target', async () => {
const { browser, server } = getTestState();
let resolved = false;
const targetPromise = browser.waitForTarget(target => target.url() === server.EMPTY_PAGE);
targetPromise.then(() => resolved = true);
const targetPromise = browser.waitForTarget(
(target) => target.url() === server.EMPTY_PAGE
);
targetPromise.then(() => (resolved = true));
const page = await browser.newPage();
expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE);
@ -196,13 +268,15 @@ describe('Target', function() {
expect(await target.page()).toBe(page);
await page.close();
});
it('should timeout waiting for a non-existent target', async() => {
const {browser, server, puppeteer} = getTestState();
it('should timeout waiting for a non-existent target', async () => {
const { browser, server, puppeteer } = getTestState();
let error = null;
await browser.waitForTarget(target => target.url() === server.EMPTY_PAGE, {
timeout: 1
}).catch(error_ => error = error_);
await browser
.waitForTarget((target) => target.url() === server.EMPTY_PAGE, {
timeout: 1,
})
.catch((error_) => (error = error_));
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
});

View File

@ -15,27 +15,34 @@
*/
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describeFailsFirefox('Touchscreen', function() {
describeFailsFirefox('Touchscreen', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should tap the button', async() => {
const {puppeteer, page, server} = getTestState();
it('should tap the button', async () => {
const { puppeteer, page, server } = getTestState();
const iPhone = puppeteer.devices['iPhone 6'];
await page.emulate(iPhone);
await page.goto(server.PREFIX + '/input/button.html');
await page.tap('button');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should report touches', async() => {
const {puppeteer, page, server} = getTestState();
it('should report touches', async () => {
const { puppeteer, page, server } = getTestState();
const iPhone = puppeteer.devices['iPhone 6'];
await page.emulate(iPhone);
await page.goto(server.PREFIX + '/input/touches.html');
const button = await page.$('button');
await button.tap();
expect(await page.evaluate(() => getResult())).toEqual(['Touchstart: 0', 'Touchend: 0']);
expect(await page.evaluate(() => getResult())).toEqual([
'Touchstart: 0',
'Touchend: 0',
]);
});
});

View File

@ -17,9 +17,9 @@
const fs = require('fs');
const path = require('path');
const expect = require('expect');
const {getTestState} = require('./mocha-utils');
const { getTestState } = require('./mocha-utils');
describeChromeOnly('Tracing', function() {
describeChromeOnly('Tracing', function () {
let outputFile;
let browser;
let page;
@ -28,14 +28,14 @@ describeChromeOnly('Tracing', function() {
* individual test, which isn't the default behaviour of getTestState()
*/
beforeEach(async() => {
const {defaultBrowserOptions, puppeteer} = getTestState();
beforeEach(async () => {
const { defaultBrowserOptions, puppeteer } = getTestState();
browser = await puppeteer.launch(defaultBrowserOptions);
page = await browser.newPage();
outputFile = path.join(__dirname, 'assets', 'trace.json');
});
afterEach(async() => {
afterEach(async () => {
await browser.close();
browser = null;
page = null;
@ -44,42 +44,49 @@ describeChromeOnly('Tracing', function() {
outputFile = null;
}
});
it('should output a trace', async() => {
const {server} = getTestState();
it('should output a trace', async () => {
const { server } = getTestState();
await page.tracing.start({screenshots: true, path: outputFile});
await page.tracing.start({ screenshots: true, path: outputFile });
await page.goto(server.PREFIX + '/grid.html');
await page.tracing.stop();
expect(fs.existsSync(outputFile)).toBe(true);
});
it('should run with custom categories if provided', async() => {
await page.tracing.start({path: outputFile, categories: ['disabled-by-default-v8.cpu_profiler.hires']});
it('should run with custom categories if provided', async () => {
await page.tracing.start({
path: outputFile,
categories: ['disabled-by-default-v8.cpu_profiler.hires'],
});
await page.tracing.stop();
const traceJson = JSON.parse(fs.readFileSync(outputFile));
expect(traceJson.metadata['trace-config']).toContain('disabled-by-default-v8.cpu_profiler.hires');
expect(traceJson.metadata['trace-config']).toContain(
'disabled-by-default-v8.cpu_profiler.hires'
);
});
it('should throw if tracing on two pages', async() => {
await page.tracing.start({path: outputFile});
it('should throw if tracing on two pages', async () => {
await page.tracing.start({ path: outputFile });
const newPage = await browser.newPage();
let error = null;
await newPage.tracing.start({path: outputFile}).catch(error_ => error = error_);
await newPage.tracing
.start({ path: outputFile })
.catch((error_) => (error = error_));
await newPage.close();
expect(error).toBeTruthy();
await page.tracing.stop();
});
it('should return a buffer', async() => {
const {server} = getTestState();
it('should return a buffer', async () => {
const { server } = getTestState();
await page.tracing.start({screenshots: true, path: outputFile});
await page.tracing.start({ screenshots: true, path: outputFile });
await page.goto(server.PREFIX + '/grid.html');
const trace = await page.tracing.stop();
const buf = fs.readFileSync(outputFile);
expect(trace.toString()).toEqual(buf.toString());
});
it('should work without options', async() => {
const {server} = getTestState();
it('should work without options', async () => {
const { server } = getTestState();
await page.tracing.start();
await page.goto(server.PREFIX + '/grid.html');
@ -87,13 +94,13 @@ describeChromeOnly('Tracing', function() {
expect(trace).toBeTruthy();
});
it('should return null in case of Buffer error', async() => {
const {server} = getTestState();
it('should return null in case of Buffer error', async () => {
const { server } = getTestState();
await page.tracing.start({screenshots: true});
await page.tracing.start({ screenshots: true });
await page.goto(server.PREFIX + '/grid.html');
const oldBufferConcat = Buffer.concat;
Buffer.concat = bufs => {
Buffer.concat = (bufs) => {
throw 'error';
};
const trace = await page.tracing.stop();
@ -101,10 +108,10 @@ describeChromeOnly('Tracing', function() {
Buffer.concat = oldBufferConcat;
});
it('should support a buffer without a path', async() => {
const {server} = getTestState();
it('should support a buffer without a path', async () => {
const { server } = getTestState();
await page.tracing.start({screenshots: true});
await page.tracing.start({ screenshots: true });
await page.goto(server.PREFIX + '/grid.html');
const trace = await page.tracing.stop();
expect(trace.toString()).toContain('screenshot');

View File

@ -18,26 +18,33 @@ const fs = require('fs');
const path = require('path');
const expect = require('expect');
const GoldenUtils = require('./golden-utils');
const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..');
const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json'))
? path.join(__dirname, '..')
: path.join(__dirname, '..', '..');
const utils = module.exports = {
extendExpectWithToBeGolden: function(goldenDir, outputDir) {
const utils = (module.exports = {
extendExpectWithToBeGolden: function (goldenDir, outputDir) {
expect.extend({
toBeGolden: (testScreenshot, goldenFilePath) => {
const result = GoldenUtils.compare(goldenDir, outputDir, testScreenshot, goldenFilePath);
const result = GoldenUtils.compare(
goldenDir,
outputDir,
testScreenshot,
goldenFilePath
);
return {
message: () => result.message,
pass: result.pass
pass: result.pass,
};
}
},
});
},
/**
* @return {string}
*/
projectRoot: function() {
projectRoot: function () {
return PROJECT_ROOT;
},
@ -47,7 +54,7 @@ const utils = module.exports = {
* @param {string} url
* @return {!Frame}
*/
attachFrame: async function(page, frameId, url) {
attachFrame: async function (page, frameId, url) {
const handle = await page.evaluateHandle(attachFrame, frameId, url);
return await handle.asElement().contentFrame();
@ -56,12 +63,12 @@ const utils = module.exports = {
frame.src = url;
frame.id = frameId;
document.body.appendChild(frame);
await new Promise(x => frame.onload = x);
await new Promise((x) => (frame.onload = x));
return frame;
}
},
isFavicon: function(request) {
isFavicon: function (request) {
return request.url().includes('favicon.ico');
},
@ -69,7 +76,7 @@ const utils = module.exports = {
* @param {!Page} page
* @param {string} frameId
*/
detachFrame: async function(page, frameId) {
detachFrame: async function (page, frameId) {
await page.evaluate(detachFrame, frameId);
function detachFrame(frameId) {
@ -83,13 +90,13 @@ const utils = module.exports = {
* @param {string} frameId
* @param {string} url
*/
navigateFrame: async function(page, frameId, url) {
navigateFrame: async function (page, frameId, url) {
await page.evaluate(navigateFrame, frameId, url);
function navigateFrame(frameId, url) {
const frame = document.getElementById(frameId);
frame.src = url;
return new Promise(x => frame.onload = x);
return new Promise((x) => (frame.onload = x));
}
},
@ -98,11 +105,10 @@ const utils = module.exports = {
* @param {string=} indentation
* @return {Array<string>}
*/
dumpFrames: function(frame, indentation) {
dumpFrames: function (frame, indentation) {
indentation = indentation || '';
let description = frame.url().replace(/:\d{4}\//, ':<PORT>/');
if (frame.name())
description += ' (' + frame.name() + ')';
if (frame.name()) description += ' (' + frame.name() + ')';
const result = [indentation + description];
for (const child of frame.childFrames())
result.push(...utils.dumpFrames(child, ' ' + indentation));
@ -114,14 +120,13 @@ const utils = module.exports = {
* @param {string} eventName
* @return {!Promise<!Object>}
*/
waitEvent: function(emitter, eventName, predicate = () => true) {
return new Promise(fulfill => {
waitEvent: function (emitter, eventName, predicate = () => true) {
return new Promise((fulfill) => {
emitter.on(eventName, function listener(event) {
if (!predicate(event))
return;
if (!predicate(event)) return;
emitter.removeListener(eventName, listener);
fulfill(event);
});
});
},
};
});

View File

@ -16,18 +16,22 @@
const utils = require('./utils');
const expect = require('expect');
const {getTestState,setupTestBrowserHooks,setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
describe('waittask specs', function() {
describe('waittask specs', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
describe('Page.waitFor', function() {
itFailsFirefox('should wait for selector', async() => {
const {page, server} = getTestState();
describe('Page.waitFor', function () {
itFailsFirefox('should wait for selector', async () => {
const { page, server } = getTestState();
let found = false;
const waitFor = page.waitFor('div').then(() => found = true);
const waitFor = page.waitFor('div').then(() => (found = true));
await page.goto(server.EMPTY_PAGE);
expect(found).toBe(false);
await page.goto(server.PREFIX + '/grid.html');
@ -35,230 +39,259 @@ describe('waittask specs', function() {
expect(found).toBe(true);
});
itFailsFirefox('should wait for an xpath', async() => {
const {page, server} = getTestState();
itFailsFirefox('should wait for an xpath', async () => {
const { page, server } = getTestState();
let found = false;
const waitFor = page.waitFor('//div').then(() => found = true);
const waitFor = page.waitFor('//div').then(() => (found = true));
await page.goto(server.EMPTY_PAGE);
expect(found).toBe(false);
await page.goto(server.PREFIX + '/grid.html');
await waitFor;
expect(found).toBe(true);
});
itFailsFirefox('should not allow you to select an element with single slash xpath', async() => {
const {page} = getTestState();
itFailsFirefox(
'should not allow you to select an element with single slash xpath',
async () => {
const { page } = getTestState();
await page.setContent(`<div>some text</div>`);
let error = null;
await page.waitFor('/html/body/div').catch(error_ => error = error_);
expect(error).toBeTruthy();
});
it('should timeout', async() => {
const {page} = getTestState();
await page.setContent(`<div>some text</div>`);
let error = null;
await page
.waitFor('/html/body/div')
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
}
);
it('should timeout', async () => {
const { page } = getTestState();
const startTime = Date.now();
const timeout = 42;
await page.waitFor(timeout);
expect(Date.now() - startTime).not.toBeLessThan(timeout / 2);
});
it('should work with multiline body', async() => {
const {page} = getTestState();
it('should work with multiline body', async () => {
const { page } = getTestState();
const result = await page.waitForFunction(`
(() => true)()
`);
expect(await result.jsonValue()).toBe(true);
});
it('should wait for predicate', async() => {
const {page} = getTestState();
it('should wait for predicate', async () => {
const { page } = getTestState();
await Promise.all([
page.waitFor(() => window.innerWidth < 100),
page.setViewport({width: 10, height: 10}),
page.setViewport({ width: 10, height: 10 }),
]);
});
it('should throw when unknown type', async() => {
const {page} = getTestState();
it('should throw when unknown type', async () => {
const { page } = getTestState();
let error = null;
await page.waitFor({foo: 'bar'}).catch(error_ => error = error_);
await page.waitFor({ foo: 'bar' }).catch((error_) => (error = error_));
expect(error.message).toContain('Unsupported target type');
});
it('should wait for predicate with arguments', async() => {
const {page} = getTestState();
it('should wait for predicate with arguments', async () => {
const { page } = getTestState();
await page.waitFor((arg1, arg2) => arg1 !== arg2, {}, 1, 2);
});
});
describe('Frame.waitForFunction', function() {
it('should accept a string', async() => {
const {page} = getTestState();
describe('Frame.waitForFunction', function () {
it('should accept a string', async () => {
const { page } = getTestState();
const watchdog = page.waitForFunction('window.__FOO === 1');
await page.evaluate(() => window.__FOO = 1);
await page.evaluate(() => (window.__FOO = 1));
await watchdog;
});
itFailsFirefox('should work when resolved right before execution context disposal', async() => {
const {page} = getTestState();
itFailsFirefox(
'should work when resolved right before execution context disposal',
async () => {
const { page } = getTestState();
await page.evaluateOnNewDocument(() => window.__RELOADED = true);
await page.waitForFunction(() => {
if (!window.__RELOADED)
window.location.reload();
return true;
});
});
it('should poll on interval', async() => {
const {page} = getTestState();
await page.evaluateOnNewDocument(() => (window.__RELOADED = true));
await page.waitForFunction(() => {
if (!window.__RELOADED) window.location.reload();
return true;
});
}
);
it('should poll on interval', async () => {
const { page } = getTestState();
let success = false;
const startTime = Date.now();
const polling = 100;
const watchdog = page.waitForFunction(() => window.__FOO === 'hit', {polling})
.then(() => success = true);
await page.evaluate(() => window.__FOO = 'hit');
const watchdog = page
.waitForFunction(() => window.__FOO === 'hit', { polling })
.then(() => (success = true));
await page.evaluate(() => (window.__FOO = 'hit'));
expect(success).toBe(false);
await page.evaluate(() => document.body.appendChild(document.createElement('div')));
await page.evaluate(() =>
document.body.appendChild(document.createElement('div'))
);
await watchdog;
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
});
it('should poll on mutation', async() => {
const {page} = getTestState();
it('should poll on mutation', async () => {
const { page } = getTestState();
let success = false;
const watchdog = page.waitForFunction(() => window.__FOO === 'hit', {polling: 'mutation'})
.then(() => success = true);
await page.evaluate(() => window.__FOO = 'hit');
const watchdog = page
.waitForFunction(() => window.__FOO === 'hit', { polling: 'mutation' })
.then(() => (success = true));
await page.evaluate(() => (window.__FOO = 'hit'));
expect(success).toBe(false);
await page.evaluate(() => document.body.appendChild(document.createElement('div')));
await page.evaluate(() =>
document.body.appendChild(document.createElement('div'))
);
await watchdog;
});
it('should poll on raf', async() => {
const {page} = getTestState();
it('should poll on raf', async () => {
const { page } = getTestState();
const watchdog = page.waitForFunction(() => window.__FOO === 'hit', {polling: 'raf'});
await page.evaluate(() => window.__FOO = 'hit');
const watchdog = page.waitForFunction(() => window.__FOO === 'hit', {
polling: 'raf',
});
await page.evaluate(() => (window.__FOO = 'hit'));
await watchdog;
});
itFailsFirefox('should work with strict CSP policy', async() => {
const {page, server} = getTestState();
itFailsFirefox('should work with strict CSP policy', async () => {
const { page, server } = getTestState();
server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
await page.goto(server.EMPTY_PAGE);
let error = null;
await Promise.all([
page.waitForFunction(() => window.__FOO === 'hit', {polling: 'raf'}).catch(error_ => error = error_),
page.evaluate(() => window.__FOO = 'hit')
page
.waitForFunction(() => window.__FOO === 'hit', { polling: 'raf' })
.catch((error_) => (error = error_)),
page.evaluate(() => (window.__FOO = 'hit')),
]);
expect(error).toBe(null);
});
it('should throw on bad polling value', async() => {
const {page} = getTestState();
it('should throw on bad polling value', async () => {
const { page } = getTestState();
let error = null;
try {
await page.waitForFunction(() => !!document.body, {polling: 'unknown'});
await page.waitForFunction(() => !!document.body, {
polling: 'unknown',
});
} catch (error_) {
error = error_;
}
expect(error).toBeTruthy();
expect(error.message).toContain('polling');
});
it('should throw negative polling interval', async() => {
const {page} = getTestState();
it('should throw negative polling interval', async () => {
const { page } = getTestState();
let error = null;
try {
await page.waitForFunction(() => !!document.body, {polling: -10});
await page.waitForFunction(() => !!document.body, { polling: -10 });
} catch (error_) {
error = error_;
}
expect(error).toBeTruthy();
expect(error.message).toContain('Cannot poll with non-positive interval');
});
it('should return the success value as a JSHandle', async() => {
const {page} = getTestState();
it('should return the success value as a JSHandle', async () => {
const { page } = getTestState();
expect(await (await page.waitForFunction(() => 5)).jsonValue()).toBe(5);
});
it('should return the window as a success value', async() => {
const {page} = getTestState();
it('should return the window as a success value', async () => {
const { page } = getTestState();
expect(await page.waitForFunction(() => window)).toBeTruthy();
});
itFailsFirefox('should accept ElementHandle arguments', async() => {
const {page} = getTestState();
itFailsFirefox('should accept ElementHandle arguments', async () => {
const { page } = getTestState();
await page.setContent('<div></div>');
const div = await page.$('div');
let resolved = false;
const waitForFunction = page.waitForFunction(element => !element.parentElement, {}, div).then(() => resolved = true);
const waitForFunction = page
.waitForFunction((element) => !element.parentElement, {}, div)
.then(() => (resolved = true));
expect(resolved).toBe(false);
await page.evaluate(element => element.remove(), div);
await page.evaluate((element) => element.remove(), div);
await waitForFunction;
});
it('should respect timeout', async() => {
const {page, puppeteer} = getTestState();
it('should respect timeout', async () => {
const { page, puppeteer } = getTestState();
let error = null;
await page.waitForFunction('false', {timeout: 10}).catch(error_ => error = error_);
await page
.waitForFunction('false', { timeout: 10 })
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for function failed: timeout');
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should respect default timeout', async() => {
const {page, puppeteer} = getTestState();
it('should respect default timeout', async () => {
const { page, puppeteer } = getTestState();
page.setDefaultTimeout(1);
let error = null;
await page.waitForFunction('false').catch(error_ => error = error_);
await page.waitForFunction('false').catch((error_) => (error = error_));
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error.message).toContain('waiting for function failed: timeout');
});
it('should disable timeout when its set to 0', async() => {
const {page} = getTestState();
it('should disable timeout when its set to 0', async () => {
const { page } = getTestState();
const watchdog = page.waitForFunction(() => {
window.__counter = (window.__counter || 0) + 1;
return window.__injected;
}, {timeout: 0, polling: 10});
const watchdog = page.waitForFunction(
() => {
window.__counter = (window.__counter || 0) + 1;
return window.__injected;
},
{ timeout: 0, polling: 10 }
);
await page.waitForFunction(() => window.__counter > 10);
await page.evaluate(() => window.__injected = true);
await page.evaluate(() => (window.__injected = true));
await watchdog;
});
it('should survive cross-process navigation', async() => {
const {page, server} = getTestState();
it('should survive cross-process navigation', async () => {
const { page, server } = getTestState();
let fooFound = false;
const waitForFunction = page.waitForFunction('window.__FOO === 1').then(() => fooFound = true);
const waitForFunction = page
.waitForFunction('window.__FOO === 1')
.then(() => (fooFound = true));
await page.goto(server.EMPTY_PAGE);
expect(fooFound).toBe(false);
await page.reload();
expect(fooFound).toBe(false);
await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
expect(fooFound).toBe(false);
await page.evaluate(() => window.__FOO = 1);
await page.evaluate(() => (window.__FOO = 1));
await waitForFunction;
expect(fooFound).toBe(true);
});
it('should survive navigations', async() => {
const {page, server} = getTestState();
it('should survive navigations', async () => {
const { page, server } = getTestState();
const watchdog = page.waitForFunction(() => window.__done);
await page.goto(server.EMPTY_PAGE);
await page.goto(server.PREFIX + '/consolelog.html');
await page.evaluate(() => window.__done = true);
await page.evaluate(() => (window.__done = true));
await watchdog;
});
});
describeFailsFirefox('Frame.waitForSelector', function() {
const addElement = tag => document.body.appendChild(document.createElement(tag));
describeFailsFirefox('Frame.waitForSelector', function () {
const addElement = (tag) =>
document.body.appendChild(document.createElement(tag));
it('should immediately resolve promise if node exists', async() => {
const {page, server} = getTestState();
it('should immediately resolve promise if node exists', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const frame = page.mainFrame();
@ -267,19 +300,21 @@ describe('waittask specs', function() {
await frame.waitForSelector('div');
});
it('should work with removed MutationObserver', async() => {
const {page} = getTestState();
it('should work with removed MutationObserver', async () => {
const { page } = getTestState();
await page.evaluate(() => delete window.MutationObserver);
const [handle] = await Promise.all([
page.waitForSelector('.zombo'),
page.setContent(`<div class='zombo'>anything</div>`),
]);
expect(await page.evaluate(x => x.textContent, handle)).toBe('anything');
expect(await page.evaluate((x) => x.textContent, handle)).toBe(
'anything'
);
});
it('should resolve promise when node is added', async() => {
const {page, server} = getTestState();
it('should resolve promise when node is added', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const frame = page.mainFrame();
@ -287,22 +322,27 @@ describe('waittask specs', function() {
await frame.evaluate(addElement, 'br');
await frame.evaluate(addElement, 'div');
const eHandle = await watchdog;
const tagName = await eHandle.getProperty('tagName').then(e => e.jsonValue());
const tagName = await eHandle
.getProperty('tagName')
.then((e) => e.jsonValue());
expect(tagName).toBe('DIV');
});
it('should work when node is added through innerHTML', async() => {
const {page, server} = getTestState();
it('should work when node is added through innerHTML', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const watchdog = page.waitForSelector('h3 div');
await page.evaluate(addElement, 'span');
await page.evaluate(() => document.querySelector('span').innerHTML = '<h3><div></div></h3>');
await page.evaluate(
() =>
(document.querySelector('span').innerHTML = '<h3><div></div></h3>')
);
await watchdog;
});
it('Page.waitForSelector is shortcut for main frame', async() => {
const {page, server} = getTestState();
it('Page.waitForSelector is shortcut for main frame', async () => {
const { page, server } = getTestState();
await page.goto(server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
@ -314,8 +354,8 @@ describe('waittask specs', function() {
expect(eHandle.executionContext().frame()).toBe(page.mainFrame());
});
it('should run in specified frame', async() => {
const {page, server} = getTestState();
it('should run in specified frame', async () => {
const { page, server } = getTestState();
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE);
@ -328,23 +368,29 @@ describe('waittask specs', function() {
expect(eHandle.executionContext().frame()).toBe(frame2);
});
it('should throw when frame is detached', async() => {
const {page, server} = getTestState();
it('should throw when frame is detached', async () => {
const { page, server } = getTestState();
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame = page.frames()[1];
let waitError = null;
const waitPromise = frame.waitForSelector('.box').catch(error => waitError = error);
const waitPromise = frame
.waitForSelector('.box')
.catch((error) => (waitError = error));
await utils.detachFrame(page, 'frame1');
await waitPromise;
expect(waitError).toBeTruthy();
expect(waitError.message).toContain('waitForFunction failed: frame got detached.');
expect(waitError.message).toContain(
'waitForFunction failed: frame got detached.'
);
});
it('should survive cross-process navigation', async() => {
const {page, server} = getTestState();
it('should survive cross-process navigation', async () => {
const { page, server } = getTestState();
let boxFound = false;
const waitForSelector = page.waitForSelector('.box').then(() => boxFound = true);
const waitForSelector = page
.waitForSelector('.box')
.then(() => (boxFound = true));
await page.goto(server.EMPTY_PAGE);
expect(boxFound).toBe(false);
await page.reload();
@ -353,140 +399,193 @@ describe('waittask specs', function() {
await waitForSelector;
expect(boxFound).toBe(true);
});
it('should wait for visible', async() => {
const {page} = getTestState();
it('should wait for visible', async () => {
const { page } = getTestState();
let divFound = false;
const waitForSelector = page.waitForSelector('div', {visible: true}).then(() => divFound = true);
await page.setContent(`<div style='display: none; visibility: hidden;'>1</div>`);
const waitForSelector = page
.waitForSelector('div', { visible: true })
.then(() => (divFound = true));
await page.setContent(
`<div style='display: none; visibility: hidden;'>1</div>`
);
expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('display'));
await page.evaluate(() =>
document.querySelector('div').style.removeProperty('display')
);
expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('visibility'));
await page.evaluate(() =>
document.querySelector('div').style.removeProperty('visibility')
);
expect(await waitForSelector).toBe(true);
expect(divFound).toBe(true);
});
it('should wait for visible recursively', async() => {
const {page} = getTestState();
it('should wait for visible recursively', async () => {
const { page } = getTestState();
let divVisible = false;
const waitForSelector = page.waitForSelector('div#inner', {visible: true}).then(() => divVisible = true);
await page.setContent(`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`);
const waitForSelector = page
.waitForSelector('div#inner', { visible: true })
.then(() => (divVisible = true));
await page.setContent(
`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
);
expect(divVisible).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('display'));
await page.evaluate(() =>
document.querySelector('div').style.removeProperty('display')
);
expect(divVisible).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('visibility'));
await page.evaluate(() =>
document.querySelector('div').style.removeProperty('visibility')
);
expect(await waitForSelector).toBe(true);
expect(divVisible).toBe(true);
});
it('hidden should wait for visibility: hidden', async() => {
const {page} = getTestState();
it('hidden should wait for visibility: hidden', async () => {
const { page } = getTestState();
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
const waitForSelector = page.waitForSelector('div', {hidden: true}).then(() => divHidden = true);
const waitForSelector = page
.waitForSelector('div', { hidden: true })
.then(() => (divHidden = true));
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => document.querySelector('div').style.setProperty('visibility', 'hidden'));
await page.evaluate(() =>
document.querySelector('div').style.setProperty('visibility', 'hidden')
);
expect(await waitForSelector).toBe(true);
expect(divHidden).toBe(true);
});
it('hidden should wait for display: none', async() => {
const {page} = getTestState();
it('hidden should wait for display: none', async () => {
const { page } = getTestState();
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
const waitForSelector = page.waitForSelector('div', {hidden: true}).then(() => divHidden = true);
const waitForSelector = page
.waitForSelector('div', { hidden: true })
.then(() => (divHidden = true));
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none'));
await page.evaluate(() =>
document.querySelector('div').style.setProperty('display', 'none')
);
expect(await waitForSelector).toBe(true);
expect(divHidden).toBe(true);
});
it('hidden should wait for removal', async() => {
const {page} = getTestState();
it('hidden should wait for removal', async () => {
const { page } = getTestState();
await page.setContent(`<div></div>`);
let divRemoved = false;
const waitForSelector = page.waitForSelector('div', {hidden: true}).then(() => divRemoved = true);
const waitForSelector = page
.waitForSelector('div', { hidden: true })
.then(() => (divRemoved = true));
await page.waitForSelector('div'); // do a round trip
expect(divRemoved).toBe(false);
await page.evaluate(() => document.querySelector('div').remove());
expect(await waitForSelector).toBe(true);
expect(divRemoved).toBe(true);
});
it('should return null if waiting to hide non-existing element', async() => {
const {page} = getTestState();
it('should return null if waiting to hide non-existing element', async () => {
const { page } = getTestState();
const handle = await page.waitForSelector('non-existing', {hidden: true});
const handle = await page.waitForSelector('non-existing', {
hidden: true,
});
expect(handle).toBe(null);
});
it('should respect timeout', async() => {
const {page, puppeteer} = getTestState();
it('should respect timeout', async () => {
const { page, puppeteer } = getTestState();
let error = null;
await page.waitForSelector('div', {timeout: 10}).catch(error_ => error = error_);
await page
.waitForSelector('div', { timeout: 10 })
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for selector "div" failed: timeout');
expect(error.message).toContain(
'waiting for selector "div" failed: timeout'
);
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should have an error message specifically for awaiting an element to be hidden', async() => {
const {page} = getTestState();
it('should have an error message specifically for awaiting an element to be hidden', async () => {
const { page } = getTestState();
await page.setContent(`<div></div>`);
let error = null;
await page.waitForSelector('div', {hidden: true, timeout: 10}).catch(error_ => error = error_);
await page
.waitForSelector('div', { hidden: true, timeout: 10 })
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for selector "div" to be hidden failed: timeout');
expect(error.message).toContain(
'waiting for selector "div" to be hidden failed: timeout'
);
});
it('should respond to node attribute mutation', async() => {
const {page} = getTestState();
it('should respond to node attribute mutation', async () => {
const { page } = getTestState();
let divFound = false;
const waitForSelector = page.waitForSelector('.zombo').then(() => divFound = true);
const waitForSelector = page
.waitForSelector('.zombo')
.then(() => (divFound = true));
await page.setContent(`<div class='notZombo'></div>`);
expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').className = 'zombo');
await page.evaluate(
() => (document.querySelector('div').className = 'zombo')
);
expect(await waitForSelector).toBe(true);
});
itFailsFirefox('should return the element handle', async() => {
const {page} = getTestState();
itFailsFirefox('should return the element handle', async () => {
const { page } = getTestState();
const waitForSelector = page.waitForSelector('.zombo');
await page.setContent(`<div class='zombo'>anything</div>`);
expect(await page.evaluate(x => x.textContent, await waitForSelector)).toBe('anything');
expect(
await page.evaluate((x) => x.textContent, await waitForSelector)
).toBe('anything');
});
it('should have correct stack trace for timeout', async() => {
const {page} = getTestState();
it('should have correct stack trace for timeout', async () => {
const { page } = getTestState();
let error;
await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => error = error_);
await page
.waitForSelector('.zombo', { timeout: 10 })
.catch((error_) => (error = error_));
expect(error.stack).toContain('waittask.spec.js');
});
});
describeFailsFirefox('Frame.waitForXPath', function() {
const addElement = tag => document.body.appendChild(document.createElement(tag));
describeFailsFirefox('Frame.waitForXPath', function () {
const addElement = (tag) =>
document.body.appendChild(document.createElement(tag));
it('should support some fancy xpath', async() => {
const {page} = getTestState();
it('should support some fancy xpath', async () => {
const { page } = getTestState();
await page.setContent(`<p>red herring</p><p>hello world </p>`);
const waitForXPath = page.waitForXPath('//p[normalize-space(.)="hello world"]');
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('hello world ');
const waitForXPath = page.waitForXPath(
'//p[normalize-space(.)="hello world"]'
);
expect(
await page.evaluate((x) => x.textContent, await waitForXPath)
).toBe('hello world ');
});
it('should respect timeout', async() => {
const {page, puppeteer} = getTestState();
it('should respect timeout', async () => {
const { page, puppeteer } = getTestState();
let error = null;
await page.waitForXPath('//div', {timeout: 10}).catch(error_ => error = error_);
await page
.waitForXPath('//div', { timeout: 10 })
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for XPath "//div" failed: timeout');
expect(error.message).toContain(
'waiting for XPath "//div" failed: timeout'
);
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should run in specified frame', async() => {
const {page, server} = getTestState();
it('should run in specified frame', async () => {
const { page, server } = getTestState();
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE);
@ -498,51 +597,64 @@ describe('waittask specs', function() {
const eHandle = await waitForXPathPromise;
expect(eHandle.executionContext().frame()).toBe(frame2);
});
it('should throw when frame is detached', async() => {
const {page, server} = getTestState();
it('should throw when frame is detached', async () => {
const { page, server } = getTestState();
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame = page.frames()[1];
let waitError = null;
const waitPromise = frame.waitForXPath('//*[@class="box"]').catch(error => waitError = error);
const waitPromise = frame
.waitForXPath('//*[@class="box"]')
.catch((error) => (waitError = error));
await utils.detachFrame(page, 'frame1');
await waitPromise;
expect(waitError).toBeTruthy();
expect(waitError.message).toContain('waitForFunction failed: frame got detached.');
expect(waitError.message).toContain(
'waitForFunction failed: frame got detached.'
);
});
it('hidden should wait for display: none', async() => {
const {page} = getTestState();
it('hidden should wait for display: none', async () => {
const { page } = getTestState();
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
const waitForXPath = page.waitForXPath('//div', {hidden: true}).then(() => divHidden = true);
const waitForXPath = page
.waitForXPath('//div', { hidden: true })
.then(() => (divHidden = true));
await page.waitForXPath('//div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none'));
await page.evaluate(() =>
document.querySelector('div').style.setProperty('display', 'none')
);
expect(await waitForXPath).toBe(true);
expect(divHidden).toBe(true);
});
it('should return the element handle', async() => {
const {page} = getTestState();
it('should return the element handle', async () => {
const { page } = getTestState();
const waitForXPath = page.waitForXPath('//*[@class="zombo"]');
await page.setContent(`<div class='zombo'>anything</div>`);
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('anything');
expect(
await page.evaluate((x) => x.textContent, await waitForXPath)
).toBe('anything');
});
it('should allow you to select a text node', async() => {
const {page} = getTestState();
it('should allow you to select a text node', async () => {
const { page } = getTestState();
await page.setContent(`<div>some text</div>`);
const text = await page.waitForXPath('//div/text()');
expect(await (await text.getProperty('nodeType')).jsonValue()).toBe(3 /* Node.TEXT_NODE */);
expect(await (await text.getProperty('nodeType')).jsonValue()).toBe(
3 /* Node.TEXT_NODE */
);
});
it('should allow you to select an element with single slash', async() => {
const {page} = getTestState();
it('should allow you to select an element with single slash', async () => {
const { page } = getTestState();
await page.setContent(`<div>some text</div>`);
const waitForXPath = page.waitForXPath('/html/body/div');
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('some text');
expect(
await page.evaluate((x) => x.textContent, await waitForXPath)
).toBe('some text');
});
});
});

View File

@ -15,42 +15,57 @@
*/
const expect = require('expect');
const {getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks} = require('./mocha-utils');
const {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} = require('./mocha-utils');
const utils = require('./utils');
const {waitEvent} = utils;
const { waitEvent } = utils;
describeFailsFirefox('Workers', function() {
describeFailsFirefox('Workers', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('Page.workers', async() => {
const {page, server} = getTestState();
it('Page.workers', async () => {
const { page, server } = getTestState();
await Promise.all([
new Promise(x => page.once('workercreated', x)),
page.goto(server.PREFIX + '/worker/worker.html')]);
new Promise((x) => page.once('workercreated', x)),
page.goto(server.PREFIX + '/worker/worker.html'),
]);
const worker = page.workers()[0];
expect(worker.url()).toContain('worker.js');
expect(await worker.evaluate(() => self.workerFunction())).toBe('worker function result');
expect(await worker.evaluate(() => self.workerFunction())).toBe(
'worker function result'
);
await page.goto(server.EMPTY_PAGE);
expect(page.workers().length).toBe(0);
});
it('should emit created and destroyed events', async() => {
const {page} = getTestState();
it('should emit created and destroyed events', async () => {
const { page } = getTestState();
const workerCreatedPromise = new Promise(x => page.once('workercreated', x));
const workerObj = await page.evaluateHandle(() => new Worker('data:text/javascript,1'));
const workerCreatedPromise = new Promise((x) =>
page.once('workercreated', x)
);
const workerObj = await page.evaluateHandle(
() => new Worker('data:text/javascript,1')
);
const worker = await workerCreatedPromise;
const workerThisObj = await worker.evaluateHandle(() => this);
const workerDestroyedPromise = new Promise(x => page.once('workerdestroyed', x));
await page.evaluate(workerObj => workerObj.terminate(), workerObj);
const workerDestroyedPromise = new Promise((x) =>
page.once('workerdestroyed', x)
);
await page.evaluate((workerObj) => workerObj.terminate(), workerObj);
expect(await workerDestroyedPromise).toBe(worker);
const error = await workerThisObj.getProperty('self').catch(error => error);
const error = await workerThisObj
.getProperty('self')
.catch((error) => error);
expect(error.message).toContain('Most likely the worker has been closed.');
});
it('should report console logs', async() => {
const {page} = getTestState();
it('should report console logs', async () => {
const { page } = getTestState();
const [message] = await Promise.all([
waitEvent(page, 'console'),
@ -63,29 +78,40 @@ describeFailsFirefox('Workers', function() {
columnNumber: 8,
});
});
it('should have JSHandles for console logs', async() => {
const {page} = getTestState();
it('should have JSHandles for console logs', async () => {
const { page } = getTestState();
const logPromise = new Promise(x => page.on('console', x));
await page.evaluate(() => new Worker(`data:text/javascript,console.log(1,2,3,this)`));
const logPromise = new Promise((x) => page.on('console', x));
await page.evaluate(
() => new Worker(`data:text/javascript,console.log(1,2,3,this)`)
);
const log = await logPromise;
expect(log.text()).toBe('1 2 3 JSHandle@object');
expect(log.args().length).toBe(4);
expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe('null');
expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe(
'null'
);
});
it('should have an execution context', async() => {
const {page} = getTestState();
it('should have an execution context', async () => {
const { page } = getTestState();
const workerCreatedPromise = new Promise(x => page.once('workercreated', x));
await page.evaluate(() => new Worker(`data:text/javascript,console.log(1)`));
const workerCreatedPromise = new Promise((x) =>
page.once('workercreated', x)
);
await page.evaluate(
() => new Worker(`data:text/javascript,console.log(1)`)
);
const worker = await workerCreatedPromise;
expect(await (await worker.executionContext()).evaluate('1+1')).toBe(2);
});
it('should report errors', async() => {
const {page} = getTestState();
it('should report errors', async () => {
const { page } = getTestState();
const errorPromise = new Promise(x => page.on('pageerror', x));
await page.evaluate(() => new Worker(`data:text/javascript, throw new Error('this is my error');`));
const errorPromise = new Promise((x) => page.on('pageerror', x));
await page.evaluate(
() =>
new Worker(`data:text/javascript, throw new Error('this is my error');`)
);
const errorLog = await errorPromise;
expect(errorLog.message).toContain('this is my error');
});

View File

@ -17,12 +17,15 @@
const child_process = require('child_process');
const path = require('path');
const fs = require('fs');
const {promisify} = require('util');
const { promisify } = require('util');
const exec = promisify(child_process.exec);
const fsAccess = promisify(fs.access);
const fileExists = async filePath => fsAccess(filePath).then(() => true).catch(() => false);
const fileExists = async (filePath) =>
fsAccess(filePath)
.then(() => true)
.catch(() => false);
/*
* Now Puppeteer is built with TypeScript, we need to ensure that
@ -37,7 +40,7 @@ const fileExists = async filePath => fsAccess(filePath).then(() => true).catch((
* place.
*/
async function compileTypeScript() {
return exec('npm run tsc').catch(error => {
return exec('npm run tsc').catch((error) => {
console.error('Error running TypeScript', error);
process.exit(1);
});
@ -53,7 +56,6 @@ async function compileTypeScriptIfRequired() {
}
// It's being run as node typescript-if-required.js, not require('..')
if (require.main === module)
compileTypeScriptIfRequired();
if (require.main === module) compileTypeScriptIfRequired();
module.exports = compileTypeScriptIfRequired;

View File

@ -27,8 +27,7 @@ class ESTreeWalker {
* @param {?ESTree.Node} parent
*/
_innerWalk(node, parent) {
if (!node)
return;
if (!node) return;
node.parent = parent;
if (this._beforeVisit.call(null, node) === ESTreeWalker.SkipSubtree) {
@ -37,8 +36,7 @@ class ESTreeWalker {
}
const walkOrder = ESTreeWalker._walkOrder[node.type];
if (!walkOrder)
return;
if (!walkOrder) return;
if (node.type === 'TemplateLiteral') {
const templateLiteral = /** @type {!ESTree.TemplateLiteralNode} */ (node);
@ -47,14 +45,15 @@ class ESTreeWalker {
this._innerWalk(templateLiteral.quasis[i], templateLiteral);
this._innerWalk(templateLiteral.expressions[i], templateLiteral);
}
this._innerWalk(templateLiteral.quasis[expressionsLength], templateLiteral);
this._innerWalk(
templateLiteral.quasis[expressionsLength],
templateLiteral
);
} else {
for (let i = 0; i < walkOrder.length; ++i) {
const entity = node[walkOrder[i]];
if (Array.isArray(entity))
this._walkArray(entity, node);
else
this._innerWalk(entity, node);
if (Array.isArray(entity)) this._walkArray(entity, node);
else this._innerWalk(entity, node);
}
}
@ -76,61 +75,61 @@ ESTreeWalker.SkipSubtree = {};
/** @enum {!Array.<string>} */
ESTreeWalker._walkOrder = {
'AwaitExpression': ['argument'],
'ArrayExpression': ['elements'],
'ArrowFunctionExpression': ['params', 'body'],
'AssignmentExpression': ['left', 'right'],
'AssignmentPattern': ['left', 'right'],
'BinaryExpression': ['left', 'right'],
'BlockStatement': ['body'],
'BreakStatement': ['label'],
'CallExpression': ['callee', 'arguments'],
'CatchClause': ['param', 'body'],
'ClassBody': ['body'],
'ClassDeclaration': ['id', 'superClass', 'body'],
'ClassExpression': ['id', 'superClass', 'body'],
'ConditionalExpression': ['test', 'consequent', 'alternate'],
'ContinueStatement': ['label'],
'DebuggerStatement': [],
'DoWhileStatement': ['body', 'test'],
'EmptyStatement': [],
'ExpressionStatement': ['expression'],
'ForInStatement': ['left', 'right', 'body'],
'ForOfStatement': ['left', 'right', 'body'],
'ForStatement': ['init', 'test', 'update', 'body'],
'FunctionDeclaration': ['id', 'params', 'body'],
'FunctionExpression': ['id', 'params', 'body'],
'Identifier': [],
'IfStatement': ['test', 'consequent', 'alternate'],
'LabeledStatement': ['label', 'body'],
'Literal': [],
'LogicalExpression': ['left', 'right'],
'MemberExpression': ['object', 'property'],
'MethodDefinition': ['key', 'value'],
'NewExpression': ['callee', 'arguments'],
'ObjectExpression': ['properties'],
'ObjectPattern': ['properties'],
'ParenthesizedExpression': ['expression'],
'Program': ['body'],
'Property': ['key', 'value'],
'ReturnStatement': ['argument'],
'SequenceExpression': ['expressions'],
'Super': [],
'SwitchCase': ['test', 'consequent'],
'SwitchStatement': ['discriminant', 'cases'],
'TaggedTemplateExpression': ['tag', 'quasi'],
'TemplateElement': [],
'TemplateLiteral': ['quasis', 'expressions'],
'ThisExpression': [],
'ThrowStatement': ['argument'],
'TryStatement': ['block', 'handler', 'finalizer'],
'UnaryExpression': ['argument'],
'UpdateExpression': ['argument'],
'VariableDeclaration': ['declarations'],
'VariableDeclarator': ['id', 'init'],
'WhileStatement': ['test', 'body'],
'WithStatement': ['object', 'body'],
'YieldExpression': ['argument']
AwaitExpression: ['argument'],
ArrayExpression: ['elements'],
ArrowFunctionExpression: ['params', 'body'],
AssignmentExpression: ['left', 'right'],
AssignmentPattern: ['left', 'right'],
BinaryExpression: ['left', 'right'],
BlockStatement: ['body'],
BreakStatement: ['label'],
CallExpression: ['callee', 'arguments'],
CatchClause: ['param', 'body'],
ClassBody: ['body'],
ClassDeclaration: ['id', 'superClass', 'body'],
ClassExpression: ['id', 'superClass', 'body'],
ConditionalExpression: ['test', 'consequent', 'alternate'],
ContinueStatement: ['label'],
DebuggerStatement: [],
DoWhileStatement: ['body', 'test'],
EmptyStatement: [],
ExpressionStatement: ['expression'],
ForInStatement: ['left', 'right', 'body'],
ForOfStatement: ['left', 'right', 'body'],
ForStatement: ['init', 'test', 'update', 'body'],
FunctionDeclaration: ['id', 'params', 'body'],
FunctionExpression: ['id', 'params', 'body'],
Identifier: [],
IfStatement: ['test', 'consequent', 'alternate'],
LabeledStatement: ['label', 'body'],
Literal: [],
LogicalExpression: ['left', 'right'],
MemberExpression: ['object', 'property'],
MethodDefinition: ['key', 'value'],
NewExpression: ['callee', 'arguments'],
ObjectExpression: ['properties'],
ObjectPattern: ['properties'],
ParenthesizedExpression: ['expression'],
Program: ['body'],
Property: ['key', 'value'],
ReturnStatement: ['argument'],
SequenceExpression: ['expressions'],
Super: [],
SwitchCase: ['test', 'consequent'],
SwitchStatement: ['discriminant', 'cases'],
TaggedTemplateExpression: ['tag', 'quasi'],
TemplateElement: [],
TemplateLiteral: ['quasis', 'expressions'],
ThisExpression: [],
ThrowStatement: ['argument'],
TryStatement: ['block', 'handler', 'finalizer'],
UnaryExpression: ['argument'],
UpdateExpression: ['argument'],
VariableDeclaration: ['declarations'],
VariableDeclarator: ['id', 'init'],
WhileStatement: ['test', 'body'],
WithStatement: ['object', 'body'],
YieldExpression: ['argument'],
};
module.exports = ESTreeWalker;

View File

@ -5,7 +5,9 @@ const execSync = require('child_process').execSync;
// Compare current HEAD to upstream master SHA.
// If they are not equal - refuse to publish since
// we're not tip-of-tree.
const upstream_sha = execSync(`git ls-remote https://github.com/puppeteer/puppeteer --tags master | cut -f1`).toString('utf8');
const upstream_sha = execSync(
`git ls-remote https://github.com/puppeteer/puppeteer --tags master | cut -f1`
).toString('utf8');
const current_sha = execSync(`git rev-parse HEAD`).toString('utf8');
if (upstream_sha.trim() !== current_sha.trim()) {
console.log('REFUSING TO PUBLISH: this is not tip-of-tree!');
@ -13,13 +15,14 @@ if (upstream_sha.trim() !== current_sha.trim()) {
return;
}
const package = require('../package.json');
let version = package.version;
const dashIndex = version.indexOf('-');
if (dashIndex !== -1)
version = version.substring(0, dashIndex);
if (dashIndex !== -1) version = version.substring(0, dashIndex);
version += '-next.' + Date.now();
console.log('Setting version to ' + version);
package.version = version;
fs.writeFileSync(path.join(__dirname, '..', 'package.json'), JSON.stringify(package, undefined, 2) + '\n');
fs.writeFileSync(
path.join(__dirname, '..', 'package.json'),
JSON.stringify(package, undefined, 2) + '\n'
);

View File

@ -21,7 +21,7 @@ const pptr = require('..');
const browserFetcher = pptr.createBrowserFetcher();
const path = require('path');
const fs = require('fs');
const {fork} = require('child_process');
const { fork } = require('child_process');
const COLOR_RESET = '\x1b[0m';
const COLOR_RED = '\x1b[31m';
@ -49,39 +49,49 @@ if (argv.h || argv.help) {
}
if (typeof argv.good !== 'number') {
console.log(COLOR_RED + 'ERROR: expected --good argument to be a number' + COLOR_RESET);
console.log(
COLOR_RED + 'ERROR: expected --good argument to be a number' + COLOR_RESET
);
console.log(help);
process.exit(1);
}
if (typeof argv.bad !== 'number') {
console.log(COLOR_RED + 'ERROR: expected --bad argument to be a number' + COLOR_RESET);
console.log(
COLOR_RED + 'ERROR: expected --bad argument to be a number' + COLOR_RESET
);
console.log(help);
process.exit(1);
}
const scriptPath = path.resolve(argv._[0]);
if (!fs.existsSync(scriptPath)) {
console.log(COLOR_RED + 'ERROR: Expected to be given a path to a script to run' + COLOR_RESET);
console.log(
COLOR_RED +
'ERROR: Expected to be given a path to a script to run' +
COLOR_RESET
);
console.log(help);
process.exit(1);
}
(async(scriptPath, good, bad) => {
(async (scriptPath, good, bad) => {
const span = Math.abs(good - bad);
console.log(`Bisecting ${COLOR_YELLOW}${span}${COLOR_RESET} revisions in ${COLOR_YELLOW}~${span.toString(2).length}${COLOR_RESET} iterations`);
console.log(
`Bisecting ${COLOR_YELLOW}${span}${COLOR_RESET} revisions in ${COLOR_YELLOW}~${
span.toString(2).length
}${COLOR_RESET} iterations`
);
while (true) {
const middle = Math.round((good + bad) / 2);
const revision = await findDownloadableRevision(middle, good, bad);
if (!revision || revision === good || revision === bad)
break;
if (!revision || revision === good || revision === bad) break;
let info = browserFetcher.revisionInfo(revision);
const shouldRemove = !info.local;
info = await downloadRevision(revision);
const exitCode = await runScript(scriptPath, info);
if (shouldRemove)
await browserFetcher.remove(revision);
if (shouldRemove) await browserFetcher.remove(revision);
let outcome;
if (exitCode) {
bad = revision;
@ -100,14 +110,20 @@ if (!fs.existsSync(scriptPath)) {
fromText = COLOR_RED + bad + COLOR_RESET;
toText = COLOR_GREEN + good + COLOR_RESET;
}
console.log(`- ${COLOR_YELLOW}r${revision}${COLOR_RESET} was ${outcome}. Bisecting [${fromText}, ${toText}] - ${COLOR_YELLOW}${span}${COLOR_RESET} revisions and ${COLOR_YELLOW}~${span.toString(2).length}${COLOR_RESET} iterations`);
console.log(
`- ${COLOR_YELLOW}r${revision}${COLOR_RESET} was ${outcome}. Bisecting [${fromText}, ${toText}] - ${COLOR_YELLOW}${span}${COLOR_RESET} revisions and ${COLOR_YELLOW}~${
span.toString(2).length
}${COLOR_RESET} iterations`
);
}
const [fromSha, toSha] = await Promise.all([
revisionToSha(Math.min(good, bad)),
revisionToSha(Math.max(good, bad)),
]);
console.log(`RANGE: https://chromium.googlesource.com/chromium/src/+log/${fromSha}..${toSha}`);
console.log(
`RANGE: https://chromium.googlesource.com/chromium/src/+log/${fromSha}..${toSha}`
);
})(scriptPath, argv.good, argv.bad);
function runScript(scriptPath, revisionInfo) {
@ -121,8 +137,8 @@ function runScript(scriptPath, revisionInfo) {
},
});
return new Promise((resolve, reject) => {
child.on('error', err => reject(err));
child.on('exit', code => resolve(code));
child.on('error', (err) => reject(err));
child.on('exit', (code) => resolve(code));
});
}
@ -131,20 +147,28 @@ async function downloadRevision(revision) {
log(`Downloading ${revision}`);
let progressBar = null;
let lastDownloadedBytes = 0;
return await browserFetcher.download(revision, (downloadedBytes, totalBytes) => {
if (!progressBar) {
const ProgressBar = require('progress');
progressBar = new ProgressBar(`- downloading Chromium r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
});
return await browserFetcher.download(
revision,
(downloadedBytes, totalBytes) => {
if (!progressBar) {
const ProgressBar = require('progress');
progressBar = new ProgressBar(
`- downloading Chromium r${revision} - ${toMegabytes(
totalBytes
)} [:bar] :percent :etas `,
{
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
}
);
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
});
);
function toMegabytes(bytes) {
const mb = bytes / 1024 / 1024;
return `${Math.round(mb * 10) / 10} Mb`;
@ -156,8 +180,7 @@ async function findDownloadableRevision(rev, from, to) {
const min = Math.min(from, to);
const max = Math.max(from, to);
log(`Looking around ${rev} from [${min}, ${max}]`);
if (await browserFetcher.canDownload(rev))
return rev;
if (await browserFetcher.canDownload(rev)) return rev;
let down = rev;
let up = rev;
while (min <= down || up <= max) {
@ -165,10 +188,8 @@ async function findDownloadableRevision(rev, from, to) {
down > min ? probe(--down) : Promise.resolve(false),
up < max ? probe(++up) : Promise.resolve(false),
]);
if (downOk)
return down;
if (upOk)
return up;
if (downOk) return down;
if (upOk) return up;
}
return null;
@ -180,25 +201,29 @@ async function findDownloadableRevision(rev, from, to) {
}
async function revisionToSha(revision) {
const json = await fetchJSON('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/' + revision);
const json = await fetchJSON(
'https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/' + revision
);
return json.git_sha;
}
function fetchJSON(url) {
return new Promise((resolve, reject) => {
const agent = url.startsWith('https://') ? require('https') : require('http');
const agent = url.startsWith('https://')
? require('https')
: require('http');
const options = URL.parse(url);
options.method = 'GET';
options.headers = {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
};
const req = agent.request(options, function(res) {
const req = agent.request(options, function (res) {
let result = '';
res.setEncoding('utf8');
res.on('data', chunk => result += chunk);
res.on('data', (chunk) => (result += chunk));
res.on('end', () => resolve(JSON.parse(result)));
});
req.on('error', err => reject(err));
req.on('error', (err) => reject(err));
req.end();
});
}

View File

@ -20,26 +20,28 @@ const puppeteer = require('..');
const https = require('https');
const SUPPORTER_PLATFORMS = ['linux', 'mac', 'win32', 'win64'];
const fetchers = SUPPORTER_PLATFORMS.map(platform => puppeteer.createBrowserFetcher({platform}));
const fetchers = SUPPORTER_PLATFORMS.map((platform) =>
puppeteer.createBrowserFetcher({ platform })
);
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m'
yellow: '\x1b[33m',
};
class Table {
/**
* @param {!Array<number>} columnWidths
*/
* @param {!Array<number>} columnWidths
*/
constructor(columnWidths) {
this.widths = columnWidths;
}
/**
* @param {!Array<string>} values
*/
* @param {!Array<string>} values
*/
drawRow(values) {
assert(values.length === this.widths.length);
let row = '';
@ -64,15 +66,29 @@ Running command without arguments will check against omahaproxy revisions.`);
const fromRevision = parseInt(process.argv[2], 10);
const toRevision = parseInt(process.argv[3], 10);
checkRangeAvailability(fromRevision, toRevision, false /* stopWhenAllAvailable */);
checkRangeAvailability(
fromRevision,
toRevision,
false /* stopWhenAllAvailable */
);
async function checkOmahaProxyAvailability() {
const lastchanged = (await Promise.all([
fetch('https://storage.googleapis.com/chromium-browser-snapshots/Mac/LAST_CHANGE'),
fetch('https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/LAST_CHANGE'),
fetch('https://storage.googleapis.com/chromium-browser-snapshots/Win/LAST_CHANGE'),
fetch('https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/LAST_CHANGE'),
])).map(s => parseInt(s, 10));
const lastchanged = (
await Promise.all([
fetch(
'https://storage.googleapis.com/chromium-browser-snapshots/Mac/LAST_CHANGE'
),
fetch(
'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/LAST_CHANGE'
),
fetch(
'https://storage.googleapis.com/chromium-browser-snapshots/Win/LAST_CHANGE'
),
fetch(
'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/LAST_CHANGE'
),
])
).map((s) => parseInt(s, 10));
const from = Math.max(...lastchanged);
checkRangeAvailability(from, 0, true /* stopWhenAllAvailable */);
}
@ -82,14 +98,21 @@ async function checkOmahaProxyAvailability() {
* @param {number} toRevision
* @param {boolean} stopWhenAllAvailable
*/
async function checkRangeAvailability(fromRevision, toRevision, stopWhenAllAvailable) {
async function checkRangeAvailability(
fromRevision,
toRevision,
stopWhenAllAvailable
) {
const table = new Table([10, 7, 7, 7, 7]);
table.drawRow([''].concat(SUPPORTER_PLATFORMS));
const inc = fromRevision < toRevision ? 1 : -1;
for (let revision = fromRevision; revision !== toRevision; revision += inc) {
const allAvailable = await checkAndDrawRevisionAvailability(table, '', revision);
if (allAvailable && stopWhenAllAvailable)
break;
const allAvailable = await checkAndDrawRevisionAvailability(
table,
'',
revision
);
if (allAvailable && stopWhenAllAvailable) break;
}
}
@ -100,10 +123,14 @@ async function checkRangeAvailability(fromRevision, toRevision, stopWhenAllAvail
* @return {boolean}
*/
async function checkAndDrawRevisionAvailability(table, name, revision) {
const promises = fetchers.map(fetcher => fetcher.canDownload(revision));
const promises = fetchers.map((fetcher) => fetcher.canDownload(revision));
const availability = await Promise.all(promises);
const allAvailable = availability.every(e => !!e);
const values = [name + ' ' + (allAvailable ? colors.green + revision + colors.reset : revision)];
const allAvailable = availability.every((e) => !!e);
const values = [
name +
' ' +
(allAvailable ? colors.green + revision + colors.reset : revision),
];
for (let i = 0; i < availability.length; ++i) {
const decoration = availability[i] ? '+' : '-';
const color = availability[i] ? colors.green : colors.red;
@ -119,23 +146,25 @@ async function checkAndDrawRevisionAvailability(table, name, revision) {
*/
function fetch(url) {
let resolve;
const promise = new Promise(x => resolve = x);
https.get(url, response => {
if (response.statusCode !== 200) {
const promise = new Promise((x) => (resolve = x));
https
.get(url, (response) => {
if (response.statusCode !== 200) {
resolve(null);
return;
}
let body = '';
response.on('data', function (chunk) {
body += chunk;
});
response.on('end', function () {
resolve(body);
});
})
.on('error', function (e) {
console.error('Error fetching json: ' + e);
resolve(null);
return;
}
let body = '';
response.on('data', function(chunk){
body += chunk;
});
response.on('end', function(){
resolve(body);
});
}).on('error', function(e){
console.error('Error fetching json: ' + e);
resolve(null);
});
return promise;
}
@ -166,8 +195,7 @@ function filterOutColors(text) {
*/
function padCenter(text, length) {
const printableCharacters = filterOutColors(text);
if (printableCharacters.length >= length)
return text;
if (printableCharacters.length >= length) return text;
const left = Math.floor((length - printableCharacters.length) / 2);
const right = Math.ceil((length - printableCharacters.length) / 2);
return spaceString(left) + text + spaceString(right);

View File

@ -63,8 +63,7 @@ class Source {
* @return {boolean}
*/
setText(text) {
if (text === this._text)
return false;
if (text === this._text) return false;
this._hasUpdatedText = true;
this._text = text;
return true;
@ -94,7 +93,7 @@ class Source {
*/
static async readFile(filePath) {
filePath = path.resolve(filePath);
const text = await readFileAsync(filePath, {encoding: 'utf8'});
const text = await readFileAsync(filePath, { encoding: 'utf8' });
return new Source(filePath, text);
}
@ -105,9 +104,10 @@ class Source {
*/
static async readdir(dirPath, extension = '') {
const fileNames = await readdirAsync(dirPath);
const filePaths = fileNames.filter(fileName => fileName.endsWith(extension)).map(fileName => path.join(dirPath, fileName));
return Promise.all(filePaths.map(filePath => Source.readFile(filePath)));
const filePaths = fileNames
.filter((fileName) => fileName.endsWith(extension))
.map((fileName) => path.join(dirPath, fileName));
return Promise.all(filePaths.map((filePath) => Source.readFile(filePath)));
}
}
module.exports = Source;

View File

@ -22,8 +22,7 @@ class Documentation {
this.classesArray = classesArray;
/** @type {!Map<string, !Documentation.Class>} */
this.classes = new Map();
for (const cls of classesArray)
this.classes.set(cls.name, cls);
for (const cls of classesArray) this.classes.set(cls.name, cls);
}
}
@ -76,7 +75,15 @@ Documentation.Member = class {
* @param {?Documentation.Type} type
* @param {!Array<!Documentation.Member>} argsArray
*/
constructor(kind, name, type, argsArray, comment = '', returnComment = '', required = true) {
constructor(
kind,
name,
type,
argsArray,
comment = '',
returnComment = '',
required = true
) {
this.kind = kind;
this.name = name;
this.type = type;
@ -86,8 +93,7 @@ Documentation.Member = class {
this.required = required;
/** @type {!Map<string, !Documentation.Member>} */
this.args = new Map();
for (const arg of argsArray)
this.args.set(arg.name, arg);
for (const arg of argsArray) this.args.set(arg.name, arg);
}
/**
@ -97,7 +103,14 @@ Documentation.Member = class {
* @return {!Documentation.Member}
*/
static createMethod(name, argsArray, returnType, returnComment, comment) {
return new Documentation.Member('method', name, returnType, argsArray, comment, returnComment);
return new Documentation.Member(
'method',
name,
returnType,
argsArray,
comment,
returnComment
);
}
/**
@ -108,7 +121,15 @@ Documentation.Member = class {
* @return {!Documentation.Member}
*/
static createProperty(name, type, comment, required) {
return new Documentation.Member('property', name, type, [], comment, undefined, required);
return new Documentation.Member(
'property',
name,
type,
[],
comment,
undefined,
required
);
}
/**
@ -134,4 +155,3 @@ Documentation.Type = class {
};
module.exports = Documentation;

View File

@ -3,27 +3,31 @@ const path = require('path');
const Documentation = require('./Documentation');
module.exports = checkSources;
/**
* @param {!Array<!import('../Source')>} sources
*/
function checkSources(sources) {
// special treatment for Events.js
const classEvents = new Map();
const eventsSource = sources.find(source => source.name() === 'Events.js');
const eventsSource = sources.find((source) => source.name() === 'Events.js');
if (eventsSource) {
const {Events} = require(eventsSource.filePath());
const { Events } = require(eventsSource.filePath());
for (const [className, events] of Object.entries(Events))
classEvents.set(className, Array.from(Object.values(events)).filter(e => typeof e === 'string').map(e => Documentation.Member.createEvent(e)));
classEvents.set(
className,
Array.from(Object.values(events))
.filter((e) => typeof e === 'string')
.map((e) => Documentation.Member.createEvent(e))
);
}
const excludeClasses = new Set([]);
const program = ts.createProgram({
options: {
allowJs: true,
target: ts.ScriptTarget.ES2017
target: ts.ScriptTarget.ES2017,
},
rootNames: sources.map(source => source.filePath())
rootNames: sources.map((source) => source.filePath()),
});
const checker = program.getTypeChecker();
const sourceFiles = program.getSourceFiles();
@ -32,11 +36,17 @@ function checkSources(sources) {
/** @type {!Map<string, string>} */
const inheritance = new Map();
const sourceFilesNoNodeModules = sourceFiles.filter(x => !x.fileName.includes('node_modules'));
const sourceFileNamesSet = new Set(sourceFilesNoNodeModules.map(x => x.fileName));
sourceFilesNoNodeModules.map(x => {
const sourceFilesNoNodeModules = sourceFiles.filter(
(x) => !x.fileName.includes('node_modules')
);
const sourceFileNamesSet = new Set(
sourceFilesNoNodeModules.map((x) => x.fileName)
);
sourceFilesNoNodeModules.map((x) => {
if (x.fileName.includes('/lib/')) {
const potentialTSSource = x.fileName.replace('lib', 'src').replace('.js', '.ts');
const potentialTSSource = x.fileName
.replace('lib', 'src')
.replace('.js', '.ts');
if (sourceFileNamesSet.has(potentialTSSource)) {
/* Not going to visit this file because we have the TypeScript src code
* which we'll use instead.
@ -49,9 +59,11 @@ function checkSources(sources) {
});
const errors = [];
const documentation = new Documentation(recreateClassesWithInheritance(classes, inheritance));
const documentation = new Documentation(
recreateClassesWithInheritance(classes, inheritance)
);
return {errors, documentation};
return { errors, documentation };
/**
* @param {!Array<!Documentation.Class>} classes
@ -59,15 +71,14 @@ function checkSources(sources) {
* @return {!Array<!Documentation.Class>}
*/
function recreateClassesWithInheritance(classes, inheritance) {
const classesByName = new Map(classes.map(cls => [cls.name, cls]));
return classes.map(cls => {
const classesByName = new Map(classes.map((cls) => [cls.name, cls]));
return classes.map((cls) => {
const membersMap = new Map();
for (let wp = cls; wp; wp = classesByName.get(inheritance.get(wp.name))) {
for (const member of wp.membersArray) {
// Member was overridden.
const memberId = member.kind + ':' + member.name;
if (membersMap.has(memberId))
continue;
if (membersMap.has(memberId)) continue;
membersMap.set(memberId, member);
}
}
@ -75,26 +86,25 @@ function checkSources(sources) {
});
}
/**
* @param {!ts.Node} node
*/
function visit(node) {
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
const symbol = node.name ? checker.getSymbolAtLocation(node.name) : node.symbol;
const symbol = node.name
? checker.getSymbolAtLocation(node.name)
: node.symbol;
let className = symbol.getName();
if (className === '__class') {
let parent = node;
while (parent.parent)
parent = parent.parent;
className = path.basename(parent.fileName, '.js');
while (parent.parent) parent = parent.parent;
className = path.basename(parent.fileName, '.js');
}
if (className && !excludeClasses.has(className)) {
classes.push(serializeClass(className, symbol, node));
const parentClassName = parentClass(node);
if (parentClassName)
inheritance.set(className, parentClassName);
if (parentClassName) inheritance.set(className, parentClassName);
excludeClasses.add(className);
}
}
@ -109,39 +119,36 @@ function checkSources(sources) {
}
}
return null;
}
function serializeSymbol(symbol, circular = []) {
const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
const type = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration
);
const name = symbol.getName();
if (symbol.valueDeclaration && symbol.valueDeclaration.dotDotDotToken) {
const innerType = serializeType(type.typeArguments[0], circular);
innerType.name = '...' + innerType.name;
return Documentation.Member.createProperty('...' + name, innerType);
}
return Documentation.Member.createProperty(name, serializeType(type, circular));
return Documentation.Member.createProperty(
name,
serializeType(type, circular)
);
}
/**
* @param {!ts.ObjectType} type
*/
function isRegularObject(type) {
if (type.isIntersection())
return true;
if (!type.objectFlags)
return false;
if (!('aliasSymbol' in type))
return false;
if (type.getConstructSignatures().length)
return false;
if (type.getCallSignatures().length)
return false;
if (type.isLiteral())
return false;
if (type.isUnion())
return false;
if (type.isIntersection()) return true;
if (!type.objectFlags) return false;
if (!('aliasSymbol' in type)) return false;
if (type.getConstructSignatures().length) return false;
if (type.getCallSignatures().length) return false;
if (type.isLiteral()) return false;
if (type.isUnion()) return false;
return true;
}
@ -152,34 +159,48 @@ function checkSources(sources) {
*/
function serializeType(type, circular = []) {
let typeName = checker.typeToString(type);
if (typeName === 'any' || typeName === '{ [x: string]: string; }' || typeName === '{}')
if (
typeName === 'any' ||
typeName === '{ [x: string]: string; }' ||
typeName === '{}'
)
typeName = 'Object';
const nextCircular = [typeName].concat(circular);
if (isRegularObject(type)) {
let properties = undefined;
if (!circular.includes(typeName))
properties = type.getProperties().map(property => serializeSymbol(property, nextCircular));
properties = type
.getProperties()
.map((property) => serializeSymbol(property, nextCircular));
return new Documentation.Type('Object', properties);
}
if (type.isUnion() && typeName.includes('|')) {
const types = type.types.map(type => serializeType(type, circular));
const name = types.map(type => type.name).join('|');
const properties = [].concat(...types.map(type => type.properties));
return new Documentation.Type(name.replace(/false\|true/g, 'boolean'), properties);
const types = type.types.map((type) => serializeType(type, circular));
const name = types.map((type) => type.name).join('|');
const properties = [].concat(...types.map((type) => type.properties));
return new Documentation.Type(
name.replace(/false\|true/g, 'boolean'),
properties
);
}
if (type.typeArguments) {
const properties = [];
const innerTypeNames = [];
for (const typeArgument of type.typeArguments) {
const innerType = serializeType(typeArgument, nextCircular);
if (innerType.properties)
properties.push(...innerType.properties);
if (innerType.properties) properties.push(...innerType.properties);
innerTypeNames.push(innerType.name);
}
if (innerTypeNames.length === 0 || innerTypeNames.length === 1 && innerTypeNames[0] === 'void')
if (
innerTypeNames.length === 0 ||
(innerTypeNames.length === 1 && innerTypeNames[0] === 'void')
)
return new Documentation.Type(type.symbol.name);
return new Documentation.Type(`${type.symbol.name}<${innerTypeNames.join(', ')}>`, properties);
return new Documentation.Type(
`${type.symbol.name}<${innerTypeNames.join(', ')}>`,
properties
);
}
return new Documentation.Type(typeName, []);
}
@ -189,8 +210,11 @@ function checkSources(sources) {
* @return {boolean}
*/
function symbolHasPrivateModifier(symbol) {
const modifiers = symbol.valueDeclaration && symbol.valueDeclaration.modifiers || [];
return modifiers.some(modifier => modifier.kind === ts.SyntaxKind.PrivateKeyword);
const modifiers =
(symbol.valueDeclaration && symbol.valueDeclaration.modifiers) || [];
return modifiers.some(
(modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword
);
}
/**
@ -203,20 +227,19 @@ function checkSources(sources) {
const members = classEvents.get(className) || [];
for (const [name, member] of symbol.members || []) {
/* Before TypeScript we denoted private methods with an underscore
* but in TypeScript we use the private keyword
* hence we check for either here.
*/
if (name.startsWith('_') || symbolHasPrivateModifier(member))
continue;
if (name.startsWith('_') || symbolHasPrivateModifier(member)) continue;
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
const memberType = checker.getTypeOfSymbolAtLocation(
member,
member.valueDeclaration
);
const signature = memberType.getCallSignatures()[0];
if (signature)
members.push(serializeSignature(name, signature));
else
members.push(serializeProperty(name, memberType));
if (signature) members.push(serializeSignature(name, signature));
else members.push(serializeProperty(name, memberType));
}
return new Documentation.Class(className, members);
@ -227,9 +250,13 @@ function checkSources(sources) {
* @param {!ts.Signature} signature
*/
function serializeSignature(name, signature) {
const parameters = signature.parameters.map(s => serializeSymbol(s));
const parameters = signature.parameters.map((s) => serializeSymbol(s));
const returnType = serializeType(signature.getReturnType());
return Documentation.Member.createMethod(name, parameters, returnType.name !== 'void' ? returnType : null);
return Documentation.Member.createMethod(
name,
parameters,
returnType.name !== 'void' ? returnType : null
);
}
/**

Some files were not shown because too many files have changed in this diff Show More