chore: remove obsolete code (#8307)
This commit is contained in:
parent
ac17ce252b
commit
b05ef09e3a
@ -1,17 +0,0 @@
|
||||
FROM node:10.18.1-stretch
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
|
||||
libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
|
||||
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
|
||||
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
|
||||
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add user so we don't need --no-sandbox.
|
||||
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
|
||||
&& mkdir -p /home/pptruser/Downloads \
|
||||
&& chown -R pptruser:pptruser /home/pptruser
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
USER pptruser
|
@ -1,11 +0,0 @@
|
||||
FROM microsoft/windowsservercore:latest
|
||||
|
||||
ENV NODE_VERSION 10.18.1
|
||||
|
||||
RUN setx /m PATH "%PATH%;C:\nodejs"
|
||||
|
||||
RUN powershell -Command \
|
||||
netsh interface ipv4 set subinterface 18 mtu=1460 store=persistent ; \
|
||||
Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \
|
||||
Expand-Archive node.zip -DestinationPath C:\ ; \
|
||||
Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs'
|
@ -1,31 +0,0 @@
|
||||
env:
|
||||
DISPLAY: :99.0
|
||||
|
||||
task:
|
||||
name: node10 (linux)
|
||||
container:
|
||||
dockerfile: .ci/node10/Dockerfile.linux
|
||||
xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24
|
||||
install_script: npm install
|
||||
test_script: npm run fjunit
|
||||
|
||||
task:
|
||||
name: node10 (macOS)
|
||||
osx_instance:
|
||||
image: high-sierra-base
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
node_install_script:
|
||||
- brew install node@10
|
||||
- brew link --force node@10
|
||||
install_script: npm install
|
||||
test_script: npm run fjunit
|
||||
|
||||
# task:
|
||||
# allow_failures: true
|
||||
# windows_container:
|
||||
# dockerfile: .ci/node10/Dockerfile.windows
|
||||
# os_version: 2016
|
||||
# name: node10 (windows)
|
||||
# install_script: npm install --unsafe-perm
|
||||
# test_script: npm run fjunit
|
10
experimental/puppeteer-firefox/.gitignore
vendored
10
experimental/puppeteer-firefox/.gitignore
vendored
@ -1,10 +0,0 @@
|
||||
/node_modules/
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.pyc
|
||||
.vscode
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.local-browser
|
||||
/test/output-chromium
|
||||
/test/output-firefox
|
@ -1,36 +0,0 @@
|
||||
# exclude all tests
|
||||
test
|
||||
utils/node6-transform
|
||||
|
||||
# exclude internal type definition files
|
||||
/lib/*.d.ts
|
||||
/node6/lib/*.d.ts
|
||||
|
||||
# repeats from .gitignore
|
||||
node_modules
|
||||
.local-chromium
|
||||
.local-browser
|
||||
.dev_profile*
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.pyc
|
||||
.vscode
|
||||
package-lock.json
|
||||
/node6/test
|
||||
/node6/utils
|
||||
/test
|
||||
/utils
|
||||
/docs
|
||||
yarn.lock
|
||||
|
||||
# other
|
||||
/.ci
|
||||
/examples
|
||||
.appveyour.yml
|
||||
.cirrus.yml
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc.js
|
||||
README.md
|
||||
tsconfig.json
|
||||
|
@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright 2019 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
module.exports = require('./lib/DeviceDescriptors');
|
@ -1 +0,0 @@
|
||||
module.exports = require('./lib/Errors');
|
@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,48 +0,0 @@
|
||||
<img src="https://user-images.githubusercontent.com/39191/49555713-a07b3c00-f8b5-11e8-8aba-f2d03cd83da5.png" height="200" align="right">
|
||||
|
||||
# Prototype: Puppeteer for Firefox
|
||||
|
||||
**⚠️ The puppeteer-firefox package has been deprecated**: Firefox support is gradually transitioning to the puppeteer package. As of puppeteer v2.1.0 you can interact with Firefox Nightly. The puppeteer-firefox package will remain available until the transition is complete, but it is no longer actively maintained. For more information visit https://wiki.mozilla.org/Remote
|
||||
|
||||
This project is an experimental feasibility prototype to guide the work of implementing Puppeteer endpoints into Firefox's code base. Mozilla's [bug 1545057](https://bugzilla.mozilla.org/show_bug.cgi?id=1545057) tracks the initial milestone, which will be based on a CDP-based [remote protocol](https://wiki.mozilla.org/Remote).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
To try out Puppeteer with Firefox in your project, run:
|
||||
|
||||
```bash
|
||||
npm i puppeteer-firefox
|
||||
# or "yarn add puppeteer-firefox"
|
||||
```
|
||||
|
||||
Note: When you install puppeteer-firefox, it downloads a [custom-built Firefox](https://github.com/puppeteer/juggler) (Firefox/63.0.4) that is guaranteed to work with the API.
|
||||
|
||||
### Usage
|
||||
|
||||
**Example** - navigating to https://example.com and saving a screenshot as `example.png`:
|
||||
|
||||
Save file as **example.js**
|
||||
|
||||
```js
|
||||
const pptrFirefox = require('puppeteer-firefox');
|
||||
|
||||
(async () => {
|
||||
const browser = await pptrFirefox.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://example.com');
|
||||
await page.screenshot({ path: 'example.png' });
|
||||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
Execute script on the command line
|
||||
|
||||
```bash
|
||||
node example.js
|
||||
```
|
||||
|
||||
### Credits
|
||||
|
||||
Special thanks to [Amine Bouhlali](https://bitbucket.org/aminerop/) who volunteered the [`puppeteer-firefox`](https://www.npmjs.com/package/puppeteer-firefox) NPM package.
|
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer-firefox');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('http://example.com');
|
||||
await page.screenshot({path: 'example.png'});
|
||||
await browser.close();
|
||||
})();
|
@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Search developers.google.com/web for articles tagged
|
||||
* "Headless Chrome" and scrape results from the results page.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer-firefox');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto('https://developers.google.com/web/');
|
||||
|
||||
// Type into search box.
|
||||
await page.type('.devsite-searchbox input', 'Headless Chrome');
|
||||
|
||||
// Wait for suggest overlay to appear and click "show all results".
|
||||
const allResultsSelector = '.devsite-suggest-all-results';
|
||||
await page.waitForSelector(allResultsSelector);
|
||||
await page.click(allResultsSelector);
|
||||
|
||||
// Wait for the results page to load and display the results.
|
||||
const resultsSelector = '.gsc-results .gsc-thumbnail-inside a.gs-title';
|
||||
await page.waitForSelector(resultsSelector);
|
||||
|
||||
// Extract the results from the page.
|
||||
const links = await page.evaluate(resultsSelector => {
|
||||
const anchors = Array.from(document.querySelectorAll(resultsSelector));
|
||||
return anchors.map(anchor => {
|
||||
const title = anchor.textContent.split('|')[0].trim();
|
||||
return `${title} - ${anchor.href}`;
|
||||
});
|
||||
}, resultsSelector);
|
||||
console.log(links.join('\n'));
|
||||
|
||||
await browser.close();
|
||||
})();
|
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const {helper} = require('./lib/helper');
|
||||
const api = require('./lib/api');
|
||||
for (const className in api)
|
||||
helper.installAsyncStackHooks(api[className]);
|
||||
|
||||
const {Puppeteer} = require('./lib/Puppeteer');
|
||||
const packageJson = require('./package.json');
|
||||
const preferredRevision = packageJson.puppeteer.firefox_revision;
|
||||
module.exports = new Puppeteer(__dirname, preferredRevision);
|
@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
// puppeteer-core should not install anything.
|
||||
if (require('./package.json').name === 'puppeteer-core')
|
||||
return;
|
||||
|
||||
const downloadHost = process.env.PUPPETEER_DOWNLOAD_HOST || process.env.npm_config_puppeteer_download_host || process.env.npm_package_config_puppeteer_download_host;
|
||||
const downloadPath = process.env.PUPPETEER_DOWNLOAD_PATH || process.env.npm_config_puppeteer_download_path || process.env.npm_package_config_puppeteer_download_path;
|
||||
|
||||
const puppeteer = require('./index');
|
||||
const browserFetcher = puppeteer.createBrowserFetcher({ host: downloadHost, product: 'firefox', path: downloadPath });
|
||||
|
||||
const revision = require('./package.json').puppeteer.firefox_revision;
|
||||
|
||||
const revisionInfo = browserFetcher.revisionInfo(revision);
|
||||
|
||||
// Do nothing if the revision is already downloaded.
|
||||
if (revisionInfo.local)
|
||||
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_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;
|
||||
|
||||
browserFetcher.download(revisionInfo.revision, onProgress)
|
||||
.then(() => browserFetcher.localRevisions())
|
||||
.then(onSuccess)
|
||||
.catch(onError);
|
||||
|
||||
/**
|
||||
* @param {!Array<string>}
|
||||
* @return {!Promise}
|
||||
*/
|
||||
function onSuccess(localRevisions) {
|
||||
console.log('Firefox downloaded to ' + revisionInfo.folderPath);
|
||||
localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
|
||||
// Remove previous firefox revisions.
|
||||
const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));
|
||||
const installFirefoxPreferences = require('./misc/install-preferences');
|
||||
return Promise.all([...cleanupOldVersions, installFirefoxPreferences(revisionInfo.executablePath)]).then(() => {
|
||||
console.log('Firefox preferences installed!');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
*/
|
||||
function onError(error) {
|
||||
console.error(`ERROR: Failed to download Firefox r${revision}!`);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let progressBar = null;
|
||||
let lastDownloadedBytes = 0;
|
||||
function onProgress(downloadedBytes, totalBytes) {
|
||||
if (!progressBar) {
|
||||
const ProgressBar = require('progress');
|
||||
progressBar = new ProgressBar(`Downloading Firefox+Puppeteer ${revision.substring(0, 8)} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
|
||||
complete: '|',
|
||||
incomplete: ' ',
|
||||
width: 20,
|
||||
total: totalBytes,
|
||||
});
|
||||
}
|
||||
const delta = downloadedBytes - lastDownloadedBytes;
|
||||
lastDownloadedBytes = downloadedBytes;
|
||||
progressBar.tick(delta);
|
||||
}
|
||||
|
||||
function toMegabytes(bytes) {
|
||||
const mb = bytes / 1024 / 1024;
|
||||
return `${Math.round(mb * 10) / 10} Mb`;
|
||||
}
|
@ -1,322 +0,0 @@
|
||||
/**
|
||||
* @typedef {Object} SerializedAXNode
|
||||
* @property {string} role
|
||||
*
|
||||
* @property {string=} name
|
||||
* @property {string|number=} value
|
||||
* @property {string=} description
|
||||
*
|
||||
* @property {string=} keyshortcuts
|
||||
* @property {string=} roledescription
|
||||
* @property {string=} valuetext
|
||||
*
|
||||
* @property {boolean=} disabled
|
||||
* @property {boolean=} expanded
|
||||
* @property {boolean=} focused
|
||||
* @property {boolean=} modal
|
||||
* @property {boolean=} multiline
|
||||
* @property {boolean=} multiselectable
|
||||
* @property {boolean=} readonly
|
||||
* @property {boolean=} required
|
||||
* @property {boolean=} selected
|
||||
*
|
||||
* @property {boolean|"mixed"=} checked
|
||||
* @property {boolean|"mixed"=} pressed
|
||||
*
|
||||
* @property {number=} level
|
||||
*
|
||||
* @property {string=} autocomplete
|
||||
* @property {string=} haspopup
|
||||
* @property {string=} invalid
|
||||
* @property {string=} orientation
|
||||
*
|
||||
* @property {Array<SerializedAXNode>=} children
|
||||
*/
|
||||
|
||||
class Accessibility {
|
||||
constructor(session) {
|
||||
this._session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{interestingOnly?: boolean}=} options
|
||||
* @return {!Promise<!SerializedAXNode>}
|
||||
*/
|
||||
async snapshot(options = {}) {
|
||||
const {interestingOnly = true} = options;
|
||||
const {tree} = await this._session.send('Accessibility.getFullAXTree');
|
||||
const root = new AXNode(tree);
|
||||
if (!interestingOnly)
|
||||
return serializeTree(root)[0];
|
||||
|
||||
/** @type {!Set<!AXNode>} */
|
||||
const interestingNodes = new Set();
|
||||
collectInterestingNodes(interestingNodes, root, false);
|
||||
return serializeTree(root, interestingNodes)[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Set<!AXNode>} collection
|
||||
* @param {!AXNode} node
|
||||
* @param {boolean} insideControl
|
||||
*/
|
||||
function collectInterestingNodes(collection, node, insideControl) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!AXNode} node
|
||||
* @param {!Set<!AXNode>=} whitelistedNodes
|
||||
* @return {!Array<!SerializedAXNode>}
|
||||
*/
|
||||
function serializeTree(node, whitelistedNodes) {
|
||||
/** @type {!Array<!SerializedAXNode>} */
|
||||
const children = [];
|
||||
for (const child of node._children)
|
||||
children.push(...serializeTree(child, whitelistedNodes));
|
||||
|
||||
if (whitelistedNodes && !whitelistedNodes.has(node))
|
||||
return children;
|
||||
|
||||
const serializedNode = node.serialize();
|
||||
if (children.length)
|
||||
serializedNode.children = children;
|
||||
return [serializedNode];
|
||||
}
|
||||
|
||||
|
||||
class AXNode {
|
||||
constructor(payload) {
|
||||
this._payload = payload;
|
||||
|
||||
/** @type {!Array<!AXNode>} */
|
||||
this._children = (payload.children || []).map(x => new AXNode(x));
|
||||
|
||||
this._editable = payload.editable;
|
||||
this._richlyEditable = this._editable && (payload.tag !== 'textarea' && payload.tag !== 'input');
|
||||
this._focusable = payload.focusable;
|
||||
this._expanded = payload.expanded;
|
||||
this._name = this._payload.name;
|
||||
this._role = this._payload.role;
|
||||
this._cachedHasFocusableChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isPlainTextField() {
|
||||
if (this._richlyEditable)
|
||||
return false;
|
||||
if (this._editable)
|
||||
return true;
|
||||
return this._role === 'entry';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isTextOnlyObject() {
|
||||
const role = this._role;
|
||||
return (role === 'text leaf' || role === 'text' || role === 'statictext');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_hasFocusableChild() {
|
||||
if (this._cachedHasFocusableChild === undefined) {
|
||||
this._cachedHasFocusableChild = false;
|
||||
for (const child of this._children) {
|
||||
if (child._focusable || child._hasFocusableChild()) {
|
||||
this._cachedHasFocusableChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._cachedHasFocusableChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isLeafNode() {
|
||||
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;
|
||||
|
||||
// Roles whose children are only presentational according to the ARIA and
|
||||
// HTML5 Specs should be hidden from screen readers.
|
||||
// (Note that whilst ARIA buttons can have only presentational children, HTML5
|
||||
// buttons are allowed to have content.)
|
||||
switch (this._role) {
|
||||
case 'graphic':
|
||||
case 'scrollbar':
|
||||
case 'slider':
|
||||
case 'separator':
|
||||
case 'progressbar':
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isControl() {
|
||||
switch (this._role) {
|
||||
case 'checkbutton':
|
||||
case 'check menu item':
|
||||
case 'check rich option':
|
||||
case 'combobox':
|
||||
case 'combobox option':
|
||||
case 'color chooser':
|
||||
case 'listbox':
|
||||
case 'listbox option':
|
||||
case 'listbox rich option':
|
||||
case 'popup menu':
|
||||
case 'menupopup':
|
||||
case 'menuitem':
|
||||
case 'menubar':
|
||||
case 'button':
|
||||
case 'pushbutton':
|
||||
case 'radiobutton':
|
||||
case 'radio menuitem':
|
||||
case 'scrollbar':
|
||||
case 'slider':
|
||||
case 'spinbutton':
|
||||
case 'switch':
|
||||
case 'pagetab':
|
||||
case 'entry':
|
||||
case 'tree table':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} insideControl
|
||||
* @return {boolean}
|
||||
*/
|
||||
isInteresting(insideControl) {
|
||||
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;
|
||||
|
||||
// A non focusable child of a control is not interesting
|
||||
if (insideControl)
|
||||
return false;
|
||||
|
||||
return this.isLeafNode() && !!this._name.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!SerializedAXNode}
|
||||
*/
|
||||
serialize() {
|
||||
/** @type {SerializedAXNode} */
|
||||
const node = {
|
||||
role: this._role
|
||||
};
|
||||
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const userStringProperties = [
|
||||
'name',
|
||||
'value',
|
||||
'description',
|
||||
'roledescription',
|
||||
'valuetext',
|
||||
'keyshortcuts',
|
||||
];
|
||||
for (const userStringProperty of userStringProperties) {
|
||||
if (!(userStringProperty in this._payload))
|
||||
continue;
|
||||
node[userStringProperty] = this._payload[userStringProperty];
|
||||
}
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const booleanProperties = [
|
||||
'disabled',
|
||||
'expanded',
|
||||
'focused',
|
||||
'modal',
|
||||
'multiline',
|
||||
'multiselectable',
|
||||
'readonly',
|
||||
'required',
|
||||
'selected',
|
||||
];
|
||||
for (const booleanProperty of booleanProperties) {
|
||||
if (this._role === 'document' && booleanProperty === 'focused')
|
||||
continue; // document focusing is strange
|
||||
const value = this._payload[booleanProperty];
|
||||
if (!value)
|
||||
continue;
|
||||
node[booleanProperty] = value;
|
||||
}
|
||||
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const tristateProperties = [
|
||||
'checked',
|
||||
'pressed',
|
||||
];
|
||||
for (const tristateProperty of tristateProperties) {
|
||||
if (!(tristateProperty in this._payload))
|
||||
continue;
|
||||
const value = this._payload[tristateProperty];
|
||||
node[tristateProperty] = value;
|
||||
}
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const numericalProperties = [
|
||||
'level',
|
||||
'valuemax',
|
||||
'valuemin',
|
||||
];
|
||||
for (const numericalProperty of numericalProperties) {
|
||||
if (!(numericalProperty in this._payload))
|
||||
continue;
|
||||
node[numericalProperty] = this._payload[numericalProperty];
|
||||
}
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const tokenProperties = [
|
||||
'autocomplete',
|
||||
'haspopup',
|
||||
'invalid',
|
||||
'orientation',
|
||||
];
|
||||
for (const tokenProperty of tokenProperties) {
|
||||
const value = this._payload[tokenProperty];
|
||||
if (!value || value === 'false')
|
||||
continue;
|
||||
node[tokenProperty] = value;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Accessibility};
|
@ -1,380 +0,0 @@
|
||||
const {helper, assert} = require('./helper');
|
||||
const {Page} = require('./Page');
|
||||
const {Events} = require('./Events');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Browser extends EventEmitter {
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
* @param {?Puppeteer.ChildProcess} process
|
||||
* @param {function():void} closeCallback
|
||||
*/
|
||||
static async create(connection, defaultViewport, process, closeCallback) {
|
||||
const {browserContextIds} = await connection.send('Target.getBrowserContexts');
|
||||
const browser = new Browser(connection, browserContextIds, defaultViewport, process, closeCallback);
|
||||
await connection.send('Target.enable');
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {!Array<string>} browserContextIds
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
* @param {?Puppeteer.ChildProcess} process
|
||||
* @param {function():void} closeCallback
|
||||
*/
|
||||
constructor(connection, browserContextIds, defaultViewport, process, closeCallback) {
|
||||
super();
|
||||
this._connection = connection;
|
||||
this._defaultViewport = defaultViewport;
|
||||
this._process = process;
|
||||
this._closeCallback = closeCallback;
|
||||
|
||||
/** @type {!Map<string, !Target>} */
|
||||
this._targets = new Map();
|
||||
|
||||
this._defaultContext = new BrowserContext(this._connection, this, null);
|
||||
/** @type {!Map<string, !BrowserContext>} */
|
||||
this._contexts = new Map();
|
||||
for (const browserContextId of browserContextIds)
|
||||
this._contexts.set(browserContextId, new BrowserContext(this._connection, this, browserContextId));
|
||||
|
||||
this._connection.on(Events.Connection.Disconnected, () => this.emit(Events.Browser.Disconnected));
|
||||
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._connection, 'Target.targetCreated', this._onTargetCreated.bind(this)),
|
||||
helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
|
||||
helper.addEventListener(this._connection, 'Target.targetInfoChanged', this._onTargetInfoChanged.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
wsEndpoint() {
|
||||
return this._connection.url();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._connection.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isConnected() {
|
||||
return !this._connection._closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!BrowserContext}
|
||||
*/
|
||||
async createIncognitoBrowserContext() {
|
||||
const {browserContextId} = await this._connection.send('Target.createBrowserContext');
|
||||
const context = new BrowserContext(this._connection, this, browserContextId);
|
||||
this._contexts.set(browserContextId, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!BrowserContext>}
|
||||
*/
|
||||
browserContexts() {
|
||||
return [this._defaultContext, ...Array.from(this._contexts.values())];
|
||||
}
|
||||
|
||||
defaultBrowserContext() {
|
||||
return this._defaultContext;
|
||||
}
|
||||
|
||||
async _disposeContext(browserContextId) {
|
||||
await this._connection.send('Target.removeBrowserContext', {browserContextId});
|
||||
this._contexts.delete(browserContextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async userAgent() {
|
||||
const info = await this._connection.send('Browser.getInfo');
|
||||
return info.userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async version() {
|
||||
const info = await this._connection.send('Browser.getInfo');
|
||||
return info.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.ChildProcess}
|
||||
*/
|
||||
process() {
|
||||
return this._process;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(!Target):boolean|Promise<boolean>} predicate
|
||||
* @param {{timeout?: number}=} options
|
||||
* @return {!Promise<!Target>}
|
||||
*/
|
||||
async waitForTarget(predicate, options = {}) {
|
||||
const {
|
||||
timeout = 30000
|
||||
} = options;
|
||||
let resolve;
|
||||
const targetPromise = new Promise(x => resolve = x);
|
||||
this.on(Events.Browser.TargetCreated, check);
|
||||
this.on('targetchanged', check);
|
||||
try {
|
||||
if (!timeout)
|
||||
return await targetPromise;
|
||||
return await helper.waitWithTimeout(
|
||||
Promise.race([
|
||||
targetPromise,
|
||||
(async () => {
|
||||
for (const target of this.targets()) {
|
||||
if (await predicate(target)) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
await targetPromise;
|
||||
})(),
|
||||
]),
|
||||
'target',
|
||||
timeout
|
||||
);
|
||||
} finally {
|
||||
this.removeListener(Events.Browser.TargetCreated, check);
|
||||
this.removeListener('targetchanged', check);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Target} target
|
||||
*/
|
||||
async function check(target) {
|
||||
if (await predicate(target))
|
||||
resolve(target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<Page>}
|
||||
*/
|
||||
newPage() {
|
||||
return this._createPageInContext(this._defaultContext._browserContextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?string} browserContextId
|
||||
* @return {Promise<Page>}
|
||||
*/
|
||||
async _createPageInContext(browserContextId) {
|
||||
const {targetId} = await this._connection.send('Target.newPage', {
|
||||
browserContextId: browserContextId || undefined
|
||||
});
|
||||
const target = this._targets.get(targetId);
|
||||
return await target.page();
|
||||
}
|
||||
|
||||
async pages() {
|
||||
const pageTargets = Array.from(this._targets.values()).filter(target => target.type() === 'page');
|
||||
return await Promise.all(pageTargets.map(target => target.page()));
|
||||
}
|
||||
|
||||
targets() {
|
||||
return Array.from(this._targets.values());
|
||||
}
|
||||
|
||||
target() {
|
||||
return this.targets().find(target => target.type() === 'browser');
|
||||
}
|
||||
|
||||
async _onTargetCreated({targetId, url, browserContextId, openerId, type}) {
|
||||
const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext;
|
||||
const target = new Target(this._connection, this, context, targetId, type, url, openerId);
|
||||
this._targets.set(targetId, target);
|
||||
if (target.opener() && target.opener()._pagePromise) {
|
||||
const openerPage = await target.opener()._pagePromise;
|
||||
if (openerPage.listenerCount(Events.Page.Popup)) {
|
||||
const popupPage = await target.page();
|
||||
openerPage.emit(Events.Page.Popup, popupPage);
|
||||
}
|
||||
}
|
||||
this.emit(Events.Browser.TargetCreated, target);
|
||||
context.emit(Events.BrowserContext.TargetCreated, target);
|
||||
}
|
||||
|
||||
_onTargetDestroyed({targetId}) {
|
||||
const target = this._targets.get(targetId);
|
||||
this._targets.delete(targetId);
|
||||
target._closedCallback();
|
||||
this.emit(Events.Browser.TargetDestroyed, target);
|
||||
target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target);
|
||||
}
|
||||
|
||||
_onTargetInfoChanged({targetId, url}) {
|
||||
const target = this._targets.get(targetId);
|
||||
target._url = url;
|
||||
this.emit(Events.Browser.TargetChanged, target);
|
||||
target.browserContext().emit(Events.BrowserContext.TargetChanged, target);
|
||||
}
|
||||
|
||||
async close() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
await this._closeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
class Target {
|
||||
/**
|
||||
*
|
||||
* @param {*} connection
|
||||
* @param {!Browser} browser
|
||||
* @param {!BrowserContext} context
|
||||
* @param {string} targetId
|
||||
* @param {string} type
|
||||
* @param {string} url
|
||||
* @param {string=} openerId
|
||||
*/
|
||||
constructor(connection, browser, context, targetId, type, url, openerId) {
|
||||
this._browser = browser;
|
||||
this._context = context;
|
||||
this._connection = connection;
|
||||
this._targetId = targetId;
|
||||
this._type = type;
|
||||
/** @type {?Promise<!Page>} */
|
||||
this._pagePromise = null;
|
||||
this._url = url;
|
||||
this._openerId = openerId;
|
||||
this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Target}
|
||||
*/
|
||||
opener() {
|
||||
return this._openerId ? this._browser._targets.get(this._openerId) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {"page"|"browser"}
|
||||
*/
|
||||
type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!BrowserContext}
|
||||
*/
|
||||
browserContext() {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
async page() {
|
||||
if (this._type === 'page' && !this._pagePromise) {
|
||||
const session = await this._connection.createSession(this._targetId);
|
||||
this._pagePromise = Page.create(session, this, this._browser._defaultViewport);
|
||||
}
|
||||
return this._pagePromise;
|
||||
}
|
||||
|
||||
browser() {
|
||||
return this._browser;
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserContext extends EventEmitter {
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {!Browser} browser
|
||||
* @param {?string} browserContextId
|
||||
*/
|
||||
constructor(connection, browser, browserContextId) {
|
||||
super();
|
||||
this._connection = connection;
|
||||
this._browser = browser;
|
||||
this._browserContextId = browserContextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} origin
|
||||
* @param {!Array<string>} permissions
|
||||
*/
|
||||
async overridePermissions(origin, permissions) {
|
||||
const webPermissionToProtocol = new Map([
|
||||
['geolocation', 'geo'],
|
||||
['microphone', 'microphone'],
|
||||
['camera', 'camera'],
|
||||
['notifications', 'desktop-notifications'],
|
||||
]);
|
||||
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._browserContextId || undefined, permissions});
|
||||
}
|
||||
|
||||
async clearPermissionOverrides() {
|
||||
await this._connection.send('Browser.resetPermissions', {browserContextId: this._browserContextId || undefined});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<Target>}
|
||||
*/
|
||||
targets() {
|
||||
return this._browser.targets().filter(target => target.browserContext() === this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<Array<Puppeteer.Page>>}
|
||||
*/
|
||||
async pages() {
|
||||
const pages = await Promise.all(
|
||||
this.targets()
|
||||
.filter(target => target.type() === 'page')
|
||||
.map(target => target.page())
|
||||
);
|
||||
return pages.filter(page => !!page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(Target):boolean|Promise<boolean>} predicate
|
||||
* @param {{timeout?: number}=} options
|
||||
* @return {!Promise<Target>}
|
||||
*/
|
||||
waitForTarget(predicate, options) {
|
||||
return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isIncognito() {
|
||||
return !!this._browserContextId;
|
||||
}
|
||||
|
||||
newPage() {
|
||||
return this._browser._createPageInContext(this._browserContextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Browser}
|
||||
*/
|
||||
browser() {
|
||||
return this._browser;
|
||||
}
|
||||
|
||||
async close() {
|
||||
assert(this._browserContextId, 'Non-incognito contexts cannot be closed!');
|
||||
await this._browser._disposeContext(this._browserContextId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Browser, BrowserContext, Target};
|
@ -1,342 +0,0 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const extract = require('extract-zip');
|
||||
const util = require('util');
|
||||
const URL = require('url');
|
||||
const {helper, assert} = require('./helper');
|
||||
const removeRecursive = require('rimraf');
|
||||
// @ts-ignore
|
||||
const ProxyAgent = require('https-proxy-agent');
|
||||
// @ts-ignore
|
||||
const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
|
||||
|
||||
const downloadURLs = {
|
||||
chromium: {
|
||||
host: 'https://storage.googleapis.com',
|
||||
linux: '%s/chromium-browser-snapshots/Linux_x64/%s/%s.zip',
|
||||
mac: '%s/chromium-browser-snapshots/Mac/%s/%s.zip',
|
||||
win32: '%s/chromium-browser-snapshots/Win/%s/%s.zip',
|
||||
win64: '%s/chromium-browser-snapshots/Win_x64/%s/%s.zip',
|
||||
},
|
||||
firefox: {
|
||||
host: 'https://github.com/puppeteer/juggler/releases',
|
||||
linux: '%s/download/%s/%s.zip',
|
||||
mac: '%s/download/%s/%s.zip',
|
||||
win32: '%s/download/%s/%s.zip',
|
||||
win64: '%s/download/%s/%s.zip',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} product
|
||||
* @param {string} platform
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
function archiveName(product, platform, revision) {
|
||||
if (product === 'chromium') {
|
||||
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';
|
||||
}
|
||||
} else if (product === 'firefox') {
|
||||
if (platform === 'linux')
|
||||
return 'firefox-linux';
|
||||
if (platform === 'mac')
|
||||
return 'firefox-mac';
|
||||
if (platform === 'win32' || platform === 'win64')
|
||||
return 'firefox-' + platform;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} product
|
||||
* @param {string} platform
|
||||
* @param {string} host
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
function downloadURL(product, platform, host, revision) {
|
||||
const url = util.format(downloadURLs[product][platform], host, revision, archiveName(product, platform, revision));
|
||||
return url;
|
||||
}
|
||||
|
||||
const readdirAsync = helper.promisify(fs.readdir.bind(fs));
|
||||
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
||||
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
|
||||
const chmodAsync = helper.promisify(fs.chmod.bind(fs));
|
||||
|
||||
function existsAsync(filePath) {
|
||||
let fulfill = null;
|
||||
const promise = new Promise(x => fulfill = x);
|
||||
fs.access(filePath, err => fulfill(!err));
|
||||
return promise;
|
||||
}
|
||||
|
||||
class BrowserFetcher {
|
||||
/**
|
||||
* @param {string} projectRoot
|
||||
* @param {!BrowserFetcher.Options=} options
|
||||
*/
|
||||
constructor(projectRoot, options = {}) {
|
||||
this._product = (options.product || 'chromium').toLowerCase();
|
||||
assert(this._product === 'chromium' || this._product === 'firefox', `Unknown product: "${options.product}"`);
|
||||
this._downloadsFolder = options.path || path.join(projectRoot, '.local-browser');
|
||||
this._downloadHost = options.host || downloadURLs[this._product].host;
|
||||
this._platform = options.platform || '';
|
||||
if (!this._platform) {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin')
|
||||
this._platform = 'mac';
|
||||
else if (platform === 'linux')
|
||||
this._platform = 'linux';
|
||||
else if (platform === 'win32')
|
||||
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
|
||||
assert(this._platform, 'Unsupported platform: ' + os.platform());
|
||||
}
|
||||
assert(downloadURLs[this._product][this._platform], 'Unsupported platform: ' + this._platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
platform() {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {!Promise<boolean>}
|
||||
*/
|
||||
canDownload(revision) {
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
let resolve;
|
||||
const promise = new Promise(x => resolve = x);
|
||||
const request = httpRequest(url, 'HEAD', response => {
|
||||
resolve(response.statusCode === 200);
|
||||
});
|
||||
request.on('error', error => {
|
||||
console.error(error);
|
||||
resolve(false);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @param {?function(number, number)} progressCallback
|
||||
* @return {!Promise<!BrowserFetcher.RevisionInfo>}
|
||||
*/
|
||||
async download(revision, progressCallback) {
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
const zipPath = path.join(this._downloadsFolder, `download-${this._product}-${this._platform}-${revision}.zip`);
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
if (await existsAsync(folderPath))
|
||||
return this.revisionInfo(revision);
|
||||
if (!(await existsAsync(this._downloadsFolder)))
|
||||
await mkdirAsync(this._downloadsFolder);
|
||||
try {
|
||||
await downloadFile(url, zipPath, progressCallback);
|
||||
await extractZip(zipPath, folderPath);
|
||||
} finally {
|
||||
if (await existsAsync(zipPath))
|
||||
await unlinkAsync(zipPath);
|
||||
}
|
||||
const revisionInfo = this.revisionInfo(revision);
|
||||
if (revisionInfo)
|
||||
await chmodAsync(revisionInfo.executablePath, 0o755);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
async localRevisions() {
|
||||
if (!await existsAsync(this._downloadsFolder))
|
||||
return [];
|
||||
const fileNames = await readdirAsync(this._downloadsFolder);
|
||||
return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
*/
|
||||
async remove(revision) {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
|
||||
await new Promise(fulfill => removeRecursive(folderPath, fulfill));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {!BrowserFetcher.RevisionInfo}
|
||||
*/
|
||||
revisionInfo(revision) {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
let executablePath = '';
|
||||
if (this._product === 'chromium') {
|
||||
if (this._platform === 'mac')
|
||||
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');
|
||||
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);
|
||||
} else if (this._product === 'firefox') {
|
||||
if (this._platform === 'mac')
|
||||
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);
|
||||
}
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
const local = fs.existsSync(folderPath);
|
||||
return {revision, executablePath, folderPath, local, url};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
_getFolderPath(revision) {
|
||||
return path.join(this._downloadsFolder, this._product + '-' + this._platform + '-' + revision);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {BrowserFetcher};
|
||||
|
||||
/**
|
||||
* @param {string} folderPath
|
||||
* @return {?{platform: string, revision: string}}
|
||||
*/
|
||||
function parseFolderPath(folderPath) {
|
||||
const name = path.basename(folderPath);
|
||||
const splits = name.split('-');
|
||||
if (splits.length !== 3)
|
||||
return null;
|
||||
const [product, platform, revision] = splits;
|
||||
if (!downloadURLs[product][platform])
|
||||
return null;
|
||||
return {platform, revision};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string} destinationPath
|
||||
* @param {?function(number, number)} progressCallback
|
||||
* @return {!Promise}
|
||||
*/
|
||||
function downloadFile(url, destinationPath, progressCallback) {
|
||||
let fulfill, reject;
|
||||
let downloadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
const promise = new Promise((x, y) => { fulfill = x; reject = y; });
|
||||
|
||||
const request = httpRequest(url, 'GET', response => {
|
||||
if (response.statusCode !== 200) {
|
||||
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
|
||||
// consume response data to free up memory
|
||||
response.resume();
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath);
|
||||
file.on('finish', () => fulfill());
|
||||
file.on('error', error => reject(error));
|
||||
response.pipe(file);
|
||||
totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
|
||||
if (progressCallback)
|
||||
response.on('data', onData);
|
||||
});
|
||||
request.on('error', error => reject(error));
|
||||
return promise;
|
||||
|
||||
function onData(chunk) {
|
||||
downloadedBytes += chunk.length;
|
||||
progressCallback(downloadedBytes, totalBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} zipPath
|
||||
* @param {string} folderPath
|
||||
* @return {!Promise<?Error>}
|
||||
*/
|
||||
async function extractZip(zipPath, folderPath) {
|
||||
try {
|
||||
await extract(zipPath, {dir: folderPath});
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
function httpRequest(url, method, response) {
|
||||
/** @type {Object} */
|
||||
const options = URL.parse(url);
|
||||
options.method = method;
|
||||
|
||||
const proxyURL = getProxyForUrl(url);
|
||||
if (proxyURL) {
|
||||
/** @type {Object} */
|
||||
const parsedProxyURL = URL.parse(proxyURL);
|
||||
parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
|
||||
|
||||
options.agent = new ProxyAgent(parsedProxyURL);
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
const requestCallback = res => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
|
||||
httpRequest(res.headers.location, method, response);
|
||||
else
|
||||
response(res);
|
||||
};
|
||||
const request = options.protocol === 'https:' ?
|
||||
require('https').request(options, requestCallback) :
|
||||
require('http').request(options, requestCallback);
|
||||
request.end();
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} BrowserFetcher.Options
|
||||
* @property {string=} platform
|
||||
* @property {string=} path
|
||||
* @property {string=} host
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BrowserFetcher.RevisionInfo
|
||||
* @property {string} folderPath
|
||||
* @property {string} executablePath
|
||||
* @property {string} url
|
||||
* @property {boolean} local
|
||||
* @property {string} revision
|
||||
*/
|
@ -1,242 +0,0 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const {assert} = require('./helper');
|
||||
const {Events} = require('./Events');
|
||||
const debugProtocol = require('debug')('puppeteer:protocol');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Connection extends EventEmitter {
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {!Puppeteer.ConnectionTransport} transport
|
||||
* @param {number=} delay
|
||||
*/
|
||||
constructor(url, transport, delay = 0) {
|
||||
super();
|
||||
this._url = url;
|
||||
this._lastId = 0;
|
||||
/** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
|
||||
this._callbacks = new Map();
|
||||
this._delay = delay;
|
||||
|
||||
this._transport = transport;
|
||||
this._transport.onmessage = this._onMessage.bind(this);
|
||||
this._transport.onclose = this._onClose.bind(this);
|
||||
/** @type {!Map<string, !JugglerSession>}*/
|
||||
this._sessions = new Map();
|
||||
this._closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!JugglerSession} session
|
||||
* @return {!Connection}
|
||||
*/
|
||||
static fromSession(session) {
|
||||
return session._connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sessionId
|
||||
* @return {?JugglerSession}
|
||||
*/
|
||||
session(sessionId) {
|
||||
return this._sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {!Object=} params
|
||||
* @return {!Promise<?Object>}
|
||||
*/
|
||||
send(method, params = {}) {
|
||||
const id = this._rawSend({method, params});
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} message
|
||||
* @return {number}
|
||||
*/
|
||||
_rawSend(message) {
|
||||
const id = ++this._lastId;
|
||||
message = JSON.stringify(Object.assign({}, message, {id}));
|
||||
debugProtocol('SEND ► ' + message);
|
||||
this._transport.send(message);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
async _onMessage(message) {
|
||||
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 JugglerSession(this, object.params.targetInfo.type, sessionId);
|
||||
this._sessions.set(sessionId, session);
|
||||
} else if (object.method === 'Browser.detachedFromTarget') {
|
||||
const session = this._sessions.get(object.params.sessionId);
|
||||
if (session) {
|
||||
session._onClosed();
|
||||
this._sessions.delete(object.params.sessionId);
|
||||
}
|
||||
}
|
||||
if (object.sessionId) {
|
||||
const session = this._sessions.get(object.sessionId);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
this.emit(object.method, object.params);
|
||||
}
|
||||
}
|
||||
|
||||
_onClose() {
|
||||
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.`));
|
||||
this._callbacks.clear();
|
||||
for (const session of this._sessions.values())
|
||||
session._onClosed();
|
||||
this._sessions.clear();
|
||||
this.emit(Events.Connection.Disconnected);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onClose();
|
||||
this._transport.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} targetId
|
||||
* @return {!Promise<!JugglerSession>}
|
||||
*/
|
||||
async createSession(targetId) {
|
||||
const {sessionId} = await this.send('Target.attachToTarget', {targetId});
|
||||
return this._sessions.get(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
class JugglerSession extends EventEmitter {
|
||||
/**
|
||||
* @param {!Connection} connection
|
||||
* @param {string} targetType
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
constructor(connection, targetType, sessionId) {
|
||||
super();
|
||||
/** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
|
||||
this._callbacks = new Map();
|
||||
this._connection = connection;
|
||||
this._targetType = targetType;
|
||||
this._sessionId = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {!Object=} params
|
||||
* @return {!Promise<?Object>}
|
||||
*/
|
||||
send(method, params = {}) {
|
||||
if (!this._connection)
|
||||
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, method, params});
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{id?: number, method: string, params: Object, error: {message: string, data: any}, result?: *}} object
|
||||
*/
|
||||
_onMessage(object) {
|
||||
if (object.id && this._callbacks.has(object.id)) {
|
||||
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);
|
||||
} else {
|
||||
assert(!object.id);
|
||||
this.emit(object.method, object.params);
|
||||
}
|
||||
}
|
||||
|
||||
async detach() {
|
||||
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});
|
||||
}
|
||||
|
||||
_onClosed() {
|
||||
for (const callback of this._callbacks.values())
|
||||
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
|
||||
this._callbacks.clear();
|
||||
this._connection = null;
|
||||
this.emit(Events.JugglerSession.Disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
* @param {string} method
|
||||
* @param {{error: {message: string, data: any}}} object
|
||||
* @return {!Error}
|
||||
*/
|
||||
function createProtocolError(error, method, object) {
|
||||
let message = `Protocol error (${method}): ${object.error.message}`;
|
||||
if ('data' in object.error)
|
||||
message += ` ${object.error.data}`;
|
||||
return rewriteError(error, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
* @param {string} message
|
||||
* @return {!Error}
|
||||
*/
|
||||
function rewriteError(error, message) {
|
||||
error.message = message;
|
||||
return error;
|
||||
}
|
||||
|
||||
module.exports = {Connection, JugglerSession};
|
@ -1,636 +0,0 @@
|
||||
const {helper, assert} = require('./helper');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
|
||||
class DOMWorld {
|
||||
constructor(frame, timeoutSettings) {
|
||||
this._frame = frame;
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
|
||||
this._documentPromise = null;
|
||||
this._contextPromise;
|
||||
this._contextResolveCallback = null;
|
||||
this._setContext(null);
|
||||
|
||||
/** @type {!Set<!WaitTask>} */
|
||||
this._waitTasks = new Set();
|
||||
this._detached = false;
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
_setContext(context) {
|
||||
if (context) {
|
||||
this._contextResolveCallback.call(null, context);
|
||||
this._contextResolveCallback = null;
|
||||
for (const waitTask of this._waitTasks)
|
||||
waitTask.rerun();
|
||||
} else {
|
||||
this._documentPromise = null;
|
||||
this._contextPromise = new Promise(fulfill => {
|
||||
this._contextResolveCallback = fulfill;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_detach() {
|
||||
this._detached = true;
|
||||
for (const waitTask of this._waitTasks)
|
||||
waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
||||
}
|
||||
|
||||
async executionContext() {
|
||||
if (this._detached)
|
||||
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
||||
return this._contextPromise;
|
||||
}
|
||||
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
const context = await this.executionContext();
|
||||
return context.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
async evaluate(pageFunction, ...args) {
|
||||
const context = await this.executionContext();
|
||||
return context.evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
const document = await this._document();
|
||||
return document.$(selector);
|
||||
}
|
||||
|
||||
_document() {
|
||||
if (!this._documentPromise)
|
||||
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement());
|
||||
return this._documentPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
const document = await this._document();
|
||||
return document.$x(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
const document = await this._document();
|
||||
return document.$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
const document = await this._document();
|
||||
return document.$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
const document = await this._document();
|
||||
return document.$$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<String>}
|
||||
*/
|
||||
async content() {
|
||||
return await this.evaluate(() => {
|
||||
let retVal = '';
|
||||
if (document.doctype)
|
||||
retVal = new XMLSerializer().serializeToString(document.doctype);
|
||||
if (document.documentElement)
|
||||
retVal += document.documentElement.outerHTML;
|
||||
return retVal;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
async setContent(html) {
|
||||
await this.evaluate(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
}, html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, type?: string, url?: string, id?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addScriptTag(options) {
|
||||
const {
|
||||
type = '',
|
||||
id = ''
|
||||
} = options;
|
||||
|
||||
if (typeof options.url === 'string') {
|
||||
const url = options.url;
|
||||
try {
|
||||
return (await this.evaluateHandle(addScriptUrl, url, id, type)).asElement();
|
||||
} catch (error) {
|
||||
throw new Error(`Loading script from ${url} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.path === 'string') {
|
||||
let contents = await readFileAsync(options.path, 'utf8');
|
||||
contents += '//# sourceURL=' + options.path.replace(/\n/g, '');
|
||||
return (await this.evaluateHandle(addScriptContent, contents, id, type)).asElement();
|
||||
}
|
||||
|
||||
if (typeof options.content === 'string') {
|
||||
return (await this.evaluateHandle(addScriptContent, options.content, id, type)).asElement();
|
||||
}
|
||||
|
||||
throw new Error('Provide an object with a `url`, `path` or `content` property');
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string} id
|
||||
* @param {string} type
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
async function addScriptUrl(url, id, type) {
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
if (id)
|
||||
script.id = id;
|
||||
if (type)
|
||||
script.type = type;
|
||||
const promise = new Promise((res, rej) => {
|
||||
script.onload = res;
|
||||
script.onerror = rej;
|
||||
});
|
||||
document.head.appendChild(script);
|
||||
await promise;
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {string} id
|
||||
* @param {string} type
|
||||
* @return {!HTMLElement}
|
||||
*/
|
||||
function addScriptContent(content, id, type = 'text/javascript') {
|
||||
const script = document.createElement('script');
|
||||
script.type = type;
|
||||
script.text = content;
|
||||
if (id)
|
||||
script.id = id;
|
||||
let error = null;
|
||||
script.onerror = e => error = e;
|
||||
document.head.appendChild(script);
|
||||
if (error)
|
||||
throw error;
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addStyleTag(options) {
|
||||
if (typeof options.url === 'string') {
|
||||
const url = options.url;
|
||||
try {
|
||||
return (await this.evaluateHandle(addStyleUrl, url)).asElement();
|
||||
} catch (error) {
|
||||
throw new Error(`Loading style from ${url} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.path === 'string') {
|
||||
let contents = await readFileAsync(options.path, 'utf8');
|
||||
contents += '/*# sourceURL=' + options.path.replace(/\n/g, '') + '*/';
|
||||
return (await this.evaluateHandle(addStyleContent, contents)).asElement();
|
||||
}
|
||||
|
||||
if (typeof options.content === 'string') {
|
||||
return (await this.evaluateHandle(addStyleContent, options.content)).asElement();
|
||||
}
|
||||
|
||||
throw new Error('Provide an object with a `url`, `path` or `content` property');
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
async function addStyleUrl(url) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
const promise = new Promise((res, rej) => {
|
||||
link.onload = res;
|
||||
link.onerror = rej;
|
||||
});
|
||||
document.head.appendChild(link);
|
||||
await promise;
|
||||
return link;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
async function addStyleContent(content) {
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.appendChild(document.createTextNode(content));
|
||||
const promise = new Promise((res, rej) => {
|
||||
style.onload = res;
|
||||
style.onerror = rej;
|
||||
});
|
||||
document.head.appendChild(style);
|
||||
await promise;
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(selector, options = {}) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.click(options);
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async focus(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.focus();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async hover(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.hover();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!Array<string>} values
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
select(selector, ...values) {
|
||||
for (const value of values)
|
||||
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
return this.$eval(selector, (element, values) => {
|
||||
if (element.nodeName.toLowerCase() !== 'select')
|
||||
throw new Error('Element is not a <select> element.');
|
||||
|
||||
const options = Array.from(element.options);
|
||||
element.value = undefined;
|
||||
for (const option of options) {
|
||||
option.selected = values.includes(option.value);
|
||||
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);
|
||||
}, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async tap(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.tap();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(selector, text, options) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.type(text, options);
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
waitForSelector(selector, options) {
|
||||
return this._waitForSelectorOrXPath(selector, false, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
waitForXPath(xpath, options) {
|
||||
return this._waitForSelectorOrXPath(xpath, true, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!{polling?: string|number, timeout?: number}=} options
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
waitForFunction(pageFunction, options = {}, ...args) {
|
||||
const {
|
||||
polling = 'raf',
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async title() {
|
||||
return this.evaluate(() => document.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selectorOrXPath
|
||||
* @param {boolean} isXPath
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
|
||||
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 waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden);
|
||||
const handle = await waitTask.promise;
|
||||
if (!handle.asElement()) {
|
||||
await handle.dispose();
|
||||
return null;
|
||||
}
|
||||
return handle.asElement();
|
||||
|
||||
/**
|
||||
* @param {string} selectorOrXPath
|
||||
* @param {boolean} isXPath
|
||||
* @param {boolean} waitForVisible
|
||||
* @param {boolean} waitForHidden
|
||||
* @return {?Node|boolean}
|
||||
*/
|
||||
function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
|
||||
const node = isXPath
|
||||
? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
|
||||
: document.querySelector(selectorOrXPath);
|
||||
if (!node)
|
||||
return waitForHidden;
|
||||
if (!waitForVisible && !waitForHidden)
|
||||
return node;
|
||||
const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
|
||||
|
||||
const style = window.getComputedStyle(element);
|
||||
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
|
||||
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
|
||||
return success ? node : null;
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
function hasVisibleBoundingBox() {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class WaitTask {
|
||||
/**
|
||||
* @param {!DOMWorld} domWorld
|
||||
* @param {Function|string} predicateBody
|
||||
* @param {string|number} polling
|
||||
* @param {number} timeout
|
||||
* @param {!Array<*>} args
|
||||
*/
|
||||
constructor(domWorld, predicateBody, title, polling, timeout, ...args) {
|
||||
if (helper.isString(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);
|
||||
|
||||
this._domWorld = domWorld;
|
||||
this._polling = polling;
|
||||
this._timeout = timeout;
|
||||
this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)';
|
||||
this._args = args;
|
||||
this._runCount = 0;
|
||||
domWorld._waitTasks.add(this);
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
this.rerun();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
*/
|
||||
terminate(error) {
|
||||
this._terminated = true;
|
||||
this._reject(error);
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
async rerun() {
|
||||
const runCount = ++this._runCount;
|
||||
/** @type {?JSHandle} */
|
||||
let success = null;
|
||||
let error = null;
|
||||
try {
|
||||
success = await this._domWorld.evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (this._terminated || runCount !== this._runCount) {
|
||||
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(e => true)) {
|
||||
await success.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// When the page is navigated, the promise is rejected.
|
||||
// Try again right away.
|
||||
if (error && error.message.includes('Execution context was destroyed')) {
|
||||
this.rerun();
|
||||
return;
|
||||
}
|
||||
|
||||
if (error)
|
||||
this._reject(error);
|
||||
else
|
||||
this._resolve(success);
|
||||
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
clearTimeout(this._timeoutTimer);
|
||||
this._domWorld._waitTasks.delete(this);
|
||||
this._runningTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} predicateBody
|
||||
* @param {string} polling
|
||||
* @param {number} timeout
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {
|
||||
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);
|
||||
|
||||
/**
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
function pollMutation() {
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
return Promise.resolve(success);
|
||||
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
const observer = new MutationObserver(mutations => {
|
||||
if (timedOut) {
|
||||
observer.disconnect();
|
||||
fulfill();
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success) {
|
||||
observer.disconnect();
|
||||
fulfill(success);
|
||||
}
|
||||
});
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
function pollRaf() {
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
onRaf();
|
||||
return result;
|
||||
|
||||
function onRaf() {
|
||||
if (timedOut) {
|
||||
fulfill();
|
||||
return;
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
fulfill(success);
|
||||
else
|
||||
requestAnimationFrame(onRaf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pollInterval
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
function pollInterval(pollInterval) {
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
onTimeout();
|
||||
return result;
|
||||
|
||||
function onTimeout() {
|
||||
if (timedOut) {
|
||||
fulfill();
|
||||
return;
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
fulfill(success);
|
||||
else
|
||||
setTimeout(onTimeout, pollInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {DOMWorld};
|
@ -1,824 +0,0 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
'name': 'Blackberry PlayBook',
|
||||
'userAgent': 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
|
||||
'viewport': {
|
||||
'width': 600,
|
||||
'height': 1024,
|
||||
'deviceScaleFactor': 1,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Blackberry PlayBook landscape',
|
||||
'userAgent': 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
|
||||
'viewport': {
|
||||
'width': 1024,
|
||||
'height': 600,
|
||||
'deviceScaleFactor': 1,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'BlackBerry Z30',
|
||||
'userAgent': 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
|
||||
'viewport': {
|
||||
'width': 360,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'BlackBerry Z30 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 360,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Galaxy Note 3',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
|
||||
'viewport': {
|
||||
'width': 360,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Galaxy Note 3 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 360,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Galaxy Note II',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
|
||||
'viewport': {
|
||||
'width': 360,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Galaxy Note II landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 360,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Galaxy S III',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
|
||||
'viewport': {
|
||||
'width': 360,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Galaxy S III landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 360,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Galaxy S5',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 360,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Galaxy S5 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 360,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPad',
|
||||
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 768,
|
||||
'height': 1024,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPad landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 1024,
|
||||
'height': 768,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPad Mini',
|
||||
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 768,
|
||||
'height': 1024,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPad Mini landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 1024,
|
||||
'height': 768,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPad Pro',
|
||||
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 1024,
|
||||
'height': 1366,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPad Pro landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 1366,
|
||||
'height': 1024,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 4',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
|
||||
'viewport': {
|
||||
'width': 320,
|
||||
'height': 480,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 4 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
|
||||
'viewport': {
|
||||
'width': 480,
|
||||
'height': 320,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 5',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
|
||||
'viewport': {
|
||||
'width': 320,
|
||||
'height': 568,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 5 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
|
||||
'viewport': {
|
||||
'width': 568,
|
||||
'height': 320,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 6',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 375,
|
||||
'height': 667,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 6 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 667,
|
||||
'height': 375,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 6 Plus',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 414,
|
||||
'height': 736,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 6 Plus landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 736,
|
||||
'height': 414,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 7',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 375,
|
||||
'height': 667,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 7 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 667,
|
||||
'height': 375,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 7 Plus',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 414,
|
||||
'height': 736,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 7 Plus landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 736,
|
||||
'height': 414,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 8',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 375,
|
||||
'height': 667,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 8 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 667,
|
||||
'height': 375,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 8 Plus',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 414,
|
||||
'height': 736,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone 8 Plus landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 736,
|
||||
'height': 414,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone SE',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
|
||||
'viewport': {
|
||||
'width': 320,
|
||||
'height': 568,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone SE landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
|
||||
'viewport': {
|
||||
'width': 568,
|
||||
'height': 320,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone X',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 375,
|
||||
'height': 812,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'iPhone X landscape',
|
||||
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
|
||||
'viewport': {
|
||||
'width': 812,
|
||||
'height': 375,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Kindle Fire HDX',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
|
||||
'viewport': {
|
||||
'width': 800,
|
||||
'height': 1280,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Kindle Fire HDX landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
|
||||
'viewport': {
|
||||
'width': 1280,
|
||||
'height': 800,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'LG Optimus L70',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 384,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 1.25,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'LG Optimus L70 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 384,
|
||||
'deviceScaleFactor': 1.25,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Microsoft Lumia 550',
|
||||
'userAgent': 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 360,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Microsoft Lumia 950',
|
||||
'userAgent': 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
|
||||
'viewport': {
|
||||
'width': 360,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 4,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Microsoft Lumia 950 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 360,
|
||||
'deviceScaleFactor': 4,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 10',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 800,
|
||||
'height': 1280,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 10 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 1280,
|
||||
'height': 800,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 4',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 384,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 4 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 384,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 5',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 360,
|
||||
'height': 640,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 5 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 640,
|
||||
'height': 360,
|
||||
'deviceScaleFactor': 3,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 5X',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 412,
|
||||
'height': 732,
|
||||
'deviceScaleFactor': 2.625,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 5X landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 732,
|
||||
'height': 412,
|
||||
'deviceScaleFactor': 2.625,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 6',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 412,
|
||||
'height': 732,
|
||||
'deviceScaleFactor': 3.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 6 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 732,
|
||||
'height': 412,
|
||||
'deviceScaleFactor': 3.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 6P',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 412,
|
||||
'height': 732,
|
||||
'deviceScaleFactor': 3.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 6P landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 732,
|
||||
'height': 412,
|
||||
'deviceScaleFactor': 3.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 7',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 600,
|
||||
'height': 960,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 7 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 960,
|
||||
'height': 600,
|
||||
'deviceScaleFactor': 2,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nokia Lumia 520',
|
||||
'userAgent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
|
||||
'viewport': {
|
||||
'width': 320,
|
||||
'height': 533,
|
||||
'deviceScaleFactor': 1.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nokia Lumia 520 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
|
||||
'viewport': {
|
||||
'width': 533,
|
||||
'height': 320,
|
||||
'deviceScaleFactor': 1.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nokia N9',
|
||||
'userAgent': 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
|
||||
'viewport': {
|
||||
'width': 480,
|
||||
'height': 854,
|
||||
'deviceScaleFactor': 1,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Nokia N9 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
|
||||
'viewport': {
|
||||
'width': 854,
|
||||
'height': 480,
|
||||
'deviceScaleFactor': 1,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Pixel 2',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 411,
|
||||
'height': 731,
|
||||
'deviceScaleFactor': 2.625,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Pixel 2 landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 731,
|
||||
'height': 411,
|
||||
'deviceScaleFactor': 2.625,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Pixel 2 XL',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 411,
|
||||
'height': 823,
|
||||
'deviceScaleFactor': 3.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': false
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Pixel 2 XL landscape',
|
||||
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 823,
|
||||
'height': 411,
|
||||
'deviceScaleFactor': 3.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
}
|
||||
];
|
||||
for (const device of module.exports)
|
||||
module.exports[device.name] = device;
|
@ -1,57 +0,0 @@
|
||||
const {helper, assert, debugError} = require('./helper');
|
||||
|
||||
class Dialog {
|
||||
constructor(client, payload) {
|
||||
this._client = client;
|
||||
this._dialogId = payload.dialogId;
|
||||
this._type = payload.type;
|
||||
this._message = payload.message;
|
||||
this._handled = false;
|
||||
this._defaultValue = payload.defaultValue || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
message() {
|
||||
return this._message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
defaultValue() {
|
||||
return this._defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string=} promptText
|
||||
*/
|
||||
async accept(promptText) {
|
||||
assert(!this._handled, 'Cannot accept dialog which is already handled!');
|
||||
this._handled = true;
|
||||
await this._client.send('Page.handleDialog', {
|
||||
dialogId: this._dialogId,
|
||||
accept: true,
|
||||
promptText: promptText
|
||||
}).catch(debugError);
|
||||
}
|
||||
|
||||
async dismiss() {
|
||||
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
|
||||
this._handled = true;
|
||||
await this._client.send('Page.handleDialog', {
|
||||
dialogId: this._dialogId,
|
||||
accept: false
|
||||
}).catch(debugError);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Dialog};
|
@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class CustomError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutError extends CustomError {}
|
||||
|
||||
module.exports = {
|
||||
TimeoutError,
|
||||
};
|
@ -1,53 +0,0 @@
|
||||
const Events = {
|
||||
Page: {
|
||||
Close: 'close',
|
||||
Console: 'console',
|
||||
Dialog: 'dialog',
|
||||
DOMContentLoaded: 'domcontentloaded',
|
||||
FrameAttached: 'frameattached',
|
||||
FrameDetached: 'framedetached',
|
||||
FrameNavigated: 'framenavigated',
|
||||
Load: 'load',
|
||||
PageError: 'pageerror',
|
||||
Popup: 'popup',
|
||||
Request: 'request',
|
||||
Response: 'response',
|
||||
RequestFinished: 'requestfinished',
|
||||
RequestFailed: 'requestfailed',
|
||||
},
|
||||
Browser: {
|
||||
Disconnected: 'disconnected',
|
||||
TargetCreated: 'targetcreated',
|
||||
TargetChanged: 'targetchanged',
|
||||
TargetDestroyed: 'targetdestroyed',
|
||||
},
|
||||
BrowserContext: {
|
||||
TargetCreated: 'targetcreated',
|
||||
TargetChanged: 'targetchanged',
|
||||
TargetDestroyed: 'targetdestroyed',
|
||||
},
|
||||
|
||||
Connection: {
|
||||
Disconnected: Symbol('Events.Connection.Disconnected'),
|
||||
},
|
||||
|
||||
JugglerSession: {
|
||||
Disconnected: Symbol('Events.JugglerSession.Disconnected'),
|
||||
},
|
||||
|
||||
FrameManager: {
|
||||
Load: Symbol('Events.FrameManager.Load'),
|
||||
DOMContentLoaded: Symbol('Events.FrameManager.DOMContentLoaded'),
|
||||
FrameAttached: Symbol('Events.FrameManager.FrameAttached'),
|
||||
FrameNavigated: Symbol('Events.FrameManager.FrameNavigated'),
|
||||
FrameDetached: Symbol('Events.FrameManager.FrameDetached'),
|
||||
},
|
||||
|
||||
NetworkManager: {
|
||||
Request: Symbol('Events.NetworkManager.Request'),
|
||||
Response: Symbol('Events.NetworkManager.Response'),
|
||||
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {Events};
|
@ -1,103 +0,0 @@
|
||||
const {helper, assert, debugError} = require('./helper');
|
||||
const {JSHandle, createHandle} = require('./JSHandle');
|
||||
|
||||
class ExecutionContext {
|
||||
/**
|
||||
* @param {!PageSession} session
|
||||
* @param {?Frame} frame
|
||||
* @param {string} executionContextId
|
||||
*/
|
||||
constructor(session, frame, executionContextId) {
|
||||
this._session = session;
|
||||
this._frame = frame;
|
||||
this._executionContextId = executionContextId;
|
||||
}
|
||||
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
if (helper.isString(pageFunction)) {
|
||||
const payload = await this._session.send('Runtime.evaluate', {
|
||||
expression: pageFunction,
|
||||
executionContextId: this._executionContextId,
|
||||
}).catch(rewriteError);
|
||||
return createHandle(this, payload.result, payload.exceptionDetails);
|
||||
}
|
||||
if (typeof pageFunction !== 'function')
|
||||
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
||||
|
||||
let functionText = pageFunction.toString();
|
||||
try {
|
||||
new Function('(' + functionText + ')');
|
||||
} catch (e1) {
|
||||
// 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;
|
||||
try {
|
||||
new Function('(' + functionText + ')');
|
||||
} catch (e2) {
|
||||
// We tried hard to serialize, but there's a weird beast here.
|
||||
throw new Error('Passed function is not well-serializable!');
|
||||
}
|
||||
}
|
||||
args = args.map(arg => {
|
||||
if (arg instanceof JSHandle) {
|
||||
if (arg._context !== this)
|
||||
throw new Error('JSHandles can be evaluated only in the context they were created!');
|
||||
if (arg._disposed)
|
||||
throw new Error('JSHandle is disposed!');
|
||||
return arg._protocolValue;
|
||||
}
|
||||
if (Object.is(arg, Infinity))
|
||||
return {unserializableValue: 'Infinity'};
|
||||
if (Object.is(arg, -Infinity))
|
||||
return {unserializableValue: '-Infinity'};
|
||||
if (Object.is(arg, -0))
|
||||
return {unserializableValue: '-0'};
|
||||
if (Object.is(arg, NaN))
|
||||
return {unserializableValue: 'NaN'};
|
||||
return {value: arg};
|
||||
});
|
||||
let callFunctionPromise;
|
||||
try {
|
||||
callFunctionPromise = this._session.send('Runtime.callFunction', {
|
||||
functionDeclaration: functionText,
|
||||
args,
|
||||
executionContextId: this._executionContextId
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON'))
|
||||
err.message += ' Are you passing a nested JSHandle?';
|
||||
throw err;
|
||||
}
|
||||
const payload = await callFunctionPromise.catch(rewriteError);
|
||||
return createHandle(this, payload.result, payload.exceptionDetails);
|
||||
|
||||
function rewriteError(error) {
|
||||
if (error.message.includes('Failed to find execution context with id'))
|
||||
throw new Error('Execution context was destroyed, most likely because of a navigation.');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
async evaluate(pageFunction, ...args) {
|
||||
try {
|
||||
const handle = await this.evaluateHandle(pageFunction, ...args);
|
||||
const result = await handle.jsonValue();
|
||||
await handle.dispose();
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.message.includes('cyclic object value') || e.message.includes('Object is not serializable'))
|
||||
return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {ExecutionContext};
|
@ -1,478 +0,0 @@
|
||||
const {helper, assert} = require('./helper');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const EventEmitter = require('events');
|
||||
const {Events} = require('./Events');
|
||||
const {ExecutionContext} = require('./ExecutionContext');
|
||||
const {NavigationWatchdog, NextNavigationWatchdog} = require('./NavigationWatchdog');
|
||||
const {DOMWorld} = require('./DOMWorld');
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
|
||||
class FrameManager extends EventEmitter {
|
||||
/**
|
||||
* @param {PageSession} session
|
||||
* @param {Page} page
|
||||
*/
|
||||
constructor(session, page, networkManager, timeoutSettings) {
|
||||
super();
|
||||
this._session = session;
|
||||
this._page = page;
|
||||
this._networkManager = networkManager;
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
this._mainFrame = null;
|
||||
this._frames = new Map();
|
||||
/** @type {!Map<string, !ExecutionContext>} */
|
||||
this._contextIdToContext = new Map();
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
executionContextById(executionContextId) {
|
||||
return this._contextIdToContext.get(executionContextId) || null;
|
||||
}
|
||||
|
||||
_onExecutionContextCreated({executionContextId, auxData}) {
|
||||
const frameId = auxData ? auxData.frameId : null;
|
||||
const frame = this._frames.get(frameId) || null;
|
||||
const context = new ExecutionContext(this._session, frame, executionContextId);
|
||||
if (frame)
|
||||
frame._mainWorld._setContext(context);
|
||||
this._contextIdToContext.set(executionContextId, context);
|
||||
}
|
||||
|
||||
_onExecutionContextDestroyed({executionContextId}) {
|
||||
const context = this._contextIdToContext.get(executionContextId);
|
||||
if (!context)
|
||||
return;
|
||||
this._contextIdToContext.delete(executionContextId);
|
||||
if (context._frame)
|
||||
context._frame._mainWorld._setContext(null);
|
||||
}
|
||||
|
||||
frame(frameId) {
|
||||
return this._frames.get(frameId);
|
||||
}
|
||||
|
||||
mainFrame() {
|
||||
return this._mainFrame;
|
||||
}
|
||||
|
||||
frames() {
|
||||
/** @type {!Array<!Frame>} */
|
||||
let frames = [];
|
||||
collect(this._mainFrame);
|
||||
return frames;
|
||||
|
||||
function collect(frame) {
|
||||
frames.push(frame);
|
||||
for (const subframe of frame._children)
|
||||
collect(subframe);
|
||||
}
|
||||
}
|
||||
|
||||
_onNavigationCommitted(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
frame._navigated(params.url, params.name, params.navigationId);
|
||||
frame._DOMContentLoadedFired = false;
|
||||
frame._loadFired = false;
|
||||
this.emit(Events.FrameManager.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
_onSameDocumentNavigation(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
frame._url = params.url;
|
||||
this.emit(Events.FrameManager.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
_onFrameAttached(params) {
|
||||
const frame = new Frame(this._session, this, this._networkManager, this._page, params.frameId, this._timeoutSettings);
|
||||
const parentFrame = this._frames.get(params.parentFrameId) || null;
|
||||
if (parentFrame) {
|
||||
frame._parentFrame = parentFrame;
|
||||
parentFrame._children.add(frame);
|
||||
} else {
|
||||
assert(!this._mainFrame, 'INTERNAL ERROR: re-attaching main frame!');
|
||||
this._mainFrame = frame;
|
||||
}
|
||||
this._frames.set(params.frameId, frame);
|
||||
this.emit(Events.FrameManager.FrameAttached, frame);
|
||||
}
|
||||
|
||||
_onFrameDetached(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
this._frames.delete(params.frameId);
|
||||
frame._detach();
|
||||
this.emit(Events.FrameManager.FrameDetached, frame);
|
||||
}
|
||||
|
||||
_onEventFired({frameId, name}) {
|
||||
const frame = this._frames.get(frameId);
|
||||
frame._firedEvents.add(name.toLowerCase());
|
||||
if (frame === this._mainFrame) {
|
||||
if (name === 'load')
|
||||
this.emit(Events.FrameManager.Load);
|
||||
else if (name === 'DOMContentLoaded')
|
||||
this.emit(Events.FrameManager.DOMContentLoaded);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
class Frame {
|
||||
/**
|
||||
* @param {*} session
|
||||
* @param {!Page} page
|
||||
* @param {string} frameId
|
||||
*/
|
||||
constructor(session, frameManager, networkManager, page, frameId, timeoutSettings) {
|
||||
this._session = session;
|
||||
this._page = page;
|
||||
this._frameManager = frameManager;
|
||||
this._networkManager = networkManager;
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
this._frameId = frameId;
|
||||
/** @type {?Frame} */
|
||||
this._parentFrame = null;
|
||||
this._url = '';
|
||||
this._name = '';
|
||||
/** @type {!Set<!Frame>} */
|
||||
this._children = new Set();
|
||||
this._detached = false;
|
||||
|
||||
|
||||
this._firedEvents = new Set();
|
||||
|
||||
this._mainWorld = new DOMWorld(this, timeoutSettings);
|
||||
}
|
||||
|
||||
async executionContext() {
|
||||
return this._mainWorld.executionContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async waitForNavigation(options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
} = options;
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const nextNavigationDog = new NextNavigationWatchdog(this._session, this);
|
||||
const error1 = await Promise.race([
|
||||
nextNavigationDog.promise(),
|
||||
timeoutPromise,
|
||||
]);
|
||||
nextNavigationDog.dispose();
|
||||
|
||||
// If timeout happened first - throw.
|
||||
if (error1) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error1;
|
||||
}
|
||||
|
||||
const {navigationId, url} = nextNavigationDog.navigation();
|
||||
|
||||
if (!navigationId) {
|
||||
// Same document navigation happened.
|
||||
clearTimeout(timeoutId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, this, this._networkManager, navigationId, url, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async goto(url, options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
referer,
|
||||
} = options;
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId} = await this._session.send('Page.navigate', {
|
||||
frameId: this._frameId,
|
||||
referer,
|
||||
url,
|
||||
});
|
||||
if (!navigationId)
|
||||
return;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, this, this._networkManager, navigationId, url, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(selector, options = {}) {
|
||||
return this._mainWorld.click(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async tap(selector) {
|
||||
return this._mainWorld.tap(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(selector, text, options) {
|
||||
return this._mainWorld.type(selector, text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async focus(selector) {
|
||||
return this._mainWorld.focus(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async hover(selector) {
|
||||
return this._mainWorld.hover(selector);
|
||||
}
|
||||
|
||||
_detach() {
|
||||
this._parentFrame._children.delete(this);
|
||||
this._parentFrame = null;
|
||||
this._detached = true;
|
||||
this._mainWorld._detach();
|
||||
}
|
||||
|
||||
_navigated(url, name, navigationId) {
|
||||
this._url = url;
|
||||
this._name = name;
|
||||
this._lastCommittedNavigationId = navigationId;
|
||||
this._firedEvents.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!Array<string>} values
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
select(selector, ...values) {
|
||||
return this._mainWorld.select(selector, ...values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
||||
* @param {!{polling?: string|number, timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
waitFor(selectorOrFunctionOrTimeout, options, ...args) {
|
||||
const xPathPattern = '//';
|
||||
|
||||
if (helper.isString(selectorOrFunctionOrTimeout)) {
|
||||
const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
|
||||
if (string.startsWith(xPathPattern))
|
||||
return this.waitForXPath(string, options);
|
||||
return this.waitForSelector(string, options);
|
||||
}
|
||||
if (helper.isNumber(selectorOrFunctionOrTimeout))
|
||||
return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
|
||||
if (typeof selectorOrFunctionOrTimeout === 'function')
|
||||
return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
|
||||
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!{polling?: string|number, timeout?: number}=} options
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
waitForFunction(pageFunction, options = {}, ...args) {
|
||||
return this._mainWorld.waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
waitForSelector(selector, options) {
|
||||
return this._mainWorld.waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
waitForXPath(xpath, options) {
|
||||
return this._mainWorld.waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<String>}
|
||||
*/
|
||||
async content() {
|
||||
return this._mainWorld.content();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
async setContent(html) {
|
||||
return this._mainWorld.setContent(html);
|
||||
}
|
||||
|
||||
async evaluate(pageFunction, ...args) {
|
||||
return this._mainWorld.evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
return this._mainWorld.$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
return this._mainWorld.$$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
return this._mainWorld.$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
return this._mainWorld.$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
return this._mainWorld.$x(expression);
|
||||
}
|
||||
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
return this._mainWorld.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, type?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addScriptTag(options) {
|
||||
return this._mainWorld.addScriptTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addStyleTag(options) {
|
||||
return this._mainWorld.addStyleTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async title() {
|
||||
return this._mainWorld.title();
|
||||
}
|
||||
|
||||
name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
isDetached() {
|
||||
return this._detached;
|
||||
}
|
||||
|
||||
childFrames() {
|
||||
return Array.from(this._children);
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
parentFrame() {
|
||||
return this._parentFrame;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWaitUntil(waitUntil) {
|
||||
if (!Array.isArray(waitUntil))
|
||||
waitUntil = [waitUntil];
|
||||
for (const condition of waitUntil) {
|
||||
if (condition !== 'load' && condition !== 'domcontentloaded')
|
||||
throw new Error('Unknown waitUntil condition: ' + condition);
|
||||
}
|
||||
return waitUntil;
|
||||
}
|
||||
|
||||
module.exports = {FrameManager, Frame, normalizeWaitUntil};
|
@ -1,340 +0,0 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const keyDefinitions = require('./USKeyboardLayout');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* @typedef {Object} KeyDescription
|
||||
* @property {number} keyCode
|
||||
* @property {string} key
|
||||
* @property {string} text
|
||||
* @property {string} code
|
||||
* @property {number} location
|
||||
*/
|
||||
|
||||
class Keyboard {
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
this._modifiers = 0;
|
||||
this._pressedKeys = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
async down(key) {
|
||||
const description = this._keyDescriptionForString(key);
|
||||
|
||||
const repeat = this._pressedKeys.has(description.code);
|
||||
this._pressedKeys.add(description.code);
|
||||
this._modifiers |= this._modifierBit(description.key);
|
||||
|
||||
await this._client.send('Page.dispatchKeyEvent', {
|
||||
type: 'keydown',
|
||||
keyCode: description.keyCode,
|
||||
code: description.code,
|
||||
key: description.key,
|
||||
repeat,
|
||||
location: description.location
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @return {number}
|
||||
*/
|
||||
_modifierBit(key) {
|
||||
if (key === 'Alt')
|
||||
return 1;
|
||||
if (key === 'Control')
|
||||
return 2;
|
||||
if (key === 'Shift')
|
||||
return 4;
|
||||
if (key === 'Meta')
|
||||
return 8;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} keyString
|
||||
* @return {KeyDescription}
|
||||
*/
|
||||
_keyDescriptionForString(keyString) {
|
||||
const shift = this._modifiers & 8;
|
||||
const description = {
|
||||
key: '',
|
||||
keyCode: 0,
|
||||
code: '',
|
||||
text: '',
|
||||
location: 0
|
||||
};
|
||||
const definition = keyDefinitions[keyString];
|
||||
if (!definition)
|
||||
throw new Error(`Unknown key: "${keyString}"`);
|
||||
|
||||
if (definition.key)
|
||||
description.key = definition.key;
|
||||
if (shift && definition.shiftKey)
|
||||
description.key = definition.shiftKey;
|
||||
|
||||
if (definition.keyCode)
|
||||
description.keyCode = definition.keyCode;
|
||||
if (shift && definition.shiftKeyCode)
|
||||
description.keyCode = definition.shiftKeyCode;
|
||||
|
||||
if (definition.code)
|
||||
description.code = definition.code;
|
||||
|
||||
if (definition.location)
|
||||
description.location = definition.location;
|
||||
|
||||
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 any modifiers besides shift are pressed, no text should be sent
|
||||
if (this._modifiers & ~8)
|
||||
description.text = '';
|
||||
|
||||
if (description.code === 'MetaLeft')
|
||||
description.code = 'OSLeft';
|
||||
if (description.code === 'MetaRight')
|
||||
description.code = 'OSRight';
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
async up(key) {
|
||||
const description = this._keyDescriptionForString(key);
|
||||
|
||||
this._modifiers &= ~this._modifierBit(description.key);
|
||||
this._pressedKeys.delete(description.code);
|
||||
await this._client.send('Page.dispatchKeyEvent', {
|
||||
type: 'keyup',
|
||||
key: description.key,
|
||||
keyCode: description.keyCode,
|
||||
code: description.code,
|
||||
location: description.location,
|
||||
repeat: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} char
|
||||
*/
|
||||
async sendCharacter(char) {
|
||||
await this._client.send('Page.insertText', {
|
||||
text: char
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {!{delay?: number}=} options
|
||||
*/
|
||||
async type(text, options = {}) {
|
||||
const {delay = null} = options;
|
||||
for (const char of text) {
|
||||
if (keyDefinitions[char])
|
||||
await this.press(char, {delay});
|
||||
else
|
||||
await this.sendCharacter(char);
|
||||
if (delay !== null)
|
||||
await new Promise(f => setTimeout(f, delay));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {!{delay?: number}=} options
|
||||
*/
|
||||
async press(key, options = {}) {
|
||||
const {delay = null} = options;
|
||||
await this.down(key);
|
||||
if (delay !== null)
|
||||
await new Promise(f => setTimeout(f, options.delay));
|
||||
await this.up(key);
|
||||
}
|
||||
}
|
||||
|
||||
class Mouse {
|
||||
/**
|
||||
* @param {!Keyboard} keyboard
|
||||
*/
|
||||
constructor(client, keyboard) {
|
||||
this._client = client;
|
||||
this._keyboard = keyboard;
|
||||
this._x = 0;
|
||||
this._y = 0;
|
||||
this._buttons = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {{steps?: number}=} options
|
||||
*/
|
||||
async move(x, y, options = {}) {
|
||||
const {steps = 1} = options;
|
||||
const fromX = this._x, fromY = this._y;
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
await this._client.send('Page.dispatchMouseEvent', {
|
||||
type: 'mousemove',
|
||||
button: 0,
|
||||
x: fromX + (this._x - fromX) * (i / steps),
|
||||
y: fromY + (this._y - fromY) * (i / steps),
|
||||
modifiers: this._keyboard._modifiers,
|
||||
buttons: this._buttons,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(x, y, options = {}) {
|
||||
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 this.up(options);
|
||||
} else {
|
||||
await Promise.all([
|
||||
this.move(x, y),
|
||||
this.down(options),
|
||||
this.up(options),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async down(options = {}) {
|
||||
const {
|
||||
button = "left",
|
||||
clickCount = 1
|
||||
} = options;
|
||||
if (button === 'left')
|
||||
this._buttons |= 1;
|
||||
if (button === 'right')
|
||||
this._buttons |= 2;
|
||||
if (button === 'middle')
|
||||
this._buttons |= 4;
|
||||
await this._client.send('Page.dispatchMouseEvent', {
|
||||
type: 'mousedown',
|
||||
button: this._buttonNameToButton(button),
|
||||
x: this._x,
|
||||
y: this._y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
clickCount,
|
||||
buttons: this._buttons,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} buttonName
|
||||
* @return {number}
|
||||
*/
|
||||
_buttonNameToButton(buttonName) {
|
||||
if (buttonName === 'left')
|
||||
return 0;
|
||||
if (buttonName === 'middle')
|
||||
return 1;
|
||||
if (buttonName === 'right')
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async up(options = {}) {
|
||||
const {
|
||||
button = "left",
|
||||
clickCount = 1
|
||||
} = options;
|
||||
if (button === 'left')
|
||||
this._buttons &= ~1;
|
||||
if (button === 'right')
|
||||
this._buttons &= ~2;
|
||||
if (button === 'middle')
|
||||
this._buttons &= ~4;
|
||||
await this._client.send('Page.dispatchMouseEvent', {
|
||||
type: 'mouseup',
|
||||
button: this._buttonNameToButton(button),
|
||||
x: this._x,
|
||||
y: this._y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
clickCount: clickCount,
|
||||
buttons: this._buttons,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Touchscreen {
|
||||
/**
|
||||
* @param {Puppeteer.JugglerSession} client
|
||||
* @param {Keyboard} keyboard
|
||||
* @param {Mouse} mouse
|
||||
*/
|
||||
constructor(client, keyboard, mouse) {
|
||||
this._client = client;
|
||||
this._keyboard = keyboard;
|
||||
this._mouse = mouse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
async tap(x, y) {
|
||||
const touchPoints = [{x: Math.round(x), y: Math.round(y)}];
|
||||
let {defaultPrevented} = (await this._client.send('Page.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints,
|
||||
modifiers: this._keyboard._modifiers
|
||||
}));
|
||||
defaultPrevented = (await this._client.send('Page.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints,
|
||||
modifiers: this._keyboard._modifiers
|
||||
})).defaultPrevented || defaultPrevented;
|
||||
// Do not dispatch related mouse events if either of touch events
|
||||
// were prevented.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent#Event_order
|
||||
if (defaultPrevented)
|
||||
return;
|
||||
await this._mouse.move(x, y);
|
||||
await this._mouse.down();
|
||||
await this._mouse.up();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Keyboard, Mouse, Touchscreen };
|
@ -1,435 +0,0 @@
|
||||
const {assert, debugError} = require('./helper');
|
||||
const path = require('path');
|
||||
|
||||
class JSHandle {
|
||||
|
||||
/**
|
||||
* @param {!ExecutionContext} context
|
||||
* @param {*} payload
|
||||
*/
|
||||
constructor(context, payload) {
|
||||
this._context = context;
|
||||
this._session = this._context._session;
|
||||
this._executionContextId = this._context._executionContextId;
|
||||
this._objectId = payload.objectId;
|
||||
this._type = payload.type;
|
||||
this._subtype = payload.subtype;
|
||||
this._disposed = false;
|
||||
this._protocolValue = {
|
||||
unserializableValue: payload.unserializableValue,
|
||||
value: payload.value,
|
||||
objectId: payload.objectId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ExecutionContext}
|
||||
*/
|
||||
executionContext() {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @return {string}
|
||||
*/
|
||||
toString() {
|
||||
if (this._objectId)
|
||||
return 'JSHandle@' + (this._subtype || this._type);
|
||||
return 'JSHandle:' + this._deserializeValue(this._protocolValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} propertyName
|
||||
* @return {!Promise<?JSHandle>}
|
||||
*/
|
||||
async getProperty(propertyName) {
|
||||
const objectHandle = await this._context.evaluateHandle((object, propertyName) => {
|
||||
const result = {__proto__: null};
|
||||
result[propertyName] = object[propertyName];
|
||||
return result;
|
||||
}, this, propertyName);
|
||||
const properties = await objectHandle.getProperties();
|
||||
const result = properties.get(propertyName) || null;
|
||||
await objectHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<Map<string, !JSHandle>>}
|
||||
*/
|
||||
async getProperties() {
|
||||
const response = await this._session.send('Runtime.getObjectProperties', {
|
||||
executionContextId: this._executionContextId,
|
||||
objectId: this._objectId,
|
||||
});
|
||||
const result = new Map();
|
||||
for (const property of response.properties) {
|
||||
result.set(property.name, createHandle(this._context, property.value, null));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_deserializeValue({unserializableValue, value}) {
|
||||
if (unserializableValue === 'Infinity')
|
||||
return Infinity;
|
||||
if (unserializableValue === '-Infinity')
|
||||
return -Infinity;
|
||||
if (unserializableValue === '-0')
|
||||
return -0;
|
||||
if (unserializableValue === 'NaN')
|
||||
return NaN;
|
||||
return value;
|
||||
}
|
||||
|
||||
async jsonValue() {
|
||||
if (!this._objectId)
|
||||
return this._deserializeValue(this._protocolValue);
|
||||
const simpleValue = await this._session.send('Runtime.callFunction', {
|
||||
executionContextId: this._executionContextId,
|
||||
returnByValue: true,
|
||||
functionDeclaration: (e => e).toString(),
|
||||
args: [this._protocolValue],
|
||||
});
|
||||
return this._deserializeValue(simpleValue.result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?ElementHandle}
|
||||
*/
|
||||
asElement() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
if (!this._objectId)
|
||||
return;
|
||||
this._disposed = true;
|
||||
await this._session.send('Runtime.disposeObject', {
|
||||
executionContextId: this._executionContextId,
|
||||
objectId: this._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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ElementHandle extends JSHandle {
|
||||
/**
|
||||
* @param {Frame} frame
|
||||
* @param {ExecutionContext} context
|
||||
* @param {*} payload
|
||||
*/
|
||||
constructor(frame, context, payload) {
|
||||
super(context, payload);
|
||||
this._frame = frame;
|
||||
this._frameId = frame._frameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Frame}
|
||||
*/
|
||||
async contentFrame() {
|
||||
const {frameId} = await this._session.send('Page.contentFrame', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
});
|
||||
if (!frameId)
|
||||
return null;
|
||||
const frame = this._frame._frameManager.frame(frameId);
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @return {!ElementHandle}
|
||||
*/
|
||||
asElement() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<{width: number, height: number, x: number, y: number}>}
|
||||
*/
|
||||
async boundingBox() {
|
||||
return await this._session.send('Page.getBoundingBox', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{encoding?: string, path?: string}} options
|
||||
*/
|
||||
async screenshot(options = {}) {
|
||||
const clip = await this._session.send('Page.getBoundingBox', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
});
|
||||
if (!clip)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
assert(clip.width, 'Node has 0 width.');
|
||||
assert(clip.height, 'Node has 0 height.');
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
|
||||
return await this._frame._page.screenshot(Object.assign({}, options, {
|
||||
clip: {
|
||||
x: clip.x,
|
||||
y: clip.y,
|
||||
width: clip.width,
|
||||
height: clip.height,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {!Promise<boolean>}
|
||||
*/
|
||||
isIntersectingViewport() {
|
||||
return this._frame.evaluate(async element => {
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
// Firefox doesn't call IntersectionObserver callback unless
|
||||
// there are rafs.
|
||||
requestAnimationFrame(() => {});
|
||||
});
|
||||
return visibleRatio > 0;
|
||||
}, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
const handle = await this._frame.evaluateHandle(
|
||||
(element, selector) => element.querySelector(selector),
|
||||
this, selector
|
||||
);
|
||||
const element = handle.asElement();
|
||||
if (element)
|
||||
return element;
|
||||
await handle.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
const arrayHandle = await this._frame.evaluateHandle(
|
||||
(element, selector) => element.querySelectorAll(selector),
|
||||
this, selector
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
const elementHandle = await this.$(selector);
|
||||
if (!elementHandle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await this._frame.evaluate(pageFunction, elementHandle, ...args);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
const arrayHandle = await this._frame.evaluateHandle(
|
||||
(element, selector) => Array.from(element.querySelectorAll(selector)),
|
||||
this, selector
|
||||
);
|
||||
|
||||
const result = await this._frame.evaluate(pageFunction, arrayHandle, ...args);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
const arrayHandle = await this._frame.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;
|
||||
},
|
||||
this, 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 _scrollIntoViewIfNeeded() {
|
||||
const error = await this._frame.evaluate(async(element) => {
|
||||
if (!element.isConnected)
|
||||
return 'Node is detached from document';
|
||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'Node is not of type HTMLElement';
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
// Firefox doesn't call IntersectionObserver callback unless
|
||||
// there are rafs.
|
||||
requestAnimationFrame(() => {});
|
||||
});
|
||||
if (visibleRatio !== 1.0)
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||
return false;
|
||||
}, this);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(options) {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._frame._page.mouse.click(x, y, options);
|
||||
}
|
||||
|
||||
async tap() {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._frame._page.touchscreen.tap(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<string>} filePaths
|
||||
*/
|
||||
async uploadFile(...filePaths) {
|
||||
const files = filePaths.map(filePath => path.resolve(filePath));
|
||||
await this._session.send('Page.setFileInputFiles', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
||||
async hover() {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._frame._page.mouse.move(x, y);
|
||||
}
|
||||
|
||||
async focus() {
|
||||
await this._frame.evaluate(element => element.focus(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(text, options) {
|
||||
await this.focus();
|
||||
await this._frame._page.keyboard.type(text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {!{delay?: number}=} options
|
||||
*/
|
||||
async press(key, options) {
|
||||
await this.focus();
|
||||
await this._frame._page.keyboard.press(key, options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Promise<!{x: number, y: number}>}
|
||||
*/
|
||||
async _clickablePoint() {
|
||||
const result = await this._session.send('Page.getContentQuads', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
}).catch(debugError);
|
||||
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 quads = result.quads.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.
|
||||
return computeQuadCenter(quads[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function createHandle(context, result, exceptionDetails) {
|
||||
const frame = context.frame();
|
||||
if (exceptionDetails) {
|
||||
if (exceptionDetails.value)
|
||||
throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value));
|
||||
else
|
||||
throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack);
|
||||
}
|
||||
return result.subtype === 'node' ? new ElementHandle(frame, context, result) : new JSHandle(context, result);
|
||||
}
|
||||
|
||||
function computeQuadArea(quad) {
|
||||
// Compute sum of all directed areas of adjacent triangles
|
||||
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
|
||||
let area = 0;
|
||||
const points = [quad.p1, quad.p2, quad.p3, quad.p4];
|
||||
for (let i = 0; i < points.length; ++i) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % points.length];
|
||||
area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
||||
}
|
||||
return Math.abs(area);
|
||||
}
|
||||
|
||||
function computeQuadCenter(quad) {
|
||||
let x = 0, y = 0;
|
||||
for (const point of [quad.p1, quad.p2, quad.p3, quad.p4]) {
|
||||
x += point.x;
|
||||
y += point.y;
|
||||
}
|
||||
return {x: x / 4, y: y / 4};
|
||||
}
|
||||
|
||||
|
||||
module.exports = {JSHandle, ElementHandle, createHandle};
|
@ -1,300 +0,0 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const removeFolder = require('rimraf');
|
||||
const childProcess = require('child_process');
|
||||
const {Connection} = require('./Connection');
|
||||
const {Browser} = require('./Browser');
|
||||
const {BrowserFetcher} = require('./BrowserFetcher');
|
||||
const readline = require('readline');
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const {helper, debugError} = require('./helper');
|
||||
const {TimeoutError} = require('./Errors')
|
||||
const WebSocketTransport = require('./WebSocketTransport');
|
||||
|
||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||
const removeFolderAsync = util.promisify(removeFolder);
|
||||
|
||||
const tmpDir = () => process.env.PUPPETEER_TMP_DIR || os.tmpdir();
|
||||
|
||||
const FIREFOX_PROFILE_PATH = path.join(tmpDir(), 'puppeteer_firefox_profile-');
|
||||
|
||||
const DEFAULT_ARGS = [
|
||||
'-no-remote',
|
||||
'-foreground',
|
||||
];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Launcher {
|
||||
constructor(projectRoot, preferredRevision) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._preferredRevision = preferredRevision;
|
||||
}
|
||||
|
||||
defaultArgs(options = {}) {
|
||||
const {
|
||||
headless = true,
|
||||
args = [],
|
||||
userDataDir = null,
|
||||
} = options;
|
||||
const firefoxArguments = [...DEFAULT_ARGS];
|
||||
if (userDataDir)
|
||||
firefoxArguments.push('-profile', userDataDir);
|
||||
if (headless)
|
||||
firefoxArguments.push('-headless');
|
||||
firefoxArguments.push(...args);
|
||||
if (args.every(arg => arg.startsWith('-')))
|
||||
firefoxArguments.push('about:blank');
|
||||
return firefoxArguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @return {!Promise<!Browser>}
|
||||
*/
|
||||
async launch(options = {}) {
|
||||
const {
|
||||
ignoreDefaultArgs = false,
|
||||
args = [],
|
||||
dumpio = false,
|
||||
executablePath = null,
|
||||
env = process.env,
|
||||
handleSIGHUP = true,
|
||||
handleSIGINT = true,
|
||||
handleSIGTERM = true,
|
||||
ignoreHTTPSErrors = false,
|
||||
headless = true,
|
||||
defaultViewport = {width: 800, height: 600},
|
||||
slowMo = 0,
|
||||
timeout = 30000,
|
||||
} = options;
|
||||
|
||||
const firefoxArguments = [];
|
||||
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);
|
||||
|
||||
if (!firefoxArguments.includes('-juggler'))
|
||||
firefoxArguments.push('-juggler', '0');
|
||||
|
||||
let temporaryProfileDir = null;
|
||||
if (!firefoxArguments.includes('-profile') && !firefoxArguments.includes('--profile')) {
|
||||
temporaryProfileDir = await mkdtempAsync(FIREFOX_PROFILE_PATH);
|
||||
firefoxArguments.push(`-profile`, temporaryProfileDir);
|
||||
}
|
||||
|
||||
let firefoxExecutable = executablePath;
|
||||
if (!firefoxExecutable) {
|
||||
const {missingText, executablePath} = this._resolveExecutablePath();
|
||||
if (missingText)
|
||||
throw new Error(missingText);
|
||||
firefoxExecutable = executablePath;
|
||||
}
|
||||
const stdio = ['pipe', 'pipe', 'pipe'];
|
||||
const firefoxProcess = childProcess.spawn(
|
||||
firefoxExecutable,
|
||||
firefoxArguments,
|
||||
{
|
||||
// On non-windows platforms, `detached: false` 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',
|
||||
stdio,
|
||||
// On linux Juggler ships the libstdc++ it was linked against.
|
||||
env: os.platform() === 'linux' ? {
|
||||
...env,
|
||||
LD_LIBRARY_PATH: `${path.dirname(firefoxExecutable)}:${process.env.LD_LIBRARY_PATH}`,
|
||||
} : env,
|
||||
}
|
||||
);
|
||||
|
||||
if (dumpio) {
|
||||
firefoxProcess.stderr.pipe(process.stderr);
|
||||
firefoxProcess.stdout.pipe(process.stdout);
|
||||
}
|
||||
|
||||
let firefoxClosed = false;
|
||||
const waitForFirefoxToClose = new Promise((fulfill, reject) => {
|
||||
firefoxProcess.once('exit', () => {
|
||||
firefoxClosed = true;
|
||||
// Cleanup as processes exit.
|
||||
if (temporaryProfileDir) {
|
||||
removeFolderAsync(temporaryProfileDir)
|
||||
.then(() => fulfill())
|
||||
.catch(err => console.error(err));
|
||||
} else {
|
||||
fulfill();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const listeners = [ helper.addEventListener(process, 'exit', killFirefox) ];
|
||||
if (handleSIGINT)
|
||||
listeners.push(helper.addEventListener(process, 'SIGINT', () => { killFirefox(); process.exit(130); }));
|
||||
if (handleSIGTERM)
|
||||
listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyCloseFirefox));
|
||||
if (handleSIGHUP)
|
||||
listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyCloseFirefox));
|
||||
/** @type {?Connection} */
|
||||
let connection = null;
|
||||
try {
|
||||
const url = await waitForWSEndpoint(firefoxProcess, timeout);
|
||||
const transport = await WebSocketTransport.create(url);
|
||||
connection = new Connection(url, transport, slowMo);
|
||||
const browser = await Browser.create(connection, defaultViewport, firefoxProcess, gracefullyCloseFirefox);
|
||||
if (ignoreHTTPSErrors)
|
||||
await connection.send('Browser.setIgnoreHTTPSErrors', {enabled: true});
|
||||
await browser.waitForTarget(t => t.type() === 'page');
|
||||
return browser;
|
||||
} catch (e) {
|
||||
killFirefox();
|
||||
throw e;
|
||||
}
|
||||
|
||||
function gracefullyCloseFirefox() {
|
||||
helper.removeEventListeners(listeners);
|
||||
if (temporaryProfileDir) {
|
||||
killFirefox();
|
||||
} else if (connection) {
|
||||
connection.send('Browser.close').catch(error => {
|
||||
debugError(error);
|
||||
killFirefox();
|
||||
});
|
||||
}
|
||||
return waitForFirefoxToClose;
|
||||
}
|
||||
|
||||
// This method has to be sync to be used as 'exit' event handler.
|
||||
function killFirefox() {
|
||||
helper.removeEventListeners(listeners);
|
||||
if (firefoxProcess.pid && !firefoxProcess.killed && !firefoxClosed) {
|
||||
// Force kill chrome.
|
||||
try {
|
||||
if (process.platform === 'win32')
|
||||
childProcess.execSync(`taskkill /pid ${firefoxProcess.pid} /T /F`);
|
||||
else
|
||||
process.kill(-firefoxProcess.pid, 'SIGKILL');
|
||||
} catch (e) {
|
||||
// the process might have already stopped
|
||||
}
|
||||
}
|
||||
// Attempt to remove temporary profile directory to avoid littering.
|
||||
try {
|
||||
removeFolder.sync(temporaryProfileDir);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @return {!Promise<!Browser>}
|
||||
*/
|
||||
async connect(options = {}) {
|
||||
const {
|
||||
browserWSEndpoint,
|
||||
slowMo = 0,
|
||||
defaultViewport = {width: 800, height: 600},
|
||||
ignoreHTTPSErrors = false,
|
||||
} = options;
|
||||
let connection = null;
|
||||
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
connection = new Connection(browserWSEndpoint, transport, slowMo);
|
||||
const browser = await Browser.create(connection, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
|
||||
if (ignoreHTTPSErrors)
|
||||
await connection.send('Browser.setIgnoreHTTPSErrors', {enabled: true});
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
executablePath() {
|
||||
return this._resolveExecutablePath().executablePath;
|
||||
}
|
||||
|
||||
_resolveExecutablePath() {
|
||||
const downloadPath = process.env.PUPPETEER_DOWNLOAD_PATH ||
|
||||
process.env.npm_config_puppeteer_download_path ||
|
||||
process.env.npm_package_config_puppeteer_download_path;
|
||||
const browserFetcher = new BrowserFetcher(this._projectRoot, { product: 'firefox', path: downloadPath });
|
||||
const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
|
||||
const missingText = !revisionInfo.local ? `Firefox revision is not downloaded. Run "npm install" or "yarn install"` : null;
|
||||
return {executablePath: revisionInfo.executablePath, missingText};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.ChildProcess} firefoxProcess
|
||||
* @param {number} timeout
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
function waitForWSEndpoint(firefoxProcess, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rl = readline.createInterface({ input: firefoxProcess.stdout });
|
||||
let stderr = '';
|
||||
const listeners = [
|
||||
helper.addEventListener(rl, 'line', onLine),
|
||||
helper.addEventListener(rl, 'close', () => onClose()),
|
||||
helper.addEventListener(firefoxProcess, 'exit', () => onClose()),
|
||||
helper.addEventListener(firefoxProcess, 'error', error => onClose(error))
|
||||
];
|
||||
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
|
||||
|
||||
/**
|
||||
* @param {!Error=} error
|
||||
*/
|
||||
function onClose(error) {
|
||||
cleanup();
|
||||
reject(new Error([
|
||||
'Failed to launch Firefox!' + (error ? ' ' + error.message : ''),
|
||||
stderr,
|
||||
'',
|
||||
].join('\n')));
|
||||
}
|
||||
|
||||
function onTimeout() {
|
||||
cleanup();
|
||||
reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
*/
|
||||
function onLine(line) {
|
||||
stderr += line + '\n';
|
||||
const match = line.match(/^Juggler listening on (ws:\/\/.*)$/);
|
||||
if (!match)
|
||||
return;
|
||||
cleanup();
|
||||
resolve(match[1]);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (timeoutId)
|
||||
clearTimeout(timeoutId);
|
||||
helper.removeEventListeners(listeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {Launcher};
|
@ -1,119 +0,0 @@
|
||||
const {helper} = require('./helper');
|
||||
const {Events} = require('./Events');
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class NextNavigationWatchdog {
|
||||
constructor(session, navigatedFrame) {
|
||||
this._navigatedFrame = navigatedFrame;
|
||||
this._promise = new Promise(x => this._resolveCallback = x);
|
||||
this._navigation = null;
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)),
|
||||
helper.addEventListener(session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
promise() {
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
navigation() {
|
||||
return this._navigation;
|
||||
}
|
||||
|
||||
_onNavigationStarted(params) {
|
||||
if (params.frameId === this._navigatedFrame._frameId) {
|
||||
this._navigation = {
|
||||
navigationId: params.navigationId,
|
||||
url: params.url,
|
||||
};
|
||||
this._resolveCallback();
|
||||
}
|
||||
}
|
||||
|
||||
_onSameDocumentNavigation(params) {
|
||||
if (params.frameId === this._navigatedFrame._frameId) {
|
||||
this._navigation = {
|
||||
navigationId: null,
|
||||
};
|
||||
this._resolveCallback();
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class NavigationWatchdog {
|
||||
constructor(session, navigatedFrame, networkManager, targetNavigationId, targetURL, firedEvents) {
|
||||
this._navigatedFrame = navigatedFrame;
|
||||
this._targetNavigationId = targetNavigationId;
|
||||
this._firedEvents = firedEvents;
|
||||
this._targetURL = targetURL;
|
||||
|
||||
this._promise = new Promise(x => this._resolveCallback = x);
|
||||
this._navigationRequest = null;
|
||||
|
||||
const check = this._checkNavigationComplete.bind(this);
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(session, Events.JugglerSession.Disconnected, () => this._resolveCallback(new Error('Navigation failed because browser has disconnected!'))),
|
||||
helper.addEventListener(session, 'Page.eventFired', check),
|
||||
helper.addEventListener(session, 'Page.frameAttached', check),
|
||||
helper.addEventListener(session, 'Page.frameDetached', check),
|
||||
helper.addEventListener(session, 'Page.navigationStarted', check),
|
||||
helper.addEventListener(session, 'Page.navigationCommitted', check),
|
||||
helper.addEventListener(session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)),
|
||||
helper.addEventListener(networkManager, Events.NetworkManager.Request, this._onRequest.bind(this)),
|
||||
helper.addEventListener(navigatedFrame._frameManager, Events.FrameManager.FrameDetached, check),
|
||||
];
|
||||
check();
|
||||
}
|
||||
|
||||
_onRequest(request) {
|
||||
if (request.frame() !== this._navigatedFrame || !request.isNavigationRequest())
|
||||
return;
|
||||
this._navigationRequest = request;
|
||||
}
|
||||
|
||||
navigationResponse() {
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
_checkNavigationComplete() {
|
||||
if (this._navigatedFrame.isDetached()) {
|
||||
this._resolveCallback(new Error('Navigating frame was detached'));
|
||||
} else if (this._navigatedFrame._lastCommittedNavigationId === this._targetNavigationId
|
||||
&& checkFiredEvents(this._navigatedFrame, this._firedEvents)) {
|
||||
this._resolveCallback(null);
|
||||
}
|
||||
|
||||
function checkFiredEvents(frame, firedEvents) {
|
||||
for (const subframe of frame._children) {
|
||||
if (!checkFiredEvents(subframe, firedEvents))
|
||||
return false;
|
||||
}
|
||||
return firedEvents.every(event => frame._firedEvents.has(event));
|
||||
}
|
||||
}
|
||||
|
||||
_onNavigationAborted(params) {
|
||||
if (params.frameId === this._navigatedFrame._frameId && params.navigationId === this._targetNavigationId)
|
||||
this._resolveCallback(new Error('Navigation to ' + this._targetURL + ' failed: ' + params.errorText));
|
||||
}
|
||||
|
||||
promise() {
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {NavigationWatchdog, NextNavigationWatchdog};
|
@ -1,356 +0,0 @@
|
||||
const {helper, assert, debugError} = require('./helper');
|
||||
const util = require('util');
|
||||
const EventEmitter = require('events');
|
||||
const {Events} = require('./Events');
|
||||
|
||||
class NetworkManager extends EventEmitter {
|
||||
constructor(session) {
|
||||
super();
|
||||
this._session = session;
|
||||
|
||||
this._requests = new Map();
|
||||
this._frameManager = null;
|
||||
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)),
|
||||
helper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)),
|
||||
helper.addEventListener(session, 'Network.requestFinished', this._onRequestFinished.bind(this)),
|
||||
helper.addEventListener(session, 'Network.requestFailed', this._onRequestFailed.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
|
||||
setFrameManager(frameManager) {
|
||||
this._frameManager = frameManager;
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(headers) {
|
||||
const array = [];
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
assert(helper.isString(value), `Expected value of header "${name}" to be String, but "${typeof value}" is found.`);
|
||||
array.push({name, value});
|
||||
}
|
||||
await this._session.send('Network.setExtraHTTPHeaders', {headers: array});
|
||||
}
|
||||
|
||||
async setRequestInterception(enabled) {
|
||||
await this._session.send('Network.setRequestInterception', {enabled});
|
||||
}
|
||||
|
||||
_onRequestWillBeSent(event) {
|
||||
const redirected = event.redirectedFrom ? this._requests.get(event.redirectedFrom) : null;
|
||||
const frame = redirected ? redirected.frame() : (this._frameManager && event.frameId ? this._frameManager.frame(event.frameId) : null);
|
||||
if (!frame)
|
||||
return;
|
||||
let redirectChain = [];
|
||||
if (redirected) {
|
||||
redirectChain = redirected._redirectChain;
|
||||
redirectChain.push(redirected);
|
||||
this._requests.delete(redirected._id);
|
||||
}
|
||||
const request = new Request(this._session, frame, redirectChain, event);
|
||||
this._requests.set(request._id, request);
|
||||
this.emit(Events.NetworkManager.Request, request);
|
||||
}
|
||||
|
||||
_onResponseReceived(event) {
|
||||
const request = this._requests.get(event.requestId);
|
||||
if (!request)
|
||||
return;
|
||||
const response = new Response(this._session, request, event);
|
||||
request._response = response;
|
||||
this.emit(Events.NetworkManager.Response, response);
|
||||
}
|
||||
|
||||
_onRequestFinished(event) {
|
||||
const request = this._requests.get(event.requestId);
|
||||
if (!request)
|
||||
return;
|
||||
// Keep redirected requests in the map for future reference in redirectChain.
|
||||
const isRedirected = request.response().status() >= 300 && request.response().status() <= 399;
|
||||
if (isRedirected) {
|
||||
request.response()._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
|
||||
} else {
|
||||
this._requests.delete(request._id);
|
||||
request.response()._bodyLoadedPromiseFulfill.call(null);
|
||||
}
|
||||
this.emit(Events.NetworkManager.RequestFinished, request);
|
||||
}
|
||||
|
||||
_onRequestFailed(event) {
|
||||
const request = this._requests.get(event.requestId);
|
||||
if (!request)
|
||||
return;
|
||||
this._requests.delete(request._id);
|
||||
if (request.response())
|
||||
request.response()._bodyLoadedPromiseFulfill.call(null);
|
||||
request._errorText = event.errorCode;
|
||||
this.emit(Events.NetworkManager.RequestFailed, request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* document, stylesheet, image, media, font, script, texttrack, xhr, fetch, eventsource, websocket, manifest, other.
|
||||
*/
|
||||
const causeToResourceType = {
|
||||
TYPE_INVALID: 'other',
|
||||
TYPE_OTHER: 'other',
|
||||
TYPE_SCRIPT: 'script',
|
||||
TYPE_IMAGE: 'image',
|
||||
TYPE_STYLESHEET: 'stylesheet',
|
||||
TYPE_OBJECT: 'other',
|
||||
TYPE_DOCUMENT: 'document',
|
||||
TYPE_SUBDOCUMENT: 'document',
|
||||
TYPE_REFRESH: 'document',
|
||||
TYPE_XBL: 'other',
|
||||
TYPE_PING: 'other',
|
||||
TYPE_XMLHTTPREQUEST: 'xhr',
|
||||
TYPE_OBJECT_SUBREQUEST: 'other',
|
||||
TYPE_DTD: 'other',
|
||||
TYPE_FONT: 'font',
|
||||
TYPE_MEDIA: 'media',
|
||||
TYPE_WEBSOCKET: 'websocket',
|
||||
TYPE_CSP_REPORT: 'other',
|
||||
TYPE_XSLT: 'other',
|
||||
TYPE_BEACON: 'other',
|
||||
TYPE_FETCH: 'fetch',
|
||||
TYPE_IMAGESET: 'images',
|
||||
TYPE_WEB_MANIFEST: 'manifest',
|
||||
};
|
||||
|
||||
class Request {
|
||||
constructor(session, frame, redirectChain, payload) {
|
||||
this._session = session;
|
||||
this._frame = frame;
|
||||
this._id = payload.requestId;
|
||||
this._redirectChain = redirectChain;
|
||||
this._url = payload.url;
|
||||
this._postData = payload.postData;
|
||||
this._suspended = payload.suspended;
|
||||
this._response = null;
|
||||
this._errorText = null;
|
||||
this._isNavigationRequest = payload.isNavigationRequest;
|
||||
this._method = payload.method;
|
||||
this._resourceType = causeToResourceType[payload.cause] || 'other';
|
||||
this._headers = {};
|
||||
this._interceptionHandled = false;
|
||||
for (const {name, value} of payload.headers)
|
||||
this._headers[name.toLowerCase()] = value;
|
||||
}
|
||||
|
||||
failure() {
|
||||
return this._errorText ? {errorText: this._errorText} : null;
|
||||
}
|
||||
|
||||
async continue(overrides = {}) {
|
||||
assert(!overrides.url, 'Puppeteer-Firefox does not support overriding URL');
|
||||
assert(!overrides.method, 'Puppeteer-Firefox does not support overriding method');
|
||||
assert(!overrides.postData, 'Puppeteer-Firefox does not support overriding postData');
|
||||
assert(this._suspended, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
const {
|
||||
headers,
|
||||
} = overrides;
|
||||
await this._session.send('Network.resumeSuspendedRequest', {
|
||||
requestId: this._id,
|
||||
headers: headers ? Object.entries(headers).filter(([, value]) => !Object.is(value, undefined)).map(([name, value]) => ({name, value})) : undefined,
|
||||
}).catch(error => {
|
||||
debugError(error);
|
||||
});
|
||||
}
|
||||
|
||||
async abort() {
|
||||
assert(this._suspended, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
await this._session.send('Network.abortSuspendedRequest', {
|
||||
requestId: this._id,
|
||||
}).catch(error => {
|
||||
debugError(error);
|
||||
});
|
||||
}
|
||||
|
||||
postData() {
|
||||
return this._postData;
|
||||
}
|
||||
|
||||
headers() {
|
||||
return {...this._headers};
|
||||
}
|
||||
|
||||
redirectChain() {
|
||||
return this._redirectChain.slice();
|
||||
}
|
||||
|
||||
resourceType() {
|
||||
return this._resourceType;
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
method() {
|
||||
return this._method;
|
||||
}
|
||||
|
||||
isNavigationRequest() {
|
||||
return this._isNavigationRequest;
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
response() {
|
||||
return this._response;
|
||||
}
|
||||
}
|
||||
|
||||
class Response {
|
||||
constructor(session, request, payload) {
|
||||
this._session = session;
|
||||
this._request = request;
|
||||
this._remoteIPAddress = payload.remoteIPAddress;
|
||||
this._remotePort = payload.remotePort;
|
||||
this._status = payload.status;
|
||||
this._statusText = payload.statusText;
|
||||
this._headers = {};
|
||||
this._securityDetails = payload.securityDetails ? new SecurityDetails(payload.securityDetails) : null;
|
||||
for (const {name, value} of payload.headers)
|
||||
this._headers[name.toLowerCase()] = value;
|
||||
this._bodyLoadedPromise = new Promise(fulfill => {
|
||||
this._bodyLoadedPromiseFulfill = fulfill;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Buffer>}
|
||||
*/
|
||||
buffer() {
|
||||
if (!this._contentPromise) {
|
||||
this._contentPromise = this._bodyLoadedPromise.then(async error => {
|
||||
if (error)
|
||||
throw error;
|
||||
const response = await this._session.send('Network.getResponseBody', {
|
||||
requestId: this._request._id
|
||||
});
|
||||
if (response.evicted)
|
||||
throw new Error(`Response body for ${this._request.method()} ${this._request.url()} was evicted!`);
|
||||
return Buffer.from(response.base64body, 'base64');
|
||||
});
|
||||
}
|
||||
return this._contentPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async text() {
|
||||
const content = await this.buffer();
|
||||
return content.toString('utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Object>}
|
||||
*/
|
||||
async json() {
|
||||
const content = await this.text();
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
securityDetails() {
|
||||
return this._securityDetails;
|
||||
}
|
||||
|
||||
headers() {
|
||||
return {...this._headers};
|
||||
}
|
||||
|
||||
status() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
statusText() {
|
||||
return this._statusText;
|
||||
}
|
||||
|
||||
ok() {
|
||||
return this._status >= 200 && this._status <= 299;
|
||||
}
|
||||
|
||||
remoteAddress() {
|
||||
return {
|
||||
ip: this._remoteIPAddress,
|
||||
port: this._remotePort,
|
||||
};
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._request.frame();
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._request.url();
|
||||
}
|
||||
|
||||
request() {
|
||||
return this._request;
|
||||
}
|
||||
}
|
||||
|
||||
class SecurityDetails {
|
||||
/**
|
||||
* @param {!Protocol.Network.SecurityDetails} securityPayload
|
||||
*/
|
||||
constructor(securityPayload) {
|
||||
this._subjectName = securityPayload['subjectName'];
|
||||
this._issuer = securityPayload['issuer'];
|
||||
this._validFrom = securityPayload['validFrom'];
|
||||
this._validTo = securityPayload['validTo'];
|
||||
this._protocol = securityPayload['protocol'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
subjectName() {
|
||||
return this._subjectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
issuer() {
|
||||
return this._issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
validFrom() {
|
||||
return this._validFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
validTo() {
|
||||
return this._validTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
protocol() {
|
||||
return this._protocol;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {NetworkManager, Request, Response, SecurityDetails};
|
@ -1,813 +0,0 @@
|
||||
const {helper, debugError, assert} = require('./helper');
|
||||
const {Keyboard, Mouse, Touchscreen} = require('./Input');
|
||||
const {Dialog} = require('./Dialog');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
const fs = require('fs');
|
||||
const mime = require('mime');
|
||||
const EventEmitter = require('events');
|
||||
const {createHandle} = require('./JSHandle');
|
||||
const {Events} = require('./Events');
|
||||
const {Connection} = require('./Connection');
|
||||
const {FrameManager, normalizeWaitUntil} = require('./FrameManager');
|
||||
const {NetworkManager} = require('./NetworkManager');
|
||||
const {TimeoutSettings} = require('./TimeoutSettings');
|
||||
const {NavigationWatchdog} = require('./NavigationWatchdog');
|
||||
const {Accessibility} = require('./Accessibility');
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
||||
class Page extends EventEmitter {
|
||||
/**
|
||||
*
|
||||
* @param {!Puppeteer.JugglerSession} connection
|
||||
* @param {!Puppeteer.Target} target
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
*/
|
||||
static async create(session, target, defaultViewport) {
|
||||
const page = new Page(session, target);
|
||||
await Promise.all([
|
||||
session.send('Runtime.enable'),
|
||||
session.send('Network.enable'),
|
||||
session.send('Page.enable'),
|
||||
]);
|
||||
|
||||
if (defaultViewport)
|
||||
await page.setViewport(defaultViewport);
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!PageSession} session
|
||||
* @param {!Puppeteer.Target} target
|
||||
*/
|
||||
constructor(session, target) {
|
||||
super();
|
||||
this._timeoutSettings = new TimeoutSettings();
|
||||
this._session = session;
|
||||
this._target = target;
|
||||
this._keyboard = new Keyboard(session);
|
||||
this._mouse = new Mouse(session, this._keyboard);
|
||||
this._touchscreen = new Touchscreen(session, this._keyboard, this._mouse);
|
||||
this._accessibility = new Accessibility(session);
|
||||
this._closed = false;
|
||||
/** @type {!Map<string, Function>} */
|
||||
this._pageBindings = new Map();
|
||||
this._networkManager = new NetworkManager(session);
|
||||
this._frameManager = new FrameManager(session, this, this._networkManager, this._timeoutSettings);
|
||||
this._networkManager.setFrameManager(this._frameManager);
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.Load, () => this.emit(Events.Page.Load)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.DOMContentLoaded, () => this.emit(Events.Page.DOMContentLoaded)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.FrameAttached, frame => this.emit(Events.Page.FrameAttached, frame)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.FrameDetached, frame => this.emit(Events.Page.FrameDetached, frame)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.FrameNavigated, frame => this.emit(Events.Page.FrameNavigated, frame)),
|
||||
helper.addEventListener(this._networkManager, Events.NetworkManager.Request, request => this.emit(Events.Page.Request, request)),
|
||||
helper.addEventListener(this._networkManager, Events.NetworkManager.Response, response => this.emit(Events.Page.Response, response)),
|
||||
helper.addEventListener(this._networkManager, Events.NetworkManager.RequestFinished, request => this.emit(Events.Page.RequestFinished, request)),
|
||||
helper.addEventListener(this._networkManager, Events.NetworkManager.RequestFailed, request => this.emit(Events.Page.RequestFailed, request)),
|
||||
];
|
||||
this._viewport = null;
|
||||
this._target._isClosedPromise.then(() => {
|
||||
this._closed = true;
|
||||
this._frameManager.dispose();
|
||||
this._networkManager.dispose();
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
this.emit(Events.Page.Close);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<string>} urls
|
||||
* @return {!Promise<!Array<Network.Cookie>>}
|
||||
*/
|
||||
async cookies(...urls) {
|
||||
const connection = Connection.fromSession(this._session);
|
||||
return (await connection.send('Browser.getCookies', {
|
||||
browserContextId: this._target._context._browserContextId || undefined,
|
||||
urls: urls.length ? urls : [this.url()]
|
||||
})).cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Protocol.Network.deleteCookiesParameters>} cookies
|
||||
*/
|
||||
async deleteCookie(...cookies) {
|
||||
const pageURL = this.url();
|
||||
const items = [];
|
||||
for (const cookie of cookies) {
|
||||
const item = {
|
||||
url: cookie.url,
|
||||
domain: cookie.domain,
|
||||
path: cookie.path,
|
||||
name: cookie.name,
|
||||
};
|
||||
if (!item.url && pageURL.startsWith('http'))
|
||||
item.url = pageURL;
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
const connection = Connection.fromSession(this._session);
|
||||
await connection.send('Browser.deleteCookies', {
|
||||
browserContextId: this._target._context._browserContextId || undefined,
|
||||
cookies: items,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Network.CookieParam>} cookies
|
||||
*/
|
||||
async setCookie(...cookies) {
|
||||
const pageURL = this.url();
|
||||
const startsWithHTTP = pageURL.startsWith('http');
|
||||
const items = cookies.map(cookie => {
|
||||
const item = Object.assign({}, cookie);
|
||||
if (!item.url && startsWithHTTP)
|
||||
item.url = pageURL;
|
||||
assert(item.url !== 'about:blank', `Blank page can not have cookie "${item.name}"`);
|
||||
assert(!String.prototype.startsWith.call(item.url || '', 'data:'), `Data URL page can not have cookie "${item.name}"`);
|
||||
return item;
|
||||
});
|
||||
await this.deleteCookie(...items);
|
||||
if (items.length) {
|
||||
const connection = Connection.fromSession(this._session);
|
||||
await connection.send('Browser.setCookies', {
|
||||
browserContextId: this._target._context._browserContextId || undefined,
|
||||
cookies: items
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setRequestInterception(enabled) {
|
||||
await this._networkManager.setRequestInterception(enabled);
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(headers) {
|
||||
await this._networkManager.setExtraHTTPHeaders(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?string} type
|
||||
*/
|
||||
async emulateMediaType(type) {
|
||||
assert(type === 'screen' || type === 'print' || type === null, 'Unsupported media type: ' + type);
|
||||
await this._session.send('Page.setEmulatedMedia', {media: type || ''});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Function} puppeteerFunction
|
||||
*/
|
||||
async exposeFunction(name, puppeteerFunction) {
|
||||
if (this._pageBindings.has(name))
|
||||
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
|
||||
this._pageBindings.set(name, puppeteerFunction);
|
||||
|
||||
const expression = helper.evaluationString(addPageBinding, name);
|
||||
await this._session.send('Page.addBinding', {name: name});
|
||||
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: expression});
|
||||
await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError)));
|
||||
|
||||
function addPageBinding(bindingName) {
|
||||
const binding = window[bindingName];
|
||||
window[bindingName] = (...args) => {
|
||||
const me = window[bindingName];
|
||||
let callbacks = me['callbacks'];
|
||||
if (!callbacks) {
|
||||
callbacks = new Map();
|
||||
me['callbacks'] = callbacks;
|
||||
}
|
||||
const seq = (me['lastSeq'] || 0) + 1;
|
||||
me['lastSeq'] = seq;
|
||||
const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject}));
|
||||
binding(JSON.stringify({name: bindingName, seq, args}));
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Runtime.bindingCalledPayload} event
|
||||
*/
|
||||
async _onBindingCalled(event) {
|
||||
const {name, seq, args} = JSON.parse(event.payload);
|
||||
let expression = null;
|
||||
try {
|
||||
const result = await this._pageBindings.get(name)(...args);
|
||||
expression = helper.evaluationString(deliverResult, name, seq, result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error)
|
||||
expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack);
|
||||
else
|
||||
expression = helper.evaluationString(deliverErrorValue, name, seq, error);
|
||||
}
|
||||
this._session.send('Runtime.evaluate', { expression, executionContextId: event.executionContextId }).catch(debugError);
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number} seq
|
||||
* @param {*} result
|
||||
*/
|
||||
function deliverResult(name, seq, result) {
|
||||
window[name]['callbacks'].get(seq).resolve(result);
|
||||
window[name]['callbacks'].delete(seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number} seq
|
||||
* @param {string} message
|
||||
* @param {string} stack
|
||||
*/
|
||||
function deliverError(name, seq, message, stack) {
|
||||
const error = new Error(message);
|
||||
error.stack = stack;
|
||||
window[name]['callbacks'].get(seq).reject(error);
|
||||
window[name]['callbacks'].delete(seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number} seq
|
||||
* @param {*} value
|
||||
*/
|
||||
function deliverErrorValue(name, seq, value) {
|
||||
window[name]['callbacks'].get(seq).reject(value);
|
||||
window[name]['callbacks'].delete(seq);
|
||||
}
|
||||
}
|
||||
|
||||
_sessionClosePromise() {
|
||||
if (!this._disconnectPromise)
|
||||
this._disconnectPromise = new Promise(fulfill => this._session.once(Events.JugglerSession.Disconnected, () => fulfill(new Error('Target closed'))));
|
||||
return this._disconnectPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|Function)} urlOrPredicate
|
||||
* @param {!{timeout?: number}=} options
|
||||
* @return {!Promise<!Puppeteer.Request>}
|
||||
*/
|
||||
async waitForRequest(urlOrPredicate, options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
return helper.waitForEvent(this._networkManager, Events.NetworkManager.Request, request => {
|
||||
if (helper.isString(urlOrPredicate))
|
||||
return (urlOrPredicate === request.url());
|
||||
if (typeof urlOrPredicate === 'function')
|
||||
return !!(urlOrPredicate(request));
|
||||
return false;
|
||||
}, timeout, this._sessionClosePromise());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|Function)} urlOrPredicate
|
||||
* @param {!{timeout?: number}=} options
|
||||
* @return {!Promise<!Puppeteer.Response>}
|
||||
*/
|
||||
async waitForResponse(urlOrPredicate, options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
return helper.waitForEvent(this._networkManager, Events.NetworkManager.Response, response => {
|
||||
if (helper.isString(urlOrPredicate))
|
||||
return (urlOrPredicate === response.url());
|
||||
if (typeof urlOrPredicate === 'function')
|
||||
return !!(urlOrPredicate(response));
|
||||
return false;
|
||||
}, timeout, this._sessionClosePromise());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userAgent
|
||||
*/
|
||||
async setUserAgent(userAgent) {
|
||||
await this._session.send('Page.setUserAgent', {userAgent});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userAgent
|
||||
*/
|
||||
async setJavaScriptEnabled(enabled) {
|
||||
await this._session.send('Page.setJavascriptEnabled', {enabled});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userAgent
|
||||
*/
|
||||
async setCacheEnabled(enabled) {
|
||||
await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{viewport: !Puppeteer.Viewport, userAgent: string}} options
|
||||
*/
|
||||
async emulate(options) {
|
||||
await Promise.all([
|
||||
this.setViewport(options.viewport),
|
||||
this.setUserAgent(options.userAgent),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {BrowserContext}
|
||||
*/
|
||||
browserContext() {
|
||||
return this._target.browserContext();
|
||||
}
|
||||
|
||||
_onUncaughtError(params) {
|
||||
const error = new Error(params.message);
|
||||
error.stack = params.stack;
|
||||
this.emit(Events.Page.PageError, error);
|
||||
}
|
||||
|
||||
viewport() {
|
||||
return this._viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Viewport} viewport
|
||||
*/
|
||||
async setViewport(viewport) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
isMobile = false,
|
||||
deviceScaleFactor = 1,
|
||||
hasTouch = false,
|
||||
isLandscape = false,
|
||||
} = viewport;
|
||||
await this._session.send('Page.setViewport', {
|
||||
viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape },
|
||||
});
|
||||
const oldIsMobile = this._viewport ? this._viewport.isMobile : false;
|
||||
const oldHasTouch = this._viewport ? this._viewport.hasTouch : false;
|
||||
this._viewport = viewport;
|
||||
if (oldIsMobile !== isMobile || oldHasTouch !== hasTouch)
|
||||
await this.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function()|string} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
*/
|
||||
async evaluateOnNewDocument(pageFunction, ...args) {
|
||||
const script = helper.evaluationString(pageFunction, ...args);
|
||||
await this._session.send('Page.addScriptToEvaluateOnNewDocument', { script });
|
||||
}
|
||||
|
||||
browser() {
|
||||
return this._target.browser();
|
||||
}
|
||||
|
||||
target() {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._frameManager.mainFrame().url();
|
||||
}
|
||||
|
||||
frames() {
|
||||
return this._frameManager.frames();
|
||||
}
|
||||
|
||||
_onDialogOpened(params) {
|
||||
this.emit(Events.Page.Dialog, new Dialog(this._session, params));
|
||||
}
|
||||
|
||||
mainFrame() {
|
||||
return this._frameManager.mainFrame();
|
||||
}
|
||||
|
||||
get accessibility() {
|
||||
return this._accessibility;
|
||||
}
|
||||
|
||||
get keyboard(){
|
||||
return this._keyboard;
|
||||
}
|
||||
|
||||
get mouse(){
|
||||
return this._mouse;
|
||||
}
|
||||
|
||||
get touchscreen(){
|
||||
return this._touchscreen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async waitForNavigation(options = {}) {
|
||||
return this._frameManager.mainFrame().waitForNavigation(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async goto(url, options = {}) {
|
||||
return this._frameManager.mainFrame().goto(url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async goBack(options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
} = options;
|
||||
const frame = this._frameManager.mainFrame();
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId, navigationURL} = await this._session.send('Page.goBack', {
|
||||
frameId: frame._frameId,
|
||||
});
|
||||
if (!navigationId)
|
||||
return null;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async goForward(options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
} = options;
|
||||
const frame = this._frameManager.mainFrame();
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId, navigationURL} = await this._session.send('Page.goForward', {
|
||||
frameId: frame._frameId,
|
||||
});
|
||||
if (!navigationId)
|
||||
return null;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async reload(options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
} = options;
|
||||
const frame = this._frameManager.mainFrame();
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId, navigationURL} = await this._session.send('Page.reload', {
|
||||
frameId: frame._frameId,
|
||||
});
|
||||
if (!navigationId)
|
||||
return null;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{fullPage?: boolean, clip?: {width: number, height: number, x: number, y: number}, encoding?: string, path?: string}} options
|
||||
* @return {Promise<string|Buffer>}
|
||||
*/
|
||||
async screenshot(options = {}) {
|
||||
const {data} = await this._session.send('Page.screenshot', {
|
||||
mimeType: getScreenshotMimeType(options),
|
||||
fullPage: options.fullPage,
|
||||
clip: processClip(options.clip),
|
||||
});
|
||||
const buffer = options.encoding === 'base64' ? data : Buffer.from(data, 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
|
||||
function processClip(clip) {
|
||||
if (!clip)
|
||||
return undefined;
|
||||
const x = Math.round(clip.x);
|
||||
const y = Math.round(clip.y);
|
||||
const width = Math.round(clip.width + clip.x - x);
|
||||
const height = Math.round(clip.height + clip.y - y);
|
||||
return {x, y, width, height};
|
||||
}
|
||||
}
|
||||
|
||||
async evaluate(pageFunction, ...args) {
|
||||
return await this._frameManager.mainFrame().evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, type?: string, url?: string, id?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addScriptTag(options) {
|
||||
return await this._frameManager.mainFrame().addScriptTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addStyleTag(options) {
|
||||
return await this._frameManager.mainFrame().addStyleTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(selector, options = {}) {
|
||||
return await this._frameManager.mainFrame().click(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
tap(selector) {
|
||||
return this.mainFrame().tap(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(selector, text, options) {
|
||||
return await this._frameManager.mainFrame().type(selector, text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async focus(selector) {
|
||||
return await this._frameManager.mainFrame().focus(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async hover(selector) {
|
||||
return await this._frameManager.mainFrame().hover(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
||||
* @param {!{polling?: string|number, timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
async waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
||||
return await this._frameManager.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!{polling?: string|number, timeout?: number}=} options
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
async waitForFunction(pageFunction, options = {}, ...args) {
|
||||
return await this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async waitForSelector(selector, options = {}) {
|
||||
return await this._frameManager.mainFrame().waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async waitForXPath(xpath, options = {}) {
|
||||
return await this._frameManager.mainFrame().waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async title() {
|
||||
return await this._frameManager.mainFrame().title();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
return await this._frameManager.mainFrame().$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
return await this._frameManager.mainFrame().$$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
return await this._frameManager.mainFrame().$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
return await this._frameManager.mainFrame().$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
return await this._frameManager.mainFrame().$x(expression);
|
||||
}
|
||||
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
return await this._frameManager.mainFrame().evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!Array<string>} values
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
async select(selector, ...values) {
|
||||
return await this._frameManager.mainFrame().select(selector, ...values);
|
||||
}
|
||||
|
||||
async close(options = {}) {
|
||||
const {
|
||||
runBeforeUnload = false,
|
||||
} = options;
|
||||
await this._session.send('Page.close', { runBeforeUnload });
|
||||
if (!runBeforeUnload)
|
||||
await this._target._isClosedPromise;
|
||||
}
|
||||
|
||||
async content() {
|
||||
return await this._frameManager.mainFrame().content();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
async setContent(html) {
|
||||
return await this._frameManager.mainFrame().setContent(html);
|
||||
}
|
||||
|
||||
_onConsole({type, args, executionContextId, location}) {
|
||||
const context = this._frameManager.executionContextById(executionContextId);
|
||||
this.emit(Events.Page.Console, new ConsoleMessage(type, args.map(arg => createHandle(context, arg)), location));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isClosed() {
|
||||
return this._closed;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose alias for deprecated method.
|
||||
Page.prototype.emulateMedia = Page.prototype.emulateMediaType;
|
||||
|
||||
class ConsoleMessage {
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {!Array<!JSHandle>} args
|
||||
*/
|
||||
constructor(type, args, location) {
|
||||
this._type = type;
|
||||
this._args = args;
|
||||
this._location = location;
|
||||
}
|
||||
|
||||
location() {
|
||||
return this._location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!JSHandle>}
|
||||
*/
|
||||
args() {
|
||||
return this._args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
text() {
|
||||
return this._args.map(arg => {
|
||||
if (arg._objectId)
|
||||
return arg.toString();
|
||||
return arg._deserializeValue(arg._protocolValue);
|
||||
}).join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
function getScreenshotMimeType(options) {
|
||||
// options.type takes precedence over inferring the type from options.path
|
||||
// because it may be a 0-length file with no extension created beforehand (i.e. as a temp file).
|
||||
if (options.type) {
|
||||
if (options.type === 'png')
|
||||
return 'image/png';
|
||||
if (options.type === 'jpeg')
|
||||
return 'image/jpeg';
|
||||
throw new Error('Unknown options.type value: ' + options.type);
|
||||
}
|
||||
if (options.path) {
|
||||
const fileType = mime.getType(options.path);
|
||||
if (fileType === 'image/png' || fileType === 'image/jpeg')
|
||||
return fileType;
|
||||
throw new Error('Unsupported screenshot mime type: ' + fileType);
|
||||
}
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
module.exports = {Page, ConsoleMessage};
|
@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Copyright 2019 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const {Launcher} = require('./Launcher.js');
|
||||
const {BrowserFetcher} = require('./BrowserFetcher.js');
|
||||
const Errors = require('./Errors');
|
||||
const DeviceDescriptors = require('./DeviceDescriptors');
|
||||
|
||||
class Puppeteer {
|
||||
/**
|
||||
* @param {string} projectRoot
|
||||
* @param {string} preferredRevision
|
||||
*/
|
||||
constructor(projectRoot, preferredRevision) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._launcher = new Launcher(projectRoot, preferredRevision);
|
||||
}
|
||||
|
||||
async launch(options = {}) {
|
||||
return this._launcher.launch(options);
|
||||
}
|
||||
|
||||
async connect(options) {
|
||||
return this._launcher.connect(options);
|
||||
}
|
||||
|
||||
createBrowserFetcher(options) {
|
||||
return new BrowserFetcher(this._projectRoot, options);
|
||||
}
|
||||
|
||||
executablePath() {
|
||||
return this._launcher.executablePath();
|
||||
}
|
||||
|
||||
defaultArgs(options) {
|
||||
return this._launcher.defaultArgs(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object}
|
||||
*/
|
||||
get devices() {
|
||||
return DeviceDescriptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object}
|
||||
*/
|
||||
get errors() {
|
||||
return Errors;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Puppeteer};
|
@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Copyright 2019 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
class TimeoutSettings {
|
||||
constructor() {
|
||||
this._defaultTimeout = null;
|
||||
this._defaultNavigationTimeout = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultTimeout(timeout) {
|
||||
this._defaultTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._defaultNavigationTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
navigationTimeout() {
|
||||
if (this._defaultNavigationTimeout !== null)
|
||||
return this._defaultNavigationTimeout;
|
||||
if (this._defaultTimeout !== null)
|
||||
return this._defaultTimeout;
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
timeout() {
|
||||
if (this._defaultTimeout !== null)
|
||||
return this._defaultTimeout;
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {TimeoutSettings};
|
@ -1,281 +0,0 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} KeyDefinition
|
||||
* @property {number=} keyCode
|
||||
* @property {number=} shiftKeyCode
|
||||
* @property {string=} key
|
||||
* @property {string=} shiftKey
|
||||
* @property {string=} code
|
||||
* @property {string=} text
|
||||
* @property {string=} shiftText
|
||||
* @property {number=} location
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Object<string, KeyDefinition>}
|
||||
*/
|
||||
module.exports = {
|
||||
'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'}
|
||||
};
|
@ -1,102 +0,0 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const WebSocket = require('ws');
|
||||
|
||||
/**
|
||||
* @implements {!Puppeteer.ConnectionTransport}
|
||||
*/
|
||||
class WebSocketTransport {
|
||||
/**
|
||||
* @param {string} url
|
||||
* @return {!Promise<!WebSocketTransport>}
|
||||
*/
|
||||
static create(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(url, [], { perMessageDeflate: false });
|
||||
ws.addEventListener('open', () => resolve(new WebSocketTransport(ws)));
|
||||
ws.addEventListener('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!WebSocket} ws
|
||||
*/
|
||||
constructor(ws) {
|
||||
this._ws = ws;
|
||||
this._dispatchQueue = new DispatchQueue(this);
|
||||
this._ws.addEventListener('message', event => {
|
||||
this._dispatchQueue.enqueue(event.data);
|
||||
});
|
||||
this._ws.addEventListener('close', event => {
|
||||
if (this.onclose)
|
||||
this.onclose.call(null);
|
||||
});
|
||||
// Silently ignore all errors - we don't know what to do with them.
|
||||
this._ws.addEventListener('error', () => {});
|
||||
this.onmessage = null;
|
||||
this.onclose = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
send(message) {
|
||||
this._ws.send(message);
|
||||
}
|
||||
|
||||
close() {
|
||||
this._ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
// We want to dispatch all "message" events in separate tasks
|
||||
// to make sure all message-related promises are resolved first
|
||||
// before dispatching next message.
|
||||
//
|
||||
// We cannot just use setTimeout() in Node.js here like we would
|
||||
// do in Browser - see https://github.com/nodejs/node/issues/23773
|
||||
// Thus implement a dispatch queue that enforces new tasks manually.
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class DispatchQueue {
|
||||
constructor(transport) {
|
||||
this._transport = transport;
|
||||
|
||||
this._timeoutId = null;
|
||||
this._queue = [];
|
||||
this._dispatch = this._dispatch.bind(this);
|
||||
}
|
||||
|
||||
enqueue(message) {
|
||||
this._queue.push(message);
|
||||
if (!this._timeoutId)
|
||||
this._timeoutId = setTimeout(this._dispatch, 0);
|
||||
}
|
||||
|
||||
_dispatch() {
|
||||
const message = this._queue.shift();
|
||||
if (this._queue.length)
|
||||
this._timeoutId = setTimeout(this._dispatch, 0)
|
||||
else
|
||||
this._timeoutId = null;
|
||||
|
||||
if (this._transport.onmessage)
|
||||
this._transport.onmessage.call(null, message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocketTransport;
|
@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
Accessibility: require('./Accessibility').Accessibility,
|
||||
Browser: require('./Browser').Browser,
|
||||
BrowserContext: require('./Browser').BrowserContext,
|
||||
BrowserFetcher: require('./BrowserFetcher').BrowserFetcher,
|
||||
ConsoleMessage: require('./Page').ConsoleMessage,
|
||||
Dialog: require('./Dialog').Dialog,
|
||||
ElementHandle: require('./JSHandle').ElementHandle,
|
||||
ExecutionContext: require('./ExecutionContext').ExecutionContext,
|
||||
Frame: require('./FrameManager').Frame,
|
||||
JSHandle: require('./JSHandle').JSHandle,
|
||||
Keyboard: require('./Input').Keyboard,
|
||||
Mouse: require('./Input').Mouse,
|
||||
Page: require('./Page').Page,
|
||||
Puppeteer: require('./Puppeteer').Puppeteer,
|
||||
Request: require('./NetworkManager').Request,
|
||||
Response: require('./NetworkManager').Response,
|
||||
SecurityDetails: require('./NetworkManager').SecurityDetails,
|
||||
Target: require('./Browser').Target,
|
||||
Touchscreen: require('./Input').Touchscreen,
|
||||
TimeoutError: require('./Errors').TimeoutError,
|
||||
};
|
28
experimental/puppeteer-firefox/lib/externs.d.ts
vendored
28
experimental/puppeteer-firefox/lib/externs.d.ts
vendored
@ -1,28 +0,0 @@
|
||||
import { Connection as RealConnection } from './Connection';
|
||||
import { Target as RealTarget } from './Browser';
|
||||
import * as child_process from 'child_process';
|
||||
declare global {
|
||||
module Puppeteer {
|
||||
|
||||
export interface ConnectionTransport {
|
||||
send(string);
|
||||
close();
|
||||
onmessage?: (message: string) => void,
|
||||
onclose?: () => void,
|
||||
}
|
||||
|
||||
export interface ChildProcess extends child_process.ChildProcess { }
|
||||
|
||||
export type Viewport = {
|
||||
width: number;
|
||||
height: number;
|
||||
deviceScaleFactor?: number;
|
||||
isMobile?: boolean;
|
||||
isLandscape?: boolean;
|
||||
hasTouch?: boolean;
|
||||
}
|
||||
|
||||
export class Connection extends RealConnection { }
|
||||
export class Target extends RealTarget { }
|
||||
}
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const {TimeoutError} = require('./Errors');
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Helper {
|
||||
/**
|
||||
* @param {!Object} classType
|
||||
*/
|
||||
static installAsyncStackHooks(classType) {
|
||||
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')
|
||||
continue;
|
||||
Reflect.set(classType.prototype, methodName, function(...args) {
|
||||
const syncStack = {};
|
||||
Error.captureStackTrace(syncStack);
|
||||
return method.call(this, ...args).catch(e => {
|
||||
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
|
||||
const clientStack = stack.substring(stack.indexOf('\n'));
|
||||
if (e instanceof Error && e.stack && !e.stack.includes(clientStack))
|
||||
e.stack += '\n -- ASYNC --\n' + stack;
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} fun
|
||||
* @param {!Array<*>} args
|
||||
* @return {string}
|
||||
*/
|
||||
static evaluationString(fun, ...args) {
|
||||
if (Helper.isString(fun)) {
|
||||
if (args.length !== 0)
|
||||
throw new Error('Cannot evaluate a string with arguments');
|
||||
return /** @type {string} */ (fun);
|
||||
}
|
||||
return `(${fun})(${args.map(serializeArgument).join(',')})`;
|
||||
|
||||
/**
|
||||
* @param {*} arg
|
||||
* @return {string}
|
||||
*/
|
||||
function serializeArgument(arg) {
|
||||
if (Object.is(arg, undefined))
|
||||
return 'undefined';
|
||||
return JSON.stringify(arg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function} nodeFunction
|
||||
* @return {function}
|
||||
*/
|
||||
static promisify(nodeFunction) {
|
||||
function promisified(...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function callback(err, ...result) {
|
||||
if (err)
|
||||
return reject(err);
|
||||
if (result.length === 1)
|
||||
return resolve(result[0]);
|
||||
return resolve(result);
|
||||
}
|
||||
nodeFunction.call(null, ...args, callback);
|
||||
});
|
||||
}
|
||||
return promisified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Object} obj
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isNumber(obj) {
|
||||
return typeof obj === 'number' || obj instanceof Number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Object} obj
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isString(obj) {
|
||||
return typeof obj === 'string' || obj instanceof String;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!NodeJS.EventEmitter} emitter
|
||||
* @param {(string|symbol)} eventName
|
||||
* @param {function(?)} handler
|
||||
* @return {{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}}
|
||||
*/
|
||||
static addEventListener(emitter, eventName, handler) {
|
||||
emitter.on(eventName, handler);
|
||||
return { emitter, eventName, handler };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}>} listeners
|
||||
*/
|
||||
static removeEventListeners(listeners) {
|
||||
for (const listener of listeners)
|
||||
listener.emitter.removeListener(listener.eventName, listener.handler);
|
||||
listeners.splice(0, listeners.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!NodeJS.EventEmitter} emitter
|
||||
* @param {(string|symbol)} eventName
|
||||
* @param {function} predicate
|
||||
* @param {number} timeout
|
||||
* @param {!Promise<!Error>} abortPromise
|
||||
* @return {!Promise}
|
||||
*/
|
||||
static async waitForEvent(emitter, eventName, predicate, timeout, abortPromise) {
|
||||
let eventTimeout, resolveCallback, rejectCallback;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
resolveCallback = resolve;
|
||||
rejectCallback = reject;
|
||||
});
|
||||
const listener = Helper.addEventListener(emitter, eventName, event => {
|
||||
if (!predicate(event))
|
||||
return;
|
||||
resolveCallback(event);
|
||||
});
|
||||
if (timeout) {
|
||||
eventTimeout = setTimeout(() => {
|
||||
rejectCallback(new TimeoutError('Timeout exceeded while waiting for event'));
|
||||
}, timeout);
|
||||
}
|
||||
function cleanup() {
|
||||
Helper.removeEventListeners([listener]);
|
||||
clearTimeout(eventTimeout);
|
||||
}
|
||||
const result = await Promise.race([promise, abortPromise]).then(r => {
|
||||
cleanup();
|
||||
return r;
|
||||
}, e => {
|
||||
cleanup();
|
||||
throw e;
|
||||
});
|
||||
if (result instanceof Error)
|
||||
throw result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {!Promise<T>} promise
|
||||
* @param {string} taskName
|
||||
* @param {number} timeout
|
||||
* @return {!Promise<T>}
|
||||
*/
|
||||
static async waitWithTimeout(promise, taskName, timeout) {
|
||||
let reject;
|
||||
const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`);
|
||||
const timeoutPromise = new Promise((resolve, x) => reject = x);
|
||||
const timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
|
||||
try {
|
||||
return await Promise.race([promise, timeoutPromise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, errorText) {
|
||||
if (!condition)
|
||||
throw new Error(errorText);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
helper: Helper,
|
||||
debugError: require('debug')(`puppeteer:error`),
|
||||
assert,
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
// Any comment. You must start the file with a single-line comment!
|
||||
pref("general.config.filename", "puppeteer.cfg");
|
||||
pref("general.config.obscure_value", 0);
|
@ -1,59 +0,0 @@
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
// Install browser preferences after downloading and unpacking
|
||||
// firefox instances.
|
||||
// Based on: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Enterprise_deployment_before_60#Configuration
|
||||
async function installFirefoxPreferences(executablePath) {
|
||||
const firefoxFolder = path.dirname(executablePath);
|
||||
const mkdirAsync = util.promisify(fs.mkdir.bind(fs));
|
||||
|
||||
let prefPath = '';
|
||||
let configPath = '';
|
||||
if (os.platform() === 'darwin') {
|
||||
prefPath = path.join(firefoxFolder, '..', 'Resources', 'defaults', 'pref');
|
||||
configPath = path.join(firefoxFolder, '..', 'Resources');
|
||||
} else if (os.platform() === 'linux') {
|
||||
if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults')))
|
||||
await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults'));
|
||||
if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences')))
|
||||
await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences'));
|
||||
prefPath = path.join(firefoxFolder, 'browser', 'defaults', 'preferences');
|
||||
configPath = firefoxFolder;
|
||||
} else if (os.platform() === 'win32') {
|
||||
prefPath = path.join(firefoxFolder, 'defaults', 'pref');
|
||||
configPath = firefoxFolder;
|
||||
} else {
|
||||
throw new Error('Unsupported platform: ' + os.platform());
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
copyFile({
|
||||
from: path.join(__dirname, '00-puppeteer-prefs.js'),
|
||||
to: path.join(prefPath, '00-puppeteer-prefs.js'),
|
||||
}),
|
||||
copyFile({
|
||||
from: path.join(__dirname, 'puppeteer.cfg'),
|
||||
to: path.join(configPath, 'puppeteer.cfg'),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
function copyFile({from, to}) {
|
||||
var rd = fs.createReadStream(from);
|
||||
var wr = fs.createWriteStream(to);
|
||||
return new Promise(function(resolve, reject) {
|
||||
rd.on('error', reject);
|
||||
wr.on('error', reject);
|
||||
wr.on('finish', resolve);
|
||||
rd.pipe(wr);
|
||||
}).catch(function(error) {
|
||||
rd.destroy();
|
||||
wr.end();
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = installFirefoxPreferences;
|
@ -1,212 +0,0 @@
|
||||
// Any comment. You must start the file with a comment!
|
||||
|
||||
// Make sure Shield doesn't hit the network.
|
||||
// pref("app.normandy.api_url", "");
|
||||
pref("app.normandy.enabled", false);
|
||||
|
||||
// Disable updater
|
||||
pref("app.update.enabled", false);
|
||||
// make absolutely sure it is really off
|
||||
pref("app.update.auto", false);
|
||||
pref("app.update.mode", 0);
|
||||
pref("app.update.service.enabled", false);
|
||||
|
||||
// Dislabe newtabpage
|
||||
pref("browser.startup.homepage", 'about:blank');
|
||||
pref("browser.newtabpage.enabled", false);
|
||||
// Disable topstories
|
||||
pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
|
||||
|
||||
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
|
||||
// This doesn't affect Puppeteer operations, but spams console with a lot of
|
||||
// unpleasant errors.
|
||||
// (bug 1424372)
|
||||
pref("devtools.jsonview.enabled", false);
|
||||
|
||||
// Increase the APZ content response timeout in tests to 1 minute.
|
||||
// This is to accommodate the fact that test environments tends to be
|
||||
// slower than production environments (with the b2g emulator being
|
||||
// the slowest of them all), resulting in the production timeout value
|
||||
// sometimes being exceeded and causing false-positive test failures.
|
||||
//
|
||||
// (bug 1176798, bug 1177018, bug 1210465)
|
||||
pref("apz.content_response_timeout", 60000);
|
||||
|
||||
// Allow creating files in content process - required for
|
||||
// |Page.setFileInputFiles| protocol method.
|
||||
pref("dom.file.createInChild", true);
|
||||
|
||||
// Indicate that the download panel has been shown once so that
|
||||
// whichever download test runs first doesn't show the popup
|
||||
// inconsistently.
|
||||
pref("browser.download.panel.shown", true);
|
||||
|
||||
// Background thumbnails in particular cause grief, and disabling
|
||||
// thumbnails in general cannot hurt
|
||||
pref("browser.pagethumbnails.capturing_disabled", true);
|
||||
|
||||
// Disable safebrowsing components.
|
||||
pref("browser.safebrowsing.blockedURIs.enabled", false);
|
||||
pref("browser.safebrowsing.downloads.enabled", false);
|
||||
pref("browser.safebrowsing.passwords.enabled", false);
|
||||
pref("browser.safebrowsing.malware.enabled", false);
|
||||
pref("browser.safebrowsing.phishing.enabled", false);
|
||||
|
||||
// Disable updates to search engines.
|
||||
pref("browser.search.update", false);
|
||||
|
||||
// Do not restore the last open set of tabs if the browser has crashed
|
||||
pref("browser.sessionstore.resume_from_crash", false);
|
||||
|
||||
// Don't check for the default web browser during startup.
|
||||
pref("browser.shell.checkDefaultBrowser", false);
|
||||
|
||||
// Do not redirect user when a milstone upgrade of Firefox is detected
|
||||
pref("browser.startup.homepage_override.mstone", "ignore");
|
||||
|
||||
// Disable browser animations (tabs, fullscreen, sliding alerts)
|
||||
pref("toolkit.cosmeticAnimations.enabled", false);
|
||||
|
||||
// Close the window when the last tab gets closed
|
||||
pref("browser.tabs.closeWindowWithLastTab", true);
|
||||
|
||||
// Do not allow background tabs to be zombified on Android, otherwise for
|
||||
// tests that open additional tabs, the test harness tab itself might get
|
||||
// unloaded
|
||||
pref("browser.tabs.disableBackgroundZombification", false);
|
||||
|
||||
// Do not warn when closing all open tabs
|
||||
pref("browser.tabs.warnOnClose", false);
|
||||
|
||||
// Do not warn when closing all other open tabs
|
||||
pref("browser.tabs.warnOnCloseOtherTabs", false);
|
||||
|
||||
// Do not warn when multiple tabs will be opened
|
||||
pref("browser.tabs.warnOnOpen", false);
|
||||
|
||||
// Disable first run splash page on Windows 10
|
||||
pref("browser.usedOnWindows10.introURL", "");
|
||||
|
||||
// Disable the UI tour.
|
||||
//
|
||||
// Should be set in profile.
|
||||
pref("browser.uitour.enabled", false);
|
||||
|
||||
// Turn off search suggestions in the location bar so as not to trigger
|
||||
// network connections.
|
||||
pref("browser.urlbar.suggest.searches", false);
|
||||
|
||||
// Do not warn on quitting Firefox
|
||||
pref("browser.warnOnQuit", false);
|
||||
|
||||
// Do not show datareporting policy notifications which can
|
||||
// interfere with tests
|
||||
pref(
|
||||
"datareporting.healthreport.documentServerURI",
|
||||
"http://%(server)s/dummy/healthreport/",
|
||||
);
|
||||
pref("datareporting.healthreport.logging.consoleEnabled", false);
|
||||
pref("datareporting.healthreport.service.enabled", false);
|
||||
pref("datareporting.healthreport.service.firstRun", false);
|
||||
pref("datareporting.healthreport.uploadEnabled", false);
|
||||
pref("datareporting.policy.dataSubmissionEnabled", false);
|
||||
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
|
||||
pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
|
||||
|
||||
// Automatically unload beforeunload alerts
|
||||
pref("dom.disable_beforeunload", false);
|
||||
|
||||
// Disable popup-blocker
|
||||
pref("dom.disable_open_during_load", false);
|
||||
|
||||
// Disable the ProcessHangMonitor
|
||||
pref("dom.ipc.reportProcessHangs", false);
|
||||
|
||||
// Disable slow script dialogues
|
||||
pref("dom.max_chrome_script_run_time", 0);
|
||||
pref("dom.max_script_run_time", 0);
|
||||
|
||||
// Only load extensions from the application and user profile
|
||||
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
|
||||
pref("extensions.autoDisableScopes", 0);
|
||||
pref("extensions.enabledScopes", 5);
|
||||
|
||||
// Disable metadata caching for installed add-ons by default
|
||||
pref("extensions.getAddons.cache.enabled", false);
|
||||
|
||||
// Disable installing any distribution extensions or add-ons.
|
||||
pref("extensions.installDistroAddons", false);
|
||||
|
||||
// Turn off extension updates so they do not bother tests
|
||||
pref("extensions.update.enabled", false);
|
||||
pref("extensions.update.notifyUser", false);
|
||||
|
||||
// Make sure opening about:addons will not hit the network
|
||||
pref(
|
||||
"extensions.webservice.discoverURL",
|
||||
"http://%(server)s/dummy/discoveryURL",
|
||||
);
|
||||
|
||||
pref("extensions.screenshots.disabled", true);
|
||||
pref("extensions.screenshots.upload-disabled", true);
|
||||
|
||||
// Allow the application to have focus even it runs in the background
|
||||
pref("focusmanager.testmode", true);
|
||||
|
||||
// Disable useragent updates
|
||||
pref("general.useragent.updates.enabled", false);
|
||||
|
||||
// Always use network provider for geolocation tests so we bypass the
|
||||
// macOS dialog raised by the corelocation provider
|
||||
pref("geo.provider.testing", true);
|
||||
|
||||
// Do not scan Wifi
|
||||
pref("geo.wifi.scan", false);
|
||||
|
||||
// Show chrome errors and warnings in the error console
|
||||
pref("javascript.options.showInConsole", true);
|
||||
|
||||
// Do not prompt with long usernames or passwords in URLs
|
||||
pref("network.http.phishy-userpass-length", 255);
|
||||
|
||||
// Do not prompt for temporary redirects
|
||||
pref("network.http.prompt-temp-redirect", false);
|
||||
|
||||
// Disable speculative connections so they are not reported as leaking
|
||||
// when they are hanging around
|
||||
pref("network.http.speculative-parallel-limit", 0);
|
||||
|
||||
// Do not automatically switch between offline and online
|
||||
pref("network.manage-offline-status", false);
|
||||
|
||||
// Make sure SNTP requests do not hit the network
|
||||
pref("network.sntp.pools", "%(server)s");
|
||||
|
||||
// Local documents have access to all other local documents,
|
||||
// including directory listings
|
||||
pref("security.fileuri.strict_origin_policy", false);
|
||||
|
||||
// Tests do not wait for the notification button security delay
|
||||
pref("security.notification_enable_delay", 0);
|
||||
|
||||
// Ensure blocklist updates do not hit the network
|
||||
pref("services.settings.server", "http://%(server)s/dummy/blocklist/");
|
||||
|
||||
// Do not automatically fill sign-in forms with known usernames and
|
||||
// passwords
|
||||
pref("signon.autofillForms", false);
|
||||
|
||||
// Disable password capture, so that tests that include forms are not
|
||||
// influenced by the presence of the persistent doorhanger notification
|
||||
pref("signon.rememberSignons", false);
|
||||
|
||||
// Disable first-run welcome page
|
||||
pref("startup.homepage_welcome_url", "about:blank");
|
||||
pref("startup.homepage_welcome_url.additional", "");
|
||||
|
||||
// Prevent starting into safe mode after application crashes
|
||||
pref("toolkit.startup.max_resumed_crashes", -1);
|
||||
lockPref("toolkit.crashreporter.enabled", false);
|
||||
|
||||
// Disable crash reporter.
|
||||
Components.classes["@mozilla.org/toolkit/crash-reporter;1"].getService(Components.interfaces.nsICrashReporter).submitReports = false;
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "puppeteer-firefox",
|
||||
"version": "0.5.1",
|
||||
"description": "Puppeteer API for Firefox",
|
||||
"main": "index.js",
|
||||
"repository": "github:puppeteer/puppeteer",
|
||||
"homepage": "https://github.com/puppeteer/puppeteer/tree/main/experimental/puppeteer-firefox",
|
||||
"engines": {
|
||||
"node": ">=10.18.1"
|
||||
},
|
||||
"puppeteer": {
|
||||
"firefox_revision": "v0.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "node install.js",
|
||||
"tsc": "tsc -p ."
|
||||
},
|
||||
"author": "The Chromium Authors",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0",
|
||||
"extract-zip": "^1.6.6",
|
||||
"https-proxy-agent": "^2.2.1",
|
||||
"mime": "^2.0.3",
|
||||
"progress": "^2.0.1",
|
||||
"proxy-from-env": "^1.0.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"ws": "^6.1.0"
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"target": "es2017",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"lib"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user