chore: add Prettier (#5825)
This commit is contained in:
parent
ae576aff61
commit
4fdb1e3cab
47
.eslintrc.js
47
.eslintrc.js
@ -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",
|
||||
|
||||
|
@ -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();
|
||||
})();
|
||||
|
||||
|
@ -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}`;
|
||||
});
|
||||
|
@ -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();
|
||||
})();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
})();
|
||||
|
@ -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();
|
||||
})();
|
||||
|
@ -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}`;
|
||||
});
|
||||
|
19
index.js
19
index.js
@ -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,
|
||||
|
166
install.js
166
install.js
@ -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();
|
||||
|
||||
|
@ -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
5
prettier.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'es5',
|
||||
singleQuote: true,
|
||||
};
|
@ -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;
|
||||
|
441
src/Browser.ts
441
src/Browser.ts
@ -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> {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
145
src/Coverage.ts
145
src/Coverage.ts
@ -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);
|
||||
}
|
||||
|
||||
|
374
src/DOMWorld.ts
374
src/DOMWorld.ts
@ -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
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
126
src/Input.ts
126
src/Input.ts
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
495
src/JSHandle.ts
495
src/JSHandle.ts
@ -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;
|
||||
|
573
src/Launcher.ts
573
src/Launcher.ts
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
|
929
src/Page.ts
929
src/Page.ts
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
102
src/Target.ts
102
src/Target.ts
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 },
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
177
src/helper.ts
177
src/helper.ts
@ -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,
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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'));
|
||||
}
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
import num from './es6module.js';
|
||||
window.__es6injected = num;
|
||||
window.__es6injected = num;
|
||||
|
@ -1 +1 @@
|
||||
export default 42;
|
||||
export default 42;
|
||||
|
@ -1,2 +1,2 @@
|
||||
import num from './es6/es6module.js';
|
||||
window.__es6injected = num;
|
||||
window.__es6injected = num;
|
||||
|
@ -1,2 +1,2 @@
|
||||
window.__injected = 42;
|
||||
window.__injectedError = new Error('hi');
|
||||
window.__injectedError = new Error('hi');
|
||||
|
@ -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));
|
||||
|
@ -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());
|
||||
});
|
||||
|
@ -1,3 +1,2 @@
|
||||
console.log('hey from the content-script');
|
||||
self.thisIsTheContentScript = true;
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -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?'));
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
2
test/fixtures/closeme.js
vendored
2
test/fixtures/closeme.js
vendored
@ -1,4 +1,4 @@
|
||||
(async() => {
|
||||
(async () => {
|
||||
const [, , puppeteerRoot, options] = process.argv;
|
||||
const browser = await require(puppeteerRoot).launch(JSON.parse(options));
|
||||
console.log(browser.wsEndpoint());
|
||||
|
2
test/fixtures/dumpio.js
vendored
2
test/fixtures/dumpio.js
vendored
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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([
|
||||
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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 });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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');
|
||||
}
|
||||
|
1214
test/page.spec.js
1214
test/page.spec.js
File diff suppressed because it is too large
Load Diff
@ -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([]);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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'
|
||||
);
|
||||
|
103
utils/bisect.js
103
utils/bisect.js
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user