diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000000..b7db8e7aad --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,19 @@ +environment: + matrix: + - nodejs_version: "8.16.0" + FLAKINESS_DASHBOARD_NAME: Appveyor Chromium (Win + node8) + FLAKINESS_DASHBOARD_PASSWORD: + secure: g66jP+j6C+hkXLutBV9fdxB5fRJgcQQzy93SgQzXUmcCl/RjkJwnzyHvX0xfCVnv + +build: off + +install: + - ps: $env:FLAKINESS_DASHBOARD_BUILD_URL="https://ci.appveyor.com/project/aslushnikov/playwright/builds/$env:APPVEYOR_BUILD_ID/job/$env:APPVEYOR_JOB_ID" + - ps: Install-Product node $env:nodejs_version + - npm install + - if "%nodejs_version%" == "8.16.0" ( + npm run lint && + npm run coverage && + npm run test-doclint && + npm run test-types + ) diff --git a/.ci/node10/Dockerfile.linux b/.ci/node10/Dockerfile.linux new file mode 100644 index 0000000000..a7cf643b8d --- /dev/null +++ b/.ci/node10/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:10 + +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 diff --git a/.ci/node12/Dockerfile.linux b/.ci/node12/Dockerfile.linux new file mode 100644 index 0000000000..79f9426157 --- /dev/null +++ b/.ci/node12/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:12 + +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 diff --git a/.ci/node8/Dockerfile.linux b/.ci/node8/Dockerfile.linux new file mode 100644 index 0000000000..26f1197984 --- /dev/null +++ b/.ci/node8/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:8.11.3 + +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 diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 0000000000..41ac3430a9 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,47 @@ +env: + DISPLAY: :99.0 + FLAKINESS_DASHBOARD_PASSWORD: ENCRYPTED[b3e207db5d153b543f219d3c3b9123d8321834b783b9e45ac7d380e026ab3a56398bde51b521ac5859e7e45cb95d0992] + FLAKINESS_DASHBOARD_NAME: Cirrus ${CIRRUS_TASK_NAME} + FLAKINESS_DASHBOARD_BUILD_URL: https://cirrus-ci.com/task/${CIRRUS_TASK_ID} + +task: + matrix: + - name: Chromium (node8 + linux) + container: + dockerfile: .ci/node8/Dockerfile.linux + - name: Chromium (node10 + linux) + container: + dockerfile: .ci/node10/Dockerfile.linux + - name: Chromium (node12 + linux) + container: + dockerfile: .ci/node12/Dockerfile.linux + xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24 + install_script: npm install --unsafe-perm + lint_script: npm run lint + coverage_script: npm run coverage + test_doclint_script: npm run test-doclint + test_types_script: npm run test-types + +task: + matrix: + - name: Firefox (node8 + linux) + container: + dockerfile: .ci/node8/Dockerfile.linux + xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24 + install_script: npm install --unsafe-perm + test_script: npm run funit + +task: + osx_instance: + image: high-sierra-base + name: Chromium (node8 + macOS) + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + node_install_script: + - brew install node@8 + - brew link --force node@8 + install_script: npm install --unsafe-perm + lint_script: npm run lint + coverage_script: npm run coverage + test_doclint_script: npm run test-doclint + test_types_script: npm run test-types diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..c6c8b36219 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..28f6fcb632 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,12 @@ +test/assets/modernizr.js +third_party/* +utils/browser/playwright-web.js +utils/doclint/check_public_api/test/ +utils/testrunner/examples/ +node6/* +node6-test/* +node6-testrunner/* +lib/ +*.js +src/chromium/protocol.d.ts +src/webkit/protocol.d.ts \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..ab34834e10 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,107 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 9, + sourceType: 'module', + }, + + /** + * ESLint rules + * + * All available rules: http://eslint.org/docs/rules/ + * + * Rules take the following form: + * "rule-name", [severity, { opts }] + * Severity: 2 == error, 1 == warning, 0 == off. + */ + "rules": { + '@typescript-eslint/no-unused-vars': [2, {args: 'none'}], + /** + * Enforced rules + */ + // syntax preferences + "quotes": [2, "single", { + "avoidEscape": true, + "allowTemplateLiterals": true + }], + "semi": 2, + "no-extra-semi": 2, + "comma-style": [2, "last"], + "wrap-iife": [2, "inside"], + "spaced-comment": [2, "always", { + "markers": ["*"] + }], + "eqeqeq": [2], + "arrow-body-style": [2, "as-needed"], + "accessor-pairs": [2, { + "getWithoutSet": false, + "setWithoutGet": false + }], + "brace-style": [2, "1tbs", {"allowSingleLine": true}], + "curly": [2, "multi-or-nest", "consistent"], + "new-parens": 2, + "func-call-spacing": 2, + "arrow-parens": [2, "as-needed"], + "prefer-const": 2, + "quote-props": [2, "consistent"], + + // anti-patterns + "no-var": 2, + "no-with": 2, + "no-multi-str": 2, + "no-caller": 2, + "no-implied-eval": 2, + "no-labels": 2, + "no-new-object": 2, + "no-octal-escape": 2, + "no-self-compare": 2, + "no-shadow-restricted-names": 2, + "no-cond-assign": 2, + "no-debugger": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty-character-class": 2, + "no-unreachable": 2, + "no-unsafe-negation": 2, + "radix": 2, + "valid-typeof": 2, + "no-implicit-globals": [2], + + // es2015 features + "require-yield": 2, + "template-curly-spacing": [2, "never"], + + // spacing details + "space-infix-ops": 2, + "space-in-parens": [2, "never"], + "space-before-function-paren": [2, "never"], + "no-whitespace-before-property": 2, + "keyword-spacing": [2, { + "overrides": { + "if": {"after": true}, + "else": {"after": true}, + "for": {"after": true}, + "while": {"after": true}, + "do": {"after": true}, + "switch": {"after": true}, + "return": {"after": true} + } + }], + "arrow-spacing": [2, { + "after": true, + "before": true + }], + + // file whitespace + "no-multiple-empty-lines": [2, {"max": 2}], + "no-mixed-spaces-and-tabs": 2, + "no-trailing-spaces": 2, + "linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ], + "indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }], + "key-spacing": [2, { + "beforeColon": false + }] + } +}; diff --git a/.gitignore b/.gitignore index ad46b30886..8d2eb40ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,20 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next +/node_modules/ +/test/output-chromium +/test/output-firefox +/test/test-user-data-dir* +/.local-chromium/ +/.local-browser/ +/.local-webkit/ +/.dev_profile* +.DS_Store +*.swp +*.pyc +.vscode +package-lock.json +yarn.lock +/node6 +/src/chromium/protocol.d.ts +/src/webkit/protocol.d.ts +/utils/browser/playwright-web.js +/index.d.ts +lib/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..71a68fec8e --- /dev/null +++ b/.npmignore @@ -0,0 +1,44 @@ +.appveyor.yml +.gitattributes + +# no longer generated, but old checkouts might still have it +node6 + +# exclude all tests +test +utils/node6-transform + +# exclude source files +src + +# 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 +.travis.yml +README.md +tsconfig.json + +# exclude types, see https://github.com/GoogleChrome/puppeteer/issues/3878 +/index.d.ts diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..3cd30ce746 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,48 @@ +language: node_js +dist: trusty +addons: + apt: + packages: + # This is required to run new chrome on old trusty + - libnss3 +notifications: + email: false +cache: + directories: + - node_modules +# allow headful tests +before_install: + - "sysctl kernel.unprivileged_userns_clone=1" + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" +script: + - 'if [ "$NODE8" = "true" ]; then npm run lint; fi' + - 'if [ "$NODE8" = "true" ]; then npm run coverage; fi' + - 'if [ "$FIREFOX" = "true" ]; then npm run funit; fi' + - 'if [ "$NODE8" = "true" ]; then npm run test-doclint; fi' + - 'if [ "$NODE8" = "true" ]; then npm run test-types; fi' + - 'if [ "$NODE8" = "true" ]; then npm run bundle; fi' + - 'if [ "$NODE8" = "true" ]; then npm run unit-bundle; fi' +jobs: + include: + - node_js: "8.16.0" + env: + - NODE8=true + - FLAKINESS_DASHBOARD_NAME="Travis Chromium (node8 + linux)" + - FLAKINESS_DASHBOARD_BUILD_URL="${TRAVIS_JOB_WEB_URL}" + - node_js: "8.16.0" + env: + - FIREFOX=true + - FLAKINESS_DASHBOARD_NAME="Travis Firefox (node8 + linux)" + - FLAKINESS_DASHBOARD_BUILD_URL="${TRAVIS_JOB_WEB_URL}" +before_deploy: "npm run apply-next-version" +deploy: + provider: npm + email: aslushnikov@gmail.com + api_key: + secure: Ng8o2KwJf90XCBNgUKK3jRZnwtdBSJatjYNmZBERJEqBWFTadFAp1NdhxZaqjnuG8aFYaH5bRJdL+EQBYUksVCbrv/gcaXeEFkwsfPfVX1QXGqu7NnZmtme2hbxppLQ7dEJ8hz2Z9K4vehqVOxmLabxvoupOumxEQMLCphVHh2FOmsm/S5JrRZqZ4V9k76eIc0/PiyfXNMdx5WTZjHbIRDIHRy9nqOXjFp2Rx3PMa3uU2fS8mTshYEYs151TA6e6VdHjqmBwEQC/M5tXbDlLCMNUr4JBtLTcL4OipNYjzkwD1N2xYlbSRqtvqqF4ifdvFhoI65a31GinlMC7Z/SH1Zy+d+/z3Mo7D63eYcsJVnsg9OYxTFy2piUntr0JqTBHtQoe/CvGxJmkcVt+H6YSkcBibSG9s9tG3qpAD5wBCFqqOYnfClX+YZziEd+Hngd9inxAf87qdvgVIZ5tPD2dygtE+te2/qoEHtvccv/HuS8MxNj5iKwlP7JaBPM6uAkazYqZP2R99I2ph9gNOEVuQLtk+3+OIdb8HWrEKUrJBgKhdKY1dvcKYElI+D8NRlyzrr6BnZfudACuAt2EtfKpfJ3mL+iRMFdBJ3ntLt93xBrB+j4z3pD0iWZcg1g3I742PFzQEHzyd/DDTP1yRTUoJeQWwoQRJyNO1m6Qk4wx77c= + on: + branch: master + condition: "$NODE8 = true" + skip_cleanup: true + tag: next diff --git a/DeviceDescriptors.js b/DeviceDescriptors.js new file mode 100644 index 0000000000..b5f92b43ea --- /dev/null +++ b/DeviceDescriptors.js @@ -0,0 +1,23 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const {DeviceDescriptors} = require('./lib/DeviceDescriptors'); + +const descriptors = DeviceDescriptors.slice(); +module.exports = descriptors; +for (const device of descriptors) + module.exports[device.name] = device; diff --git a/Errors.js b/Errors.js new file mode 100644 index 0000000000..4779e1d850 --- /dev/null +++ b/Errors.js @@ -0,0 +1,17 @@ +/** + * 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. + */ + +module.exports = require('./lib/Errors'); diff --git a/browser_patches/README.md b/browser_patches/README.md new file mode 100644 index 0000000000..51c7554d68 --- /dev/null +++ b/browser_patches/README.md @@ -0,0 +1,31 @@ +# Compiling and Uploading Builds + +### 1. Getting code + +```sh +$ ./checkout.sh firefox/ # or ./checkout.sh webkit/ +``` + +This command will create a `./firefox/checkout` folder that contains firefox GIT checkout. +Checkout current branch will be set to `pwdev` and it will have all additional changes +applied to the browser atop of the `./firefox/BASE_REVISION` version. + +### 2. Compiling + +> **NOTE** You might need to prepare your host environment according to browser build instructions: +> - [firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions) +> - [webkit](https://webkit.org/building-webkit/) + +```sh +$ ./firefox/build.sh # or ./webkit/build.sh +``` + +### 3. Uploading builds to Azure CDN + +> **NOTE** You should have `$AZ_ACCOUNT_KEY` and `$AZ_ACCOUNT_NAME` variables set in your environment. + +```sh +$ ./upload.sh firefox/ # or ./upload.sh webkit/ +``` + +This will package archives and upload builds to Azure CDN. diff --git a/browser_patches/check_cdn.sh b/browser_patches/check_cdn.sh new file mode 100755 index 0000000000..9b9407589c --- /dev/null +++ b/browser_patches/check_cdn.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -e +set +x + +HOST="https://playwrightaccount.blob.core.windows.net/builds" +ARCHIVES=( + "$HOST/firefox/%s/firefox-mac.zip" + "$HOST/firefox/%s/firefox-linux.zip" + "$HOST/firefox/%s/firefox-win.zip" + "$HOST/webkit/%s/minibrowser-linux.zip" + "$HOST/webkit/%s/minibrowser-mac10.14.zip" + "$HOST/webkit/%s/minibrowser-mac10.15.zip" +) + +ALIASES=( + "FF-MAC" + "FF-LINUX" + "FF-WIN" + "WK-MAC-10.14" + "WK-MAC-10.15" + "WK-LINUX" +) +COLUMN="%-15s" + +# COLORS +RED=$'\e[1;31m' +GRN=$'\e[1;32m' +YEL=$'\e[1;33m' +END=$'\e[0m' + +# Read start revision if there's any. +REVISION=$(git rev-parse HEAD) +if [[ $# == 1 ]]; then + if ! git rev-parse $1; then + echo "ERROR: there is no $REVISION in this repo - pull from upstream?" + exit 1 + fi + REVISION=$(git rev-parse $1) +fi + +printf "%12s" "" +for i in "${ALIASES[@]}"; do + printf $COLUMN $i +done +printf "\n" +while true; do + printf "%-12s" ${REVISION:0:10} + for i in "${ARCHIVES[@]}"; do + URL=$(printf $i $REVISION) + if [[ $(curl -s -L -I $URL | head -1 | cut -f2 -d' ') == 200 ]]; then + printf ${GRN}$COLUMN${END} "YES" + else + printf ${RED}$COLUMN${END} "NO" + fi + done; + echo + REVISION=$(git rev-parse $REVISION^) +done; diff --git a/browser_patches/do_checkout.sh b/browser_patches/do_checkout.sh new file mode 100755 index 0000000000..b83bd9b11d --- /dev/null +++ b/browser_patches/do_checkout.sh @@ -0,0 +1,136 @@ +#!/bin/bash +set -e +set +x + +function cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +if [[ ($1 == '--help') || ($1 == '-h') ]]; then + echo "usage: do_something.sh [firefox|webkit]" + echo + echo "Produces a browser checkout ready to be built." + echo + exit 0 +fi + +if [[ $# == 0 ]]; then + echo "missing browser: 'firefox' or 'webkit'" + echo "try './do_something.sh --help' for more information" + exit 1 +fi + +# FRIENDLY_CHECKOUT_PATH is used only for logging. +FRIENDLY_CHECKOUT_PATH=""; +CHECKOUT_PATH="" +# Export path is where we put the patches and BASE_REVISION +REMOTE_URL="" +BASE_BRANCH="" +if [[ ("$1" == "firefox") || ("$1" == "firefox/") ]]; then + # we always apply our patches atop of beta since it seems to get better + # reliability guarantees. + BASE_BRANCH="beta" + FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox/checkout"; + CHECKOUT_PATH="$PWD/firefox/checkout" + REMOTE_URL="https://github.com/mozilla/gecko-dev" +elif [[ ("$1" == "webkit") || ("$1" == "webkit/") ]]; then + # webkit has only a master branch. + BASE_BRANCH="master" + FRIENDLY_CHECKOUT_PATH="//browser_patches/webkit/checkout"; + CHECKOUT_PATH="$PWD/webkit/checkout" + REMOTE_URL="" + REMOTE_URL="https://github.com/webkit/webkit" +else + echo ERROR: unknown browser to export - "$1" + exit 1 +fi + +# if there's no checkout folder - checkout one. +if ! [[ -d $CHECKOUT_PATH ]]; then + echo "-- $FRIENDLY_CHECKOUT_PATH is missing - checking out.." + git clone --single-branch --branch $BASE_BRANCH $REMOTE_URL $CHECKOUT_PATH +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH folder - OK" +fi + +# if folder exists but not a git repository - bail out. +if ! [[ -d $CHECKOUT_PATH/.git ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH is not a git repository! Remove it and re-run the script." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH is a git repo - OK" +fi + +# Switch to git repository. +cd $CHECKOUT_PATH + +# Check if git repo is dirty. +if [[ -n $(git status -s) ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH has dirty GIT state - commit everything and re-run the script." + exit 1 +fi + +if [[ $(git config --get remote.origin.url) == "$REMOTE_URL" ]]; then + echo "-- checking git origin url to point to $REMOTE_URL - OK"; +else + echo "ERROR: git origin url DOES NOT point to $REMOTE_URL. Remove $FRIENDLY_CHECKOUT_PATH and re-run the script."; + exit 1 +fi + +# if there's no "BASE_BRANCH" branch - bail out. +if ! git show-ref --verify --quiet refs/heads/$BASE_BRANCH; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH/ does not have '$BASE_BRANCH' branch! Remove checkout/ and re-run the script." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH has 'beta' branch - OK" +fi + +if ! [[ -z $(git log --oneline origin/$BASE_BRANCH..$BASE_BRANCH) ]]; then + echo "ERROR: branch '$BASE_BRANCH' and branch 'origin/$BASE_BRANCH' have diverged - bailing out. Remove checkout/ and re-run the script." + exit 1; +else + echo "-- checking that $BASE_BRANCH and origin/$BASE_BRANCH are not diverged - OK" +fi + +git checkout $BASE_BRANCH +git pull origin $BASE_BRANCH + +PINNED_COMMIT=$(cat ../BASE_REVISION) +if ! git cat-file -e $PINNED_COMMIT^{commit}; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH/ does not include the BASE_REVISION (@$PINNED_COMMIT). Remove checkout/ and re-run the script." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH repo has BASE_REVISION (@$PINNED_COMMIT) commit - OK" +fi + +# If there's already a PWDEV branch than we should check if it's fine to reset all changes +# to it. +if git show-ref --verify --quiet refs/heads/pwdev; then + read -p "Do you want to reset 'PWDEV' branch? (ALL CHANGES WILL BE LOST) Y/n " -n 1 -r + echo + # if it's not fine to reset branch - bail out. + if ! [[ $REPLY =~ ^[Yy]$ ]]; then + echo "If you want to keep the branch, than I can't do much! Bailing out!" + exit 1 + else + git checkout pwdev + git reset --hard $PINNED_COMMIT + echo "-- PWDEV now points to BASE_REVISION (@$PINNED_COMMIT)" + fi +else + # Otherwise just create a new branch. + git checkout -b pwdev + git reset --hard $PINNED_COMMIT + echo "-- created 'pwdev' branch that points to BASE_REVISION (@$PINNED_COMMIT)." +fi + +echo "-- applying all patches" +git am ../patches/* + +echo +echo +echo "DONE. Browser is ready to be built." diff --git a/browser_patches/export.sh b/browser_patches/export.sh new file mode 100755 index 0000000000..b6384e79f1 --- /dev/null +++ b/browser_patches/export.sh @@ -0,0 +1,115 @@ +#!/bin/bash +set -e +set +x + +cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +if [[ ($1 == '--help') || ($1 == '-h') ]]; then + echo "usage: export.sh [firefox|webkit] [custom_checkout_path]" + echo + echo "Exports BASE_REVISION and patch from the checkout to browser folder." + echo + echo "You can optionally specify custom_checkout_path if you have browser checkout somewhere else" + echo "and wish to export patches from it." + echo + exit 0 +fi + +if [[ $# == 0 ]]; then + echo "missing browser: 'firefox' or 'webkit'" + echo "try './export.sh --help' for more information" + exit 1 +fi + +# FRIENDLY_CHECKOUT_PATH is used only for logging. +FRIENDLY_CHECKOUT_PATH=""; +CHECKOUT_PATH="" +# Export path is where we put the patches and BASE_REVISION +EXPORT_PATH="" +BASE_BRANCH="" +if [[ ("$1" == "firefox") || ("$1" == "firefox/") ]]; then + # we always apply our patches atop of beta since it seems to get better + # reliability guarantees. + BASE_BRANCH="origin/beta" + FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox/checkout"; + CHECKOUT_PATH="$PWD/firefox/checkout" + EXPORT_PATH="$PWD/firefox/" +elif [[ ("$1" == "webkit") || ("$1" == "webkit/") ]]; then + # webkit has only a master branch. + BASE_BRANCH="origin/master" + FRIENDLY_CHECKOUT_PATH="//browser_patches/webkit/checkout"; + CHECKOUT_PATH="$PWD/webkit/checkout" + EXPORT_PATH="$PWD/webkit/" +else + echo ERROR: unknown browser to export - "$1" + exit 1 +fi + +# we will use this just for beauty. +if [[ $# == 2 ]]; then + echo "WARNING: using custom checkout path $CHECKOUT_PATH" + CHECKOUT_PATH=$2 + FRIENDLY_CHECKOUT_PATH="" +fi + +# if there's no checkout folder - bail out. +if ! [[ -d $CHECKOUT_PATH ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH is missing - nothing to export." + exit 1; +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH exists - OK" +fi + +# if folder exists but not a git repository - bail out. +if ! [[ -d $CHECKOUT_PATH/.git ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH is not a git repository! Nothing to export." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH is a git repo - OK" +fi + +# Switch to git repository. +cd $CHECKOUT_PATH + +# Check if git repo is dirty. +if [[ -n $(git status -s) ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH has dirty GIT state - aborting export." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH is clean - OK" +fi + +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +MERGE_BASE=$(git merge-base $BASE_BRANCH $CURRENT_BRANCH) +echo "==============================================================" +echo " Repository: $FRIENDLY_CHECKOUT_PATH" +echo " Changes between branches: $BASE_BRANCH..$CURRENT_BRANCH" +echo " BASE_REVISION: $MERGE_BASE" +echo +read -p "Export? Y/n " -n 1 -r +echo +# if it's not fine to reset branch - bail out. +if ! [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Exiting." + exit 1 +fi + +echo $MERGE_BASE > $EXPORT_PATH/BASE_REVISION +git checkout -b tmpsquash_export_script $MERGE_BASE +git merge --squash $CURRENT_BRANCH +git commit -am "chore: bootstrap" +PATCH_NAME=$(git format-patch -1 HEAD) +mv $PATCH_NAME $EXPORT_PATH/patches/ +git checkout $CURRENT_BRANCH +git branch -D tmpsquash_export_script + +# Increment BUILD_NUMBER +BUILD_NUMBER=$(cat $EXPORT_PATH/BUILD_NUMBER) +BUILD_NUMBER=$((BUILD_NUMBER+1)) +echo $BUILD_NUMBER > $EXPORT_PATH/BUILD_NUMBER diff --git a/browser_patches/firefox/.gitignore b/browser_patches/firefox/.gitignore new file mode 100644 index 0000000000..5e660dc18e --- /dev/null +++ b/browser_patches/firefox/.gitignore @@ -0,0 +1 @@ +/checkout diff --git a/browser_patches/firefox/BASE_REVISION b/browser_patches/firefox/BASE_REVISION new file mode 100644 index 0000000000..37eb8e8658 --- /dev/null +++ b/browser_patches/firefox/BASE_REVISION @@ -0,0 +1 @@ +46ca28eadfe840021e2ea496fa6b26f924fa135b diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER new file mode 100644 index 0000000000..d474e1b4d6 --- /dev/null +++ b/browser_patches/firefox/BUILD_NUMBER @@ -0,0 +1,2 @@ +1 + diff --git a/browser_patches/firefox/README.md b/browser_patches/firefox/README.md new file mode 100644 index 0000000000..e3368007ed --- /dev/null +++ b/browser_patches/firefox/README.md @@ -0,0 +1,22 @@ +# Building Juggler (Linux & Mac) + +1. Run `./do_checkout.sh` script. This will create a "checkout" folder with gecko-dev mirror from +GitHub and apply the PlayWright-specific patches. +2. Run `./do_build.sh` script to compile browser. Note: you'll need to follow [build instructions](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions) to setup host environment first. + +# Updating `FIREFOX_REVISION` and `//patches/*` + +The `./export.sh` script will export a patch that describes all the differences between the current branch in `./checkout` +and the `beta` branch in `./checkout`. + +# Uploading to Azure CDN + +Uploading requires having both `AZ_ACCOUNT_KEY` and `AZ_ACCOUNT_NAME` env variables to be defined. + +The following sequence of steps will checkout, build and upload build to Azure CDN on both Linux and Mac: + +```sh +$ ./do_checkout.sh +$ ./build.sh +$ ./upload.sh +``` diff --git a/browser_patches/firefox/archive.sh b/browser_patches/firefox/archive.sh new file mode 100755 index 0000000000..42e75711da --- /dev/null +++ b/browser_patches/firefox/archive.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +if [[ ("$1" == "-h") || ("$1" == "--help") ]]; then + echo "usage: $0" + echo + echo "Generate distributable .zip archive from ./checkout folder that was previously built." + echo + exit 0 +fi + +set -e +set -x + +createZIPForLinuxOrMac() { + cd checkout + local zipname=$1 + local OBJ_FOLDER=$(ls -1 | grep obj-) + if [[ $OBJ_FOLDER == "" ]]; then + echo "ERROR: cannot find obj-* folder in the checkout/. Did you build?" + exit 1; + fi + if ! [[ -d $OBJ_FOLDER/dist/firefox ]]; then + echo "ERROR: cannot find $OBJ_FOLDER/dist/firefox folder in the checkout/. Did you build?" + exit 1; + fi + # Copy the libstdc++ version we linked against. + # TODO(aslushnikov): this won't be needed with official builds. + if [[ "$(uname)" == "Linux" ]]; then + cp /usr/lib/x86_64-linux-gnu/libstdc++.so.6 $OBJ_FOLDER/dist/firefox/libstdc++.so.6 + fi + + # tar resulting directory and cleanup TMP. + cd $OBJ_FOLDER/dist + zip -r ../../../$zipname firefox + cd - +} + +cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +if [[ "$(uname)" == "Darwin" ]]; then + createZIPForLinuxOrMac "firefox-mac.zip" +elif [[ "$(uname)" == "Linux" ]]; then + createZIPForLinuxOrMac "firefox-linux.zip" +else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; +fi diff --git a/browser_patches/firefox/build.sh b/browser_patches/firefox/build.sh new file mode 100755 index 0000000000..1870546b1b --- /dev/null +++ b/browser_patches/firefox/build.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -e +set +x + +function cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +cd checkout + +if ! [[ $(git rev-parse --abbrev-ref HEAD) == "pwdev" ]]; then + echo "ERROR: Cannot build any branch other than PWDEV" + exit 1; +else + echo "-- checking git branch is PWDEV - OK" +fi + +if [[ "$(uname)" == "Darwin" ]]; then + # Firefox currently does not build on 10.15 out of the box - it requires SDK for 10.14. + # Make sure the SDK is out there. + if [[ $(sw_vers -productVersion) == "10.15" ]]; then + if ! [[ -d $HOME/SDK-archive/MacOSX10.14.sdk ]]; then + echo "As of Nov 2019, Firefox does not build on Mac 10.15 without 10.14 SDK." + echo "Check out instructions on getting 10.14 sdk at https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Mac_OS_X_Prerequisites" + echo "and make sure to put SDK to $HOME/SDK-archive/MacOSX10.14.sdk/" + exit 1 + else + echo "-- configuting .mozconfig with 10.14 SDK path" + echo "ac_add_options --with-macos-sdk=$HOME/SDK-archive/MacOSX10.14.sdk/" > .mozconfig + fi + fi + echo "-- building on Mac" +elif [[ "$(uname)" == "Linux" ]]; then + echo "-- building on Linux" +else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; +fi + +./mach build +./mach package diff --git a/browser_patches/firefox/patches/0001-chore-bootstrap.patch b/browser_patches/firefox/patches/0001-chore-bootstrap.patch new file mode 100644 index 0000000000..2e1f1298b4 --- /dev/null +++ b/browser_patches/firefox/patches/0001-chore-bootstrap.patch @@ -0,0 +1,4895 @@ +From db891a78ac18605d1fb4c6c4a260c1e16340789c Mon Sep 17 00:00:00 2001 +From: Andrey Lushnikov +Date: Fri, 15 Nov 2019 18:10:56 -0800 +Subject: [PATCH] chore: bootstrap + +--- + browser/installer/allowed-dupes.mn | 5 + + browser/installer/package-manifest.in | 5 + + docshell/base/nsDocShell.cpp | 1 + + dom/ipc/BrowserChild.cpp | 7 + + .../permissions/nsPermissionManager.cpp | 8 +- + .../manager/ssl/nsCertOverrideService.cpp | 2 +- + testing/juggler/BrowserContextManager.js | 194 +++++ + testing/juggler/Helper.js | 101 +++ + testing/juggler/NetworkObserver.js | 450 ++++++++++++ + testing/juggler/TargetRegistry.js | 187 +++++ + testing/juggler/components/juggler.js | 112 +++ + testing/juggler/components/juggler.manifest | 3 + + testing/juggler/components/moz.build | 9 + + testing/juggler/content/ContentSession.js | 63 ++ + testing/juggler/content/FrameTree.js | 232 ++++++ + testing/juggler/content/NetworkMonitor.js | 62 ++ + testing/juggler/content/PageAgent.js | 621 ++++++++++++++++ + testing/juggler/content/RuntimeAgent.js | 460 ++++++++++++ + testing/juggler/content/ScrollbarManager.js | 85 +++ + .../juggler/content/floating-scrollbars.css | 47 ++ + testing/juggler/content/hidden-scrollbars.css | 13 + + testing/juggler/content/main.js | 39 ++ + testing/juggler/jar.mn | 29 + + testing/juggler/moz.build | 15 + + .../juggler/protocol/AccessibilityHandler.js | 15 + + testing/juggler/protocol/BrowserHandler.js | 66 ++ + testing/juggler/protocol/Dispatcher.js | 255 +++++++ + testing/juggler/protocol/NetworkHandler.js | 154 ++++ + testing/juggler/protocol/PageHandler.js | 269 +++++++ + testing/juggler/protocol/PrimitiveTypes.js | 143 ++++ + testing/juggler/protocol/Protocol.js | 660 ++++++++++++++++++ + testing/juggler/protocol/RuntimeHandler.js | 41 ++ + testing/juggler/protocol/TargetHandler.js | 75 ++ + .../statusfilter/nsBrowserStatusFilter.cpp | 12 +- + toolkit/toolkit.mozbuild | 1 + + uriloader/base/nsDocLoader.cpp | 18 + + uriloader/base/nsDocLoader.h | 5 + + uriloader/base/nsIWebProgress.idl | 7 +- + uriloader/base/nsIWebProgressListener2.idl | 23 + + 39 files changed, 4487 insertions(+), 7 deletions(-) + create mode 100644 testing/juggler/BrowserContextManager.js + create mode 100644 testing/juggler/Helper.js + create mode 100644 testing/juggler/NetworkObserver.js + create mode 100644 testing/juggler/TargetRegistry.js + create mode 100644 testing/juggler/components/juggler.js + create mode 100644 testing/juggler/components/juggler.manifest + create mode 100644 testing/juggler/components/moz.build + create mode 100644 testing/juggler/content/ContentSession.js + create mode 100644 testing/juggler/content/FrameTree.js + create mode 100644 testing/juggler/content/NetworkMonitor.js + create mode 100644 testing/juggler/content/PageAgent.js + create mode 100644 testing/juggler/content/RuntimeAgent.js + create mode 100644 testing/juggler/content/ScrollbarManager.js + create mode 100644 testing/juggler/content/floating-scrollbars.css + create mode 100644 testing/juggler/content/hidden-scrollbars.css + create mode 100644 testing/juggler/content/main.js + create mode 100644 testing/juggler/jar.mn + create mode 100644 testing/juggler/moz.build + create mode 100644 testing/juggler/protocol/AccessibilityHandler.js + create mode 100644 testing/juggler/protocol/BrowserHandler.js + create mode 100644 testing/juggler/protocol/Dispatcher.js + create mode 100644 testing/juggler/protocol/NetworkHandler.js + create mode 100644 testing/juggler/protocol/PageHandler.js + create mode 100644 testing/juggler/protocol/PrimitiveTypes.js + create mode 100644 testing/juggler/protocol/Protocol.js + create mode 100644 testing/juggler/protocol/RuntimeHandler.js + create mode 100644 testing/juggler/protocol/TargetHandler.js + +diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn +index 1ffaa0997927..c1bb33c8e63c 100644 +--- a/browser/installer/allowed-dupes.mn ++++ b/browser/installer/allowed-dupes.mn +@@ -141,6 +141,11 @@ browser/chrome/browser/res/payments/formautofill/autofillEditForms.js + # Bug 1451050 - Remote settings empty dumps (will be populated with data eventually) + browser/defaults/settings/pinning/pins.json + browser/defaults/settings/main/example.json ++# Juggler/marionette files ++chrome/juggler/content/content/floating-scrollbars.css ++browser/chrome/devtools/skin/floating-scrollbars-responsive-design.css ++chrome/juggler/content/server/stream-utils.js ++chrome/marionette/content/stream-utils.js + #ifdef MOZ_EME_WIN32_ARTIFACT + gmp-clearkey/0.1/manifest.json + i686/gmp-clearkey/0.1/manifest.json +diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in +index 0efb8c4210bf..6695fa1deb70 100644 +--- a/browser/installer/package-manifest.in ++++ b/browser/installer/package-manifest.in +@@ -208,6 +208,11 @@ + @RESPATH@/components/marionette.js + #endif + ++@RESPATH@/chrome/juggler@JAREXT@ ++@RESPATH@/chrome/juggler.manifest ++@RESPATH@/components/juggler.manifest ++@RESPATH@/components/juggler.js ++ + #if defined(ENABLE_TESTS) && defined(MOZ_DEBUG) + @RESPATH@/components/TestInterfaceJS.js + @RESPATH@/components/TestInterfaceJS.manifest +diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp +index b56ce1764dbb..1f4e7cb24d6f 100644 +--- a/docshell/base/nsDocShell.cpp ++++ b/docshell/base/nsDocShell.cpp +@@ -1241,6 +1241,7 @@ bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest, + isSubFrame = mLSHE->GetIsSubFrame(); + } + ++ FireOnFrameLocationChange(this, aRequest, aURI, aLocationFlags); + if (!isSubFrame && !isRoot) { + /* + * We don't want to send OnLocationChange notifications when +diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp +index d033474bec84..e97ab5373f13 100644 +--- a/dom/ipc/BrowserChild.cpp ++++ b/dom/ipc/BrowserChild.cpp +@@ -3576,6 +3576,13 @@ NS_IMETHODIMP BrowserChild::OnStateChange(nsIWebProgress* aWebProgress, + return NS_OK; + } + ++NS_IMETHODIMP BrowserChild::OnFrameLocationChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, ++ nsIURI *aLocation, ++ uint32_t aFlags) { ++ return NS_OK; ++} ++ + NS_IMETHODIMP BrowserChild::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, +diff --git a/extensions/permissions/nsPermissionManager.cpp b/extensions/permissions/nsPermissionManager.cpp +index ce3d5e64bb4e..64b86791e582 100644 +--- a/extensions/permissions/nsPermissionManager.cpp ++++ b/extensions/permissions/nsPermissionManager.cpp +@@ -189,6 +189,8 @@ nsresult GetOriginFromPrincipal(nsIPrincipal* aPrincipal, nsACString& aOrigin) { + + OriginAppendOASuffix(attrs, aOrigin); + ++ // Disable userContext for permissions. ++ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); + return NS_OK; + } + +@@ -220,7 +222,7 @@ nsresult GetPrincipalFromOrigin(const nsACString& aOrigin, + attrs.mPrivateBrowsingId = 0; + + // Disable userContext for permissions. +- attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); ++ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); + + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix); +@@ -312,7 +314,7 @@ already_AddRefed GetNextSubDomainPrincipal( + mozilla::OriginAttributes attrs = aPrincipal->OriginAttributesRef(); + + // Disable userContext for permissions. +- attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); ++ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); + + nsCOMPtr principal = + mozilla::BasePrincipal::CreateContentPrincipal(newURI, attrs); +@@ -3220,7 +3222,7 @@ void nsPermissionManager::GetKeyForOrigin(const nsACString& aOrigin, + attrs.mPrivateBrowsingId = 0; + + // Disable userContext for permissions. +- attrs.StripAttributes(OriginAttributes::STRIP_USER_CONTEXT_ID); ++ // attrs.StripAttributes(OriginAttributes::STRIP_USER_CONTEXT_ID); + + #ifdef DEBUG + // Parse the origin string into a principal, and extract some useful +diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp +index 31737688948a..255e5ae967b4 100644 +--- a/security/manager/ssl/nsCertOverrideService.cpp ++++ b/security/manager/ssl/nsCertOverrideService.cpp +@@ -611,7 +611,7 @@ nsCertOverrideService::IsCertUsedForOverrides(nsIX509Cert* aCert, + NS_IMETHODIMP + nsCertOverrideService:: + SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(bool aDisable) { +- if (!(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || ++ if (false /* juggler hacks */ && !(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || + PR_GetEnv("MOZ_MARIONETTE"))) { + return NS_ERROR_NOT_AVAILABLE; + } +diff --git a/testing/juggler/BrowserContextManager.js b/testing/juggler/BrowserContextManager.js +new file mode 100644 +index 000000000000..751fac95177c +--- /dev/null ++++ b/testing/juggler/BrowserContextManager.js +@@ -0,0 +1,194 @@ ++"use strict"; ++ ++const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm"); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); ++ ++const IDENTITY_NAME = 'JUGGLER '; ++const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100; ++ ++const ALL_PERMISSIONS = [ ++ 'geo', ++ 'microphone', ++ 'camera', ++ 'desktop-notifications', ++]; ++ ++class BrowserContextManager { ++ static instance() { ++ return BrowserContextManager._instance || null; ++ } ++ ++ static initialize() { ++ if (BrowserContextManager._instance) ++ return; ++ BrowserContextManager._instance = new BrowserContextManager(); ++ } ++ ++ constructor() { ++ this._id = 0; ++ this._browserContextIdToUserContextId = new Map(); ++ this._userContextIdToBrowserContextId = new Map(); ++ this._principalsForBrowserContextId = new Map(); ++ ++ // Cleanup containers from previous runs (if any) ++ for (const identity of ContextualIdentityService.getPublicIdentities()) { ++ if (identity.name && identity.name.startsWith(IDENTITY_NAME)) { ++ ContextualIdentityService.remove(identity.userContextId); ++ ContextualIdentityService.closeContainerTabs(identity.userContextId); ++ } ++ } ++ } ++ ++ grantPermissions(browserContextId, origin, permissions) { ++ const attrs = browserContextId ? {userContextId: this.userContextId(browserContextId)} : {}; ++ const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(origin), attrs); ++ if (!this._principalsForBrowserContextId.has(browserContextId)) ++ this._principalsForBrowserContextId.set(browserContextId, []); ++ this._principalsForBrowserContextId.get(browserContextId).push(principal); ++ for (const permission of ALL_PERMISSIONS) { ++ const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION; ++ Services.perms.addFromPrincipal(principal, permission, action); ++ } ++ } ++ ++ resetPermissions(browserContextId) { ++ if (!this._principalsForBrowserContextId.has(browserContextId)) ++ return; ++ const principals = this._principalsForBrowserContextId.get(browserContextId); ++ for (const principal of principals) { ++ for (const permission of ALL_PERMISSIONS) ++ Services.perms.removeFromPrincipal(principal, permission); ++ } ++ this._principalsForBrowserContextId.delete(browserContextId); ++ } ++ ++ createBrowserContext() { ++ const browserContextId = (++this._id) + ''; ++ const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId); ++ this._browserContextIdToUserContextId.set(browserContextId, identity.userContextId); ++ this._userContextIdToBrowserContextId.set(identity.userContextId, browserContextId); ++ return browserContextId; ++ } ++ ++ browserContextId(userContextId) { ++ return this._userContextIdToBrowserContextId.get(userContextId); ++ } ++ ++ userContextId(browserContextId) { ++ return this._browserContextIdToUserContextId.get(browserContextId); ++ } ++ ++ removeBrowserContext(browserContextId) { ++ const userContextId = this._browserContextIdToUserContextId.get(browserContextId); ++ ContextualIdentityService.remove(userContextId); ++ ContextualIdentityService.closeContainerTabs(userContextId); ++ this._browserContextIdToUserContextId.delete(browserContextId); ++ this._userContextIdToBrowserContextId.delete(userContextId); ++ } ++ ++ getBrowserContexts() { ++ return Array.from(this._browserContextIdToUserContextId.keys()); ++ } ++ ++ setCookies(browserContextId, cookies) { ++ const protocolToSameSite = { ++ [undefined]: Ci.nsICookie.SAMESITE_NONE, ++ 'Lax': Ci.nsICookie.SAMESITE_LAX, ++ 'Strict': Ci.nsICookie.SAMESITE_STRICT, ++ }; ++ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : undefined; ++ for (const cookie of cookies) { ++ const uri = cookie.url ? NetUtil.newURI(cookie.url) : null; ++ let domain = cookie.domain; ++ if (!domain) { ++ if (!uri) ++ throw new Error('At least one of the url and domain needs to be specified'); ++ domain = uri.host; ++ } ++ let path = cookie.path; ++ if (!path) ++ path = uri ? dirPath(uri.filePath) : '/'; ++ let secure = false; ++ if (cookie.secure !== undefined) ++ secure = cookie.secure; ++ else if (uri.scheme === 'https') ++ secure = true; ++ Services.cookies.add( ++ domain, ++ path, ++ cookie.name, ++ cookie.value, ++ secure, ++ cookie.httpOnly || false, ++ cookie.expires === undefined || cookie.expires === -1 /* isSession */, ++ cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires, ++ { userContextId } /* originAttributes */, ++ protocolToSameSite[cookie.sameSite], ++ ); ++ } ++ } ++ ++ deleteCookies(browserContextId, cookies) { ++ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : undefined; ++ for (const cookie of cookies) { ++ let defaultDomain = ''; ++ let defaultPath = '/'; ++ if (cookie.url) { ++ const uri = NetUtil.newURI(cookie.url); ++ defaultDomain = uri.host; ++ defaultPath = dirPath(uri.filePath); ++ } ++ Services.cookies.remove( ++ cookie.domain || defaultDomain, ++ cookie.name, ++ cookie.path || defaultPath, ++ { userContextId } /* originAttributes */, ++ ); ++ } ++ } ++ ++ getCookies(browserContextId, urls) { ++ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : 0; ++ const result = []; ++ const sameSiteToProtocol = { ++ [Ci.nsICookie.SAMESITE_NONE]: undefined, ++ [Ci.nsICookie.SAMESITE_LAX]: 'Lax', ++ [Ci.nsICookie.SAMESITE_STRICT]: 'Strict', ++ }; ++ const uris = urls.map(url => NetUtil.newURI(url)); ++ for (let cookie of Services.cookies.enumerator) { ++ if (cookie.originAttributes.userContextId !== userContextId) ++ continue; ++ if (!uris.some(uri => cookieMatchesURI(cookie, uri))) ++ continue; ++ result.push({ ++ name: cookie.name, ++ value: cookie.value, ++ domain: cookie.host, ++ path: cookie.path, ++ expires: cookie.isSession ? -1 : cookie.expiry, ++ size: cookie.name.length + cookie.value.length, ++ httpOnly: cookie.isHttpOnly, ++ secure: cookie.isSecure, ++ session: cookie.isSession, ++ sameSite: sameSiteToProtocol[cookie.sameSite], ++ }); ++ } ++ return result; ++ } ++} ++ ++function cookieMatchesURI(cookie, uri) { ++ const hostMatches = cookie.host === uri.host || cookie.host === '.' + uri.host; ++ const pathMatches = uri.filePath.startsWith(cookie.path); ++ return hostMatches && pathMatches; ++} ++ ++function dirPath(path) { ++ return path.substring(0, path.lastIndexOf('/') + 1); ++} ++ ++var EXPORTED_SYMBOLS = ['BrowserContextManager']; ++this.BrowserContextManager = BrowserContextManager; ++ +diff --git a/testing/juggler/Helper.js b/testing/juggler/Helper.js +new file mode 100644 +index 000000000000..673e93b0278a +--- /dev/null ++++ b/testing/juggler/Helper.js +@@ -0,0 +1,101 @@ ++const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++class Helper { ++ addObserver(handler, topic) { ++ Services.obs.addObserver(handler, topic); ++ return () => Services.obs.removeObserver(handler, topic); ++ } ++ ++ addMessageListener(receiver, eventName, handler) { ++ receiver.addMessageListener(eventName, handler); ++ return () => receiver.removeMessageListener(eventName, handler); ++ } ++ ++ addEventListener(receiver, eventName, handler) { ++ receiver.addEventListener(eventName, handler); ++ return () => receiver.removeEventListener(eventName, handler); ++ } ++ ++ on(receiver, eventName, handler) { ++ // The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument. ++ // Fire event listeners without it for convenience. ++ const handlerWrapper = (_, ...args) => handler(...args); ++ receiver.on(eventName, handlerWrapper); ++ return () => receiver.off(eventName, handlerWrapper); ++ } ++ ++ addProgressListener(progress, listener, flags) { ++ progress.addProgressListener(listener, flags); ++ return () => progress.removeProgressListener(listener); ++ } ++ ++ removeListeners(listeners) { ++ for (const tearDown of listeners) ++ tearDown.call(null); ++ listeners.splice(0, listeners.length); ++ } ++ ++ generateId() { ++ return uuidGen.generateUUID().toString(); ++ } ++ ++ getNetworkErrorStatusText(status) { ++ if (!status) ++ return null; ++ for (const key of Object.keys(Cr)) { ++ if (Cr[key] === status) ++ return key; ++ } ++ // Security module. The following is taken from ++ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL ++ if ((status & 0xff0000) === 0x5a0000) { ++ // NSS_SEC errors (happen below the base value because of negative vals) ++ if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) { ++ // The bases are actually negative, so in our positive numeric space, we ++ // need to subtract the base off our value. ++ const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff); ++ switch (nssErr) { ++ case 11: ++ return 'SEC_ERROR_EXPIRED_CERTIFICATE'; ++ case 12: ++ return 'SEC_ERROR_REVOKED_CERTIFICATE'; ++ case 13: ++ return 'SEC_ERROR_UNKNOWN_ISSUER'; ++ case 20: ++ return 'SEC_ERROR_UNTRUSTED_ISSUER'; ++ case 21: ++ return 'SEC_ERROR_UNTRUSTED_CERT'; ++ case 36: ++ return 'SEC_ERROR_CA_CERT_INVALID'; ++ case 90: ++ return 'SEC_ERROR_INADEQUATE_KEY_USAGE'; ++ case 176: ++ return 'SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED'; ++ default: ++ return 'SEC_ERROR_UNKNOWN'; ++ } ++ } ++ const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff); ++ switch (sslErr) { ++ case 3: ++ return 'SSL_ERROR_NO_CERTIFICATE'; ++ case 4: ++ return 'SSL_ERROR_BAD_CERTIFICATE'; ++ case 8: ++ return 'SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE'; ++ case 9: ++ return 'SSL_ERROR_UNSUPPORTED_VERSION'; ++ case 12: ++ return 'SSL_ERROR_BAD_CERT_DOMAIN'; ++ default: ++ return 'SSL_ERROR_UNKNOWN'; ++ } ++ } ++ return ''; ++ } ++} ++ ++var EXPORTED_SYMBOLS = [ "Helper" ]; ++this.Helper = Helper; ++ +diff --git a/testing/juggler/NetworkObserver.js b/testing/juggler/NetworkObserver.js +new file mode 100644 +index 000000000000..cc8cb8fe9d83 +--- /dev/null ++++ b/testing/juggler/NetworkObserver.js +@@ -0,0 +1,450 @@ ++"use strict"; ++ ++const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); ++const {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js"); ++ ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++const Cr = Components.results; ++const Cm = Components.manager; ++const CC = Components.Constructor; ++const helper = new Helper(); ++ ++const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream'); ++const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream'); ++const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init'); ++ ++// Cap response storage with 100Mb per tracked tab. ++const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024; ++ ++/** ++ * This is a nsIChannelEventSink implementation that monitors channel redirects. ++ */ ++const SINK_CLASS_DESCRIPTION = "Juggler NetworkMonitor Channel Event Sink"; ++const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}"); ++const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1"; ++const SINK_CATEGORY_NAME = "net-channel-event-sinks"; ++ ++class NetworkObserver { ++ static instance() { ++ return NetworkObserver._instance || null; ++ } ++ ++ static initialize() { ++ if (NetworkObserver._instance) ++ return; ++ NetworkObserver._instance = new NetworkObserver(); ++ } ++ ++ constructor() { ++ EventEmitter.decorate(this); ++ this._browserSessionCount = new Map(); ++ this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor); ++ this._activityDistributor.addObserver(this); ++ ++ this._redirectMap = new Map(); ++ this._channelSink = { ++ QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]), ++ asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => { ++ this._onRedirect(oldChannel, newChannel); ++ callback.onRedirectVerifyCallback(Cr.NS_OK); ++ }, ++ }; ++ this._channelSinkFactory = { ++ QueryInterface: ChromeUtils.generateQI([Ci.nsIFactory]), ++ createInstance: (aOuter, aIID) => this._channelSink.QueryInterface(aIID), ++ }; ++ // Register self as ChannelEventSink to track redirects. ++ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); ++ registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory); ++ Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true); ++ ++ // Request interception state. ++ this._browserSuspendedChannels = new Map(); ++ this._extraHTTPHeaders = new Map(); ++ this._browserResponseStorages = new Map(); ++ ++ this._eventListeners = [ ++ helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'), ++ helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'), ++ helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'), ++ helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'), ++ ]; ++ } ++ ++ setExtraHTTPHeaders(browser, headers) { ++ if (!headers) ++ this._extraHTTPHeaders.delete(browser); ++ else ++ this._extraHTTPHeaders.set(browser, headers); ++ } ++ ++ enableRequestInterception(browser) { ++ if (!this._browserSuspendedChannels.has(browser)) ++ this._browserSuspendedChannels.set(browser, new Map()); ++ } ++ ++ disableRequestInterception(browser) { ++ const suspendedChannels = this._browserSuspendedChannels.get(browser); ++ if (!suspendedChannels) ++ return; ++ this._browserSuspendedChannels.delete(browser); ++ for (const channel of suspendedChannels.values()) ++ channel.resume(); ++ } ++ ++ resumeSuspendedRequest(browser, requestId, headers) { ++ const suspendedChannels = this._browserSuspendedChannels.get(browser); ++ if (!suspendedChannels) ++ throw new Error(`Request interception is not enabled`); ++ const httpChannel = suspendedChannels.get(requestId); ++ if (!httpChannel) ++ throw new Error(`Cannot find request "${requestId}"`); ++ if (headers) { ++ // 1. Clear all previous headers. ++ for (const header of requestHeaders(httpChannel)) ++ httpChannel.setRequestHeader(header.name, '', false /* merge */); ++ // 2. Set new headers. ++ for (const header of headers) ++ httpChannel.setRequestHeader(header.name, header.value, false /* merge */); ++ } ++ suspendedChannels.delete(requestId); ++ httpChannel.resume(); ++ } ++ ++ getResponseBody(browser, requestId) { ++ const responseStorage = this._browserResponseStorages.get(browser); ++ if (!responseStorage) ++ throw new Error('Responses are not tracked for the given browser'); ++ return responseStorage.getBase64EncodedResponse(requestId); ++ } ++ ++ abortSuspendedRequest(browser, aRequestId) { ++ const suspendedChannels = this._browserSuspendedChannels.get(browser); ++ if (!suspendedChannels) ++ throw new Error(`Request interception is not enabled`); ++ const httpChannel = suspendedChannels.get(aRequestId); ++ if (!httpChannel) ++ throw new Error(`Cannot find request "${aRequestId}"`); ++ suspendedChannels.delete(aRequestId); ++ httpChannel.cancel(Cr.NS_ERROR_FAILURE); ++ httpChannel.resume(); ++ this.emit('requestfailed', httpChannel, { ++ requestId: requestId(httpChannel), ++ errorCode: helper.getNetworkErrorStatusText(httpChannel.status), ++ }); ++ } ++ ++ _onRedirect(oldChannel, newChannel) { ++ if (!(oldChannel instanceof Ci.nsIHttpChannel)) ++ return; ++ const httpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel); ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) ++ return; ++ this._redirectMap.set(newChannel, oldChannel); ++ } ++ ++ observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) { ++ if (activityType !== Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION) ++ return; ++ if (!(channel instanceof Ci.nsIHttpChannel)) ++ return; ++ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) ++ return; ++ if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) ++ return; ++ this.emit('requestfinished', httpChannel, { ++ requestId: requestId(httpChannel), ++ }); ++ } ++ ++ _onRequest(channel, topic) { ++ if (!(channel instanceof Ci.nsIHttpChannel)) ++ return; ++ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) ++ return; ++ const extraHeaders = this._extraHTTPHeaders.get(loadContext.topFrameElement); ++ if (extraHeaders) { ++ for (const header of extraHeaders) ++ httpChannel.setRequestHeader(header.name, header.value, false /* merge */); ++ } ++ const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; ++ const suspendedChannels = this._browserSuspendedChannels.get(loadContext.topFrameElement); ++ if (suspendedChannels) { ++ httpChannel.suspend(); ++ suspendedChannels.set(requestId(httpChannel), httpChannel); ++ } ++ const oldChannel = this._redirectMap.get(httpChannel); ++ this._redirectMap.delete(httpChannel); ++ ++ // Install response body hooks. ++ new ResponseBodyListener(this, loadContext.topFrameElement, httpChannel); ++ ++ this.emit('request', httpChannel, { ++ url: httpChannel.URI.spec, ++ suspended: suspendedChannels ? true : undefined, ++ requestId: requestId(httpChannel), ++ redirectedFrom: oldChannel ? requestId(oldChannel) : undefined, ++ postData: readRequestPostData(httpChannel), ++ headers: requestHeaders(httpChannel), ++ method: httpChannel.requestMethod, ++ isNavigationRequest: httpChannel.isMainDocumentChannel, ++ cause: causeTypeToString(causeType), ++ }); ++ } ++ ++ _onResponse(fromCache, httpChannel, topic) { ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) ++ return; ++ httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); ++ const headers = []; ++ httpChannel.visitResponseHeaders({ ++ visitHeader: (name, value) => headers.push({name, value}), ++ }); ++ ++ let remoteIPAddress = undefined; ++ let remotePort = undefined; ++ try { ++ remoteIPAddress = httpChannel.remoteAddress; ++ remotePort = httpChannel.remotePort; ++ } catch (e) { ++ // remoteAddress is not defined for cached requests. ++ } ++ this.emit('response', httpChannel, { ++ requestId: requestId(httpChannel), ++ securityDetails: getSecurityDetails(httpChannel), ++ fromCache, ++ headers, ++ remoteIPAddress, ++ remotePort, ++ status: httpChannel.responseStatus, ++ statusText: httpChannel.responseStatusText, ++ }); ++ } ++ ++ _onResponseFinished(browser, httpChannel, body) { ++ const responseStorage = this._browserResponseStorages.get(browser); ++ if (!responseStorage) ++ return; ++ responseStorage.addResponseBody(httpChannel, body); ++ this.emit('requestfinished', httpChannel, { ++ requestId: requestId(httpChannel), ++ }); ++ } ++ ++ startTrackingBrowserNetwork(browser) { ++ const value = this._browserSessionCount.get(browser) || 0; ++ this._browserSessionCount.set(browser, value + 1); ++ if (value === 0) ++ this._browserResponseStorages.set(browser, new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10)); ++ return () => this.stopTrackingBrowserNetwork(browser); ++ } ++ ++ stopTrackingBrowserNetwork(browser) { ++ const value = this._browserSessionCount.get(browser); ++ if (value) { ++ this._browserSessionCount.set(browser, value - 1); ++ } else { ++ this._browserSessionCount.delete(browser); ++ this._browserResponseStorages.delete(browser); ++ } ++ } ++ ++ dispose() { ++ this._activityDistributor.removeObserver(this); ++ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); ++ registrar.unregisterFactory(SINK_CLASS_ID, this._channelSinkFactory); ++ Services.catMan.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, false); ++ helper.removeListeners(this._eventListeners); ++ } ++} ++ ++const protocolVersionNames = { ++ [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1', ++ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1', ++ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2', ++ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3', ++}; ++ ++function getSecurityDetails(httpChannel) { ++ const securityInfo = httpChannel.securityInfo; ++ if (!securityInfo) ++ return null; ++ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); ++ if (!securityInfo.serverCert) ++ return null; ++ return { ++ protocol: protocolVersionNames[securityInfo.protocolVersion] || '', ++ subjectName: securityInfo.serverCert.commonName, ++ issuer: securityInfo.serverCert.issuerCommonName, ++ // Convert to seconds. ++ validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000, ++ validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000, ++ }; ++} ++ ++function readRequestPostData(httpChannel) { ++ if (!(httpChannel instanceof Ci.nsIUploadChannel)) ++ return undefined; ++ const iStream = httpChannel.uploadStream; ++ if (!iStream) ++ return undefined; ++ const isSeekableStream = iStream instanceof Ci.nsISeekableStream; ++ ++ let prevOffset; ++ if (isSeekableStream) { ++ prevOffset = iStream.tell(); ++ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); ++ } ++ ++ // Read data from the stream. ++ let text = undefined; ++ try { ++ text = NetUtil.readInputStreamToString(iStream, iStream.available()); ++ const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter'] ++ .createInstance(Ci.nsIScriptableUnicodeConverter); ++ converter.charset = 'UTF-8'; ++ text = converter.ConvertToUnicode(text); ++ } catch (err) { ++ text = undefined; ++ } ++ ++ // Seek locks the file, so seek to the beginning only if necko hasn't ++ // read it yet, since necko doesn't seek to 0 before reading (at lest ++ // not till 459384 is fixed). ++ if (isSeekableStream && prevOffset == 0) ++ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); ++ return text; ++} ++ ++function getLoadContext(httpChannel) { ++ let loadContext = null; ++ try { ++ if (httpChannel.notificationCallbacks) ++ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext); ++ } catch (e) {} ++ try { ++ if (!loadContext && httpChannel.loadGroup) ++ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); ++ } catch (e) { } ++ return loadContext; ++} ++ ++function requestId(httpChannel) { ++ return httpChannel.channelId + ''; ++} ++ ++function requestHeaders(httpChannel) { ++ const headers = []; ++ httpChannel.visitRequestHeaders({ ++ visitHeader: (name, value) => headers.push({name, value}), ++ }); ++ return headers; ++} ++ ++function causeTypeToString(causeType) { ++ for (let key in Ci.nsIContentPolicy) { ++ if (Ci.nsIContentPolicy[key] === causeType) ++ return key; ++ } ++ return 'TYPE_OTHER'; ++} ++ ++class ResponseStorage { ++ constructor(maxTotalSize, maxResponseSize) { ++ this._totalSize = 0; ++ this._maxResponseSize = maxResponseSize; ++ this._maxTotalSize = maxTotalSize; ++ this._responses = new Map(); ++ } ++ ++ addResponseBody(httpChannel, body) { ++ if (body.length > this._maxResponseSize) { ++ this._responses.set(requestId, { ++ evicted: true, ++ body: '', ++ }); ++ return; ++ } ++ let encodings = []; ++ if ((httpChannel instanceof Ci.nsIEncodedChannel) && httpChannel.contentEncodings && !httpChannel.applyConversion) { ++ const encodingHeader = httpChannel.getResponseHeader("Content-Encoding"); ++ encodings = encodingHeader.split(/\s*\t*,\s*\t*/); ++ } ++ this._responses.set(requestId(httpChannel), {body, encodings}); ++ this._totalSize += body.length; ++ if (this._totalSize > this._maxTotalSize) { ++ for (let [requestId, response] of this._responses) { ++ this._totalSize -= response.body.length; ++ response.body = ''; ++ response.evicted = true; ++ if (this._totalSize < this._maxTotalSize) ++ break; ++ } ++ } ++ } ++ ++ getBase64EncodedResponse(requestId) { ++ const response = this._responses.get(requestId); ++ if (!response) ++ throw new Error(`Request "${requestId}" is not found`); ++ if (response.evicted) ++ return {base64body: '', evicted: true}; ++ let result = response.body; ++ if (response.encodings && response.encodings.length) { ++ for (const encoding of response.encodings) ++ result = CommonUtils.convertString(result, encoding, 'uncompressed'); ++ } ++ return {base64body: btoa(result)}; ++ } ++} ++ ++class ResponseBodyListener { ++ constructor(networkObserver, browser, httpChannel) { ++ this._networkObserver = networkObserver; ++ this._browser = browser; ++ this._httpChannel = httpChannel; ++ this._chunks = []; ++ this.QueryInterface = ChromeUtils.generateQI([Ci.nsIStreamListener]); ++ httpChannel.QueryInterface(Ci.nsITraceableChannel); ++ this.originalListener = httpChannel.setNewListener(this); ++ } ++ ++ onDataAvailable(aRequest, aInputStream, aOffset, aCount) { ++ const iStream = new BinaryInputStream(aInputStream); ++ const sStream = new StorageStream(8192, aCount, null); ++ const oStream = new BinaryOutputStream(sStream.getOutputStream(0)); ++ ++ // Copy received data as they come. ++ const data = iStream.readBytes(aCount); ++ this._chunks.push(data); ++ ++ oStream.writeBytes(data, aCount); ++ this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); ++ } ++ ++ onStartRequest(aRequest) { ++ this.originalListener.onStartRequest(aRequest); ++ } ++ ++ onStopRequest(aRequest, aStatusCode) { ++ this.originalListener.onStopRequest(aRequest, aStatusCode); ++ const body = this._chunks.join(''); ++ delete this._chunks; ++ this._networkObserver._onResponseFinished(this._browser, this._httpChannel, body); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['NetworkObserver']; ++this.NetworkObserver = NetworkObserver; +diff --git a/testing/juggler/TargetRegistry.js b/testing/juggler/TargetRegistry.js +new file mode 100644 +index 000000000000..da5e4ee371d0 +--- /dev/null ++++ b/testing/juggler/TargetRegistry.js +@@ -0,0 +1,187 @@ ++const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++ ++const helper = new Helper(); ++ ++class TargetRegistry { ++ static instance() { ++ return TargetRegistry._instance || null; ++ } ++ ++ static initialize(mainWindow, contextManager) { ++ if (TargetRegistry._instance) ++ return; ++ TargetRegistry._instance = new TargetRegistry(mainWindow, contextManager); ++ } ++ ++ constructor(mainWindow, contextManager) { ++ EventEmitter.decorate(this); ++ ++ this._mainWindow = mainWindow; ++ this._contextManager = contextManager; ++ this._targets = new Map(); ++ ++ this._browserTarget = new BrowserTarget(); ++ this._targets.set(this._browserTarget.id(), this._browserTarget); ++ this._tabToTarget = new Map(); ++ ++ for (const tab of this._mainWindow.gBrowser.tabs) ++ this._ensureTargetForTab(tab); ++ this._mainWindow.gBrowser.tabContainer.addEventListener('TabOpen', event => { ++ this._ensureTargetForTab(event.target); ++ }); ++ this._mainWindow.gBrowser.tabContainer.addEventListener('TabClose', event => { ++ const tab = event.target; ++ const target = this._tabToTarget.get(tab); ++ if (!target) ++ return; ++ this._targets.delete(target.id()); ++ this._tabToTarget.delete(tab); ++ target.dispose(); ++ this.emit(TargetRegistry.Events.TargetDestroyed, target.info()); ++ }); ++ } ++ ++ async newPage({browserContextId}) { ++ const tab = this._mainWindow.gBrowser.addTab('about:blank', { ++ userContextId: this._contextManager.userContextId(browserContextId), ++ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), ++ }); ++ this._mainWindow.gBrowser.selectedTab = tab; ++ // Await navigation to about:blank ++ await new Promise(resolve => { ++ const wpl = { ++ onLocationChange: function(aWebProgress, aRequest, aLocation) { ++ tab.linkedBrowser.removeProgressListener(wpl); ++ resolve(); ++ }, ++ QueryInterface: ChromeUtils.generateQI([ ++ Ci.nsIWebProgressListener, ++ Ci.nsISupportsWeakReference, ++ ]), ++ }; ++ tab.linkedBrowser.addProgressListener(wpl); ++ }); ++ const target = this._ensureTargetForTab(tab); ++ return target.id(); ++ } ++ ++ async closePage(targetId, runBeforeUnload = false) { ++ const tab = this.tabForTarget(targetId); ++ await this._mainWindow.gBrowser.removeTab(tab, { ++ skipPermitUnload: !runBeforeUnload, ++ }); ++ } ++ ++ targetInfos() { ++ return Array.from(this._targets.values()).map(target => target.info()); ++ } ++ ++ targetInfo(targetId) { ++ const target = this._targets.get(targetId); ++ return target ? target.info() : null; ++ } ++ ++ browserTargetInfo() { ++ return this._browserTarget.info(); ++ } ++ ++ tabForTarget(targetId) { ++ const target = this._targets.get(targetId); ++ if (!target) ++ throw new Error(`Target "${targetId}" does not exist!`); ++ if (!(target instanceof PageTarget)) ++ throw new Error(`Target "${targetId}" is not a page!`); ++ return target._tab; ++ } ++ ++ _ensureTargetForTab(tab) { ++ if (this._tabToTarget.has(tab)) ++ return this._tabToTarget.get(tab); ++ const openerTarget = tab.openerTab ? this._ensureTargetForTab(tab.openerTab) : null; ++ const target = new PageTarget(this, tab, this._contextManager.browserContextId(tab.userContextId), openerTarget); ++ ++ this._targets.set(target.id(), target); ++ this._tabToTarget.set(tab, target); ++ this.emit(TargetRegistry.Events.TargetCreated, target.info()); ++ } ++} ++ ++let lastTabTargetId = 0; ++ ++class PageTarget { ++ constructor(registry, tab, browserContextId, opener) { ++ this._targetId = 'target-page-' + (++lastTabTargetId); ++ this._registry = registry; ++ this._tab = tab; ++ this._browserContextId = browserContextId; ++ this._openerId = opener ? opener.id() : undefined; ++ this._url = tab.linkedBrowser.currentURI.spec; ++ ++ // First navigation always happens to about:blank - do not report it. ++ this._skipNextNavigation = true; ++ ++ const navigationListener = { ++ QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]), ++ onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation), ++ }; ++ this._eventListeners = [ ++ helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION), ++ ]; ++ } ++ ++ id() { ++ return this._targetId; ++ } ++ ++ info() { ++ return { ++ targetId: this.id(), ++ type: 'page', ++ url: this._url, ++ browserContextId: this._browserContextId, ++ openerId: this._openerId, ++ }; ++ } ++ ++ _onNavigated(aLocation) { ++ if (this._skipNextNavigation) { ++ this._skipNextNavigation = false; ++ return; ++ } ++ this._url = aLocation.spec; ++ this._registry.emit(TargetRegistry.Events.TargetChanged, this.info()); ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++} ++ ++class BrowserTarget { ++ id() { ++ return 'target-browser'; ++ } ++ ++ info() { ++ return { ++ targetId: this.id(), ++ type: 'browser', ++ url: '', ++ } ++ } ++} ++ ++TargetRegistry.Events = { ++ TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'), ++ TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'), ++ TargetChanged: Symbol('TargetRegistry.Events.TargetChanged'), ++}; ++ ++var EXPORTED_SYMBOLS = ['TargetRegistry']; ++this.TargetRegistry = TargetRegistry; +diff --git a/testing/juggler/components/juggler.js b/testing/juggler/components/juggler.js +new file mode 100644 +index 000000000000..9654aeeb257d +--- /dev/null ++++ b/testing/juggler/components/juggler.js +@@ -0,0 +1,112 @@ ++const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js"); ++const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js"); ++const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js"); ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++ ++const FRAME_SCRIPT = "chrome://juggler/content/content/main.js"; ++ ++// Command Line Handler ++function CommandLineHandler() { ++ this._port = -1; ++}; ++ ++CommandLineHandler.prototype = { ++ classDescription: "Sample command-line handler", ++ classID: Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'), ++ contractID: "@mozilla.org/remote/juggler;1", ++ _xpcom_categories: [{ ++ category: "command-line-handler", ++ entry: "m-juggler" ++ }], ++ ++ /* nsICommandLineHandler */ ++ handle: async function(cmdLine) { ++ const jugglerFlag = cmdLine.handleFlagWithParam("juggler", false); ++ if (!jugglerFlag || isNaN(jugglerFlag)) ++ return; ++ this._port = parseInt(jugglerFlag, 10); ++ Services.obs.addObserver(this, 'sessionstore-windows-restored'); ++ }, ++ ++ observe: async function(subject, topic) { ++ Services.obs.removeObserver(this, 'sessionstore-windows-restored'); ++ ++ const win = await waitForBrowserWindow(); ++ BrowserContextManager.initialize(); ++ NetworkObserver.initialize(); ++ TargetRegistry.initialize(win, BrowserContextManager.instance()); ++ ++ const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); ++ const WebSocketServer = require('devtools/server/socket/websocket-server'); ++ this._server = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket); ++ this._server.initSpecialConnection(this._port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4); ++ this._server.asyncListen({ ++ onSocketAccepted: async(socket, transport) => { ++ const input = transport.openInputStream(0, 0, 0); ++ const output = transport.openOutputStream(0, 0, 0); ++ const webSocket = await WebSocketServer.accept(transport, input, output); ++ new Dispatcher(webSocket); ++ } ++ }); ++ ++ Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */); ++ dump(`Juggler listening on ws://127.0.0.1:${this._server.port}\n`); ++ }, ++ ++ QueryInterface: ChromeUtils.generateQI([ Ci.nsICommandLineHandler ]), ++ ++ // CHANGEME: change the help info as appropriate, but ++ // follow the guidelines in nsICommandLineHandler.idl ++ // specifically, flag descriptions should start at ++ // character 24, and lines should be wrapped at ++ // 72 characters with embedded newlines, ++ // and finally, the string should end with a newline ++ helpInfo : " --juggler Enable Juggler automation\n" ++}; ++ ++var NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandLineHandler]); ++ ++/** ++ * @return {!Promise} ++ */ ++async function waitForBrowserWindow() { ++ const windowsIt = Services.wm.getEnumerator('navigator:browser'); ++ if (windowsIt.hasMoreElements()) ++ return waitForWindowLoaded(windowsIt.getNext()); ++ ++ let fulfill; ++ let promise = new Promise(x => fulfill = x); ++ ++ const listener = { ++ onOpenWindow: window => { ++ if (window instanceof Ci.nsIDOMChromeWindow) { ++ Services.wm.removeListener(listener); ++ fulfill(waitForWindowLoaded(window)); ++ } ++ }, ++ onCloseWindow: () => {} ++ }; ++ Services.wm.addListener(listener); ++ return promise; ++ ++ /** ++ * @param {!Ci.nsIDOMChromeWindow} window ++ * @return {!Promise} ++ */ ++ function waitForWindowLoaded(window) { ++ if (window.document.readyState === 'complete') ++ return window; ++ return new Promise(fulfill => { ++ window.addEventListener('load', function listener() { ++ window.removeEventListener('load', listener); ++ fulfill(window); ++ }); ++ }); ++ } ++} +diff --git a/testing/juggler/components/juggler.manifest b/testing/juggler/components/juggler.manifest +new file mode 100644 +index 000000000000..50f893020756 +--- /dev/null ++++ b/testing/juggler/components/juggler.manifest +@@ -0,0 +1,3 @@ ++component {f7a74a33-e2ab-422d-b022-4fb213dd2639} juggler.js ++contract @mozilla.org/remote/juggler;1 {f7a74a33-e2ab-422d-b022-4fb213dd2639} ++category command-line-handler m-juggler @mozilla.org/remote/juggler;1 +diff --git a/testing/juggler/components/moz.build b/testing/juggler/components/moz.build +new file mode 100644 +index 000000000000..268fbc361d80 +--- /dev/null ++++ b/testing/juggler/components/moz.build +@@ -0,0 +1,9 @@ ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, You can obtain one at http://mozilla.org/MPL/2.0/. ++ ++EXTRA_COMPONENTS += [ ++ "juggler.js", ++ "juggler.manifest", ++] ++ +diff --git a/testing/juggler/content/ContentSession.js b/testing/juggler/content/ContentSession.js +new file mode 100644 +index 000000000000..f68780d529e7 +--- /dev/null ++++ b/testing/juggler/content/ContentSession.js +@@ -0,0 +1,63 @@ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {RuntimeAgent} = ChromeUtils.import('chrome://juggler/content/content/RuntimeAgent.js'); ++const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js'); ++ ++const helper = new Helper(); ++ ++class ContentSession { ++ /** ++ * @param {string} sessionId ++ * @param {!ContentFrameMessageManager} messageManager ++ * @param {!FrameTree} frameTree ++ * @param {!ScrollbarManager} scrollbarManager ++ * @param {!NetworkMonitor} networkMonitor ++ */ ++ constructor(sessionId, messageManager, frameTree, scrollbarManager, networkMonitor) { ++ this._sessionId = sessionId; ++ this._messageManager = messageManager; ++ const runtimeAgent = new RuntimeAgent(this); ++ const pageAgent = new PageAgent(this, runtimeAgent, frameTree, scrollbarManager, networkMonitor); ++ this._agents = { ++ Page: pageAgent, ++ Runtime: runtimeAgent, ++ }; ++ this._eventListeners = [ ++ helper.addMessageListener(messageManager, this._sessionId, this._onMessage.bind(this)), ++ ]; ++ } ++ ++ emitEvent(eventName, params) { ++ this._messageManager.sendAsyncMessage(this._sessionId, {eventName, params}); ++ } ++ ++ mm() { ++ return this._messageManager; ++ } ++ ++ async _onMessage(msg) { ++ const id = msg.data.id; ++ try { ++ const [domainName, methodName] = msg.data.methodName.split('.'); ++ const agent = this._agents[domainName]; ++ if (!agent) ++ throw new Error(`unknown domain: ${domainName}`); ++ const handler = agent[methodName]; ++ if (!handler) ++ throw new Error(`unknown method: ${domainName}.${methodName}`); ++ const result = await handler.call(agent, msg.data.params); ++ this._messageManager.sendAsyncMessage(this._sessionId, {id, result}); ++ } catch (e) { ++ this._messageManager.sendAsyncMessage(this._sessionId, {id, error: e.message + '\n' + e.stack}); ++ } ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ for (const agent of Object.values(this._agents)) ++ agent.dispose(); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['ContentSession']; ++this.ContentSession = ContentSession; ++ +diff --git a/testing/juggler/content/FrameTree.js b/testing/juggler/content/FrameTree.js +new file mode 100644 +index 000000000000..2931c75e60d2 +--- /dev/null ++++ b/testing/juggler/content/FrameTree.js +@@ -0,0 +1,232 @@ ++"use strict"; ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); ++ ++const helper = new Helper(); ++ ++class FrameTree { ++ constructor(rootDocShell) { ++ EventEmitter.decorate(this); ++ this._docShellToFrame = new Map(); ++ this._frameIdToFrame = new Map(); ++ this._mainFrame = this._createFrame(rootDocShell); ++ const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor) ++ .getInterface(Ci.nsIWebProgress); ++ this.QueryInterface = ChromeUtils.generateQI([ ++ Ci.nsIWebProgressListener, ++ Ci.nsIWebProgressListener2, ++ Ci.nsISupportsWeakReference, ++ ]); ++ ++ const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT | ++ Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION; ++ this._eventListeners = [ ++ helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'), ++ helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'), ++ helper.addProgressListener(webProgress, this, flags), ++ ]; ++ } ++ ++ frameForDocShell(docShell) { ++ return this._docShellToFrame.get(docShell) || null; ++ } ++ ++ frame(frameId) { ++ return this._frameIdToFrame.get(frameId) || null; ++ } ++ ++ frames() { ++ let result = []; ++ collect(this._mainFrame); ++ return result; ++ ++ function collect(frame) { ++ result.push(frame); ++ for (const subframe of frame._children) ++ collect(subframe); ++ } ++ } ++ ++ mainFrame() { ++ return this._mainFrame; ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ onStateChange(progress, request, flag, status) { ++ if (!(request instanceof Ci.nsIChannel)) ++ return; ++ const channel = request.QueryInterface(Ci.nsIChannel); ++ const docShell = progress.DOMWindow.docShell; ++ const frame = this._docShellToFrame.get(docShell); ++ if (!frame) { ++ dump(`ERROR: got a state changed event for un-tracked docshell!\n`); ++ return; ++ } ++ ++ const isStart = flag & Ci.nsIWebProgressListener.STATE_START; ++ const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING; ++ const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; ++ ++ if (isStart) { ++ // Starting a new navigation. ++ frame._pendingNavigationId = helper.generateId(); ++ frame._pendingNavigationURL = channel.URI.spec; ++ this.emit(FrameTree.Events.NavigationStarted, frame); ++ } else if (isTransferring || (isStop && frame._pendingNavigationId && !status)) { ++ // Navigation is committed. ++ for (const subframe of frame._children) ++ this._detachFrame(subframe); ++ const navigationId = frame._pendingNavigationId; ++ frame._pendingNavigationId = null; ++ frame._pendingNavigationURL = null; ++ frame._lastCommittedNavigationId = navigationId; ++ frame._url = channel.URI.spec; ++ this.emit(FrameTree.Events.NavigationCommitted, frame); ++ } else if (isStop && frame._pendingNavigationId && status) { ++ // Navigation is aborted. ++ const navigationId = frame._pendingNavigationId; ++ frame._pendingNavigationId = null; ++ frame._pendingNavigationURL = null; ++ this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, helper.getNetworkErrorStatusText(status)); ++ } ++ } ++ ++ onFrameLocationChange(progress, request, location, flags) { ++ const docShell = progress.DOMWindow.docShell; ++ const frame = this._docShellToFrame.get(docShell); ++ const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); ++ if (frame && sameDocumentNavigation) { ++ frame._url = location.spec; ++ this.emit(FrameTree.Events.SameDocumentNavigation, frame); ++ } ++ } ++ ++ _onDocShellCreated(docShell) { ++ // Bug 1142752: sometimes, the docshell appears to be immediately ++ // destroyed, bailout early to prevent random exceptions. ++ if (docShell.isBeingDestroyed()) ++ return; ++ // If this docShell doesn't belong to our frame tree - do nothing. ++ let root = docShell; ++ while (root.parent) ++ root = root.parent; ++ if (root === this._mainFrame._docShell) ++ this._createFrame(docShell); ++ } ++ ++ _createFrame(docShell) { ++ const parentFrame = this._docShellToFrame.get(docShell.parent) || null; ++ const frame = new Frame(this, docShell, parentFrame); ++ this._docShellToFrame.set(docShell, frame); ++ this._frameIdToFrame.set(frame.id(), frame); ++ this.emit(FrameTree.Events.FrameAttached, frame); ++ return frame; ++ } ++ ++ _onDocShellDestroyed(docShell) { ++ const frame = this._docShellToFrame.get(docShell); ++ if (frame) ++ this._detachFrame(frame); ++ } ++ ++ _detachFrame(frame) { ++ // Detach all children first ++ for (const subframe of frame._children) ++ this._detachFrame(subframe); ++ this._docShellToFrame.delete(frame._docShell); ++ this._frameIdToFrame.delete(frame.id()); ++ if (frame._parentFrame) ++ frame._parentFrame._children.delete(frame); ++ frame._parentFrame = null; ++ this.emit(FrameTree.Events.FrameDetached, frame); ++ } ++} ++ ++FrameTree.Events = { ++ FrameAttached: 'frameattached', ++ FrameDetached: 'framedetached', ++ NavigationStarted: 'navigationstarted', ++ NavigationCommitted: 'navigationcommitted', ++ NavigationAborted: 'navigationaborted', ++ SameDocumentNavigation: 'samedocumentnavigation', ++}; ++ ++class Frame { ++ constructor(frameTree, docShell, parentFrame) { ++ this._frameTree = frameTree; ++ this._docShell = docShell; ++ this._children = new Set(); ++ this._frameId = helper.generateId(); ++ this._parentFrame = null; ++ this._url = ''; ++ if (parentFrame) { ++ this._parentFrame = parentFrame; ++ parentFrame._children.add(this); ++ } ++ ++ this._lastCommittedNavigationId = null; ++ this._pendingNavigationId = null; ++ this._pendingNavigationURL = null; ++ ++ this._textInputProcessor = null; ++ } ++ ++ textInputProcessor() { ++ if (!this._textInputProcessor) { ++ this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor); ++ this._textInputProcessor.beginInputTransactionForTests(this._docShell.DOMWindow); ++ } ++ return this._textInputProcessor; ++ } ++ ++ pendingNavigationId() { ++ return this._pendingNavigationId; ++ } ++ ++ pendingNavigationURL() { ++ return this._pendingNavigationURL; ++ } ++ ++ lastCommittedNavigationId() { ++ return this._lastCommittedNavigationId; ++ } ++ ++ docShell() { ++ return this._docShell; ++ } ++ ++ domWindow() { ++ return this._docShell.domWindow; ++ } ++ ++ name() { ++ const frameElement = this._docShell.domWindow.frameElement; ++ let name = ''; ++ if (frameElement) ++ name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || ''; ++ return name; ++ } ++ ++ parentFrame() { ++ return this._parentFrame; ++ } ++ ++ id() { ++ return this._frameId; ++ } ++ ++ url() { ++ return this._url; ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['FrameTree']; ++this.FrameTree = FrameTree; ++ +diff --git a/testing/juggler/content/NetworkMonitor.js b/testing/juggler/content/NetworkMonitor.js +new file mode 100644 +index 000000000000..2508cce41565 +--- /dev/null ++++ b/testing/juggler/content/NetworkMonitor.js +@@ -0,0 +1,62 @@ ++"use strict"; ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++ ++const helper = new Helper(); ++ ++class NetworkMonitor { ++ constructor(rootDocShell, frameTree) { ++ this._frameTree = frameTree; ++ this._requestDetails = new Map(); ++ ++ this._eventListeners = [ ++ helper.addObserver(this._onRequest.bind(this), 'http-on-opening-request'), ++ ]; ++ } ++ ++ _onRequest(channel) { ++ if (!(channel instanceof Ci.nsIHttpChannel)) ++ return; ++ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext) ++ return; ++ const window = loadContext.associatedWindow; ++ const frame = this._frameTree.frameForDocShell(window.docShell) ++ if (!frame) ++ return; ++ this._requestDetails.set(httpChannel.channelId, { ++ frameId: frame.id(), ++ }); ++ } ++ ++ requestDetails(channelId) { ++ return this._requestDetails.get(channelId) || null; ++ } ++ ++ dispose() { ++ this._requestDetails.clear(); ++ helper.removeListeners(this._eventListeners); ++ } ++} ++ ++function getLoadContext(httpChannel) { ++ let loadContext = null; ++ try { ++ if (httpChannel.notificationCallbacks) ++ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext); ++ } catch (e) {} ++ try { ++ if (!loadContext && httpChannel.loadGroup) ++ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); ++ } catch (e) { } ++ return loadContext; ++} ++ ++ ++var EXPORTED_SYMBOLS = ['NetworkMonitor']; ++this.NetworkMonitor = NetworkMonitor; ++ +diff --git a/testing/juggler/content/PageAgent.js b/testing/juggler/content/PageAgent.js +new file mode 100644 +index 000000000000..e8db4031620e +--- /dev/null ++++ b/testing/juggler/content/PageAgent.js +@@ -0,0 +1,621 @@ ++"use strict"; ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); ++ ++const helper = new Helper(); ++ ++class PageAgent { ++ constructor(session, runtimeAgent, frameTree, scrollbarManager, networkMonitor) { ++ this._session = session; ++ this._runtime = runtimeAgent; ++ this._frameTree = frameTree; ++ this._networkMonitor = networkMonitor; ++ this._scrollbarManager = scrollbarManager; ++ ++ this._frameToExecutionContext = new Map(); ++ this._scriptsToEvaluateOnNewDocument = new Map(); ++ this._bindingsToAdd = new Set(); ++ ++ this._eventListeners = []; ++ this._enabled = false; ++ ++ const docShell = frameTree.mainFrame().docShell(); ++ this._initialDPPX = docShell.contentViewer.overrideDPPX; ++ this._customScrollbars = null; ++ } ++ ++ async awaitViewportDimensions({width, height}) { ++ const win = this._frameTree.mainFrame().domWindow(); ++ if (win.innerWidth === width && win.innerHeight === height) ++ return; ++ await new Promise(resolve => { ++ const listener = helper.addEventListener(win, 'resize', () => { ++ if (win.innerWidth === width && win.innerHeight === height) { ++ helper.removeListeners([listener]); ++ resolve(); ++ } ++ }); ++ }); ++ } ++ ++ requestDetails({channelId}) { ++ return this._networkMonitor.requestDetails(channelId); ++ } ++ ++ async setViewport({deviceScaleFactor, isMobile, hasTouch}) { ++ const docShell = this._frameTree.mainFrame().docShell(); ++ docShell.contentViewer.overrideDPPX = deviceScaleFactor || this._initialDPPX; ++ docShell.deviceSizeIsPageSize = isMobile; ++ docShell.touchEventsOverride = hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE; ++ this._scrollbarManager.setFloatingScrollbars(isMobile); ++ } ++ ++ async setEmulatedMedia({media}) { ++ const docShell = this._frameTree.mainFrame().docShell(); ++ if (media) ++ docShell.contentViewer.emulateMedium(media); ++ else ++ docShell.contentViewer.stopEmulatingMedium(); ++ } ++ ++ async setUserAgent({userAgent}) { ++ const docShell = this._frameTree.mainFrame().docShell(); ++ docShell.customUserAgent = userAgent; ++ } ++ ++ addScriptToEvaluateOnNewDocument({script}) { ++ const scriptId = helper.generateId(); ++ this._scriptsToEvaluateOnNewDocument.set(scriptId, script); ++ return {scriptId}; ++ } ++ ++ removeScriptToEvaluateOnNewDocument({scriptId}) { ++ this._scriptsToEvaluateOnNewDocument.delete(scriptId); ++ } ++ ++ setCacheDisabled({cacheDisabled}) { ++ const enable = Ci.nsIRequest.LOAD_NORMAL; ++ const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE | ++ Ci.nsIRequest.INHIBIT_CACHING; ++ ++ const docShell = this._frameTree.mainFrame().docShell(); ++ docShell.defaultLoadFlags = cacheDisabled ? disable : enable; ++ } ++ ++ setJavascriptEnabled({enabled}) { ++ const docShell = this._frameTree.mainFrame().docShell(); ++ docShell.allowJavascript = enabled; ++ } ++ ++ enable() { ++ if (this._enabled) ++ return; ++ ++ this._enabled = true; ++ // Dispatch frameAttached events for all initial frames ++ for (const frame of this._frameTree.frames()) { ++ this._onFrameAttached(frame); ++ if (frame.url()) ++ this._onNavigationCommitted(frame); ++ if (frame.pendingNavigationId()) ++ this._onNavigationStarted(frame); ++ } ++ this._eventListeners = [ ++ helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'), ++ helper.addEventListener(this._session.mm(), 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)), ++ helper.addEventListener(this._session.mm(), 'pageshow', this._onLoad.bind(this)), ++ helper.addEventListener(this._session.mm(), 'error', this._onError.bind(this)), ++ helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)), ++ helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)), ++ helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)), ++ helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)), ++ helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)), ++ helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)), ++ ]; ++ } ++ ++ _onDOMContentLoaded(event) { ++ const docShell = event.target.ownerGlobal.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ if (!frame) ++ return; ++ this._session.emitEvent('Page.eventFired', { ++ frameId: frame.id(), ++ name: 'DOMContentLoaded', ++ }); ++ } ++ ++ _onError(errorEvent) { ++ const docShell = errorEvent.target.ownerGlobal.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ if (!frame) ++ return; ++ this._session.emitEvent('Page.uncaughtError', { ++ frameId: frame.id(), ++ message: errorEvent.message, ++ stack: errorEvent.error.stack ++ }); ++ } ++ ++ _onLoad(event) { ++ const docShell = event.target.ownerGlobal.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ if (!frame) ++ return; ++ this._session.emitEvent('Page.eventFired', { ++ frameId: frame.id(), ++ name: 'load' ++ }); ++ } ++ ++ _onNavigationStarted(frame) { ++ this._session.emitEvent('Page.navigationStarted', { ++ frameId: frame.id(), ++ navigationId: frame.pendingNavigationId(), ++ url: frame.pendingNavigationURL(), ++ }); ++ } ++ ++ _onNavigationAborted(frame, navigationId, errorText) { ++ this._session.emitEvent('Page.navigationAborted', { ++ frameId: frame.id(), ++ navigationId, ++ errorText, ++ }); ++ } ++ ++ _onSameDocumentNavigation(frame) { ++ this._session.emitEvent('Page.sameDocumentNavigation', { ++ frameId: frame.id(), ++ url: frame.url(), ++ }); ++ } ++ ++ _onNavigationCommitted(frame) { ++ this._session.emitEvent('Page.navigationCommitted', { ++ frameId: frame.id(), ++ navigationId: frame.lastCommittedNavigationId(), ++ url: frame.url(), ++ name: frame.name(), ++ }); ++ } ++ ++ _onDOMWindowCreated(window) { ++ const docShell = window.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ if (!frame) ++ return; ++ ++ if (this._frameToExecutionContext.has(frame)) { ++ this._runtime.destroyExecutionContext(this._frameToExecutionContext.get(frame)); ++ this._frameToExecutionContext.delete(frame); ++ } ++ const executionContext = this._ensureExecutionContext(frame); ++ ++ if (!this._scriptsToEvaluateOnNewDocument.size && !this._bindingsToAdd.size) ++ return; ++ for (const bindingName of this._bindingsToAdd.values()) ++ this._exposeFunction(frame, bindingName); ++ for (const script of this._scriptsToEvaluateOnNewDocument.values()) { ++ try { ++ let result = executionContext.evaluateScript(script); ++ if (result && result.objectId) ++ executionContext.disposeObject(result.objectId); ++ } catch (e) { ++ } ++ } ++ } ++ ++ _onFrameAttached(frame) { ++ this._session.emitEvent('Page.frameAttached', { ++ frameId: frame.id(), ++ parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined, ++ }); ++ this._ensureExecutionContext(frame); ++ } ++ ++ _onFrameDetached(frame) { ++ this._session.emitEvent('Page.frameDetached', { ++ frameId: frame.id(), ++ }); ++ } ++ ++ _ensureExecutionContext(frame) { ++ let executionContext = this._frameToExecutionContext.get(frame); ++ if (!executionContext) { ++ executionContext = this._runtime.createExecutionContext(frame.domWindow(), { ++ frameId: frame.id(), ++ }); ++ this._frameToExecutionContext.set(frame, executionContext); ++ } ++ return executionContext; ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ async navigate({frameId, url, referer}) { ++ try { ++ const uri = NetUtil.newURI(url); ++ } catch (e) { ++ throw new Error(`Invalid url: "${url}"`); ++ } ++ let referrerURI = null; ++ let referrerInfo = null; ++ if (referer) { ++ try { ++ referrerURI = NetUtil.newURI(referer); ++ const ReferrerInfo = Components.Constructor( ++ '@mozilla.org/referrer-info;1', ++ 'nsIReferrerInfo', ++ 'init' ++ ); ++ referrerInfo = new ReferrerInfo(Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI); ++ } catch (e) { ++ throw new Error(`Invalid referer: "${referer}"`); ++ } ++ } ++ const frame = this._frameTree.frame(frameId); ++ const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation); ++ docShell.loadURI(url, { ++ flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE, ++ referrerInfo, ++ postData: null, ++ headers: null, ++ }); ++ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; ++ } ++ ++ async reload({frameId, url}) { ++ const frame = this._frameTree.frame(frameId); ++ const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation); ++ docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); ++ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; ++ } ++ ++ async goBack({frameId, url}) { ++ const frame = this._frameTree.frame(frameId); ++ const docShell = frame.docShell(); ++ if (!docShell.canGoBack) ++ return {navigationId: null, navigationURL: null}; ++ docShell.goBack(); ++ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; ++ } ++ ++ async goForward({frameId, url}) { ++ const frame = this._frameTree.frame(frameId); ++ const docShell = frame.docShell(); ++ if (!docShell.canGoForward) ++ return {navigationId: null, navigationURL: null}; ++ docShell.goForward(); ++ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; ++ } ++ ++ addBinding({name}) { ++ if (this._bindingsToAdd.has(name)) ++ throw new Error(`Binding with name ${name} already exists`); ++ this._bindingsToAdd.add(name); ++ for (const frame of this._frameTree.frames()) ++ this._exposeFunction(frame, name); ++ } ++ ++ _exposeFunction(frame, name) { ++ Cu.exportFunction((...args) => { ++ const executionContext = this._ensureExecutionContext(frame); ++ this._session.emitEvent('Page.bindingCalled', { ++ executionContextId: executionContext.id(), ++ name, ++ payload: args[0] ++ }); ++ }, frame.domWindow(), { ++ defineAs: name, ++ }); ++ } ++ ++ async setFileInputFiles({objectId, frameId, files}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const executionContext = this._ensureExecutionContext(frame); ++ const unsafeObject = executionContext.unsafeObject(objectId); ++ if (!unsafeObject) ++ throw new Error('Object is not input!'); ++ const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath))); ++ unsafeObject.mozSetFileArray(nsFiles); ++ } ++ ++ getContentQuads({objectId, frameId}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const executionContext = this._ensureExecutionContext(frame); ++ const unsafeObject = executionContext.unsafeObject(objectId); ++ if (!unsafeObject.getBoxQuads) ++ throw new Error('RemoteObject is not a node'); ++ const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => { ++ return { ++ p1: {x: quad.p1.x, y: quad.p1.y}, ++ p2: {x: quad.p2.x, y: quad.p2.y}, ++ p3: {x: quad.p3.x, y: quad.p3.y}, ++ p4: {x: quad.p4.x, y: quad.p4.y}, ++ }; ++ }); ++ return {quads}; ++ } ++ ++ contentFrame({objectId, frameId}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const executionContext = this._ensureExecutionContext(frame); ++ const unsafeObject = executionContext.unsafeObject(objectId); ++ if (!unsafeObject.contentWindow) ++ return null; ++ const contentFrame = this._frameTree.frameForDocShell(unsafeObject.contentWindow.docShell); ++ return {frameId: contentFrame.id()}; ++ } ++ ++ async getBoundingBox({frameId, objectId}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const executionContext = this._ensureExecutionContext(frame); ++ const unsafeObject = executionContext.unsafeObject(objectId); ++ if (!unsafeObject.getBoxQuads) ++ throw new Error('RemoteObject is not a node'); ++ const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}); ++ if (!quads.length) ++ return null; ++ let x1 = Infinity; ++ let y1 = Infinity; ++ let x2 = -Infinity; ++ let y2 = -Infinity; ++ for (const quad of quads) { ++ const boundingBox = quad.getBounds(); ++ x1 = Math.min(boundingBox.x, x1); ++ y1 = Math.min(boundingBox.y, y1); ++ x2 = Math.max(boundingBox.x + boundingBox.width, x2); ++ y2 = Math.max(boundingBox.y + boundingBox.height, y2); ++ } ++ return {x: x1 + frame.domWindow().scrollX, y: y1 + frame.domWindow().scrollY, width: x2 - x1, height: y2 - y1}; ++ } ++ ++ async screenshot({mimeType, fullPage, clip}) { ++ const content = this._session.mm().content; ++ if (clip) { ++ const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType); ++ return {data}; ++ } ++ if (fullPage) { ++ const rect = content.document.documentElement.getBoundingClientRect(); ++ const width = content.innerWidth + content.scrollMaxX - content.scrollMinX; ++ const height = content.innerHeight + content.scrollMaxY - content.scrollMinY; ++ const data = takeScreenshot(content, 0, 0, width, height, mimeType); ++ return {data}; ++ } ++ const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType); ++ return {data}; ++ } ++ ++ async dispatchKeyEvent({type, keyCode, code, key, repeat, location}) { ++ const frame = this._frameTree.mainFrame(); ++ const tip = frame.textInputProcessor(); ++ if (key === 'Meta' && Services.appinfo.OS !== 'Darwin') ++ key = 'OS'; ++ else if (key === 'OS' && Services.appinfo.OS === 'Darwin') ++ key = 'Meta'; ++ let keyEvent = new (frame.domWindow().KeyboardEvent)("", { ++ key, ++ code, ++ location, ++ repeat, ++ keyCode ++ }); ++ const flags = 0; ++ if (type === 'keydown') ++ tip.keydown(keyEvent, flags); ++ else if (type === 'keyup') ++ tip.keyup(keyEvent, flags); ++ else ++ throw new Error(`Unknown type ${type}`); ++ } ++ ++ async dispatchTouchEvent({type, touchPoints, modifiers}) { ++ const frame = this._frameTree.mainFrame(); ++ const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent( ++ type.toLowerCase(), ++ touchPoints.map((point, id) => id), ++ touchPoints.map(point => point.x), ++ touchPoints.map(point => point.y), ++ touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX), ++ touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY), ++ touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle), ++ touchPoints.map(point => point.force === undefined ? 1.0 : point.force), ++ touchPoints.length, ++ modifiers); ++ return {defaultPrevented}; ++ } ++ ++ async dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) { ++ const frame = this._frameTree.mainFrame(); ++ frame.domWindow().windowUtils.sendMouseEvent( ++ type, ++ x, ++ y, ++ button, ++ clickCount, ++ modifiers, ++ false /*aIgnoreRootScrollFrame*/, ++ undefined /*pressure*/, ++ undefined /*inputSource*/, ++ undefined /*isDOMEventSynthesized*/, ++ undefined /*isWidgetEventSynthesized*/, ++ buttons); ++ if (type === 'mousedown' && button === 2) { ++ frame.domWindow().windowUtils.sendMouseEvent( ++ 'contextmenu', ++ x, ++ y, ++ button, ++ clickCount, ++ modifiers, ++ false /*aIgnoreRootScrollFrame*/, ++ undefined /*pressure*/, ++ undefined /*inputSource*/, ++ undefined /*isDOMEventSynthesized*/, ++ undefined /*isWidgetEventSynthesized*/, ++ buttons); ++ } ++ } ++ ++ async insertText({text}) { ++ const frame = this._frameTree.mainFrame(); ++ frame.textInputProcessor().commitCompositionWith(text); ++ } ++ ++ async getFullAXTree() { ++ const service = Cc["@mozilla.org/accessibilityService;1"] ++ .getService(Ci.nsIAccessibilityService); ++ const document = this._frameTree.mainFrame().domWindow().document; ++ const docAcc = service.getAccessibleFor(document); ++ ++ async function waitForQuiet() { ++ let state = {}; ++ docAcc.getState(state, {}); ++ if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) ++ return; ++ let resolve, reject; ++ const promise = new Promise((x, y) => {resolve = x, reject = y}); ++ let eventObserver = { ++ observe(subject, topic) { ++ if (topic !== "accessible-event") { ++ return; ++ } ++ ++ // If event type does not match expected type, skip the event. ++ let event = subject.QueryInterface(Ci.nsIAccessibleEvent); ++ if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) { ++ return; ++ } ++ ++ // If event's accessible does not match expected accessible, ++ // skip the event. ++ if (event.accessible !== docAcc) { ++ return; ++ } ++ ++ Services.obs.removeObserver(this, "accessible-event"); ++ resolve(); ++ }, ++ }; ++ Services.obs.addObserver(eventObserver, "accessible-event"); ++ return promise; ++ } ++ function buildNode(accElement) { ++ let a = {}, b = {}; ++ accElement.getState(a, b); ++ const tree = { ++ role: service.getStringRole(accElement.role), ++ name: accElement.name || '', ++ }; ++ for (const userStringProperty of [ ++ 'value', ++ 'description' ++ ]) { ++ tree[userStringProperty] = accElement[userStringProperty] || undefined; ++ } ++ ++ const states = {}; ++ for (const name of service.getStringStates(a.value, b.value)) ++ states[name] = true; ++ for (const name of ['selected', ++ 'focused', ++ 'pressed', ++ 'focusable', ++ 'haspopup', ++ 'required', ++ 'invalid', ++ 'modal', ++ 'editable', ++ 'busy', ++ 'checked', ++ 'multiselectable']) { ++ if (states[name]) ++ tree[name] = true; ++ } ++ ++ if (states['multi line']) ++ tree['multiline'] = true; ++ if (states['editable'] && states['readonly']) ++ tree['readonly'] = true; ++ if (states['checked']) ++ tree['checked'] = true; ++ if (states['mixed']) ++ tree['checked'] = 'mixed'; ++ if (states['expanded']) ++ tree['expanded'] = true; ++ else if (states['collapsed']) ++ tree['expanded'] = false ++ if (!states['enabled']) ++ tree['disabled'] = true; ++ ++ const attributes = {}; ++ if (accElement.attributes) { ++ for (const { key, value } of accElement.attributes.enumerate()) { ++ attributes[key] = value; ++ } ++ } ++ for (const numericalProperty of ['level']) { ++ if (numericalProperty in attributes) ++ tree[numericalProperty] = parseFloat(attributes[numericalProperty]); ++ } ++ for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts']) { ++ if (stringProperty in attributes) ++ tree[stringProperty] = attributes[stringProperty]; ++ } ++ const children = []; ++ ++ for (let child = accElement.firstChild; child; child = child.nextSibling) { ++ children.push(buildNode(child)); ++ } ++ if (children.length) ++ tree.children = children; ++ return tree; ++ } ++ await waitForQuiet(); ++ return { ++ tree: buildNode(docAcc) ++ }; ++ } ++} ++ ++function takeScreenshot(win, left, top, width, height, mimeType) { ++ const MAX_SKIA_DIMENSIONS = 32767; ++ ++ const scale = win.devicePixelRatio; ++ const canvasWidth = width * scale; ++ const canvasHeight = height * scale; ++ ++ if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS) ++ throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS); ++ ++ const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas'); ++ canvas.width = canvasWidth; ++ canvas.height = canvasHeight; ++ ++ let ctx = canvas.getContext('2d'); ++ ctx.scale(scale, scale); ++ ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET); ++ const dataURL = canvas.toDataURL(mimeType); ++ return dataURL.substring(dataURL.indexOf(',') + 1); ++}; ++ ++var EXPORTED_SYMBOLS = ['PageAgent']; ++this.PageAgent = PageAgent; ++ +diff --git a/testing/juggler/content/RuntimeAgent.js b/testing/juggler/content/RuntimeAgent.js +new file mode 100644 +index 000000000000..2c474230071b +--- /dev/null ++++ b/testing/juggler/content/RuntimeAgent.js +@@ -0,0 +1,460 @@ ++"use strict"; ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {}); ++ ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++addDebuggerToGlobal(Cu.getGlobalForObject(this)); ++const helper = new Helper(); ++ ++const consoleLevelToProtocolType = { ++ 'dir': 'dir', ++ 'log': 'log', ++ 'debug': 'debug', ++ 'info': 'info', ++ 'error': 'error', ++ 'warn': 'warning', ++ 'dirxml': 'dirxml', ++ 'table': 'table', ++ 'trace': 'trace', ++ 'clear': 'clear', ++ 'group': 'startGroup', ++ 'groupCollapsed': 'startGroupCollapsed', ++ 'groupEnd': 'endGroup', ++ 'assert': 'assert', ++ 'profile': 'profile', ++ 'profileEnd': 'profileEnd', ++ 'count': 'count', ++ 'countReset': 'countReset', ++ 'time': null, ++ 'timeLog': 'timeLog', ++ 'timeEnd': 'timeEnd', ++ 'timeStamp': 'timeStamp', ++}; ++ ++const disallowedMessageCategories = new Set([ ++ 'XPConnect JavaScript', ++ 'component javascript', ++ 'chrome javascript', ++ 'chrome registration', ++ 'XBL', ++ 'XBL Prototype Handler', ++ 'XBL Content Sink', ++ 'xbl javascript', ++]); ++ ++class RuntimeAgent { ++ constructor(session) { ++ this._debugger = new Debugger(); ++ this._pendingPromises = new Map(); ++ this._session = session; ++ this._executionContexts = new Map(); ++ this._windowToExecutionContext = new Map(); ++ this._consoleServiceListener = { ++ QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]), ++ ++ observe: message => { ++ if (!(message instanceof Ci.nsIScriptError) || !message.outerWindowID || ++ !message.category || disallowedMessageCategories.has(message.category)) { ++ return; ++ } ++ const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID); ++ const executionContext = this._windowToExecutionContext.get(errorWindow); ++ if (!executionContext) ++ return; ++ const typeNames = { ++ [Ci.nsIConsoleMessage.debug]: 'debug', ++ [Ci.nsIConsoleMessage.info]: 'info', ++ [Ci.nsIConsoleMessage.warn]: 'warn', ++ [Ci.nsIConsoleMessage.error]: 'error', ++ }; ++ this._session.emitEvent('Runtime.console', { ++ args: [{ ++ value: message.message, ++ }], ++ type: typeNames[message.logLevel], ++ executionContextId: executionContext.id(), ++ location: { ++ lineNumber: message.lineNumber, ++ columnNumber: message.columnNumber, ++ url: message.sourceName, ++ }, ++ }); ++ }, ++ }; ++ ++ this._eventListeners = []; ++ this._enabled = false; ++ } ++ ++ _consoleAPICalled({wrappedJSObject}, topic, data) { ++ const type = consoleLevelToProtocolType[wrappedJSObject.level]; ++ if (!type) ++ return; ++ const executionContext = Array.from(this._executionContexts.values()).find(context => { ++ const domWindow = context._domWindow; ++ return domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID; ++ }); ++ if (!executionContext) ++ return; ++ const args = wrappedJSObject.arguments.map(arg => executionContext.rawValueToRemoteObject(arg)); ++ this._session.emitEvent('Runtime.console', { ++ args, ++ type, ++ executionContextId: executionContext.id(), ++ location: { ++ lineNumber: wrappedJSObject.lineNumber - 1, ++ columnNumber: wrappedJSObject.columnNumber - 1, ++ url: wrappedJSObject.filename, ++ }, ++ }); ++ } ++ ++ enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ for (const executionContext of this._executionContexts.values()) ++ this._notifyExecutionContextCreated(executionContext); ++ Services.console.registerListener(this._consoleServiceListener); ++ this._eventListeners = [ ++ () => Services.console.unregisterListener(this._consoleServiceListener), ++ helper.addObserver(this._consoleAPICalled.bind(this), "console-api-log-event"), ++ ]; ++ } ++ ++ _notifyExecutionContextCreated(executionContext) { ++ if (!this._enabled) ++ return; ++ this._session.emitEvent('Runtime.executionContextCreated', { ++ executionContextId: executionContext._id, ++ auxData: executionContext._auxData, ++ }); ++ } ++ ++ _notifyExecutionContextDestroyed(executionContext) { ++ if (!this._enabled) ++ return; ++ this._session.emitEvent('Runtime.executionContextDestroyed', { ++ executionContextId: executionContext._id, ++ }); ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ async _awaitPromise(executionContext, obj, exceptionDetails = {}) { ++ if (obj.promiseState === 'fulfilled') ++ return {success: true, obj: obj.promiseValue}; ++ if (obj.promiseState === 'rejected') { ++ const global = executionContext._global; ++ exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return; ++ exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return; ++ return {success: false, obj: null}; ++ } ++ let resolve, reject; ++ const promise = new Promise((a, b) => { ++ resolve = a; ++ reject = b; ++ }); ++ this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails}); ++ if (this._pendingPromises.size === 1) ++ this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this); ++ return await promise; ++ } ++ ++ _onPromiseSettled(obj) { ++ const pendingPromise = this._pendingPromises.get(obj.promiseID); ++ if (!pendingPromise) ++ return; ++ this._pendingPromises.delete(obj.promiseID); ++ if (!this._pendingPromises.size) ++ this._debugger.onPromiseSettled = undefined; ++ ++ if (obj.promiseState === 'fulfilled') { ++ pendingPromise.resolve({success: true, obj: obj.promiseValue}); ++ return; ++ }; ++ const global = pendingPromise.executionContext._global; ++ pendingPromise.exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return; ++ pendingPromise.exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return; ++ pendingPromise.resolve({success: false, obj: null}); ++ } ++ ++ createExecutionContext(domWindow, auxData) { ++ const context = new ExecutionContext(this, domWindow, this._debugger.addDebuggee(domWindow), auxData); ++ this._executionContexts.set(context._id, context); ++ this._windowToExecutionContext.set(domWindow, context); ++ this._notifyExecutionContextCreated(context); ++ return context; ++ } ++ ++ destroyExecutionContext(destroyedContext) { ++ for (const [promiseID, {reject, executionContext}] of this._pendingPromises) { ++ if (executionContext === destroyedContext) { ++ reject(new Error('Execution context was destroyed!')); ++ this._pendingPromises.delete(promiseID); ++ } ++ } ++ if (!this._pendingPromises.size) ++ this._debugger.onPromiseSettled = undefined; ++ this._debugger.removeDebuggee(destroyedContext._domWindow); ++ this._executionContexts.delete(destroyedContext._id); ++ this._windowToExecutionContext.delete(destroyedContext._domWindow); ++ this._notifyExecutionContextDestroyed(destroyedContext); ++ } ++ ++ async evaluate({executionContextId, expression, returnByValue}) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ const exceptionDetails = {}; ++ let result = await executionContext.evaluateScript(expression, exceptionDetails); ++ if (!result) ++ return {exceptionDetails}; ++ let isNode = undefined; ++ if (returnByValue) ++ result = executionContext.ensureSerializedToValue(result); ++ return {result}; ++ } ++ ++ async callFunction({executionContextId, functionDeclaration, args, returnByValue}) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ const exceptionDetails = {}; ++ let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails); ++ if (!result) ++ return {exceptionDetails}; ++ let isNode = undefined; ++ if (returnByValue) ++ result = executionContext.ensureSerializedToValue(result); ++ return {result}; ++ } ++ ++ async getObjectProperties({executionContextId, objectId}) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ return {properties: executionContext.getObjectProperties(objectId)}; ++ } ++ ++ async disposeObject({executionContextId, objectId}) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ return executionContext.disposeObject(objectId); ++ } ++} ++ ++class ExecutionContext { ++ constructor(runtime, domWindow, global, auxData) { ++ this._runtime = runtime; ++ this._domWindow = domWindow; ++ this._global = global; ++ this._remoteObjects = new Map(); ++ this._id = helper.generateId(); ++ this._auxData = auxData; ++ } ++ ++ id() { ++ return this._id; ++ } ++ ++ async evaluateScript(script, exceptionDetails = {}) { ++ const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true); ++ let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails); ++ userInputHelper.destruct(); ++ if (!success) ++ return null; ++ if (obj && obj.isPromise) { ++ const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails); ++ if (!awaitResult.success) ++ return null; ++ obj = awaitResult.obj; ++ } ++ return this._createRemoteObject(obj); ++ } ++ ++ async evaluateFunction(functionText, args, exceptionDetails = {}) { ++ const funEvaluation = this._getResult(this._global.executeInGlobal('(' + functionText + ')'), exceptionDetails); ++ if (!funEvaluation.success) ++ return null; ++ if (!funEvaluation.obj.callable) ++ throw new Error('functionText does not evaluate to a function!'); ++ args = args.map(arg => { ++ if (arg.objectId) { ++ if (!this._remoteObjects.has(arg.objectId)) ++ throw new Error('Cannot find object with id = ' + arg.objectId); ++ return this._remoteObjects.get(arg.objectId); ++ } ++ switch (arg.unserializableValue) { ++ case 'Infinity': return Infinity; ++ case '-Infinity': return -Infinity; ++ case '-0': return -0; ++ case 'NaN': return NaN; ++ default: return this._toDebugger(arg.value); ++ } ++ }); ++ const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true); ++ let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails); ++ userInputHelper.destruct(); ++ if (!success) ++ return null; ++ if (obj && obj.isPromise) { ++ const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails); ++ if (!awaitResult.success) ++ return null; ++ obj = awaitResult.obj; ++ } ++ return this._createRemoteObject(obj); ++ } ++ ++ unsafeObject(objectId) { ++ if (!this._remoteObjects.has(objectId)) ++ throw new Error('Cannot find object with id = ' + objectId); ++ return this._remoteObjects.get(objectId).unsafeDereference(); ++ } ++ ++ rawValueToRemoteObject(rawValue) { ++ const debuggerObj = this._global.makeDebuggeeValue(rawValue); ++ return this._createRemoteObject(debuggerObj); ++ } ++ ++ _createRemoteObject(debuggerObj) { ++ if (debuggerObj instanceof Debugger.Object) { ++ const objectId = helper.generateId(); ++ this._remoteObjects.set(objectId, debuggerObj); ++ const rawObj = debuggerObj.unsafeDereference(); ++ const type = typeof rawObj; ++ let subtype = undefined; ++ if (debuggerObj.isProxy) ++ subtype = 'proxy'; ++ else if (Array.isArray(rawObj)) ++ subtype = 'array'; ++ else if (Object.is(rawObj, null)) ++ subtype = 'null'; ++ else if (rawObj instanceof this._domWindow.Node) ++ subtype = 'node'; ++ else if (rawObj instanceof this._domWindow.RegExp) ++ subtype = 'regexp'; ++ else if (rawObj instanceof this._domWindow.Date) ++ subtype = 'date'; ++ else if (rawObj instanceof this._domWindow.Map) ++ subtype = 'map'; ++ else if (rawObj instanceof this._domWindow.Set) ++ subtype = 'set'; ++ else if (rawObj instanceof this._domWindow.WeakMap) ++ subtype = 'weakmap'; ++ else if (rawObj instanceof this._domWindow.WeakSet) ++ subtype = 'weakset'; ++ else if (rawObj instanceof this._domWindow.Error) ++ subtype = 'error'; ++ else if (rawObj instanceof this._domWindow.Promise) ++ subtype = 'promise'; ++ else if ((rawObj instanceof this._domWindow.Int8Array) || (rawObj instanceof this._domWindow.Uint8Array) || ++ (rawObj instanceof this._domWindow.Uint8ClampedArray) || (rawObj instanceof this._domWindow.Int16Array) || ++ (rawObj instanceof this._domWindow.Uint16Array) || (rawObj instanceof this._domWindow.Int32Array) || ++ (rawObj instanceof this._domWindow.Uint32Array) || (rawObj instanceof this._domWindow.Float32Array) || ++ (rawObj instanceof this._domWindow.Float64Array)) { ++ subtype = 'typedarray'; ++ } ++ const isNode = debuggerObj.unsafeDereference() instanceof this._domWindow.Node; ++ return {objectId, type, subtype}; ++ } ++ if (typeof debuggerObj === 'symbol') { ++ const objectId = helper.generateId(); ++ this._remoteObjects.set(objectId, debuggerObj); ++ return {objectId, type: 'symbol'}; ++ } ++ ++ let unserializableValue = undefined; ++ if (Object.is(debuggerObj, NaN)) ++ unserializableValue = 'NaN'; ++ else if (Object.is(debuggerObj, -0)) ++ unserializableValue = '-0'; ++ else if (Object.is(debuggerObj, Infinity)) ++ unserializableValue = 'Infinity'; ++ else if (Object.is(debuggerObj, -Infinity)) ++ unserializableValue = '-Infinity'; ++ return unserializableValue ? {unserializableValue} : {value: debuggerObj}; ++ } ++ ++ ensureSerializedToValue(protocolObject) { ++ if (!protocolObject.objectId) ++ return protocolObject; ++ const obj = this._remoteObjects.get(protocolObject.objectId); ++ this._remoteObjects.delete(protocolObject.objectId); ++ return {value: this._serialize(obj)}; ++ } ++ ++ _toDebugger(obj) { ++ if (typeof obj !== 'object') ++ return obj; ++ const properties = {}; ++ for (let [key, value] of Object.entries(obj)) { ++ properties[key] = { ++ writable: true, ++ enumerable: true, ++ value: this._toDebugger(value), ++ }; ++ } ++ const baseObject = Array.isArray(obj) ? '([])' : '({})'; ++ const debuggerObj = this._global.executeInGlobal(baseObject).return; ++ debuggerObj.defineProperties(properties); ++ return debuggerObj; ++ } ++ ++ _serialize(obj) { ++ const result = this._global.executeInGlobalWithBindings('JSON.stringify(e)', {e: obj}); ++ if (result.throw) ++ throw new Error('Object is not serializable'); ++ return JSON.parse(result.return); ++ } ++ ++ disposeObject(objectId) { ++ this._remoteObjects.delete(objectId); ++ } ++ ++ getObjectProperties(objectId) { ++ if (!this._remoteObjects.has(objectId)) ++ throw new Error('Cannot find object with id = ' + arg.objectId); ++ const result = []; ++ for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) { ++ for (const propertyName of obj.getOwnPropertyNames()) { ++ const descriptor = obj.getOwnPropertyDescriptor(propertyName); ++ if (!descriptor.enumerable) ++ continue; ++ result.push({ ++ name: propertyName, ++ value: this._createRemoteObject(descriptor.value), ++ }); ++ } ++ } ++ return result; ++ } ++ ++ _getResult(completionValue, exceptionDetails = {}) { ++ if (!completionValue) { ++ exceptionDetails.text = 'Evaluation terminated!'; ++ exceptionDetails.stack = ''; ++ return {success: false, obj: null}; ++ } ++ if (completionValue.throw) { ++ if (this._global.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) { ++ exceptionDetails.text = this._global.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return; ++ exceptionDetails.stack = this._global.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return; ++ } else { ++ exceptionDetails.value = this._serialize(completionValue.throw); ++ } ++ return {success: false, obj: null}; ++ } ++ return {success: true, obj: completionValue.return}; ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['RuntimeAgent']; ++this.RuntimeAgent = RuntimeAgent; +diff --git a/testing/juggler/content/ScrollbarManager.js b/testing/juggler/content/ScrollbarManager.js +new file mode 100644 +index 000000000000..caee4df323d0 +--- /dev/null ++++ b/testing/juggler/content/ScrollbarManager.js +@@ -0,0 +1,85 @@ ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++const Cc = Components.classes; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++const HIDDEN_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/hidden-scrollbars.css'); ++const FLOATING_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/floating-scrollbars.css'); ++ ++const isHeadless = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless; ++const helper = new Helper(); ++ ++class ScrollbarManager { ++ constructor(docShell) { ++ this._docShell = docShell; ++ this._customScrollbars = null; ++ this._contentViewerScrollBars = new Map(); ++ ++ if (isHeadless) ++ this._setCustomScrollbars(HIDDEN_SCROLLBARS); ++ ++ const webProgress = this._docShell.QueryInterface(Ci.nsIInterfaceRequestor) ++ .getInterface(Ci.nsIWebProgress); ++ ++ this.QueryInterface = ChromeUtils.generateQI(['nsIWebProgressListener', 'nsISupportsWeakReference']); ++ this._eventListeners = [ ++ helper.addProgressListener(webProgress, this, Ci.nsIWebProgress.NOTIFY_ALL), ++ ]; ++ } ++ ++ onLocationChange(webProgress, request, URI, flags) { ++ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ++ return; ++ this._updateAllDocShells(); ++ } ++ ++ setFloatingScrollbars(enabled) { ++ if (this._customScrollbars === HIDDEN_SCROLLBARS) ++ return; ++ this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null); ++ } ++ ++ _setCustomScrollbars(customScrollbars) { ++ if (this._customScrollbars === customScrollbars) ++ return; ++ this._customScrollbars = customScrollbars; ++ this._updateAllDocShells(); ++ } ++ ++ _updateAllDocShells() { ++ const allDocShells = [this._docShell]; ++ for (let i = 0; i < this._docShell.childCount; i++) ++ allDocShells.push(this._docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell)); ++ // At this point, a content viewer might not be loaded for certain docShells. ++ // Scrollbars will be updated in onLocationChange. ++ const contentViewers = allDocShells.map(docShell => docShell.contentViewer).filter(contentViewer => !!contentViewer); ++ ++ // Update scrollbar stylesheets. ++ for (const contentViewer of contentViewers) { ++ const oldScrollbars = this._contentViewerScrollBars.get(contentViewer); ++ if (oldScrollbars === this._customScrollbars) ++ continue; ++ const winUtils = contentViewer.DOMDocument.defaultView.windowUtils; ++ if (oldScrollbars) ++ winUtils.removeSheet(oldScrollbars, winUtils.AGENT_SHEET); ++ if (this._customScrollbars) ++ winUtils.loadSheet(this._customScrollbars, winUtils.AGENT_SHEET); ++ } ++ // Update state for all *existing* docShells. ++ this._contentViewerScrollBars.clear(); ++ for (const contentViewer of contentViewers) ++ this._contentViewerScrollBars.set(contentViewer, this._customScrollbars); ++ } ++ ++ dispose() { ++ this._setCustomScrollbars(null); ++ helper.removeListeners(this._eventListeners); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['ScrollbarManager']; ++this.ScrollbarManager = ScrollbarManager; ++ +diff --git a/testing/juggler/content/floating-scrollbars.css b/testing/juggler/content/floating-scrollbars.css +new file mode 100644 +index 000000000000..7709bdd34c65 +--- /dev/null ++++ b/testing/juggler/content/floating-scrollbars.css +@@ -0,0 +1,47 @@ ++@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); ++@namespace html url("http://www.w3.org/1999/xhtml"); ++ ++/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars ++ inside a . */ ++*|*:not(html|select) > scrollbar { ++ -moz-appearance: none !important; ++ position: relative; ++ background-color: transparent; ++ background-image: none; ++ z-index: 2147483647; ++ padding: 2px; ++ border: none; ++} ++ ++/* Scrollbar code will reset the margin to the correct side depending on ++ where layout actually puts the scrollbar */ ++*|*:not(html|select) > scrollbar[orient="vertical"] { ++ margin-left: -10px; ++ min-width: 10px; ++ max-width: 10px; ++} ++ ++*|*:not(html|select) > scrollbar[orient="horizontal"] { ++ margin-top: -10px; ++ min-height: 10px; ++ max-height: 10px; ++} ++ ++*|*:not(html|select) > scrollbar slider { ++ -moz-appearance: none !important; ++} ++ ++*|*:not(html|select) > scrollbar thumb { ++ -moz-appearance: none !important; ++ background-color: rgba(0,0,0,0.2); ++ border-width: 0px !important; ++ border-radius: 3px !important; ++} ++ ++*|*:not(html|select) > scrollbar scrollbarbutton, ++*|*:not(html|select) > scrollbar gripper { ++ display: none; ++} +diff --git a/testing/juggler/content/hidden-scrollbars.css b/testing/juggler/content/hidden-scrollbars.css +new file mode 100644 +index 000000000000..3a386425d379 +--- /dev/null ++++ b/testing/juggler/content/hidden-scrollbars.css +@@ -0,0 +1,13 @@ ++@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); ++@namespace html url("http://www.w3.org/1999/xhtml"); ++ ++/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars ++ inside a . */ ++*|*:not(html|select) > scrollbar { ++ -moz-appearance: none !important; ++ display: none; ++} ++ +diff --git a/testing/juggler/content/main.js b/testing/juggler/content/main.js +new file mode 100644 +index 000000000000..8585092e04e7 +--- /dev/null ++++ b/testing/juggler/content/main.js +@@ -0,0 +1,39 @@ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {ContentSession} = ChromeUtils.import('chrome://juggler/content/content/ContentSession.js'); ++const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js'); ++const {NetworkMonitor} = ChromeUtils.import('chrome://juggler/content/content/NetworkMonitor.js'); ++const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js'); ++ ++const sessions = new Map(); ++const frameTree = new FrameTree(docShell); ++const networkMonitor = new NetworkMonitor(docShell, frameTree); ++const scrollbarManager = new ScrollbarManager(docShell); ++ ++const helper = new Helper(); ++ ++const gListeners = [ ++ helper.addMessageListener(this, 'juggler:create-content-session', msg => { ++ const sessionId = msg.data; ++ sessions.set(sessionId, new ContentSession(sessionId, this, frameTree, scrollbarManager, networkMonitor)); ++ }), ++ ++ helper.addMessageListener(this, 'juggler:dispose-content-session', msg => { ++ const sessionId = msg.data; ++ const session = sessions.get(sessionId); ++ if (!session) ++ return; ++ sessions.delete(sessionId); ++ session.dispose(); ++ }), ++ ++ helper.addEventListener(this, 'unload', msg => { ++ helper.removeListeners(gListeners); ++ for (const session of sessions.values()) ++ session.dispose(); ++ sessions.clear(); ++ scrollbarManager.dispose(); ++ networkMonitor.dispose(); ++ frameTree.dispose(); ++ }), ++]; ++ +diff --git a/testing/juggler/jar.mn b/testing/juggler/jar.mn +new file mode 100644 +index 000000000000..27f5a15fd7f1 +--- /dev/null ++++ b/testing/juggler/jar.mn +@@ -0,0 +1,29 @@ ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, You can obtain one at http://mozilla.org/MPL/2.0/. ++ ++juggler.jar: ++% content juggler %content/ ++ content/Helper.js (Helper.js) ++ content/NetworkObserver.js (NetworkObserver.js) ++ content/BrowserContextManager.js (BrowserContextManager.js) ++ content/TargetRegistry.js (TargetRegistry.js) ++ content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js) ++ content/protocol/Protocol.js (protocol/Protocol.js) ++ content/protocol/Dispatcher.js (protocol/Dispatcher.js) ++ content/protocol/PageHandler.js (protocol/PageHandler.js) ++ content/protocol/RuntimeHandler.js (protocol/RuntimeHandler.js) ++ content/protocol/NetworkHandler.js (protocol/NetworkHandler.js) ++ content/protocol/BrowserHandler.js (protocol/BrowserHandler.js) ++ content/protocol/TargetHandler.js (protocol/TargetHandler.js) ++ content/protocol/AccessibilityHandler.js (protocol/AccessibilityHandler.js) ++ content/content/main.js (content/main.js) ++ content/content/ContentSession.js (content/ContentSession.js) ++ content/content/FrameTree.js (content/FrameTree.js) ++ content/content/NetworkMonitor.js (content/NetworkMonitor.js) ++ content/content/PageAgent.js (content/PageAgent.js) ++ content/content/RuntimeAgent.js (content/RuntimeAgent.js) ++ content/content/ScrollbarManager.js (content/ScrollbarManager.js) ++ content/content/floating-scrollbars.css (content/floating-scrollbars.css) ++ content/content/hidden-scrollbars.css (content/hidden-scrollbars.css) ++ +diff --git a/testing/juggler/moz.build b/testing/juggler/moz.build +new file mode 100644 +index 000000000000..1a0a3130bf95 +--- /dev/null ++++ b/testing/juggler/moz.build +@@ -0,0 +1,15 @@ ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, You can obtain one at http://mozilla.org/MPL/2.0/. ++ ++DIRS += ["components"] ++ ++JAR_MANIFESTS += ["jar.mn"] ++#JS_PREFERENCE_FILES += ["prefs/marionette.js"] ++ ++#MARIONETTE_UNIT_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.ini"] ++#XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] ++ ++with Files("**"): ++ BUG_COMPONENT = ("Testing", "Juggler") ++ +diff --git a/testing/juggler/protocol/AccessibilityHandler.js b/testing/juggler/protocol/AccessibilityHandler.js +new file mode 100644 +index 000000000000..fc8a7397e50a +--- /dev/null ++++ b/testing/juggler/protocol/AccessibilityHandler.js +@@ -0,0 +1,15 @@ ++class AccessibilityHandler { ++ constructor(chromeSession, contentSession) { ++ this._chromeSession = chromeSession; ++ this._contentSession = contentSession; ++ } ++ ++ async getFullAXTree() { ++ return await this._contentSession.send('Page.getFullAXTree'); ++ } ++ ++ dispose() { } ++} ++ ++var EXPORTED_SYMBOLS = ['AccessibilityHandler']; ++this.AccessibilityHandler = AccessibilityHandler; +diff --git a/testing/juggler/protocol/BrowserHandler.js b/testing/juggler/protocol/BrowserHandler.js +new file mode 100644 +index 000000000000..e16d1c5c5798 +--- /dev/null ++++ b/testing/juggler/protocol/BrowserHandler.js +@@ -0,0 +1,66 @@ ++"use strict"; ++ ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const { allowAllCerts } = ChromeUtils.import( ++ "chrome://marionette/content/cert.js" ++); ++const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js"); ++ ++class BrowserHandler { ++ /** ++ * @param {ChromeSession} session ++ */ ++ constructor() { ++ this._sweepingOverride = null; ++ this._contextManager = BrowserContextManager.instance(); ++ } ++ ++ async close() { ++ Services.startup.quit(Ci.nsIAppStartup.eForceQuit); ++ } ++ ++ async setIgnoreHTTPSErrors({enabled}) { ++ if (!enabled) { ++ allowAllCerts.disable() ++ Services.prefs.setBoolPref('security.mixed_content.block_active_content', true); ++ } else { ++ allowAllCerts.enable() ++ Services.prefs.setBoolPref('security.mixed_content.block_active_content', false); ++ } ++ } ++ ++ grantPermissions({browserContextId, origin, permissions}) { ++ this._contextManager.grantPermissions(browserContextId, origin, permissions); ++ } ++ ++ resetPermissions({browserContextId}) { ++ this._contextManager.resetPermissions(browserContextId); ++ } ++ ++ setCookies({browserContextId, cookies}) { ++ this._contextManager.setCookies(browserContextId, cookies); ++ } ++ ++ deleteCookies({browserContextId, cookies}) { ++ this._contextManager.deleteCookies(browserContextId, cookies); ++ } ++ ++ getCookies({browserContextId, urls}) { ++ return {cookies: this._contextManager.getCookies(browserContextId, urls)}; ++ } ++ ++ async getInfo() { ++ const version = Components.classes["@mozilla.org/xre/app-info;1"] ++ .getService(Components.interfaces.nsIXULAppInfo) ++ .version; ++ const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"] ++ .getService(Components.interfaces.nsIHttpProtocolHandler) ++ .userAgent; ++ return {version: 'Firefox/' + version, userAgent}; ++ } ++ ++ dispose() { } ++} ++ ++var EXPORTED_SYMBOLS = ['BrowserHandler']; ++this.BrowserHandler = BrowserHandler; +diff --git a/testing/juggler/protocol/Dispatcher.js b/testing/juggler/protocol/Dispatcher.js +new file mode 100644 +index 000000000000..7b3a6fa4fe7a +--- /dev/null ++++ b/testing/juggler/protocol/Dispatcher.js +@@ -0,0 +1,255 @@ ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js"); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const helper = new Helper(); ++ ++const PROTOCOL_HANDLERS = { ++ Page: ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js").PageHandler, ++ Network: ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js").NetworkHandler, ++ Browser: ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js").BrowserHandler, ++ Target: ChromeUtils.import("chrome://juggler/content/protocol/TargetHandler.js").TargetHandler, ++ Runtime: ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js").RuntimeHandler, ++ Accessibility: ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js").AccessibilityHandler, ++}; ++ ++class Dispatcher { ++ /** ++ * @param {Connection} connection ++ */ ++ constructor(connection) { ++ this._connection = connection; ++ this._connection.onmessage = this._dispatch.bind(this); ++ this._connection.onclose = this._dispose.bind(this); ++ ++ this._targetSessions = new Map(); ++ this._sessions = new Map(); ++ this._rootSession = new ChromeSession(this, undefined, null /* contentSession */, TargetRegistry.instance().browserTargetInfo()); ++ ++ this._eventListeners = [ ++ helper.on(TargetRegistry.instance(), TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)), ++ ]; ++ } ++ ++ async createSession(targetId) { ++ const targetInfo = TargetRegistry.instance().targetInfo(targetId); ++ if (!targetInfo) ++ throw new Error(`Target "${targetId}" is not found`); ++ let targetSessions = this._targetSessions.get(targetId); ++ if (!targetSessions) { ++ targetSessions = new Map(); ++ this._targetSessions.set(targetId, targetSessions); ++ } ++ ++ const sessionId = helper.generateId(); ++ const contentSession = targetInfo.type === 'page' ? new ContentSession(this, sessionId, targetInfo) : null; ++ const chromeSession = new ChromeSession(this, sessionId, contentSession, targetInfo); ++ targetSessions.set(sessionId, chromeSession); ++ this._sessions.set(sessionId, chromeSession); ++ this._emitEvent(this._rootSession._sessionId, 'Target.attachedToTarget', { ++ sessionId: sessionId, ++ targetInfo ++ }); ++ return sessionId; ++ } ++ ++ _dispose() { ++ helper.removeListeners(this._eventListeners); ++ this._connection.onmessage = null; ++ this._connection.onclose = null; ++ this._rootSession.dispose(); ++ this._rootSession = null; ++ for (const session of this._sessions.values()) ++ session.dispose(); ++ this._sessions.clear(); ++ this._targetSessions.clear(); ++ } ++ ++ _onTargetDestroyed({targetId}) { ++ const sessions = this._targetSessions.get(targetId); ++ if (!sessions) ++ return; ++ this._targetSessions.delete(targetId); ++ for (const [sessionId, session] of sessions) { ++ session.dispose(); ++ this._sessions.delete(sessionId); ++ } ++ } ++ ++ async _dispatch(event) { ++ const data = JSON.parse(event.data); ++ const id = data.id; ++ const sessionId = data.sessionId; ++ delete data.sessionId; ++ try { ++ const session = sessionId ? this._sessions.get(sessionId) : this._rootSession; ++ if (!session) ++ throw new Error(`ERROR: cannot find session with id "${sessionId}"`); ++ const method = data.method; ++ const params = data.params || {}; ++ if (!id) ++ throw new Error(`ERROR: every message must have an 'id' parameter`); ++ if (!method) ++ throw new Error(`ERROR: every message must have a 'method' parameter`); ++ ++ const [domain, methodName] = method.split('.'); ++ const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null; ++ if (!descriptor) ++ throw new Error(`ERROR: method '${method}' is not supported`); ++ let details = {}; ++ if (!checkScheme(descriptor.params || {}, params, details)) ++ throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`); ++ ++ const result = await session.dispatch(method, params); ++ ++ details = {}; ++ if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details)) ++ throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`); ++ ++ this._connection.send(JSON.stringify({id, sessionId, result})); ++ } catch (e) { ++ this._connection.send(JSON.stringify({id, sessionId, error: { ++ message: e.message, ++ data: e.stack ++ }})); ++ } ++ } ++ ++ _emitEvent(sessionId, eventName, params) { ++ const [domain, eName] = eventName.split('.'); ++ const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null; ++ if (!scheme) ++ throw new Error(`ERROR: event '${eventName}' is not supported`); ++ const details = {}; ++ if (!checkScheme(scheme, params || {}, details)) ++ throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`); ++ this._connection.send(JSON.stringify({method: eventName, params, sessionId})); ++ } ++} ++ ++class ChromeSession { ++ /** ++ * @param {Connection} connection ++ */ ++ constructor(dispatcher, sessionId, contentSession, targetInfo) { ++ this._dispatcher = dispatcher; ++ this._sessionId = sessionId; ++ this._contentSession = contentSession; ++ this._targetInfo = targetInfo; ++ ++ this._handlers = {}; ++ for (const [domainName, handlerFactory] of Object.entries(PROTOCOL_HANDLERS)) { ++ if (protocol.domains[domainName].targets.includes(targetInfo.type)) ++ this._handlers[domainName] = new handlerFactory(this, contentSession); ++ } ++ } ++ ++ dispatcher() { ++ return this._dispatcher; ++ } ++ ++ targetId() { ++ return this._targetInfo.targetId; ++ } ++ ++ dispose() { ++ if (this._contentSession) ++ this._contentSession.dispose(); ++ this._contentSession = null; ++ for (const [domainName, handler] of Object.entries(this._handlers)) { ++ if (!handler.dispose) ++ throw new Error(`Handler for "${domainName}" domain does not define |dispose| method!`); ++ handler.dispose(); ++ delete this._handlers[domainName]; ++ } ++ // Root session don't have sessionId and don't emit detachedFromTarget. ++ if (this._sessionId) { ++ this._dispatcher._emitEvent(this._sessionId, 'Target.detachedFromTarget', { ++ sessionId: this._sessionId, ++ }); ++ } ++ } ++ ++ emitEvent(eventName, params) { ++ this._dispatcher._emitEvent(this._sessionId, eventName, params); ++ } ++ ++ async dispatch(method, params) { ++ const [domainName, methodName] = method.split('.'); ++ if (!this._handlers[domainName]) ++ throw new Error(`Domain "${domainName}" does not exist`); ++ if (!this._handlers[domainName][methodName]) ++ throw new Error(`Handler for domain "${domainName}" does not implement method "${methodName}"`); ++ return await this._handlers[domainName][methodName](params); ++ } ++} ++ ++class ContentSession { ++ constructor(dispatcher, sessionId, targetInfo) { ++ this._dispatcher = dispatcher; ++ const tab = TargetRegistry.instance().tabForTarget(targetInfo.targetId); ++ this._browser = tab.linkedBrowser; ++ this._messageId = 0; ++ this._pendingMessages = new Map(); ++ this._sessionId = sessionId; ++ this._browser.messageManager.sendAsyncMessage('juggler:create-content-session', this._sessionId); ++ this._disposed = false; ++ this._eventListeners = [ ++ helper.addMessageListener(this._browser.messageManager, this._sessionId, { ++ receiveMessage: message => this._onMessage(message) ++ }), ++ ]; ++ } ++ ++ isDisposed() { ++ return this._disposed; ++ } ++ ++ dispose() { ++ if (this._disposed) ++ return; ++ this._disposed = true; ++ helper.removeListeners(this._eventListeners); ++ for (const {resolve, reject, methodName} of this._pendingMessages.values()) ++ reject(new Error(`Failed "${methodName}": Page closed.`)); ++ this._pendingMessages.clear(); ++ if (this._browser.messageManager) ++ this._browser.messageManager.sendAsyncMessage('juggler:dispose-content-session', this._sessionId); ++ } ++ ++ /** ++ * @param {string} methodName ++ * @param {*} params ++ * @return {!Promise<*>} ++ */ ++ send(methodName, params) { ++ const id = ++this._messageId; ++ const promise = new Promise((resolve, reject) => { ++ this._pendingMessages.set(id, {resolve, reject, methodName}); ++ }); ++ this._browser.messageManager.sendAsyncMessage(this._sessionId, {id, methodName, params}); ++ return promise; ++ } ++ ++ _onMessage({data}) { ++ if (data.id) { ++ let id = data.id; ++ const {resolve, reject} = this._pendingMessages.get(data.id); ++ this._pendingMessages.delete(data.id); ++ if (data.error) ++ reject(new Error(data.error)); ++ else ++ resolve(data.result); ++ } else { ++ const { ++ eventName, ++ params = {} ++ } = data; ++ this._dispatcher._emitEvent(this._sessionId, eventName, params); ++ } ++ } ++} ++ ++ ++this.EXPORTED_SYMBOLS = ['Dispatcher']; ++this.Dispatcher = Dispatcher; ++ +diff --git a/testing/juggler/protocol/NetworkHandler.js b/testing/juggler/protocol/NetworkHandler.js +new file mode 100644 +index 000000000000..f5e7e919594b +--- /dev/null ++++ b/testing/juggler/protocol/NetworkHandler.js +@@ -0,0 +1,154 @@ ++"use strict"; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {NetworkObserver} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js'); ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; ++const FRAME_SCRIPT = "chrome://juggler/content/content/ContentSession.js"; ++const helper = new Helper(); ++ ++class NetworkHandler { ++ constructor(chromeSession, contentSession) { ++ this._chromeSession = chromeSession; ++ this._contentSession = contentSession; ++ this._networkObserver = NetworkObserver.instance(); ++ this._httpActivity = new Map(); ++ this._enabled = false; ++ this._browser = TargetRegistry.instance().tabForTarget(this._chromeSession.targetId()).linkedBrowser; ++ this._requestInterception = false; ++ this._eventListeners = []; ++ this._pendingRequstWillBeSentEvents = new Set(); ++ } ++ ++ async enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ this._eventListeners = [ ++ helper.on(this._networkObserver, 'request', this._onRequest.bind(this)), ++ helper.on(this._networkObserver, 'response', this._onResponse.bind(this)), ++ helper.on(this._networkObserver, 'requestfinished', this._onRequestFinished.bind(this)), ++ helper.on(this._networkObserver, 'requestfailed', this._onRequestFailed.bind(this)), ++ this._networkObserver.startTrackingBrowserNetwork(this._browser), ++ ]; ++ } ++ ++ async getResponseBody({requestId}) { ++ return this._networkObserver.getResponseBody(this._browser, requestId); ++ } ++ ++ async setExtraHTTPHeaders({headers}) { ++ this._networkObserver.setExtraHTTPHeaders(this._browser, headers); ++ } ++ ++ async setRequestInterception({enabled}) { ++ if (enabled) ++ this._networkObserver.enableRequestInterception(this._browser); ++ else ++ this._networkObserver.disableRequestInterception(this._browser); ++ // Right after we enable/disable request interception we need to await all pending ++ // requestWillBeSent events before successfully returning from the method. ++ await Promise.all(Array.from(this._pendingRequstWillBeSentEvents)); ++ } ++ ++ async resumeSuspendedRequest({requestId, headers}) { ++ this._networkObserver.resumeSuspendedRequest(this._browser, requestId, headers); ++ } ++ ++ async abortSuspendedRequest({requestId}) { ++ this._networkObserver.abortSuspendedRequest(this._browser, requestId); ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ _ensureHTTPActivity(requestId) { ++ let activity = this._httpActivity.get(requestId); ++ if (!activity) { ++ activity = { ++ _id: requestId, ++ _lastSentEvent: null, ++ request: null, ++ response: null, ++ complete: null, ++ failed: null, ++ }; ++ this._httpActivity.set(requestId, activity); ++ } ++ return activity; ++ } ++ ++ _reportHTTPAcitivityEvents(activity) { ++ // State machine - sending network events. ++ if (!activity._lastSentEvent && activity.request) { ++ this._chromeSession.emitEvent('Network.requestWillBeSent', activity.request); ++ activity._lastSentEvent = 'requestWillBeSent'; ++ } ++ if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) { ++ this._chromeSession.emitEvent('Network.responseReceived', activity.response); ++ activity._lastSentEvent = 'responseReceived'; ++ } ++ if (activity._lastSentEvent === 'responseReceived' && activity.complete) { ++ this._chromeSession.emitEvent('Network.requestFinished', activity.complete); ++ activity._lastSentEvent = 'requestFinished'; ++ } ++ if (activity._lastSentEvent && activity.failed) { ++ this._chromeSession.emitEvent('Network.requestFailed', activity.failed); ++ activity._lastSentEvent = 'requestFailed'; ++ } ++ ++ // Clean up if request lifecycle is over. ++ if (activity._lastSentEvent === 'requestFinished' || activity._lastSentEvent === 'requestFailed') ++ this._httpActivity.delete(activity._id); ++ } ++ ++ async _onRequest(httpChannel, eventDetails) { ++ let pendingRequestCallback; ++ let pendingRequestPromise = new Promise(x => pendingRequestCallback = x); ++ this._pendingRequstWillBeSentEvents.add(pendingRequestPromise); ++ let details = null; ++ try { ++ details = await this._contentSession.send('Page.requestDetails', {channelId: httpChannel.channelId}); ++ } catch (e) { ++ if (this._contentSession.isDisposed()) { ++ pendingRequestCallback(); ++ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise); ++ return; ++ } ++ } ++ const activity = this._ensureHTTPActivity(eventDetails.requestId); ++ activity.request = { ++ frameId: details ? details.frameId : undefined, ++ ...eventDetails, ++ }; ++ this._reportHTTPAcitivityEvents(activity); ++ pendingRequestCallback(); ++ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise); ++ } ++ ++ async _onResponse(httpChannel, eventDetails) { ++ const activity = this._ensureHTTPActivity(eventDetails.requestId); ++ activity.response = eventDetails; ++ this._reportHTTPAcitivityEvents(activity); ++ } ++ ++ async _onRequestFinished(httpChannel, eventDetails) { ++ const activity = this._ensureHTTPActivity(eventDetails.requestId); ++ activity.complete = eventDetails; ++ this._reportHTTPAcitivityEvents(activity); ++ } ++ ++ async _onRequestFailed(httpChannel, eventDetails) { ++ const activity = this._ensureHTTPActivity(eventDetails.requestId); ++ activity.failed = eventDetails; ++ this._reportHTTPAcitivityEvents(activity); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['NetworkHandler']; ++this.NetworkHandler = NetworkHandler; +diff --git a/testing/juggler/protocol/PageHandler.js b/testing/juggler/protocol/PageHandler.js +new file mode 100644 +index 000000000000..32fb7e9d928a +--- /dev/null ++++ b/testing/juggler/protocol/PageHandler.js +@@ -0,0 +1,269 @@ ++"use strict"; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; ++const FRAME_SCRIPT = "chrome://juggler/content/content/ContentSession.js"; ++const helper = new Helper(); ++ ++class PageHandler { ++ constructor(chromeSession, contentSession) { ++ this._chromeSession = chromeSession; ++ this._contentSession = contentSession; ++ this._browser = TargetRegistry.instance().tabForTarget(chromeSession.targetId()).linkedBrowser; ++ this._dialogs = new Map(); ++ ++ this._eventListeners = []; ++ this._enabled = false; ++ } ++ ++ async close({runBeforeUnload}) { ++ // Postpone target close to deliver response in session. ++ Services.tm.dispatchToMainThread(() => { ++ TargetRegistry.instance().closePage(this._chromeSession.targetId(), runBeforeUnload); ++ }); ++ } ++ ++ async enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ this._updateModalDialogs(); ++ ++ this._eventListeners = [ ++ helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => { ++ // wait for the dialog to be actually added to DOM. ++ await Promise.resolve(); ++ this._updateModalDialogs(); ++ }), ++ helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), ++ ]; ++ await this._contentSession.send('Page.enable'); ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ async setViewport({viewport}) { ++ if (viewport) { ++ const {width, height} = viewport; ++ this._browser.style.setProperty('min-width', width + 'px'); ++ this._browser.style.setProperty('min-height', height + 'px'); ++ this._browser.style.setProperty('max-width', width + 'px'); ++ this._browser.style.setProperty('max-height', height + 'px'); ++ } else { ++ this._browser.style.removeProperty('min-width'); ++ this._browser.style.removeProperty('min-height'); ++ this._browser.style.removeProperty('max-width'); ++ this._browser.style.removeProperty('max-height'); ++ } ++ const dimensions = this._browser.getBoundingClientRect(); ++ await Promise.all([ ++ this._contentSession.send('Page.setViewport', { ++ deviceScaleFactor: viewport ? viewport.deviceScaleFactor : 0, ++ isMobile: viewport && viewport.isMobile, ++ hasTouch: viewport && viewport.hasTouch, ++ }), ++ this._contentSession.send('Page.awaitViewportDimensions', { ++ width: dimensions.width, ++ height: dimensions.height ++ }), ++ ]); ++ } ++ ++ _updateModalDialogs() { ++ const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []); ++ for (const dialog of this._dialogs.values()) { ++ if (!prompts.has(dialog.prompt())) { ++ this._dialogs.delete(dialog.id()); ++ this._chromeSession.emitEvent('Page.dialogClosed', { ++ dialogId: dialog.id(), ++ }); ++ } else { ++ prompts.delete(dialog.prompt()); ++ } ++ } ++ for (const prompt of prompts) { ++ const dialog = Dialog.createIfSupported(prompt); ++ if (!dialog) ++ continue; ++ this._dialogs.set(dialog.id(), dialog); ++ this._chromeSession.emitEvent('Page.dialogOpened', { ++ dialogId: dialog.id(), ++ type: dialog.type(), ++ message: dialog.message(), ++ defaultValue: dialog.defaultValue(), ++ }); ++ } ++ } ++ ++ async setUserAgent(options) { ++ return await this._contentSession.send('Page.setUserAgent', options); ++ } ++ ++ async setFileInputFiles(options) { ++ return await this._contentSession.send('Page.setFileInputFiles', options); ++ } ++ ++ async setEmulatedMedia(options) { ++ return await this._contentSession.send('Page.setEmulatedMedia', options); ++ } ++ ++ async setJavascriptEnabled(options) { ++ return await this._contentSession.send('Page.setJavascriptEnabled', options); ++ } ++ ++ async setCacheDisabled(options) { ++ return await this._contentSession.send('Page.setCacheDisabled', options); ++ } ++ ++ async addBinding(options) { ++ return await this._contentSession.send('Page.addBinding', options); ++ } ++ ++ async screenshot(options) { ++ return await this._contentSession.send('Page.screenshot', options); ++ } ++ ++ async getBoundingBox(options) { ++ return await this._contentSession.send('Page.getBoundingBox', options); ++ } ++ ++ async getContentQuads(options) { ++ return await this._contentSession.send('Page.getContentQuads', options); ++ } ++ ++ /** ++ * @param {{frameId: string, url: string}} options ++ */ ++ async navigate(options) { ++ return await this._contentSession.send('Page.navigate', options); ++ } ++ ++ /** ++ * @param {{frameId: string, url: string}} options ++ */ ++ async goBack(options) { ++ return await this._contentSession.send('Page.goBack', options); ++ } ++ ++ /** ++ * @param {{frameId: string, url: string}} options ++ */ ++ async goForward(options) { ++ return await this._contentSession.send('Page.goForward', options); ++ } ++ ++ /** ++ * @param {{frameId: string, url: string}} options ++ */ ++ async reload(options) { ++ return await this._contentSession.send('Page.reload', options); ++ } ++ ++ /** ++ * @param {{frameId: String, objectId: String}} options ++ * @return {!Promise<*>} ++ */ ++ async contentFrame(options) { ++ return await this._contentSession.send('Page.contentFrame', options); ++ } ++ ++ async addScriptToEvaluateOnNewDocument(options) { ++ return await this._contentSession.send('Page.addScriptToEvaluateOnNewDocument', options); ++ } ++ ++ async removeScriptToEvaluateOnNewDocument(options) { ++ return await this._contentSession.send('Page.removeScriptToEvaluateOnNewDocument', options); ++ } ++ ++ async dispatchKeyEvent(options) { ++ return await this._contentSession.send('Page.dispatchKeyEvent', options); ++ } ++ ++ async dispatchTouchEvent(options) { ++ return await this._contentSession.send('Page.dispatchTouchEvent', options); ++ } ++ ++ async dispatchMouseEvent(options) { ++ return await this._contentSession.send('Page.dispatchMouseEvent', options); ++ } ++ ++ async insertText(options) { ++ return await this._contentSession.send('Page.insertText', options); ++ } ++ ++ async handleDialog({dialogId, accept, promptText}) { ++ const dialog = this._dialogs.get(dialogId); ++ if (!dialog) ++ throw new Error('Failed to find dialog with id = ' + dialogId); ++ if (accept) ++ dialog.accept(promptText); ++ else ++ dialog.dismiss(); ++ } ++} ++ ++class Dialog { ++ static createIfSupported(prompt) { ++ const type = prompt.args.promptType; ++ switch (type) { ++ case 'alert': ++ case 'prompt': ++ case 'confirm': ++ return new Dialog(prompt, type); ++ case 'confirmEx': ++ return new Dialog(prompt, 'beforeunload'); ++ default: ++ return null; ++ }; ++ } ++ ++ constructor(prompt, type) { ++ this._id = helper.generateId(); ++ this._type = type; ++ this._prompt = prompt; ++ } ++ ++ id() { ++ return this._id; ++ } ++ ++ message() { ++ return this._prompt.ui.infoBody.textContent; ++ } ++ ++ type() { ++ return this._type; ++ } ++ ++ prompt() { ++ return this._prompt; ++ } ++ ++ dismiss() { ++ if (this._prompt.ui.button1) ++ this._prompt.ui.button1.click(); ++ else ++ this._prompt.ui.button0.click(); ++ } ++ ++ defaultValue() { ++ return this._prompt.ui.loginTextbox.value; ++ } ++ ++ accept(promptValue) { ++ if (typeof promptValue === 'string' && this._type === 'prompt') ++ this._prompt.ui.loginTextbox.value = promptValue; ++ this._prompt.ui.button0.click(); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['PageHandler']; ++this.PageHandler = PageHandler; +diff --git a/testing/juggler/protocol/PrimitiveTypes.js b/testing/juggler/protocol/PrimitiveTypes.js +new file mode 100644 +index 000000000000..78b6601b91d0 +--- /dev/null ++++ b/testing/juggler/protocol/PrimitiveTypes.js +@@ -0,0 +1,143 @@ ++const t = {}; ++ ++t.String = function(x, details = {}, path = ['']) { ++ if (typeof x === 'string' || typeof x === 'String') ++ return true; ++ details.error = `Expected "${path.join('.')}" to be |string|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Number = function(x, details = {}, path = ['']) { ++ if (typeof x === 'number') ++ return true; ++ details.error = `Expected "${path.join('.')}" to be |number|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Boolean = function(x, details = {}, path = ['']) { ++ if (typeof x === 'boolean') ++ return true; ++ details.error = `Expected "${path.join('.')}" to be |boolean|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Null = function(x, details = {}, path = ['']) { ++ if (Object.is(x, null)) ++ return true; ++ details.error = `Expected "${path.join('.')}" to be \`null\`; found \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Undefined = function(x, details = {}, path = ['']) { ++ if (Object.is(x, undefined)) ++ return true; ++ details.error = `Expected "${path.join('.')}" to be \`undefined\`; found \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Any = x => true, ++ ++t.Enum = function(values) { ++ return function(x, details = {}, path = ['']) { ++ if (values.indexOf(x) !== -1) ++ return true; ++ details.error = `Expected "${path.join('.')}" to be one of [${values.join(', ')}]; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; ++ return false; ++ } ++} ++ ++t.Nullable = function(scheme) { ++ return function(x, details = {}, path = ['']) { ++ if (Object.is(x, null)) ++ return true; ++ return checkScheme(scheme, x, details, path); ++ } ++} ++ ++t.Optional = function(scheme) { ++ return function(x, details = {}, path = ['']) { ++ if (Object.is(x, undefined)) ++ return true; ++ return checkScheme(scheme, x, details, path); ++ } ++} ++ ++t.Array = function(scheme) { ++ return function(x, details = {}, path = ['']) { ++ if (!Array.isArray(x)) { ++ details.error = `Expected "${path.join('.')}" to be an array; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; ++ return false; ++ } ++ const lastPathElement = path[path.length - 1]; ++ for (let i = 0; i < x.length; ++i) { ++ path[path.length - 1] = lastPathElement + `[${i}]`; ++ if (!checkScheme(scheme, x[i], details, path)) ++ return false; ++ } ++ path[path.length - 1] = lastPathElement; ++ return true; ++ } ++} ++ ++t.Recursive = function(types, schemeName) { ++ return function(x, details = {}, path = ['']) { ++ const scheme = types[schemeName]; ++ return checkScheme(scheme, x, details, path); ++ } ++} ++ ++function beauty(path, obj) { ++ if (path.length === 1) ++ return `object ${JSON.stringify(obj, null, 2)}`; ++ return `property "${path.join('.')}" - ${JSON.stringify(obj, null, 2)}`; ++} ++ ++function checkScheme(scheme, x, details = {}, path = ['']) { ++ if (!scheme) ++ throw new Error(`ILLDEFINED SCHEME: ${path.join('.')}`); ++ if (typeof scheme === 'object') { ++ if (!x) { ++ details.error = `Object "${path.join('.')}" is undefined, but has some scheme`; ++ return false; ++ } ++ for (const [propertyName, aScheme] of Object.entries(scheme)) { ++ path.push(propertyName); ++ const result = checkScheme(aScheme, x[propertyName], details, path); ++ path.pop(); ++ if (!result) ++ return false; ++ } ++ for (const propertyName of Object.keys(x)) { ++ if (!scheme[propertyName]) { ++ path.push(propertyName); ++ details.error = `Found ${beauty(path, x[propertyName])} which is not described in this scheme`; ++ return false; ++ } ++ } ++ return true; ++ } ++ return scheme(x, details, path); ++} ++ ++/* ++ ++function test(scheme, obj) { ++ const details = {}; ++ if (!checkScheme(scheme, obj, details)) { ++ dump(`FAILED: ${JSON.stringify(obj)} ++ details.error: ${details.error} ++ `); ++ } else { ++ dump(`SUCCESS: ${JSON.stringify(obj)} ++`); ++ } ++} ++ ++test(t.Array(t.String), ['a', 'b', 2, 'c']); ++test(t.Either(t.String, t.Number), {}); ++ ++*/ ++ ++this.t = t; ++this.checkScheme = checkScheme; ++this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; +diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js +new file mode 100644 +index 000000000000..63186502775d +--- /dev/null ++++ b/testing/juggler/protocol/Protocol.js +@@ -0,0 +1,660 @@ ++const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); ++ ++// Protocol-specific types. ++const types = {}; ++ ++types.TargetInfo = { ++ type: t.Enum(['page', 'browser']), ++ targetId: t.String, ++ browserContextId: t.Optional(t.String), ++ url: t.String, ++ // PageId of parent tab, if any. ++ openerId: t.Optional(t.String), ++}; ++ ++types.DOMPoint = { ++ x: t.Number, ++ y: t.Number, ++}; ++ ++types.DOMQuad = { ++ p1: types.DOMPoint, ++ p2: types.DOMPoint, ++ p3: types.DOMPoint, ++ p4: types.DOMPoint, ++}; ++ ++types.TouchPoint = { ++ x: t.Number, ++ y: t.Number, ++ radiusX: t.Optional(t.Number), ++ radiusY: t.Optional(t.Number), ++ rotationAngle: t.Optional(t.Number), ++ force: t.Optional(t.Number), ++}; ++ ++types.RemoteObject = { ++ type: t.Optional(t.Enum(['object', 'function', 'undefined', 'string', 'number', 'boolean', 'symbol', 'bigint'])), ++ subtype: t.Optional(t.Enum(['array', 'null', 'node', 'regexp', 'date', 'map', 'set', 'weakmap', 'weakset', 'error', 'proxy', 'promise', 'typedarray'])), ++ objectId: t.Optional(t.String), ++ unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])), ++ value: t.Any ++}; ++ ++types.AXTree = { ++ role: t.String, ++ name: t.String, ++ children: t.Optional(t.Array(t.Recursive(types, 'AXTree'))), ++ ++ selected: t.Optional(t.Boolean), ++ focused: t.Optional(t.Boolean), ++ pressed: t.Optional(t.Boolean), ++ focusable: t.Optional(t.Boolean), ++ haspopup: t.Optional(t.Boolean), ++ required: t.Optional(t.Boolean), ++ invalid: t.Optional(t.Boolean), ++ modal: t.Optional(t.Boolean), ++ editable: t.Optional(t.Boolean), ++ busy: t.Optional(t.Boolean), ++ multiline: t.Optional(t.Boolean), ++ readonly: t.Optional(t.Boolean), ++ checked: t.Optional(t.Enum(['mixed', true])), ++ expanded: t.Optional(t.Boolean), ++ disabled: t.Optional(t.Boolean), ++ multiselectable: t.Optional(t.Boolean), ++ ++ value: t.Optional(t.String), ++ description: t.Optional(t.String), ++ ++ value: t.Optional(t.String), ++ roledescription: t.Optional(t.String), ++ valuetext: t.Optional(t.String), ++ orientation: t.Optional(t.String), ++ autocomplete: t.Optional(t.String), ++ keyshortcuts: t.Optional(t.String), ++ ++ level: t.Optional(t.Number), ++ ++ tag: t.Optional(t.String), ++} ++ ++const Browser = { ++ targets: ['browser'], ++ ++ events: {}, ++ ++ methods: { ++ 'close': {}, ++ 'getInfo': { ++ returns: { ++ userAgent: t.String, ++ version: t.String, ++ }, ++ }, ++ 'setIgnoreHTTPSErrors': { ++ params: { ++ enabled: t.Boolean, ++ }, ++ }, ++ 'grantPermissions': { ++ params: { ++ origin: t.String, ++ browserContextId: t.Optional(t.String), ++ permissions: t.Array(t.Enum([ ++ 'geo', 'microphone', 'camera', 'desktop-notifications' ++ ])), ++ }, ++ }, ++ 'resetPermissions': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ } ++ }, ++ 'setCookies': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ cookies: t.Array({ ++ name: t.String, ++ value: t.String, ++ url: t.Optional(t.String), ++ domain: t.Optional(t.String), ++ path: t.Optional(t.String), ++ secure: t.Optional(t.Boolean), ++ httpOnly: t.Optional(t.Boolean), ++ sameSite: t.Optional(t.Enum(['Strict', 'Lax'])), ++ expires: t.Optional(t.Number), ++ }), ++ } ++ }, ++ 'deleteCookies': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ cookies: t.Array({ ++ name: t.String, ++ domain: t.Optional(t.String), ++ path: t.Optional(t.String), ++ url: t.Optional(t.String), ++ }), ++ } ++ }, ++ 'getCookies': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ urls: t.Array(t.String), ++ }, ++ returns: { ++ cookies: t.Array({ ++ name: t.String, ++ domain: t.String, ++ path: t.String, ++ value: t.String, ++ expires: t.Number, ++ size: t.Number, ++ httpOnly: t.Boolean, ++ secure: t.Boolean, ++ session: t.Boolean, ++ sameSite: t.Optional(t.Enum(['Strict', 'Lax'])), ++ }), ++ }, ++ }, ++ }, ++}; ++ ++const Target = { ++ targets: ['browser'], ++ ++ events: { ++ 'attachedToTarget': { ++ sessionId: t.String, ++ targetInfo: types.TargetInfo, ++ }, ++ 'detachedFromTarget': { ++ sessionId: t.String, ++ }, ++ 'targetCreated': types.TargetInfo, ++ 'targetDestroyed': types.TargetInfo, ++ 'targetInfoChanged': types.TargetInfo, ++ }, ++ ++ methods: { ++ // Start emitting tagOpened/tabClosed events ++ 'enable': {}, ++ 'attachToTarget': { ++ params: { ++ targetId: t.String, ++ }, ++ returns: { ++ sessionId: t.String, ++ }, ++ }, ++ 'newPage': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ }, ++ returns: { ++ targetId: t.String, ++ } ++ }, ++ 'createBrowserContext': { ++ returns: { ++ browserContextId: t.String, ++ }, ++ }, ++ 'removeBrowserContext': { ++ params: { ++ browserContextId: t.String, ++ }, ++ }, ++ 'getBrowserContexts': { ++ returns: { ++ browserContextIds: t.Array(t.String), ++ }, ++ }, ++ }, ++}; ++ ++const Network = { ++ targets: ['page'], ++ events: { ++ 'requestWillBeSent': { ++ // frameId may be absent for redirected requests. ++ frameId: t.Optional(t.String), ++ requestId: t.String, ++ // RequestID of redirected request. ++ redirectedFrom: t.Optional(t.String), ++ postData: t.Optional(t.String), ++ headers: t.Array({ ++ name: t.String, ++ value: t.String, ++ }), ++ suspended: t.Optional(t.Boolean), ++ url: t.String, ++ method: t.String, ++ isNavigationRequest: t.Boolean, ++ cause: t.String, ++ }, ++ 'responseReceived': { ++ securityDetails: t.Nullable({ ++ protocol: t.String, ++ subjectName: t.String, ++ issuer: t.String, ++ validFrom: t.Number, ++ validTo: t.Number, ++ }), ++ requestId: t.String, ++ fromCache: t.Boolean, ++ remoteIPAddress: t.Optional(t.String), ++ remotePort: t.Optional(t.Number), ++ status: t.Number, ++ statusText: t.String, ++ headers: t.Array({ ++ name: t.String, ++ value: t.String, ++ }), ++ }, ++ 'requestFinished': { ++ requestId: t.String, ++ }, ++ 'requestFailed': { ++ requestId: t.String, ++ errorCode: t.String, ++ }, ++ }, ++ methods: { ++ 'enable': {}, ++ 'setRequestInterception': { ++ params: { ++ enabled: t.Boolean, ++ }, ++ }, ++ 'setExtraHTTPHeaders': { ++ params: { ++ headers: t.Array({ ++ name: t.String, ++ value: t.String, ++ }), ++ }, ++ }, ++ 'abortSuspendedRequest': { ++ params: { ++ requestId: t.String, ++ }, ++ }, ++ 'resumeSuspendedRequest': { ++ params: { ++ requestId: t.String, ++ headers: t.Optional(t.Array({ ++ name: t.String, ++ value: t.String, ++ })), ++ }, ++ }, ++ 'getResponseBody': { ++ params: { ++ requestId: t.String, ++ }, ++ returns: { ++ base64body: t.String, ++ evicted: t.Optional(t.Boolean), ++ }, ++ }, ++ }, ++}; ++ ++const Runtime = { ++ targets: ['page'], ++ events: { ++ 'executionContextCreated': { ++ executionContextId: t.String, ++ auxData: t.Any, ++ }, ++ 'executionContextDestroyed': { ++ executionContextId: t.String, ++ }, ++ 'console': { ++ executionContextId: t.String, ++ args: t.Array(types.RemoteObject), ++ type: t.String, ++ location: { ++ columnNumber: t.Number, ++ lineNumber: t.Number, ++ url: t.String, ++ }, ++ }, ++ }, ++ methods: { ++ 'enable': { ++ params: {}, ++ }, ++ 'evaluate': { ++ params: { ++ // Pass frameId here. ++ executionContextId: t.String, ++ expression: t.String, ++ returnByValue: t.Optional(t.Boolean), ++ }, ++ ++ returns: { ++ result: t.Optional(types.RemoteObject), ++ exceptionDetails: t.Optional({ ++ text: t.Optional(t.String), ++ stack: t.Optional(t.String), ++ value: t.Optional(t.Any), ++ }), ++ } ++ }, ++ 'callFunction': { ++ params: { ++ // Pass frameId here. ++ executionContextId: t.String, ++ functionDeclaration: t.String, ++ returnByValue: t.Optional(t.Boolean), ++ args: t.Array({ ++ objectId: t.Optional(t.String), ++ unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])), ++ value: t.Any, ++ }), ++ }, ++ ++ returns: { ++ result: t.Optional(types.RemoteObject), ++ exceptionDetails: t.Optional({ ++ text: t.Optional(t.String), ++ stack: t.Optional(t.String), ++ value: t.Optional(t.Any), ++ }), ++ } ++ }, ++ 'disposeObject': { ++ params: { ++ executionContextId: t.String, ++ objectId: t.String, ++ }, ++ }, ++ ++ 'getObjectProperties': { ++ params: { ++ executionContextId: t.String, ++ objectId: t.String, ++ }, ++ ++ returns: { ++ properties: t.Array({ ++ name: t.String, ++ value: types.RemoteObject, ++ }), ++ } ++ }, ++ }, ++}; ++ ++const Page = { ++ targets: ['page'], ++ events: { ++ 'eventFired': { ++ frameId: t.String, ++ name: t.Enum(['load', 'DOMContentLoaded']), ++ }, ++ 'uncaughtError': { ++ frameId: t.String, ++ message: t.String, ++ stack: t.String, ++ }, ++ 'frameAttached': { ++ frameId: t.String, ++ parentFrameId: t.Optional(t.String), ++ }, ++ 'frameDetached': { ++ frameId: t.String, ++ }, ++ 'navigationStarted': { ++ frameId: t.String, ++ navigationId: t.String, ++ url: t.String, ++ }, ++ 'navigationCommitted': { ++ frameId: t.String, ++ navigationId: t.String, ++ url: t.String, ++ // frame.id or frame.name ++ name: t.String, ++ }, ++ 'navigationAborted': { ++ frameId: t.String, ++ navigationId: t.String, ++ errorText: t.String, ++ }, ++ 'sameDocumentNavigation': { ++ frameId: t.String, ++ url: t.String, ++ }, ++ 'dialogOpened': { ++ dialogId: t.String, ++ type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']), ++ message: t.String, ++ defaultValue: t.Optional(t.String), ++ }, ++ 'dialogClosed': { ++ dialogId: t.String, ++ }, ++ 'bindingCalled': { ++ executionContextId: t.String, ++ name: t.String, ++ payload: t.Any, ++ }, ++ }, ++ ++ methods: { ++ 'enable': { ++ params: {}, ++ }, ++ 'close': { ++ params: { ++ runBeforeUnload: t.Optional(t.Boolean), ++ }, ++ }, ++ 'setFileInputFiles': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ files: t.Array(t.String), ++ }, ++ }, ++ 'addBinding': { ++ params: { ++ name: t.String, ++ }, ++ }, ++ 'setViewport': { ++ params: { ++ viewport: t.Nullable({ ++ width: t.Number, ++ height: t.Number, ++ deviceScaleFactor: t.Number, ++ isMobile: t.Boolean, ++ hasTouch: t.Boolean, ++ isLandscape: t.Boolean, ++ }), ++ }, ++ }, ++ 'setUserAgent': { ++ params: { ++ userAgent: t.Nullable(t.String), ++ }, ++ }, ++ 'setEmulatedMedia': { ++ params: { ++ media: t.Enum(['screen', 'print', '']), ++ }, ++ }, ++ 'setCacheDisabled': { ++ params: { ++ cacheDisabled: t.Boolean, ++ }, ++ }, ++ 'setJavascriptEnabled': { ++ params: { ++ enabled: t.Boolean, ++ }, ++ }, ++ 'contentFrame': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ }, ++ returns: { ++ frameId: t.Nullable(t.String), ++ }, ++ }, ++ 'addScriptToEvaluateOnNewDocument': { ++ params: { ++ script: t.String, ++ }, ++ returns: { ++ scriptId: t.String, ++ } ++ }, ++ 'removeScriptToEvaluateOnNewDocument': { ++ params: { ++ scriptId: t.String, ++ }, ++ }, ++ 'navigate': { ++ params: { ++ frameId: t.String, ++ url: t.String, ++ referer: t.Optional(t.String), ++ }, ++ returns: { ++ navigationId: t.Nullable(t.String), ++ navigationURL: t.Nullable(t.String), ++ } ++ }, ++ 'goBack': { ++ params: { ++ frameId: t.String, ++ }, ++ returns: { ++ navigationId: t.Nullable(t.String), ++ navigationURL: t.Nullable(t.String), ++ } ++ }, ++ 'goForward': { ++ params: { ++ frameId: t.String, ++ }, ++ returns: { ++ navigationId: t.Nullable(t.String), ++ navigationURL: t.Nullable(t.String), ++ } ++ }, ++ 'reload': { ++ params: { ++ frameId: t.String, ++ }, ++ returns: { ++ navigationId: t.String, ++ navigationURL: t.String, ++ } ++ }, ++ 'getBoundingBox': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ }, ++ returns: t.Nullable({ ++ x: t.Number, ++ y: t.Number, ++ width: t.Number, ++ height: t.Number, ++ }), ++ }, ++ 'screenshot': { ++ params: { ++ mimeType: t.Enum(['image/png', 'image/jpeg']), ++ fullPage: t.Optional(t.Boolean), ++ clip: t.Optional({ ++ x: t.Number, ++ y: t.Number, ++ width: t.Number, ++ height: t.Number, ++ }) ++ }, ++ returns: { ++ data: t.String, ++ } ++ }, ++ 'getContentQuads': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ }, ++ returns: { ++ quads: t.Array(types.DOMQuad), ++ }, ++ }, ++ 'dispatchKeyEvent': { ++ params: { ++ type: t.String, ++ key: t.String, ++ keyCode: t.Number, ++ location: t.Number, ++ code: t.String, ++ repeat: t.Boolean, ++ } ++ }, ++ 'dispatchTouchEvent': { ++ params: { ++ type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']), ++ touchPoints: t.Array(types.TouchPoint), ++ modifiers: t.Number, ++ }, ++ returns: { ++ defaultPrevented: t.Boolean, ++ } ++ }, ++ 'dispatchMouseEvent': { ++ params: { ++ type: t.String, ++ button: t.Number, ++ x: t.Number, ++ y: t.Number, ++ modifiers: t.Number, ++ clickCount: t.Optional(t.Number), ++ buttons: t.Number, ++ } ++ }, ++ 'insertText': { ++ params: { ++ text: t.String, ++ } ++ }, ++ 'handleDialog': { ++ params: { ++ dialogId: t.String, ++ accept: t.Boolean, ++ promptText: t.Optional(t.String), ++ }, ++ }, ++ }, ++}; ++ ++ ++const Accessibility = { ++ targets: ['page'], ++ events: {}, ++ methods: { ++ 'getFullAXTree': { ++ params: {}, ++ returns: { ++ tree:types.AXTree ++ }, ++ } ++ } ++} ++ ++this.protocol = { ++ domains: {Browser, Target, Page, Runtime, Network, Accessibility}, ++}; ++this.checkScheme = checkScheme; ++this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme']; +diff --git a/testing/juggler/protocol/RuntimeHandler.js b/testing/juggler/protocol/RuntimeHandler.js +new file mode 100644 +index 000000000000..0026e8ff58ef +--- /dev/null ++++ b/testing/juggler/protocol/RuntimeHandler.js +@@ -0,0 +1,41 @@ ++"use strict"; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++const helper = new Helper(); ++ ++class RuntimeHandler { ++ constructor(chromeSession, contentSession) { ++ this._chromeSession = chromeSession; ++ this._contentSession = contentSession; ++ } ++ ++ async enable(options) { ++ return await this._contentSession.send('Runtime.enable', options); ++ } ++ ++ async evaluate(options) { ++ return await this._contentSession.send('Runtime.evaluate', options); ++ } ++ ++ async callFunction(options) { ++ return await this._contentSession.send('Runtime.callFunction', options); ++ } ++ ++ async getObjectProperties(options) { ++ return await this._contentSession.send('Runtime.getObjectProperties', options); ++ } ++ ++ async disposeObject(options) { ++ return await this._contentSession.send('Runtime.disposeObject', options); ++ } ++ ++ dispose() {} ++} ++ ++var EXPORTED_SYMBOLS = ['RuntimeHandler']; ++this.RuntimeHandler = RuntimeHandler; +diff --git a/testing/juggler/protocol/TargetHandler.js b/testing/juggler/protocol/TargetHandler.js +new file mode 100644 +index 000000000000..4ea36eeba758 +--- /dev/null ++++ b/testing/juggler/protocol/TargetHandler.js +@@ -0,0 +1,75 @@ ++"use strict"; ++ ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js"); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const helper = new Helper(); ++ ++class TargetHandler { ++ /** ++ * @param {ChromeSession} session ++ */ ++ constructor(session) { ++ this._session = session; ++ this._contextManager = BrowserContextManager.instance(); ++ this._targetRegistry = TargetRegistry.instance(); ++ this._enabled = false; ++ this._eventListeners = []; ++ } ++ ++ async attachToTarget({targetId}) { ++ const sessionId = await this._session.dispatcher().createSession(targetId); ++ return {sessionId}; ++ } ++ ++ async createBrowserContext() { ++ return {browserContextId: this._contextManager.createBrowserContext()}; ++ } ++ ++ async removeBrowserContext({browserContextId}) { ++ this._contextManager.removeBrowserContext(browserContextId); ++ } ++ ++ async getBrowserContexts() { ++ return {browserContextIds: this._contextManager.getBrowserContexts()}; ++ } ++ ++ async enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ for (const targetInfo of this._targetRegistry.targetInfos()) ++ this._onTargetCreated(targetInfo); ++ ++ this._eventListeners = [ ++ helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)), ++ helper.on(this._targetRegistry, TargetRegistry.Events.TargetChanged, this._onTargetChanged.bind(this)), ++ helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)), ++ ]; ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ _onTargetCreated(targetInfo) { ++ this._session.emitEvent('Target.targetCreated', targetInfo); ++ } ++ ++ _onTargetChanged(targetInfo) { ++ this._session.emitEvent('Target.targetInfoChanged', targetInfo); ++ } ++ ++ _onTargetDestroyed(targetInfo) { ++ this._session.emitEvent('Target.targetDestroyed', targetInfo); ++ } ++ ++ async newPage({browserContextId}) { ++ const targetId = await this._targetRegistry.newPage({browserContextId}); ++ return {targetId}; ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['TargetHandler']; ++this.TargetHandler = TargetHandler; +diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp +index 9aea55ddf773..188a0f28b8e1 100644 +--- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp ++++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp +@@ -179,8 +179,16 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress, + } + + NS_IMETHODIMP +-nsBrowserStatusFilter::OnProgressChange(nsIWebProgress* aWebProgress, +- nsIRequest* aRequest, ++nsBrowserStatusFilter::OnFrameLocationChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, ++ nsIURI *aLocation, ++ uint32_t aFlags) { ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, +diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild +index 79d6eeed7247..0362763ead99 100644 +--- a/toolkit/toolkit.mozbuild ++++ b/toolkit/toolkit.mozbuild +@@ -168,6 +168,7 @@ if CONFIG['ENABLE_MARIONETTE']: + DIRS += [ + '/testing/firefox-ui', + '/testing/marionette', ++ '/testing/juggler', + '/toolkit/components/telemetry/tests/marionette', + ] + +diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp +index 92cb5f3cf6da..7918f127c801 100644 +--- a/uriloader/base/nsDocLoader.cpp ++++ b/uriloader/base/nsDocLoader.cpp +@@ -1370,6 +1370,24 @@ void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress, + } + } + ++void nsDocLoader::FireOnFrameLocationChange(nsIWebProgress* aWebProgress, ++ nsIRequest* aRequest, ++ nsIURI *aUri, ++ uint32_t aFlags) { ++ NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_FRAME_LOCATION, ++ nsCOMPtr listener2 = ++ do_QueryReferent(info.mWeakListener); ++ if (!listener2) ++ continue; ++ listener2->OnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags); ++ ); ++ ++ // Pass the notification up to the parent... ++ if (mParent) { ++ mParent->FireOnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags); ++ } ++} ++ + void nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { +diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h +index 4b551ff8e5c1..abc59361f2d6 100644 +--- a/uriloader/base/nsDocLoader.h ++++ b/uriloader/base/nsDocLoader.h +@@ -210,6 +210,11 @@ class nsDocLoader : public nsIDocumentLoader, + void FireOnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsIURI* aUri, uint32_t aFlags); + ++ void FireOnFrameLocationChange(nsIWebProgress* aWebProgress, ++ nsIRequest* aRequest, ++ nsIURI *aUri, ++ uint32_t aFlags); ++ + MOZ_MUST_USE bool RefreshAttempted(nsIWebProgress* aWebProgress, nsIURI* aURI, + int32_t aDelay, bool aSameURI); + +diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl +index b0cde5026dc7..09ebb0ef6799 100644 +--- a/uriloader/base/nsIWebProgress.idl ++++ b/uriloader/base/nsIWebProgress.idl +@@ -87,6 +87,10 @@ interface nsIWebProgress : nsISupports + * NOTIFY_REFRESH + * Receive onRefreshAttempted events. + * This is defined on nsIWebProgressListener2. ++ * ++ * NOTIFY_FRAME_LOCATION ++ * Receive onFrameLocationChange events. ++ * This is defined on nsIWebProgressListener2. + */ + const unsigned long NOTIFY_PROGRESS = 0x00000010; + const unsigned long NOTIFY_STATUS = 0x00000020; +@@ -94,11 +98,12 @@ interface nsIWebProgress : nsISupports + const unsigned long NOTIFY_LOCATION = 0x00000080; + const unsigned long NOTIFY_REFRESH = 0x00000100; + const unsigned long NOTIFY_CONTENT_BLOCKING = 0x00000200; ++ const unsigned long NOTIFY_FRAME_LOCATION = 0x00000400; + + /** + * This flag enables all notifications. + */ +- const unsigned long NOTIFY_ALL = 0x000003ff; ++ const unsigned long NOTIFY_ALL = 0x000007ff; + + /** + * Registers a listener to receive web progress events. +diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl +index 87701f8d2cfe..ae1aa85c019c 100644 +--- a/uriloader/base/nsIWebProgressListener2.idl ++++ b/uriloader/base/nsIWebProgressListener2.idl +@@ -66,4 +66,27 @@ interface nsIWebProgressListener2 : nsIWebProgressListener { + in nsIURI aRefreshURI, + in long aMillis, + in boolean aSameURI); ++ ++ /** ++ * Called when the location of the window or its subframes changes. This is not ++ * when a load is requested, but rather once it is verified that the load is ++ * going to occur in the given window. For instance, a load that starts in a ++ * window might send progress and status messages for the new site, but it ++ * will not send the onLocationChange until we are sure that we are loading ++ * this new page here. ++ * ++ * @param aWebProgress ++ * The nsIWebProgress instance that fired the notification. ++ * @param aRequest ++ * The associated nsIRequest. This may be null in some cases. ++ * @param aLocation ++ * The URI of the location that is being loaded. ++ * @param aFlags ++ * This is a value which explains the situation or the reason why ++ * the location has changed. ++ */ ++ void onFrameLocationChange(in nsIWebProgress aWebProgress, ++ in nsIRequest aRequest, ++ in nsIURI aLocation, ++ [optional] in unsigned long aFlags); + }; +-- +2.17.1 + diff --git a/browser_patches/upload.sh b/browser_patches/upload.sh new file mode 100755 index 0000000000..0266f1ef8c --- /dev/null +++ b/browser_patches/upload.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -e +set +x + +cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +if [[ ($1 == '--help') || ($1 == '-h') ]]; then + echo "usage: $0 [firefox|webkit]" + echo + echo "Archive and upload a browser" + echo + echo "NOTE: \$AZ_ACCOUNT_KEY (azure account name) and \$AZ_ACCOUNT_NAME (azure account name)" + echo "env variables are required to upload builds to CDN." + exit 0 +fi + +if [[ $# == 0 ]]; then + echo "missing browser: 'firefox' or 'webkit'" + echo "try '$0 --help' for more information" + exit 1 +fi + +if [[ (-z $AZ_ACCOUNT_KEY) || (-z $AZ_ACCOUNT_NAME) ]]; then + echo "ERROR: Either \$AZ_ACCOUNT_KEY or \$AZ_ACCOUNT_NAME environment variable is missing." + echo " 'Azure Account Name' and 'Azure Account Key' secrets that are required" + echo " to upload builds ot Azure CDN." + exit 1 +fi + +ARCHIVE_SCRIPT="" +BROWSER_NAME="" +BUILD_NUMBER="" +if [[ ("$1" == "firefox") || ("$1" == "firefox/") ]]; then + # we always apply our patches atop of beta since it seems to get better + # reliability guarantees. + ARCHIVE_FOLDER="$PWD/firefox" + BUILD_NUMBER=$(cat "$PWD/firefox/BUILD_NUMBER") + ARCHIVE_SCRIPT="$PWD/firefox/archive.sh" + BROWSER_NAME="firefox" +elif [[ ("$1" == "webkit") || ("$1" == "webkit/") ]]; then + ARCHIVE_FOLDER="$PWD/webkit" + BUILD_NUMBER=$(cat "$PWD/webkit/BUILD_NUMBER") + ARCHIVE_SCRIPT="$PWD/webkit/archive.sh" + BROWSER_NAME="webkit" +else + echo ERROR: unknown browser to export - "$1" + exit 1 +fi + +if ! [[ -z $(ls $ARCHIVE_FOLDER | grep '.zip') ]]; then + echo ERROR: .zip file already exists in $ARCHIVE_FOLDER! + echo Remove manually all zip files and re-run the script. + exit 1 +fi + +$ARCHIVE_SCRIPT +ZIP_NAME=$(ls $ARCHIVE_FOLDER | grep '.zip') +ZIP_PATH=$ARCHIVE_FOLDER/$ZIP_NAME +BLOB_NAME="$BROWSER_NAME/$BUILD_NUMBER/$ZIP_NAME" +az storage blob upload -c builds --account-key $AZ_ACCOUNT_KEY --account-name $AZ_ACCOUNT_NAME -f $ZIP_PATH -n "$BLOB_NAME" +echo "Uploaded $(du -h "$ZIP_PATH" | awk '{print $1}') as $BLOB_NAME" +rm $ZIP_PATH diff --git a/browser_patches/webkit/.gitignore b/browser_patches/webkit/.gitignore new file mode 100644 index 0000000000..5e660dc18e --- /dev/null +++ b/browser_patches/webkit/.gitignore @@ -0,0 +1 @@ +/checkout diff --git a/browser_patches/webkit/BASE_REVISION b/browser_patches/webkit/BASE_REVISION new file mode 100644 index 0000000000..0fc3234513 --- /dev/null +++ b/browser_patches/webkit/BASE_REVISION @@ -0,0 +1 @@ +cadee71e3e832cc0b78184a714ade07d9a6d3173 diff --git a/browser_patches/webkit/BUILD_NUMBER b/browser_patches/webkit/BUILD_NUMBER new file mode 100644 index 0000000000..d474e1b4d6 --- /dev/null +++ b/browser_patches/webkit/BUILD_NUMBER @@ -0,0 +1,2 @@ +1 + diff --git a/browser_patches/webkit/archive.sh b/browser_patches/webkit/archive.sh new file mode 100755 index 0000000000..57ebdcb0ad --- /dev/null +++ b/browser_patches/webkit/archive.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +if [[ ("$1" == "-h") || ("$1" == "--help") ]]; then + echo "usage: $0" + echo + echo "Generate distributable .zip archive from ./checkout folder that was previously built." + echo + exit 0 +fi + +set -e +set -x + +main() { + cd checkout + + if [[ "$(uname)" == "Darwin" ]]; then + createZipForMac + elif [[ "$(uname)" == "Linux" ]]; then + createZipForLinux + else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; + fi +} + +createZipForLinux() { + # create a TMP directory to copy all necessary files + local tmpdir=$(mktemp -d -t webkit-deploy-XXXXXXXXXX) + mkdir -p $tmpdir + + # copy all relevant binaries + cp -t $tmpdir ./WebKitBuild/Release/bin/MiniBrowser ./WebKitBuild/Release/bin/WebKit*Process + # copy runner + cp -t $tmpdir ../pw_run.sh + # copy protocol + node ../concat_protocol.js > $tmpdir/protocol.json + # copy all relevant shared objects + LD_LIBRARY_PATH="$PWD/WebKitBuild/DependenciesGTK/Root/lib" ldd WebKitBuild/Release/bin/MiniBrowser | grep -o '[^ ]*WebKitBuild/[^ ]*' | xargs cp -t $tmpdir + + # we failed to nicely build libgdk_pixbuf - expect it in the env + rm $tmpdir/libgdk_pixbuf* + + # tar resulting directory and cleanup TMP. + local zipname="minibrowser-linux.zip" + zip -jr ../$zipname $tmpdir + rm -rf $tmpdir +} + +createZipForMac() { + # create a TMP directory to copy all necessary files + local tmpdir=$(mktemp -d) + + # copy all relevant files + ditto {./WebKitBuild/Release,$tmpdir}/com.apple.WebKit.Networking.xpc + ditto {./WebKitBuild/Release,$tmpdir}/com.apple.WebKit.Plugin.64.xpc + ditto {./WebKitBuild/Release,$tmpdir}/com.apple.WebKit.WebContent.xpc + ditto {./WebKitBuild/Release,$tmpdir}/JavaScriptCore.framework + ditto {./WebKitBuild/Release,$tmpdir}/libwebrtc.dylib + ditto {./WebKitBuild/Release,$tmpdir}/MiniBrowser.app + ditto {./WebKitBuild/Release,$tmpdir}/PluginProcessShim.dylib + ditto {./WebKitBuild/Release,$tmpdir}/SecItemShim.dylib + ditto {./WebKitBuild/Release,$tmpdir}/WebCore.framework + ditto {./WebKitBuild/Release,$tmpdir}/WebInspectorUI.framework + ditto {./WebKitBuild/Release,$tmpdir}/WebKit.framework + ditto {./WebKitBuild/Release,$tmpdir}/WebKitLegacy.framework + ditto {..,$tmpdir}/pw_run.sh + # copy protocol + node ../concat_protocol.js > $tmpdir/protocol.json + + # zip resulting directory and cleanup TMP. + local MAC_MAJOR_MINOR_VERSION=$(sw_vers -productVersion | grep -o '^\d\+.\d\+') + local zipname="minibrowser-mac-$MAC_MAJOR_MINOR_VERSION.zip" + ditto -c -k $tmpdir ../$zipname + rm -rf $tmpdir +} + +cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT +main "$@" diff --git a/browser_patches/webkit/build.sh b/browser_patches/webkit/build.sh new file mode 100755 index 0000000000..ee2c12c28c --- /dev/null +++ b/browser_patches/webkit/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e +set +x + +function cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +cd checkout + +if ! [[ $(git rev-parse --abbrev-ref HEAD) == "pwdev" ]]; then + echo "ERROR: Cannot build any branch other than PWDEV" + exit 1; +else + echo "-- checking git branch is PWDEV - OK" +fi + +if [[ "$(uname)" == "Darwin" ]]; then + ./Tools/Scripts/build-webkit --release +elif [[ "$(uname)" == "Linux" ]]; then + ./Tools/Scripts/build-webkit --gtk --release MiniBrowser +else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; +fi diff --git a/browser_patches/webkit/concat_protocol.js b/browser_patches/webkit/concat_protocol.js new file mode 100644 index 0000000000..bf3d84bcc4 --- /dev/null +++ b/browser_patches/webkit/concat_protocol.js @@ -0,0 +1,6 @@ +const fs = require('fs'); +const path = require('path'); +const protocolDir = path.join(__dirname, './checkout/Source/JavaScriptCore/inspector/protocol'); +const files = fs.readdirSync(protocolDir).filter(f => f.endsWith('.json')).map(f => path.join(protocolDir, f)); +const json = files.map(file => JSON.parse(fs.readFileSync(file))); +console.log(JSON.stringify(json)); diff --git a/browser_patches/webkit/patches/0001-chore-bootstrap.patch b/browser_patches/webkit/patches/0001-chore-bootstrap.patch new file mode 100644 index 0000000000..1e2b20bc94 --- /dev/null +++ b/browser_patches/webkit/patches/0001-chore-bootstrap.patch @@ -0,0 +1,5890 @@ +From 23d352fbff5f65f02eec92327a5c839b9f6c6fca Mon Sep 17 00:00:00 2001 +From: Andrey Lushnikov +Date: Fri, 15 Nov 2019 18:07:34 -0800 +Subject: [PATCH xserver] chore: bootstrap + +--- + Source/JavaScriptCore/CMakeLists.txt | 3 + + Source/JavaScriptCore/DerivedSources.make | 3 + + .../inspector/InspectorBackendDispatcher.cpp | 21 +- + .../inspector/InspectorBackendDispatcher.h | 5 +- + .../inspector/InspectorTarget.h | 3 + + .../inspector/agents/InspectorTargetAgent.cpp | 46 +++- + .../inspector/agents/InspectorTargetAgent.h | 6 +- + .../inspector/protocol/Browser.json | 40 +++ + .../inspector/protocol/DOM.json | 21 ++ + .../inspector/protocol/Emulation.json | 14 + + .../inspector/protocol/Input.json | 160 +++++++++++ + .../inspector/protocol/Page.json | 18 +- + .../inspector/protocol/Target.json | 18 +- + .../inspector/InspectorInstrumentation.cpp | 6 + + .../inspector/InspectorInstrumentation.h | 9 + + .../inspector/agents/InspectorDOMAgent.cpp | 57 ++++ + .../inspector/agents/InspectorDOMAgent.h | 1 + + .../inspector/agents/InspectorPageAgent.cpp | 18 +- + .../inspector/agents/InspectorPageAgent.h | 3 +- + Source/WebCore/loader/FrameLoader.cpp | 1 + + Source/WebCore/page/History.cpp | 1 + + .../WebCore/platform/PlatformKeyboardEvent.h | 2 + + .../platform/gtk/PlatformKeyboardEventGtk.cpp | 242 +++++++++++++++++ + .../libwpe/PlatformKeyboardEventLibWPE.cpp | 240 ++++++++++++++++ + Source/WebKit/Shared/API/c/wpe/WebKit.h | 1 + + Source/WebKit/Shared/NativeWebKeyboardEvent.h | 5 + + Source/WebKit/Shared/NativeWebMouseEvent.h | 4 + + Source/WebKit/Shared/WebEvent.h | 6 +- + Source/WebKit/Shared/WebKeyboardEvent.cpp | 22 ++ + .../Shared/gtk/NativeWebKeyboardEventGtk.cpp | 2 +- + .../Shared/gtk/NativeWebMouseEventGtk.cpp | 4 +- + Source/WebKit/Sources.txt | 8 + + Source/WebKit/SourcesCocoa.txt | 1 + + Source/WebKit/SourcesGTK.txt | 5 + + Source/WebKit/SourcesWPE.txt | 6 +- + Source/WebKit/UIProcess/API/APIAttachment.cpp | 1 + + Source/WebKit/UIProcess/API/C/WKPage.cpp | 2 + + .../UIProcess/API/Cocoa/_WKBrowserInspector.h | 50 ++++ + .../API/Cocoa/_WKBrowserInspector.mm | 52 ++++ + .../API/glib/WebKitBrowserInspector.cpp | 141 ++++++++++ + .../API/glib/WebKitBrowserInspectorPrivate.h | 36 +++ + .../UIProcess/API/glib/WebKitWebContext.cpp | 5 + + .../UIProcess/API/gtk/PageClientImpl.cpp | 2 + + .../API/gtk/WebKitBrowserInspector.h | 84 ++++++ + Source/WebKit/UIProcess/API/gtk/webkit2.h | 1 + + .../API/wpe/WebKitBrowserInspector.h | 81 ++++++ + Source/WebKit/UIProcess/API/wpe/webkit.h | 1 + + .../UIProcess/BrowserInspectorController.cpp | 128 +++++++++ + .../UIProcess/BrowserInspectorController.h | 74 +++++ + .../WebKit/UIProcess/BrowserInspectorPipe.cpp | 62 +++++ + .../WebKit/UIProcess/BrowserInspectorPipe.h | 43 +++ + .../UIProcess/BrowserInspectorTargetAgent.cpp | 110 ++++++++ + .../UIProcess/BrowserInspectorTargetAgent.h | 62 +++++ + .../PopUpSOAuthorizationSession.h | 4 + + .../PopUpSOAuthorizationSession.mm | 1 + + .../UIProcess/InspectorBrowserAgent.cpp | 101 +++++++ + .../WebKit/UIProcess/InspectorBrowserAgent.h | 81 ++++++ + .../UIProcess/InspectorBrowserAgentClient.h | 52 ++++ + .../WebKit/UIProcess/InspectorTargetProxy.cpp | 18 +- + .../WebKit/UIProcess/InspectorTargetProxy.h | 11 +- + .../WebKit/UIProcess/RemoteInspectorPipe.cpp | 159 +++++++++++ + Source/WebKit/UIProcess/RemoteInspectorPipe.h | 70 +++++ + .../AuthenticatorManager.cpp | 1 + + .../UIProcess/WebPageInspectorController.cpp | 56 +++- + .../UIProcess/WebPageInspectorController.h | 8 + + .../WebPageInspectorEmulationAgent.cpp | 61 +++++ + .../WebPageInspectorEmulationAgent.h | 63 +++++ + .../UIProcess/WebPageInspectorInputAgent.cpp | 257 ++++++++++++++++++ + .../UIProcess/WebPageInspectorInputAgent.h | 76 ++++++ + .../UIProcess/WebPageInspectorTargetProxy.cpp | 129 +++++++++ + .../UIProcess/WebPageInspectorTargetProxy.h | 67 +++++ + Source/WebKit/UIProcess/WebPageProxy.cpp | 12 +- + Source/WebKit/UIProcess/WebPageProxy.h | 9 + + .../glib/InspectorBrowserAgentClientGLib.cpp | 130 +++++++++ + .../glib/InspectorBrowserAgentClientGLib.h | 63 +++++ + .../gtk/WebPageInspectorEmulationAgentGtk.cpp | 58 ++++ + .../gtk/WebPageInspectorInputAgentGtk.cpp | 108 ++++++++ + .../gtk/WebPageInspectorTargetProxyGtk.cpp | 45 +++ + .../WebKit/UIProcess/ios/PageClientImplIOS.mm | 2 + + .../mac/InspectorBrowserAgentClientMac.h | 56 ++++ + .../mac/InspectorBrowserAgentClientMac.mm | 95 +++++++ + .../WebKit/UIProcess/mac/PageClientImplMac.mm | 5 + + .../mac/WebPageInspectorEmulationAgentMac.mm | 42 +++ + .../mac/WebPageInspectorInputAgentMac.mm | 37 +++ + .../mac/WebPageInspectorTargetProxyMac.mm | 41 +++ + .../wpe/WebPageInspectorEmulationAgentWPE.cpp | 41 +++ + .../wpe/WebPageInspectorInputAgentWPE.cpp | 99 +++++++ + .../wpe/WebPageInspectorTargetProxyWPE.cpp | 41 +++ + .../WebKit/WebKit.xcodeproj/project.pbxproj | 54 ++++ + .../WebPage/WebPageInspectorTarget.cpp | 7 + + .../WebPage/WebPageInspectorTarget.h | 1 + + Source/WebKit/WebProcess/WebProcess.cpp | 3 +- + Tools/MiniBrowser/gtk/main.c | 28 ++ + Tools/MiniBrowser/mac/AppDelegate.h | 4 +- + Tools/MiniBrowser/mac/AppDelegate.m | 25 +- + .../mac/WK2BrowserWindowController.h | 3 + + .../mac/WK2BrowserWindowController.m | 17 +- + Tools/MiniBrowser/wpe/main.cpp | 37 +++ + 98 files changed, 4162 insertions(+), 53 deletions(-) + create mode 100644 Source/JavaScriptCore/inspector/protocol/Browser.json + create mode 100644 Source/JavaScriptCore/inspector/protocol/Emulation.json + create mode 100644 Source/JavaScriptCore/inspector/protocol/Input.json + create mode 100644 Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h + create mode 100644 Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm + create mode 100644 Source/WebKit/UIProcess/API/glib/WebKitBrowserInspector.cpp + create mode 100644 Source/WebKit/UIProcess/API/glib/WebKitBrowserInspectorPrivate.h + create mode 100644 Source/WebKit/UIProcess/API/gtk/WebKitBrowserInspector.h + create mode 100644 Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorController.cpp + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorController.h + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorPipe.cpp + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorPipe.h + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h + create mode 100644 Source/WebKit/UIProcess/InspectorBrowserAgent.cpp + create mode 100644 Source/WebKit/UIProcess/InspectorBrowserAgent.h + create mode 100644 Source/WebKit/UIProcess/InspectorBrowserAgentClient.h + create mode 100644 Source/WebKit/UIProcess/RemoteInspectorPipe.cpp + create mode 100644 Source/WebKit/UIProcess/RemoteInspectorPipe.h + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorInputAgent.h + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorTargetProxy.cpp + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorTargetProxy.h + create mode 100644 Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.cpp + create mode 100644 Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.h + create mode 100644 Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp + create mode 100644 Source/WebKit/UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp + create mode 100644 Source/WebKit/UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp + create mode 100644 Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.h + create mode 100644 Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.mm + create mode 100644 Source/WebKit/UIProcess/mac/WebPageInspectorEmulationAgentMac.mm + create mode 100644 Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm + create mode 100644 Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm + create mode 100644 Source/WebKit/UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp + create mode 100644 Source/WebKit/UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp + create mode 100644 Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp + +diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt +index f25ff61db99..a7085df58f8 100644 +--- a/Source/JavaScriptCore/CMakeLists.txt ++++ b/Source/JavaScriptCore/CMakeLists.txt +@@ -1142,6 +1142,7 @@ set(JavaScriptCore_INSPECTOR_DOMAINS + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Animation.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/ApplicationCache.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Audit.json ++ ${JAVASCRIPTCORE_DIR}/inspector/protocol/Browser.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/CSS.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Canvas.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Console.json +@@ -1150,8 +1151,10 @@ set(JavaScriptCore_INSPECTOR_DOMAINS + ${JAVASCRIPTCORE_DIR}/inspector/protocol/DOMStorage.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Database.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Debugger.json ++ ${JAVASCRIPTCORE_DIR}/inspector/protocol/Emulation.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/GenericTypes.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Heap.json ++ ${JAVASCRIPTCORE_DIR}/inspector/protocol/Input.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Inspector.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/LayerTree.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Network.json +diff --git a/Source/JavaScriptCore/DerivedSources.make b/Source/JavaScriptCore/DerivedSources.make +index f59212ff01c..c1fb126e017 100644 +--- a/Source/JavaScriptCore/DerivedSources.make ++++ b/Source/JavaScriptCore/DerivedSources.make +@@ -238,6 +238,7 @@ INSPECTOR_DOMAINS := \ + $(JavaScriptCore)/inspector/protocol/Animation.json \ + $(JavaScriptCore)/inspector/protocol/ApplicationCache.json \ + $(JavaScriptCore)/inspector/protocol/Audit.json \ ++ $(JavaScriptCore)/inspector/protocol/Browser.json \ + $(JavaScriptCore)/inspector/protocol/CSS.json \ + $(JavaScriptCore)/inspector/protocol/Canvas.json \ + $(JavaScriptCore)/inspector/protocol/Console.json \ +@@ -246,8 +247,10 @@ INSPECTOR_DOMAINS := \ + $(JavaScriptCore)/inspector/protocol/DOMStorage.json \ + $(JavaScriptCore)/inspector/protocol/Database.json \ + $(JavaScriptCore)/inspector/protocol/Debugger.json \ ++ $(JavaScriptCore)/inspector/protocol/Emulation.json \ + $(JavaScriptCore)/inspector/protocol/GenericTypes.json \ + $(JavaScriptCore)/inspector/protocol/Heap.json \ ++ $(JavaScriptCore)/inspector/protocol/Input.json \ + $(JavaScriptCore)/inspector/protocol/Inspector.json \ + $(JavaScriptCore)/inspector/protocol/LayerTree.json \ + $(JavaScriptCore)/inspector/protocol/Network.json \ +diff --git a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp +index 038cb646d31..8a01d7679bf 100644 +--- a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp ++++ b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp +@@ -102,7 +102,7 @@ void BackendDispatcher::registerDispatcherForDomain(const String& domain, Supple + m_dispatchers.set(domain, dispatcher); + } + +-void BackendDispatcher::dispatch(const String& message) ++BackendDispatcher::DispatchResult BackendDispatcher::dispatch(const String& message, Mode mode) + { + Ref protect(*this); + +@@ -120,26 +120,26 @@ void BackendDispatcher::dispatch(const String& message) + if (!JSON::Value::parseJSON(message, parsedMessage)) { + reportProtocolError(ParseError, "Message must be in JSON format"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + if (!parsedMessage->asObject(messageObject)) { + reportProtocolError(InvalidRequest, "Message must be a JSONified object"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + RefPtr requestIdValue; + if (!messageObject->getValue("id"_s, requestIdValue)) { + reportProtocolError(InvalidRequest, "'id' property was not found"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + if (!requestIdValue->asInteger(requestId)) { + reportProtocolError(InvalidRequest, "The type of 'id' property must be integer"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + } + +@@ -151,29 +151,31 @@ void BackendDispatcher::dispatch(const String& message) + if (!messageObject->getValue("method"_s, methodValue)) { + reportProtocolError(InvalidRequest, "'method' property wasn't found"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + String methodString; + if (!methodValue->asString(methodString)) { + reportProtocolError(InvalidRequest, "The type of 'method' property must be string"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + Vector domainAndMethod = methodString.splitAllowingEmptyEntries('.'); + if (domainAndMethod.size() != 2 || !domainAndMethod[0].length() || !domainAndMethod[1].length()) { + reportProtocolError(InvalidRequest, "The 'method' property was formatted incorrectly. It should be 'Domain.method'"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + String domain = domainAndMethod[0]; + SupplementalBackendDispatcher* domainDispatcher = m_dispatchers.get(domain); + if (!domainDispatcher) { ++ if (mode == Mode::ContinueIfDomainIsMissing) ++ return DispatchResult::Continue; + reportProtocolError(MethodNotFound, "'" + domain + "' domain was not found"); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + String method = domainAndMethod[1]; +@@ -182,6 +184,7 @@ void BackendDispatcher::dispatch(const String& message) + if (m_protocolErrors.size()) + sendPendingErrors(); + } ++ return DispatchResult::Finished; + } + + // FIXME: remove this function when legacy InspectorObject symbols are no longer needed . +diff --git a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h +index 95d9d81188e..6f96f174dff 100644 +--- a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h ++++ b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h +@@ -82,7 +82,10 @@ public: + }; + + void registerDispatcherForDomain(const String& domain, SupplementalBackendDispatcher*); +- void dispatch(const String& message); ++ ++ enum class DispatchResult { Finished, Continue }; ++ enum class Mode { FailIfDomainIsMissing, ContinueIfDomainIsMissing }; ++ DispatchResult dispatch(const String& message, Mode mode = Mode::FailIfDomainIsMissing); + + // Note that 'unused' is a workaround so the compiler can pick the right sendResponse based on arity. + // When is fixed or this class is renamed for the JSON::Object case, +diff --git a/Source/JavaScriptCore/inspector/InspectorTarget.h b/Source/JavaScriptCore/inspector/InspectorTarget.h +index a9f04b8a0c9..045b9ecd39a 100644 +--- a/Source/JavaScriptCore/inspector/InspectorTarget.h ++++ b/Source/JavaScriptCore/inspector/InspectorTarget.h +@@ -45,6 +45,7 @@ public: + // State. + virtual String identifier() const = 0; + virtual InspectorTargetType type() const = 0; ++ virtual String url() const = 0; + + virtual bool isProvisional() const { return false; } + +@@ -52,6 +53,8 @@ public: + virtual void connect(FrontendChannel::ConnectionType) = 0; + virtual void disconnect() = 0; + virtual void sendMessageToTargetBackend(const String&) = 0; ++ virtual void activate(String& error) { error = "Target cannot be activated"; } ++ virtual void close(String& error) { error = "Target cannot be closed"; } + }; + + } // namespace Inspector +diff --git a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp +index 1177090fc18..764b62c727c 100644 +--- a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp ++++ b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp +@@ -30,11 +30,12 @@ + + namespace Inspector { + +-InspectorTargetAgent::InspectorTargetAgent(FrontendRouter& frontendRouter, BackendDispatcher& backendDispatcher) ++InspectorTargetAgent::InspectorTargetAgent(FrontendRouter& frontendRouter, BackendDispatcher& backendDispatcher, const String& browserContextID) + : InspectorAgentBase("Target"_s) + , m_router(frontendRouter) + , m_frontendDispatcher(makeUnique(frontendRouter)) + , m_backendDispatcher(TargetBackendDispatcher::create(backendDispatcher, this)) ++ , m_browserContextID(browserContextID) + { + } + +@@ -65,6 +66,28 @@ void InspectorTargetAgent::sendMessageToTarget(ErrorString& errorString, const S + target->sendMessageToTargetBackend(message); + } + ++void InspectorTargetAgent::activate(ErrorString& errorString, const String& targetId) ++{ ++ InspectorTarget* target = m_targets.get(targetId); ++ if (!target) { ++ errorString = "Missing target for given targetId"_s; ++ return; ++ } ++ ++ target->activate(errorString); ++} ++ ++void InspectorTargetAgent::close(ErrorString& errorString, const String& targetId) ++{ ++ InspectorTarget* target = m_targets.get(targetId); ++ if (!target) { ++ errorString = "Missing target for given targetId"_s; ++ return; ++ } ++ ++ target->close(errorString); ++} ++ + void InspectorTargetAgent::sendMessageFromTargetToFrontend(const String& targetId, const String& message) + { + ASSERT_WITH_MESSAGE(m_targets.get(targetId), "Sending a message from an untracked target to the frontend."); +@@ -87,14 +110,17 @@ static Protocol::Target::TargetInfo::Type targetTypeToProtocolType(InspectorTarg + return Protocol::Target::TargetInfo::Type::Page; + } + +-static Ref buildTargetInfoObject(const InspectorTarget& target) ++static Ref buildTargetInfoObject(const InspectorTarget& target, const String& browserContextID) + { + auto result = Protocol::Target::TargetInfo::create() + .setTargetId(target.identifier()) + .setType(targetTypeToProtocolType(target.type())) ++ .setUrl(target.url()) + .release(); + if (target.isProvisional()) + result->setIsProvisional(true); ++ if (!browserContextID.isEmpty()) ++ result->setBrowserContextId(browserContextID); + return result; + } + +@@ -108,7 +134,7 @@ void InspectorTargetAgent::targetCreated(InspectorTarget& target) + + target.connect(connectionType()); + +- m_frontendDispatcher->targetCreated(buildTargetInfoObject(target)); ++ m_frontendDispatcher->targetCreated(buildTargetInfoObject(target, m_browserContextID)); + } + + void InspectorTargetAgent::targetDestroyed(InspectorTarget& target) +@@ -135,6 +161,18 @@ void InspectorTargetAgent::didCommitProvisionalTarget(const String& oldTargetID, + m_frontendDispatcher->didCommitProvisionalTarget(oldTargetID, committedTargetID); + } + ++void InspectorTargetAgent::ensureConnected(const String& targetID) ++{ ++ if (!m_isConnected) ++ return; ++ ++ auto* target = m_targets.get(targetID); ++ if (!target) ++ return; ++ ++ target->connect(connectionType()); ++} ++ + FrontendChannel::ConnectionType InspectorTargetAgent::connectionType() const + { + return m_router.hasLocalFrontend() ? Inspector::FrontendChannel::ConnectionType::Local : Inspector::FrontendChannel::ConnectionType::Remote; +@@ -144,7 +182,7 @@ void InspectorTargetAgent::connectToTargets() + { + for (InspectorTarget* target : m_targets.values()) { + target->connect(connectionType()); +- m_frontendDispatcher->targetCreated(buildTargetInfoObject(*target)); ++ m_frontendDispatcher->targetCreated(buildTargetInfoObject(*target, m_browserContextID)); + } + } + +diff --git a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h +index 38cb318986b..4287e05e559 100644 +--- a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h ++++ b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h +@@ -41,7 +41,7 @@ class JS_EXPORT_PRIVATE InspectorTargetAgent : public InspectorAgentBase, public + WTF_MAKE_NONCOPYABLE(InspectorTargetAgent); + WTF_MAKE_FAST_ALLOCATED; + public: +- InspectorTargetAgent(FrontendRouter&, BackendDispatcher&); ++ InspectorTargetAgent(FrontendRouter&, BackendDispatcher&, const String& browserContextID); + ~InspectorTargetAgent() override; + + // InspectorAgentBase +@@ -50,11 +50,14 @@ public: + + // TargetBackendDispatcherHandler + void sendMessageToTarget(ErrorString&, const String& targetId, const String& message) final; ++ void activate(ErrorString&, const String& targetId) override; ++ void close(ErrorString&, const String& targetId) override; + + // Target lifecycle. + void targetCreated(InspectorTarget&); + void targetDestroyed(InspectorTarget&); + void didCommitProvisionalTarget(const String& oldTargetID, const String& committedTargetID); ++ void ensureConnected(const String& targetID); + + // Target messages. + void sendMessageFromTargetToFrontend(const String& targetId, const String& message); +@@ -68,6 +71,7 @@ private: + Inspector::FrontendRouter& m_router; + std::unique_ptr m_frontendDispatcher; + Ref m_backendDispatcher; ++ const String m_browserContextID; + HashMap m_targets; + bool m_isConnected { false }; + }; +diff --git a/Source/JavaScriptCore/inspector/protocol/Browser.json b/Source/JavaScriptCore/inspector/protocol/Browser.json +new file mode 100644 +index 00000000000..bed4a3bfe6f +--- /dev/null ++++ b/Source/JavaScriptCore/inspector/protocol/Browser.json +@@ -0,0 +1,40 @@ ++{ ++ "domain": "Browser", ++ "availability": ["web"], ++ "types": [ ++ { ++ "id": "ContextID", ++ "type": "string", ++ "description": "Id of Browser context." ++ } ++ ], ++ "commands": [ ++ { ++ "name": "close", ++ "description": "Close browser." ++ }, ++ { ++ "name": "createContext", ++ "description": "Creates new ephemeral browser context.", ++ "returns": [ ++ { "name": "browserContextId", "$ref": "ContextID", "description": "Unique identifier of the context." } ++ ] ++ }, ++ { ++ "name": "deleteContext", ++ "description": "Deletes browser context previously created with createContect. The command will automatically close all pages that use the context.", ++ "parameters": [ ++ { "name": "browserContextId", "$ref": "ContextID", "description": "Identifier of the context to delete." } ++ ] ++ }, ++ { ++ "name": "createPage", ++ "parameters": [ ++ { "name": "browserContextId", "$ref": "ContextID", "optional": true, "description": "JSON Inspector Protocol message (command) to be dispatched on the backend." } ++ ], ++ "returns": [ ++ { "name": "targetId", "type": "string", "description": "Unique identifier for the page target." } ++ ] ++ } ++ ] ++} +diff --git a/Source/JavaScriptCore/inspector/protocol/DOM.json b/Source/JavaScriptCore/inspector/protocol/DOM.json +index 38cb48bedf2..285027ae5d7 100644 +--- a/Source/JavaScriptCore/inspector/protocol/DOM.json ++++ b/Source/JavaScriptCore/inspector/protocol/DOM.json +@@ -542,6 +542,27 @@ + "parameters": [ + { "name": "allow", "type": "boolean" } + ] ++ }, ++ { ++ "name": "getContentQuads", ++ "description": "Returns quads that describe node position on the page. This method\nmight return multiple quads for inline nodes.", ++ "parameters": [ ++ { ++ "name": "objectId", ++ "description": "JavaScript object id of the node wrapper.", ++ "$ref": "Runtime.RemoteObjectId" ++ } ++ ], ++ "returns": [ ++ { ++ "name": "quads", ++ "description": "Quads that describe node layout relative to viewport.", ++ "type": "array", ++ "items": { ++ "$ref": "Quad" ++ } ++ } ++ ] + } + ], + "events": [ +diff --git a/Source/JavaScriptCore/inspector/protocol/Emulation.json b/Source/JavaScriptCore/inspector/protocol/Emulation.json +new file mode 100644 +index 00000000000..168e3f2b93d +--- /dev/null ++++ b/Source/JavaScriptCore/inspector/protocol/Emulation.json +@@ -0,0 +1,14 @@ ++{ ++ "domain": "Emulation", ++ "availability": ["web"], ++ "commands": [ ++ { ++ "name": "setDeviceMetricsOverride", ++ "description": "Overrides device metrics with provided values.", ++ "parameters": [ ++ { "name": "width", "type": "integer" }, ++ { "name": "height", "type": "integer" } ++ ] ++ } ++ ] ++} +diff --git a/Source/JavaScriptCore/inspector/protocol/Input.json b/Source/JavaScriptCore/inspector/protocol/Input.json +new file mode 100644 +index 00000000000..79bbe73b0df +--- /dev/null ++++ b/Source/JavaScriptCore/inspector/protocol/Input.json +@@ -0,0 +1,160 @@ ++{ ++ "domain": "Input", ++ "availability": ["web"], ++ "types": [ ++ { ++ "id": "TimeSinceEpoch", ++ "description": "UTC time in seconds, counted from January 1, 1970.", ++ "type": "number" ++ } ++ ], ++ "commands": [ ++ { ++ "name": "goBack", ++ "description": "FIXME: move this to Page domain." ++ }, ++ { ++ "name": "dispatchKeyEvent", ++ "description": "Dispatches a key event to the page.", ++ "async": true, ++ "parameters": [ ++ { ++ "name": "type", ++ "description": "Type of the key event.", ++ "type": "string", ++ "enum": [ ++ "keyDown", ++ "keyUp" ++ ] ++ }, ++ { ++ "name": "modifiers", ++ "description": "Bit field representing pressed modifier keys. (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "text", ++ "description": "Text as generated by processing a virtual key code with a keyboard layout. Not needed for\nfor `keyUp` and `rawKeyDown` events (default: \"\")", ++ "optional": true, ++ "type": "string" ++ }, ++ { ++ "name": "unmodifiedText", ++ "description": "Text that would have been generated by the keyboard if no modifiers were pressed (except for\nshift). Useful for shortcut (accelerator) key handling (default: \"\").", ++ "optional": true, ++ "type": "string" ++ }, ++ { ++ "name": "code", ++ "description": "Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: \"\").", ++ "optional": true, ++ "type": "string" ++ }, ++ { ++ "name": "key", ++ "description": "Unique DOM defined string value describing the meaning of the key in the context of active\nmodifiers, keyboard layout, etc (e.g., 'AltGr') (default: \"\").", ++ "optional": true, ++ "type": "string" ++ }, ++ { ++ "name": "windowsVirtualKeyCode", ++ "description": "Windows virtual key code (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "nativeVirtualKeyCode", ++ "description": "Native virtual key code (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "autoRepeat", ++ "description": "Whether the event was generated from auto repeat (default: false).", ++ "optional": true, ++ "type": "boolean" ++ }, ++ { ++ "name": "isKeypad", ++ "description": "Whether the event was generated from the keypad (default: false).", ++ "optional": true, ++ "type": "boolean" ++ }, ++ { ++ "name": "isSystemKey", ++ "description": "Whether the event was a system key event (default: false).", ++ "optional": true, ++ "type": "boolean" ++ } ++ ] ++ }, ++ { ++ "name": "dispatchMouseEvent", ++ "description": "Dispatches a mouse event to the page.", ++ "async": true, ++ "parameters": [ ++ { ++ "name": "type", ++ "description": "Type of the mouse event.", ++ "type": "string", ++ "enum": [ "move", "down", "up", "wheel"] ++ }, ++ { ++ "name": "x", ++ "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.", ++ "type": "integer" ++ }, ++ { ++ "name": "y", ++ "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.", ++ "type": "integer" ++ }, ++ { ++ "name": "modifiers", ++ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "button", ++ "description": "Mouse button (default: \"none\").", ++ "optional": true, ++ "type": "string", ++ "enum": [ ++ "none", ++ "left", ++ "middle", ++ "right", ++ "back", ++ "forward" ++ ] ++ }, ++ { ++ "name": "buttons", ++ "description": "A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "clickCount", ++ "description": "Number of times the mouse button was clicked (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "deltaX", ++ "description": "X delta in CSS pixels for mouse wheel event (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "deltaY", ++ "description": "Y delta in CSS pixels for mouse wheel event (default: 0).", ++ "optional": true, ++ "type": "integer" ++ } ++ ] ++ } ++ ] ++} +diff --git a/Source/JavaScriptCore/inspector/protocol/Page.json b/Source/JavaScriptCore/inspector/protocol/Page.json +index 367d1f235a8..62321e6c893 100644 +--- a/Source/JavaScriptCore/inspector/protocol/Page.json ++++ b/Source/JavaScriptCore/inspector/protocol/Page.json +@@ -131,7 +131,8 @@ + "name": "navigate", + "description": "Navigates current page to the given URL.", + "parameters": [ +- { "name": "url", "type": "string", "description": "URL to navigate the page to." } ++ { "name": "url", "type": "string", "description": "URL to navigate the page to." }, ++ { "name": "frameId", "$ref": "Network.FrameId", "optional": true, "description": "Id of the frame to navigate."} + ] + }, + { +@@ -347,6 +348,21 @@ + ] + }, + { ++ "name": "navigatedWithinDocument", ++ "description": "Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.", ++ "parameters": [ ++ { ++ "name": "frameId", ++ "description": "Id of the frame.", ++ "$ref": "Network.FrameId" ++ }, ++ { ++ "name": "url", ++ "description": "Frame's new url.", ++ "type": "string" ++ } ++ ] ++ }, { + "name": "defaultAppearanceDidChange", + "description": "Fired when page's default appearance changes, even if there is a forced appearance.", + "parameters": [ +diff --git a/Source/JavaScriptCore/inspector/protocol/Target.json b/Source/JavaScriptCore/inspector/protocol/Target.json +index 240cd42e67e..f635c67ef3f 100644 +--- a/Source/JavaScriptCore/inspector/protocol/Target.json ++++ b/Source/JavaScriptCore/inspector/protocol/Target.json +@@ -10,7 +10,9 @@ + "properties": [ + { "name": "targetId", "type": "string", "description": "Unique identifier for the target." }, + { "name": "type", "type": "string", "enum": ["page", "service-worker", "worker"] }, +- { "name": "isProvisional", "type": "boolean", "optional": true, "description": "True value indicates that this is a provisional page target i.e. Such target may be created when current page starts cross-origin navigation. Eventually each provisional target is either committed and swaps with the current target or gets destroyed, e.g. in case of load request failure." } ++ { "name": "url", "type": "string" }, ++ { "name": "isProvisional", "type": "boolean", "optional": true, "description": "True value indicates that this is a provisional page target i.e. Such target may be created when current page starts cross-origin navigation. Eventually each provisional target is either committed and swaps with the current target or gets destroyed, e.g. in case of load request failure." }, ++ { "name": "browserContextId", "$ref": "Browser.ContextID", "optional": true } + ] + } + ], +@@ -22,6 +24,20 @@ + { "name": "targetId", "type": "string" }, + { "name": "message", "type": "string", "description": "JSON Inspector Protocol message (command) to be dispatched on the backend." } + ] ++ }, ++ { ++ "name": "activate", ++ "description": "Reveals the target on screen.", ++ "parameters": [ ++ { "name": "targetId", "type": "string" } ++ ] ++ }, ++ { ++ "name": "close", ++ "description": "Closes the target.", ++ "parameters": [ ++ { "name": "targetId", "type": "string" } ++ ] + } + ], + "events": [ +diff --git a/Source/WebCore/inspector/InspectorInstrumentation.cpp b/Source/WebCore/inspector/InspectorInstrumentation.cpp +index 4b7bec1b460..97fb3543d40 100644 +--- a/Source/WebCore/inspector/InspectorInstrumentation.cpp ++++ b/Source/WebCore/inspector/InspectorInstrumentation.cpp +@@ -782,6 +782,12 @@ void InspectorInstrumentation::frameClearedScheduledNavigationImpl(Instrumenting + inspectorPageAgent->frameClearedScheduledNavigation(frame); + } + ++void InspectorInstrumentation::didNavigateWithinPageImpl(InstrumentingAgents& instrumentingAgents, Frame& frame) ++{ ++ if (InspectorPageAgent* inspectorPageAgent = instrumentingAgents.inspectorPageAgent()) ++ inspectorPageAgent->didNavigateWithinPage(frame); ++} ++ + void InspectorInstrumentation::defaultAppearanceDidChangeImpl(InstrumentingAgents& instrumentingAgents, bool useDarkAppearance) + { + if (InspectorPageAgent* inspectorPageAgent = instrumentingAgents.inspectorPageAgent()) +diff --git a/Source/WebCore/inspector/InspectorInstrumentation.h b/Source/WebCore/inspector/InspectorInstrumentation.h +index 6698431f316..40dd67f43e9 100644 +--- a/Source/WebCore/inspector/InspectorInstrumentation.h ++++ b/Source/WebCore/inspector/InspectorInstrumentation.h +@@ -228,6 +228,7 @@ public: + static void frameStoppedLoading(Frame&); + static void frameScheduledNavigation(Frame&, Seconds delay); + static void frameClearedScheduledNavigation(Frame&); ++ static void didNavigateWithinPage(Frame&); + static void defaultAppearanceDidChange(Page&, bool useDarkAppearance); + static void willDestroyCachedResource(CachedResource&); + +@@ -428,6 +429,7 @@ private: + static void frameStoppedLoadingImpl(InstrumentingAgents&, Frame&); + static void frameScheduledNavigationImpl(InstrumentingAgents&, Frame&, Seconds delay); + static void frameClearedScheduledNavigationImpl(InstrumentingAgents&, Frame&); ++ static void didNavigateWithinPageImpl(InstrumentingAgents&, Frame&); + static void defaultAppearanceDidChangeImpl(InstrumentingAgents&, bool useDarkAppearance); + static void willDestroyCachedResourceImpl(CachedResource&); + +@@ -1219,6 +1221,13 @@ inline void InspectorInstrumentation::frameClearedScheduledNavigation(Frame& fra + frameClearedScheduledNavigationImpl(*instrumentingAgents, frame); + } + ++inline void InspectorInstrumentation::didNavigateWithinPage(Frame& frame) ++{ ++ FAST_RETURN_IF_NO_FRONTENDS(void()); ++ if (InstrumentingAgents* instrumentingAgents = instrumentingAgentsForFrame(frame)) ++ didNavigateWithinPageImpl(*instrumentingAgents, frame); ++} ++ + inline void InspectorInstrumentation::defaultAppearanceDidChange(Page& page, bool useDarkAppearance) + { + FAST_RETURN_IF_NO_FRONTENDS(void()); +diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp +index 19bd04b805c..21745502b3e 100644 +--- a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp ++++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp +@@ -61,6 +61,7 @@ + #include "FrameTree.h" + #include "FrameView.h" + #include "FullscreenManager.h" ++#include "FloatQuad.h" + #include "HTMLElement.h" + #include "HTMLFrameOwnerElement.h" + #include "HTMLMediaElement.h" +@@ -89,6 +90,7 @@ + #include "Page.h" + #include "Pasteboard.h" + #include "PseudoElement.h" ++#include "RenderObject.h" + #include "RenderStyle.h" + #include "RenderStyleConstants.h" + #include "ScriptState.h" +@@ -1417,6 +1419,61 @@ void InspectorDOMAgent::setInspectedNode(ErrorString& errorString, int nodeId) + m_suppressEventListenerChangedEvent = false; + } + ++static void frameQuadToViewport(const FrameView* containingView, FloatQuad& quad) ++{ ++ quad.setP1(containingView->contentsToRootView(quad.p1())); ++ quad.setP2(containingView->contentsToRootView(quad.p2())); ++ quad.setP3(containingView->contentsToRootView(quad.p3())); ++ quad.setP4(containingView->contentsToRootView(quad.p4())); ++} ++ ++static RefPtr buildObjectForQuad(const FloatQuad& quad) ++{ ++ auto result = Inspector::Protocol::DOM::Quad::create(); ++ result->addItem(quad.p1().x()); ++ result->addItem(quad.p1().y()); ++ result->addItem(quad.p2().x()); ++ result->addItem(quad.p2().y()); ++ result->addItem(quad.p3().x()); ++ result->addItem(quad.p3().y()); ++ result->addItem(quad.p4().x()); ++ result->addItem(quad.p4().y()); ++ return result; ++} ++ ++static RefPtr> buildArrayOfQuads(const Vector& quads) ++{ ++ auto result = JSON::ArrayOf::create(); ++ for (const auto& quad : quads) ++ result->addItem(buildObjectForQuad(quad)); ++ return result; ++} ++ ++void InspectorDOMAgent::getContentQuads(ErrorString& error, const String& objectId, RefPtr>& out_quads) ++{ ++ Node* node = nodeForObjectId(objectId); ++ if (!node) { ++ error = "Node not found"; ++ return; ++ } ++ RenderObject* renderer = node->renderer(); ++ if (!renderer) { ++ error = "Node doesn't have renderer"; ++ return; ++ } ++ Frame* containingFrame = renderer->document().frame(); ++ if (!containingFrame) { ++ error = "No containing frame"; ++ return; ++ } ++ FrameView* containingView = containingFrame->view(); ++ Vector quads; ++ renderer->absoluteQuads(quads); ++ for (auto& quad : quads) ++ frameQuadToViewport(containingView, quad); ++ out_quads = buildArrayOfQuads(quads); ++} ++ + void InspectorDOMAgent::resolveNode(ErrorString& errorString, int nodeId, const String* objectGroup, RefPtr& result) + { + String objectGroupName = objectGroup ? *objectGroup : emptyString(); +diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.h b/Source/WebCore/inspector/agents/InspectorDOMAgent.h +index 51639abeb84..fbb7773978d 100644 +--- a/Source/WebCore/inspector/agents/InspectorDOMAgent.h ++++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.h +@@ -148,6 +148,7 @@ public: + void focus(ErrorString&, int nodeId) override; + void setInspectedNode(ErrorString&, int nodeId) override; + void setAllowEditingUserAgentShadowTrees(ErrorString&, bool allow) final; ++ void getContentQuads(ErrorString&, const String& objectId, RefPtr>& out_quads) override; + + // InspectorInstrumentation + int identifierForNode(Node&); +diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp +index f2e228b7f74..77ccbe29ea3 100644 +--- a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp ++++ b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp +@@ -412,14 +412,16 @@ void InspectorPageAgent::reload(ErrorString&, const bool* optionalReloadFromOrig + m_inspectedPage.mainFrame().loader().reload(reloadOptions); + } + +-void InspectorPageAgent::navigate(ErrorString&, const String& url) ++void InspectorPageAgent::navigate(ErrorString& errorString, const String& url, const String* frameId) + { + UserGestureIndicator indicator { ProcessingUserGesture }; +- Frame& frame = m_inspectedPage.mainFrame(); ++ Frame* frame = frameId ? assertFrame(errorString, *frameId) : &m_inspectedPage.mainFrame(); ++ if (!frame) ++ return; + +- ResourceRequest resourceRequest { frame.document()->completeURL(url) }; +- FrameLoadRequest frameLoadRequest { *frame.document(), frame.document()->securityOrigin(), resourceRequest, "_self"_s, LockHistory::No, LockBackForwardList::No, MaybeSendReferrer, AllowNavigationToInvalidURL::No, NewFrameOpenerPolicy::Allow, ShouldOpenExternalURLsPolicy::ShouldNotAllow, InitiatedByMainFrame::Unknown }; +- frame.loader().changeLocation(WTFMove(frameLoadRequest)); ++ ResourceRequest resourceRequest { frame->document()->completeURL(url) }; ++ FrameLoadRequest frameLoadRequest { *frame->document(), frame->document()->securityOrigin(), resourceRequest, "_self"_s, LockHistory::No, LockBackForwardList::No, MaybeSendReferrer, AllowNavigationToInvalidURL::No, NewFrameOpenerPolicy::Allow, ShouldOpenExternalURLsPolicy::ShouldNotAllow, InitiatedByMainFrame::Unknown }; ++ frame->loader().changeLocation(WTFMove(frameLoadRequest)); + } + + void InspectorPageAgent::overrideUserAgent(ErrorString&, const String* value) +@@ -761,6 +763,12 @@ void InspectorPageAgent::frameClearedScheduledNavigation(Frame& frame) + m_frontendDispatcher->frameClearedScheduledNavigation(frameId(&frame)); + } + ++void InspectorPageAgent::didNavigateWithinPage(Frame& frame) ++{ ++ String url = frame.document()->url().string(); ++ m_frontendDispatcher->navigatedWithinDocument(frameId(&frame), url); ++} ++ + void InspectorPageAgent::defaultAppearanceDidChange(bool useDarkAppearance) + { + m_frontendDispatcher->defaultAppearanceDidChange(useDarkAppearance ? Inspector::Protocol::Page::Appearance::Dark : Inspector::Protocol::Page::Appearance::Light); +diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.h b/Source/WebCore/inspector/agents/InspectorPageAgent.h +index 4fd8c0b1016..78af692dc09 100644 +--- a/Source/WebCore/inspector/agents/InspectorPageAgent.h ++++ b/Source/WebCore/inspector/agents/InspectorPageAgent.h +@@ -96,7 +96,7 @@ public: + void enable(ErrorString&) override; + void disable(ErrorString&) override; + void reload(ErrorString&, const bool* optionalReloadFromOrigin, const bool* optionalRevalidateAllResources) override; +- void navigate(ErrorString&, const String& url) override; ++ void navigate(ErrorString&, const String& url, const String* frameId) override; + void overrideUserAgent(ErrorString&, const String* value) override; + void overrideSetting(ErrorString&, const String& setting, const bool* value) override; + void getCookies(ErrorString&, RefPtr>& cookies) override; +@@ -126,6 +126,7 @@ public: + void frameStoppedLoading(Frame&); + void frameScheduledNavigation(Frame&, Seconds delay); + void frameClearedScheduledNavigation(Frame&); ++ void didNavigateWithinPage(Frame&); + void defaultAppearanceDidChange(bool useDarkAppearance); + void applyUserAgentOverride(String&); + void applyEmulatedMedia(String&); +diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp +index 26246f7deb6..cf215bed32b 100644 +--- a/Source/WebCore/loader/FrameLoader.cpp ++++ b/Source/WebCore/loader/FrameLoader.cpp +@@ -1179,6 +1179,7 @@ void FrameLoader::loadInSameDocument(const URL& url, SerializedScriptValue* stat + } + + m_client.dispatchDidNavigateWithinPage(); ++ InspectorInstrumentation::didNavigateWithinPage(m_frame); + + m_frame.document()->statePopped(stateObject ? Ref { *stateObject } : SerializedScriptValue::nullValue()); + m_client.dispatchDidPopStateWithinPage(); +diff --git a/Source/WebCore/page/History.cpp b/Source/WebCore/page/History.cpp +index 9c58b06f4c4..3d624733c36 100644 +--- a/Source/WebCore/page/History.cpp ++++ b/Source/WebCore/page/History.cpp +@@ -259,6 +259,7 @@ ExceptionOr History::stateObjectAdded(RefPtr&& data + + if (!urlString.isEmpty()) + frame->document()->updateURLForPushOrReplaceState(fullURL); ++ InspectorInstrumentation::didNavigateWithinPage(*frame); + + if (stateObjectType == StateObjectType::Push) { + frame->loader().history().pushState(WTFMove(data), title, fullURL.string()); +diff --git a/Source/WebCore/platform/PlatformKeyboardEvent.h b/Source/WebCore/platform/PlatformKeyboardEvent.h +index 4aaf5bde32b..e9c047d4f26 100644 +--- a/Source/WebCore/platform/PlatformKeyboardEvent.h ++++ b/Source/WebCore/platform/PlatformKeyboardEvent.h +@@ -147,6 +147,7 @@ namespace WebCore { + static String keyCodeForHardwareKeyCode(unsigned); + static String keyIdentifierForGdkKeyCode(unsigned); + static int windowsKeyCodeForGdkKeyCode(unsigned); ++ static unsigned gdkKeyCodeForWindowsKeyCode(int); + static String singleCharacterString(unsigned); + static bool modifiersContainCapsLock(unsigned); + #endif +@@ -156,6 +157,7 @@ namespace WebCore { + static String keyCodeForHardwareKeyCode(unsigned); + static String keyIdentifierForWPEKeyCode(unsigned); + static int windowsKeyCodeForWPEKeyCode(unsigned); ++ static unsigned WPEKeyCodeForWindowsKeyCode(int); + static String singleCharacterString(unsigned); + #endif + +diff --git a/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp b/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp +index 356b09f2fba..8f0c19b6031 100644 +--- a/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp ++++ b/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp +@@ -36,8 +36,10 @@ + #include "WindowsKeyboardCodes.h" + #include + #include ++#include + #include + #include ++#include + + namespace WebCore { + +@@ -1293,6 +1295,246 @@ int PlatformKeyboardEvent::windowsKeyCodeForGdkKeyCode(unsigned keycode) + + } + ++static const HashMap& gdkToWindowsKeyCodeMap() ++{ ++ static HashMap* result; ++ static std::once_flag once; ++ std::call_once( ++ once, ++ [] { ++ const unsigned gdkKeyCodes[] = { ++ GDK_KEY_Cancel, ++ // FIXME: non-keypad keys should take precedence, so we skip GDK_KEY_KP_* ++ // GDK_KEY_KP_0, ++ // GDK_KEY_KP_1, ++ // GDK_KEY_KP_2, ++ // GDK_KEY_KP_3, ++ // GDK_KEY_KP_4, ++ // GDK_KEY_KP_5, ++ // GDK_KEY_KP_6, ++ // GDK_KEY_KP_7, ++ // GDK_KEY_KP_8, ++ // GDK_KEY_KP_9, ++ // GDK_KEY_KP_Multiply, ++ // GDK_KEY_KP_Add, ++ // GDK_KEY_KP_Subtract, ++ // GDK_KEY_KP_Decimal, ++ // GDK_KEY_KP_Divide, ++ // GDK_KEY_KP_Page_Up, ++ // GDK_KEY_KP_Page_Down, ++ // GDK_KEY_KP_End, ++ // GDK_KEY_KP_Home, ++ // GDK_KEY_KP_Left, ++ // GDK_KEY_KP_Up, ++ // GDK_KEY_KP_Right, ++ // GDK_KEY_KP_Down, ++ GDK_KEY_BackSpace, ++ // GDK_KEY_ISO_Left_Tab, ++ // GDK_KEY_3270_BackTab, ++ GDK_KEY_Tab, ++ GDK_KEY_Clear, ++ // GDK_KEY_ISO_Enter, ++ // GDK_KEY_KP_Enter, ++ GDK_KEY_Return, ++ GDK_KEY_Menu, ++ GDK_KEY_Pause, ++ GDK_KEY_AudioPause, ++ GDK_KEY_Caps_Lock, ++ GDK_KEY_Kana_Lock, ++ GDK_KEY_Kana_Shift, ++ GDK_KEY_Hangul, ++ GDK_KEY_Hangul_Hanja, ++ GDK_KEY_Kanji, ++ GDK_KEY_Escape, ++ GDK_KEY_space, ++ GDK_KEY_Page_Up, ++ GDK_KEY_Page_Down, ++ GDK_KEY_End, ++ GDK_KEY_Home, ++ GDK_KEY_Left, ++ GDK_KEY_Up, ++ GDK_KEY_Right, ++ GDK_KEY_Down, ++ GDK_KEY_Select, ++ GDK_KEY_Print, ++ GDK_KEY_Execute, ++ GDK_KEY_Insert, ++ GDK_KEY_KP_Insert, ++ GDK_KEY_Delete, ++ GDK_KEY_KP_Delete, ++ GDK_KEY_Help, ++ GDK_KEY_0, ++ GDK_KEY_parenright, ++ GDK_KEY_1, ++ GDK_KEY_exclam, ++ GDK_KEY_2, ++ GDK_KEY_at, ++ GDK_KEY_3, ++ GDK_KEY_numbersign, ++ GDK_KEY_4, ++ GDK_KEY_dollar, ++ GDK_KEY_5, ++ GDK_KEY_percent, ++ GDK_KEY_6, ++ GDK_KEY_asciicircum, ++ GDK_KEY_7, ++ GDK_KEY_ampersand, ++ GDK_KEY_8, ++ GDK_KEY_asterisk, ++ GDK_KEY_9, ++ GDK_KEY_parenleft, ++ GDK_KEY_a, ++ GDK_KEY_A, ++ GDK_KEY_b, ++ GDK_KEY_B, ++ GDK_KEY_c, ++ GDK_KEY_C, ++ GDK_KEY_d, ++ GDK_KEY_D, ++ GDK_KEY_e, ++ GDK_KEY_E, ++ GDK_KEY_f, ++ GDK_KEY_F, ++ GDK_KEY_g, ++ GDK_KEY_G, ++ GDK_KEY_h, ++ GDK_KEY_H, ++ GDK_KEY_i, ++ GDK_KEY_I, ++ GDK_KEY_j, ++ GDK_KEY_J, ++ GDK_KEY_k, ++ GDK_KEY_K, ++ GDK_KEY_l, ++ GDK_KEY_L, ++ GDK_KEY_m, ++ GDK_KEY_M, ++ GDK_KEY_n, ++ GDK_KEY_N, ++ GDK_KEY_o, ++ GDK_KEY_O, ++ GDK_KEY_p, ++ GDK_KEY_P, ++ GDK_KEY_q, ++ GDK_KEY_Q, ++ GDK_KEY_r, ++ GDK_KEY_R, ++ GDK_KEY_s, ++ GDK_KEY_S, ++ GDK_KEY_t, ++ GDK_KEY_T, ++ GDK_KEY_u, ++ GDK_KEY_U, ++ GDK_KEY_v, ++ GDK_KEY_V, ++ GDK_KEY_w, ++ GDK_KEY_W, ++ GDK_KEY_x, ++ GDK_KEY_X, ++ GDK_KEY_y, ++ GDK_KEY_Y, ++ GDK_KEY_z, ++ GDK_KEY_Z, ++ GDK_KEY_Meta_L, ++ GDK_KEY_Meta_R, ++ GDK_KEY_Sleep, ++ GDK_KEY_Num_Lock, ++ GDK_KEY_Scroll_Lock, ++ GDK_KEY_Shift_L, ++ GDK_KEY_Shift_R, ++ GDK_KEY_Control_L, ++ GDK_KEY_Control_R, ++ GDK_KEY_Alt_L, ++ GDK_KEY_Alt_R, ++ GDK_KEY_Back, ++ GDK_KEY_Forward, ++ GDK_KEY_Refresh, ++ GDK_KEY_Stop, ++ GDK_KEY_Search, ++ GDK_KEY_Favorites, ++ GDK_KEY_HomePage, ++ GDK_KEY_AudioMute, ++ GDK_KEY_AudioLowerVolume, ++ GDK_KEY_AudioRaiseVolume, ++ GDK_KEY_AudioNext, ++ GDK_KEY_AudioPrev, ++ GDK_KEY_AudioStop, ++ GDK_KEY_AudioMedia, ++ GDK_KEY_semicolon, ++ GDK_KEY_colon, ++ GDK_KEY_plus, ++ GDK_KEY_equal, ++ GDK_KEY_comma, ++ GDK_KEY_less, ++ GDK_KEY_minus, ++ GDK_KEY_underscore, ++ GDK_KEY_period, ++ GDK_KEY_greater, ++ GDK_KEY_slash, ++ GDK_KEY_question, ++ GDK_KEY_asciitilde, ++ GDK_KEY_quoteleft, ++ GDK_KEY_bracketleft, ++ GDK_KEY_braceleft, ++ GDK_KEY_backslash, ++ GDK_KEY_bar, ++ GDK_KEY_bracketright, ++ GDK_KEY_braceright, ++ GDK_KEY_quoteright, ++ GDK_KEY_quotedbl, ++ GDK_KEY_AudioRewind, ++ GDK_KEY_AudioForward, ++ GDK_KEY_AudioPlay, ++ GDK_KEY_F1, ++ GDK_KEY_F2, ++ GDK_KEY_F3, ++ GDK_KEY_F4, ++ GDK_KEY_F5, ++ GDK_KEY_F6, ++ GDK_KEY_F7, ++ GDK_KEY_F8, ++ GDK_KEY_F9, ++ GDK_KEY_F10, ++ GDK_KEY_F11, ++ GDK_KEY_F12, ++ GDK_KEY_F13, ++ GDK_KEY_F14, ++ GDK_KEY_F15, ++ GDK_KEY_F16, ++ GDK_KEY_F17, ++ GDK_KEY_F18, ++ GDK_KEY_F19, ++ GDK_KEY_F20, ++ GDK_KEY_F21, ++ GDK_KEY_F22, ++ GDK_KEY_F23, ++ GDK_KEY_F24, ++ GDK_KEY_VoidSymbol, ++ GDK_KEY_Red, ++ GDK_KEY_Green, ++ GDK_KEY_Yellow, ++ GDK_KEY_Blue, ++ GDK_KEY_PowerOff, ++ GDK_KEY_AudioRecord, ++ GDK_KEY_Display, ++ GDK_KEY_Subtitle, ++ GDK_KEY_Video ++ }; ++ result = new HashMap(); ++ for (unsigned gdkKeyCode : gdkKeyCodes) { ++ int winKeyCode = PlatformKeyboardEvent::windowsKeyCodeForGdkKeyCode(gdkKeyCode); ++ // If several gdk key codes map to the same win key code first one is used. ++ result->add(winKeyCode, gdkKeyCode); ++ } ++ }); ++ return *result; ++} ++ ++unsigned PlatformKeyboardEvent::gdkKeyCodeForWindowsKeyCode(int keycode) ++{ ++ return gdkToWindowsKeyCodeMap().get(keycode); ++} ++ + String PlatformKeyboardEvent::singleCharacterString(unsigned val) + { + switch (val) { +diff --git a/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp b/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp +index cf46da15083..efbda20f28b 100644 +--- a/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp ++++ b/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp +@@ -1291,6 +1291,246 @@ int PlatformKeyboardEvent::windowsKeyCodeForWPEKeyCode(unsigned keycode) + return 0; + } + ++static const HashMap& WPEToWindowsKeyCodeMap() ++{ ++ static HashMap* result; ++ static std::once_flag once; ++ std::call_once( ++ once, ++ [] { ++ const unsigned WPEKeyCodes[] = { ++ WPE_KEY_Cancel, ++ // FIXME: non-keypad keys should take precedence, so we skip WPE_KEY_KP_* ++ // WPE_KEY_KP_0, ++ // WPE_KEY_KP_1, ++ // WPE_KEY_KP_2, ++ // WPE_KEY_KP_3, ++ // WPE_KEY_KP_4, ++ // WPE_KEY_KP_5, ++ // WPE_KEY_KP_6, ++ // WPE_KEY_KP_7, ++ // WPE_KEY_KP_8, ++ // WPE_KEY_KP_9, ++ // WPE_KEY_KP_Multiply, ++ // WPE_KEY_KP_Add, ++ // WPE_KEY_KP_Subtract, ++ // WPE_KEY_KP_Decimal, ++ // WPE_KEY_KP_Divide, ++ // WPE_KEY_KP_Page_Up, ++ // WPE_KEY_KP_Page_Down, ++ // WPE_KEY_KP_End, ++ // WPE_KEY_KP_Home, ++ // WPE_KEY_KP_Left, ++ // WPE_KEY_KP_Up, ++ // WPE_KEY_KP_Right, ++ // WPE_KEY_KP_Down, ++ WPE_KEY_BackSpace, ++ // WPE_KEY_ISO_Left_Tab, ++ // WPE_KEY_3270_BackTab, ++ WPE_KEY_Tab, ++ WPE_KEY_Clear, ++ // WPE_KEY_ISO_Enter, ++ // WPE_KEY_KP_Enter, ++ WPE_KEY_Return, ++ WPE_KEY_Menu, ++ WPE_KEY_Pause, ++ WPE_KEY_AudioPause, ++ WPE_KEY_Caps_Lock, ++ WPE_KEY_Kana_Lock, ++ WPE_KEY_Kana_Shift, ++ WPE_KEY_Hangul, ++ WPE_KEY_Hangul_Hanja, ++ WPE_KEY_Kanji, ++ WPE_KEY_Escape, ++ WPE_KEY_space, ++ WPE_KEY_Page_Up, ++ WPE_KEY_Page_Down, ++ WPE_KEY_End, ++ WPE_KEY_Home, ++ WPE_KEY_Left, ++ WPE_KEY_Up, ++ WPE_KEY_Right, ++ WPE_KEY_Down, ++ WPE_KEY_Select, ++ WPE_KEY_Print, ++ WPE_KEY_Execute, ++ WPE_KEY_Insert, ++ WPE_KEY_KP_Insert, ++ WPE_KEY_Delete, ++ WPE_KEY_KP_Delete, ++ WPE_KEY_Help, ++ WPE_KEY_0, ++ WPE_KEY_parenright, ++ WPE_KEY_1, ++ WPE_KEY_exclam, ++ WPE_KEY_2, ++ WPE_KEY_at, ++ WPE_KEY_3, ++ WPE_KEY_numbersign, ++ WPE_KEY_4, ++ WPE_KEY_dollar, ++ WPE_KEY_5, ++ WPE_KEY_percent, ++ WPE_KEY_6, ++ WPE_KEY_asciicircum, ++ WPE_KEY_7, ++ WPE_KEY_ampersand, ++ WPE_KEY_8, ++ WPE_KEY_asterisk, ++ WPE_KEY_9, ++ WPE_KEY_parenleft, ++ WPE_KEY_a, ++ WPE_KEY_A, ++ WPE_KEY_b, ++ WPE_KEY_B, ++ WPE_KEY_c, ++ WPE_KEY_C, ++ WPE_KEY_d, ++ WPE_KEY_D, ++ WPE_KEY_e, ++ WPE_KEY_E, ++ WPE_KEY_f, ++ WPE_KEY_F, ++ WPE_KEY_g, ++ WPE_KEY_G, ++ WPE_KEY_h, ++ WPE_KEY_H, ++ WPE_KEY_i, ++ WPE_KEY_I, ++ WPE_KEY_j, ++ WPE_KEY_J, ++ WPE_KEY_k, ++ WPE_KEY_K, ++ WPE_KEY_l, ++ WPE_KEY_L, ++ WPE_KEY_m, ++ WPE_KEY_M, ++ WPE_KEY_n, ++ WPE_KEY_N, ++ WPE_KEY_o, ++ WPE_KEY_O, ++ WPE_KEY_p, ++ WPE_KEY_P, ++ WPE_KEY_q, ++ WPE_KEY_Q, ++ WPE_KEY_r, ++ WPE_KEY_R, ++ WPE_KEY_s, ++ WPE_KEY_S, ++ WPE_KEY_t, ++ WPE_KEY_T, ++ WPE_KEY_u, ++ WPE_KEY_U, ++ WPE_KEY_v, ++ WPE_KEY_V, ++ WPE_KEY_w, ++ WPE_KEY_W, ++ WPE_KEY_x, ++ WPE_KEY_X, ++ WPE_KEY_y, ++ WPE_KEY_Y, ++ WPE_KEY_z, ++ WPE_KEY_Z, ++ WPE_KEY_Meta_L, ++ WPE_KEY_Meta_R, ++ WPE_KEY_Sleep, ++ WPE_KEY_Num_Lock, ++ WPE_KEY_Scroll_Lock, ++ WPE_KEY_Shift_L, ++ WPE_KEY_Shift_R, ++ WPE_KEY_Control_L, ++ WPE_KEY_Control_R, ++ WPE_KEY_Alt_L, ++ WPE_KEY_Alt_R, ++ WPE_KEY_Back, ++ WPE_KEY_Forward, ++ WPE_KEY_Refresh, ++ WPE_KEY_Stop, ++ WPE_KEY_Search, ++ WPE_KEY_Favorites, ++ WPE_KEY_HomePage, ++ WPE_KEY_AudioMute, ++ WPE_KEY_AudioLowerVolume, ++ WPE_KEY_AudioRaiseVolume, ++ WPE_KEY_AudioNext, ++ WPE_KEY_AudioPrev, ++ WPE_KEY_AudioStop, ++ WPE_KEY_AudioMedia, ++ WPE_KEY_semicolon, ++ WPE_KEY_colon, ++ WPE_KEY_plus, ++ WPE_KEY_equal, ++ WPE_KEY_comma, ++ WPE_KEY_less, ++ WPE_KEY_minus, ++ WPE_KEY_underscore, ++ WPE_KEY_period, ++ WPE_KEY_greater, ++ WPE_KEY_slash, ++ WPE_KEY_question, ++ WPE_KEY_asciitilde, ++ WPE_KEY_quoteleft, ++ WPE_KEY_bracketleft, ++ WPE_KEY_braceleft, ++ WPE_KEY_backslash, ++ WPE_KEY_bar, ++ WPE_KEY_bracketright, ++ WPE_KEY_braceright, ++ WPE_KEY_quoteright, ++ WPE_KEY_quotedbl, ++ WPE_KEY_AudioRewind, ++ WPE_KEY_AudioForward, ++ WPE_KEY_AudioPlay, ++ WPE_KEY_F1, ++ WPE_KEY_F2, ++ WPE_KEY_F3, ++ WPE_KEY_F4, ++ WPE_KEY_F5, ++ WPE_KEY_F6, ++ WPE_KEY_F7, ++ WPE_KEY_F8, ++ WPE_KEY_F9, ++ WPE_KEY_F10, ++ WPE_KEY_F11, ++ WPE_KEY_F12, ++ WPE_KEY_F13, ++ WPE_KEY_F14, ++ WPE_KEY_F15, ++ WPE_KEY_F16, ++ WPE_KEY_F17, ++ WPE_KEY_F18, ++ WPE_KEY_F19, ++ WPE_KEY_F20, ++ WPE_KEY_F21, ++ WPE_KEY_F22, ++ WPE_KEY_F23, ++ WPE_KEY_F24, ++ WPE_KEY_VoidSymbol, ++ WPE_KEY_Red, ++ WPE_KEY_Green, ++ WPE_KEY_Yellow, ++ WPE_KEY_Blue, ++ WPE_KEY_PowerOff, ++ WPE_KEY_AudioRecord, ++ WPE_KEY_Display, ++ WPE_KEY_Subtitle, ++ WPE_KEY_Video ++ }; ++ result = new HashMap(); ++ for (unsigned WPEKeyCode : WPEKeyCodes) { ++ int winKeyCode = PlatformKeyboardEvent::windowsKeyCodeForWPEKeyCode(WPEKeyCode); ++ // If several gdk key codes map to the same win key code first one is used. ++ result->add(winKeyCode, WPEKeyCode); ++ } ++ }); ++ return *result; ++} ++ ++unsigned PlatformKeyboardEvent::WPEKeyCodeForWindowsKeyCode(int keycode) ++{ ++ return WPEToWindowsKeyCodeMap().get(keycode); ++} ++ + String PlatformKeyboardEvent::singleCharacterString(unsigned val) + { + switch (val) { +diff --git a/Source/WebKit/Shared/API/c/wpe/WebKit.h b/Source/WebKit/Shared/API/c/wpe/WebKit.h +index 898e30b370d..74945e06fac 100644 +--- a/Source/WebKit/Shared/API/c/wpe/WebKit.h ++++ b/Source/WebKit/Shared/API/c/wpe/WebKit.h +@@ -78,6 +78,7 @@ + // From Source/WebKit/UIProcess/API/C + #include + #include ++#include + #include + #include + #include +diff --git a/Source/WebKit/Shared/NativeWebKeyboardEvent.h b/Source/WebKit/Shared/NativeWebKeyboardEvent.h +index 6f4e29b7c65..9dd287efc40 100644 +--- a/Source/WebKit/Shared/NativeWebKeyboardEvent.h ++++ b/Source/WebKit/Shared/NativeWebKeyboardEvent.h +@@ -34,6 +34,7 @@ + #if USE(APPKIT) + #include + OBJC_CLASS NSView; ++OBJC_CLASS NSEvent; + + namespace WebCore { + struct KeypressCommand; +@@ -70,6 +71,10 @@ public: + enum class HandledByInputMethod : bool { No, Yes }; + enum class FakedForComposition : bool { No, Yes }; + NativeWebKeyboardEvent(GdkEvent*, const String&, HandledByInputMethod, FakedForComposition, Vector&& commands); ++ NativeWebKeyboardEvent(Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp, Vector&& commands) ++ : WebKeyboardEvent(type, text, unmodifiedText, key, code, keyIdentifier, windowsVirtualKeyCode, nativeVirtualKeyCode, isAutoRepeat, isKeypad, isSystemKey, modifiers, timestamp, WTFMove(commands)) ++ { ++ } + #elif PLATFORM(IOS_FAMILY) + enum class HandledByInputMethod : bool { No, Yes }; + NativeWebKeyboardEvent(::WebEvent *, HandledByInputMethod); +diff --git a/Source/WebKit/Shared/NativeWebMouseEvent.h b/Source/WebKit/Shared/NativeWebMouseEvent.h +index ba7f93c924f..d19eb2f2932 100644 +--- a/Source/WebKit/Shared/NativeWebMouseEvent.h ++++ b/Source/WebKit/Shared/NativeWebMouseEvent.h +@@ -61,6 +61,10 @@ public: + #elif PLATFORM(GTK) + NativeWebMouseEvent(const NativeWebMouseEvent&); + NativeWebMouseEvent(GdkEvent*, int); ++ NativeWebMouseEvent(Type type, Button button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet modifiers, WallTime timestamp) ++ : WebMouseEvent(type, button, buttons, position, globalPosition, deltaX, deltaY, deltaZ, clickCount, modifiers, timestamp) ++ { ++ } + #elif PLATFORM(IOS_FAMILY) + NativeWebMouseEvent(::WebEvent *); + NativeWebMouseEvent(Type, Button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet, WallTime timestamp, double force); +diff --git a/Source/WebKit/Shared/WebEvent.h b/Source/WebKit/Shared/WebEvent.h +index c36100cf5c4..216402f0a24 100644 +--- a/Source/WebKit/Shared/WebEvent.h ++++ b/Source/WebKit/Shared/WebEvent.h +@@ -35,6 +35,7 @@ + #include + #include + #include ++#include + #include + #include + +@@ -138,7 +139,7 @@ public: + WebMouseEvent(); + + #if PLATFORM(MAC) +- WebMouseEvent(Type, Button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet, WallTime timestamp, double force, SyntheticClickType = NoTap, int eventNumber = -1, int menuType = 0); ++ WebMouseEvent(Type, Button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet, WallTime timestamp, double force = 0, SyntheticClickType = NoTap, int eventNumber = -1, int menuType = 0); + #else + WebMouseEvent(Type, Button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet, WallTime timestamp, double force = 0, SyntheticClickType = NoTap); + #endif +@@ -258,6 +259,7 @@ public: + WebKeyboardEvent(Type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, int macCharCode, bool handledByInputMethod, const Vector&, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet, WallTime timestamp); + #elif PLATFORM(GTK) + WebKeyboardEvent(Type, const String& text, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool handledByInputMethod, Vector&& commands, bool isKeypad, OptionSet, WallTime timestamp); ++ WebKeyboardEvent(Type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet, WallTime timestamp, Vector&& commands); + #elif PLATFORM(IOS_FAMILY) + WebKeyboardEvent(Type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, int macCharCode, bool handledByInputMethod, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet, WallTime timestamp); + #elif USE(LIBWPE) +@@ -309,7 +311,7 @@ private: + int32_t m_nativeVirtualKeyCode; + int32_t m_macCharCode; + #if USE(APPKIT) || USE(UIKIT_KEYBOARD_ADDITIONS) || PLATFORM(GTK) +- bool m_handledByInputMethod; ++ bool m_handledByInputMethod = false; + #endif + #if USE(APPKIT) + Vector m_commands; +diff --git a/Source/WebKit/Shared/WebKeyboardEvent.cpp b/Source/WebKit/Shared/WebKeyboardEvent.cpp +index a5a23cf148e..390eaf847b6 100644 +--- a/Source/WebKit/Shared/WebKeyboardEvent.cpp ++++ b/Source/WebKit/Shared/WebKeyboardEvent.cpp +@@ -81,6 +81,28 @@ WebKeyboardEvent::WebKeyboardEvent(Type type, const String& text, const String& + ASSERT(isKeyboardEventType(type)); + } + ++WebKeyboardEvent::WebKeyboardEvent(Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp, Vector&& commands) ++ : WebEvent(type, modifiers, timestamp) ++ , m_text(text) ++ , m_unmodifiedText(text) ++#if ENABLE(KEYBOARD_KEY_ATTRIBUTE) ++ , m_key(key) ++#endif ++#if ENABLE(KEYBOARD_CODE_ATTRIBUTE) ++ , m_code(code) ++#endif ++ , m_keyIdentifier(keyIdentifier) ++ , m_windowsVirtualKeyCode(windowsVirtualKeyCode) ++ , m_nativeVirtualKeyCode(nativeVirtualKeyCode) ++ , m_macCharCode(0) ++ , m_commands(WTFMove(commands)) ++ , m_isAutoRepeat(isAutoRepeat) ++ , m_isKeypad(isKeypad) ++ , m_isSystemKey(isSystemKey) ++{ ++ ASSERT(isKeyboardEventType(type)); ++} ++ + #elif PLATFORM(IOS_FAMILY) + + WebKeyboardEvent::WebKeyboardEvent(Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, int macCharCode, bool handledByInputMethod, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp) +diff --git a/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp b/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp +index 45aa449644b..3a6b4169194 100644 +--- a/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp ++++ b/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp +@@ -43,7 +43,7 @@ NativeWebKeyboardEvent::NativeWebKeyboardEvent(GdkEvent* event, const String& te + } + + NativeWebKeyboardEvent::NativeWebKeyboardEvent(const NativeWebKeyboardEvent& event) +- : WebKeyboardEvent(WebEventFactory::createWebKeyboardEvent(event.nativeEvent(), event.text(), event.handledByInputMethod(), Vector(event.commands()))) ++ : WebKeyboardEvent(event) + , m_nativeEvent(gdk_event_copy(event.nativeEvent())) + , m_text(event.text()) + , m_handledByInputMethod(event.handledByInputMethod() ? HandledByInputMethod::Yes : HandledByInputMethod::No) +diff --git a/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp b/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp +index 28d00f0e6fd..23a013e754e 100644 +--- a/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp ++++ b/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp +@@ -38,8 +38,8 @@ NativeWebMouseEvent::NativeWebMouseEvent(GdkEvent* event, int eventClickCount) + } + + NativeWebMouseEvent::NativeWebMouseEvent(const NativeWebMouseEvent& event) +- : WebMouseEvent(WebEventFactory::createWebMouseEvent(event.nativeEvent(), event.clickCount())) +- , m_nativeEvent(gdk_event_copy(event.nativeEvent())) ++ : WebMouseEvent(event) ++ , m_nativeEvent(event.nativeEvent() ? gdk_event_copy(event.nativeEvent()) : nullptr) + { + } + +diff --git a/Source/WebKit/Sources.txt b/Source/WebKit/Sources.txt +index edbfb56e49b..623f393da74 100644 +--- a/Source/WebKit/Sources.txt ++++ b/Source/WebKit/Sources.txt +@@ -240,17 +240,22 @@ Shared/WebsiteData/WebsiteData.cpp + + UIProcess/AuxiliaryProcessProxy.cpp + UIProcess/BackgroundProcessResponsivenessTimer.cpp ++UIProcess/BrowserInspectorController.cpp ++UIProcess/BrowserInspectorPipe.cpp ++UIProcess/BrowserInspectorTargetAgent.cpp + UIProcess/DeviceIdHashSaltStorage.cpp + UIProcess/DrawingAreaProxy.cpp + UIProcess/FrameLoadState.cpp + UIProcess/GeolocationPermissionRequestManagerProxy.cpp + UIProcess/GeolocationPermissionRequestProxy.cpp ++UIProcess/InspectorBrowserAgent.cpp + UIProcess/InspectorTargetProxy.cpp + UIProcess/LegacyGlobalSettings.cpp + UIProcess/PageLoadState.cpp + UIProcess/ProcessAssertion.cpp + UIProcess/ProcessThrottler.cpp + UIProcess/ProvisionalPageProxy.cpp ++UIProcess/RemoteInspectorPipe.cpp + UIProcess/RemoteWebInspectorProxy.cpp + UIProcess/ResponsivenessTimer.cpp + UIProcess/StatisticsRequest.cpp +@@ -292,6 +297,9 @@ UIProcess/WebPageDiagnosticLoggingClient.cpp + UIProcess/WebPageGroup.cpp + UIProcess/WebPageInjectedBundleClient.cpp + UIProcess/WebPageInspectorController.cpp ++UIProcess/WebPageInspectorEmulationAgent.cpp ++UIProcess/WebPageInspectorInputAgent.cpp ++UIProcess/WebPageInspectorTargetProxy.cpp + UIProcess/WebPageProxy.cpp + UIProcess/WebPasteboardProxy.cpp + UIProcess/WebPreferences.cpp +diff --git a/Source/WebKit/SourcesCocoa.txt b/Source/WebKit/SourcesCocoa.txt +index a22aaba0310..386dd8586c4 100644 +--- a/Source/WebKit/SourcesCocoa.txt ++++ b/Source/WebKit/SourcesCocoa.txt +@@ -243,6 +243,7 @@ UIProcess/API/Cocoa/_WKApplicationManifest.mm + UIProcess/API/Cocoa/_WKAttachment.mm + UIProcess/API/Cocoa/_WKAutomationSession.mm + UIProcess/API/Cocoa/_WKAutomationSessionConfiguration.mm ++UIProcess/API/Cocoa/_WKBrowserInspector.mm + UIProcess/API/Cocoa/_WKContentRuleListAction.mm + UIProcess/API/Cocoa/_WKContextMenuElementInfo.mm + UIProcess/API/Cocoa/_WKCustomHeaderFields.mm @no-unify +diff --git a/Source/WebKit/SourcesGTK.txt b/Source/WebKit/SourcesGTK.txt +index c9e153d6a2c..70788976bee 100644 +--- a/Source/WebKit/SourcesGTK.txt ++++ b/Source/WebKit/SourcesGTK.txt +@@ -129,6 +129,7 @@ UIProcess/API/glib/WebKitAuthenticationRequest.cpp @no-unify + UIProcess/API/glib/WebKitAutomationSession.cpp @no-unify + UIProcess/API/glib/WebKitBackForwardList.cpp @no-unify + UIProcess/API/glib/WebKitBackForwardListItem.cpp @no-unify ++UIProcess/API/glib/WebKitBrowserInspector.cpp @no-unify + UIProcess/API/glib/WebKitContextMenuClient.cpp @no-unify + UIProcess/API/glib/WebKitCookieManager.cpp @no-unify + UIProcess/API/glib/WebKitCredential.cpp @no-unify +@@ -224,6 +225,7 @@ UIProcess/WebsiteData/unix/WebsiteDataStoreUnix.cpp + + UIProcess/cairo/BackingStoreCairo.cpp @no-unify + ++UIProcess/glib/InspectorBrowserAgentClientGLib.cpp + UIProcess/glib/RemoteInspectorClient.cpp + UIProcess/glib/WebProcessPoolGLib.cpp + UIProcess/glib/WebProcessProxyGLib.cpp +@@ -249,6 +251,9 @@ UIProcess/gtk/WebColorPickerGtk.cpp + UIProcess/gtk/WebContextMenuProxyGtk.cpp + UIProcess/gtk/WebDataListSuggestionsDropdownGtk.cpp + UIProcess/gtk/WebInspectorProxyGtk.cpp ++UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp ++UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp ++UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp + UIProcess/gtk/WebKitInspectorWindow.cpp + UIProcess/gtk/WebPageProxyGtk.cpp @no-unify + UIProcess/gtk/WebPasteboardProxyGtk.cpp +diff --git a/Source/WebKit/SourcesWPE.txt b/Source/WebKit/SourcesWPE.txt +index 5b514d5216e..75bd77c7614 100644 +--- a/Source/WebKit/SourcesWPE.txt ++++ b/Source/WebKit/SourcesWPE.txt +@@ -118,6 +118,7 @@ UIProcess/API/glib/WebKitAuthenticationRequest.cpp @no-unify + UIProcess/API/glib/WebKitAutomationSession.cpp @no-unify + UIProcess/API/glib/WebKitBackForwardList.cpp @no-unify + UIProcess/API/glib/WebKitBackForwardListItem.cpp @no-unify ++UIProcess/API/glib/WebKitBrowserInspector.cpp @no-unify + UIProcess/API/glib/WebKitContextMenuClient.cpp @no-unify + UIProcess/API/glib/WebKitCookieManager.cpp @no-unify + UIProcess/API/glib/WebKitCredential.cpp @no-unify +@@ -186,7 +187,7 @@ UIProcess/Automation/wpe/WebAutomationSessionWPE.cpp + UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp + + UIProcess/geoclue/GeoclueGeolocationProvider.cpp +- ++UIProcess/glib/InspectorBrowserAgentClientGLib.cpp + UIProcess/glib/WebProcessPoolGLib.cpp + UIProcess/glib/WebProcessProxyGLib.cpp + UIProcess/glib/WebsiteDataStoreGLib.cpp @no-unify +@@ -211,6 +212,9 @@ UIProcess/soup/WebProcessPoolSoup.cpp + UIProcess/wpe/TextCheckerWPE.cpp + UIProcess/wpe/WebInspectorProxyWPE.cpp + UIProcess/wpe/WebPageProxyWPE.cpp ++UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp ++UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp ++UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp + UIProcess/wpe/WebPasteboardProxyWPE.cpp + UIProcess/wpe/WebPreferencesWPE.cpp + +diff --git a/Source/WebKit/UIProcess/API/APIAttachment.cpp b/Source/WebKit/UIProcess/API/APIAttachment.cpp +index f9a4cadfae1..1386ed63eca 100644 +--- a/Source/WebKit/UIProcess/API/APIAttachment.cpp ++++ b/Source/WebKit/UIProcess/API/APIAttachment.cpp +@@ -28,6 +28,7 @@ + + #if ENABLE(ATTACHMENT_ELEMENT) + ++#include "WebPageProxy.h" + #include + #include + +diff --git a/Source/WebKit/UIProcess/API/C/WKPage.cpp b/Source/WebKit/UIProcess/API/C/WKPage.cpp +index 44637251dff..1e64b9f34fe 100644 +--- a/Source/WebKit/UIProcess/API/C/WKPage.cpp ++++ b/Source/WebKit/UIProcess/API/C/WKPage.cpp +@@ -1734,6 +1734,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient + { + if (!m_client.didNotHandleKeyEvent) + return; ++ if (!event.nativeEvent()) ++ return; + m_client.didNotHandleKeyEvent(toAPI(page), event.nativeEvent(), m_client.base.clientInfo); + } + +diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h +new file mode 100644 +index 00000000000..812c2913e4f +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h +@@ -0,0 +1,50 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * ++ * met: ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#import ++#import ++ ++NS_ASSUME_NONNULL_BEGIN ++ ++@class WKWebView; ++ ++@protocol _WKBrowserInspectorDelegate ++- (WKWebView *)createNewPage; ++- (void)quit; ++@end ++ ++WK_CLASS_AVAILABLE(macos(10.15.0)) ++@interface _WKBrowserInspector : NSObject +++ (void)initializeRemoteInspectorPipe:(id<_WKBrowserInspectorDelegate>)delegate; ++@end ++ ++ ++NS_ASSUME_NONNULL_END ++ +diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm +new file mode 100644 +index 00000000000..2c9aead1b47 +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm +@@ -0,0 +1,52 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * ++ * met: ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "_WKBrowserInspector.h" ++ ++#include "BrowserInspectorPipe.h" ++#include "InspectorBrowserAgentClientMac.h" ++#include "WebKit2Initialize.h" ++ ++#import "WKWebView.h" ++ ++using namespace WebKit; ++ ++@implementation _WKBrowserInspector ++ +++ (void)initializeRemoteInspectorPipe:(id<_WKBrowserInspectorDelegate>)delegate ++{ ++#if ENABLE(REMOTE_INSPECTOR) ++ InitializeWebKit2(); ++ initializeBrowserInspectorPipe(makeUnique(delegate)); ++#endif ++} ++ ++@end +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspector.cpp b/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspector.cpp +new file mode 100644 +index 00000000000..a893558f98b +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspector.cpp +@@ -0,0 +1,141 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebKitBrowserInspector.h" ++ ++#include "BrowserInspectorPipe.h" ++#include "InspectorBrowserAgentClientGLib.h" ++#include "WebKitBrowserInspectorPrivate.h" ++#include "WebKitWebViewPrivate.h" ++#include ++#include ++ ++/** ++ * SECTION: WebKitBrowserInspector ++ * @Short_description: Access to the WebKit browser inspector ++ * @Title: WebKitBrowserInspector ++ * ++ * The WebKit Browser Inspector is an experimental API that provides ++ * access to the inspector via the remote debugging protocol. The protocol ++ * allows to create ephemeral contexts and create pages in them and then ++ * manipulate them using the inspector commands. This may be useful for ++ * the browser automation or remote debugging. ++ * ++ * Currently the protocol can be exposed to the parent process via a unix ++ * pipe. ++ */ ++ ++enum { ++ CREATE_NEW_PAGE, ++ ++ LAST_SIGNAL ++}; ++ ++struct _WebKitBrowserInspectorPrivate { ++ int unused { 0 }; ++}; ++ ++WEBKIT_DEFINE_TYPE(WebKitBrowserInspector, webkit_browser_inspector, G_TYPE_OBJECT) ++ ++static guint signals[LAST_SIGNAL] = { 0, }; ++ ++static void webkit_browser_inspector_class_init(WebKitBrowserInspectorClass* findClass) ++{ ++ GObjectClass* gObjectClass = G_OBJECT_CLASS(findClass); ++ ++ /** ++ * WebKitBrowserInspector::create-new-page: ++ * @inspector: the #WebKitBrowserInspector on which the signal is emitted ++ * ++ * Emitted when the inspector is requested to create a new page in the provided ++ * #WebKitWebContext. ++ * ++ * This signal is emitted when inspector receives 'Browser.createPage' command ++ * from its remote client. If the signla is not handled the command will fail. ++ * ++ * Returns: %WebKitWebView that contains created page. ++ */ ++ signals[CREATE_NEW_PAGE] = g_signal_new( ++ "create-new-page", ++ G_TYPE_FROM_CLASS(gObjectClass), ++ G_SIGNAL_RUN_LAST, ++ G_STRUCT_OFFSET(WebKitBrowserInspectorClass, create_new_page), ++ nullptr, nullptr, ++ g_cclosure_marshal_generic, ++#if PLATFORM(GTK) ++ GTK_TYPE_WIDGET, ++#else ++ WEBKIT_TYPE_WEB_VIEW, ++#endif ++ 1, ++ WEBKIT_TYPE_WEB_CONTEXT); ++} ++ ++WebKit::WebPageProxy* webkitBrowserInspectorCreateNewPageInContext(WebKitWebContext* context) ++{ ++ WebKitWebView* newWebView; ++ g_signal_emit(webkit_browser_inspector_get_default(), signals[CREATE_NEW_PAGE], 0, context, &newWebView); ++ if (!newWebView) ++ return nullptr; ++ return &webkitWebViewGetPage(newWebView); ++} ++ ++static gpointer createWebKitBrowserInspector(gpointer) ++{ ++ static GRefPtr browserInspector = adoptGRef(WEBKIT_BROWSER_INSPECTOR(g_object_new(WEBKIT_TYPE_BROWSER_INSPECTOR, nullptr))); ++ return browserInspector.get(); ++} ++ ++/** ++ * webkit_browser_inspector_get_default: ++ * ++ * Gets the default instance of the browser inspector. ++ * ++ * Returns: (transfer none): a #WebKitBrowserInspector ++ */ ++WebKitBrowserInspector* webkit_browser_inspector_get_default(void) ++{ ++ static GOnce onceInit = G_ONCE_INIT; ++ return WEBKIT_BROWSER_INSPECTOR(g_once(&onceInit, createWebKitBrowserInspector, 0)); ++} ++ ++/** ++ * webkit_browser_inspector_initialize_pipe: ++ * ++ * Creates browser inspector and configures pipe handler to communicate with ++ * the parent process. ++ */ ++void webkit_browser_inspector_initialize_pipe(void) ++{ ++#if ENABLE(REMOTE_INSPECTOR) ++ WebKit::initializeBrowserInspectorPipe(makeUnique()); ++#endif ++} +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspectorPrivate.h b/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspectorPrivate.h +new file mode 100644 +index 00000000000..6e9afeac99a +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspectorPrivate.h +@@ -0,0 +1,36 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "WebKitBrowserInspector.h" ++#include "WebPageProxy.h" ++ ++WebKit::WebPageProxy* webkitBrowserInspectorCreateNewPageInContext(WebKitWebContext*); +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp +index 126bccf1314..a095db63bc5 100644 +--- a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp ++++ b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp +@@ -373,6 +373,11 @@ static void webkitWebContextConstructed(GObject* object) + if (!webkit_website_data_manager_is_ephemeral(priv->websiteDataManager.get())) + WebKit::LegacyGlobalSettings::singleton().setHSTSStorageDirectory(FileSystem::stringFromFileSystemRepresentation(webkit_website_data_manager_get_hsts_cache_directory(priv->websiteDataManager.get()))); + ++ const gchar *singleprocess = g_getenv("MINIBROWSER_SINGLEPROCESS"); ++ if (singleprocess && *singleprocess) { ++ // processModel is not set at this point, force single process. ++ configuration.setUsesSingleWebProcess(true); ++ } + priv->processPool = WebProcessPool::create(configuration); + priv->processPool->setPrimaryDataStore(webkitWebsiteDataManagerGetDataStore(priv->websiteDataManager.get())); + priv->processPool->setUserMessageHandler([webContext](UserMessage&& message, CompletionHandler&& completionHandler) { +diff --git a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp +index 00b7c6bbc46..c3a6cf416e1 100644 +--- a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp ++++ b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp +@@ -226,6 +226,8 @@ void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool + return; + if (event.fakedForComposition()) + return; ++ if (!event.nativeEvent()) ++ return; + + WebKitWebViewBase* webkitWebViewBase = WEBKIT_WEB_VIEW_BASE(m_viewWidget); + webkitWebViewBaseForwardNextKeyEvent(webkitWebViewBase); +diff --git a/Source/WebKit/UIProcess/API/gtk/WebKitBrowserInspector.h b/Source/WebKit/UIProcess/API/gtk/WebKitBrowserInspector.h +new file mode 100644 +index 00000000000..4ee8204a9b8 +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/gtk/WebKitBrowserInspector.h +@@ -0,0 +1,84 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#if !defined(__WEBKIT2_H_INSIDE__) && !defined(WEBKIT2_COMPILATION) ++#error "Only can be included directly." ++#endif ++ ++#ifndef WebKitBrowserInspector_h ++#define WebKitBrowserInspector_h ++ ++#include ++#include ++#include ++ ++G_BEGIN_DECLS ++ ++#define WEBKIT_TYPE_BROWSER_INSPECTOR (webkit_browser_inspector_get_type()) ++#define WEBKIT_BROWSER_INSPECTOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspector)) ++#define WEBKIT_IS_BROWSER_INSPECTOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), WEBKIT_TYPE_BROWSER_INSPECTOR)) ++#define WEBKIT_BROWSER_INSPECTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspectorClass)) ++#define WEBKIT_IS_BROWSER_INSPECTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), WEBKIT_TYPE_BROWSER_INSPECTOR)) ++#define WEBKIT_BROWSER_INSPECTOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspectorClass)) ++ ++typedef struct _WebKitBrowserInspector WebKitBrowserInspector; ++typedef struct _WebKitBrowserInspectorClass WebKitBrowserInspectorClass; ++typedef struct _WebKitBrowserInspectorPrivate WebKitBrowserInspectorPrivate; ++ ++struct _WebKitBrowserInspector { ++ GObject parent; ++ ++ WebKitBrowserInspectorPrivate *priv; ++}; ++ ++struct _WebKitBrowserInspectorClass { ++ GObjectClass parent_class; ++ ++ WebKitWebView *(* create_new_page) (WebKitBrowserInspector *browser_inspector, ++ WebKitWebContext *context); ++ ++ void (*_webkit_reserved0) (void); ++ void (*_webkit_reserved1) (void); ++ void (*_webkit_reserved2) (void); ++ void (*_webkit_reserved3) (void); ++}; ++ ++WEBKIT_API GType ++webkit_browser_inspector_get_type (void); ++ ++WEBKIT_API WebKitBrowserInspector * ++webkit_browser_inspector_get_default (void); ++ ++WEBKIT_API void ++webkit_browser_inspector_initialize_pipe (void); ++ ++G_END_DECLS ++ ++#endif +diff --git a/Source/WebKit/UIProcess/API/gtk/webkit2.h b/Source/WebKit/UIProcess/API/gtk/webkit2.h +index 16ef7eb6d42..eb3759b05bb 100644 +--- a/Source/WebKit/UIProcess/API/gtk/webkit2.h ++++ b/Source/WebKit/UIProcess/API/gtk/webkit2.h +@@ -32,6 +32,7 @@ + #include + #include + #include ++#include + #include + #include + #include +diff --git a/Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h b/Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h +new file mode 100644 +index 00000000000..675e517596b +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h +@@ -0,0 +1,81 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#if !defined(__WEBKIT_H_INSIDE__) && !defined(WEBKIT2_COMPILATION) ++#error "Only can be included directly." ++#endif ++ ++#ifndef WebKitBrowserInspector_h ++#define WebKitBrowserInspector_h ++ ++#include ++#include ++#include ++ ++G_BEGIN_DECLS ++ ++#define WEBKIT_TYPE_BROWSER_INSPECTOR (webkit_browser_inspector_get_type()) ++#define WEBKIT_BROWSER_INSPECTOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspector)) ++#define WEBKIT_IS_BROWSER_INSPECTOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), WEBKIT_TYPE_BROWSER_INSPECTOR)) ++#define WEBKIT_BROWSER_INSPECTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspectorClass)) ++#define WEBKIT_IS_BROWSER_INSPECTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), WEBKIT_TYPE_BROWSER_INSPECTOR)) ++#define WEBKIT_BROWSER_INSPECTOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspectorClass)) ++ ++typedef struct _WebKitBrowserInspector WebKitBrowserInspector; ++typedef struct _WebKitBrowserInspectorClass WebKitBrowserInspectorClass; ++typedef struct _WebKitBrowserInspectorPrivate WebKitBrowserInspectorPrivate; ++ ++struct _WebKitBrowserInspector { ++ GObject parent; ++ ++ WebKitBrowserInspectorPrivate *priv; ++}; ++ ++struct _WebKitBrowserInspectorClass { ++ GObjectClass parent_class; ++ ++ WebKitWebView *(* create_new_page) (WebKitBrowserInspector *browser_inspector, ++ WebKitWebContext *context); ++ ++ void (*_webkit_reserved0) (void); ++ void (*_webkit_reserved1) (void); ++ void (*_webkit_reserved2) (void); ++ void (*_webkit_reserved3) (void); ++}; ++ ++WEBKIT_API GType ++webkit_browser_inspector_get_type (void); ++ ++WEBKIT_API WebKitBrowserInspector * ++webkit_browser_inspector_get_default (void); ++ ++G_END_DECLS ++ ++#endif +diff --git a/Source/WebKit/UIProcess/API/wpe/webkit.h b/Source/WebKit/UIProcess/API/wpe/webkit.h +index 9cc31cb4968..930499e65b6 100644 +--- a/Source/WebKit/UIProcess/API/wpe/webkit.h ++++ b/Source/WebKit/UIProcess/API/wpe/webkit.h +@@ -32,6 +32,7 @@ + #include + #include + #include ++#include + #include + #include + #include +diff --git a/Source/WebKit/UIProcess/BrowserInspectorController.cpp b/Source/WebKit/UIProcess/BrowserInspectorController.cpp +new file mode 100644 +index 00000000000..b4e14cb4390 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorController.cpp +@@ -0,0 +1,128 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "BrowserInspectorController.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "BrowserInspectorTargetAgent.h" ++#include "InspectorBrowserAgent.h" ++#include "InspectorBrowserAgentClient.h" ++#include "WebPageInspectorController.h" ++#include "WebPageProxy.h" ++#include "WebProcessPool.h" ++#include "WebProcessProxy.h" ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++using namespace Inspector; ++ ++namespace WebKit { ++ ++static Vector allPages() ++{ ++ ASSERT(isMainThread()); ++ Vector result; ++ for (WebProcessPool* pool : WebProcessPool::allProcessPools()) { ++ for (auto& process : pool->processes()) { ++ result.appendRange(process->pages().begin(), process->pages().end()); ++ } ++ } ++ return result; ++} ++ ++BrowserInspectorController::BrowserInspectorController(std::unique_ptr client) ++ : m_frontendChannel(nullptr) ++ , m_frontendRouter(FrontendRouter::create()) ++ , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) ++ , m_browserAgentClient(std::move(client)) ++{ ++ m_agents.append(makeUnique(m_backendDispatcher, m_browserAgentClient.get())); ++ m_agents.append(makeUnique(m_backendDispatcher)); ++} ++ ++BrowserInspectorController::~BrowserInspectorController() = default; ++ ++void BrowserInspectorController::connectFrontend(FrontendChannel& frontendChannel) ++{ ++ ASSERT(!m_frontendChannel); ++ m_frontendChannel = &frontendChannel; ++ // Auto-connect to all new pages. ++ WebPageInspectorController::setCreationListener([this](WebPageInspectorController& inspectorController) { ++ inspectorController.connectFrontend(*m_frontendChannel); ++ }); ++ ++ bool connectingFirstFrontend = !m_frontendRouter->hasFrontends(); ++ m_frontendRouter->connectFrontend(frontendChannel); ++ if (connectingFirstFrontend) ++ m_agents.didCreateFrontendAndBackend(&m_frontendRouter.get(), &m_backendDispatcher.get()); ++ ++ connectToAllPages(); ++} ++ ++void BrowserInspectorController::disconnectFrontend() ++{ ++ ASSERT(m_frontendChannel); ++ disconnectFromAllPages(); ++ ++ m_frontendRouter->disconnectFrontend(*m_frontendChannel); ++ if (!m_frontendRouter->hasFrontends()) ++ m_agents.willDestroyFrontendAndBackend(DisconnectReason::InspectorDestroyed); ++ ++ WebPageInspectorController::setCreationListener(nullptr); ++ m_frontendChannel = nullptr; ++} ++ ++void BrowserInspectorController::dispatchMessageFromFrontend(const String& message) ++{ ++ m_backendDispatcher->dispatch(message); ++} ++ ++void BrowserInspectorController::connectToAllPages() ++{ ++ for (auto* page : allPages()) ++ page->inspectorController().connectFrontend(*m_frontendChannel); ++} ++ ++void BrowserInspectorController::disconnectFromAllPages() ++{ ++ for (auto* page : allPages()) ++ page->inspectorController().disconnectFrontend(*m_frontendChannel); ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/BrowserInspectorController.h b/Source/WebKit/UIProcess/BrowserInspectorController.h +new file mode 100644 +index 00000000000..d1e7ea17002 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorController.h +@@ -0,0 +1,74 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class FrontendChannel; ++class FrontendRouter; ++} ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClient; ++ ++class BrowserInspectorController { ++ WTF_MAKE_NONCOPYABLE(BrowserInspectorController); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ BrowserInspectorController(std::unique_ptr client); ++ ~BrowserInspectorController(); ++ ++ void connectFrontend(Inspector::FrontendChannel&); ++ void disconnectFrontend(); ++ void dispatchMessageFromFrontend(const String& message); ++ ++private: ++ class TargetHandler; ++ void connectToAllPages(); ++ void disconnectFromAllPages(); ++ ++ Inspector::FrontendChannel* m_frontendChannel { nullptr }; ++ Ref m_frontendRouter; ++ Ref m_backendDispatcher; ++ std::unique_ptr m_browserAgentClient; ++ Inspector::AgentRegistry m_agents; ++}; ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/BrowserInspectorPipe.cpp b/Source/WebKit/UIProcess/BrowserInspectorPipe.cpp +new file mode 100644 +index 00000000000..483b4e46c98 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorPipe.cpp +@@ -0,0 +1,62 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "BrowserInspectorPipe.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "BrowserInspectorController.h" ++#include "RemoteInspectorPipe.h" ++#include ++#include "InspectorBrowserAgentClient.h" ++ ++namespace WebKit { ++ ++void initializeBrowserInspectorPipe(std::unique_ptr client) ++{ ++ class BrowserInspectorPipe { ++ public: ++ BrowserInspectorPipe(std::unique_ptr client) ++ : m_browserInspectorController(std::move(client)) ++ , m_remoteInspectorPipe(m_browserInspectorController) ++ { ++ } ++ ++ BrowserInspectorController m_browserInspectorController; ++ RemoteInspectorPipe m_remoteInspectorPipe; ++ }; ++ ++ static NeverDestroyed pipe(std::move(client)); ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/BrowserInspectorPipe.h b/Source/WebKit/UIProcess/BrowserInspectorPipe.h +new file mode 100644 +index 00000000000..a0088a43590 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorPipe.h +@@ -0,0 +1,43 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClient; ++ ++void initializeBrowserInspectorPipe(std::unique_ptr client); ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp +new file mode 100644 +index 00000000000..0d1f5d75c3a +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp +@@ -0,0 +1,110 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "BrowserInspectorTargetAgent.h" ++ ++#include "WebPageInspectorController.h" ++#include "WebPageProxy.h" ++#include "WebProcessPool.h" ++#include "WebProcessProxy.h" ++#include ++#include ++ ++using namespace Inspector; ++ ++namespace WebKit { ++ ++namespace { ++ ++InspectorTarget* targetForId(const String& targetId) ++{ ++ ASSERT(isMainThread()); ++ for (auto* pool : WebProcessPool::allProcessPools()) { ++ for (auto& process : pool->processes()) { ++ for (auto* page : process->pages()) { ++ auto* result = page->inspectorController().findTarget(targetId); ++ if (result != nullptr) ++ return result; ++ } ++ } ++ } ++ return nullptr; ++} ++ ++} // namespace ++ ++BrowserInspectorTargetAgent::BrowserInspectorTargetAgent(BackendDispatcher& backendDispatcher) ++ : InspectorAgentBase("Target"_s) ++ , m_backendDispatcher(TargetBackendDispatcher::create(backendDispatcher, this)) ++{ ++} ++ ++BrowserInspectorTargetAgent::~BrowserInspectorTargetAgent() = default; ++ ++void BrowserInspectorTargetAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) ++{ ++} ++ ++void BrowserInspectorTargetAgent::willDestroyFrontendAndBackend(DisconnectReason) ++{ ++} ++ ++void BrowserInspectorTargetAgent::sendMessageToTarget(ErrorString& error, const String& in_targetId, const String& in_message) ++{ ++ auto* target = targetForId(in_targetId); ++ if (target == nullptr) { ++ error = "Cannot find target with provided id."; ++ return; ++ } ++ target->sendMessageToTargetBackend(in_message); ++} ++ ++void BrowserInspectorTargetAgent::activate(ErrorString& error, const String& targetId) ++{ ++ auto* target = targetForId(targetId); ++ if (target == nullptr) { ++ error = "Cannot find target with provided id."; ++ return; ++ } ++ target->activate(error); ++} ++ ++void BrowserInspectorTargetAgent::close(ErrorString& error, const String& targetId) ++{ ++ auto* target = targetForId(targetId); ++ if (target == nullptr) { ++ error = "Cannot find target with provided id."; ++ return; ++ } ++ target->close(error); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h +new file mode 100644 +index 00000000000..8b4d9273574 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h +@@ -0,0 +1,62 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "config.h" ++ ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++ ++class BrowserInspectorTargetAgent final : public Inspector::InspectorAgentBase, public Inspector::TargetBackendDispatcherHandler { ++ WTF_MAKE_NONCOPYABLE(BrowserInspectorTargetAgent); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ explicit BrowserInspectorTargetAgent(Inspector::BackendDispatcher&); ++ ~BrowserInspectorTargetAgent() override; ++ ++ // InspectorAgentBase ++ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; ++ ++ // TargetBackendDispatcherHandler ++ void sendMessageToTarget(Inspector::ErrorString&, const String& targetId, const String& message) final; ++ void activate(Inspector::ErrorString&, const String& targetId) override; ++ void close(Inspector::ErrorString&, const String& targetId) override; ++ ++private: ++ Ref m_backendDispatcher; ++}; ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.h b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.h +index b6694fe906e..4df2ca7d9cc 100644 +--- a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.h ++++ b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.h +@@ -27,6 +27,8 @@ + + #if HAVE(APP_SSO) + ++#include ++#include + #include "SOAuthorizationSession.h" + + OBJC_CLASS WKSOSecretDelegate; +@@ -38,6 +40,8 @@ class NavigationAction; + + namespace WebKit { + ++class WebPageProxy; ++ + // FSM: Idle => Active => Completed + class PopUpSOAuthorizationSession final : public SOAuthorizationSession { + public: +diff --git a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.mm b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.mm +index 076cfaa676a..bd20a2b95f9 100644 +--- a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.mm ++++ b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.mm +@@ -29,6 +29,7 @@ + #if HAVE(APP_SSO) + + #import "APINavigationAction.h" ++#import "WebPageProxy.h" + #import "WKNavigationDelegatePrivate.h" + #import "WKUIDelegate.h" + #import "WKWebViewConfigurationPrivate.h" +diff --git a/Source/WebKit/UIProcess/InspectorBrowserAgent.cpp b/Source/WebKit/UIProcess/InspectorBrowserAgent.cpp +new file mode 100644 +index 00000000000..5967318c785 +--- /dev/null ++++ b/Source/WebKit/UIProcess/InspectorBrowserAgent.cpp +@@ -0,0 +1,101 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "InspectorBrowserAgent.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "InspectorBrowserAgentClient.h" ++#include "WebPageInspectorTarget.h" ++#include "WebPageProxy.h" ++#include ++#include ++#include ++ ++using namespace Inspector; ++ ++namespace WebKit { ++ ++InspectorBrowserAgent::InspectorBrowserAgent(Inspector::BackendDispatcher& backendDispatcher, InspectorBrowserAgentClient* client) ++ : InspectorAgentBase("Browser"_s) ++ , m_backendDispatcher(BrowserBackendDispatcher::create(backendDispatcher, this)) ++ , m_client(client) ++{ ++} ++ ++InspectorBrowserAgent::~InspectorBrowserAgent() = default; ++ ++void InspectorBrowserAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) ++{ ++} ++ ++void InspectorBrowserAgent::willDestroyFrontendAndBackend(DisconnectReason) ++{ ++} ++ ++void InspectorBrowserAgent::close(ErrorString& error) ++{ ++ if (m_client == nullptr) { ++ error = "no platform delegate to close browser"; ++ } else { ++ m_client->closeAllWindows(); ++ } ++} ++ ++void InspectorBrowserAgent::createContext(ErrorString& error, String* browserContextID) ++{ ++ m_client->createBrowserContext(error, browserContextID); ++} ++ ++void InspectorBrowserAgent::deleteContext(ErrorString& error, const String& browserContextID) ++{ ++ m_client->deleteBrowserContext(error, browserContextID); ++} ++ ++void InspectorBrowserAgent::createPage(ErrorString& error, const String* browserContextID, String* targetID) ++{ ++ RefPtr page = m_client->createPage(error, browserContextID); ++ if (page == nullptr) ++ return; ++ ++ *targetID = WebPageInspectorTarget::toTargetID(page->webPageID()); ++} ++ ++String InspectorBrowserAgent::toBrowserContextIDProtocolString(const PAL::SessionID& sessionID) ++{ ++ StringBuilder builder; ++ appendUnsignedAsHexFixedSize(sessionID.toUInt64(), builder, 16); ++ return builder.toString(); ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/InspectorBrowserAgent.h b/Source/WebKit/UIProcess/InspectorBrowserAgent.h +new file mode 100644 +index 00000000000..f24c655ab39 +--- /dev/null ++++ b/Source/WebKit/UIProcess/InspectorBrowserAgent.h +@@ -0,0 +1,81 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include ++#include ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class FrontendChannel; ++class FrontendRouter; ++} ++ ++namespace PAL { ++class SessionID; ++} ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClient; ++class WebPageInspectorController; ++ ++class InspectorBrowserAgent final : public Inspector::InspectorAgentBase, public Inspector::BrowserBackendDispatcherHandler { ++ WTF_MAKE_NONCOPYABLE(InspectorBrowserAgent); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ InspectorBrowserAgent(Inspector::BackendDispatcher&, InspectorBrowserAgentClient*); ++ ~InspectorBrowserAgent() override; ++ ++ // InspectorAgentBase ++ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; ++ ++ // BrowserBackendDispatcherHandler ++ void close(Inspector::ErrorString&) override; ++ void createContext(Inspector::ErrorString&, String* browserContextID) override; ++ void deleteContext(Inspector::ErrorString&, const String& browserContextID) override; ++ void createPage(Inspector::ErrorString&, const String* browserContextID, String* targetId) override; ++ ++ static String toBrowserContextIDProtocolString(const PAL::SessionID&); ++ ++private: ++ Ref m_backendDispatcher; ++ InspectorBrowserAgentClient* m_client; ++}; ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/InspectorBrowserAgentClient.h b/Source/WebKit/UIProcess/InspectorBrowserAgentClient.h +new file mode 100644 +index 00000000000..f05cd030bac +--- /dev/null ++++ b/Source/WebKit/UIProcess/InspectorBrowserAgentClient.h +@@ -0,0 +1,52 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include ++ ++namespace WebKit { ++ ++class WebPageProxy; ++ ++class InspectorBrowserAgentClient { ++public: ++ virtual ~InspectorBrowserAgentClient() = default; ++ virtual RefPtr createPage(WTF::String& error, const WTF::String* browserContextID) = 0; ++ virtual void closeAllWindows() = 0; ++ virtual void createBrowserContext(WTF::String& error, WTF::String* browserContextID) = 0; ++ virtual void deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) = 0; ++}; ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/InspectorTargetProxy.cpp b/Source/WebKit/UIProcess/InspectorTargetProxy.cpp +index 1b37c1ed439..c45d45de342 100644 +--- a/Source/WebKit/UIProcess/InspectorTargetProxy.cpp ++++ b/Source/WebKit/UIProcess/InspectorTargetProxy.cpp +@@ -32,6 +32,8 @@ + #include "WebPageMessages.h" + #include "WebPageProxy.h" + #include "WebProcessProxy.h" ++#include "ProvisionalPageProxy.h" ++ + + namespace WebKit { + +@@ -39,23 +41,29 @@ using namespace Inspector; + + std::unique_ptr InspectorTargetProxy::create(WebPageProxy& page, const String& targetId, Inspector::InspectorTargetType type) + { +- return makeUnique(page, targetId, type); ++ return makeUnique(page, nullptr, targetId, type); + } + + std::unique_ptr InspectorTargetProxy::create(ProvisionalPageProxy& provisionalPage, const String& targetId, Inspector::InspectorTargetType type) + { +- auto target = InspectorTargetProxy::create(provisionalPage.page(), targetId, type); +- target->m_provisionalPage = makeWeakPtr(provisionalPage); +- return target; ++ return makeUnique(provisionalPage.page(), &provisionalPage, targetId, type); + } + +-InspectorTargetProxy::InspectorTargetProxy(WebPageProxy& page, const String& targetId, Inspector::InspectorTargetType type) ++InspectorTargetProxy::InspectorTargetProxy(WebPageProxy& page, ProvisionalPageProxy* provisionalPage, const String& targetId, Inspector::InspectorTargetType type) + : m_page(page) ++ , m_provisionalPage(makeWeakPtr(provisionalPage)) + , m_identifier(targetId) + , m_type(type) + { + } + ++String InspectorTargetProxy::url() const ++{ ++ if (m_page.provisionalPageProxy()) ++ return m_page.provisionalPageProxy()->provisionalURL().string(); ++ return m_page.pageLoadState().activeURL(); ++} ++ + void InspectorTargetProxy::connect(Inspector::FrontendChannel::ConnectionType connectionType) + { + if (m_provisionalPage) { +diff --git a/Source/WebKit/UIProcess/InspectorTargetProxy.h b/Source/WebKit/UIProcess/InspectorTargetProxy.h +index a2239cec8e1..43415afbc77 100644 +--- a/Source/WebKit/UIProcess/InspectorTargetProxy.h ++++ b/Source/WebKit/UIProcess/InspectorTargetProxy.h +@@ -37,17 +37,18 @@ class WebPageProxy; + // NOTE: This UIProcess side InspectorTarget doesn't care about the frontend channel, since + // any target -> frontend messages will be routed to the WebPageProxy with a targetId. + +-class InspectorTargetProxy final : public Inspector::InspectorTarget { ++class InspectorTargetProxy : public Inspector::InspectorTarget { + WTF_MAKE_FAST_ALLOCATED; + WTF_MAKE_NONCOPYABLE(InspectorTargetProxy); + public: + static std::unique_ptr create(WebPageProxy&, const String& targetId, Inspector::InspectorTargetType); + static std::unique_ptr create(ProvisionalPageProxy&, const String& targetId, Inspector::InspectorTargetType); +- InspectorTargetProxy(WebPageProxy&, const String& targetId, Inspector::InspectorTargetType); ++ InspectorTargetProxy(WebPageProxy&, ProvisionalPageProxy*, const String& targetId, Inspector::InspectorTargetType); + ~InspectorTargetProxy() = default; + + Inspector::InspectorTargetType type() const final { return m_type; } + String identifier() const final { return m_identifier; } ++ String url() const final; + + void didCommitProvisionalTarget(); + bool isProvisional() const override; +@@ -56,11 +57,13 @@ public: + void disconnect() override; + void sendMessageToTargetBackend(const String&) override; + +-private: ++protected: + WebPageProxy& m_page; ++ ++private: ++ WeakPtr m_provisionalPage; + String m_identifier; + Inspector::InspectorTargetType m_type; +- WeakPtr m_provisionalPage; + }; + + } // namespace WebKit +diff --git a/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp b/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp +new file mode 100644 +index 00000000000..87b426e9fff +--- /dev/null ++++ b/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp +@@ -0,0 +1,159 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "RemoteInspectorPipe.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "BrowserInspectorController.h" ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#if OS(UNIX) ++#include ++#include ++#endif ++ ++namespace WebKit { ++ ++static const int readFD = 3; ++static const int writeFD = 4; ++ ++class RemoteInspectorPipe::RemoteFrontendChannel : public Inspector::FrontendChannel { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ RemoteFrontendChannel() ++ : m_senderQueue(WorkQueue::create("Inspector pipe writer")) ++ { ++ } ++ ++ ~RemoteFrontendChannel() override = default; ++ ++ ConnectionType connectionType() const override ++ { ++ return ConnectionType::Remote; ++ } ++ ++ void sendMessageToFrontend(const String& message) override ++ { ++ m_senderQueue->dispatch([message = message.isolatedCopy()]() { ++ dprintf(writeFD, "%s%c", message.ascii().data(), '\0'); ++ }); ++ } ++ ++private: ++ Ref m_senderQueue; ++}; ++ ++RemoteInspectorPipe::RemoteInspectorPipe(BrowserInspectorController& browserInspectorController) ++ : m_remoteFrontendChannel(makeUnique()) ++ , m_browserInspectorController(browserInspectorController) ++{ ++ start(); ++} ++ ++RemoteInspectorPipe::~RemoteInspectorPipe() ++{ ++ stop(); ++} ++ ++bool RemoteInspectorPipe::start() ++{ ++ if (m_receiverThread) ++ return true; ++ ++ m_browserInspectorController.connectFrontend(*m_remoteFrontendChannel); ++ m_terminated = false; ++ m_receiverThread = Thread::create("Inspector pipe reader", [this] { ++ workerRun(); ++ }); ++ return true; ++} ++ ++void RemoteInspectorPipe::stop() ++{ ++ if (!m_receiverThread) ++ return; ++ ++ m_browserInspectorController.disconnectFrontend(); ++ ++ m_terminated = true; ++ m_receiverThread->waitForCompletion(); ++ m_receiverThread = nullptr; ++} ++ ++void RemoteInspectorPipe::workerRun() ++{ ++ const size_t bufSize = 256 * 1024; ++ auto buffer = makeUniqueArray(bufSize); ++ Vector line; ++ while (!m_terminated) { ++ ssize_t size = read(readFD, buffer.get(), bufSize); ++ if (size == 0) { ++ break; ++ } ++ if (size < 0) { ++ break; ++ } ++ size_t start = 0; ++ size_t end = line.size(); ++ line.append(buffer.get(), size); ++ while (true) { ++ for (; end < line.size(); ++end) { ++ if (line[end] == '\0') ++ break; ++ } ++ if (end == line.size()) ++ break; ++ ++ if (end > start) { ++ String message = String::fromUTF8(line.data() + start, end - start); ++ RunLoop::main().dispatch([this, message] { ++ if (!m_terminated) ++ m_browserInspectorController.dispatchMessageFromFrontend(message); ++ }); ++ } ++ ++end; ++ start = end; ++ } ++ if (start != 0 && start < line.size()) ++ memmove(line.data(), line.data() + start, line.size() - start); ++ line.shrink(line.size() - start); ++ } ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/RemoteInspectorPipe.h b/Source/WebKit/UIProcess/RemoteInspectorPipe.h +new file mode 100644 +index 00000000000..37b0622557c +--- /dev/null ++++ b/Source/WebKit/UIProcess/RemoteInspectorPipe.h +@@ -0,0 +1,70 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include ++#include ++#include ++ ++namespace Inspector { ++class FrontendChannel; ++} ++ ++namespace WebKit { ++ ++class BrowserInspectorController; ++ ++class RemoteInspectorPipe { ++ WTF_MAKE_NONCOPYABLE(RemoteInspectorPipe); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ explicit RemoteInspectorPipe(BrowserInspectorController&); ++ ~RemoteInspectorPipe(); ++ ++private: ++ class RemoteFrontendChannel; ++ ++ bool start(); ++ void stop(); ++ ++ void workerRun(); ++ ++ RefPtr m_receiverThread; ++ std::atomic m_terminated { false }; ++ std::unique_ptr m_remoteFrontendChannel; ++ BrowserInspectorController& m_browserInspectorController; ++}; ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp +index cce83796f13..eb019b70e0b 100644 +--- a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp ++++ b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp +@@ -35,6 +35,7 @@ + #include "NfcService.h" + #include "WebPageProxy.h" + #include "WebPreferencesKeys.h" ++#include "WebProcessProxy.h" + #include + #include + #include +diff --git a/Source/WebKit/UIProcess/WebPageInspectorController.cpp b/Source/WebKit/UIProcess/WebPageInspectorController.cpp +index b9a9469ab59..81129896554 100644 +--- a/Source/WebKit/UIProcess/WebPageInspectorController.cpp ++++ b/Source/WebKit/UIProcess/WebPageInspectorController.cpp +@@ -26,9 +26,11 @@ + #include "config.h" + #include "WebPageInspectorController.h" + ++#include "InspectorBrowserAgent.h" + #include "ProvisionalPageProxy.h" + #include "WebFrameProxy.h" + #include "WebPageInspectorTarget.h" ++#include "WebPageInspectorTargetProxy.h" + #include "WebPageProxy.h" + #include + #include +@@ -46,26 +48,59 @@ static String getTargetID(const ProvisionalPageProxy& provisionalPage) + return WebPageInspectorTarget::toTargetID(provisionalPage.webPageID()); + } + ++static WebPageInspectorController::CreationListener& creationListener() { ++ static NeverDestroyed listener; ++ return listener; ++} ++ ++void WebPageInspectorController::setCreationListener(CreationListener listener) ++{ ++ creationListener() = listener; ++} ++ + WebPageInspectorController::WebPageInspectorController(WebPageProxy& page) + : m_page(page) + , m_frontendRouter(FrontendRouter::create()) + , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) + { +- auto targetAgent = makeUnique(m_frontendRouter.get(), m_backendDispatcher.get()); ++ String browserContextID; ++#if ENABLE(REMOTE_INSPECTOR) ++ browserContextID = InspectorBrowserAgent::toBrowserContextIDProtocolString(page.sessionID()); ++#endif ++ auto targetAgent = makeUnique(m_frontendRouter.get(), m_backendDispatcher.get(), browserContextID); + + m_targetAgent = targetAgent.get(); + + m_agents.append(WTFMove(targetAgent)); ++ ++ if (creationListener()) ++ creationListener()(*this); + } + + void WebPageInspectorController::init() + { ++ // window.open will create page with already running process. ++ if (!m_page.hasRunningProcess()) ++ return; + String pageTargetId = WebPageInspectorTarget::toTargetID(m_page.webPageID()); + createInspectorTarget(pageTargetId, Inspector::InspectorTargetType::Page); + } + ++void WebPageInspectorController::didFinishAttachingToWebProcess() ++{ ++ String pageTargetID = WebPageInspectorTarget::toTargetID(m_page.webPageID()); ++ // Create target only after attaching to a Web Process first time. Before that ++ // we cannot event establish frontend connection. ++ if (m_targets.contains(pageTargetID)) ++ return; ++ createInspectorTarget(pageTargetID, Inspector::InspectorTargetType::Page); ++} ++ + void WebPageInspectorController::pageClosed() + { ++ String pageTargetId = WebPageInspectorTarget::toTargetID(m_page.webPageID()); ++ destroyInspectorTarget(pageTargetId); ++ + disconnectAllFrontends(); + + m_agents.discardValues(); +@@ -134,6 +169,16 @@ void WebPageInspectorController::dispatchMessageFromFrontend(const String& messa + m_backendDispatcher->dispatch(message); + } + ++bool WebPageInspectorController::dispatchMessageToTargetBackend(const String& message) ++{ ++ return m_backendDispatcher->dispatch(message, BackendDispatcher::Mode::ContinueIfDomainIsMissing) == BackendDispatcher::DispatchResult::Finished; ++} ++ ++Inspector::InspectorTarget* WebPageInspectorController::findTarget(const String& targetId) ++{ ++ return m_targets.get(targetId); ++} ++ + #if ENABLE(REMOTE_INSPECTOR) + void WebPageInspectorController::setIndicating(bool indicating) + { +@@ -150,7 +195,12 @@ void WebPageInspectorController::setIndicating(bool indicating) + + void WebPageInspectorController::createInspectorTarget(const String& targetId, Inspector::InspectorTargetType type) + { +- addTarget(InspectorTargetProxy::create(m_page, targetId, type)); ++ std::unique_ptr target; ++ if (type == Inspector::InspectorTargetType::Page) ++ target = WebPageInspectorTargetProxy::create(m_page, *m_targetAgent, targetId); ++ else ++ target = InspectorTargetProxy::create(m_page, targetId, type); ++ addTarget(WTFMove(target)); + } + + void WebPageInspectorController::destroyInspectorTarget(const String& targetId) +@@ -169,7 +219,7 @@ void WebPageInspectorController::sendMessageToInspectorFrontend(const String& ta + + void WebPageInspectorController::didCreateProvisionalPage(ProvisionalPageProxy& provisionalPage) + { +- addTarget(InspectorTargetProxy::create(provisionalPage, getTargetID(provisionalPage), Inspector::InspectorTargetType::Page)); ++ addTarget(WebPageInspectorTargetProxy::create(provisionalPage, *m_targetAgent, getTargetID(provisionalPage))); + } + + void WebPageInspectorController::willDestroyProvisionalPage(const ProvisionalPageProxy& provisionalPage) +diff --git a/Source/WebKit/UIProcess/WebPageInspectorController.h b/Source/WebKit/UIProcess/WebPageInspectorController.h +index 828bc3ccc7e..40a333b7004 100644 +--- a/Source/WebKit/UIProcess/WebPageInspectorController.h ++++ b/Source/WebKit/UIProcess/WebPageInspectorController.h +@@ -48,7 +48,13 @@ public: + WebPageInspectorController(WebPageProxy&); + + void init(); ++ void didFinishAttachingToWebProcess(); ++ ++ using CreationListener = std::function; ++ static void setCreationListener(CreationListener); ++ + void pageClosed(); ++ void didProcessAllPendingKeyboardEvents(); + + bool hasLocalFrontend() const; + +@@ -57,6 +63,8 @@ public: + void disconnectAllFrontends(); + + void dispatchMessageFromFrontend(const String& message); ++ Inspector::InspectorTarget* findTarget(const String& targetId); ++ bool dispatchMessageToTargetBackend(const String&); + + #if ENABLE(REMOTE_INSPECTOR) + void setIndicating(bool); +diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp +new file mode 100644 +index 00000000000..415f36c5647 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp +@@ -0,0 +1,61 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorEmulationAgent.h" ++ ++#include "WebPageProxy.h" ++#include ++ ++ ++namespace WebKit { ++ ++using namespace Inspector; ++ ++WebPageInspectorEmulationAgent::WebPageInspectorEmulationAgent(BackendDispatcher& backendDispatcher, WebPageProxy& page) ++ : InspectorAgentBase("Emulation"_s) ++ , m_backendDispatcher(EmulationBackendDispatcher::create(backendDispatcher, this)) ++ , m_page(page) ++{ ++} ++ ++WebPageInspectorEmulationAgent::~WebPageInspectorEmulationAgent() ++{ ++} ++ ++void WebPageInspectorEmulationAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) ++{ ++} ++ ++void WebPageInspectorEmulationAgent::willDestroyFrontendAndBackend(DisconnectReason) ++{ ++} ++ ++void WebPageInspectorEmulationAgent::setDeviceMetricsOverride(ErrorString& error, int in_width, int in_height) ++{ ++ platformSetSize(error, in_width, in_height); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h +new file mode 100644 +index 00000000000..2f9b7807e23 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h +@@ -0,0 +1,63 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include ++#include ++ ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class FrontendChannel; ++class FrontendRouter; ++} ++ ++namespace WebKit { ++ ++class WebPageProxy; ++ ++class WebPageInspectorEmulationAgent : public Inspector::InspectorAgentBase, public Inspector::EmulationBackendDispatcherHandler { ++ WTF_MAKE_NONCOPYABLE(WebPageInspectorEmulationAgent); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ WebPageInspectorEmulationAgent(Inspector::BackendDispatcher& backendDispatcher, WebPageProxy& page); ++ ~WebPageInspectorEmulationAgent() override; ++ ++ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; ++ ++ void setDeviceMetricsOverride(Inspector::ErrorString&, int in_width, int in_height) override; ++ ++private: ++ void platformSetSize(String& error, int width, int height); ++ ++ Ref m_backendDispatcher; ++ WebPageProxy& m_page; ++}; ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp +new file mode 100644 +index 00000000000..16a05604460 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp +@@ -0,0 +1,257 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorInputAgent.h" ++ ++#include "APINavigation.h" ++#include "NativeWebKeyboardEvent.h" ++#include "NativeWebMouseEvent.h" ++#include "WebPageProxy.h" ++#include ++ ++ ++namespace WebKit { ++ ++using namespace Inspector; ++ ++namespace { ++ ++template ++class CallbackList { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ ~CallbackList() ++ { ++ for (const auto& callback : m_callbacks) ++ callback->sendFailure("Page closed"); ++ } ++ ++ void append(Ref&& callback) ++ { ++ m_callbacks.append(WTFMove(callback)); ++ } ++ ++ void sendSuccess() ++ { ++ for (const auto& callback : m_callbacks) ++ callback->sendSuccess(); ++ m_callbacks.clear(); ++ } ++ ++private: ++ Vector> m_callbacks; ++}; ++ ++} // namespace ++ ++class WebPageInspectorInputAgent::InspectorInputObserver : public WebPageProxy::InputProcessingObserver { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ using KeyboardCallback = Inspector::InputBackendDispatcherHandler::DispatchKeyEventCallback; ++ using MouseCallback = Inspector::InputBackendDispatcherHandler::DispatchMouseEventCallback; ++ ++ ~InspectorInputObserver() override = default; ++ ++ void didProcessAllPendingKeyboardEvents() override ++ { ++ m_keyboardCallbacks.sendSuccess(); ++ } ++ ++ void didProcessAllPendingMouseEvents() override ++ { ++ m_mouseCallbacks.sendSuccess(); ++ } ++ ++ void addMouseCallback(Ref&& callback) ++ { ++ m_mouseCallbacks.append(WTFMove(callback)); ++ } ++ ++ void addKeyboardCallback(Ref&& callback) ++ { ++ m_keyboardCallbacks.append(WTFMove(callback)); ++ } ++ ++private: ++ CallbackList m_keyboardCallbacks; ++ CallbackList m_mouseCallbacks; ++}; ++ ++WebPageInspectorInputAgent::WebPageInspectorInputAgent(Inspector::BackendDispatcher& backendDispatcher, WebPageProxy& page) ++ : InspectorAgentBase("Input"_s) ++ , m_backendDispatcher(InputBackendDispatcher::create(backendDispatcher, this)) ++ , m_page(page) ++ , m_inputObserver(makeUnique()) ++{ ++ m_page.setObserber(m_inputObserver.get()); ++} ++ ++WebPageInspectorInputAgent::~WebPageInspectorInputAgent() ++{ ++ m_page.setObserber(nullptr); ++} ++ ++void WebPageInspectorInputAgent::didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) ++{ ++} ++ ++void WebPageInspectorInputAgent::willDestroyFrontendAndBackend(Inspector::DisconnectReason) ++{ ++} ++ ++void WebPageInspectorInputAgent::dispatchKeyEvent(const String& in_type, const int* opt_in_modifiers, const String* opt_in_text, const String* opt_in_unmodifiedText, const String* opt_in_code, const String* opt_in_key, const int* opt_in_windowsVirtualKeyCode, const int* opt_in_nativeVirtualKeyCode, const bool* opt_in_autoRepeat, const bool* opt_in_isKeypad, const bool* opt_in_isSystemKey, Ref&& callback) ++{ ++ WebKit::WebEvent::Type type; ++ if (in_type == "keyDown") { ++ type = WebKit::WebEvent::KeyDown; ++ } else if (in_type == "keyUp") { ++ type = WebKit::WebEvent::KeyUp; ++ } else { ++ callback->sendFailure("Unsupported event type."); ++ return; ++ } ++ OptionSet modifiers; ++ if (opt_in_modifiers) ++ modifiers = modifiers.fromRaw(*opt_in_modifiers); ++ String text; ++ if (opt_in_text) ++ text = *opt_in_text; ++ String unmodifiedText; ++ if (opt_in_unmodifiedText) ++ unmodifiedText = *opt_in_unmodifiedText; ++ String code; ++ if (opt_in_code) ++ code = *opt_in_code; ++ String key; ++ if (opt_in_key) ++ key = *opt_in_key; ++ int windowsVirtualKeyCode = 0; ++ if (opt_in_windowsVirtualKeyCode) ++ windowsVirtualKeyCode = *opt_in_windowsVirtualKeyCode; ++ int nativeVirtualKeyCode = 0; ++ if (opt_in_nativeVirtualKeyCode) ++ nativeVirtualKeyCode = *opt_in_nativeVirtualKeyCode; ++ bool isAutoRepeat = false; ++ if (opt_in_autoRepeat) ++ isAutoRepeat = *opt_in_autoRepeat; ++ bool isKeypad = false; ++ if (opt_in_isKeypad) ++ isKeypad = *opt_in_isKeypad; ++ bool isSystemKey = false; ++ if (opt_in_isSystemKey) ++ isSystemKey = *opt_in_isSystemKey; ++ WallTime timestamp = WallTime::now(); ++ ++ m_inputObserver->addKeyboardCallback(WTFMove(callback)); ++ platformDispatchKeyEvent( ++ type, ++ text, ++ unmodifiedText, ++ key, ++ code, ++ windowsVirtualKeyCode, ++ nativeVirtualKeyCode, ++ isAutoRepeat, ++ isKeypad, ++ isSystemKey, ++ modifiers, ++ timestamp); ++} ++ ++void WebPageInspectorInputAgent::dispatchMouseEvent(const String& in_type, int in_x, int in_y, const int* opt_in_modifiers, const String* opt_in_button, const int* opt_in_buttons, const int* opt_in_clickCount, const int* opt_in_deltaX, const int* opt_in_deltaY, Ref&& callback) ++{ ++ WebEvent::Type type = WebEvent::NoType; ++ if (in_type == "down") ++ type = WebEvent::MouseDown; ++ else if (in_type == "up") ++ type = WebEvent::MouseUp; ++ else if (in_type == "move") ++ type = WebEvent::MouseMove; ++ else { ++ callback->sendFailure("Unsupported event type"); ++ return; ++ } ++ ++ OptionSet modifiers; ++ if (opt_in_modifiers) ++ modifiers = modifiers.fromRaw(*opt_in_modifiers); ++ ++ WebMouseEvent::Button button = WebMouseEvent::NoButton; ++ if (opt_in_button) { ++ if (*opt_in_button == "left") ++ button = WebMouseEvent::LeftButton; ++ else if (*opt_in_button == "middle") ++ button = WebMouseEvent::MiddleButton; ++ else if (*opt_in_button == "right") ++ button = WebMouseEvent::RightButton; ++ else if (*opt_in_button == "none") ++ button = WebMouseEvent::NoButton; ++ else { ++ callback->sendFailure("Unsupported button"); ++ return; ++ } ++ } ++ ++ unsigned short buttons = 0; ++ if (opt_in_buttons) ++ buttons = *opt_in_buttons; ++ ++ int clickCount = 0; ++ if (opt_in_clickCount) ++ clickCount = *opt_in_clickCount; ++ int deltaX = 0; ++ if (opt_in_deltaX) ++ deltaX = *opt_in_deltaX; ++ int deltaY = 0; ++ if (opt_in_deltaY) ++ deltaY = *opt_in_deltaY; ++ m_inputObserver->addMouseCallback(WTFMove(callback)); ++#if PLATFORM(WPE) ++ platformDispatchMouseEvent(type, in_x, in_y, button, modifiers); ++#elif PLATFORM(GTK) ++ WallTime timestamp = WallTime::now(); ++ NativeWebMouseEvent event( ++ type, ++ button, ++ buttons, ++ {in_x, in_y}, ++ WebCore::IntPoint(), ++ deltaX, ++ deltaY, ++ 0, ++ clickCount, ++ modifiers, ++ timestamp); ++ m_page.handleMouseEvent(event); ++#endif ++} ++ ++void WebPageInspectorInputAgent::goBack(Inspector::ErrorString&) ++{ ++ auto navigation = m_page.goBack(); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h +new file mode 100644 +index 00000000000..9d51a913b23 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h +@@ -0,0 +1,76 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "WebEvent.h" ++ ++#include ++#include ++ ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class FrontendChannel; ++class FrontendRouter; ++} ++ ++namespace WebKit { ++ ++class NativeWebKeyboardEvent; ++class WebPageProxy; ++ ++class WebPageInspectorInputAgent : public Inspector::InspectorAgentBase, public Inspector::InputBackendDispatcherHandler { ++ WTF_MAKE_NONCOPYABLE(WebPageInspectorInputAgent); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ WebPageInspectorInputAgent(Inspector::BackendDispatcher& backendDispatcher, WebPageProxy& page); ++ ~WebPageInspectorInputAgent() override; ++ ++ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; ++ ++ void dispatchKeyEvent(const String& in_type, const int* opt_in_modifiers, const String* opt_in_text, const String* opt_in_unmodifiedText, const String* opt_in_code, const String* opt_in_key, const int* opt_in_windowsVirtualKeyCode, const int* opt_in_nativeVirtualKeyCode, const bool* opt_in_autoRepeat, const bool* opt_in_isKeypad, const bool* opt_in_isSystemKey, Ref&& callback) override; ++ void dispatchMouseEvent(const String& in_type, int in_x, int in_y, const int* opt_in_modifiers, const String* opt_in_button, const int* opt_in_buttons, const int* opt_in_clickCount, const int* opt_in_deltaX, const int* opt_in_deltaY, Ref&& callback) override; ++ void goBack(Inspector::ErrorString&) override; ++ ++private: ++ void platformDispatchKeyEvent(WebKeyboardEvent::Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp); ++#if PLATFORM(WPE) ++ void platformDispatchMouseEvent(WebMouseEvent::Type type, int x, int y, WebMouseEvent::Button button, OptionSet modifiers); ++#endif ++ ++ Ref m_backendDispatcher; ++ WebPageProxy& m_page; ++ // Keep track of currently active modifiers across multiple keystrokes. ++ // Most platforms do not track current modifiers from synthesized events. ++ unsigned m_currentModifiers { 0 }; ++ class InspectorInputObserver; ++ std::unique_ptr m_inputObserver; ++}; ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.cpp b/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.cpp +new file mode 100644 +index 00000000000..a697c3f5355 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.cpp +@@ -0,0 +1,129 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorTargetProxy.h" ++ ++#include "ProvisionalPageProxy.h" ++#include "WebPageInspectorController.h" ++#include "WebPageInspectorEmulationAgent.h" ++#include "WebPageInspectorInputAgent.h" ++#include "WebPageMessages.h" ++#include "WebPageProxy.h" ++#include "WebProcessProxy.h" ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++ ++using namespace Inspector; ++ ++namespace { ++ ++class TargetFrontendChannel final : public FrontendChannel { ++ WTF_MAKE_NONCOPYABLE(TargetFrontendChannel); ++public: ++ TargetFrontendChannel(InspectorTargetAgent& targetAgent, const String& targetId, FrontendChannel::ConnectionType type) ++ : m_targetAgent(targetAgent) ++ , m_targetId(targetId) ++ , m_connectionType(type) ++ { ++ } ++ ~TargetFrontendChannel() override = default; ++ ++ ConnectionType connectionType() const override { return m_connectionType; } ++ void sendMessageToFrontend(const String& message) override ++ { ++ m_targetAgent.sendMessageFromTargetToFrontend(m_targetId, message); ++ } ++ ++private: ++ InspectorTargetAgent& m_targetAgent; ++ String m_targetId; ++ FrontendChannel::ConnectionType m_connectionType; ++}; ++ ++} // namespace ++ ++std::unique_ptr WebPageInspectorTargetProxy::create(WebPageProxy& page, Inspector::InspectorTargetAgent& targetAgent, const String& targetId) ++{ ++ return makeUnique(page, nullptr, targetAgent, targetId); ++} ++ ++std::unique_ptr WebPageInspectorTargetProxy::create(ProvisionalPageProxy& provisionalPage, Inspector::InspectorTargetAgent& targetAgent, const String& targetId) ++{ ++ return makeUnique(provisionalPage.page(), &provisionalPage, targetAgent, targetId); ++} ++ ++WebPageInspectorTargetProxy::WebPageInspectorTargetProxy(WebPageProxy& page, ProvisionalPageProxy* provisionalPage, Inspector::InspectorTargetAgent& targetAgent, const String& targetId) ++ : InspectorTargetProxy(page, provisionalPage, targetId, Inspector::InspectorTargetType::Page) ++ , m_frontendRouter(FrontendRouter::create()) ++ , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) ++ , m_targetAgent(targetAgent) ++{ ++ m_agents.append(std::make_unique(m_backendDispatcher.get(), page)); ++ m_agents.append(std::make_unique(m_backendDispatcher.get(), page)); ++} ++ ++void WebPageInspectorTargetProxy::connect(Inspector::FrontendChannel::ConnectionType connectionType) ++{ ++ InspectorTargetProxy::connect(connectionType); ++ ASSERT(!m_frontendChannel); ++ if (m_frontendChannel) ++ return; ++ m_frontendChannel = std::make_unique(m_targetAgent, identifier(), connectionType); ++ m_frontendRouter->connectFrontend(*m_frontendChannel); ++} ++ ++void WebPageInspectorTargetProxy::disconnect() ++{ ++ ASSERT(m_frontendChannel); ++ m_frontendRouter->disconnectAllFrontends(); ++ m_frontendChannel.reset(); ++ InspectorTargetProxy::disconnect(); ++} ++ ++void WebPageInspectorTargetProxy::sendMessageToTargetBackend(const String& message) ++{ ++ if (m_backendDispatcher->dispatch(message, BackendDispatcher::Mode::ContinueIfDomainIsMissing) == BackendDispatcher::DispatchResult::Finished) ++ return; ++ if (m_page.inspectorController().dispatchMessageToTargetBackend(message)) ++ return; ++ InspectorTargetProxy::sendMessageToTargetBackend(message); ++} ++ ++void WebPageInspectorTargetProxy::activate(String& error) ++{ ++ platformActivate(error); ++} ++ ++void WebPageInspectorTargetProxy::close(String& error) ++{ ++ m_page.closePage(false); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.h b/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.h +new file mode 100644 +index 00000000000..0550a3d8698 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.h +@@ -0,0 +1,67 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "InspectorTargetProxy.h" ++#include ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class InspectorTargetAgent; ++} ++ ++namespace WebKit { ++ ++class WebPageProxy; ++ ++class WebPageInspectorTargetProxy final : public InspectorTargetProxy { ++ WTF_MAKE_FAST_ALLOCATED; ++ WTF_MAKE_NONCOPYABLE(WebPageInspectorTargetProxy); ++public: ++ static std::unique_ptr create(WebPageProxy&, Inspector::InspectorTargetAgent&, const String& targetId); ++ static std::unique_ptr create(ProvisionalPageProxy&, Inspector::InspectorTargetAgent&, const String& targetId); ++ WebPageInspectorTargetProxy(WebPageProxy&, ProvisionalPageProxy*, Inspector::InspectorTargetAgent&, const String& targetId); ++ ~WebPageInspectorTargetProxy() = default; ++ ++ void connect(Inspector::FrontendChannel::ConnectionType) override; ++ void disconnect() override; ++ void sendMessageToTargetBackend(const String&) override; ++ void activate(String& error) override; ++ void close(String& error) override; ++ ++private: ++ void platformActivate(String& error) const; ++ ++ Ref m_frontendRouter; ++ Ref m_backendDispatcher; ++ Inspector::InspectorTargetAgent& m_targetAgent; ++ Inspector::AgentRegistry m_agents; ++ std::unique_ptr m_frontendChannel; ++}; ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp +index c6b66746b18..3bed21548da 100644 +--- a/Source/WebKit/UIProcess/WebPageProxy.cpp ++++ b/Source/WebKit/UIProcess/WebPageProxy.cpp +@@ -865,6 +865,7 @@ void WebPageProxy::finishAttachingToWebProcess(ProcessLaunchReason reason) + m_pageLoadState.didSwapWebProcesses(); + if (reason != ProcessLaunchReason::InitialProcess) + m_drawingArea->waitForBackingStoreUpdateOnNextPaint(); ++ m_inspectorController->didFinishAttachingToWebProcess(); + } + + void WebPageProxy::didAttachToRunningProcess() +@@ -1616,6 +1617,11 @@ void WebPageProxy::setControlledByAutomation(bool controlled) + m_process->processPool().sendToNetworkingProcess(Messages::NetworkProcess::SetSessionIsControlledByAutomation(m_websiteDataStore->sessionID(), m_controlledByAutomation)); + } + ++void WebPageProxy::setObserber(InputProcessingObserver* observer) ++{ ++ m_inputProcessingObserver = observer; ++} ++ + void WebPageProxy::createInspectorTarget(const String& targetId, Inspector::InspectorTargetType type) + { + m_inspectorController->createInspectorTarget(targetId, type); +@@ -6509,6 +6515,8 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) + if (auto* automationSession = process().processPool().automationSession()) + automationSession->mouseEventsFlushedForPage(*this); + pageClient().didFinishProcessingAllPendingMouseEvents(); ++ if (m_inputProcessingObserver) ++ m_inputProcessingObserver->didProcessAllPendingMouseEvents(); + } + + break; +@@ -6535,7 +6543,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) + case WebEvent::RawKeyDown: + case WebEvent::Char: { + LOG(KeyHandling, "WebPageProxy::didReceiveEvent: %s (queue empty %d)", webKeyboardEventTypeString(type), m_keyEventQueue.isEmpty()); +- + MESSAGE_CHECK(m_process, !m_keyEventQueue.isEmpty()); + NativeWebKeyboardEvent event = m_keyEventQueue.takeFirst(); + +@@ -6550,7 +6557,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) + // The call to doneWithKeyEvent may close this WebPage. + // Protect against this being destroyed. + Ref protect(*this); +- + pageClient().doneWithKeyEvent(event, handled); + if (!handled) + m_uiClient->didNotHandleKeyEvent(this, event); +@@ -6559,6 +6565,8 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) + if (!canProcessMoreKeyEvents) { + if (auto* automationSession = process().processPool().automationSession()) + automationSession->keyboardEventsFlushedForPage(*this); ++ if (m_inputProcessingObserver) ++ m_inputProcessingObserver->didProcessAllPendingKeyboardEvents(); + } + break; + } +diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h +index b84fb9e0ef3..9357ec52c83 100644 +--- a/Source/WebKit/UIProcess/WebPageProxy.h ++++ b/Source/WebKit/UIProcess/WebPageProxy.h +@@ -534,6 +534,14 @@ public: + + void setPageLoadStateObserver(std::unique_ptr&&); + ++ class InputProcessingObserver { ++ public: ++ virtual ~InputProcessingObserver() = default; ++ virtual void didProcessAllPendingKeyboardEvents() = 0; ++ virtual void didProcessAllPendingMouseEvents() = 0; ++ }; ++ void setObserber(InputProcessingObserver*); ++ + void initializeWebPage(); + void setDrawingArea(std::unique_ptr&&); + +@@ -2569,6 +2577,7 @@ private: + #if ENABLE(REMOTE_INSPECTOR) + std::unique_ptr m_inspectorDebuggable; + #endif ++ InputProcessingObserver* m_inputProcessingObserver { nullptr }; + + Optional m_spellDocumentTag; + +diff --git a/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.cpp b/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.cpp +new file mode 100644 +index 00000000000..665265973c3 +--- /dev/null ++++ b/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.cpp +@@ -0,0 +1,130 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "InspectorBrowserAgentClientGLib.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "InspectorBrowserAgent.h" ++#include "WebKitBrowserInspectorPrivate.h" ++#include "WebKitWebContextPrivate.h" ++#include "WebKitWebsiteDataManagerPrivate.h" ++#include "WebKitWebViewPrivate.h" ++#include "WebPageProxy.h" ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++ ++static Vector collectPages(Optional sessionID) ++{ ++ Vector result; ++ for (WebProcessPool* pool : WebProcessPool::allProcessPools()) { ++ for (auto& process : pool->processes()) { ++ for (auto* page : process->pages()) { ++ if (!sessionID || page->sessionID() == sessionID) ++ result.append(page); ++ } ++ } ++ } ++ return result; ++} ++ ++static void closeAllPages(Optional sessionID) ++{ ++ Vector pages = collectPages(sessionID); ++ for (auto* page : pages) ++ page->closePage(false); ++} ++ ++InspectorBrowserAgentClientGlib::InspectorBrowserAgentClientGlib() ++{ ++} ++ ++RefPtr InspectorBrowserAgentClientGlib::createPage(WTF::String& error, const WTF::String* browserContextID) ++{ ++ WebKitWebContext* context = webkit_web_context_get_default(); ++ if (browserContextID != nullptr) { ++ context = m_idToContext.get(*browserContextID); ++ if (context == nullptr) { ++ error = "Context with provided id not found"; ++ return nullptr; ++ } ++ } ++ RefPtr page = webkitBrowserInspectorCreateNewPageInContext(context); ++ if (page == nullptr) ++ error = "Failed to create new page in the context"; ++ return page; ++} ++ ++void InspectorBrowserAgentClientGlib::closeAllWindows() ++{ ++ closeAllPages(Optional()); ++ m_idToContext.clear(); ++ // FIXME(yurys): call g_main_loop_quit() ? ++} ++ ++static PAL::SessionID sessionIDFromContext(WebKitWebContext* context) ++{ ++ WebKitWebsiteDataManager* data_manager = webkit_web_context_get_website_data_manager(context); ++ WebsiteDataStore& websiteDataStore = webkitWebsiteDataManagerGetDataStore(data_manager); ++ return websiteDataStore.sessionID(); ++} ++ ++void InspectorBrowserAgentClientGlib::createBrowserContext(WTF::String& error, WTF::String* browserContextID) ++{ ++ GRefPtr manager = adoptGRef(webkit_website_data_manager_new_ephemeral()); ++ GRefPtr context = adoptGRef(WEBKIT_WEB_CONTEXT(g_object_new(WEBKIT_TYPE_WEB_CONTEXT, "website-data-manager", manager.get(), "process-swap-on-cross-site-navigation-enabled", true, nullptr))); ++ if (!context) { ++ error = "Failed to create GLib ephemeral context"; ++ return; ++ } ++ PAL::SessionID sessionID = sessionIDFromContext(context.get()); ++ String id = InspectorBrowserAgent::toBrowserContextIDProtocolString(sessionID); ++ m_idToContext.set(id, WTFMove(context)); ++ *browserContextID = id; ++} ++ ++void InspectorBrowserAgentClientGlib::deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) ++{ ++ GRefPtr context = m_idToContext.take(browserContextID); ++ if (context == nullptr) { ++ error = "Context with provided id not found"; ++ return; ++ } ++ closeAllPages(sessionIDFromContext(context.get())); ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.h b/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.h +new file mode 100644 +index 00000000000..0fefb4c55b3 +--- /dev/null ++++ b/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.h +@@ -0,0 +1,63 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "InspectorBrowserAgentClient.h" ++#include "WebKitWebContext.h" ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClientGlib : public InspectorBrowserAgentClient { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ InspectorBrowserAgentClientGlib(); ++ ~InspectorBrowserAgentClientGlib() override = default; ++ ++ RefPtr createPage(WTF::String& error, const WTF::String* browserContextID) override; ++ void closeAllWindows() override; ++ void createBrowserContext(WTF::String& error, WTF::String* browserContextID) override; ++ void deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) override; ++ ++private: ++ WebKitWebContext* findContext(WTF::String& error, const WTF::String& browserContextID); ++ ++ HashMap> m_idToContext; ++}; ++ ++} // namespace API ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp +new file mode 100644 +index 00000000000..25df994c053 +--- /dev/null ++++ b/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp +@@ -0,0 +1,58 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorEmulationAgent.h" ++ ++#include "WebPageProxy.h" ++#include ++ ++namespace WebKit { ++void WebPageInspectorEmulationAgent::platformSetSize(String& error, int width, int height) ++{ ++ GtkWidget* viewWidget = m_page.viewWidget(); ++ GtkWidget* window = gtk_widget_get_toplevel(viewWidget); ++ if (!window) { ++ error = "Cannot find parent window"; ++ return; ++ } ++ if (!GTK_IS_WINDOW(window)) { ++ error = "Toplevel is not a window"; ++ return; ++ } ++ GtkAllocation viewAllocation; ++ gtk_widget_get_allocation(viewWidget, &viewAllocation); ++ ++ GtkAllocation windowAllocation; ++ gtk_widget_get_allocation(window, &windowAllocation); ++ ++ width += windowAllocation.width - viewAllocation.width; ++ height += windowAllocation.height - viewAllocation.height; ++ ++ gtk_window_resize(GTK_WINDOW(window), width, height); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp +new file mode 100644 +index 00000000000..2427ea22acf +--- /dev/null ++++ b/Source/WebKit/UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp +@@ -0,0 +1,108 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorInputAgent.h" ++ ++#include "KeyBindingTranslator.h" ++#include "NativeWebKeyboardEvent.h" ++#include "WebPageProxy.h" ++#include ++ ++namespace WebKit { ++ ++static Vector commandsForKeyEvent(GdkEventType type, unsigned keyVal, unsigned state) ++{ ++ ASSERT(type == GDK_KEY_PRESS || type == GDK_KEY_RELEASE); ++ ++ GUniquePtr event(gdk_event_new(type)); ++ event->key.keyval = keyVal; ++ event->key.time = GDK_CURRENT_TIME; ++ event->key.state = state; ++ // When synthesizing an event, an invalid hardware_keycode value can cause it to be badly processed by GTK+. ++ GUniqueOutPtr keys; ++ int keysCount; ++ if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(), keyVal, &keys.outPtr(), &keysCount) && keysCount) ++ event->key.hardware_keycode = keys.get()[0].keycode; ++ return KeyBindingTranslator().commandsForKeyEvent(&event->key); ++} ++ ++static unsigned modifiersToEventState(OptionSet modifiers) ++{ ++ unsigned state = 0; ++ if (modifiers.contains(WebEvent::Modifier::ControlKey)) ++ state |= GDK_CONTROL_MASK; ++ if (modifiers.contains(WebEvent::Modifier::ShiftKey)) ++ state |= GDK_SHIFT_MASK; ++ if (modifiers.contains(WebEvent::Modifier::AltKey)) ++ state |= GDK_META_MASK; ++ if (modifiers.contains(WebEvent::Modifier::CapsLockKey)) ++ state |= GDK_LOCK_MASK; ++ return state; ++} ++ ++void WebPageInspectorInputAgent::platformDispatchKeyEvent(WebKeyboardEvent::Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp) ++{ ++ Vector commands; ++ const guint keyVal = WebCore::PlatformKeyboardEvent::gdkKeyCodeForWindowsKeyCode(windowsVirtualKeyCode); ++ String keyIdentifier; ++ if (keyVal) { ++ GdkEventType event = GDK_NOTHING; ++ switch (type) ++ { ++ case WebKeyboardEvent::KeyDown: ++ event = GDK_KEY_PRESS; ++ break; ++ case WebKeyboardEvent::KeyUp: ++ event = GDK_KEY_RELEASE; ++ break; ++ default: ++ fprintf(stderr, "Unsupported event type = %d\n", type); ++ break; ++ } ++ unsigned state = modifiersToEventState(modifiers); ++ commands = commandsForKeyEvent(event, keyVal, state); ++ keyIdentifier = WebCore::PlatformKeyboardEvent::keyIdentifierForGdkKeyCode(keyVal); ++ } ++ NativeWebKeyboardEvent event( ++ type, ++ text, ++ unmodifiedText, ++ key, ++ code, ++ keyIdentifier, ++ windowsVirtualKeyCode, ++ nativeVirtualKeyCode, ++ isAutoRepeat, ++ isKeypad, ++ isSystemKey, ++ modifiers, ++ timestamp, ++ WTFMove(commands)); ++ m_page.handleKeyboardEvent(event); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp +new file mode 100644 +index 00000000000..b6981cae157 +--- /dev/null ++++ b/Source/WebKit/UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp +@@ -0,0 +1,45 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorTargetProxy.h" ++ ++#include "WebPageProxy.h" ++#include ++#include ++ ++namespace WebKit { ++ ++void WebPageInspectorTargetProxy::platformActivate(String& error) const ++{ ++ GtkWidget* parent = gtk_widget_get_toplevel(m_page.viewWidget()); ++ if (WebCore::widgetIsOnscreenToplevelWindow(parent)) ++ gtk_window_present(GTK_WINDOW(parent)); ++ else ++ error = "The view is not on screen"; ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm b/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm +index e139968d4f0..98093c684db 100644 +--- a/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm ++++ b/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm +@@ -408,6 +408,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) + + void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool eventWasHandled) + { ++ if (!event.nativeEvent()) ++ return; + [m_contentView _didHandleKeyEvent:event.nativeEvent() eventWasHandled:eventWasHandled]; + } + +diff --git a/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.h b/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.h +new file mode 100644 +index 00000000000..719a0bb54d7 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.h +@@ -0,0 +1,56 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "InspectorBrowserAgentClient.h" ++#include ++ ++OBJC_PROTOCOL(_WKBrowserInspectorDelegate); ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClientMac : public InspectorBrowserAgentClient { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ InspectorBrowserAgentClientMac(_WKBrowserInspectorDelegate* delegate); ++ ~InspectorBrowserAgentClientMac() override = default; ++ ++ RefPtr createPage(WTF::String& error, const WTF::String* browserContextID) override; ++ void closeAllWindows() override; ++ void createBrowserContext(WTF::String& error, WTF::String* browserContextID) override; ++ void deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) override; ++ private: ++ ++ _WKBrowserInspectorDelegate* delegate_; ++}; ++ ++ ++} // namespace API +diff --git a/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.mm b/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.mm +new file mode 100644 +index 00000000000..8426bd70ba9 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.mm +@@ -0,0 +1,95 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#import "config.h" ++#import "InspectorBrowserAgentClientMac.h" ++ ++#import ++#import ++#import "WebPageProxy.h" ++#import "WebProcessPool.h" ++#import "_WKBrowserInspector.h" ++#import "WKWebView.h" ++#import "WKWebViewInternal.h" ++ ++namespace WebKit { ++ ++/* ++static Vector collectPages(Optional sessionID) ++{ ++ Vector result; ++ for (WebProcessPool* pool : WebProcessPool::allProcessPools()) { ++ for (auto& process : pool->processes()) { ++ for (auto* page : process->pages()) { ++ if (!sessionID || page->sessionID() == sessionID) ++ result.append(page); ++ } ++ } ++ } ++ return result; ++} ++ ++static void closeAllPages(Optional sessionID) ++{ ++ Vector pages = collectPages(sessionID); ++ for (auto* page : pages) ++ page->closePage(false); ++} ++*/ ++ ++InspectorBrowserAgentClientMac::InspectorBrowserAgentClientMac(_WKBrowserInspectorDelegate* delegate) ++ : delegate_(delegate) ++{ ++} ++ ++RefPtr InspectorBrowserAgentClientMac::createPage(WTF::String& error, const WTF::String* browserContextID) ++{ ++ WKWebView *webView = [delegate_ createNewPage]; ++ return [webView _page]; ++} ++ ++void InspectorBrowserAgentClientMac::closeAllWindows() ++{ ++ [delegate_ quit]; ++} ++ ++void InspectorBrowserAgentClientMac::createBrowserContext(WTF::String& error, WTF::String* browserContextID) ++{ ++ error = "Failed to create Mac ephemeral context"; ++ fprintf(stderr, "InspectorBrowserAgentClientMac::createBrowserContext - NOT IMPLEMENTED\n"); ++} ++ ++void InspectorBrowserAgentClientMac::deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) ++{ ++ error = "Context with provided id not found"; ++ fprintf(stderr, "InspectorBrowserAgentClientMac::deleteBrowserContext - NOT IMPLEMENTED\n"); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm +index 22653d74398..bf27558fdfd 100644 +--- a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm ++++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm +@@ -455,6 +455,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) + + void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool eventWasHandled) + { ++ if (!event.nativeEvent()) ++ return; + m_impl->doneWithKeyEvent(event.nativeEvent(), eventWasHandled); + } + +@@ -930,6 +932,9 @@ void PageClientImpl::didRestoreScrollPosition() + + bool PageClientImpl::windowIsFrontWindowUnderMouse(const NativeWebMouseEvent& event) + { ++ // Simulated event. ++ if (!event.nativeEvent()) ++ return false; + return m_impl->windowIsFrontWindowUnderMouse(event.nativeEvent()); + } + +diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorEmulationAgentMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorEmulationAgentMac.mm +new file mode 100644 +index 00000000000..857195777b5 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/WebPageInspectorEmulationAgentMac.mm +@@ -0,0 +1,42 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorEmulationAgent.h" ++ ++#include "WebPageProxy.h" ++ ++namespace WebKit { ++void WebPageInspectorEmulationAgent::platformSetSize(String& error, int width, int height) ++{ ++ NSWindow* window = m_page.platformWindow(); ++ NSRect frame = [window frame]; ++ frame.origin.y += frame.size.height; ++ frame.origin.y -= height; ++ frame.size = NSMakeSize(width, height); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm +new file mode 100644 +index 00000000000..0f09fd52ae1 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm +@@ -0,0 +1,37 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorInputAgent.h" ++ ++namespace WebKit { ++ ++void WebPageInspectorInputAgent::platformDispatchKeyEvent(WebKeyboardEvent::Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp) ++{ ++ fprintf(stderr, "Mac does not support dispatching key events"); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm +new file mode 100644 +index 00000000000..b0f0172a028 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm +@@ -0,0 +1,41 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorTargetProxy.h" ++ ++#if PLATFORM(MAC) ++ ++namespace WebKit { ++ ++void WebPageInspectorTargetProxy::platformActivate(String& error) const ++{ ++ error = "Not Implemented"; ++} ++ ++} // namespace WebKit ++ ++#endif +diff --git a/Source/WebKit/UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp b/Source/WebKit/UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp +new file mode 100644 +index 00000000000..5465c0ae99d +--- /dev/null ++++ b/Source/WebKit/UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp +@@ -0,0 +1,41 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorEmulationAgent.h" ++ ++#include "WebPageProxy.h" ++#include ++ ++namespace WebKit { ++ ++void WebPageInspectorEmulationAgent::platformSetSize(String& error, int width, int height) ++{ ++ struct wpe_view_backend* backend = m_page.viewBackend(); ++ wpe_view_backend_dispatch_set_size(backend, width, height); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp b/Source/WebKit/UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp +new file mode 100644 +index 00000000000..772ca6bc674 +--- /dev/null ++++ b/Source/WebKit/UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp +@@ -0,0 +1,99 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorInputAgent.h" ++ ++#include "NativeWebKeyboardEvent.h" ++#include "WebPageProxy.h" ++#include ++#include ++ ++namespace WebKit { ++ ++void WebPageInspectorInputAgent::platformDispatchKeyEvent(String& error, const String& type, const String& keyRef) ++{ ++} ++ ++static unsigned toWPEButton(WebMouseEvent::Button button) ++{ ++ switch (button) { ++ case WebMouseEvent::NoButton: ++ case WebMouseEvent::LeftButton: ++ return 1; ++ case WebMouseEvent::MiddleButton: ++ return 3; ++ case WebMouseEvent::RightButton: ++ return 2; ++ } ++ return 1; ++} ++ ++static unsigned toWPEModifiers(OptionSet modifiers) ++{ ++ unsigned result = 0; ++ if (modifiers.contains(WebEvent::Modifier::ControlKey)) ++ result |= wpe_input_keyboard_modifier_control; ++ if (modifiers.contains(WebEvent::Modifier::ShiftKey)) ++ result |= wpe_input_keyboard_modifier_shift; ++ if (modifiers.contains(WebEvent::Modifier::AltKey)) ++ result |= wpe_input_keyboard_modifier_alt; ++ if (modifiers.contains(WebEvent::Modifier::CapsLockKey)) ++ fprintf(stderr, "Unsupported modifier CapsLock will be ignored.\n"); ++ return result; ++} ++ ++void WebPageInspectorInputAgent::platformDispatchKeyEvent(WebKeyboardEvent::Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp) ++{ ++ unsigned keyCode = WebCore::PlatformKeyboardEvent::WPEKeyCodeForWindowsKeyCode(windowsVirtualKeyCode); ++ struct wpe_input_xkb_keymap_entry* entries; ++ uint32_t entriesCount; ++ fprintf(stderr, "platformDispatchKeyEvent %s => %d\n", key.ascii().data(), keyCode); ++ wpe_input_xkb_context_get_entries_for_key_code(wpe_input_xkb_context_get_default(), keyCode, &entries, &entriesCount); ++ bool pressed = type == WebKeyboardEvent::KeyDown; ++ struct wpe_input_keyboard_event event = { 0, keyCode, entriesCount ? entries[0].hardware_key_code : 0, pressed, toWPEModifiers(modifiers) }; ++ // event.time = timestamp.secondsSinceEpoch().milliseconds(); ++ wpe_view_backend_dispatch_keyboard_event(m_page.viewBackend(), &event); ++ free(entries); ++} ++ ++void WebPageInspectorInputAgent::platformDispatchMouseEvent(WebMouseEvent::Type type, int x, int y, WebMouseEvent::Button button, OptionSet modifiers) ++{ ++ wpe_input_pointer_event_type eventType = wpe_input_pointer_event_type_null; ++ uint32_t eventButton = 0; ++ uint32_t state = 0; ++ if (type == WebEvent::MouseDown || type == WebEvent::MouseUp) { ++ eventType = wpe_input_pointer_event_type_button; ++ state = (type == WebEvent::MouseDown); ++ eventButton = toWPEButton(button); ++ } else if (type == WebEvent::MouseMove) { ++ eventType = wpe_input_pointer_event_type_motion; ++ } ++ struct wpe_input_pointer_event event { eventType, 0, x, y, eventButton, state, toWPEModifiers(modifiers) }; ++ wpe_view_backend_dispatch_pointer_event(m_page.viewBackend(), &event); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp b/Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp +new file mode 100644 +index 00000000000..d64407d5822 +--- /dev/null ++++ b/Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp +@@ -0,0 +1,41 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorTargetProxy.h" ++ ++#include "WebPageProxy.h" ++#include ++ ++namespace WebKit { ++ ++void WebPageInspectorTargetProxy::platformActivate(String& error) const ++{ ++ struct wpe_view_backend* backend = m_page.viewBackend(); ++ wpe_view_backend_add_activity_state(backend, wpe_view_activity_state_visible | wpe_view_activity_state_focused | wpe_view_activity_state_in_window); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/WebKit.xcodeproj/project.pbxproj b/Source/WebKit/WebKit.xcodeproj/project.pbxproj +index 21f3f6ad8cd..d0191b4cafd 100644 +--- a/Source/WebKit/WebKit.xcodeproj/project.pbxproj ++++ b/Source/WebKit/WebKit.xcodeproj/project.pbxproj +@@ -1663,6 +1663,19 @@ + CEE4AE2B1A5DCF430002F49B /* UIKitSPI.h in Headers */ = {isa = PBXBuildFile; fileRef = CEE4AE2A1A5DCF430002F49B /* UIKitSPI.h */; }; + D3B9484711FF4B6500032B39 /* WebPopupMenu.h in Headers */ = {isa = PBXBuildFile; fileRef = D3B9484311FF4B6500032B39 /* WebPopupMenu.h */; }; + D3B9484911FF4B6500032B39 /* WebSearchPopupMenu.h in Headers */ = {isa = PBXBuildFile; fileRef = D3B9484511FF4B6500032B39 /* WebSearchPopupMenu.h */; }; ++ D71A94322370E025002C4D9E /* InspectorBrowserAgentClientMac.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94302370E025002C4D9E /* InspectorBrowserAgentClientMac.h */; }; ++ D71A94342370E07A002C4D9E /* InspectorBrowserAgentClient.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94332370E07A002C4D9E /* InspectorBrowserAgentClient.h */; }; ++ D71A94382370F032002C4D9E /* BrowserInspectorController.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94372370F032002C4D9E /* BrowserInspectorController.h */; }; ++ D71A943A2370F061002C4D9E /* RemoteInspectorPipe.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94392370F060002C4D9E /* RemoteInspectorPipe.h */; }; ++ D71A94412371F67E002C4D9E /* WebPageInspectorTargetProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A943E2371F67E002C4D9E /* WebPageInspectorTargetProxy.h */; }; ++ D71A94422371F67E002C4D9E /* WebPageInspectorEmulationAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A943F2371F67E002C4D9E /* WebPageInspectorEmulationAgent.h */; }; ++ D71A94432371F67E002C4D9E /* WebPageInspectorInputAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94402371F67E002C4D9E /* WebPageInspectorInputAgent.h */; }; ++ D71A944A2372290B002C4D9E /* _WKBrowserInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94492372290B002C4D9E /* _WKBrowserInspector.h */; settings = {ATTRIBUTES = (Private, ); }; }; ++ D71A944C237239FB002C4D9E /* BrowserInspectorPipe.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A944B237239FB002C4D9E /* BrowserInspectorPipe.h */; }; ++ D79902B1236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D79902AE236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm */; }; ++ D79902B2236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D79902AF236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm */; }; ++ D79902B3236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D79902B0236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm */; }; ++ D7EB04E72372A73B00F744CE /* InspectorBrowserAgentClientMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D7EB04E62372A73B00F744CE /* InspectorBrowserAgentClientMac.mm */; }; + E105FE5418D7B9DE008F57A8 /* EditingRange.h in Headers */ = {isa = PBXBuildFile; fileRef = E105FE5318D7B9DE008F57A8 /* EditingRange.h */; }; + E11D35AE16B63D1B006D23D7 /* com.apple.WebProcess.sb in Resources */ = {isa = PBXBuildFile; fileRef = E1967E37150AB5E200C73169 /* com.apple.WebProcess.sb */; }; + E14A954A16E016A40068DE82 /* NetworkProcessPlatformStrategies.h in Headers */ = {isa = PBXBuildFile; fileRef = E14A954816E016A40068DE82 /* NetworkProcessPlatformStrategies.h */; }; +@@ -4692,6 +4705,20 @@ + D3B9484311FF4B6500032B39 /* WebPopupMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPopupMenu.h; sourceTree = ""; }; + D3B9484411FF4B6500032B39 /* WebSearchPopupMenu.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WebSearchPopupMenu.cpp; sourceTree = ""; }; + D3B9484511FF4B6500032B39 /* WebSearchPopupMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebSearchPopupMenu.h; sourceTree = ""; }; ++ D71A942C2370DF81002C4D9E /* WKBrowserInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKBrowserInspector.h; sourceTree = ""; }; ++ D71A94302370E025002C4D9E /* InspectorBrowserAgentClientMac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorBrowserAgentClientMac.h; sourceTree = ""; }; ++ D71A94332370E07A002C4D9E /* InspectorBrowserAgentClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorBrowserAgentClient.h; sourceTree = ""; }; ++ D71A94372370F032002C4D9E /* BrowserInspectorController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrowserInspectorController.h; sourceTree = ""; }; ++ D71A94392370F060002C4D9E /* RemoteInspectorPipe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RemoteInspectorPipe.h; sourceTree = ""; }; ++ D71A943E2371F67E002C4D9E /* WebPageInspectorTargetProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPageInspectorTargetProxy.h; sourceTree = ""; }; ++ D71A943F2371F67E002C4D9E /* WebPageInspectorEmulationAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPageInspectorEmulationAgent.h; sourceTree = ""; }; ++ D71A94402371F67E002C4D9E /* WebPageInspectorInputAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPageInspectorInputAgent.h; sourceTree = ""; }; ++ D71A94492372290B002C4D9E /* _WKBrowserInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _WKBrowserInspector.h; sourceTree = ""; }; ++ D71A944B237239FB002C4D9E /* BrowserInspectorPipe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrowserInspectorPipe.h; sourceTree = ""; }; ++ D79902AE236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WebPageInspectorEmulationAgentMac.mm; sourceTree = ""; }; ++ D79902AF236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WebPageInspectorTargetProxyMac.mm; sourceTree = ""; }; ++ D79902B0236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WebPageInspectorInputAgentMac.mm; sourceTree = ""; }; ++ D7EB04E62372A73B00F744CE /* InspectorBrowserAgentClientMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = InspectorBrowserAgentClientMac.mm; sourceTree = ""; }; + DF58C6311371AC5800F9A37C /* NativeWebWheelEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeWebWheelEvent.h; sourceTree = ""; }; + DF58C6351371ACA000F9A37C /* NativeWebWheelEventMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NativeWebWheelEventMac.mm; sourceTree = ""; }; + E105FE5318D7B9DE008F57A8 /* EditingRange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EditingRange.h; sourceTree = ""; }; +@@ -6294,6 +6321,7 @@ + 37C4C08318149C2A003688B9 /* Cocoa */ = { + isa = PBXGroup; + children = ( ++ D71A94492372290B002C4D9E /* _WKBrowserInspector.h */, + 1A43E826188F38E2009E4D30 /* Deprecated */, + 37A5E01218BBF937000A081E /* _WKActivatedElementInfo.h */, + 37A5E01118BBF937000A081E /* _WKActivatedElementInfo.mm */, +@@ -7774,6 +7802,13 @@ + BC032DC310F438260058C15A /* UIProcess */ = { + isa = PBXGroup; + children = ( ++ D71A944B237239FB002C4D9E /* BrowserInspectorPipe.h */, ++ D71A943F2371F67E002C4D9E /* WebPageInspectorEmulationAgent.h */, ++ D71A94402371F67E002C4D9E /* WebPageInspectorInputAgent.h */, ++ D71A943E2371F67E002C4D9E /* WebPageInspectorTargetProxy.h */, ++ D71A94392370F060002C4D9E /* RemoteInspectorPipe.h */, ++ D71A94372370F032002C4D9E /* BrowserInspectorController.h */, ++ D71A94332370E07A002C4D9E /* InspectorBrowserAgentClient.h */, + BC032DC410F4387C0058C15A /* API */, + 512F588D12A8836F00629530 /* Authentication */, + 9955A6E81C79809000EB6A93 /* Automation */, +@@ -8051,6 +8086,7 @@ + BC0C376610F807660076D7CB /* C */ = { + isa = PBXGroup; + children = ( ++ D71A942C2370DF81002C4D9E /* WKBrowserInspector.h */, + 5123CF18133D25E60056F800 /* cg */, + 6EE849C41368D9040038D481 /* mac */, + BCB63477116BF10600603215 /* WebKit2_C.h */, +@@ -8646,6 +8682,11 @@ + BCCF085C113F3B7500C650C5 /* mac */ = { + isa = PBXGroup; + children = ( ++ D7EB04E62372A73B00F744CE /* InspectorBrowserAgentClientMac.mm */, ++ D71A94302370E025002C4D9E /* InspectorBrowserAgentClientMac.h */, ++ D79902AE236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm */, ++ D79902B0236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm */, ++ D79902AF236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm */, + B878B613133428DC006888E9 /* CorrectionPanel.h */, + B878B614133428DC006888E9 /* CorrectionPanel.mm */, + C1817362205844A900DFDA65 /* DisplayLink.cpp */, +@@ -9323,6 +9364,7 @@ + 510F59101DDE296900412FF5 /* _WKIconLoadingDelegate.h in Headers */, + 37A64E5518F38E3C00EB30F1 /* _WKInputDelegate.h in Headers */, + 5CAFDE452130846300B1F7E1 /* _WKInspector.h in Headers */, ++ D71A944A2372290B002C4D9E /* _WKBrowserInspector.h in Headers */, + 5CAFDE472130846A00B1F7E1 /* _WKInspectorInternal.h in Headers */, + A5C0F0AB2000658200536536 /* _WKInspectorWindow.h in Headers */, + 31B362952141EBCD007BFA53 /* _WKInternalDebugFeature.h in Headers */, +@@ -9434,6 +9476,7 @@ + 7C89D2981A6753B2003A5FDE /* APIPageConfiguration.h in Headers */, + 1AC1336C18565C7A00F3EC05 /* APIPageHandle.h in Headers */, + 1AFDD3151891B54000153970 /* APIPolicyClient.h in Headers */, ++ D71A94382370F032002C4D9E /* BrowserInspectorController.h in Headers */, + 7CE4D2201A4914CA00C7F152 /* APIProcessPoolConfiguration.h in Headers */, + F634445612A885C8000612D8 /* APISecurityOrigin.h in Headers */, + 1AFDE6621954E9B100C48FFA /* APISessionState.h in Headers */, +@@ -9552,6 +9595,7 @@ + BC06F43A12DBCCFB002D78DE /* GeolocationPermissionRequestProxy.h in Headers */, + 2DA944A41884E4F000ED86DB /* GestureTypes.h in Headers */, + 2DA049B8180CCD0A00AAFA9E /* GraphicsLayerCARemote.h in Headers */, ++ D71A94342370E07A002C4D9E /* InspectorBrowserAgentClient.h in Headers */, + C0CE72AD1247E78D00BC0EC4 /* HandleMessage.h in Headers */, + 1AC75A1B1B3368270056745B /* HangDetectionDisabler.h in Headers */, + 57AC8F50217FEED90055438C /* HidConnection.h in Headers */, +@@ -9675,8 +9719,10 @@ + 41DC45961E3D6E2200B11F51 /* NetworkRTCProvider.h in Headers */, + 413075AB1DE85F330039EC69 /* NetworkRTCSocket.h in Headers */, + 5C20CBA01BB1ECD800895BB1 /* NetworkSession.h in Headers */, ++ D71A94422371F67E002C4D9E /* WebPageInspectorEmulationAgent.h in Headers */, + 532159551DBAE7290054AA3C /* NetworkSessionCocoa.h in Headers */, + 417915B92257046F00D6F97E /* NetworkSocketChannel.h in Headers */, ++ D71A943A2370F061002C4D9E /* RemoteInspectorPipe.h in Headers */, + 570DAAC22303730300E8FC04 /* NfcConnection.h in Headers */, + 570DAAAE23026F5C00E8FC04 /* NfcService.h in Headers */, + 31A2EC5614899C0900810D71 /* NotificationPermissionRequest.h in Headers */, +@@ -9758,6 +9804,7 @@ + CD2865EE2255562000606AC7 /* ProcessTaskStateObserver.h in Headers */, + 463FD4821EB94EC000A2982C /* ProcessTerminationReason.h in Headers */, + 86E67A251910B9D100004AB7 /* ProcessThrottler.h in Headers */, ++ D71A944C237239FB002C4D9E /* BrowserInspectorPipe.h in Headers */, + 83048AE61ACA45DC0082C832 /* ProcessThrottlerClient.h in Headers */, + A1E688701F6E2BAB007006A6 /* QuarantineSPI.h in Headers */, + 57FD318222B3515E008D0E8B /* RedirectSOAuthorizationSession.h in Headers */, +@@ -9920,6 +9967,7 @@ + F430E94422473DFF005FE053 /* WebContentMode.h in Headers */, + 31A505FA1680025500A930EB /* WebContextClient.h in Headers */, + BC09B8F9147460F7005F5625 /* WebContextConnectionClient.h in Headers */, ++ D71A94412371F67E002C4D9E /* WebPageInspectorTargetProxy.h in Headers */, + BCDE059B11CDA8AE00E41AF1 /* WebContextInjectedBundleClient.h in Headers */, + 51871B5C127CB89D00F76232 /* WebContextMenu.h in Headers */, + BC032D7710F4378D0058C15A /* WebContextMenuClient.h in Headers */, +@@ -10153,6 +10201,7 @@ + BCD25F1711D6BDE100169B0E /* WKBundleFrame.h in Headers */, + BCF049E611FE20F600F86A58 /* WKBundleFramePrivate.h in Headers */, + BC49862F124D18C100D834E1 /* WKBundleHitTestResult.h in Headers */, ++ D71A94432371F67E002C4D9E /* WebPageInspectorInputAgent.h in Headers */, + BC204EF211C83EC8008F3375 /* WKBundleInitialize.h in Headers */, + 65B86F1E12F11DE300B7DD8A /* WKBundleInspector.h in Headers */, + 1A8B66B41BC45B010082DF77 /* WKBundleMac.h in Headers */, +@@ -10346,6 +10395,7 @@ + 1AB8A1F818400BB800E9AE69 /* WKPageContextMenuClient.h in Headers */, + 8372DB251A674C8F00C697C5 /* WKPageDiagnosticLoggingClient.h in Headers */, + 1AB8A1F418400B8F00E9AE69 /* WKPageFindClient.h in Headers */, ++ D71A94322370E025002C4D9E /* InspectorBrowserAgentClientMac.h in Headers */, + 1AB8A1F618400B9D00E9AE69 /* WKPageFindMatchesClient.h in Headers */, + 1AB8A1F018400B0000E9AE69 /* WKPageFormClient.h in Headers */, + BC7B633712A45ABA00D174A4 /* WKPageGroup.h in Headers */, +@@ -11302,6 +11352,7 @@ + 2D92A781212B6A7100F493FD /* MessageReceiverMap.cpp in Sources */, + 2D92A782212B6A7100F493FD /* MessageSender.cpp in Sources */, + 2D92A77A212B6A6100F493FD /* Module.cpp in Sources */, ++ D79902B1236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm in Sources */, + 57B826452304F14000B72EB0 /* NearFieldSoftLink.mm in Sources */, + 2D913443212CF9F000128AFD /* NetscapeBrowserFuncs.cpp in Sources */, + 2D913444212CF9F000128AFD /* NetscapePlugin.cpp in Sources */, +@@ -11326,6 +11377,7 @@ + 1A2D8439127F65D5001EB962 /* NPObjectMessageReceiverMessageReceiver.cpp in Sources */, + 2D92A792212B6AD400F493FD /* NPObjectProxy.cpp in Sources */, + 2D92A793212B6AD400F493FD /* NPRemoteObjectMap.cpp in Sources */, ++ D7EB04E72372A73B00F744CE /* InspectorBrowserAgentClientMac.mm in Sources */, + 2D913447212CF9F000128AFD /* NPRuntimeObjectMap.cpp in Sources */, + 2D913448212CF9F000128AFD /* NPRuntimeUtilities.cpp in Sources */, + 2D92A794212B6AD400F493FD /* NPVariantData.cpp in Sources */, +@@ -11365,11 +11417,13 @@ + A1ADAFB62368E6A8009CB776 /* SharedMemory.cpp in Sources */, + 2DE6943D18BD2A68005C15E5 /* SmartMagnificationControllerMessageReceiver.cpp in Sources */, + 1A334DED16DE8F88006A8E38 /* StorageAreaMapMessageReceiver.cpp in Sources */, ++ D79902B3236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm in Sources */, + 9368EEDE2303A90200BDB11A /* StorageManagerSetMessageReceiver.cpp in Sources */, + 2D92A783212B6A7100F493FD /* StringReference.cpp in Sources */, + 2D11B7512126A282006F8878 /* UnifiedSource1-mm.mm in Sources */, + 2D11B7522126A282006F8878 /* UnifiedSource1.cpp in Sources */, + 2D11B7542126A282006F8878 /* UnifiedSource2.cpp in Sources */, ++ D79902B2236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm in Sources */, + 2D11B7532126A282006F8878 /* UnifiedSource2-mm.mm in Sources */, + 2D11B7562126A282006F8878 /* UnifiedSource3.cpp in Sources */, + 2D11B7552126A282006F8878 /* UnifiedSource3-mm.mm in Sources */, +diff --git a/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.cpp b/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.cpp +index a70f6fd5209..f02e5c774a4 100644 +--- a/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.cpp ++++ b/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.cpp +@@ -26,6 +26,8 @@ + #include "config.h" + #include "WebPageInspectorTarget.h" + ++#include "FrameInfoData.h" ++#include "WebFrame.h" + #include "WebPage.h" + #include "WebPageInspectorTargetFrontendChannel.h" + #include +@@ -45,6 +47,11 @@ String WebPageInspectorTarget::identifier() const + return toTargetID(m_page.identifier()); + } + ++String WebPageInspectorTarget::url() const ++{ ++ return m_page.mainWebFrame()->info().request.url().string(); ++} ++ + void WebPageInspectorTarget::connect(Inspector::FrontendChannel::ConnectionType connectionType) + { + if (m_channel) +diff --git a/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.h b/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.h +index 6cbd7fad5ff..176c46f186b 100644 +--- a/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.h ++++ b/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.h +@@ -44,6 +44,7 @@ public: + Inspector::InspectorTargetType type() const final { return Inspector::InspectorTargetType::Page; } + + String identifier() const final; ++ String url() const final; + + void connect(Inspector::FrontendChannel::ConnectionType) override; + void disconnect() override; +diff --git a/Source/WebKit/WebProcess/WebProcess.cpp b/Source/WebKit/WebProcess/WebProcess.cpp +index 6c16fa01df1..2b79bf41601 100644 +--- a/Source/WebKit/WebProcess/WebProcess.cpp ++++ b/Source/WebKit/WebProcess/WebProcess.cpp +@@ -625,7 +625,8 @@ void WebProcess::setCacheModel(CacheModel cacheModel) + unsigned cacheMaxDeadCapacity = 0; + Seconds deadDecodedDataDeletionInterval; + unsigned backForwardCacheSize = 0; +- calculateMemoryCacheSizes(cacheModel, cacheTotalCapacity, cacheMinDeadCapacity, cacheMaxDeadCapacity, deadDecodedDataDeletionInterval, backForwardCacheSize); ++ // FIXME(yurys): forcefully disable cache becaus it swallows Runtime.executionContextCreated events on goBack navigation. ++ // calculateMemoryCacheSizes(cacheModel, cacheTotalCapacity, cacheMinDeadCapacity, cacheMaxDeadCapacity, deadDecodedDataDeletionInterval, backForwardCacheSize); + + auto& memoryCache = MemoryCache::singleton(); + memoryCache.setCapacities(cacheMinDeadCapacity, cacheMaxDeadCapacity, cacheTotalCapacity); +diff --git a/Tools/MiniBrowser/gtk/main.c b/Tools/MiniBrowser/gtk/main.c +index 93d93592bc4..6e27ef37742 100644 +--- a/Tools/MiniBrowser/gtk/main.c ++++ b/Tools/MiniBrowser/gtk/main.c +@@ -53,6 +53,7 @@ static const char *cookiesFile; + static const char *cookiesPolicy; + static const char *proxy; + static gboolean darkMode; ++static gboolean inspectorPipe; + static gboolean printVersion; + + typedef enum { +@@ -121,6 +122,7 @@ static const GOptionEntry commandLineOptions[] = + { "ignore-tls-errors", 0, 0, G_OPTION_ARG_NONE, &ignoreTLSErrors, "Ignore TLS errors", NULL }, + { "content-filter", 0, 0, G_OPTION_ARG_FILENAME, &contentFilter, "JSON with content filtering rules", "FILE" }, + { "version", 'v', 0, G_OPTION_ARG_NONE, &printVersion, "Print the WebKitGTK version", NULL }, ++ { "inspector-pipe", 0, 0, G_OPTION_ARG_NONE, &inspectorPipe, "Open pipe connection to the remote inspector", NULL }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &uriArguments, 0, "[URL…]" }, + { 0, 0, 0, 0, 0, 0, 0 } + }; +@@ -492,6 +494,29 @@ static void filterSavedCallback(WebKitUserContentFilterStore *store, GAsyncResul + g_main_loop_quit(data->mainLoop); + } + ++static WebKitWebView *createNewPage(WebKitBrowserInspector *browser_inspector, WebKitWebContext *context) ++{ ++ WebKitWebView *newWebView = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, ++ "web-context", context, ++ "is-ephemeral", webkit_web_context_is_ephemeral(context), ++ "is-controlled-by-automation", TRUE, ++ NULL)); ++ GtkWidget *newWindow = browser_window_new(NULL, context); ++ browser_window_append_view(BROWSER_WINDOW(newWindow), newWebView); ++ gtk_widget_grab_focus(GTK_WIDGET(newWebView)); ++ gtk_widget_show(GTK_WIDGET(newWindow)); ++ webkit_web_view_load_uri(newWebView, "about:blank"); ++ return newWebView; ++} ++ ++static void configureBrowserInspectorPipe() ++{ ++ WebKitBrowserInspector* browserInspector = webkit_browser_inspector_get_default(); ++ g_signal_connect(browserInspector, "create-new-page", G_CALLBACK(createNewPage), NULL); ++ ++ webkit_browser_inspector_initialize_pipe(); ++} ++ + int main(int argc, char *argv[]) + { + #if ENABLE_DEVELOPER_MODE +@@ -539,6 +564,9 @@ int main(int argc, char *argv[]) + WebKitWebContext *webContext = g_object_new(WEBKIT_TYPE_WEB_CONTEXT, "website-data-manager", manager, "process-swap-on-cross-site-navigation-enabled", TRUE, NULL); + g_object_unref(manager); + ++ if (inspectorPipe) ++ configureBrowserInspectorPipe(); ++ + if (cookiesPolicy) { + WebKitCookieManager *cookieManager = webkit_web_context_get_cookie_manager(webContext); + GEnumClass *enumClass = g_type_class_ref(WEBKIT_TYPE_COOKIE_ACCEPT_POLICY); +diff --git a/Tools/MiniBrowser/mac/AppDelegate.h b/Tools/MiniBrowser/mac/AppDelegate.h +index 45ef1a6424e..928486be325 100644 +--- a/Tools/MiniBrowser/mac/AppDelegate.h ++++ b/Tools/MiniBrowser/mac/AppDelegate.h +@@ -23,9 +23,11 @@ + * THE POSSIBILITY OF SUCH DAMAGE. + */ + ++#import ++ + @class ExtensionManagerWindowController; + +-@interface BrowserAppDelegate : NSObject { ++@interface BrowserAppDelegate : NSObject { + NSMutableSet *_browserWindowControllers; + ExtensionManagerWindowController *_extensionManagerWindowController; + +diff --git a/Tools/MiniBrowser/mac/AppDelegate.m b/Tools/MiniBrowser/mac/AppDelegate.m +index b6af4ef724f..46c39768156 100644 +--- a/Tools/MiniBrowser/mac/AppDelegate.m ++++ b/Tools/MiniBrowser/mac/AppDelegate.m +@@ -61,7 +61,9 @@ - (id)init + _browserWindowControllers = [[NSMutableSet alloc] init]; + _extensionManagerWindowController = [[ExtensionManagerWindowController alloc] init]; + } +- ++ NSArray *arguments = [[NSProcessInfo processInfo] arguments]; ++ if ([arguments containsObject: @"--inspector-pipe"]) ++ [_WKBrowserInspector initializeRemoteInspectorPipe:self]; + return self; + } + +@@ -158,9 +160,9 @@ - (BrowserWindowController *)createBrowserWindowController:(id)sender + } + + if (!useWebKit2) +- controller = [[WK1BrowserWindowController alloc] initWithWindowNibName:@"BrowserWindow"]; ++ controller = [[[WK1BrowserWindowController alloc] initWithWindowNibName:@"BrowserWindow"] autorelease]; + else +- controller = [[WK2BrowserWindowController alloc] initWithConfiguration:defaultConfiguration()]; ++ controller = [[[WK2BrowserWindowController alloc] initWithConfiguration:defaultConfiguration()] autorelease]; + + if (makeEditable) + controller.editable = YES; +@@ -345,4 +347,21 @@ - (IBAction)clearDefaultStoreWebsiteData:(id)sender + }]; + } + ++#pragma mark WKBrowserInspectorDelegate ++ ++- (WKWebView *)createNewPage ++{ ++ WK2BrowserWindowController *controller = [[[WK2BrowserWindowController alloc] initWithConfiguration:defaultConfiguration()] autorelease]; ++ [_browserWindowControllers addObject:controller]; ++ ++ [[controller window] makeKeyAndOrderFront:self]; ++ [controller loadURLString:[SettingsController shared].defaultURL]; ++ return [controller webView]; ++} ++ ++- (void)quit ++{ ++ [NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0]; ++} ++ + @end +diff --git a/Tools/MiniBrowser/mac/WK2BrowserWindowController.h b/Tools/MiniBrowser/mac/WK2BrowserWindowController.h +index 6f0949b0f4a..e774433031a 100644 +--- a/Tools/MiniBrowser/mac/WK2BrowserWindowController.h ++++ b/Tools/MiniBrowser/mac/WK2BrowserWindowController.h +@@ -25,8 +25,11 @@ + + #import "BrowserWindowController.h" + ++@class WKWebView; ++ + @interface WK2BrowserWindowController : BrowserWindowController + + - (instancetype)initWithConfiguration:(WKWebViewConfiguration *)configuration; ++- (WKWebView *)webView; + + @end +diff --git a/Tools/MiniBrowser/mac/WK2BrowserWindowController.m b/Tools/MiniBrowser/mac/WK2BrowserWindowController.m +index 3ca15403d5f..5905526473d 100644 +--- a/Tools/MiniBrowser/mac/WK2BrowserWindowController.m ++++ b/Tools/MiniBrowser/mac/WK2BrowserWindowController.m +@@ -105,7 +105,7 @@ - (void)awakeFromNib + // telling WebKit to load every icon referenced by the page. + if ([[SettingsController shared] loadsAllSiteIcons]) + _webView._iconLoadingDelegate = self; +- ++ + _webView._observedRenderingProgressEvents = _WKRenderingProgressEventFirstLayout + | _WKRenderingProgressEventFirstVisuallyNonEmptyLayout + | _WKRenderingProgressEventFirstPaintWithSignificantArea +@@ -139,14 +139,10 @@ - (instancetype)initWithConfiguration:(WKWebViewConfiguration *)configuration + + - (void)dealloc + { +- [_webView removeObserver:self forKeyPath:@"title"]; +- [_webView removeObserver:self forKeyPath:@"URL"]; +- + [progressIndicator unbind:NSHiddenBinding]; + [progressIndicator unbind:NSValueBinding]; + + [_textFinder release]; +- + [_webView release]; + [_configuration release]; + +@@ -369,9 +365,15 @@ - (BOOL)windowShouldClose:(id)sender + - (void)windowWillClose:(NSNotification *)notification + { + [(BrowserAppDelegate *)[[NSApplication sharedApplication] delegate] browserWindowWillClose:self.window]; ++ [_webView removeObserver:self forKeyPath:@"title"]; ++ [_webView removeObserver:self forKeyPath:@"URL"]; + [self autorelease]; + } + ++- (void)webViewDidClose:(WKWebView *)webView { ++ [self.window close]; ++} ++ + #define DefaultMinimumZoomFactor (.5) + #define DefaultMaximumZoomFactor (3.0) + #define DefaultZoomFactorRatio (1.2) +@@ -845,4 +847,9 @@ - (IBAction)saveAsWebArchive:(id)sender + }]; + } + ++- (WKWebView *)webView ++{ ++ return _webView; ++} ++ + @end +diff --git a/Tools/MiniBrowser/wpe/main.cpp b/Tools/MiniBrowser/wpe/main.cpp +index 2d183d39412..d94d4f06fc5 100644 +--- a/Tools/MiniBrowser/wpe/main.cpp ++++ b/Tools/MiniBrowser/wpe/main.cpp +@@ -172,6 +172,41 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi + return newWebView; + } + ++static WebKitWebView *createNewPage(WebKitBrowserInspector*, WebKitWebContext *context) ++{ ++ auto backend = createViewBackend(1280, 720); ++ struct wpe_view_backend* wpeBackend = backend->backend(); ++ if (!wpeBackend) ++ return nullptr; ++ ++ auto* viewBackend = webkit_web_view_backend_new(wpeBackend, ++ [](gpointer data) { ++ delete static_cast(data); ++ }, backend.release()); ++ ++ auto* newWebView = webkit_web_view_new_with_context(viewBackend, context); ++ ++ g_signal_connect(newWebView, "close", G_CALLBACK(webViewClose), nullptr); ++ ++ webkit_web_view_load_uri(newWebView, "about:blank"); ++ ++ return newWebView; ++ ++} ++ ++static void closeAll(WebKitBrowserInspector*, GMainLoop* mainLoop) ++{ ++ g_main_loop_quit(mainLoop); ++} ++ ++static void configureBrowserInspector(GMainLoop* mainLoop, WebKitWebView *firstWebView) ++{ ++ WebKitBrowserInspector* browserInspector = webkit_browser_inspector_get_default(); ++ g_signal_connect(browserInspector, "create-new-page", G_CALLBACK(createNewPage), NULL); ++ // FIXME: This signal is received only when closeAll is called. We should not rely on that. ++ g_signal_connect(firstWebView, "close", G_CALLBACK(closeAll), mainLoop); ++} ++ + int main(int argc, char *argv[]) + { + #if ENABLE_DEVELOPER_MODE +@@ -301,6 +336,8 @@ int main(int argc, char *argv[]) + g_signal_connect(webView, "permission-request", G_CALLBACK(decidePermissionRequest), nullptr); + g_signal_connect(webView, "create", G_CALLBACK(createWebView), nullptr); + ++ configureBrowserInspector(loop, webView); ++ + if (ignoreTLSErrors) + webkit_web_context_set_tls_errors_policy(webContext, WEBKIT_TLS_ERRORS_POLICY_IGNORE); + +-- +2.17.1 + diff --git a/browser_patches/webkit/pw_run.sh b/browser_patches/webkit/pw_run.sh new file mode 100755 index 0000000000..71c060241b --- /dev/null +++ b/browser_patches/webkit/pw_run.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +function runOSX() { + # if script is run as-is + if [ -d $SCRIPT_PATH/checkout/WebKitBuild/Release/MiniBrowser.app ]; then + DYLIB_PATH="$SCRIPT_PATH/checkout/WebKitBuild/Release" + elif [ -d $SCRIPT_PATH/MiniBrowser.app ]; then + DYLIB_PATH="$SCRIPT_PATH" + else + echo "Cannot find a MiniBrowser.app in neither location" 1>&2 + exit 1 + fi + MINIBROWSER="$DYLIB_PATH/MiniBrowser.app/Contents/MacOS/MiniBrowser" + DYLD_FRAMEWORK_PATH=$DYLIB_PATH DYLD_LIBRARY_PATH=$DYLIB_PATH $MINIBROWSER "$@" +} + +function runLinux() { + # if script is run as-is + if [ -d $SCRIPT_PATH/checkout/WebKitBuild ]; then + LD_PATH="$SCRIPT_PATH/checkout/WebKitBuild/DependenciesGTK/Root/lib:$SCRIPT_PATH/checkout/WebKitBuild/Release/bin" + MINIBROWSER="$SCRIPT_PATH/checkout/WebKitBuild/Release/bin/MiniBrowser" + elif [ -f $SCRIPT_PATH/MiniBrowser ]; then + LD_PATH="$SCRIPT_PATH" + MINIBROWSER="$SCRIPT_PATH/MiniBrowser" + else + echo "Cannot find a MiniBrowser.app in neither location" 1>&2 + exit 1 + fi + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LD_PATH $MINIBROWSER "$@" +} + +SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" +if [ "$(uname)" == "Darwin" ]; then + runOSX "$@" +elif [ "$(uname)" == "Linux" ]; then + runLinux "$@" +else + echo "ERROR: cannot run on this platform!" 1>&2 + exit 1; +fi diff --git a/chromium.js b/chromium.js new file mode 100644 index 0000000000..be153287e4 --- /dev/null +++ b/chromium.js @@ -0,0 +1,30 @@ +/** + * 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 {helper} = require('./lib/helper'); +const api = require('./lib/api'); +for (const className in api.Chromium) { + // Playwright-web excludes certain classes from bundle, e.g. BrowserFetcher. + if (typeof api.Chromium[className] === 'function') + helper.installAsyncStackHooks(api.Chromium[className]); +} + +// If node does not support async await, use the compiled version. +const {Playwright} = require('./lib/chromium/Playwright'); +const packageJson = require('./package.json'); +const isPlaywrightCore = packageJson.name === 'playwright-core'; + +module.exports = new Playwright(__dirname, packageJson.playwright.chromium_revision, isPlaywrightCore); diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000000..186d72e896 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,4107 @@ + +# Playwright API Tip-Of-Tree + + +- API Translations: [中文|Chinese](https://zhaoqize.github.io/playwright-api-zh_CN/#/) +- Troubleshooting: [troubleshooting.md](https://github.com/Microsoft/playwright/blob/master/docs/troubleshooting.md) + + +##### Table of Contents + + +- [Playwright API Tip-Of-Tree](#playwright-api-tip-of-tree) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [playwright vs playwright-core](#playwright-vs-playwright-core) + - [Environment Variables](#environment-variables) + - [Working with Chrome Extensions](#working-with-chrome-extensions) + - [class: Playwright](#class-playwright) + - [playwright.connect(options)](#playwrightconnectoptions) + - [playwright.createBrowserFetcher([options])](#playwrightcreatebrowserfetcheroptions) + - [playwright.defaultArgs([options])](#playwrightdefaultargsoptions) + - [playwright.devices](#playwrightdevices) + - [playwright.errors](#playwrighterrors) + - [playwright.executablePath()](#playwrightexecutablepath) + - [playwright.launch([options])](#playwrightlaunchoptions) + - [class: BrowserFetcher](#class-browserfetcher) + - [browserFetcher.canDownload(revision)](#browserfetchercandownloadrevision) + - [browserFetcher.download(revision[, progressCallback])](#browserfetcherdownloadrevision-progresscallback) + - [browserFetcher.localRevisions()](#browserfetcherlocalrevisions) + - [browserFetcher.platform()](#browserfetcherplatform) + - [browserFetcher.remove(revision)](#browserfetcherremoverevision) + - [browserFetcher.revisionInfo(revision)](#browserfetcherrevisioninforevision) + - [class: Browser](#class-browser) + - [event: 'disconnected'](#event-disconnected) + - [event: 'targetchanged'](#event-targetchanged) + - [event: 'targetcreated'](#event-targetcreated) + - [event: 'targetdestroyed'](#event-targetdestroyed) + - [browser.browserContexts()](#browserbrowsercontexts) + - [browser.close()](#browserclose) + - [browser.createIncognitoBrowserContext()](#browsercreateincognitobrowsercontext) + - [browser.defaultBrowserContext()](#browserdefaultbrowsercontext) + - [browser.disconnect()](#browserdisconnect) + - [browser.isConnected()](#browserisconnected) + - [browser.newPage()](#browsernewpage) + - [browser.pages()](#browserpages) + - [browser.process()](#browserprocess) + - [browser.target()](#browsertarget) + - [browser.targets()](#browsertargets) + - [browser.userAgent()](#browseruseragent) + - [browser.version()](#browserversion) + - [browser.waitForTarget(predicate[, options])](#browserwaitfortargetpredicate-options) + - [browser.wsEndpoint()](#browserwsendpoint) + - [class: BrowserContext](#class-browsercontext) + - [event: 'targetchanged'](#event-targetchanged-1) + - [event: 'targetcreated'](#event-targetcreated-1) + - [event: 'targetdestroyed'](#event-targetdestroyed-1) + - [browserContext.browser()](#browsercontextbrowser) + - [browserContext.clearPermissionOverrides()](#browsercontextclearpermissionoverrides) + - [browserContext.close()](#browsercontextclose) + - [browserContext.isIncognito()](#browsercontextisincognito) + - [browserContext.newPage()](#browsercontextnewpage) + - [browserContext.overridePermissions(origin, permissions)](#browsercontextoverridepermissionsorigin-permissions) + - [browserContext.pages()](#browsercontextpages) + - [browserContext.targets()](#browsercontexttargets) + - [browserContext.waitForTarget(predicate[, options])](#browsercontextwaitfortargetpredicate-options) + - [class: Page](#class-page) + - [event: 'close'](#event-close) + - [event: 'console'](#event-console) + - [event: 'dialog'](#event-dialog) + - [event: 'domcontentloaded'](#event-domcontentloaded) + - [event: 'error'](#event-error) + - [event: 'frameattached'](#event-frameattached) + - [event: 'framedetached'](#event-framedetached) + - [event: 'framenavigated'](#event-framenavigated) + - [event: 'load'](#event-load) + - [event: 'metrics'](#event-metrics) + - [event: 'pageerror'](#event-pageerror) + - [event: 'popup'](#event-popup) + - [event: 'request'](#event-request) + - [event: 'requestfailed'](#event-requestfailed) + - [event: 'requestfinished'](#event-requestfinished) + - [event: 'response'](#event-response) + - [event: 'workercreated'](#event-workercreated) + - [event: 'workerdestroyed'](#event-workerdestroyed) + - [page.$(selector)](#pageselector) + - [page.$$(selector)](#pageselector) + - [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) + - [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) + - [page.$x(expression)](#pagexexpression) + - [page.accessibility](#pageaccessibility) + - [page.addScriptTag(options)](#pageaddscripttagoptions) + - [page.addStyleTag(options)](#pageaddstyletagoptions) + - [page.authenticate(credentials)](#pageauthenticatecredentials) + - [page.bringToFront()](#pagebringtofront) + - [page.browser()](#pagebrowser) + - [page.browserContext()](#pagebrowsercontext) + - [page.click(selector[, options])](#pageclickselector-options) + - [page.close([options])](#pagecloseoptions) + - [page.content()](#pagecontent) + - [page.cookies([...urls])](#pagecookiesurls) + - [page.coverage](#pagecoverage) + - [page.dblclick(selector[, options])](#pagedblclickselector-options) + - [page.deleteCookie(...cookies)](#pagedeletecookiecookies) + - [page.emulate(options)](#pageemulateoptions) + - [page.emulateMedia(type)](#pageemulatemediatype) + - [page.emulateMediaFeatures(features)](#pageemulatemediafeaturesfeatures) + - [page.emulateMediaType(type)](#pageemulatemediatypetype) + - [page.emulateTimezone(timezoneId)](#pageemulatetimezonetimezoneid) + - [page.evaluate(pageFunction[, ...args])](#pageevaluatepagefunction-args) + - [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args) + - [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args) + - [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction) + - [page.fill(selector, value)](#pagefillselector-value) + - [page.focus(selector)](#pagefocusselector) + - [page.frames()](#pageframes) + - [page.goBack([options])](#pagegobackoptions) + - [page.goForward([options])](#pagegoforwardoptions) + - [page.goto(url[, options])](#pagegotourl-options) + - [page.hover(selector[, options])](#pagehoverselector-options) + - [page.isClosed()](#pageisclosed) + - [page.keyboard](#pagekeyboard) + - [page.mainFrame()](#pagemainframe) + - [page.metrics()](#pagemetrics) + - [page.mouse](#pagemouse) + - [page.pdf([options])](#pagepdfoptions) + - [page.queryObjects(prototypeHandle)](#pagequeryobjectsprototypehandle) + - [page.reload([options])](#pagereloadoptions) + - [page.screenshot([options])](#pagescreenshotoptions) + - [page.select(selector, ...values)](#pageselectselector-values) + - [page.setBypassCSP(enabled)](#pagesetbypasscspenabled) + - [page.setCacheEnabled([enabled])](#pagesetcacheenabledenabled) + - [page.setContent(html[, options])](#pagesetcontenthtml-options) + - [page.setCookie(...cookies)](#pagesetcookiecookies) + - [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) + - [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) + - [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) + - [page.setGeolocation(options)](#pagesetgeolocationoptions) + - [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) + - [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled) + - [page.setRequestInterception(value)](#pagesetrequestinterceptionvalue) + - [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) + - [page.setViewport(viewport)](#pagesetviewportviewport) + - [page.tap(selector[, options])](#pagetapselector-options) + - [page.target()](#pagetarget) + - [page.title()](#pagetitle) + - [page.touchscreen](#pagetouchscreen) + - [page.tracing](#pagetracing) + - [page.tripleclick(selector[, options])](#pagetripleclickselector-options) + - [page.type(selector, text[, options])](#pagetypeselector-text-options) + - [page.url()](#pageurl) + - [page.viewport()](#pageviewport) + - [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) + - [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions) + - [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) + - [page.waitForNavigation([options])](#pagewaitfornavigationoptions) + - [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) + - [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options) + - [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) + - [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options) + - [page.workers()](#pageworkers) + - [class: Worker](#class-worker) + - [worker.evaluate(pageFunction[, ...args])](#workerevaluatepagefunction-args) + - [worker.evaluateHandle(pageFunction[, ...args])](#workerevaluatehandlepagefunction-args) + - [worker.executionContext()](#workerexecutioncontext) + - [worker.url()](#workerurl) + - [class: Accessibility](#class-accessibility) + - [accessibility.snapshot([options])](#accessibilitysnapshotoptions) + - [class: Keyboard](#class-keyboard) + - [keyboard.down(key[, options])](#keyboarddownkey-options) + - [keyboard.press(key[, options])](#keyboardpresskey-options) + - [keyboard.sendCharacter(char)](#keyboardsendcharacterchar) + - [keyboard.type(text[, options])](#keyboardtypetext-options) + - [keyboard.up(key)](#keyboardupkey) + - [class: Mouse](#class-mouse) + - [mouse.click(x, y[, options])](#mouseclickx-y-options) + - [mouse.dblclick(x, y[, options])](#mousedblclickx-y-options) + - [mouse.down([options])](#mousedownoptions) + - [mouse.move(x, y[, options])](#mousemovex-y-options) + - [mouse.tripleclick(x, y[, options])](#mousetripleclickx-y-options) + - [mouse.up([options])](#mouseupoptions) + - [class: Touchscreen](#class-touchscreen) + - [touchscreen.tap(x, y)](#touchscreentapx-y) + - [class: Tracing](#class-tracing) + - [tracing.start([options])](#tracingstartoptions) + - [tracing.stop()](#tracingstop) + - [class: FileChooser](#class-filechooser) + - [fileChooser.accept(filePaths)](#filechooseracceptfilepaths) + - [fileChooser.cancel()](#filechoosercancel) + - [fileChooser.isMultiple()](#filechooserismultiple) + - [class: Dialog](#class-dialog) + - [dialog.accept([promptText])](#dialogacceptprompttext) + - [dialog.defaultValue()](#dialogdefaultvalue) + - [dialog.dismiss()](#dialogdismiss) + - [dialog.message()](#dialogmessage) + - [dialog.type()](#dialogtype) + - [class: ConsoleMessage](#class-consolemessage) + - [consoleMessage.args()](#consolemessageargs) + - [consoleMessage.location()](#consolemessagelocation) + - [consoleMessage.text()](#consolemessagetext) + - [consoleMessage.type()](#consolemessagetype) + - [class: Frame](#class-frame) + - [frame.$(selector)](#frameselector) + - [frame.$$(selector)](#frameselector) + - [frame.$$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) + - [frame.$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) + - [frame.$x(expression)](#framexexpression) + - [frame.addScriptTag(options)](#frameaddscripttagoptions) + - [frame.addStyleTag(options)](#frameaddstyletagoptions) + - [frame.childFrames()](#framechildframes) + - [frame.click(selector[, options])](#frameclickselector-options) + - [frame.content()](#framecontent) + - [frame.dblclick(selector[, options])](#framedblclickselector-options) + - [frame.evaluate(pageFunction[, ...args])](#frameevaluatepagefunction-args) + - [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args) + - [frame.executionContext()](#frameexecutioncontext) + - [frame.fill(selector, value)](#framefillselector-value) + - [frame.focus(selector)](#framefocusselector) + - [frame.goto(url[, options])](#framegotourl-options) + - [frame.hover(selector[, options])](#framehoverselector-options) + - [frame.isDetached()](#frameisdetached) + - [frame.name()](#framename) + - [frame.parentFrame()](#frameparentframe) + - [frame.select(selector, ...values)](#frameselectselector-values) + - [frame.setContent(html[, options])](#framesetcontenthtml-options) + - [frame.tap(selector[, options])](#frametapselector-options) + - [frame.title()](#frametitle) + - [frame.tripleclick(selector[, options])](#frametripleclickselector-options) + - [frame.type(selector, text[, options])](#frametypeselector-text-options) + - [frame.url()](#frameurl) + - [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args) + - [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args) + - [frame.waitForNavigation([options])](#framewaitfornavigationoptions) + - [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) + - [frame.waitForXPath(xpath[, options])](#framewaitforxpathxpath-options) + - [class: ExecutionContext](#class-executioncontext) + - [executionContext.evaluate(pageFunction[, ...args])](#executioncontextevaluatepagefunction-args) + - [executionContext.evaluateHandle(pageFunction[, ...args])](#executioncontextevaluatehandlepagefunction-args) + - [executionContext.frame()](#executioncontextframe) + - [executionContext.queryObjects(prototypeHandle)](#executioncontextqueryobjectsprototypehandle) + - [class: JSHandle](#class-jshandle) + - [jsHandle.asElement()](#jshandleaselement) + - [jsHandle.dispose()](#jshandledispose) + - [jsHandle.evaluate(pageFunction[, ...args])](#jshandleevaluatepagefunction-args) + - [jsHandle.evaluateHandle(pageFunction[, ...args])](#jshandleevaluatehandlepagefunction-args) + - [jsHandle.executionContext()](#jshandleexecutioncontext) + - [jsHandle.getProperties()](#jshandlegetproperties) + - [jsHandle.getProperty(propertyName)](#jshandlegetpropertypropertyname) + - [jsHandle.jsonValue()](#jshandlejsonvalue) + - [class: ElementHandle](#class-elementhandle) + - [elementHandle.$(selector)](#elementhandleselector) + - [elementHandle.$$(selector)](#elementhandleselector) + - [elementHandle.$$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args) + - [elementHandle.$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args) + - [elementHandle.$x(expression)](#elementhandlexexpression) + - [elementHandle.asElement()](#elementhandleaselement) + - [elementHandle.boundingBox()](#elementhandleboundingbox) + - [elementHandle.boxModel()](#elementhandleboxmodel) + - [elementHandle.click([options])](#elementhandleclickoptions) + - [elementHandle.contentFrame()](#elementhandlecontentframe) + - [elementHandle.dblclick([options])](#elementhandledblclickoptions) + - [elementHandle.dispose()](#elementhandledispose) + - [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args) + - [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args) + - [elementHandle.executionContext()](#elementhandleexecutioncontext) + - [elementHandle.fill(value)](#elementhandlefillvalue) + - [elementHandle.focus()](#elementhandlefocus) + - [elementHandle.getProperties()](#elementhandlegetproperties) + - [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname) + - [elementHandle.hover([options])](#elementhandlehoveroptions) + - [elementHandle.isIntersectingViewport()](#elementhandleisintersectingviewport) + - [elementHandle.jsonValue()](#elementhandlejsonvalue) + - [elementHandle.press(key[, options])](#elementhandlepresskey-options) + - [elementHandle.screenshot([options])](#elementhandlescreenshotoptions) + - [elementHandle.select(...values)](#elementhandleselectvalues) + - [elementHandle.tap([options])](#elementhandletapoptions) + - [elementHandle.toString()](#elementhandletostring) + - [elementHandle.tripleclick([options])](#elementhandletripleclickoptions) + - [elementHandle.type(text[, options])](#elementhandletypetext-options) + - [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) + - [class: Request](#class-request) + - [request.abort([errorCode])](#requestaborterrorcode) + - [request.continue([overrides])](#requestcontinueoverrides) + - [request.failure()](#requestfailure) + - [request.frame()](#requestframe) + - [request.headers()](#requestheaders) + - [request.isNavigationRequest()](#requestisnavigationrequest) + - [request.method()](#requestmethod) + - [request.postData()](#requestpostdata) + - [request.redirectChain()](#requestredirectchain) + - [request.resourceType()](#requestresourcetype) + - [request.respond(response)](#requestrespondresponse) + - [request.response()](#requestresponse) + - [request.url()](#requesturl) + - [class: Response](#class-response) + - [response.buffer()](#responsebuffer) + - [response.frame()](#responseframe) + - [response.fromCache()](#responsefromcache) + - [response.fromServiceWorker()](#responsefromserviceworker) + - [response.headers()](#responseheaders) + - [response.json()](#responsejson) + - [response.ok()](#responseok) + - [response.remoteAddress()](#responseremoteaddress) + - [response.request()](#responserequest) + - [response.securityDetails()](#responsesecuritydetails) + - [response.status()](#responsestatus) + - [response.statusText()](#responsestatustext) + - [response.text()](#responsetext) + - [response.url()](#responseurl) + - [class: SecurityDetails](#class-securitydetails) + - [securityDetails.issuer()](#securitydetailsissuer) + - [securityDetails.protocol()](#securitydetailsprotocol) + - [securityDetails.subjectName()](#securitydetailssubjectname) + - [securityDetails.validFrom()](#securitydetailsvalidfrom) + - [securityDetails.validTo()](#securitydetailsvalidto) + - [class: Target](#class-target) + - [target.browser()](#targetbrowser) + - [target.browserContext()](#targetbrowsercontext) + - [target.createCDPSession()](#targetcreatecdpsession) + - [target.opener()](#targetopener) + - [target.page()](#targetpage) + - [target.type()](#targettype) + - [target.url()](#targeturl) + - [target.worker()](#targetworker) + - [class: CDPSession](#class-cdpsession) + - [cdpSession.detach()](#cdpsessiondetach) + - [cdpSession.send(method[, params])](#cdpsessionsendmethod-params) + - [class: Coverage](#class-coverage) + - [coverage.startCSSCoverage([options])](#coveragestartcsscoverageoptions) + - [coverage.startJSCoverage([options])](#coveragestartjscoverageoptions) + - [coverage.stopCSSCoverage()](#coveragestopcsscoverage) + - [coverage.stopJSCoverage()](#coveragestopjscoverage) + - [class: TimeoutError](#class-timeouterror) + + +### Overview + +Playwright is a Node library which provides a high-level API to control Chromium or Chrome over the DevTools Protocol. + +The Playwright API is hierarchical and mirrors the browser structure. + +> **NOTE** On the following diagram, faded entities are not currently represented in Playwright. + +![playwright overview](https://user-images.githubusercontent.com/746130/40333229-5df5480c-5d0c-11e8-83cb-c3e371de7374.png) + +- [`Playwright`](#class-playwright) communicates with the browser using [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). +- [`Browser`](#class-browser) instance can own multiple browser contexts. +- [`BrowserContext`](#class-browsercontext) instance defines a browsing session and can own multiple pages. +- [`Page`](#class-page) has at least one frame: main frame. There might be other frames created by [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) or [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frame) tags. +- [`Frame`](#class-frame) has at least one execution context - the default execution context - where the frame's JavaScript is executed. A Frame might have additional execution contexts that are associated with [extensions](https://developer.chrome.com/extensions). +- [`Worker`](#class-worker) has a single execution context and facilitates interacting with [WebWorkers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). + +(Diagram source: [link](https://docs.google.com/drawings/d/1Q_AM6KYs9kbyLZF-Lpp5mtpAWth73Cq8IKCsWYgi8MM/edit?usp=sharing)) + +### playwright vs playwright-core + +Every release since v1.7.0 we publish two packages: +- [playwright](https://www.npmjs.com/package/playwright) +- [playwright-core](https://www.npmjs.com/package/playwright-core) + +`playwright` is a *product* for browser automation. When installed, it downloads a version of +Chromium, which it then drives using `playwright-core`. Being an end-user product, `playwright` supports a bunch of convenient `PLAYWRIGHT_*` env variables to tweak its behavior. + +`playwright-core` is a *library* to help drive anything that supports DevTools protocol. `playwright-core` doesn't download Chromium when installed. Being a library, `playwright-core` is fully driven +through its programmatic interface and disregards all the `PLAYWRIGHT_*` env variables. + +To sum up, the only differences between `playwright-core` and `playwright` are: +- `playwright-core` doesn't automatically download Chromium when installed. +- `playwright-core` ignores all `PLAYWRIGHT_*` env variables. + +In most cases, you'll be fine using the `playwright` package. + +However, you should use `playwright-core` if: +- you're building another end-user product or library atop of DevTools protocol. For example, one might build a PDF generator using `playwright-core` and write a custom `install.js` script that downloads [`headless_shell`](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) instead of Chromium to save disk space. +- you're bundling Playwright to use in Chrome Extension / browser with the DevTools protocol where downloading an additional Chromium binary is unnecessary. + +When using `playwright-core`, remember to change the *include* line: + +```js +const playwright = require('playwright-core'); +``` + +You will then need to call [`playwright.connect([options])`](#playwrightconnectoptions) or [`playwright.launch([options])`](#playwrightlaunchoptions) with an explicit `executablePath` option. + +### Environment Variables + +Playwright looks for certain [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to aid its operations. +If Playwright doesn't find them in the environment during the installation step, a lowercased variant of these variables will be used from the [npm config](https://docs.npmjs.com/cli/config). + +- `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` - defines HTTP proxy settings that are used to download and run Chromium. +- `PLAYWRIGHT_SKIP_CHROMIUM_DOWNLOAD` - do not download bundled Chromium during installation step. +- `PLAYWRIGHT_DOWNLOAD_HOST` - overwrite URL prefix that is used to download Chromium. Note: this includes protocol and might even include path prefix. Defaults to `https://storage.googleapis.com`. +- `PLAYWRIGHT_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Playwright to use. See [playwright.launch([options])](#playwrightlaunchoptions) on how executable path is inferred. **BEWARE**: Playwright is only [guaranteed to work](https://github.com/Microsoft/playwright/#q-why-doesnt-playwright-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. +- `PLAYWRIGHT_EXECUTABLE_PATH` - specify an executable path to be used in `playwright.launch`. See [playwright.launch([options])](#playwrightlaunchoptions) on how the executable path is inferred. **BEWARE**: Playwright is only [guaranteed to work](https://github.com/Microsoft/playwright/#q-why-doesnt-playwright-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. + +> **NOTE** PLAYWRIGHT_* env variables are not accounted for in the [`playwright-core`](https://www.npmjs.com/package/playwright-core) package. + + +### Working with Chrome Extensions + +Playwright can be used for testing Chrome Extensions. + +> **NOTE** Extensions in Chrome / Chromium currently only work in non-headless mode. + +The following is code for getting a handle to the [background page](https://developer.chrome.com/extensions/background_pages) of an extension whose source is located in `./my-extension`: +```js +const playwright = require('playwright'); + +(async () => { + const pathToExtension = require('path').join(__dirname, 'my-extension'); + const browser = await playwright.launch({ + headless: false, + args: [ + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}` + ] + }); + const targets = await browser.targets(); + const backgroundPageTarget = targets.find(target => target.type() === 'background_page'); + const backgroundPage = await backgroundPageTarget.page(); + // Test the background page as you would any other page. + await browser.close(); +})(); +``` + +> **NOTE** It is not yet possible to test extension popups or content scripts. + +### class: Playwright + +Playwright module provides a method to launch a Chromium instance. +The following is a typical example of using Playwright to drive automation: +```js +const playwright = require('playwright'); + +(async () => { + const browser = await playwright.launch(); + const page = await browser.newPage(); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +#### playwright.connect(options) +- `options` <[Object]> + - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserURL` a browser url to connect to, in format `http://${host}:${port}`. Use interchangeably with `browserWSEndpoint` to let Playwright fetch it from [metadata endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). + - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. + - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `width` <[number]> page width in pixels. + - `height` <[number]> page height in pixels. + - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`. + - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`. + - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` + - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. + - `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. + - `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Playwright to use. +- returns: <[Promise]<[Browser]>> + +This methods attaches Playwright to an existing Chromium instance. + +#### playwright.createBrowserFetcher([options]) +- `options` <[Object]> + - `host` <[string]> A download host to be used. Defaults to `https://storage.googleapis.com`. + - `path` <[string]> A path for the downloads folder. Defaults to `/.local-chromium`, where `` is playwright's package root. + - `platform` <[string]> Possible values are: `mac`, `win32`, `win64`, `linux`. Defaults to the current platform. +- returns: <[BrowserFetcher]> + +#### playwright.defaultArgs([options]) +- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: + - `headless` <[boolean]> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. + - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). + - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). + - `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. +- returns: <[Array]<[string]>> + +The default flags that Chromium will be launched with. + +#### playwright.devices +- returns: <[Object]> + +Returns a list of devices to be used with [`page.emulate(options)`](#pageemulateoptions). Actual list of +devices can be found in [lib/DeviceDescriptors.js](https://github.com/Microsoft/playwright/blob/master/lib/DeviceDescriptors.js). + +```js +const playwright = require('playwright'); +const iPhone = playwright.devices['iPhone 6']; + +(async () => { + const browser = await playwright.launch(); + const page = await browser.newPage(); + await page.emulate(iPhone); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +> **NOTE** The old way (Playwright versions <= v1.14.0) devices can be obtained with `require('playwright/DeviceDescriptors')`. + +#### playwright.errors +- returns: <[Object]> + - `TimeoutError` <[function]> A class of [TimeoutError]. + +Playwright methods might throw errors if they are unable to fulfill a request. For example, [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) +might fail if the selector doesn't match any nodes during the given timeframe. + +For certain types of errors Playwright uses specific error classes. +These classes are available via [`playwright.errors`](#playwrighterrors) + +An example of handling a timeout error: +```js +try { + await page.waitForSelector('.foo'); +} catch (e) { + if (e instanceof playwright.errors.TimeoutError) { + // Do something if this is a timeout. + } +} +``` + +> **NOTE** The old way (Playwright versions <= v1.14.0) errors can be obtained with `require('playwright/Errors')`. + +#### playwright.executablePath() +- returns: <[string]> A path where Playwright expects to find bundled Chromium. Chromium might not exist there if the download was skipped with [`PLAYWRIGHT_SKIP_CHROMIUM_DOWNLOAD`](#environment-variables). + +> **NOTE** `playwright.executablePath()` is affected by the `PLAYWRIGHT_EXECUTABLE_PATH` and `PLAYWRIGHT_CHROMIUM_REVISION` env variables. See [Environment Variables](#environment-variables) for details. + + +#### playwright.launch([options]) +- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: + - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. + - `headless` <[boolean]> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. + - `executablePath` <[string]> Path to a Chromium or Chrome executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Playwright is only [guaranteed to work](https://github.com/Microsoft/playwright/#q-why-doesnt-playwright-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. + - `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. + - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `width` <[number]> page width in pixels. + - `height` <[number]> page height in pixels. + - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`. + - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`. + - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` + - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. + - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). + - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use [`playwright.defaultArgs()`](#playwrightdefaultargsoptions). If an array is given, then filter out the given default arguments. Dangerous option; use with care. Defaults to `false`. + - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. + - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. + - `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`. + - `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. + - `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`. + - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). + - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`. + - `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + - `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`. +- returns: <[Promise]<[Browser]>> Promise which resolves to browser instance. + + +You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: +```js +const browser = await playwright.launch({ + ignoreDefaultArgs: ['--mute-audio'] +}); +``` + +> **NOTE** Playwright can also be used to control the Chrome browser, but it works best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use `executablePath` option with extreme caution. +> +> If Google Chrome (rather than Chromium) is preferred, a [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested. +> +> In [playwright.launch([options])](#playwrightlaunchoptions) above, any mention of Chromium also applies to Chrome. +> +> See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. + +### class: BrowserFetcher + +BrowserFetcher can download and manage different versions of Chromium. + +BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from [omahaproxy.appspot.com](http://omahaproxy.appspot.com/). + +An example of using BrowserFetcher to download a specific version of Chromium and running +Playwright against it: + +```js +const browserFetcher = playwright.createBrowserFetcher(); +const revisionInfo = await browserFetcher.download('533271'); +const browser = await playwright.launch({executablePath: revisionInfo.executablePath}) +``` + +> **NOTE** BrowserFetcher is not designed to work concurrently with other +> instances of BrowserFetcher that share the same downloads directory. + +#### browserFetcher.canDownload(revision) +- `revision` <[string]> a revision to check availability. +- returns: <[Promise]<[boolean]>> returns `true` if the revision could be downloaded from the host. + +The method initiates a HEAD request to check if the revision is available. + +#### browserFetcher.download(revision[, progressCallback]) +- `revision` <[string]> a revision to download. +- `progressCallback` <[function]([number], [number])> A function that will be called with two arguments: + - `downloadedBytes` <[number]> how many bytes have been downloaded + - `totalBytes` <[number]> how large is the total download. +- returns: <[Promise]<[Object]>> Resolves with revision information when the revision is downloaded and extracted + - `revision` <[string]> the revision the info was created from + - `folderPath` <[string]> path to the extracted revision folder + - `executablePath` <[string]> path to the revision executable + - `url` <[string]> URL this revision can be downloaded from + - `local` <[boolean]> whether the revision is locally available on disk + +The method initiates a GET request to download the revision from the host. + +#### browserFetcher.localRevisions() +- returns: <[Promise]<[Array]<[string]>>> A list of all revisions available locally on disk. + +#### browserFetcher.platform() +- returns: <[string]> One of `mac`, `linux`, `win32` or `win64`. + +#### browserFetcher.remove(revision) +- `revision` <[string]> a revision to remove. The method will throw if the revision has not been downloaded. +- returns: <[Promise]> Resolves when the revision has been removed. + +#### browserFetcher.revisionInfo(revision) +- `revision` <[string]> a revision to get info for. +- returns: <[Object]> + - `revision` <[string]> the revision the info was created from + - `folderPath` <[string]> path to the extracted revision folder + - `executablePath` <[string]> path to the revision executable + - `url` <[string]> URL this revision can be downloaded from + - `local` <[boolean]> whether the revision is locally available on disk + +### class: Browser + +* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) + +A Browser is created when Playwright connects to a Chromium instance, either through [`playwright.launch`](#playwrightlaunchoptions) or [`playwright.connect`](#playwrightconnectoptions). + +An example of using a [Browser] to create a [Page]: +```js +const playwright = require('playwright'); + +(async () => { + const browser = await playwright.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await browser.close(); +})(); +``` + +An example of disconnecting from and reconnecting to a [Browser]: +```js +const playwright = require('playwright'); + +(async () => { + const browser = await playwright.launch(); + // Store the endpoint to be able to reconnect to Chromium + const browserWSEndpoint = browser.wsEndpoint(); + // Disconnect playwright from Chromium + browser.disconnect(); + + // Use the endpoint to reestablish a connection + const browser2 = await playwright.connect({browserWSEndpoint}); + // Close Chromium + await browser2.close(); +})(); +``` +#### event: 'disconnected' +Emitted when Playwright gets disconnected from the Chromium instance. This might happen because of one of the following: +- Chromium is closed or crashed +- The [`browser.disconnect`](#browserdisconnect) method was called + +#### event: 'targetchanged' +- <[Target]> + +Emitted when the url of a target changes. + +> **NOTE** This includes target changes in incognito browser contexts. + + +#### event: 'targetcreated' +- <[Target]> + +Emitted when a target is created, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browser.newPage`](#browsernewpage). + +> **NOTE** This includes target creations in incognito browser contexts. + +#### event: 'targetdestroyed' +- <[Target]> + +Emitted when a target is destroyed, for example when a page is closed. + +> **NOTE** This includes target destructions in incognito browser contexts. + +#### browser.browserContexts() +- returns: <[Array]<[BrowserContext]>> + +Returns an array of all open browser contexts. In a newly created browser, this will return +a single instance of [BrowserContext]. + +#### browser.close() +- returns: <[Promise]> + +Closes Chromium and all of its pages (if any were opened). The [Browser] object itself is considered to be disposed and cannot be used anymore. + +#### browser.createIncognitoBrowserContext() +- returns: <[Promise]<[BrowserContext]>> + +Creates a new incognito browser context. This won't share cookies/cache with other browser contexts. + +```js +(async () => { + const browser = await playwright.launch(); + // Create a new incognito browser context. + const context = await browser.createIncognitoBrowserContext(); + // Create a new page in a pristine context. + const page = await context.newPage(); + // Do stuff + await page.goto('https://example.com'); +})(); +``` + +#### browser.defaultBrowserContext() +- returns: <[BrowserContext]> + +Returns the default browser context. The default browser context can not be closed. + +#### browser.disconnect() + +Disconnects Playwright from the browser, but leaves the Chromium process running. After calling `disconnect`, the [Browser] object is considered disposed and cannot be used anymore. + +#### browser.isConnected() + +- returns: <[boolean]> + +Indicates that the browser is connected. + +#### browser.newPage() +- returns: <[Promise]<[Page]>> + +Promise which resolves to a new [Page] object. The [Page] is created in a default browser context. + +#### browser.pages() +- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [target.page()](#targetpage). + +An array of all pages inside the Browser. In case of multiple browser contexts, +the method will return an array with all the pages in all browser contexts. + +#### browser.process() +- returns: Spawned browser process. Returns `null` if the browser instance was created with [`playwright.connect`](#playwrightconnectoptions) method. + +#### browser.target() +- returns: <[Target]> + +A target associated with the browser. + +#### browser.targets() +- returns: <[Array]<[Target]>> + +An array of all active targets inside the Browser. In case of multiple browser contexts, +the method will return an array with all the targets in all browser contexts. + +#### browser.userAgent() +- returns: <[Promise]<[string]>> Promise which resolves to the browser's original user agent. + +> **NOTE** Pages can override browser user agent with [page.setUserAgent](#pagesetuseragentuseragent) + +#### browser.version() +- returns: <[Promise]<[string]>> For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is similar to `Chrome/61.0.3153.0`. + +> **NOTE** the format of browser.version() might change with future releases of Chromium. + +#### browser.waitForTarget(predicate[, options]) +- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target +- `options` <[Object]> + - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. +- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. + +This searches for a target in all browser contexts. + +An example of finding a target for a page opened via `window.open`: +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browser.waitForTarget(target => target.url() === 'https://www.example.com/'); +``` + +#### browser.wsEndpoint() +- returns: <[string]> Browser websocket url. + +Browser websocket endpoint which can be used as an argument to +[playwright.connect](#playwrightconnectoptions). The format is `ws://${host}:${port}/devtools/browser/` + +You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`. Learn more about the [devtools protocol](https://chromedevtools.github.io/devtools-protocol) and the [browser endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). + +### class: BrowserContext + +* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) + +BrowserContexts provide a way to operate multiple independent browser sessions. When a browser is launched, it has +a single BrowserContext used by default. The method `browser.newPage()` creates a page in the default browser context. + +If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser +context. + +Playwright allows creation of "incognito" browser contexts with `browser.createIncognitoBrowserContext()` method. +"Incognito" browser contexts don't write any browsing data to disk. + +```js +// Create a new incognito browser context +const context = await browser.createIncognitoBrowserContext(); +// Create a new page inside context. +const page = await context.newPage(); +// ... do stuff with page ... +await page.goto('https://example.com'); +// Dispose context once it's no longer needed. +await context.close(); +``` + +#### event: 'targetchanged' +- <[Target]> + +Emitted when the url of a target inside the browser context changes. + +#### event: 'targetcreated' +- <[Target]> + +Emitted when a new target is created inside the browser context, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browserContext.newPage`](#browsercontextnewpage). + +#### event: 'targetdestroyed' +- <[Target]> + +Emitted when a target inside the browser context is destroyed, for example when a page is closed. + +#### browserContext.browser() +- returns: <[Browser]> + +The browser this browser context belongs to. + +#### browserContext.clearPermissionOverrides() +- returns: <[Promise]> + +Clears all permission overrides for the browser context. + +```js +const context = browser.defaultBrowserContext(); +context.overridePermissions('https://example.com', ['clipboard-read']); +// do stuff .. +context.clearPermissionOverrides(); +``` + +#### browserContext.close() +- returns: <[Promise]> + +Closes the browser context. All the targets that belong to the browser context +will be closed. + +> **NOTE** only incognito browser contexts can be closed. + +#### browserContext.isIncognito() +- returns: <[boolean]> + +Returns whether BrowserContext is incognito. +The default browser context is the only non-incognito browser context. + +> **NOTE** the default browser context cannot be closed. + +#### browserContext.newPage() +- returns: <[Promise]<[Page]>> + +Creates a new page in the browser context. + + +#### browserContext.overridePermissions(origin, permissions) +- `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". +- `permissions` <[Array]<[string]>> An array of permissions to grant. All permissions that are not listed here will be automatically denied. Permissions can be one of the following values: + - `'geolocation'` + - `'midi'` + - `'midi-sysex'` (system-exclusive midi) + - `'notifications'` + - `'push'` + - `'camera'` + - `'microphone'` + - `'background-sync'` + - `'ambient-light-sensor'` + - `'accelerometer'` + - `'gyroscope'` + - `'magnetometer'` + - `'accessibility-events'` + - `'clipboard-read'` + - `'clipboard-write'` + - `'payment-handler'` +- returns: <[Promise]> + + +```js +const context = browser.defaultBrowserContext(); +await context.overridePermissions('https://html5demos.com', ['geolocation']); +``` + + +#### browserContext.pages() +- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [target.page()](#targetpage). + +An array of all pages inside the browser context. + +#### browserContext.targets() +- returns: <[Array]<[Target]>> + +An array of all active targets inside the browser context. + +#### browserContext.waitForTarget(predicate[, options]) +- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target +- `options` <[Object]> + - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. +- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. + +This searches for a target in this specific browser context. + +An example of finding a target for a page opened via `window.open`: +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browserContext.waitForTarget(target => target.url() === 'https://www.example.com/'); +``` + +### class: Page + +* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) + +Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium. One [Browser] instance might have multiple [Page] instances. + +This example creates a page, navigates it to a URL, and then saves a screenshot: +```js +const playwright = require('playwright'); + +(async () => { + const browser = await playwright.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({path: 'screenshot.png'}); + await browser.close(); +})(); +``` + +The Page class emits various events (described below) which can be handled using any of Node's native [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) methods, such as `on`, `once` or `removeListener`. + +This example logs a message for a single page `load` event: +```js +page.once('load', () => console.log('Page loaded!')); +``` + +To unsubscribe from events use the `removeListener` method: + +```js +function logRequest(interceptedRequest) { + console.log('A request was made:', interceptedRequest.url()); +} +page.on('request', logRequest); +// Sometime later... +page.removeListener('request', logRequest); +``` + +#### event: 'close' + +Emitted when the page closes. + +#### event: 'console' +- <[ConsoleMessage]> + +Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. + +The arguments passed into `console.log` appear as arguments on the event handler. + +An example of handling `console` event: +```js +page.on('console', msg => { + for (let i = 0; i < msg.args().length; ++i) + console.log(`${i}: ${msg.args()[i]}`); +}); +page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); +``` + +#### event: 'dialog' +- <[Dialog]> + +Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond to the dialog via [Dialog]'s [accept](#dialogacceptprompttext) or [dismiss](#dialogdismiss) methods. + +#### event: 'domcontentloaded' + +Emitted when the JavaScript [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched. + +#### event: 'error' +- <[Error]> + +Emitted when the page crashes. + +> **NOTE** `error` event has a special meaning in Node, see [error events](https://nodejs.org/api/events.html#events_error_events) for details. + +#### event: 'frameattached' +- <[Frame]> + +Emitted when a frame is attached. + +#### event: 'framedetached' +- <[Frame]> + +Emitted when a frame is detached. + +#### event: 'framenavigated' +- <[Frame]> + +Emitted when a frame is navigated to a new url. + +#### event: 'load' + +Emitted when the JavaScript [`load`](https://developer.mozilla.org/en-US/docs/Web/Events/load) event is dispatched. + +#### event: 'metrics' +- <[Object]> + - `title` <[string]> The title passed to `console.timeStamp`. + - `metrics` <[Object]> Object containing metrics as key/value pairs. The values + of metrics are of <[number]> type. + +Emitted when the JavaScript code makes a call to `console.timeStamp`. For the list +of metrics see `page.metrics`. + +#### event: 'pageerror' +- <[Error]> The exception message + +Emitted when an uncaught exception happens within the page. + +#### event: 'popup' +- <[Page]> Page corresponding to "popup" window + +Emitted when the page opens a new tab or window. + +```js +const [popup] = await Promise.all([ + new Promise(resolve => page.once('popup', resolve)), + page.click('a[target=_blank]'), +]); +``` + +```js +const [popup] = await Promise.all([ + new Promise(resolve => page.once('popup', resolve)), + page.evaluate(() => window.open('https://example.com')), +]); +``` + +#### event: 'request' +- <[Request]> + +Emitted when a page issues a request. The [request] object is read-only. +In order to intercept and mutate requests, see `page.setRequestInterception`. + +#### event: 'requestfailed' +- <[Request]> + +Emitted when a request fails, for example by timing out. + +> **NOTE** HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete with [`'requestfinished'`](#event-requestfinished) event and not with [`'requestfailed'`](#event-requestfailed). + +#### event: 'requestfinished' +- <[Request]> + +Emitted when a request finishes successfully. + +#### event: 'response' +- <[Response]> + +Emitted when a [response] is received. + +#### event: 'workercreated' +- <[Worker]> + +Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is spawned by the page. + +#### event: 'workerdestroyed' +- <[Worker]> + +Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated. + +#### page.$(selector) +- `selector` <[string]> A [selector] to query page for +- returns: <[Promise]> + +The method runs `document.querySelector` within the page. If no element matches the selector, the return value resolves to `null`. + +Shortcut for [page.mainFrame().$(selector)](#frameselector). + +#### page.$$(selector) +- `selector` <[string]> A [selector] to query page for +- returns: <[Promise]<[Array]<[ElementHandle]>>> + +The method runs `document.querySelectorAll` within the page. If no elements match the selector, the return value resolves to `[]`. + +Shortcut for [page.mainFrame().$$(selector)](#frameselector-1). + +#### page.$$eval(selector, pageFunction[, ...args]) +- `selector` <[string]> A [selector] to query page for +- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context +- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` + +This method runs `Array.from(document.querySelectorAll(selector))` within the page and passes it as the first argument to `pageFunction`. + +If `pageFunction` returns a [Promise], then `page.$$eval` would wait for the promise to resolve and return its value. + +Examples: +```js +const divsCounts = await page.$$eval('div', divs => divs.length); +``` + +#### page.$eval(selector, pageFunction[, ...args]) +- `selector` <[string]> A [selector] to query page for +- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context +- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` + +This method runs `document.querySelector` within the page and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. + +If `pageFunction` returns a [Promise], then `page.$eval` would wait for the promise to resolve and return its value. + +Examples: +```js +const searchValue = await page.$eval('#search', el => el.value); +const preloadHref = await page.$eval('link[rel=preload]', el => el.href); +const html = await page.$eval('.main-container', e => e.outerHTML); +``` + +Shortcut for [page.mainFrame().$eval(selector, pageFunction)](#frameevalselector-pagefunction-args). + +#### page.$x(expression) +- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). +- returns: <[Promise]<[Array]<[ElementHandle]>>> + +The method evaluates the XPath expression. + +Shortcut for [page.mainFrame().$x(expression)](#framexexpression) + +#### page.accessibility +- returns: <[Accessibility]> + +#### page.addScriptTag(options) +- `options` <[Object]> + - `url` <[string]> URL of a script to be added. + - `path` <[string]> Path to the JavaScript file to be injected into frame. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). + - `content` <[string]> Raw JavaScript content to be injected into frame. + - `type` <[string]> Script type. Use 'module' in order to load a Javascript ES6 module. See [script](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script) for more details. +- returns: <[Promise]<[ElementHandle]>> which resolves to the added tag when the script's onload fires or when the script content was injected into frame. + +Adds a ` + diff --git a/test/assets/cached/one-style.css b/test/assets/cached/one-style.css new file mode 100644 index 0000000000..04e7110b41 --- /dev/null +++ b/test/assets/cached/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/cached/one-style.html b/test/assets/cached/one-style.html new file mode 100644 index 0000000000..4760f2b9f7 --- /dev/null +++ b/test/assets/cached/one-style.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/assets/chromium-linux.zip b/test/assets/chromium-linux.zip new file mode 100644 index 0000000000..9c00ec080d Binary files /dev/null and b/test/assets/chromium-linux.zip differ diff --git a/test/assets/consolelog.html b/test/assets/consolelog.html new file mode 100644 index 0000000000..7fa1b211a4 --- /dev/null +++ b/test/assets/consolelog.html @@ -0,0 +1,11 @@ + + + + console.log test + + + + + diff --git a/test/assets/csp.html b/test/assets/csp.html new file mode 100644 index 0000000000..34fc1fc1a5 --- /dev/null +++ b/test/assets/csp.html @@ -0,0 +1 @@ + diff --git a/test/assets/csscoverage/Dosis-Regular.ttf b/test/assets/csscoverage/Dosis-Regular.ttf new file mode 100644 index 0000000000..4b208624e8 Binary files /dev/null and b/test/assets/csscoverage/Dosis-Regular.ttf differ diff --git a/test/assets/csscoverage/OFL.txt b/test/assets/csscoverage/OFL.txt new file mode 100644 index 0000000000..a9b3c8b34e --- /dev/null +++ b/test/assets/csscoverage/OFL.txt @@ -0,0 +1,95 @@ +Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com), +Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com), +with Reserved Font Names "Dosis". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/test/assets/csscoverage/involved.html b/test/assets/csscoverage/involved.html new file mode 100644 index 0000000000..bcd9845b93 --- /dev/null +++ b/test/assets/csscoverage/involved.html @@ -0,0 +1,26 @@ + +
woof!
+fancy text + diff --git a/test/assets/csscoverage/media.html b/test/assets/csscoverage/media.html new file mode 100644 index 0000000000..bfb89f8f75 --- /dev/null +++ b/test/assets/csscoverage/media.html @@ -0,0 +1,4 @@ + +
hello, world
+ diff --git a/test/assets/csscoverage/multiple.html b/test/assets/csscoverage/multiple.html new file mode 100644 index 0000000000..0fd97e962a --- /dev/null +++ b/test/assets/csscoverage/multiple.html @@ -0,0 +1,8 @@ + + + diff --git a/test/assets/csscoverage/simple.html b/test/assets/csscoverage/simple.html new file mode 100644 index 0000000000..3beae21829 --- /dev/null +++ b/test/assets/csscoverage/simple.html @@ -0,0 +1,6 @@ + +
hello, world
+ diff --git a/test/assets/csscoverage/sourceurl.html b/test/assets/csscoverage/sourceurl.html new file mode 100644 index 0000000000..df4e9c276c --- /dev/null +++ b/test/assets/csscoverage/sourceurl.html @@ -0,0 +1,7 @@ + + diff --git a/test/assets/csscoverage/stylesheet1.css b/test/assets/csscoverage/stylesheet1.css new file mode 100644 index 0000000000..60f1eab971 --- /dev/null +++ b/test/assets/csscoverage/stylesheet1.css @@ -0,0 +1,3 @@ +body { + color: red; +} diff --git a/test/assets/csscoverage/stylesheet2.css b/test/assets/csscoverage/stylesheet2.css new file mode 100644 index 0000000000..a87defb098 --- /dev/null +++ b/test/assets/csscoverage/stylesheet2.css @@ -0,0 +1,4 @@ +html { + margin: 0; + padding: 0; +} diff --git a/test/assets/csscoverage/unused.html b/test/assets/csscoverage/unused.html new file mode 100644 index 0000000000..5b8186a3bf --- /dev/null +++ b/test/assets/csscoverage/unused.html @@ -0,0 +1,7 @@ + + diff --git a/test/assets/detect-touch.html b/test/assets/detect-touch.html new file mode 100644 index 0000000000..80a4123fbd --- /dev/null +++ b/test/assets/detect-touch.html @@ -0,0 +1,12 @@ + + + + Detect Touch Test + + + + + + diff --git a/test/assets/digits/0.png b/test/assets/digits/0.png new file mode 100644 index 0000000000..ac3c4768ed Binary files /dev/null and b/test/assets/digits/0.png differ diff --git a/test/assets/digits/1.png b/test/assets/digits/1.png new file mode 100644 index 0000000000..6768222729 Binary files /dev/null and b/test/assets/digits/1.png differ diff --git a/test/assets/digits/2.png b/test/assets/digits/2.png new file mode 100644 index 0000000000..b1daa4735d Binary files /dev/null and b/test/assets/digits/2.png differ diff --git a/test/assets/digits/3.png b/test/assets/digits/3.png new file mode 100644 index 0000000000..6eca99b21b Binary files /dev/null and b/test/assets/digits/3.png differ diff --git a/test/assets/digits/4.png b/test/assets/digits/4.png new file mode 100644 index 0000000000..a721071e2c Binary files /dev/null and b/test/assets/digits/4.png differ diff --git a/test/assets/digits/5.png b/test/assets/digits/5.png new file mode 100644 index 0000000000..15cb19932a Binary files /dev/null and b/test/assets/digits/5.png differ diff --git a/test/assets/digits/6.png b/test/assets/digits/6.png new file mode 100644 index 0000000000..639f38439d Binary files /dev/null and b/test/assets/digits/6.png differ diff --git a/test/assets/digits/7.png b/test/assets/digits/7.png new file mode 100644 index 0000000000..5c1150b005 Binary files /dev/null and b/test/assets/digits/7.png differ diff --git a/test/assets/digits/8.png b/test/assets/digits/8.png new file mode 100644 index 0000000000..abb8b48b0b Binary files /dev/null and b/test/assets/digits/8.png differ diff --git a/test/assets/digits/9.png b/test/assets/digits/9.png new file mode 100644 index 0000000000..6a40a21c6f Binary files /dev/null and b/test/assets/digits/9.png differ diff --git a/test/assets/dynamic-oopif.html b/test/assets/dynamic-oopif.html new file mode 100644 index 0000000000..f00c741dfb --- /dev/null +++ b/test/assets/dynamic-oopif.html @@ -0,0 +1,10 @@ + diff --git a/test/assets/empty.html b/test/assets/empty.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/assets/error.html b/test/assets/error.html new file mode 100644 index 0000000000..130400c006 --- /dev/null +++ b/test/assets/error.html @@ -0,0 +1,15 @@ + diff --git a/test/assets/es6/.eslintrc b/test/assets/es6/.eslintrc new file mode 100644 index 0000000000..1903e176f5 --- /dev/null +++ b/test/assets/es6/.eslintrc @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "sourceType": "module" + } +} \ No newline at end of file diff --git a/test/assets/es6/es6import.js b/test/assets/es6/es6import.js new file mode 100644 index 0000000000..9a0a1095d1 --- /dev/null +++ b/test/assets/es6/es6import.js @@ -0,0 +1,2 @@ +import num from './es6module.js'; +window.__es6injected = num; \ No newline at end of file diff --git a/test/assets/es6/es6module.js b/test/assets/es6/es6module.js new file mode 100644 index 0000000000..a4012bff06 --- /dev/null +++ b/test/assets/es6/es6module.js @@ -0,0 +1 @@ +export default 42; \ No newline at end of file diff --git a/test/assets/es6/es6pathimport.js b/test/assets/es6/es6pathimport.js new file mode 100644 index 0000000000..99919621a8 --- /dev/null +++ b/test/assets/es6/es6pathimport.js @@ -0,0 +1,2 @@ +import num from './es6/es6module.js'; +window.__es6injected = num; \ No newline at end of file diff --git a/test/assets/file-to-upload.txt b/test/assets/file-to-upload.txt new file mode 100644 index 0000000000..b4ad118489 --- /dev/null +++ b/test/assets/file-to-upload.txt @@ -0,0 +1 @@ +contents of the file \ No newline at end of file diff --git a/test/assets/frames/frame.html b/test/assets/frames/frame.html new file mode 100644 index 0000000000..8f20d2da9f --- /dev/null +++ b/test/assets/frames/frame.html @@ -0,0 +1,8 @@ + + + +
Hi, I'm frame
diff --git a/test/assets/frames/frameset.html b/test/assets/frames/frameset.html new file mode 100644 index 0000000000..4d56f88839 --- /dev/null +++ b/test/assets/frames/frameset.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/assets/frames/nested-frames.html b/test/assets/frames/nested-frames.html new file mode 100644 index 0000000000..de1987586f --- /dev/null +++ b/test/assets/frames/nested-frames.html @@ -0,0 +1,25 @@ + + + + diff --git a/test/assets/frames/one-frame.html b/test/assets/frames/one-frame.html new file mode 100644 index 0000000000..e941d795a2 --- /dev/null +++ b/test/assets/frames/one-frame.html @@ -0,0 +1 @@ + diff --git a/test/assets/frames/script.js b/test/assets/frames/script.js new file mode 100644 index 0000000000..be22256d16 --- /dev/null +++ b/test/assets/frames/script.js @@ -0,0 +1 @@ +console.log('Cheers!'); diff --git a/test/assets/frames/style.css b/test/assets/frames/style.css new file mode 100644 index 0000000000..5b5436e874 --- /dev/null +++ b/test/assets/frames/style.css @@ -0,0 +1,3 @@ +div { + color: blue; +} diff --git a/test/assets/frames/two-frames.html b/test/assets/frames/two-frames.html new file mode 100644 index 0000000000..b2ee853eda --- /dev/null +++ b/test/assets/frames/two-frames.html @@ -0,0 +1,13 @@ + + + diff --git a/test/assets/global-var.html b/test/assets/global-var.html new file mode 100644 index 0000000000..b6be975038 --- /dev/null +++ b/test/assets/global-var.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/assets/grid.html b/test/assets/grid.html new file mode 100644 index 0000000000..0bdbb1220e --- /dev/null +++ b/test/assets/grid.html @@ -0,0 +1,52 @@ + + + diff --git a/test/assets/historyapi.html b/test/assets/historyapi.html new file mode 100644 index 0000000000..bacaf9e9a0 --- /dev/null +++ b/test/assets/historyapi.html @@ -0,0 +1,5 @@ + diff --git a/test/assets/injectedfile.js b/test/assets/injectedfile.js new file mode 100644 index 0000000000..6cb04f1bba --- /dev/null +++ b/test/assets/injectedfile.js @@ -0,0 +1,2 @@ +window.__injected = 42; +window.__injectedError = new Error('hi'); \ No newline at end of file diff --git a/test/assets/injectedstyle.css b/test/assets/injectedstyle.css new file mode 100644 index 0000000000..aa1634c255 --- /dev/null +++ b/test/assets/injectedstyle.css @@ -0,0 +1,3 @@ +body { + background-color: red; +} diff --git a/test/assets/input/button.html b/test/assets/input/button.html new file mode 100644 index 0000000000..aaba6a5e2a --- /dev/null +++ b/test/assets/input/button.html @@ -0,0 +1,22 @@ + + + + Button test + + + + + + + \ No newline at end of file diff --git a/test/assets/input/checkbox.html b/test/assets/input/checkbox.html new file mode 100644 index 0000000000..ca56762e2b --- /dev/null +++ b/test/assets/input/checkbox.html @@ -0,0 +1,42 @@ + + + + Selection Test + + + + + + + diff --git a/test/assets/input/fileupload.html b/test/assets/input/fileupload.html new file mode 100644 index 0000000000..55fd7c5006 --- /dev/null +++ b/test/assets/input/fileupload.html @@ -0,0 +1,9 @@ + + + + File upload test + + + + + \ No newline at end of file diff --git a/test/assets/input/keyboard.html b/test/assets/input/keyboard.html new file mode 100644 index 0000000000..fd962c7518 --- /dev/null +++ b/test/assets/input/keyboard.html @@ -0,0 +1,42 @@ + + + + Keyboard test + + + + + + \ No newline at end of file diff --git a/test/assets/input/mouse-helper.js b/test/assets/input/mouse-helper.js new file mode 100644 index 0000000000..3c4d57033c --- /dev/null +++ b/test/assets/input/mouse-helper.js @@ -0,0 +1,62 @@ +// This injects a box into the page that moves with the mouse; +// Useful for debugging +(function(){ + const box = document.createElement('div'); + box.classList.add('mouse-helper'); + const styleElement = document.createElement('style'); + styleElement.innerHTML = ` + .mouse-helper { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + background: rgba(0,0,0,.4); + border: 1px solid white; + border-radius: 10px; + margin-left: -10px; + margin-top: -10px; + transition: background .2s, border-radius .2s, border-color .2s; + } + .mouse-helper.button-1 { + transition: none; + background: rgba(0,0,0,0.9); + } + .mouse-helper.button-2 { + transition: none; + border-color: rgba(0,0,255,0.9); + } + .mouse-helper.button-3 { + transition: none; + border-radius: 4px; + } + .mouse-helper.button-4 { + transition: none; + border-color: rgba(255,0,0,0.9); + } + .mouse-helper.button-5 { + transition: none; + border-color: rgba(0,255,0,0.9); + } + `; + document.head.appendChild(styleElement); + document.body.appendChild(box); + document.addEventListener('mousemove', event => { + box.style.left = event.pageX + 'px'; + box.style.top = event.pageY + 'px'; + updateButtons(event.buttons); + }, true); + document.addEventListener('mousedown', event => { + updateButtons(event.buttons); + box.classList.add('button-' + event.which); + }, true); + document.addEventListener('mouseup', event => { + updateButtons(event.buttons); + box.classList.remove('button-' + event.which); + }, true); + function updateButtons(buttons) { + for (let i = 0; i < 5; i++) + box.classList.toggle('button-' + i, buttons & (1 << i)); + } +})(); diff --git a/test/assets/input/rotatedButton.html b/test/assets/input/rotatedButton.html new file mode 100644 index 0000000000..1bce66cf5e --- /dev/null +++ b/test/assets/input/rotatedButton.html @@ -0,0 +1,21 @@ + + + + Rotated button test + + + + + + + + diff --git a/test/assets/input/scrollable.html b/test/assets/input/scrollable.html new file mode 100644 index 0000000000..885d3739d5 --- /dev/null +++ b/test/assets/input/scrollable.html @@ -0,0 +1,23 @@ + + + + Scrollable test + + + + + + \ No newline at end of file diff --git a/test/assets/input/select.html b/test/assets/input/select.html new file mode 100644 index 0000000000..879a537a76 --- /dev/null +++ b/test/assets/input/select.html @@ -0,0 +1,69 @@ + + + + Selection Test + + + + + + diff --git a/test/assets/input/textarea.html b/test/assets/input/textarea.html new file mode 100644 index 0000000000..f71a0054b5 --- /dev/null +++ b/test/assets/input/textarea.html @@ -0,0 +1,19 @@ + + + + Textarea test + + + + +
+ + + + \ No newline at end of file diff --git a/test/assets/input/touches.html b/test/assets/input/touches.html new file mode 100644 index 0000000000..4392cfacbd --- /dev/null +++ b/test/assets/input/touches.html @@ -0,0 +1,35 @@ + + + + Touch test + + + + + + + \ No newline at end of file diff --git a/test/assets/jscoverage/eval.html b/test/assets/jscoverage/eval.html new file mode 100644 index 0000000000..838ae28763 --- /dev/null +++ b/test/assets/jscoverage/eval.html @@ -0,0 +1 @@ + diff --git a/test/assets/jscoverage/involved.html b/test/assets/jscoverage/involved.html new file mode 100644 index 0000000000..889c86bed5 --- /dev/null +++ b/test/assets/jscoverage/involved.html @@ -0,0 +1,15 @@ + diff --git a/test/assets/jscoverage/multiple.html b/test/assets/jscoverage/multiple.html new file mode 100644 index 0000000000..bdef59885b --- /dev/null +++ b/test/assets/jscoverage/multiple.html @@ -0,0 +1,2 @@ + + diff --git a/test/assets/jscoverage/ranges.html b/test/assets/jscoverage/ranges.html new file mode 100644 index 0000000000..a537a7da6a --- /dev/null +++ b/test/assets/jscoverage/ranges.html @@ -0,0 +1,2 @@ + diff --git a/test/assets/jscoverage/script1.js b/test/assets/jscoverage/script1.js new file mode 100644 index 0000000000..3bd241b50e --- /dev/null +++ b/test/assets/jscoverage/script1.js @@ -0,0 +1 @@ +console.log(3); diff --git a/test/assets/jscoverage/script2.js b/test/assets/jscoverage/script2.js new file mode 100644 index 0000000000..3bd241b50e --- /dev/null +++ b/test/assets/jscoverage/script2.js @@ -0,0 +1 @@ +console.log(3); diff --git a/test/assets/jscoverage/simple.html b/test/assets/jscoverage/simple.html new file mode 100644 index 0000000000..49eeeea6ae --- /dev/null +++ b/test/assets/jscoverage/simple.html @@ -0,0 +1,2 @@ + diff --git a/test/assets/jscoverage/sourceurl.html b/test/assets/jscoverage/sourceurl.html new file mode 100644 index 0000000000..e477750320 --- /dev/null +++ b/test/assets/jscoverage/sourceurl.html @@ -0,0 +1,4 @@ + diff --git a/test/assets/jscoverage/unused.html b/test/assets/jscoverage/unused.html new file mode 100644 index 0000000000..59c4a5a70b --- /dev/null +++ b/test/assets/jscoverage/unused.html @@ -0,0 +1 @@ + diff --git a/test/assets/mobile.html b/test/assets/mobile.html new file mode 100644 index 0000000000..8e94b2fe29 --- /dev/null +++ b/test/assets/mobile.html @@ -0,0 +1 @@ + diff --git a/test/assets/modernizr.js b/test/assets/modernizr.js new file mode 100644 index 0000000000..7991a4ec40 --- /dev/null +++ b/test/assets/modernizr.js @@ -0,0 +1,3 @@ +/*! modernizr 3.5.0 (Custom Build) | MIT * +* https://modernizr.com/download/?-touchevents-setclasses !*/ +!function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t + async function sleep(delay) { + return new Promise(resolve => setTimeout(resolve, delay)); + } + + async function main() { + const roundOne = Promise.all([ + fetch('fetch-request-a.js'), + fetch('fetch-request-b.js'), + fetch('fetch-request-c.js'), + ]); + + await roundOne; + await sleep(50); + await fetch('fetch-request-d.js'); + } + + main(); + diff --git a/test/assets/offscreenbuttons.html b/test/assets/offscreenbuttons.html new file mode 100644 index 0000000000..d45e2a4129 --- /dev/null +++ b/test/assets/offscreenbuttons.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + diff --git a/test/assets/one-style.css b/test/assets/one-style.css new file mode 100644 index 0000000000..7b26410d8a --- /dev/null +++ b/test/assets/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/one-style.html b/test/assets/one-style.html new file mode 100644 index 0000000000..4760f2b9f7 --- /dev/null +++ b/test/assets/one-style.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/assets/playground.html b/test/assets/playground.html new file mode 100644 index 0000000000..828cfb1c70 --- /dev/null +++ b/test/assets/playground.html @@ -0,0 +1,15 @@ + + + + Playground + + + + +
First div
+
+ Second div + Inner span +
+ + \ No newline at end of file diff --git a/test/assets/popup/popup.html b/test/assets/popup/popup.html new file mode 100644 index 0000000000..b855162c25 --- /dev/null +++ b/test/assets/popup/popup.html @@ -0,0 +1,9 @@ + + + + Popup + + + I am a popup + + diff --git a/test/assets/popup/window-open.html b/test/assets/popup/window-open.html new file mode 100644 index 0000000000..d138be1d22 --- /dev/null +++ b/test/assets/popup/window-open.html @@ -0,0 +1,11 @@ + + + + Popup test + + + + + diff --git a/test/assets/pptr.png b/test/assets/pptr.png new file mode 100644 index 0000000000..65d87c68e6 Binary files /dev/null and b/test/assets/pptr.png differ diff --git a/test/assets/resetcss.html b/test/assets/resetcss.html new file mode 100644 index 0000000000..e4e04b1f8a --- /dev/null +++ b/test/assets/resetcss.html @@ -0,0 +1,50 @@ + diff --git a/test/assets/self-request.html b/test/assets/self-request.html new file mode 100644 index 0000000000..88aff620ff --- /dev/null +++ b/test/assets/self-request.html @@ -0,0 +1,5 @@ + diff --git a/test/assets/serviceworkers/empty/sw.html b/test/assets/serviceworkers/empty/sw.html new file mode 100644 index 0000000000..bef85d985b --- /dev/null +++ b/test/assets/serviceworkers/empty/sw.html @@ -0,0 +1,3 @@ + diff --git a/test/assets/serviceworkers/empty/sw.js b/test/assets/serviceworkers/empty/sw.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/assets/serviceworkers/fetch/style.css b/test/assets/serviceworkers/fetch/style.css new file mode 100644 index 0000000000..7b26410d8a --- /dev/null +++ b/test/assets/serviceworkers/fetch/style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/serviceworkers/fetch/sw.html b/test/assets/serviceworkers/fetch/sw.html new file mode 100644 index 0000000000..a9d28acb09 --- /dev/null +++ b/test/assets/serviceworkers/fetch/sw.html @@ -0,0 +1,5 @@ + + diff --git a/test/assets/serviceworkers/fetch/sw.js b/test/assets/serviceworkers/fetch/sw.js new file mode 100644 index 0000000000..d44c7eab94 --- /dev/null +++ b/test/assets/serviceworkers/fetch/sw.js @@ -0,0 +1,7 @@ +self.addEventListener('fetch', event => { + event.respondWith(fetch(event.request)); +}); + +self.addEventListener('activate', event => { + event.waitUntil(clients.claim()); +}); diff --git a/test/assets/shadow.html b/test/assets/shadow.html new file mode 100644 index 0000000000..7242e673b5 --- /dev/null +++ b/test/assets/shadow.html @@ -0,0 +1,17 @@ + diff --git a/test/assets/simple-extension/content-script.js b/test/assets/simple-extension/content-script.js new file mode 100644 index 0000000000..965f99fd3d --- /dev/null +++ b/test/assets/simple-extension/content-script.js @@ -0,0 +1,3 @@ +console.log('hey from the content-script'); +self.thisIsTheContentScript = true; + diff --git a/test/assets/simple-extension/index.js b/test/assets/simple-extension/index.js new file mode 100644 index 0000000000..a0bb3f4eae --- /dev/null +++ b/test/assets/simple-extension/index.js @@ -0,0 +1,2 @@ +// Mock script for background extension +window.MAGIC = 42; diff --git a/test/assets/simple-extension/manifest.json b/test/assets/simple-extension/manifest.json new file mode 100644 index 0000000000..da2cd082ed --- /dev/null +++ b/test/assets/simple-extension/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "Simple extension", + "version": "0.1", + "background": { + "scripts": ["index.js"] + }, + "content_scripts": [{ + "matches": [""], + "css": [], + "js": ["content-script.js"] + }], + "permissions": ["background", "activeTab"], + "manifest_version": 2 +} diff --git a/test/assets/simple.json b/test/assets/simple.json new file mode 100644 index 0000000000..6d95903051 --- /dev/null +++ b/test/assets/simple.json @@ -0,0 +1 @@ +{"foo": "bar"} diff --git a/test/assets/tamperable.html b/test/assets/tamperable.html new file mode 100644 index 0000000000..d027e97038 --- /dev/null +++ b/test/assets/tamperable.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/assets/title.html b/test/assets/title.html new file mode 100644 index 0000000000..88a86ce412 --- /dev/null +++ b/test/assets/title.html @@ -0,0 +1 @@ +Woof-Woof diff --git a/test/assets/worker/worker.html b/test/assets/worker/worker.html new file mode 100644 index 0000000000..7de2d9fd9e --- /dev/null +++ b/test/assets/worker/worker.html @@ -0,0 +1,14 @@ + + + + Worker test + + + + + \ No newline at end of file diff --git a/test/assets/worker/worker.js b/test/assets/worker/worker.js new file mode 100644 index 0000000000..d0d229a192 --- /dev/null +++ b/test/assets/worker/worker.js @@ -0,0 +1,16 @@ +console.log('hello from the worker'); + +function workerFunction() { + return 'worker function result'; +} + +self.addEventListener('message', event => { + console.log('got this data: ' + event.data); +}); + +(async function() { + while (true) { + self.postMessage(workerFunction.toString()); + await new Promise(x => setTimeout(x, 100)); + } +})(); \ No newline at end of file diff --git a/test/assets/wrappedlink.html b/test/assets/wrappedlink.html new file mode 100644 index 0000000000..429b6e9156 --- /dev/null +++ b/test/assets/wrappedlink.html @@ -0,0 +1,32 @@ + +
+ 123321 +
+ diff --git a/test/browser.spec.js b/test/browser.spec.js new file mode 100644 index 0000000000..75d9f0265b --- /dev/null +++ b/test/browser.spec.js @@ -0,0 +1,73 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, headless, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(WEBKIT)('Browser.version', function() { + it('should return whether we are in headless', async({browser}) => { + const version = await browser.version(); + expect(version.length).toBeGreaterThan(0); + if (CHROME || WEBKIT) + expect(version.startsWith('Headless')).toBe(headless); + else + expect(version.startsWith('Firefox/')).toBe(true); + }); + }); + + describe.skip(WEBKIT)('Browser.userAgent', function() { + it('should include WebKit', async({browser}) => { + const userAgent = await browser.userAgent(); + expect(userAgent.length).toBeGreaterThan(0); + if (CHROME || WEBKIT) + expect(userAgent).toContain('WebKit'); + else + expect(userAgent).toContain('Gecko'); + }); + }); + + describe.skip(WEBKIT)('Browser.target', function() { + it('should return browser target', async({browser}) => { + const target = browser.target(); + expect(target.type()).toBe('browser'); + }); + }); + + describe('Browser.process', function() { + it('should return child_process instance', async function({browser}) { + const process = await browser.process(); + expect(process.pid).toBeGreaterThan(0); + }); + it.skip(WEBKIT)('should not return child_process for remote browser', async function({browser}) { + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser = await playwright.connect({browserWSEndpoint}); + expect(remoteBrowser.process()).toBe(null); + remoteBrowser.disconnect(); + }); + }); + + describe.skip(WEBKIT)('Browser.isConnected', () => { + it('should set the browser connected state', async({browser}) => { + const browserWSEndpoint = browser.wsEndpoint(); + const newBrowser = await playwright.connect({browserWSEndpoint}); + expect(newBrowser.isConnected()).toBe(true); + newBrowser.disconnect(); + expect(newBrowser.isConnected()).toBe(false); + }); + }); +}; diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js new file mode 100644 index 0000000000..4c2cd071d9 --- /dev/null +++ b/test/browsercontext.spec.js @@ -0,0 +1,156 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('BrowserContext', function() { + it('should have default context', async function({browser, server}) { + expect(browser.browserContexts().length).toBe(1); + const defaultContext = browser.browserContexts()[0]; + expect(defaultContext.isIncognito()).toBe(false); + let error = null; + await defaultContext.close().catch(e => error = e); + expect(browser.defaultBrowserContext()).toBe(defaultContext); + expect(error.message).toContain('cannot be closed'); + }); + it('should create new incognito context', async function({browser, server}) { + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(context.isIncognito()).toBe(true); + expect(browser.browserContexts().length).toBe(2); + expect(browser.browserContexts().indexOf(context) !== -1).toBe(true); + await context.close(); + expect(browser.browserContexts().length).toBe(1); + }); + it.skip(WEBKIT)('should close all belonging targets once closing context', async function({browser, server}) { + expect((await browser.pages()).length).toBe(1); + + const context = await browser.createIncognitoBrowserContext(); + await context.newPage(); + expect((await browser.pages()).length).toBe(2); + expect((await context.pages()).length).toBe(1); + + await context.close(); + expect((await browser.pages()).length).toBe(1); + }); + it('window.open should use parent tab context', async function({browser, server}) { + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const [popupTarget] = await Promise.all([ + utils.waitEvent(browser, 'targetcreated'), + page.evaluate(url => window.open(url), server.EMPTY_PAGE) + ]); + expect(popupTarget.browserContext()).toBe(context); + await context.close(); + }); + it.skip(WEBKIT)('should fire target events', async function({browser, server}) { + const context = await browser.createIncognitoBrowserContext(); + const events = []; + context.on('targetcreated', target => events.push('CREATED: ' + target.url())); + context.on('targetchanged', target => events.push('CHANGED: ' + target.url())); + context.on('targetdestroyed', target => events.push('DESTROYED: ' + target.url())); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual([ + 'CREATED: about:blank', + `CHANGED: ${server.EMPTY_PAGE}`, + `DESTROYED: ${server.EMPTY_PAGE}` + ]); + await context.close(); + }); + it('should wait for a target', async function({browser, server}) { + const context = await browser.createIncognitoBrowserContext(); + let resolved = false; + const targetPromise = context.waitForTarget(target => target.url() === server.EMPTY_PAGE); + targetPromise.then(() => resolved = true); + const page = await context.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + const target = await targetPromise; + expect(await target.page()).toBe(page); + await context.close(); + }); + it('should timeout waiting for a non-existent target', async function({browser, server}) { + const context = await browser.createIncognitoBrowserContext(); + const error = await context.waitForTarget(target => target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(e => e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + await context.close(); + }); + it('should isolate localStorage and cookies', async function({browser, server}) { + // Create two incognito contexts. + const context1 = await browser.createIncognitoBrowserContext(); + const context2 = await browser.createIncognitoBrowserContext(); + expect(context1.targets().length).toBe(0); + expect(context2.targets().length).toBe(0); + + // Create a page in first incognito context. + const page1 = await context1.newPage(); + await page1.goto(server.EMPTY_PAGE); + await page1.evaluate(() => { + localStorage.setItem('name', 'page1'); + document.cookie = 'name=page1'; + }); + + expect(context1.targets().length).toBe(1); + expect(context2.targets().length).toBe(0); + + // Create a page in second incognito context. + const page2 = await context2.newPage(); + await page2.goto(server.EMPTY_PAGE); + await page2.evaluate(() => { + localStorage.setItem('name', 'page2'); + document.cookie = 'name=page2'; + }); + + expect(context1.targets().length).toBe(1); + expect(context1.targets()[0]).toBe(page1.target()); + expect(context2.targets().length).toBe(1); + expect(context2.targets()[0]).toBe(page2.target()); + + // Make sure pages don't share localstorage or cookies. + expect(await page1.evaluate(() => localStorage.getItem('name'))).toBe('page1'); + expect(await page1.evaluate(() => document.cookie)).toBe('name=page1'); + expect(await page2.evaluate(() => localStorage.getItem('name'))).toBe('page2'); + expect(await page2.evaluate(() => document.cookie)).toBe('name=page2'); + + // Cleanup contexts. + await Promise.all([ + context1.close(), + context2.close() + ]); + expect(browser.browserContexts().length).toBe(1); + }); + it.skip(WEBKIT)('should work across sessions', async function({browser, server}) { + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts().length).toBe(2); + const remoteBrowser = await playwright.connect({ + browserWSEndpoint: browser.wsEndpoint() + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts.length).toBe(2); + remoteBrowser.disconnect(); + await context.close(); + }); + }); +}; diff --git a/test/chromiumonly.spec.js b/test/chromiumonly.spec.js new file mode 100644 index 0000000000..8d80b4a097 --- /dev/null +++ b/test/chromiumonly.spec.js @@ -0,0 +1,144 @@ +/** + * 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.addLauncherTests = function({testRunner, expect, defaultBrowserOptions, playwright}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Chromium-Specific Launcher tests', function() { + describe('Playwright.launch |browserURL| option', function() { + it('should be able to connect using browserUrl, with and without trailing slash', async({server}) => { + const originalBrowser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'] + })); + const browserURL = 'http://127.0.0.1:21222'; + + const browser1 = await playwright.connect({browserURL}); + const page1 = await browser1.newPage(); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + browser1.disconnect(); + + const browser2 = await playwright.connect({browserURL: browserURL + '/'}); + const page2 = await browser2.newPage(); + expect(await page2.evaluate(() => 8 * 7)).toBe(56); + browser2.disconnect(); + originalBrowser.close(); + }); + it('should throw when using both browserWSEndpoint and browserURL', async({server}) => { + const originalBrowser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'] + })); + const browserURL = 'http://127.0.0.1:21222'; + + let error = null; + await playwright.connect({browserURL, browserWSEndpoint: originalBrowser.wsEndpoint()}).catch(e => error = e); + expect(error.message).toContain('Exactly one of browserWSEndpoint, browserURL or transport'); + + originalBrowser.close(); + }); + it('should throw when trying to connect to non-existing browser', async({server}) => { + const originalBrowser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'] + })); + const browserURL = 'http://127.0.0.1:32333'; + + let error = null; + await playwright.connect({browserURL}).catch(e => error = e); + expect(error.message).toContain('Failed to fetch browser webSocket url from'); + originalBrowser.close(); + }); + }); + + describe('Playwright.launch |pipe| option', function() { + it('should support the pipe option', async() => { + const options = Object.assign({pipe: true}, defaultBrowserOptions); + const browser = await playwright.launch(options); + expect((await browser.pages()).length).toBe(1); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should support the pipe argument', async() => { + const options = Object.assign({}, defaultBrowserOptions); + options.args = ['--remote-debugging-pipe'].concat(options.args || []); + const browser = await playwright.launch(options); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should fire "disconnected" when closing with pipe', async() => { + const options = Object.assign({pipe: true}, defaultBrowserOptions); + const browser = await playwright.launch(options); + const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve)); + // Emulate user exiting browser. + browser.process().kill(); + await disconnectedEventPromise; + }); + }); + + describe('Page.waitForFileChooser', () => { + it('should fail gracefully when trying to work with filechoosers within multiple connections', async() => { + // 1. Launch a browser and connect to all pages. + const originalBrowser = await playwright.launch(defaultBrowserOptions); + await originalBrowser.pages(); + // 2. Connect a remote browser and connect to first page. + const remoteBrowser = await playwright.connect({browserWSEndpoint: originalBrowser.wsEndpoint()}); + const [page] = await remoteBrowser.pages(); + // 3. Make sure |page.waitForFileChooser()| does not work with multiclient. + let error = null; + await page.waitForFileChooser().catch(e => error = e); + expect(error.message).toBe('File chooser handling does not work with multiple connections to the same page'); + originalBrowser.close(); + }); + + }); + }); +}; + +module.exports.addPageTests = function({testRunner, expect}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Chromium-Specific Page Tests', function() { + it('Page.setRequestInterception should work with intervention headers', async({server, page}) => { + server.setRoute('/intervention', (req, res) => res.end(` + + `)); + server.setRedirect('/intervention.js', '/redirect.js'); + let serverRequest = null; + server.setRoute('/redirect.js', (req, res) => { + serverRequest = req; + res.end('console.log(1);'); + }); + + await page.setRequestInterception(true); + page.on('request', request => request.continue()); + await page.goto(server.PREFIX + '/intervention'); + // Check for feature URL substring rather than https://www.chromestatus.com to + // make it work with Edgium. + expect(serverRequest.headers.intervention).toContain('feature/5718547946799104'); + }); + }); +}; + diff --git a/test/click.spec.js b/test/click.spec.js new file mode 100644 index 0000000000..344809b030 --- /dev/null +++ b/test/click.spec.js @@ -0,0 +1,310 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.click', function() { + it('should click the button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + it('should click svg', async({page, server}) => { + await page.setContent(` + + + + `); + await page.click('circle'); + expect(await page.evaluate(() => window.__CLICKED)).toBe(42); + }); + it.skip(FFOX || WEBKIT)('should click the button if window.Node is removed', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => delete window.Node); + await page.click('button'); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/4281 + it('should click on a span with an inline element inside', async({page, server}) => { + await page.setContent(` + + + `); + await page.click('span'); + expect(await page.evaluate(() => window.CLICKED)).toBe(42); + }); + it('should not throw UnhandledPromiseRejection when page closes', async({page, server}) => { + const newPage = await page.browser().newPage(); + await Promise.all([ + newPage.close(), + newPage.mouse.click(1, 2), + ]).catch(e => {}); + }); + it('should click the button after navigation ', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + it.skip(FFOX || WEBKIT)('should click with disabled javascript', async({page, server}) => { + await page.setJavaScriptEnabled(false); + await page.goto(server.PREFIX + '/wrappedlink.html'); + await Promise.all([ + page.click('a'), + page.waitForNavigation() + ]); + expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); + }); + it.skip(FFOX)('should click when one of inline box children is outside of viewport', async({page, server}) => { + await page.setContent(` + + woofdoggo + `); + await page.click('span'); + expect(await page.evaluate(() => window.CLICKED)).toBe(42); + }); + it.skip(FFOX || WEBKIT)('should select the text by triple clicking', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = 'This is the text that we are going to try to select. Let\'s see how it goes.'; + await page.keyboard.type(text); + await page.tripleclick('textarea'); + expect(await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); + })).toBe(text); + }); + it('should click offscreen buttons', async({page, server}) => { + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + const messages = []; + page.on('console', msg => messages.push(msg.text())); + for (let i = 0; i < 11; ++i) { + // We might've scrolled to click a button - reset to (0, 0). + await page.evaluate(() => window.scrollTo(0, 0)); + await page.click(`#btn${i}`); + } + expect(messages).toEqual([ + 'button #0 clicked', + 'button #1 clicked', + 'button #2 clicked', + 'button #3 clicked', + 'button #4 clicked', + 'button #5 clicked', + 'button #6 clicked', + 'button #7 clicked', + 'button #8 clicked', + 'button #9 clicked', + 'button #10 clicked' + ]); + }); + + it('should click wrapped links', async({page, server}) => { + await page.goto(server.PREFIX + '/wrappedlink.html'); + await page.click('a'); + expect(await page.evaluate(() => window.__clicked)).toBe(true); + }); + + it('should click on checkbox input and toggle', async({page, server}) => { + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => result.check)).toBe(null); + await page.click('input#agree'); + expect(await page.evaluate(() => result.check)).toBe(true); + expect(await page.evaluate(() => result.events)).toEqual([ + 'mouseover', + 'mouseenter', + 'mousemove', + 'mousedown', + 'mouseup', + 'click', + 'input', + 'change', + ]); + await page.click('input#agree'); + expect(await page.evaluate(() => result.check)).toBe(false); + }); + + it('should click on checkbox label and toggle', async({page, server}) => { + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => result.check)).toBe(null); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => result.check)).toBe(true); + expect(await page.evaluate(() => result.events)).toEqual([ + 'click', + 'input', + 'change', + ]); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => result.check)).toBe(false); + }); + + it('should fail to click a missing button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + let error = null; + await page.click('button.does-not-exist').catch(e => error = e); + expect(error.message).toBe('No node found for selector: button.does-not-exist'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/161 + it('should not hang with touch-enabled viewports', async({page, server}) => { + await page.setViewport(playwright.devices['iPhone 6'].viewport); + await page.mouse.down(); + await page.mouse.move(100, 10); + await page.mouse.up(); + }); + it('should scroll and click the button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-5'); + expect(await page.evaluate(() => document.querySelector('#button-5').textContent)).toBe('clicked'); + await page.click('#button-80'); + expect(await page.evaluate(() => document.querySelector('#button-80').textContent)).toBe('clicked'); + }); + it.skip(FFOX || WEBKIT)('should double click the button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + window.double = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', event => { + window.double = true; + }); + }); + const button = await page.$('button'); + await button.dblclick(); + expect(await page.evaluate('double')).toBe(true); + expect(await page.evaluate('result')).toBe('Clicked'); + }); + it('should click a partially obscured button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + const button = document.querySelector('button'); + button.textContent = 'Some really long text that will go offscreen'; + button.style.position = 'absolute'; + button.style.left = '368px'; + }); + await page.click('button'); + expect(await page.evaluate(() => window.result)).toBe('Clicked'); + }); + it('should click a rotated button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/rotatedButton.html'); + await page.click('button'); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + it('should fire contextmenu event on right click', async({page, server}) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', {button: 'right'}); + expect(await page.evaluate(() => document.querySelector('#button-8').textContent)).toBe('context menu'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/206 + it('should click links which cause navigation', async({page, server}) => { + await page.setContent(`empty.html`); + // This await should not hang. + await page.click('a'); + }); + it('should click the button inside an iframe', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent('
spacer
'); + await utils.attachFrame(page, 'button-test', server.PREFIX + '/input/button.html'); + const frame = page.frames()[1]; + const button = await frame.$('button'); + await button.click(); + expect(await frame.evaluate(() => window.result)).toBe('Clicked'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/4110 + xit('should click the button with fixed position inside an iframe', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setViewport({width: 500, height: 500}); + await page.setContent('
spacer
'); + await utils.attachFrame(page, 'button-test', server.CROSS_PROCESS_PREFIX + '/input/button.html'); + const frame = page.frames()[1]; + await frame.$eval('button', button => button.style.setProperty('position', 'fixed')); + await frame.click('button'); + expect(await frame.evaluate(() => window.result)).toBe('Clicked'); + }); + it.skip(WEBKIT)('should click the button with deviceScaleFactor set', async({page, server}) => { + await page.setViewport({width: 400, height: 400, deviceScaleFactor: 5}); + expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5); + await page.setContent('
spacer
'); + await utils.attachFrame(page, 'button-test', server.PREFIX + '/input/button.html'); + const frame = page.frames()[1]; + const button = await frame.$('button'); + await button.click(); + expect(await frame.evaluate(() => window.result)).toBe('Clicked'); + }); + + it.skip(FFOX || WEBKIT)('should click the button with relative point', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button', { relativePoint: { x: 20, y: 10 } }); + expect(await page.evaluate(() => result)).toBe('Clicked'); + expect(await page.evaluate(() => offsetX)).toBe(20); + expect(await page.evaluate(() => offsetY)).toBe(10); + }); + it.skip(FFOX || WEBKIT)('should click a very large button with relative point', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.$eval('button', button => button.style.height = button.style.width = '2000px'); + await page.click('button', { relativePoint: { x: 1900, y: 1910 } }); + expect(await page.evaluate(() => window.result)).toBe('Clicked'); + expect(await page.evaluate(() => offsetX)).toBe(1900); + expect(await page.evaluate(() => offsetY)).toBe(1910); + }); + xit('should click a button in scrolling container with relative point', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.$eval('button', button => { + const container = document.createElement('div'); + container.style.overflow = 'auto'; + container.style.width = '200px'; + container.style.height = '200px'; + button.parentElement.insertBefore(container, button); + container.appendChild(button); + button.style.height = '2000px'; + button.style.width = '2000px'; + }); + await page.click('button', { relativePoint: { x: 1900, y: 1910 } }); + expect(await page.evaluate(() => window.result)).toBe('Clicked'); + expect(await page.evaluate(() => offsetX)).toBe(1900); + expect(await page.evaluate(() => offsetY)).toBe(1910); + }); + + it.skip(FFOX || WEBKIT)('should update modifiers correctly', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button', { modifiers: ['Shift'] }); + expect(await page.evaluate(() => shiftKey)).toBe(true); + await page.click('button', { modifiers: [] }); + expect(await page.evaluate(() => shiftKey)).toBe(false); + + await page.keyboard.down('Shift'); + await page.click('button', { modifiers: [] }); + expect(await page.evaluate(() => shiftKey)).toBe(false); + await page.click('button'); + expect(await page.evaluate(() => shiftKey)).toBe(true); + await page.keyboard.up('Shift'); + await page.click('button'); + expect(await page.evaluate(() => shiftKey)).toBe(false); + }); + }); +}; diff --git a/test/cookies.spec.js b/test/cookies.spec.js new file mode 100644 index 0000000000..72ba2507e0 --- /dev/null +++ b/test/cookies.spec.js @@ -0,0 +1,395 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.cookies', function() { + it('should return no cookies in pristine browser context', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + expect(await page.cookies()).toEqual([]); + }); + it('should get a cookie', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + expect(await page.cookies()).toEqual([{ + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + it('should properly report httpOnly cookie', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', ';HttpOnly; Path=/'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].httpOnly).toBe(true); + }); + it.skip(FFOX || WEBKIT)('should properly report "Strict" sameSite cookie', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', ';SameSite=Strict'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].sameSite).toBe('Strict'); + }); + it.skip(FFOX || WEBKIT)('should properly report "Lax" sameSite cookie', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', ';SameSite=Lax'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].sameSite).toBe('Lax'); + }); + it('should get multiple cookies', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + document.cookie = 'password=1234'; + }); + const cookies = await page.cookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + expect(cookies).toEqual([ + { + name: 'password', + value: '1234', + domain: 'localhost', + path: '/', + expires: -1, + size: 12, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }, + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }, + ]); + }); + it.skip(WEBKIT)('should get cookies from multiple urls', async({page, server}) => { + await page.setCookie({ + url: 'https://foo.com', + name: 'doggo', + value: 'woofs', + }, { + url: 'https://bar.com', + name: 'catto', + value: 'purrs', + }, { + url: 'https://baz.com', + name: 'birdo', + value: 'tweets', + }); + const cookies = await page.cookies('https://foo.com', 'https://baz.com'); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + expect(cookies).toEqual([{ + name: 'birdo', + value: 'tweets', + domain: 'baz.com', + path: '/', + expires: -1, + size: 11, + httpOnly: false, + secure: true, + session: true, + sameSite: 'None', + }, { + name: 'doggo', + value: 'woofs', + domain: 'foo.com', + path: '/', + expires: -1, + size: 10, + httpOnly: false, + secure: true, + session: true, + sameSite: 'None', + }]); + }); + }); + + describe.skip(WEBKIT)('Page.setCookie', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456' + }); + expect(await page.evaluate(() => document.cookie)).toEqual('password=123456'); + }); + it('should isolate cookies in browser contexts', async({page, server, browser}) => { + const anotherContext = await browser.createIncognitoBrowserContext(); + const anotherPage = await anotherContext.newPage(); + + await page.goto(server.EMPTY_PAGE); + await anotherPage.goto(server.EMPTY_PAGE); + + await page.setCookie({name: 'page1cookie', value: 'page1value'}); + await anotherPage.setCookie({name: 'page2cookie', value: 'page2value'}); + + const cookies1 = await page.cookies(); + const cookies2 = await anotherPage.cookies(); + expect(cookies1.length).toBe(1); + expect(cookies2.length).toBe(1); + expect(cookies1[0].name).toBe('page1cookie'); + expect(cookies1[0].value).toBe('page1value'); + expect(cookies2[0].name).toBe('page2cookie'); + expect(cookies2[0].value).toBe('page2value'); + await anotherContext.close(); + }); + it('should set multiple cookies', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456' + }, { + name: 'foo', + value: 'bar' + }); + expect(await page.evaluate(() => { + const cookies = document.cookie.split(';'); + return cookies.map(cookie => cookie.trim()).sort(); + })).toEqual([ + 'foo=bar', + 'password=123456', + ]); + }); + it('should have |expires| set to |-1| for session cookies', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456' + }); + const cookies = await page.cookies(); + expect(cookies[0].session).toBe(true); + expect(cookies[0].expires).toBe(-1); + }); + it('should set cookie with reasonable defaults', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456' + }); + const cookies = await page.cookies(); + expect(cookies.sort((a, b) => a.name.localeCompare(b.name))).toEqual([{ + name: 'password', + value: '123456', + domain: 'localhost', + path: '/', + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + it('should set a cookie with a path', async({page, server}) => { + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({ + name: 'gridcookie', + value: 'GRID', + path: '/grid.html' + }); + expect(await page.cookies()).toEqual([{ + name: 'gridcookie', + value: 'GRID', + domain: 'localhost', + path: '/grid.html', + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + await page.goto(server.EMPTY_PAGE); + expect(await page.cookies()).toEqual([]); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto(server.PREFIX + '/grid.html'); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + }); + it('should not set a cookie on a blank page', async function({page}) { + await page.goto('about:blank'); + let error = null; + try { + await page.setCookie({name: 'example-cookie', value: 'best'}); + } catch (e) { + error = e; + } + expect(error.message).toContain('At least one of the url and domain needs to be specified'); + }); + it('should not set a cookie with blank page URL', async function({page, server}) { + let error = null; + await page.goto(server.EMPTY_PAGE); + try { + await page.setCookie( + {name: 'example-cookie', value: 'best'}, + {url: 'about:blank', name: 'example-cookie-blank', value: 'best'} + ); + } catch (e) { + error = e; + } + expect(error.message).toEqual( + `Blank page can not have cookie "example-cookie-blank"` + ); + }); + it('should not set a cookie on a data URL page', async function({page}) { + let error = null; + await page.goto('data:,Hello%2C%20World!'); + try { + await page.setCookie({name: 'example-cookie', value: 'best'}); + } catch (e) { + error = e; + } + expect(error.message).toContain('At least one of the url and domain needs to be specified'); + }); + it('should default to setting secure cookie for HTTPS websites', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const SECURE_URL = 'https://example.com'; + await page.setCookie({ + url: SECURE_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(SECURE_URL); + expect(cookie.secure).toBe(true); + }); + it('should be able to set unsecure cookie for HTTP website', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const HTTP_URL = 'http://example.com'; + await page.setCookie({ + url: HTTP_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(HTTP_URL); + expect(cookie.secure).toBe(false); + }); + it('should set a cookie on a different domain', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + url: 'https://www.example.com', + name: 'example-cookie', + value: 'best', + }); + expect(await page.evaluate('document.cookie')).toBe(''); + expect(await page.cookies()).toEqual([]); + expect(await page.cookies('https://www.example.com')).toEqual([{ + name: 'example-cookie', + value: 'best', + domain: 'www.example.com', + path: '/', + expires: -1, + size: 18, + httpOnly: false, + secure: true, + session: true, + sameSite: 'None', + }]); + }); + it('should set cookies from a frame', async({page, server}) => { + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({name: 'localhost-cookie', value: 'best'}); + await page.evaluate(src => { + let fulfill; + const promise = new Promise(x => fulfill = x); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }, server.CROSS_PROCESS_PREFIX); + await page.setCookie({name: '127-cookie', value: 'worst', url: server.CROSS_PROCESS_PREFIX}); + expect(await page.evaluate('document.cookie')).toBe('localhost-cookie=best'); + expect(await page.frames()[1].evaluate('document.cookie')).toBe('127-cookie=worst'); + + expect(await page.cookies()).toEqual([{ + name: 'localhost-cookie', + value: 'best', + domain: 'localhost', + path: '/', + expires: -1, + size: 20, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + + expect(await page.cookies(server.CROSS_PROCESS_PREFIX)).toEqual([{ + name: '127-cookie', + value: 'worst', + domain: '127.0.0.1', + path: '/', + expires: -1, + size: 15, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + }); + + describe('Page.deleteCookie', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'cookie1', + value: '1' + }, { + name: 'cookie2', + value: '2' + }, { + name: 'cookie3', + value: '3' + }); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2; cookie3=3'); + await page.deleteCookie({name: 'cookie2'}); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie3=3'); + }); + }); +}; diff --git a/test/coverage.spec.js b/test/coverage.spec.js new file mode 100644 index 0000000000..aee11c12a7 --- /dev/null +++ b/test/coverage.spec.js @@ -0,0 +1,216 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('JSCoverage', function() { + it('should work', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', {waitUntil: 'networkidle0'}); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/jscoverage/simple.html'); + expect(coverage[0].ranges).toEqual([ + { start: 0, end: 17 }, + { start: 35, end: 61 }, + ]); + }); + it('should report sourceURLs', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/sourceurl.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('nicename.js'); + }); + it('should ignore eval() scripts by default', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + }); + it('shouldn\'t ignore eval() scripts if reportAnonymousScripts is true', async function({page, server}) { + await page.coverage.startJSCoverage({reportAnonymousScripts: true}); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.find(entry => entry.url.startsWith('debugger://'))).not.toBe(null); + expect(coverage.length).toBe(2); + }); + it('should ignore pptr internal scripts if reportAnonymousScripts is true', async function({page, server}) { + await page.coverage.startJSCoverage({reportAnonymousScripts: true}); + await page.goto(server.EMPTY_PAGE); + await page.evaluate('console.log("foo")'); + await page.evaluate(() => console.log('bar')); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); + it('should report multiple scripts', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + coverage.sort((a, b) => a.url.localeCompare(b.url)); + expect(coverage[0].url).toContain('/jscoverage/script1.js'); + expect(coverage[1].url).toContain('/jscoverage/script2.js'); + }); + it('should report right ranges', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/ranges.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + const entry = coverage[0]; + expect(entry.ranges.length).toBe(1); + const range = entry.ranges[0]; + expect(entry.text.substring(range.start, range.end)).toBe(`console.log('used!');`); + }); + it('should report scripts that have no coverage', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/unused.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + const entry = coverage[0]; + expect(entry.url).toContain('unused.html'); + expect(entry.ranges.length).toBe(0); + }); + it('should work with conditionals', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/involved.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':/')).toBeGolden('jscoverage-involved.txt'); + }); + describe('resetOnNavigation', function() { + it('should report scripts across navigations when disabled', async function({page, server}) { + await page.coverage.startJSCoverage({resetOnNavigation: false}); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + }); + it('should NOT report scripts across navigations when enabled', async function({page, server}) { + await page.coverage.startJSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); + }); + // @see https://crbug.com/990945 + xit('should not hang when there is a debugger statement', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + }); + + describe('CSSCoverage', function() { + it('should work', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/simple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/csscoverage/simple.html'); + expect(coverage[0].ranges).toEqual([ + {start: 1, end: 22} + ]); + const range = coverage[0].ranges[0]; + expect(coverage[0].text.substring(range.start, range.end)).toBe('div { color: green; }'); + }); + it('should report sourceURLs', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/sourceurl.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('nicename.css'); + }); + it('should report multiple stylesheets', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(2); + coverage.sort((a, b) => a.url.localeCompare(b.url)); + expect(coverage[0].url).toContain('/csscoverage/stylesheet1.css'); + expect(coverage[1].url).toContain('/csscoverage/stylesheet2.css'); + }); + it('should report stylesheets that have no coverage', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/unused.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('unused.css'); + expect(coverage[0].ranges.length).toBe(0); + }); + it('should work with media queries', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/media.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/csscoverage/media.html'); + expect(coverage[0].ranges).toEqual([ + {start: 17, end: 38} + ]); + }); + it('should work with complicated usecases', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/involved.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':/')).toBeGolden('csscoverage-involved.txt'); + }); + it('should ignore injected stylesheets', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.addStyleTag({content: 'body { margin: 10px;}'}); + // trigger style recalc + const margin = await page.evaluate(() => window.getComputedStyle(document.body).margin); + expect(margin).toBe('10px'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(0); + }); + describe('resetOnNavigation', function() { + it('should report stylesheets across navigations', async function({page, server}) { + await page.coverage.startCSSCoverage({resetOnNavigation: false}); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(2); + }); + it('should NOT report scripts across navigations', async function({page, server}) { + await page.coverage.startCSSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(0); + }); + }); + it('should work with a recently loaded stylesheet', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.evaluate(async url => { + document.body.textContent = 'hello, world'; + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + document.head.appendChild(link); + await new Promise(x => link.onload = x); + }, server.PREFIX + '/csscoverage/stylesheet1.css'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + }); + }); +}; diff --git a/test/defaultbrowsercontext.spec.js b/test/defaultbrowsercontext.spec.js new file mode 100644 index 0000000000..c55792263d --- /dev/null +++ b/test/defaultbrowsercontext.spec.js @@ -0,0 +1,96 @@ +/** + * 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.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(FFOX)('DefaultBrowserContext', function() { + beforeEach(async state => { + state.browser = await playwright.launch(defaultBrowserOptions); + state.page = await state.browser.newPage(); + }); + afterEach(async state => { + await state.browser.close(); + delete state.browser; + delete state.page; + }); + it('page.cookies() should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + expect(await page.cookies()).toEqual([{ + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + it.skip(WEBKIT)('page.setCookie() should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'username', + value: 'John Doe' + }); + expect(await page.evaluate(() => document.cookie)).toBe('username=John Doe'); + expect(await page.cookies()).toEqual([{ + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + it.skip(WEBKIT)('page.deleteCookie() should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'cookie1', + value: '1' + }, { + name: 'cookie2', + value: '2' + }); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2'); + await page.deleteCookie({name: 'cookie2'}); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); + expect(await page.cookies()).toEqual([{ + name: 'cookie1', + value: '1', + domain: 'localhost', + path: '/', + expires: -1, + size: 8, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + }); +}; diff --git a/test/dialog.spec.js b/test/dialog.spec.js new file mode 100644 index 0000000000..0d9e58b752 --- /dev/null +++ b/test/dialog.spec.js @@ -0,0 +1,50 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(WEBKIT)('Page.Events.Dialog', function() { + it('should fire', async({page, server}) => { + page.on('dialog', dialog => { + expect(dialog.type()).toBe('alert'); + expect(dialog.defaultValue()).toBe(''); + expect(dialog.message()).toBe('yo'); + dialog.accept(); + }); + await page.evaluate(() => alert('yo')); + }); + it('should allow accepting prompts', async({page, server}) => { + page.on('dialog', dialog => { + expect(dialog.type()).toBe('prompt'); + expect(dialog.defaultValue()).toBe('yes.'); + expect(dialog.message()).toBe('question?'); + dialog.accept('answer!'); + }); + const result = await page.evaluate(() => prompt('question?', 'yes.')); + expect(result).toBe('answer!'); + }); + it('should dismiss the prompt', async({page, server}) => { + page.on('dialog', dialog => { + dialog.dismiss(); + }); + const result = await page.evaluate(() => prompt('question?')); + expect(result).toBe(null); + }); + }); +}; diff --git a/test/diffstyle.css b/test/diffstyle.css new file mode 100644 index 0000000000..c58f0e90a6 --- /dev/null +++ b/test/diffstyle.css @@ -0,0 +1,13 @@ +body { + font-family: monospace; + white-space: pre; +} + +ins { + background-color: #9cffa0; + text-decoration: none; +} + +del { + background-color: #ff9e9e; +} diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js new file mode 100644 index 0000000000..feff82e641 --- /dev/null +++ b/test/elementhandle.spec.js @@ -0,0 +1,215 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(WEBKIT)('ElementHandle.boundingBox', function() { + it('should work', async({page, server}) => { + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const elementHandle = await page.$('.box:nth-of-type(13)'); + const box = await elementHandle.boundingBox(); + expect(box).toEqual({ x: 100, y: 50, width: 50, height: 50 }); + }); + it('should handle nested frames', async({page, server}) => { + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const nestedFrame = page.frames()[1].childFrames()[1]; + const elementHandle = await nestedFrame.$('div'); + const box = await elementHandle.boundingBox(); + if (CHROME) + expect(box).toEqual({ x: 28, y: 260, width: 264, height: 18 }); + else + expect(box).toEqual({ x: 28, y: 182, width: 247, height: 18 }); + }); + it('should return null for invisible elements', async({page, server}) => { + await page.setContent('
hi
'); + const element = await page.$('div'); + expect(await element.boundingBox()).toBe(null); + }); + it('should force a layout', async({page, server}) => { + await page.setViewport({ width: 500, height: 500 }); + await page.setContent('
hello
'); + const elementHandle = await page.$('div'); + await page.evaluate(element => element.style.height = '200px', elementHandle); + const box = await elementHandle.boundingBox(); + expect(box).toEqual({ x: 8, y: 8, width: 100, height: 200 }); + }); + it('should work with SVG nodes', async({page, server}) => { + await page.setContent(` + + + + `); + const element = await page.$('#therect'); + const pptrBoundingBox = await element.boundingBox(); + const webBoundingBox = await page.evaluate(e => { + const rect = e.getBoundingClientRect(); + return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}; + }, element); + expect(pptrBoundingBox).toEqual(webBoundingBox); + }); + }); + + describe.skip(FFOX || WEBKIT)('ElementHandle.boxModel', function() { + it('should work', async({page, server}) => { + await page.goto(server.PREFIX + '/resetcss.html'); + + // Step 1: Add Frame and position it absolutely. + await utils.attachFrame(page, 'frame1', server.PREFIX + '/resetcss.html'); + await page.evaluate(() => { + const frame = document.querySelector('#frame1'); + frame.style = ` + position: absolute; + left: 1px; + top: 2px; + `; + }); + + // Step 2: Add div and position it absolutely inside frame. + const frame = page.frames()[1]; + const divHandle = (await frame.evaluateHandle(() => { + const div = document.createElement('div'); + document.body.appendChild(div); + div.style = ` + box-sizing: border-box; + position: absolute; + border-left: 1px solid black; + padding-left: 2px; + margin-left: 3px; + left: 4px; + top: 5px; + width: 6px; + height: 7px; + `; + return div; + })).asElement(); + + // Step 3: query div's boxModel and assert box values. + const box = await divHandle.boxModel(); + expect(box.width).toBe(6); + expect(box.height).toBe(7); + expect(box.margin[0]).toEqual({ + x: 1 + 4, // frame.left + div.left + y: 2 + 5, + }); + expect(box.border[0]).toEqual({ + x: 1 + 4 + 3, // frame.left + div.left + div.margin-left + y: 2 + 5, + }); + expect(box.padding[0]).toEqual({ + x: 1 + 4 + 3 + 1, // frame.left + div.left + div.marginLeft + div.borderLeft + y: 2 + 5, + }); + expect(box.content[0]).toEqual({ + x: 1 + 4 + 3 + 1 + 2, // frame.left + div.left + div.marginLeft + div.borderLeft + dif.paddingLeft + y: 2 + 5, + }); + }); + + it('should return null for invisible elements', async({page, server}) => { + await page.setContent('
hi
'); + const element = await page.$('div'); + expect(await element.boxModel()).toBe(null); + }); + }); + + describe.skip(WEBKIT)('ElementHandle.contentFrame', function() { + it('should work', async({page,server}) => { + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const elementHandle = await page.$('#frame1'); + const frame = await elementHandle.contentFrame(); + expect(frame).toBe(page.frames()[1]); + }); + }); + + describe('ElementHandle.click', function() { + it('should work', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await button.click(); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + it('should work for Shadow DOM v1', async({page, server}) => { + await page.goto(server.PREFIX + '/shadow.html'); + const buttonHandle = await page.evaluateHandle(() => button); + await buttonHandle.click(); + expect(await page.evaluate(() => clicked)).toBe(true); + }); + it('should work for TextNodes', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const buttonTextNode = await page.evaluateHandle(() => document.querySelector('button').firstChild); + let error = null; + await buttonTextNode.click().catch(err => error = err); + expect(error.message).toBe('Node is not of type HTMLElement'); + }); + it('should throw for detached nodes', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(button => button.remove(), button); + let error = null; + await button.click().catch(err => error = err); + expect(error.message).toBe('Node is detached from document'); + }); + it('should throw for hidden nodes', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(button => button.style.display = 'none', button); + const error = await button.click().catch(err => err); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); + }); + it('should throw for recursively hidden nodes', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(button => button.parentElement.style.display = 'none', button); + const error = await button.click().catch(err => err); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); + }); + it('should throw for
elements', async({page, server}) => { + await page.setContent('hello
goodbye'); + const br = await page.$('br'); + const error = await br.click().catch(err => err); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); + }); + }); + + describe('ElementHandle.hover', function() { + it('should work', async({page, server}) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + const button = await page.$('#button-6'); + await button.hover(); + expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6'); + }); + }); + + describe('ElementHandle.isIntersectingViewport', function() { + it('should work', async({page, server}) => { + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + for (let i = 0; i < 11; ++i) { + const button = await page.$('#btn' + i); + // All but last button are visible. + const visible = i < 10; + expect(await button.isIntersectingViewport()).toBe(visible); + } + }); + }); +}; diff --git a/test/emulation.spec.js b/test/emulation.spec.js new file mode 100644 index 0000000000..7a64cfe565 --- /dev/null +++ b/test/emulation.spec.js @@ -0,0 +1,187 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + const iPhone = playwright.devices['iPhone 6']; + const iPhoneLandscape = playwright.devices['iPhone 6 landscape']; + + describe('Page.viewport', function() { + it('should get the proper viewport size', async({page, server}) => { + expect(page.viewport()).toEqual({width: 800, height: 600}); + await page.setViewport({width: 123, height: 456}); + expect(page.viewport()).toEqual({width: 123, height: 456}); + }); + it.skip(WEBKIT)('should support mobile emulation', async({page, server}) => { + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => window.innerWidth)).toBe(800); + await page.setViewport(iPhone.viewport); + expect(await page.evaluate(() => window.innerWidth)).toBe(375); + await page.setViewport({width: 400, height: 300}); + expect(await page.evaluate(() => window.innerWidth)).toBe(400); + }); + it.skip(WEBKIT)('should support touch emulation', async({page, server}) => { + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); + await page.setViewport(iPhone.viewport); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true); + expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); + await page.setViewport({width: 100, height: 100}); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); + + function dispatchTouch() { + let fulfill; + const promise = new Promise(x => fulfill = x); + window.ontouchstart = function(e) { + fulfill('Received touch'); + }; + window.dispatchEvent(new Event('touchstart')); + + fulfill('Did not receive touch'); + + return promise; + } + }); + it.skip(WEBKIT)('should be detectable by Modernizr', async({page, server}) => { + await page.goto(server.PREFIX + '/detect-touch.html'); + expect(await page.evaluate(() => document.body.textContent.trim())).toBe('NO'); + await page.setViewport(iPhone.viewport); + await page.goto(server.PREFIX + '/detect-touch.html'); + expect(await page.evaluate(() => document.body.textContent.trim())).toBe('YES'); + }); + it.skip(WEBKIT)('should detect touch when applying viewport with touches', async({page, server}) => { + await page.setViewport({ width: 800, height: 600, hasTouch: true }); + await page.addScriptTag({url: server.PREFIX + '/modernizr.js'}); + expect(await page.evaluate(() => Modernizr.touchevents)).toBe(true); + }); + it.skip(FFOX || WEBKIT)('should support landscape emulation', async({page, server}) => { + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => screen.orientation.type)).toBe('portrait-primary'); + await page.setViewport(iPhoneLandscape.viewport); + expect(await page.evaluate(() => screen.orientation.type)).toBe('landscape-primary'); + await page.setViewport({width: 100, height: 100}); + expect(await page.evaluate(() => screen.orientation.type)).toBe('portrait-primary'); + }); + }); + + describe('Page.emulate', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.goto(server.PREFIX + '/mobile.html'); + await page.emulate(iPhone); + expect(await page.evaluate(() => window.innerWidth)).toBe(375); + expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); + }); + it('should support clicking', async({page, server}) => { + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(button => button.style.marginTop = '200px', button); + await button.click(); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + }); + + describe('Page.emulateMedia', function() { + it.skip(WEBKIT)('should be an alias for Page.emulateMediaType', async({page, server}) => { + expect(page.emulateMedia).toEqual(page.emulateMediaType); + }); + }); + + describe.skip(WEBKIT)('Page.emulateMediaType', function() { + it('should work', async({page, server}) => { + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe(false); + await page.emulateMediaType('print'); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true); + await page.emulateMediaType(null); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe(false); + }); + it('should throw in case of bad argument', async({page, server}) => { + let error = null; + await page.emulateMediaType('bad').catch(e => error = e); + expect(error.message).toBe('Unsupported media type: bad'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.emulateMediaFeatures', function() { + it('should work', async({page, server}) => { + await page.emulateMediaFeatures([ + { name: 'prefers-reduced-motion', value: 'reduce' }, + ]); + expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(false); + await page.emulateMediaFeatures([ + { name: 'prefers-color-scheme', value: 'light' }, + ]); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false); + await page.emulateMediaFeatures([ + { name: 'prefers-color-scheme', value: 'dark' }, + ]); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false); + await page.emulateMediaFeatures([ + { name: 'prefers-reduced-motion', value: 'reduce' }, + { name: 'prefers-color-scheme', value: 'light' }, + ]); + expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false); + }); + it('should throw in case of bad argument', async({page, server}) => { + let error = null; + await page.emulateMediaFeatures([{ name: 'bad', value: '' }]).catch(e => error = e); + expect(error.message).toBe('Unsupported media feature: bad'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.emulateTimezone', function() { + it('should work', async({page, server}) => { + page.evaluate(() => { + globalThis.date = new Date(1479579154987); + }); + await page.emulateTimezone('America/Jamaica'); + expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); + + await page.emulateTimezone('Pacific/Honolulu'); + expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'); + + await page.emulateTimezone('America/Buenos_Aires'); + expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); + + await page.emulateTimezone('Europe/Berlin'); + expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'); + }); + + it('should throw for invalid timezone IDs', async({page, server}) => { + let error = null; + await page.emulateTimezone('Foo/Bar').catch(e => error = e); + expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); + await page.emulateTimezone('Baz/Qux').catch(e => error = e); + expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); + }); + }); + +}; diff --git a/test/evaluation.spec.js b/test/evaluation.spec.js new file mode 100644 index 0000000000..e7e2e38eec --- /dev/null +++ b/test/evaluation.spec.js @@ -0,0 +1,305 @@ +/** + * 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 utils = require('./utils'); + +const bigint = typeof BigInt !== 'undefined'; + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.evaluate', function() { + it('should work', async({page, server}) => { + const result = await page.evaluate(() => 7 * 3); + expect(result).toBe(21); + }); + (bigint ? it.skip(FFOX || WEBKIT) : xit)('should transfer BigInt', async({page, server}) => { + const result = await page.evaluate(a => a, BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should transfer NaN', async({page, server}) => { + const result = await page.evaluate(a => a, NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should transfer -0', async({page, server}) => { + const result = await page.evaluate(a => a, -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should transfer Infinity', async({page, server}) => { + const result = await page.evaluate(a => a, Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should transfer -Infinity', async({page, server}) => { + const result = await page.evaluate(a => a, -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should transfer arrays', async({page, server}) => { + const result = await page.evaluate(a => a, [1, 2, 3]); + expect(result).toEqual([1,2,3]); + }); + it('should transfer arrays as arrays, not objects', async({page, server}) => { + const result = await page.evaluate(a => Array.isArray(a), [1, 2, 3]); + expect(result).toBe(true); + }); + it('should modify global environment', async({page}) => { + await page.evaluate(() => window.globalVar = 123); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it('should evaluate in the page context', async({page, server}) => { + await page.goto(server.PREFIX + '/global-var.html'); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it.skip(FFOX || WEBKIT)('should return undefined for objects with symbols', async({page, server}) => { + expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined); + }); + it('should work with function shorthands', async({page, server}) => { + const a = { + sum(a, b) { return a + b; }, + + async mult(a, b) { return a * b; } + }; + expect(await page.evaluate(a.sum, 1, 2)).toBe(3); + expect(await page.evaluate(a.mult, 2, 4)).toBe(8); + }); + it('should work with unicode chars', async({page, server}) => { + const result = await page.evaluate(a => a['中文字符'], {'中文字符': 42}); + expect(result).toBe(42); + }); + it('should throw when evaluation triggers reload', async({page, server}) => { + let error = null; + await page.evaluate(() => { + location.reload(); + return new Promise(() => {}); + }).catch(e => error = e); + expect(error.message).toContain('Protocol error'); + }); + it('should await promise', async({page, server}) => { + const result = await page.evaluate(() => Promise.resolve(8 * 7)); + expect(result).toBe(56); + }); + it('should work right after framenavigated', async({page, server}) => { + let frameEvaluation = null; + page.on('framenavigated', async frame => { + frameEvaluation = frame.evaluate(() => 6 * 7); + }); + await page.goto(server.EMPTY_PAGE); + expect(await frameEvaluation).toBe(42); + }); + it.skip(WEBKIT)('should work from-inside an exposed function', async({page, server}) => { + // Setup inpage callback, which calls Page.evaluate + await page.exposeFunction('callController', async function(a, b) { + return await page.evaluate((a, b) => a * b, a, b); + }); + const result = await page.evaluate(async function() { + return await callController(9, 3); + }); + expect(result).toBe(27); + }); + it('should reject promise with exception', async({page, server}) => { + let error = null; + await page.evaluate(() => not_existing_object.property).catch(e => error = e); + expect(error).toBeTruthy(); + expect(error.message).toContain('not_existing_object'); + }); + it('should support thrown strings as error messages', async({page, server}) => { + let error = null; + await page.evaluate(() => { throw 'qwerty'; }).catch(e => error = e); + expect(error).toBeTruthy(); + expect(error.message).toContain('qwerty'); + }); + it('should support thrown numbers as error messages', async({page, server}) => { + let error = null; + await page.evaluate(() => { throw 100500; }).catch(e => error = e); + expect(error).toBeTruthy(); + expect(error.message).toContain('100500'); + }); + it('should return complex objects', async({page, server}) => { + const object = {foo: 'bar!'}; + const result = await page.evaluate(a => a, object); + expect(result).not.toBe(object); + expect(result).toEqual(object); + }); + (bigint ? it.skip(FFOX || WEBKIT) : xit)('should return BigInt', async({page, server}) => { + const result = await page.evaluate(() => BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should return NaN', async({page, server}) => { + const result = await page.evaluate(() => NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should return -0', async({page, server}) => { + const result = await page.evaluate(() => -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should return Infinity', async({page, server}) => { + const result = await page.evaluate(() => Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should return -Infinity', async({page, server}) => { + const result = await page.evaluate(() => -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should accept "undefined" as one of multiple parameters', async({page, server}) => { + const result = await page.evaluate((a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), undefined, 'foo'); + expect(result).toBe(true); + }); + it('should properly serialize null fields', async({page}) => { + expect(await page.evaluate(() => ({a: undefined}))).toEqual({}); + }); + it('should return undefined for non-serializable objects', async({page, server}) => { + expect(await page.evaluate(() => window)).toBe(undefined); + }); + it('should fail for circular object', async({page, server}) => { + const result = await page.evaluate(() => { + const a = {}; + const b = {a}; + a.b = b; + return a; + }); + expect(result).toBe(undefined); + }); + it.skip(FFOX)('should be able to throw a tricky error', async({page, server}) => { + const windowHandle = await page.evaluateHandle(() => window); + const errorText = await windowHandle.jsonValue().catch(e => e.message); + const error = await page.evaluate(errorText => { + throw new Error(errorText); + }, errorText).catch(e => e); + expect(error.message).toContain(errorText); + }); + it('should accept a string', async({page, server}) => { + const result = await page.evaluate('1 + 2'); + expect(result).toBe(3); + }); + it('should accept a string with semi colons', async({page, server}) => { + const result = await page.evaluate('1 + 5;'); + expect(result).toBe(6); + }); + it('should accept a string with comments', async({page, server}) => { + const result = await page.evaluate('2 + 5;\n// do some math!'); + expect(result).toBe(7); + }); + it('should accept element handle as an argument', async({page, server}) => { + await page.setContent('
42
'); + const element = await page.$('section'); + const text = await page.evaluate(e => e.textContent, element); + expect(text).toBe('42'); + }); + it('should throw if underlying element was disposed', async({page, server}) => { + await page.setContent('
39
'); + const element = await page.$('section'); + expect(element).toBeTruthy(); + await element.dispose(); + let error = null; + await page.evaluate(e => e.textContent, element).catch(e => error = e); + expect(error.message).toContain('JSHandle is disposed'); + }); + it('should throw if elementHandles are from other frames', async({page, server}) => { + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const bodyHandle = await page.frames()[1].$('body'); + let error = null; + await page.evaluate(body => body.innerHTML, bodyHandle).catch(e => error = e); + expect(error).toBeTruthy(); + expect(error.message).toContain('JSHandles can be evaluated only in the context they were created'); + }); + it('should simulate a user gesture', async({page, server}) => { + const result = await page.evaluate(() => { + document.body.appendChild(document.createTextNode('test')); + document.execCommand('selectAll'); + return document.execCommand('copy'); + }); + expect(result).toBe(true); + }); + it('should throw a nice error after a navigation', async({page, server}) => { + const executionContext = await page.mainFrame().executionContext(); + + await Promise.all([ + page.waitForNavigation(), + executionContext.evaluate(() => window.location.reload()) + ]); + const error = await executionContext.evaluate(() => null).catch(e => e); + expect(error.message).toContain('navigation'); + }); + it.skip(FFOX)('should not throw an error when evaluation does a navigation', async({page, server}) => { + await page.goto(server.PREFIX + '/one-style.html'); + const result = await page.evaluate(() => { + window.location = '/empty.html'; + return [42]; + }); + expect(result).toEqual([42]); + }); + // Works in WebKit, but slow + it.skip(FFOX || WEBKIT)('should transfer 100Mb of data from page to node.js', async({page, server}) => { + const a = await page.evaluate(() => Array(100 * 1024 * 1024 + 1).join('a')); + expect(a.length).toBe(100 * 1024 * 1024); + }); + it('should throw error with detailed information on exception inside promise ', async({page, server}) => { + let error = null; + await page.evaluate(() => new Promise(() => { + throw new Error('Error in promise'); + })).catch(e => error = e); + expect(error.message).toContain('Error in promise'); + }); + }); + + describe.skip(WEBKIT)('Page.evaluateOnNewDocument', function() { + it('should evaluate before anything else on the page', async({page, server}) => { + await page.evaluateOnNewDocument(function(){ + window.injected = 123; + }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect(await page.evaluate(() => window.result)).toBe(123); + }); + it('should work with CSP', async({page, server}) => { + server.setCSP('/empty.html', 'script-src ' + server.PREFIX); + await page.evaluateOnNewDocument(function(){ + window.injected = 123; + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(await page.evaluate(() => window.injected)).toBe(123); + + // Make sure CSP works. + await page.addScriptTag({content: 'window.e = 10;'}).catch(e => void e); + expect(await page.evaluate(() => window.e)).toBe(undefined); + }); + }); + + describe('Frame.evaluate', function() { + it('should have different execution contexts', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + await page.frames()[0].evaluate(() => window.FOO = 'foo'); + await page.frames()[1].evaluate(() => window.FOO = 'bar'); + expect(await page.frames()[0].evaluate(() => window.FOO)).toBe('foo'); + expect(await page.frames()[1].evaluate(() => window.FOO)).toBe('bar'); + }); + it('should have correct execution contexts', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames().length).toBe(2); + expect(await page.frames()[0].evaluate(() => document.body.textContent.trim())).toBe(''); + expect(await page.frames()[1].evaluate(() => document.body.textContent.trim())).toBe(`Hi, I'm frame`); + }); + it('should execute after cross-site navigation', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + expect(await mainFrame.evaluate(() => window.location.href)).toContain('localhost'); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(await mainFrame.evaluate(() => window.location.href)).toContain('127'); + }); + }); +}; diff --git a/test/fixtures.spec.js b/test/fixtures.spec.js new file mode 100644 index 0000000000..17230a3f20 --- /dev/null +++ b/test/fixtures.spec.js @@ -0,0 +1,79 @@ +/** + * 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 path = require('path'); + +module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, playwrightPath, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(WEBKIT)('Fixtures', function() { + it.skip(FFOX)('dumpio option should work with pipe option ', async({server}) => { + let dumpioData = ''; + const {spawn} = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, {pipe: true, dumpio: true}); + const res = spawn('node', + [path.join(__dirname, 'fixtures', 'dumpio.js'), playwrightPath, JSON.stringify(options)]); + res.stderr.on('data', data => dumpioData += data.toString('utf8')); + await new Promise(resolve => res.on('close', resolve)); + expect(dumpioData).toContain('message from dumpio'); + }); + it('should dump browser process stderr', async({server}) => { + let dumpioData = ''; + const {spawn} = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, {dumpio: true}); + const res = spawn('node', + [path.join(__dirname, 'fixtures', 'dumpio.js'), playwrightPath, JSON.stringify(options)]); + if (CHROME || WEBKIT) + res.stderr.on('data', data => dumpioData += data.toString('utf8')); + else + res.stdout.on('data', data => dumpioData += data.toString('utf8')); + await new Promise(resolve => res.on('close', resolve)); + + if (CHROME || WEBKIT) + expect(dumpioData).toContain('DevTools listening on ws://'); + else + expect(dumpioData).toContain('Juggler listening on ws://'); + }); + it('should close the browser when the node process closes', async({ server }) => { + const {spawn, execSync} = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, { + // Disable DUMPIO to cleanly read stdout. + dumpio: false, + }); + const res = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), playwrightPath, JSON.stringify(options)]); + let wsEndPointCallback; + const wsEndPointPromise = new Promise(x => wsEndPointCallback = x); + let output = ''; + res.stdout.on('data', data => { + output += data; + if (output.indexOf('\n')) + wsEndPointCallback(output.substring(0, output.indexOf('\n'))); + }); + const browser = await playwright.connect({ browserWSEndpoint: await wsEndPointPromise }); + const promises = [ + new Promise(resolve => browser.once('disconnected', resolve)), + new Promise(resolve => res.on('close', resolve)) + ]; + if (process.platform === 'win32') + execSync(`taskkill /pid ${res.pid} /T /F`); + else + process.kill(res.pid); + await Promise.all(promises); + }); + }); +}; diff --git a/test/fixtures/closeme.js b/test/fixtures/closeme.js new file mode 100644 index 0000000000..d80de2dce3 --- /dev/null +++ b/test/fixtures/closeme.js @@ -0,0 +1,5 @@ +(async() => { + const [, , playwrightRoot, options] = process.argv; + const browser = await require(playwrightRoot).launch(JSON.parse(options)); + console.log(browser.wsEndpoint()); +})(); diff --git a/test/fixtures/dumpio.js b/test/fixtures/dumpio.js new file mode 100644 index 0000000000..b20fd6b8ff --- /dev/null +++ b/test/fixtures/dumpio.js @@ -0,0 +1,8 @@ +(async() => { + const [, , playwrightRoot, options] = process.argv; + const browser = await require(playwrightRoot).launch(JSON.parse(options)); + const page = await browser.newPage(); + await page.evaluate(() => console.error('message from dumpio')); + await page.close(); + await browser.close(); +})(); diff --git a/test/frame.spec.js b/test/frame.spec.js new file mode 100644 index 0000000000..e2d9be5f95 --- /dev/null +++ b/test/frame.spec.js @@ -0,0 +1,211 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Frame.executionContext', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + const [frame1, frame2] = page.frames(); + const context1 = await frame1.executionContext(); + const context2 = await frame2.executionContext(); + expect(context1).toBeTruthy(); + expect(context2).toBeTruthy(); + expect(context1 !== context2).toBeTruthy(); + expect(context1.frame()).toBe(frame1); + expect(context2.frame()).toBe(frame2); + + await Promise.all([ + context1.evaluate(() => window.a = 1), + context2.evaluate(() => window.a = 2) + ]); + const [a1, a2] = await Promise.all([ + context1.evaluate(() => window.a), + context2.evaluate(() => window.a) + ]); + expect(a1).toBe(1); + expect(a2).toBe(2); + }); + }); + + describe('Frame.evaluateHandle', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + const windowHandle = await mainFrame.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + }); + + describe('Frame.evaluate', function() { + it.skip(FFOX)('should throw for detached frames', async({page, server}) => { + const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.detachFrame(page, 'frame1'); + let error = null; + await frame1.evaluate(() => 7 * 8).catch(e => error = e); + expect(error.message).toContain('Execution Context is not available in detached frame'); + }); + }); + + describe('Frame Management', function() { + it.skip(WEBKIT)('should handle nested frames', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(utils.dumpFrames(page.mainFrame())).toEqual([ + 'http://localhost:/frames/nested-frames.html', + ' http://localhost:/frames/two-frames.html (2frames)', + ' http://localhost:/frames/frame.html (uno)', + ' http://localhost:/frames/frame.html (dos)', + ' http://localhost:/frames/frame.html (aframe)' + ]); + }); + it('should send events when frames are manipulated dynamically', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + // validate frameattached events + const attachedFrames = []; + page.on('frameattached', frame => attachedFrames.push(frame)); + await utils.attachFrame(page, 'frame1', './assets/frame.html'); + expect(attachedFrames.length).toBe(1); + expect(attachedFrames[0].url()).toContain('/assets/frame.html'); + + // validate framenavigated events + const navigatedFrames = []; + page.on('framenavigated', frame => navigatedFrames.push(frame)); + await utils.navigateFrame(page, 'frame1', './empty.html'); + expect(navigatedFrames.length).toBe(1); + expect(navigatedFrames[0].url()).toBe(server.EMPTY_PAGE); + + // validate framedetached events + const detachedFrames = []; + page.on('framedetached', frame => detachedFrames.push(frame)); + await utils.detachFrame(page, 'frame1'); + expect(detachedFrames.length).toBe(1); + expect(detachedFrames[0].isDetached()).toBe(true); + }); + it.skip(WEBKIT)('should send "framenavigated" when navigating on anchor URLs', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await Promise.all([ + page.goto(server.EMPTY_PAGE + '#foo'), + utils.waitEvent(page, 'framenavigated') + ]); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + }); + it('should persist mainFrame on cross-process navigation', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(page.mainFrame() === mainFrame).toBeTruthy(); + }); + it('should not send attach/detach events for main frame', async({page, server}) => { + let hasEvents = false; + page.on('frameattached', frame => hasEvents = true); + page.on('framedetached', frame => hasEvents = true); + await page.goto(server.EMPTY_PAGE); + expect(hasEvents).toBe(false); + }); + it('should detach child frames on navigation', async({page, server}) => { + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', frame => attachedFrames.push(frame)); + page.on('framedetached', frame => detachedFrames.push(frame)); + page.on('framenavigated', frame => navigatedFrames.push(frame)); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(attachedFrames.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + it('should support framesets', async({page, server}) => { + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', frame => attachedFrames.push(frame)); + page.on('framedetached', frame => detachedFrames.push(frame)); + page.on('framenavigated', frame => navigatedFrames.push(frame)); + await page.goto(server.PREFIX + '/frames/frameset.html'); + expect(attachedFrames.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + it('should report frame from-inside shadow DOM', async({page, server}) => { + await page.goto(server.PREFIX + '/shadow.html'); + await page.evaluate(async url => { + const frame = document.createElement('iframe'); + frame.src = url; + document.body.shadowRoot.appendChild(frame); + await new Promise(x => frame.onload = x); + }, server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + expect(page.frames()[1].url()).toBe(server.EMPTY_PAGE); + }); + it('should report frame.name()', async({page, server}) => { + await utils.attachFrame(page, 'theFrameId', server.EMPTY_PAGE); + await page.evaluate(url => { + const frame = document.createElement('iframe'); + frame.name = 'theFrameName'; + frame.src = url; + document.body.appendChild(frame); + return new Promise(x => frame.onload = x); + }, server.EMPTY_PAGE); + expect(page.frames()[0].name()).toBe(''); + expect(page.frames()[1].name()).toBe('theFrameId'); + expect(page.frames()[2].name()).toBe('theFrameName'); + }); + it('should report frame.parent()', async({page, server}) => { + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + expect(page.frames()[0].parentFrame()).toBe(null); + expect(page.frames()[1].parentFrame()).toBe(page.mainFrame()); + expect(page.frames()[2].parentFrame()).toBe(page.mainFrame()); + }); + it('should report different frame instance when frame re-attaches', async({page, server}) => { + const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await page.evaluate(() => { + window.frame = document.querySelector('#frame1'); + window.frame.remove(); + }); + expect(frame1.isDetached()).toBe(true); + const [frame2] = await Promise.all([ + utils.waitEvent(page, 'frameattached'), + page.evaluate(() => document.body.appendChild(window.frame)), + ]); + expect(frame2.isDetached()).toBe(false); + expect(frame1).not.toBe(frame2); + }); + }); +}; diff --git a/test/golden-chromium/csscoverage-involved.txt b/test/golden-chromium/csscoverage-involved.txt new file mode 100644 index 0000000000..9b851d0bd3 --- /dev/null +++ b/test/golden-chromium/csscoverage-involved.txt @@ -0,0 +1,16 @@ +[ + { + "url": "http://localhost:/csscoverage/involved.html", + "ranges": [ + { + "start": 149, + "end": 297 + }, + { + "start": 327, + "end": 433 + } + ], + "text": "\n@charset \"utf-8\";\n@namespace svg url(http://www.w3.org/2000/svg);\n@font-face {\n font-family: \"Example Font\";\n src: url(\"./Dosis-Regular.ttf\");\n}\n\n#fluffy {\n border: 1px solid black;\n z-index: 1;\n /* -webkit-disabled-property: rgb(1, 2, 3) */\n -lol-cats: \"dogs\" /* non-existing property */\n}\n\n@media (min-width: 1px) {\n span {\n -webkit-border-radius: 10px;\n font-family: \"Example Font\";\n animation: 1s identifier;\n }\n}\n" + } +] \ No newline at end of file diff --git a/test/golden-chromium/grid-cell-0.png b/test/golden-chromium/grid-cell-0.png new file mode 100644 index 0000000000..ff282e989b Binary files /dev/null and b/test/golden-chromium/grid-cell-0.png differ diff --git a/test/golden-chromium/grid-cell-1.png b/test/golden-chromium/grid-cell-1.png new file mode 100644 index 0000000000..91a1cb8510 Binary files /dev/null and b/test/golden-chromium/grid-cell-1.png differ diff --git a/test/golden-chromium/grid-cell-2.png b/test/golden-chromium/grid-cell-2.png new file mode 100644 index 0000000000..7b01753b6a Binary files /dev/null and b/test/golden-chromium/grid-cell-2.png differ diff --git a/test/golden-chromium/grid-cell-3.png b/test/golden-chromium/grid-cell-3.png new file mode 100644 index 0000000000..b9b8b2922b Binary files /dev/null and b/test/golden-chromium/grid-cell-3.png differ diff --git a/test/golden-chromium/jscoverage-involved.txt b/test/golden-chromium/jscoverage-involved.txt new file mode 100644 index 0000000000..6f28e1580e --- /dev/null +++ b/test/golden-chromium/jscoverage-involved.txt @@ -0,0 +1,28 @@ +[ + { + "url": "http://localhost:/jscoverage/involved.html", + "ranges": [ + { + "start": 0, + "end": 35 + }, + { + "start": 50, + "end": 100 + }, + { + "start": 107, + "end": 141 + }, + { + "start": 148, + "end": 160 + }, + { + "start": 168, + "end": 207 + } + ], + "text": "\nfunction foo() {\n if (1 > 2)\n console.log(1);\n if (1 < 2)\n console.log(2);\n let x = 1 > 2 ? 'foo' : 'bar';\n let y = 1 < 2 ? 'foo' : 'bar';\n let z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n" + } +] \ No newline at end of file diff --git a/test/golden-chromium/mock-binary-response.png b/test/golden-chromium/mock-binary-response.png new file mode 100644 index 0000000000..8595e0598e Binary files /dev/null and b/test/golden-chromium/mock-binary-response.png differ diff --git a/test/golden-chromium/screenshot-clip-odd-size.png b/test/golden-chromium/screenshot-clip-odd-size.png new file mode 100644 index 0000000000..b010d1f87f Binary files /dev/null and b/test/golden-chromium/screenshot-clip-odd-size.png differ diff --git a/test/golden-chromium/screenshot-clip-rect.png b/test/golden-chromium/screenshot-clip-rect.png new file mode 100644 index 0000000000..ac23b7de50 Binary files /dev/null and b/test/golden-chromium/screenshot-clip-rect.png differ diff --git a/test/golden-chromium/screenshot-element-bounding-box.png b/test/golden-chromium/screenshot-element-bounding-box.png new file mode 100644 index 0000000000..32e05bf05b Binary files /dev/null and b/test/golden-chromium/screenshot-element-bounding-box.png differ diff --git a/test/golden-chromium/screenshot-element-fractional-offset.png b/test/golden-chromium/screenshot-element-fractional-offset.png new file mode 100644 index 0000000000..cc8669d598 Binary files /dev/null and b/test/golden-chromium/screenshot-element-fractional-offset.png differ diff --git a/test/golden-chromium/screenshot-element-fractional.png b/test/golden-chromium/screenshot-element-fractional.png new file mode 100644 index 0000000000..35c53377f9 Binary files /dev/null and b/test/golden-chromium/screenshot-element-fractional.png differ diff --git a/test/golden-chromium/screenshot-element-larger-than-viewport.png b/test/golden-chromium/screenshot-element-larger-than-viewport.png new file mode 100644 index 0000000000..5fcdb92355 Binary files /dev/null and b/test/golden-chromium/screenshot-element-larger-than-viewport.png differ diff --git a/test/golden-chromium/screenshot-element-padding-border.png b/test/golden-chromium/screenshot-element-padding-border.png new file mode 100644 index 0000000000..917dd48188 Binary files /dev/null and b/test/golden-chromium/screenshot-element-padding-border.png differ diff --git a/test/golden-chromium/screenshot-element-rotate.png b/test/golden-chromium/screenshot-element-rotate.png new file mode 100644 index 0000000000..52e2a0f6d3 Binary files /dev/null and b/test/golden-chromium/screenshot-element-rotate.png differ diff --git a/test/golden-chromium/screenshot-element-scrolled-into-view.png b/test/golden-chromium/screenshot-element-scrolled-into-view.png new file mode 100644 index 0000000000..917dd48188 Binary files /dev/null and b/test/golden-chromium/screenshot-element-scrolled-into-view.png differ diff --git a/test/golden-chromium/screenshot-grid-fullpage.png b/test/golden-chromium/screenshot-grid-fullpage.png new file mode 100644 index 0000000000..d6d38217f7 Binary files /dev/null and b/test/golden-chromium/screenshot-grid-fullpage.png differ diff --git a/test/golden-chromium/screenshot-offscreen-clip.png b/test/golden-chromium/screenshot-offscreen-clip.png new file mode 100644 index 0000000000..31a0935cda Binary files /dev/null and b/test/golden-chromium/screenshot-offscreen-clip.png differ diff --git a/test/golden-chromium/screenshot-sanity.png b/test/golden-chromium/screenshot-sanity.png new file mode 100644 index 0000000000..ecab61fe17 Binary files /dev/null and b/test/golden-chromium/screenshot-sanity.png differ diff --git a/test/golden-chromium/transparent.png b/test/golden-chromium/transparent.png new file mode 100644 index 0000000000..1cf45d8688 Binary files /dev/null and b/test/golden-chromium/transparent.png differ diff --git a/test/golden-chromium/white.jpg b/test/golden-chromium/white.jpg new file mode 100644 index 0000000000..fb9070def3 Binary files /dev/null and b/test/golden-chromium/white.jpg differ diff --git a/test/golden-firefox/grid-cell-0.png b/test/golden-firefox/grid-cell-0.png new file mode 100644 index 0000000000..4677bdbc4f Binary files /dev/null and b/test/golden-firefox/grid-cell-0.png differ diff --git a/test/golden-firefox/grid-cell-1.png b/test/golden-firefox/grid-cell-1.png new file mode 100644 index 0000000000..532dc8db65 Binary files /dev/null and b/test/golden-firefox/grid-cell-1.png differ diff --git a/test/golden-firefox/screenshot-clip-odd-size.png b/test/golden-firefox/screenshot-clip-odd-size.png new file mode 100644 index 0000000000..8e86dc9017 Binary files /dev/null and b/test/golden-firefox/screenshot-clip-odd-size.png differ diff --git a/test/golden-firefox/screenshot-clip-rect.png b/test/golden-firefox/screenshot-clip-rect.png new file mode 100644 index 0000000000..7a74457869 Binary files /dev/null and b/test/golden-firefox/screenshot-clip-rect.png differ diff --git a/test/golden-firefox/screenshot-element-bounding-box.png b/test/golden-firefox/screenshot-element-bounding-box.png new file mode 100644 index 0000000000..f4e059c300 Binary files /dev/null and b/test/golden-firefox/screenshot-element-bounding-box.png differ diff --git a/test/golden-firefox/screenshot-element-fractional-offset.png b/test/golden-firefox/screenshot-element-fractional-offset.png new file mode 100644 index 0000000000..f554b1d62c Binary files /dev/null and b/test/golden-firefox/screenshot-element-fractional-offset.png differ diff --git a/test/golden-firefox/screenshot-element-fractional.png b/test/golden-firefox/screenshot-element-fractional.png new file mode 100644 index 0000000000..d1431bd91d Binary files /dev/null and b/test/golden-firefox/screenshot-element-fractional.png differ diff --git a/test/golden-firefox/screenshot-element-larger-than-viewport.png b/test/golden-firefox/screenshot-element-larger-than-viewport.png new file mode 100644 index 0000000000..6d28cddcea Binary files /dev/null and b/test/golden-firefox/screenshot-element-larger-than-viewport.png differ diff --git a/test/golden-firefox/screenshot-element-padding-border.png b/test/golden-firefox/screenshot-element-padding-border.png new file mode 100644 index 0000000000..2b72c7528b Binary files /dev/null and b/test/golden-firefox/screenshot-element-padding-border.png differ diff --git a/test/golden-firefox/screenshot-element-rotate.png b/test/golden-firefox/screenshot-element-rotate.png new file mode 100644 index 0000000000..0a78fb1ae7 Binary files /dev/null and b/test/golden-firefox/screenshot-element-rotate.png differ diff --git a/test/golden-firefox/screenshot-element-scrolled-into-view.png b/test/golden-firefox/screenshot-element-scrolled-into-view.png new file mode 100644 index 0000000000..2b72c7528b Binary files /dev/null and b/test/golden-firefox/screenshot-element-scrolled-into-view.png differ diff --git a/test/golden-firefox/screenshot-grid-fullpage.png b/test/golden-firefox/screenshot-grid-fullpage.png new file mode 100644 index 0000000000..ac47ec83b1 Binary files /dev/null and b/test/golden-firefox/screenshot-grid-fullpage.png differ diff --git a/test/golden-firefox/screenshot-offscreen-clip.png b/test/golden-firefox/screenshot-offscreen-clip.png new file mode 100644 index 0000000000..31a0935cda Binary files /dev/null and b/test/golden-firefox/screenshot-offscreen-clip.png differ diff --git a/test/golden-firefox/screenshot-sanity.png b/test/golden-firefox/screenshot-sanity.png new file mode 100644 index 0000000000..07890a04b3 Binary files /dev/null and b/test/golden-firefox/screenshot-sanity.png differ diff --git a/test/golden-utils.js b/test/golden-utils.js new file mode 100644 index 0000000000..86989ac654 --- /dev/null +++ b/test/golden-utils.js @@ -0,0 +1,149 @@ +/** + * 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 path = require('path'); +const fs = require('fs'); +const Diff = require('text-diff'); +const mime = require('mime'); +const PNG = require('pngjs').PNG; +const jpeg = require('jpeg-js'); +const pixelmatch = require('pixelmatch'); + +module.exports = {compare}; + +const GoldenComparators = { + 'image/png': compareImages, + 'image/jpeg': compareImages, + 'text/plain': compareText +}; + + +/** + * @param {?Object} actualBuffer + * @param {!Buffer} expectedBuffer + * @param {!string} mimeType + * @return {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} + */ +function compareImages(actualBuffer, expectedBuffer, mimeType) { + if (!actualBuffer || !(actualBuffer instanceof Buffer)) + return { errorMessage: 'Actual result should be Buffer.' }; + + const actual = mimeType === 'image/png' ? PNG.sync.read(actualBuffer) : jpeg.decode(actualBuffer); + const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer); + if (expected.width !== actual.width || expected.height !== actual.height) { + return { + errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. ` + }; + } + const diff = new PNG({width: expected.width, height: expected.height}); + const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, {threshold: 0.1}); + return count > 0 ? { diff: PNG.sync.write(diff) } : null; +} + +/** + * @param {?Object} actual + * @param {!Buffer} expectedBuffer + * @return {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} + */ +function compareText(actual, expectedBuffer) { + if (typeof actual !== 'string') + return { errorMessage: 'Actual result should be string' }; + const expected = expectedBuffer.toString('utf-8'); + if (expected === actual) + return null; + const diff = new Diff(); + const result = diff.main(expected, actual); + diff.cleanupSemantic(result); + let html = diff.prettyHtml(result); + const diffStylePath = path.join(__dirname, 'diffstyle.css'); + html = `` + html; + return { + diff: html, + diffExtension: '.html' + }; +} + +/** + * @param {?Object} actual + * @param {string} goldenName + * @return {!{pass: boolean, message: (undefined|string)}} + */ +function compare(goldenPath, outputPath, actual, goldenName) { + goldenPath = path.normalize(goldenPath); + outputPath = path.normalize(outputPath); + const expectedPath = path.join(goldenPath, goldenName); + const actualPath = path.join(outputPath, goldenName); + + const messageSuffix = 'Output is saved in "' + path.basename(outputPath + '" directory'); + + if (!fs.existsSync(expectedPath)) { + ensureOutputDir(); + fs.writeFileSync(actualPath, actual); + return { + pass: false, + message: goldenName + ' is missing in golden results. ' + messageSuffix + }; + } + const expected = fs.readFileSync(expectedPath); + const mimeType = mime.getType(goldenName); + const comparator = GoldenComparators[mimeType]; + if (!comparator) { + return { + pass: false, + message: 'Failed to find comparator with type ' + mimeType + ': ' + goldenName + }; + } + const result = comparator(actual, expected, mimeType); + if (!result) + return { pass: true }; + ensureOutputDir(); + if (goldenPath === outputPath) { + fs.writeFileSync(addSuffix(actualPath, '-actual'), actual); + } else { + fs.writeFileSync(actualPath, actual); + // Copy expected to the output/ folder for convenience. + fs.writeFileSync(addSuffix(actualPath, '-expected'), expected); + } + if (result.diff) { + const diffPath = addSuffix(actualPath, '-diff', result.diffExtension); + fs.writeFileSync(diffPath, result.diff); + } + + let message = goldenName + ' mismatch!'; + if (result.errorMessage) + message += ' ' + result.errorMessage; + return { + pass: false, + message: message + ' ' + messageSuffix + }; + + function ensureOutputDir() { + if (!fs.existsSync(outputPath)) + fs.mkdirSync(outputPath); + } +} + +/** + * @param {string} filePath + * @param {string} suffix + * @param {string=} customExtension + * @return {string} + */ +function addSuffix(filePath, suffix, customExtension) { + const dirname = path.dirname(filePath); + const ext = path.extname(filePath); + const name = path.basename(filePath, ext); + return path.join(dirname, name + suffix + (customExtension || ext)); +} diff --git a/test/golden-webkit/grid-cell-0.png b/test/golden-webkit/grid-cell-0.png new file mode 100644 index 0000000000..5ae546557b Binary files /dev/null and b/test/golden-webkit/grid-cell-0.png differ diff --git a/test/golden-webkit/grid-cell-1.png b/test/golden-webkit/grid-cell-1.png new file mode 100644 index 0000000000..1110528326 Binary files /dev/null and b/test/golden-webkit/grid-cell-1.png differ diff --git a/test/golden-webkit/screenshot-clip-odd-size.png b/test/golden-webkit/screenshot-clip-odd-size.png new file mode 100644 index 0000000000..27a2b2807f Binary files /dev/null and b/test/golden-webkit/screenshot-clip-odd-size.png differ diff --git a/test/golden-webkit/screenshot-clip-rect.png b/test/golden-webkit/screenshot-clip-rect.png new file mode 100644 index 0000000000..3217645a36 Binary files /dev/null and b/test/golden-webkit/screenshot-clip-rect.png differ diff --git a/test/golden-webkit/screenshot-element-bounding-box.png b/test/golden-webkit/screenshot-element-bounding-box.png new file mode 100644 index 0000000000..5b07911c66 Binary files /dev/null and b/test/golden-webkit/screenshot-element-bounding-box.png differ diff --git a/test/golden-webkit/screenshot-element-fractional-offset.png b/test/golden-webkit/screenshot-element-fractional-offset.png new file mode 100644 index 0000000000..c9e0eb15e9 Binary files /dev/null and b/test/golden-webkit/screenshot-element-fractional-offset.png differ diff --git a/test/golden-webkit/screenshot-element-fractional.png b/test/golden-webkit/screenshot-element-fractional.png new file mode 100644 index 0000000000..390ef19d2e Binary files /dev/null and b/test/golden-webkit/screenshot-element-fractional.png differ diff --git a/test/golden-webkit/screenshot-element-larger-than-viewport.png b/test/golden-webkit/screenshot-element-larger-than-viewport.png new file mode 100644 index 0000000000..bea66e69e8 Binary files /dev/null and b/test/golden-webkit/screenshot-element-larger-than-viewport.png differ diff --git a/test/golden-webkit/screenshot-element-padding-border.png b/test/golden-webkit/screenshot-element-padding-border.png new file mode 100644 index 0000000000..7260617ec2 Binary files /dev/null and b/test/golden-webkit/screenshot-element-padding-border.png differ diff --git a/test/golden-webkit/screenshot-element-scrolled-into-view.png b/test/golden-webkit/screenshot-element-scrolled-into-view.png new file mode 100644 index 0000000000..7260617ec2 Binary files /dev/null and b/test/golden-webkit/screenshot-element-scrolled-into-view.png differ diff --git a/test/golden-webkit/screenshot-grid-fullpage.png b/test/golden-webkit/screenshot-grid-fullpage.png new file mode 100644 index 0000000000..142de08133 Binary files /dev/null and b/test/golden-webkit/screenshot-grid-fullpage.png differ diff --git a/test/golden-webkit/screenshot-offscreen-clip.png b/test/golden-webkit/screenshot-offscreen-clip.png new file mode 100644 index 0000000000..fa7bfdc3e3 Binary files /dev/null and b/test/golden-webkit/screenshot-offscreen-clip.png differ diff --git a/test/golden-webkit/screenshot-sanity.png b/test/golden-webkit/screenshot-sanity.png new file mode 100644 index 0000000000..fc41e344f9 Binary files /dev/null and b/test/golden-webkit/screenshot-sanity.png differ diff --git a/test/golden-webkit/transparent.png b/test/golden-webkit/transparent.png new file mode 100644 index 0000000000..47adcb4de0 Binary files /dev/null and b/test/golden-webkit/transparent.png differ diff --git a/test/headful.spec.js b/test/headful.spec.js new file mode 100644 index 0000000000..61ab37c602 --- /dev/null +++ b/test/headful.spec.js @@ -0,0 +1,152 @@ +/** + * 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 path = require('path'); +const os = require('os'); +const fs = require('fs'); +const util = require('util'); +const utils = require('./utils'); +const {waitEvent} = utils; + +const rmAsync = util.promisify(require('rimraf')); +const mkdtempAsync = util.promisify(fs.mkdtemp); + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +module.exports.addTests = function({testRunner, expect, playwright, defaultBrowserOptions, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + const headfulOptions = Object.assign({}, defaultBrowserOptions, { + headless: false + }); + const headlessOptions = Object.assign({}, defaultBrowserOptions, { + headless: true + }); + const extensionPath = path.join(__dirname, 'assets', 'simple-extension'); + const extensionOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + ], + }); + + describe('HEADFUL', function() { + it('background_page target type should be available', async() => { + const browserWithExtension = await playwright.launch(extensionOptions); + const page = await browserWithExtension.newPage(); + const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page'); + await page.close(); + await browserWithExtension.close(); + expect(backgroundPageTarget).toBeTruthy(); + }); + it('target.page() should return a background_page', async({}) => { + const browserWithExtension = await playwright.launch(extensionOptions); + const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page'); + const page = await backgroundPageTarget.page(); + expect(await page.evaluate(() => 2 * 3)).toBe(6); + expect(await page.evaluate(() => window.MAGIC)).toBe(42); + await browserWithExtension.close(); + }); + it('should have default url when launching browser', async function() { + const browser = await playwright.launch(extensionOptions); + const pages = (await browser.pages()).map(page => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it('headless should be able to read cookies written by headful', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + // Write a cookie in headful chrome + const headfulBrowser = await playwright.launch(Object.assign({userDataDir}, headfulOptions)); + const headfulPage = await headfulBrowser.newPage(); + await headfulPage.goto(server.EMPTY_PAGE); + await headfulPage.evaluate(() => document.cookie = 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'); + await headfulBrowser.close(); + // Read the cookie from headless chrome + const headlessBrowser = await playwright.launch(Object.assign({userDataDir}, headlessOptions)); + const headlessPage = await headlessBrowser.newPage(); + await headlessPage.goto(server.EMPTY_PAGE); + const cookie = await headlessPage.evaluate(() => document.cookie); + await headlessBrowser.close(); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + expect(cookie).toBe('foo=true'); + }); + // TODO: Support OOOPIF. @see https://github.com/GoogleChrome/puppeteer/issues/2548 + xit('OOPIF: should report google.com frame', async({server}) => { + // https://google.com is isolated by default in Chromium embedder. + const browser = await playwright.launch(headfulOptions); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', r => r.respond({body: 'YO, GOOGLE.COM'})); + await page.evaluate(() => { + const frame = document.createElement('iframe'); + frame.setAttribute('src', 'https://google.com/'); + document.body.appendChild(frame); + return new Promise(x => frame.onload = x); + }); + await page.waitForSelector('iframe[src="https://google.com/"]'); + const urls = page.frames().map(frame => frame.url()).sort(); + expect(urls).toEqual([ + server.EMPTY_PAGE, + 'https://google.com/' + ]); + await browser.close(); + }); + it('should close browser with beforeunload page', async({server}) => { + const browser = await playwright.launch(headfulOptions); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await page.click('body'); + await browser.close(); + }); + it('should open devtools when "devtools: true" option is given', async({server}) => { + const browser = await playwright.launch(Object.assign({devtools: true}, headfulOptions)); + const context = await browser.createIncognitoBrowserContext(); + await Promise.all([ + context.newPage(), + context.waitForTarget(target => target.url().includes('devtools://')), + ]); + await browser.close(); + }); + }); + + describe('Page.bringToFront', function() { + it('should work', async() => { + const browser = await playwright.launch(headfulOptions); + const page1 = await browser.newPage(); + const page2 = await browser.newPage(); + + await page1.bringToFront(); + expect(await page1.evaluate(() => document.visibilityState)).toBe('visible'); + expect(await page2.evaluate(() => document.visibilityState)).toBe('hidden'); + + await page2.bringToFront(); + expect(await page1.evaluate(() => document.visibilityState)).toBe('hidden'); + expect(await page2.evaluate(() => document.visibilityState)).toBe('visible'); + + await page1.close(); + await page2.close(); + await browser.close(); + }); + }); +}; + diff --git a/test/ignorehttpserrors.spec.js b/test/ignorehttpserrors.spec.js new file mode 100644 index 0000000000..0d25fd280d --- /dev/null +++ b/test/ignorehttpserrors.spec.js @@ -0,0 +1,97 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + describe.skip(WEBKIT)('ignoreHTTPSErrors', function() { + beforeAll(async state => { + state.browser = await playwright.launch({...defaultBrowserOptions, ignoreHTTPSErrors: true}); + }); + afterAll(async state => { + await state.browser.close(); + delete state.browser; + }); + beforeEach(async state => { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + afterEach(async state => { + await state.context.close(); + delete state.context; + delete state.page; + }); + + describe('Response.securityDetails', function() { + it('should work', async({page, httpsServer}) => { + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE) + ]); + const securityDetails = response.securityDetails(); + expect(securityDetails.issuer()).toBe('playwright-tests'); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + expect(securityDetails.subjectName()).toBe('playwright-tests'); + expect(securityDetails.validFrom()).toBe(1550084863); + expect(securityDetails.validTo()).toBe(33086084863); + }); + it('should be |null| for non-secure requests', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.securityDetails()).toBe(null); + }); + it('Network redirects should report SecurityDetails', async({page, httpsServer}) => { + httpsServer.setRedirect('/plzredirect', '/empty.html'); + const responses = []; + page.on('response', response => responses.push(response)); + const [serverRequest, ] = await Promise.all([ + httpsServer.waitForRequest('/plzredirect'), + page.goto(httpsServer.PREFIX + '/plzredirect') + ]); + expect(responses.length).toBe(2); + expect(responses[0].status()).toBe(302); + const securityDetails = responses[0].securityDetails(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + }); + }); + + it('should work', async({page, httpsServer}) => { + let error = null; + const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + }); + it('should work with request interception', async({page, server, httpsServer}) => { + await page.setRequestInterception(true); + page.on('request', request => request.continue()); + const response = await page.goto(httpsServer.EMPTY_PAGE); + expect(response.status()).toBe(200); + }); + it('should work with mixed content', async({page, server, httpsServer}) => { + httpsServer.setRoute('/mixedcontent.html', (req, res) => { + res.end(``); + }); + await page.goto(httpsServer.PREFIX + '/mixedcontent.html', {waitUntil: 'load'}); + expect(page.frames().length).toBe(2); + // Make sure blocked iframe has functional execution context + // @see https://github.com/GoogleChrome/puppeteer/issues/2709 + expect(await page.frames()[0].evaluate('1 + 2')).toBe(3); + expect(await page.frames()[1].evaluate('2 + 3')).toBe(5); + }); + }); +}; diff --git a/test/input.spec.js b/test/input.spec.js new file mode 100644 index 0000000000..764dfb8c1f --- /dev/null +++ b/test/input.spec.js @@ -0,0 +1,223 @@ +/** + * 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 path = require('path'); + +const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt'); + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + describe.skip(WEBKIT)('input', function() { + it('should upload the file', async({page, server}) => { + await page.goto(server.PREFIX + '/input/fileupload.html'); + const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); + const input = await page.$('input'); + await input.uploadFile(filePath); + expect(await page.evaluate(e => e.files[0].name, input)).toBe('file-to-upload.txt'); + expect(await page.evaluate(e => { + const reader = new FileReader(); + const promise = new Promise(fulfill => reader.onload = fulfill); + reader.readAsText(e.files[0]); + return promise.then(() => reader.result); + }, input)).toBe('contents of the file'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.waitForFileChooser', function() { + it('should work when file input is attached to DOM', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser).toBeTruthy(); + }); + it('should work when file input is not attached to DOM', async({page, server}) => { + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.evaluate(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }), + ]); + expect(chooser).toBeTruthy(); + }); + it('should respect timeout', async({page, server}) => { + let error = null; + await page.waitForFileChooser({timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async({page, server}) => { + page.setDefaultTimeout(1); + let error = null; + await page.waitForFileChooser().catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async({page, server}) => { + page.setDefaultTimeout(0); + let error = null; + await page.waitForFileChooser({timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should work with no timeout', async({page, server}) => { + const [chooser] = await Promise.all([ + page.waitForFileChooser({timeout: 0}), + page.evaluate(() => setTimeout(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }, 50)) + ]); + expect(chooser).toBeTruthy(); + }); + it('should return the same file chooser when there are many watchdogs simultaneously', async({page, server}) => { + await page.setContent(``); + const [fileChooser1, fileChooser2] = await Promise.all([ + page.waitForFileChooser(), + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + expect(fileChooser1 === fileChooser2).toBe(true); + }); + }); + + describe.skip(FFOX || WEBKIT)('FileChooser.accept', function() { + it('should accept single file', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + await Promise.all([ + chooser.accept([FILE_TO_UPLOAD]), + new Promise(x => page.once('metrics', x)), + ]); + expect(await page.$eval('input', input => input.files.length)).toBe(1); + expect(await page.$eval('input', input => input.files[0].name)).toBe('file-to-upload.txt'); + }); + it('should be able to read selected file', async({page, server}) => { + await page.setContent(``); + page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD])); + expect(await page.$eval('input', async picker => { + picker.click(); + await new Promise(x => picker.oninput = x); + const reader = new FileReader(); + const promise = new Promise(fulfill => reader.onload = fulfill); + reader.readAsText(picker.files[0]); + return promise.then(() => reader.result); + })).toBe('contents of the file'); + }); + it('should be able to reset selected files with empty file list', async({page, server}) => { + await page.setContent(``); + page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD])); + expect(await page.$eval('input', async picker => { + picker.click(); + await new Promise(x => picker.oninput = x); + return picker.files.length; + })).toBe(1); + page.waitForFileChooser().then(chooser => chooser.accept([])); + expect(await page.$eval('input', async picker => { + picker.click(); + await new Promise(x => picker.oninput = x); + return picker.files.length; + })).toBe(0); + }); + it('should not accept multiple files for single-file input', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error = null; + await chooser.accept([ + path.relative(process.cwd(), __dirname + '/assets/file-to-upload.txt'), + path.relative(process.cwd(), __dirname + '/assets/pptr.png'), + ]).catch(e => error = e); + expect(error).not.toBe(null); + }); + it('should fail when accepting file chooser twice', async({page, server}) => { + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + await fileChooser.accept([]); + let error = null; + await fileChooser.accept([]).catch(e => error = e); + expect(error.message).toBe('Cannot accept FileChooser which is already handled!'); + }); + }); + + describe.skip(FFOX || WEBKIT)('FileChooser.cancel', function() { + it('should cancel dialog', async({page, server}) => { + // Consider file chooser canceled if we can summon another one. + // There's no reliable way in WebPlatform to see that FileChooser was + // canceled. + await page.setContent(``); + const [fileChooser1] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + await fileChooser1.cancel(); + // If this resolves, than we successfully canceled file chooser. + await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + }); + it('should fail when canceling file chooser twice', async({page, server}) => { + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + await fileChooser.cancel(); + let error = null; + await fileChooser.cancel().catch(e => error = e); + expect(error.message).toBe('Cannot cancel FileChooser which is already handled!'); + }); + }); + + describe.skip(FFOX || WEBKIT)('FileChooser.isMultiple', () => { + it('should work for single file pick', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(false); + }); + it('should work for "multiple"', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + it('should work for "webkitdirectory"', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + }); +}; diff --git a/test/jshandle.spec.js b/test/jshandle.spec.js new file mode 100644 index 0000000000..7d120336c6 --- /dev/null +++ b/test/jshandle.spec.js @@ -0,0 +1,199 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, CHROME, FFOX, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.evaluateHandle', function() { + it('should work', async({page, server}) => { + const windowHandle = await page.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + it('should accept object handle as an argument', async({page, server}) => { + const navigatorHandle = await page.evaluateHandle(() => navigator); + const text = await page.evaluate(e => e.userAgent, navigatorHandle); + expect(text).toContain('Mozilla'); + }); + it('should accept object handle to primitive types', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => 5); + const isFive = await page.evaluate(e => Object.is(e, 5), aHandle); + expect(isFive).toBeTruthy(); + }); + it('should warn on nested object handles', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => document.body); + let error = null; + await page.evaluateHandle( + opts => opts.elem.querySelector('p'), + { elem: aHandle } + ).catch(e => error = e); + expect(error.message).toContain('Are you passing a nested JSHandle?'); + }); + it('should accept object handle to unserializable value', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => Infinity); + expect(await page.evaluate(e => Object.is(e, Infinity), aHandle)).toBe(true); + }); + it('should use the same JS wrappers', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => { + window.FOO = 123; + return window; + }); + expect(await page.evaluate(e => e.FOO, aHandle)).toBe(123); + }); + it('should work with primitives', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => { + window.FOO = 123; + return window; + }); + expect(await page.evaluate(e => e.FOO, aHandle)).toBe(123); + }); + }); + + describe('JSHandle.getProperty', function() { + it('should work', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => ({ + one: 1, + two: 2, + three: 3 + })); + const twoHandle = await aHandle.getProperty('two'); + expect(await twoHandle.jsonValue()).toEqual(2); + }); + }); + + describe('JSHandle.jsonValue', function() { + it('should work', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => ({foo: 'bar'})); + const json = await aHandle.jsonValue(); + expect(json).toEqual({foo: 'bar'}); + }); + it.skip(FFOX)('should not work with dates', async({page, server}) => { + const dateHandle = await page.evaluateHandle(() => new Date('2017-09-26T00:00:00.000Z')); + const json = await dateHandle.jsonValue(); + expect(json).toEqual({}); + }); + it('should throw for circular objects', async({page, server}) => { + const windowHandle = await page.evaluateHandle('window'); + let error = null; + await windowHandle.jsonValue().catch(e => error = e); + if (WEBKIT) + expect(error.message).toContain('Object has too long reference chain'); + else if (CHROME) + expect(error.message).toContain('Object reference chain is too long'); + else if (FFOX) + expect(error.message).toContain('Object is not serializable'); + }); + }); + + describe('JSHandle.getProperties', function() { + it('should work', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => ({ + foo: 'bar' + })); + const properties = await aHandle.getProperties(); + const foo = properties.get('foo'); + expect(foo).toBeTruthy(); + expect(await foo.jsonValue()).toBe('bar'); + }); + it('should return even non-own properties', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => { + class A { + constructor() { + this.a = '1'; + } + } + class B extends A { + constructor() { + super(); + this.b = '2'; + } + } + return new B(); + }); + const properties = await aHandle.getProperties(); + expect(await properties.get('a').jsonValue()).toBe('1'); + expect(await properties.get('b').jsonValue()).toBe('2'); + }); + }); + + describe('JSHandle.asElement', function() { + it('should work', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => document.body); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + }); + it('should return null for non-elements', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => 2); + const element = aHandle.asElement(); + expect(element).toBeFalsy(); + }); + it('should return ElementHandle for TextNodes', async({page, server}) => { + await page.setContent('
ee!
'); + const aHandle = await page.evaluateHandle(() => document.querySelector('div').firstChild); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + expect(await page.evaluate(e => e.nodeType === HTMLElement.TEXT_NODE, element)); + }); + it('should work with nullified Node', async({page, server}) => { + await page.setContent('
test
'); + await page.evaluate(() => delete Node); + const handle = await page.evaluateHandle(() => document.querySelector('section')); + const element = handle.asElement(); + expect(element).not.toBe(null); + }); + }); + + describe('JSHandle.toString', function() { + it('should work for primitives', async({page, server}) => { + const numberHandle = await page.evaluateHandle(() => 2); + expect(numberHandle.toString()).toBe('JSHandle:2'); + const stringHandle = await page.evaluateHandle(() => 'a'); + expect(stringHandle.toString()).toBe('JSHandle:a'); + }); + it('should work for complicated objects', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => window); + expect(aHandle.toString()).toBe('JSHandle@object'); + }); + it('should work for promises', async({page, server}) => { + // wrap the promise in an object, otherwise we will await. + const wrapperHandle = await page.evaluateHandle(() => ({b: Promise.resolve(123)})); + const bHandle = await wrapperHandle.getProperty('b'); + expect(bHandle.toString()).toBe('JSHandle@promise'); + }); + it('should work with different subtypes', async({page, server}) => { + expect((await page.evaluateHandle('(function(){})')).toString()).toBe('JSHandle@function'); + expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle:12'); + expect((await page.evaluateHandle('true')).toString()).toBe('JSHandle:true'); + expect((await page.evaluateHandle('undefined')).toString()).toBe('JSHandle:undefined'); + expect((await page.evaluateHandle('"foo"')).toString()).toBe('JSHandle:foo'); + expect((await page.evaluateHandle('Symbol()')).toString()).toBe('JSHandle@symbol'); + expect((await page.evaluateHandle('new Map()')).toString()).toBe('JSHandle@map'); + expect((await page.evaluateHandle('new Set()')).toString()).toBe('JSHandle@set'); + expect((await page.evaluateHandle('[]')).toString()).toBe('JSHandle@array'); + expect((await page.evaluateHandle('null')).toString()).toBe('JSHandle:null'); + expect((await page.evaluateHandle('/foo/')).toString()).toBe('JSHandle@regexp'); + expect((await page.evaluateHandle('document.body')).toString()).toBe('JSHandle@node'); + expect((await page.evaluateHandle('new Date()')).toString()).toBe('JSHandle@date'); + expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe('JSHandle@weakmap'); + expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe('JSHandle@weakset'); + expect((await page.evaluateHandle('new Error()')).toString()).toBe('JSHandle@error'); + // TODO(yurys): change subtype from array to typedarray in WebKit. + expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe(WEBKIT ? 'JSHandle@array' : 'JSHandle@typedarray'); + expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe('JSHandle@proxy'); + }); + }); +}; diff --git a/test/keyboard.spec.js b/test/keyboard.spec.js new file mode 100644 index 0000000000..e9597f610a --- /dev/null +++ b/test/keyboard.spec.js @@ -0,0 +1,255 @@ +/** + * 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 utils = require('./utils'); +const os = require('os'); + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Keyboard', function() { + it('should type into a textarea', async({page, server}) => { + await page.evaluate(() => { + const textarea = document.createElement('textarea'); + document.body.appendChild(textarea); + textarea.focus(); + }); + const text = 'Hello world. I am the text that was typed!'; + await page.keyboard.type(text); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe(text); + }); + it('should press the metaKey', async({page, server}) => { + await page.goto(server.PREFIX + '/empty.html'); + await page.evaluate(() => { + window.keyPromise = new Promise(resolve => document.addEventListener('keydown', event => resolve(event.key))); + }); + await page.keyboard.press('Meta'); + expect(await page.evaluate('keyPromise')).toBe(FFOX && os.platform() !== 'darwin' ? 'OS' : 'Meta'); + }); + it('should move with the arrow keys', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', 'Hello World!'); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello World!'); + for (let i = 0; i < 'World!'.length; i++) + page.keyboard.press('ArrowLeft'); + await page.keyboard.type('inserted '); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello inserted World!'); + page.keyboard.down('Shift'); + for (let i = 0; i < 'inserted '.length; i++) + page.keyboard.press('ArrowLeft'); + page.keyboard.up('Shift'); + await page.keyboard.press('Backspace'); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello World!'); + }); + it('should send a character with ElementHandle.press', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = await page.$('textarea'); + await textarea.press('a'); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('a'); + + await page.evaluate(() => window.addEventListener('keydown', e => e.preventDefault(), true)); + + await textarea.press('b'); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('a'); + }); + it.skip(FFOX || WEBKIT)('ElementHandle.press should support |text| option', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = await page.$('textarea'); + await textarea.press('a', {text: 'ё'}); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('ё'); + }); + it.skip(WEBKIT)('should send a character with sendCharacter', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.keyboard.sendCharacter('嗨'); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('嗨'); + await page.evaluate(() => window.addEventListener('keydown', e => e.preventDefault(), true)); + await page.keyboard.sendCharacter('a'); + expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('嗨a'); + }); + it.skip(WEBKIT)('should report shiftKey', async({page, server}) => { + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + const codeForKey = {'Shift': 16, 'Alt': 18, 'Control': 17}; + for (const modifierKey in codeForKey) { + await keyboard.down(modifierKey); + expect(await page.evaluate(() => getResult())).toBe('Keydown: ' + modifierKey + ' ' + modifierKey + 'Left ' + codeForKey[modifierKey] + ' [' + modifierKey + ']'); + await keyboard.down('!'); + // Shift+! will generate a keypress + if (modifierKey === 'Shift') + expect(await page.evaluate(() => getResult())).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']\nKeypress: ! Digit1 33 33 [' + modifierKey + ']'); + else + expect(await page.evaluate(() => getResult())).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']'); + + await keyboard.up('!'); + expect(await page.evaluate(() => getResult())).toBe('Keyup: ! Digit1 49 [' + modifierKey + ']'); + await keyboard.up(modifierKey); + expect(await page.evaluate(() => getResult())).toBe('Keyup: ' + modifierKey + ' ' + modifierKey + 'Left ' + codeForKey[modifierKey] + ' []'); + } + }); + it.skip(WEBKIT)('should report multiple modifiers', async({page, server}) => { + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Control'); + expect(await page.evaluate(() => getResult())).toBe('Keydown: Control ControlLeft 17 [Control]'); + await keyboard.down('Alt'); + expect(await page.evaluate(() => getResult())).toBe('Keydown: Alt AltLeft 18 [Alt Control]'); + await keyboard.down(';'); + expect(await page.evaluate(() => getResult())).toBe('Keydown: ; Semicolon 186 [Alt Control]'); + await keyboard.up(';'); + expect(await page.evaluate(() => getResult())).toBe('Keyup: ; Semicolon 186 [Alt Control]'); + await keyboard.up('Control'); + expect(await page.evaluate(() => getResult())).toBe('Keyup: Control ControlLeft 17 [Alt]'); + await keyboard.up('Alt'); + expect(await page.evaluate(() => getResult())).toBe('Keyup: Alt AltLeft 18 []'); + }); + it.skip(WEBKIT)('should send proper codes while typing', async({page, server}) => { + await page.goto(server.PREFIX + '/input/keyboard.html'); + await page.keyboard.type('!'); + expect(await page.evaluate(() => getResult())).toBe( + [ 'Keydown: ! Digit1 49 []', + 'Keypress: ! Digit1 33 33 []', + 'Keyup: ! Digit1 49 []'].join('\n')); + await page.keyboard.type('^'); + expect(await page.evaluate(() => getResult())).toBe( + [ 'Keydown: ^ Digit6 54 []', + 'Keypress: ^ Digit6 94 94 []', + 'Keyup: ^ Digit6 54 []'].join('\n')); + }); + it.skip(WEBKIT)('should send proper codes while typing with shift', async({page, server}) => { + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Shift'); + await page.keyboard.type('~'); + expect(await page.evaluate(() => getResult())).toBe( + [ 'Keydown: Shift ShiftLeft 16 [Shift]', + 'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode + 'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode + 'Keyup: ~ Backquote 192 [Shift]'].join('\n')); + await keyboard.up('Shift'); + }); + it.skip(WEBKIT)('should not type canceled events', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => { + window.addEventListener('keydown', event => { + event.stopPropagation(); + event.stopImmediatePropagation(); + if (event.key === 'l') + event.preventDefault(); + if (event.key === 'o') + event.preventDefault(); + }, false); + }); + await page.keyboard.type('Hello World!'); + expect(await page.evaluate(() => textarea.value)).toBe('He Wrd!'); + }); + it.skip(WEBKIT)('should specify repeat property', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => document.querySelector('textarea').addEventListener('keydown', e => window.lastEvent = e, true)); + await page.keyboard.down('a'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false); + await page.keyboard.press('a'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(true); + + await page.keyboard.down('b'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false); + await page.keyboard.down('b'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(true); + + await page.keyboard.up('a'); + await page.keyboard.down('a'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false); + }); + it.skip(WEBKIT)('should type all kinds of characters', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = 'This text goes onto two lines.\nThis character is 嗨.'; + await page.keyboard.type(text); + expect(await page.evaluate('result')).toBe(text); + }); + it.skip(WEBKIT)('should specify location', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => { + window.addEventListener('keydown', event => window.keyLocation = event.location, true); + }); + const textarea = await page.$('textarea'); + + await textarea.press('Digit5'); + expect(await page.evaluate('keyLocation')).toBe(0); + + await textarea.press('ControlLeft'); + expect(await page.evaluate('keyLocation')).toBe(1); + + await textarea.press('ControlRight'); + expect(await page.evaluate('keyLocation')).toBe(2); + + await textarea.press('NumpadSubtract'); + expect(await page.evaluate('keyLocation')).toBe(3); + }); + it.skip(WEBKIT)('should throw on unknown keys', async({page, server}) => { + let error = await page.keyboard.press('NotARealKey').catch(e => e); + expect(error.message).toBe('Unknown key: "NotARealKey"'); + + error = await page.keyboard.press('ё').catch(e => e); + expect(error && error.message).toBe('Unknown key: "ё"'); + + error = await page.keyboard.press('😊').catch(e => e); + expect(error && error.message).toBe('Unknown key: "😊"'); + }); + it.skip(WEBKIT)('should type emoji', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', '👹 Tokyo street Japan 🇯🇵'); + expect(await page.$eval('textarea', textarea => textarea.value)).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it.skip(WEBKIT)('should type emoji into an iframe', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'emoji-test', server.PREFIX + '/input/textarea.html'); + const frame = page.frames()[1]; + const textarea = await frame.$('textarea'); + await textarea.type('👹 Tokyo street Japan 🇯🇵'); + expect(await frame.$eval('textarea', textarea => textarea.value)).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it.skip(WEBKIT)('should press the meta key', async({page}) => { + await page.evaluate(() => { + window.result = null; + document.addEventListener('keydown', event => { + window.result = [event.key, event.code, event.metaKey]; + }); + }); + await page.keyboard.press('Meta'); + const [key, code, metaKey] = await page.evaluate('result'); + if (FFOX && os.platform() !== 'darwin') + expect(key).toBe('OS'); + else + expect(key).toBe('Meta'); + + if (FFOX) + expect(code).toBe('OSLeft'); + else + expect(code).toBe('MetaLeft'); + + if (FFOX && os.platform() !== 'darwin') + expect(metaKey).toBe(false); + else + expect(metaKey).toBe(true); + + }); + }); +}; diff --git a/test/launcher.spec.js b/test/launcher.spec.js new file mode 100644 index 0000000000..767cc0aa20 --- /dev/null +++ b/test/launcher.spec.js @@ -0,0 +1,427 @@ +/** + * 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 fs = require('fs'); +const os = require('os'); +const path = require('path'); +const {helper} = require('../lib/helper'); +const rmAsync = helper.promisify(require('rimraf')); +const mkdtempAsync = helper.promisify(fs.mkdtemp); +const readFileAsync = helper.promisify(fs.readFile); +const statAsync = helper.promisify(fs.stat); +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); +const utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Playwright', function() { + describe('BrowserFetcher', function() { + it.skip(WEBKIT)('should download and extract linux binary', async({server}) => { + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = playwright.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX + }); + let revisionInfo = browserFetcher.revisionInfo('123456'); + server.setRoute(revisionInfo.url.substring(server.PREFIX.length), (req, res) => { + server.serveFile(req, res, '/chromium-linux.zip'); + }); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload('123456')).toBe(true); + + revisionInfo = await browserFetcher.download('123456'); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe('LINUX BINARY\n'); + const expectedPermissions = os.platform() === 'win32' ? 0666 : 0755; + expect((await statAsync(revisionInfo.executablePath)).mode & 0777).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual(['123456']); + await browserFetcher.remove('123456'); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + }); + describe.skip(WEBKIT)('Browser.disconnect', function() { + it('should reject navigation when browser closes', async({server}) => { + server.setRoute('/one-style.css', () => {}); + const browser = await playwright.launch(defaultBrowserOptions); + const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browser.wsEndpoint()}); + const page = await remote.newPage(); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e); + await server.waitForRequest('/one-style.css'); + remote.disconnect(); + const error = await navigationPromise; + expect(error.message).toBe('Navigation failed because browser has disconnected!'); + await browser.close(); + }); + it('should reject waitForSelector when browser closes', async({server}) => { + server.setRoute('/empty.html', () => {}); + const browser = await playwright.launch(defaultBrowserOptions); + const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browser.wsEndpoint()}); + const page = await remote.newPage(); + const watchdog = page.waitForSelector('div', {timeout: 60000}).catch(e => e); + remote.disconnect(); + const error = await watchdog; + expect(error.message).toContain('Protocol error'); + await browser.close(); + }); + }); + describe('Browser.close', function() { + it.skip(WEBKIT)('should terminate network waiters', async({context, server}) => { + const browser = await playwright.launch(defaultBrowserOptions); + const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browser.wsEndpoint()}); + const newPage = await remote.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e), + newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e), + browser.close() + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + }); + }); + describe.skip(WEBKIT)('Playwright.launch', function() { + it('should reject all promises when browser is closed', async() => { + const browser = await playwright.launch(defaultBrowserOptions); + const page = await browser.newPage(); + let error = null; + const neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e); + await browser.close(); + await neverResolves; + expect(error.message).toContain('Protocol error'); + }); + it('should reject if executable path is invalid', async({server}) => { + let waitError = null; + const options = Object.assign({}, defaultBrowserOptions, {executablePath: 'random-invalid-path'}); + await playwright.launch(options).catch(e => waitError = e); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await playwright.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('userDataDir argument', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({}, defaultBrowserOptions); + if (CHROME || WEBKIT) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}` + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + `-profile`, + userDataDir, + ]; + } + const browser = await playwright.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('userDataDir option should restore state', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await playwright.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => localStorage.hey = 'hello'); + await browser.close(); + + const browser2 = await playwright.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect(await page2.evaluate(() => localStorage.hey)).toBe('hello'); + await browser2.close(); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + // This mysteriously fails on Windows on AppVeyor. See https://github.com/GoogleChrome/puppeteer/issues/4111 + xit('userDataDir option should restore cookies', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await playwright.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'); + await browser.close(); + + const browser2 = await playwright.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect(await page2.evaluate(() => document.cookie)).toBe('doSomethingOnlyOnce=true'); + await browser2.close(); + // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('should return the default arguments', async() => { + if (CHROME || WEBKIT) { + expect(playwright.defaultArgs()).toContain('--no-first-run'); + expect(playwright.defaultArgs()).toContain('--headless'); + expect(playwright.defaultArgs({headless: false})).not.toContain('--headless'); + expect(playwright.defaultArgs({userDataDir: 'foo'})).toContain('--user-data-dir=foo'); + } else { + expect(playwright.defaultArgs({browser: 'firefox'})).toContain('-headless'); + expect(playwright.defaultArgs({browser: 'firefox', headless: false})).not.toContain('-headless'); + expect(playwright.defaultArgs({browser: 'firefox', userDataDir: 'foo'})).toContain('-profile'); + expect(playwright.defaultArgs({browser: 'firefox', userDataDir: 'foo'})).toContain('foo'); + } + }); + it('should work with no default arguments', async() => { + const options = Object.assign({}, defaultBrowserOptions); + options.ignoreDefaultArgs = true; + const browser = await playwright.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should filter out ignored default arguments', async() => { + // Make sure we launch with `--enable-automation` by default. + const defaultArgs = playwright.defaultArgs(defaultBrowserOptions); + const browser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + // Ignore first and third default argument. + ignoreDefaultArgs: [ defaultArgs[0], defaultArgs[2] ], + })); + const spawnargs = browser.process().spawnargs; + expect(spawnargs.indexOf(defaultArgs[0])).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1])).not.toBe(-1); + expect(spawnargs.indexOf(defaultArgs[2])).toBe(-1); + await browser.close(); + }); + it.skip(FFOX)('should have default URL when launching browser', async function() { + const browser = await playwright.launch(defaultBrowserOptions); + const pages = (await browser.pages()).map(page => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it.skip(FFOX)('should have custom URL when launching browser', async function({server}) { + const options = Object.assign({}, defaultBrowserOptions); + options.args = [server.EMPTY_PAGE].concat(options.args || []); + const browser = await playwright.launch(options); + const pages = await browser.pages(); + expect(pages.length).toBe(1); + const page = pages[0]; + if (page.url() !== server.EMPTY_PAGE) + await page.waitForNavigation(); + expect(page.url()).toBe(server.EMPTY_PAGE); + await browser.close(); + }); + it('should set the default viewport', async() => { + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: { + width: 456, + height: 789 + } + }); + const browser = await playwright.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('window.innerWidth')).toBe(456); + expect(await page.evaluate('window.innerHeight')).toBe(789); + await browser.close(); + }); + it('should disable the default viewport', async() => { + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null + }); + const browser = await playwright.launch(options); + const page = await browser.newPage(); + expect(page.viewport()).toBe(null); + await browser.close(); + }); + it('should take fullPage screenshots when defaultViewport is null', async({server}) => { + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null + }); + const browser = await playwright.launch(options); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true + }); + expect(screenshot).toBeInstanceOf(Buffer); + await browser.close(); + }); + }); + describe.skip(WEBKIT)('Playwright.connect', function() { + it('should be able to connect multiple times to the same browser', async({server}) => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const browser = await playwright.connect({ + ...defaultBrowserOptions, + browserWSEndpoint: originalBrowser.wsEndpoint() + }); + const page = await browser.newPage(); + expect(await page.evaluate(() => 7 * 8)).toBe(56); + browser.disconnect(); + + const secondPage = await originalBrowser.newPage(); + expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work'); + await originalBrowser.close(); + }); + it('should be able to close remote browser', async({server}) => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const remoteBrowser = await playwright.connect({ + ...defaultBrowserOptions, + browserWSEndpoint: originalBrowser.wsEndpoint() + }); + await Promise.all([ + utils.waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should support ignoreHTTPSErrors option', async({httpsServer}) => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint, ignoreHTTPSErrors: true}); + const page = await browser.newPage(); + let error = null; + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e) + ]); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + expect(response.securityDetails()).toBeTruthy(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(response.securityDetails().protocol()).toBe(protocol); + await page.close(); + await browser.close(); + }); + it('should be able to reconnect to a disconnected browser', async({server}) => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const page = await originalBrowser.newPage(); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + originalBrowser.disconnect(); + + const browser = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint}); + const pages = await browser.pages(); + const restoredPage = pages.find(page => page.url() === server.PREFIX + '/frames/nested-frames.html'); + expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([ + 'http://localhost:/frames/nested-frames.html', + ' http://localhost:/frames/two-frames.html (2frames)', + ' http://localhost:/frames/frame.html (uno)', + ' http://localhost:/frames/frame.html (dos)', + ' http://localhost:/frames/frame.html (aframe)', + ]); + expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56); + await browser.close(); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/4197#issuecomment-481793410 + it('should be able to connect to the same page simultaneously', async({server}) => { + const browserOne = await playwright.launch(defaultBrowserOptions); + const browserTwo = await playwright.connect({ ...defaultBrowserOptions, browserWSEndpoint: browserOne.wsEndpoint() }); + const [page1, page2] = await Promise.all([ + new Promise(x => browserOne.once('targetcreated', target => x(target.page()))), + browserTwo.newPage(), + ]); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + expect(await page2.evaluate(() => 7 * 6)).toBe(42); + await browserOne.close(); + }); + + }); + describe('Playwright.executablePath', function() { + it('should work', async({server}) => { + const executablePath = playwright.executablePath(); + expect(fs.existsSync(executablePath)).toBe(true); + expect(fs.realpathSync(executablePath)).toBe(executablePath); + }); + }); + }); + + describe.skip(WEBKIT)('Top-level requires', function() { + it('should require top-level Errors', async() => { + const Errors = require(path.join(utils.projectRoot(), '/Errors')); + expect(Errors.TimeoutError).toBe(playwright.errors.TimeoutError); + }); + it('should require top-level DeviceDescriptors', async() => { + const Devices = require(path.join(utils.projectRoot(), '/DeviceDescriptors')); + expect(Devices['iPhone 6']).toBe(playwright.devices['iPhone 6']); + }); + }); + + describe.skip(WEBKIT)('Browser target events', function() { + it('should work', async({server}) => { + const browser = await playwright.launch(defaultBrowserOptions); + const events = []; + browser.on('targetcreated', () => events.push('CREATED')); + browser.on('targetchanged', () => events.push('CHANGED')); + browser.on('targetdestroyed', () => events.push('DESTROYED')); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); + await browser.close(); + }); + }); + + describe.skip(WEBKIT)('Browser.Events.disconnected', function() { + it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const remoteBrowser1 = await playwright.connect({browserWSEndpoint}); + const remoteBrowser2 = await playwright.connect({browserWSEndpoint}); + + let disconnectedOriginal = 0; + let disconnectedRemote1 = 0; + let disconnectedRemote2 = 0; + originalBrowser.on('disconnected', () => ++disconnectedOriginal); + remoteBrowser1.on('disconnected', () => ++disconnectedRemote1); + remoteBrowser2.on('disconnected', () => ++disconnectedRemote2); + + await Promise.all([ + utils.waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), + ]); + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + utils.waitEvent(remoteBrowser1, 'disconnected'), + utils.waitEvent(originalBrowser, 'disconnected'), + originalBrowser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + }); + }); + +}; diff --git a/test/mouse.spec.js b/test/mouse.spec.js new file mode 100644 index 0000000000..92a0aef73a --- /dev/null +++ b/test/mouse.spec.js @@ -0,0 +1,162 @@ +/** + * 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 os = require('os'); + +function dimensions() { + const rect = document.querySelector('textarea').getBoundingClientRect(); + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height + }; +} + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Mouse', function() { + it('should click the document', async({page, server}) => { + await page.evaluate(() => { + window.clickPromise = new Promise(resolve => { + document.addEventListener('click', event => { + resolve({ + type: event.type, + detail: event.detail, + clientX: event.clientX, + clientY: event.clientY, + isTrusted: event.isTrusted, + button: event.button + }); + }); + }); + }); + await page.mouse.click(50, 60); + const event = await page.evaluate(() => window.clickPromise); + expect(event.type).toBe('click'); + expect(event.detail).toBe(1); + expect(event.clientX).toBe(50); + expect(event.clientY).toBe(60); + expect(event.isTrusted).toBe(true); + expect(event.button).toBe(0); + }); + it('should resize the textarea', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + const {x, y, width, height} = await page.evaluate(dimensions); + const mouse = page.mouse; + // The test becomes flaky on WebKit without next line. + if (WEBKIT) + await page.evaluate(() => new Promise(requestAnimationFrame)); + await mouse.move(x + width - 4, y + height - 4); + await mouse.down(); + await mouse.move(x + width + 100, y + height + 100); + await mouse.up(); + const newDimensions = await page.evaluate(dimensions); + expect(newDimensions.width).toBe(Math.round(width + 104)); + expect(newDimensions.height).toBe(Math.round(height + 104)); + }); + it('should select the text with mouse', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = 'This is the text that we are going to try to select. Let\'s see how it goes.'; + await page.keyboard.type(text); + // Firefox needs an extra frame here after typing or it will fail to set the scrollTop + await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.evaluate(() => document.querySelector('textarea').scrollTop = 0); + const {x, y} = await page.evaluate(dimensions); + await page.mouse.move(x + 2,y + 2); + await page.mouse.down(); + await page.mouse.move(200,200); + await page.mouse.up(); + expect(await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); + })).toBe(text); + }); + it('should trigger hover state', async({page, server}) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.hover('#button-6'); + expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6'); + await page.hover('#button-2'); + expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-2'); + await page.hover('#button-91'); + expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-91'); + }); + it.skip(FFOX || WEBKIT)('should trigger hover state with removed window.Node', async({page, server}) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => delete window.Node); + await page.hover('#button-6'); + expect(await page.evaluate(() => document.querySelector('button:hover').id)).toBe('button-6'); + }); + it('should set modifier keys on click', async({page, server}) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => document.querySelector('#button-3').addEventListener('mousedown', e => window.lastEvent = e, true)); + const modifiers = {'Shift': 'shiftKey', 'Control': 'ctrlKey', 'Alt': 'altKey', 'Meta': 'metaKey'}; + // In Firefox, the Meta modifier only exists on Mac + if (FFOX && os.platform() !== 'darwin') + delete modifiers['Meta']; + for (const modifier in modifiers) { + await page.keyboard.down(modifier); + await page.click('#button-3'); + if (!(await page.evaluate(mod => window.lastEvent[mod], modifiers[modifier]))) + throw new Error(modifiers[modifier] + ' should be true'); + await page.keyboard.up(modifier); + } + await page.click('#button-3'); + for (const modifier in modifiers) { + if ((await page.evaluate(mod => window.lastEvent[mod], modifiers[modifier]))) + throw new Error(modifiers[modifier] + ' should be false'); + } + }); + it('should tween mouse movement', async({page, server}) => { + // The test becomes flaky on WebKit without next line. + if (WEBKIT) + await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.mouse.move(100, 100); + await page.evaluate(() => { + window.result = []; + document.addEventListener('mousemove', event => { + window.result.push([event.clientX, event.clientY]); + }); + }); + await page.mouse.move(200, 300, {steps: 5}); + expect(await page.evaluate('result')).toEqual([ + [120, 140], + [140, 180], + [160, 220], + [180, 260], + [200, 300] + ]); + }); + // @see https://crbug.com/929806 + it.skip(WEBKIT)('should work with mobile viewports and cross process navigations', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setViewport({width: 360, height: 640, isMobile: true}); + await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html'); + await page.evaluate(() => { + document.addEventListener('click', event => { + window.result = {x: event.clientX, y: event.clientY}; + }); + }); + + await page.mouse.click(30, 40); + + expect(await page.evaluate('result')).toEqual({x: 30, y: 40}); + }); + }); +}; diff --git a/test/navigation.spec.js b/test/navigation.spec.js new file mode 100644 index 0000000000..5af9c269c8 --- /dev/null +++ b/test/navigation.spec.js @@ -0,0 +1,574 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.goto', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it.skip(WEBKIT)('should work with anchor navigation', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goto(server.EMPTY_PAGE + '#foo'); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + await page.goto(server.EMPTY_PAGE + '#bar'); + expect(page.url()).toBe(server.EMPTY_PAGE + '#bar'); + }); + it('should work with redirects', async({page, server}) => { + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + await page.goto(server.PREFIX + '/redirect/1.html'); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it('should navigate to about:blank', async({page, server}) => { + const response = await page.goto('about:blank'); + expect(response).toBe(null); + }); + it('should return response when page changes its URL after load', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/historyapi.html'); + expect(response.status()).toBe(200); + }); + it('should work with subframes return 204', async({page, server}) => { + server.setRoute('/frames/frame.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + await page.goto(server.PREFIX + '/frames/one-frame.html'); + }); + it.skip(WEBKIT)('should fail when server returns 204', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + let error = null; + await page.goto(server.EMPTY_PAGE).catch(e => error = e); + expect(error).not.toBe(null); + if (CHROME || WEBKIT) + expect(error.message).toContain('net::ERR_ABORTED'); + else + expect(error.message).toContain('NS_BINDING_ABORTED'); + }); + it('should navigate to empty page with domcontentloaded', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'}); + expect(response.status()).toBe(200); + }); + it.skip(WEBKIT)('should work when page calls history API in beforeunload', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + window.addEventListener('beforeunload', () => history.replaceState(null, 'initial', window.location.href), false); + }); + const response = await page.goto(server.PREFIX + '/grid.html'); + expect(response.status()).toBe(200); + }); + it.skip(FFOX || WEBKIT)('should navigate to empty page with networkidle0', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle0'}); + expect(response.status()).toBe(200); + }); + it.skip(FFOX || WEBKIT)('should navigate to empty page with networkidle2', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle2'}); + expect(response.status()).toBe(200); + }); + it.skip(WEBKIT)('should fail when navigating to bad url', async({page, server}) => { + let error = null; + await page.goto('asdfasdf').catch(e => error = e); + if (CHROME || WEBKIT) + expect(error.message).toContain('Cannot navigate to invalid URL'); + else + expect(error.message).toContain('Invalid url'); + }); + // FIXME: shows dialog in WebKit. + it.skip(WEBKIT)('should fail when navigating to bad SSL', async({page, httpsServer}) => { + // Make sure that network events do not emit 'undefined'. + // @see https://crbug.com/750469 + page.on('request', request => expect(request).toBeTruthy()); + page.on('requestfinished', request => expect(request).toBeTruthy()); + page.on('requestfailed', request => expect(request).toBeTruthy()); + let error = null; + await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); + if (CHROME || WEBKIT) + expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID'); + else + expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + }); + // FIXME: shows dialog in WebKit. + it.skip(WEBKIT)('should fail when navigating to bad SSL after redirects', async({page, server, httpsServer}) => { + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + let error = null; + await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(e => error = e); + if (CHROME || WEBKIT) + expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID'); + else + expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + }); + it.skip(FFOX || WEBKIT)('should throw if networkidle is passed as an option', async({page, server}) => { + let error = null; + await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle'}).catch(err => error = err); + expect(error.message).toContain('"networkidle" option is no longer supported'); + }); + it.skip(WEBKIT)('should fail when main resources failed to load', async({page, server}) => { + let error = null; + await page.goto('http://localhost:44123/non-existing-url').catch(e => error = e); + if (CHROME || WEBKIT) + expect(error.message).toContain('net::ERR_CONNECTION_REFUSED'); + else + expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED'); + }); + it('should fail when exceeding maximum navigation timeout', async({page, server}) => { + // Hang for request to the empty.html + server.setRoute('/empty.html', (req, res) => { }); + let error = null; + await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(e => error = e); + const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + expect(error.message).toContain(message); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should fail when exceeding default maximum navigation timeout', async({page, server}) => { + // Hang for request to the empty.html + server.setRoute('/empty.html', (req, res) => { }); + let error = null; + page.setDefaultNavigationTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); + const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + expect(error.message).toContain(message); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should fail when exceeding default maximum timeout', async({page, server}) => { + // Hang for request to the empty.html + server.setRoute('/empty.html', (req, res) => { }); + let error = null; + page.setDefaultTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); + const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + expect(error.message).toContain(message); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should prioritize default navigation timeout over default timeout', async({page, server}) => { + // Hang for request to the empty.html + server.setRoute('/empty.html', (req, res) => { }); + let error = null; + page.setDefaultTimeout(0); + page.setDefaultNavigationTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); + const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + expect(error.message).toContain(message); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should disable timeout when its set to 0', async({page, server}) => { + let error = null; + let loaded = false; + page.once('load', () => loaded = true); + await page.goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']}).catch(e => error = e); + expect(error).toBe(null); + expect(loaded).toBe(true); + }); + it('should work when navigating to valid url', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + it.skip(FFOX)('should work when navigating to data url', async({page, server}) => { + const response = await page.goto('data:text/html,hello'); + expect(response.ok()).toBe(true); + }); + it('should work when navigating to 404', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/not-found'); + expect(response.ok()).toBe(false); + expect(response.status()).toBe(404); + }); + it('should return last response in redirect chain', async({page, server}) => { + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/redirect/3.html'); + server.setRedirect('/redirect/3.html', server.EMPTY_PAGE); + const response = await page.goto(server.PREFIX + '/redirect/1.html'); + expect(response.ok()).toBe(true); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it.skip(FFOX || WEBKIT)('should wait for network idle to succeed navigation', async({page, server}) => { + let responses = []; + // Hold on to a bunch of requests without answering. + server.setRoute('/fetch-request-a.js', (req, res) => responses.push(res)); + server.setRoute('/fetch-request-b.js', (req, res) => responses.push(res)); + server.setRoute('/fetch-request-c.js', (req, res) => responses.push(res)); + server.setRoute('/fetch-request-d.js', (req, res) => responses.push(res)); + const initialFetchResourcesRequested = Promise.all([ + server.waitForRequest('/fetch-request-a.js'), + server.waitForRequest('/fetch-request-b.js'), + server.waitForRequest('/fetch-request-c.js'), + ]); + const secondFetchResourceRequested = server.waitForRequest('/fetch-request-d.js'); + + // Navigate to a page which loads immediately and then does a bunch of + // requests via javascript's fetch method. + const navigationPromise = page.goto(server.PREFIX + '/networkidle.html', { + waitUntil: 'networkidle0', + }); + // Track when the navigation gets completed. + let navigationFinished = false; + navigationPromise.then(() => navigationFinished = true); + + // Wait for the page's 'load' event. + await new Promise(fulfill => page.once('load', fulfill)); + expect(navigationFinished).toBe(false); + + // Wait for the initial three resources to be requested. + await initialFetchResourcesRequested; + + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to initial requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + // Reset responses array + responses = []; + + // Wait for the second round to be requested. + await secondFetchResourceRequested; + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + const response = await navigationPromise; + // Expect navigation to succeed. + expect(response.ok()).toBe(true); + }); + it('should not leak listeners during navigation', async({page, server}) => { + let warning = null; + const warningHandler = w => warning = w; + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) + await page.goto(server.EMPTY_PAGE); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it.skip(WEBKIT)('should not leak listeners during bad navigation', async({page, server}) => { + let warning = null; + const warningHandler = w => warning = w; + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) + await page.goto('asdf').catch(e => {/* swallow navigation error */}); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should not leak listeners during navigation of 11 pages', async({page, context, server}) => { + let warning = null; + const warningHandler = w => warning = w; + process.on('warning', warningHandler); + await Promise.all([...Array(20)].map(async() => { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + })); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it.skip(FFOX)('should navigate to dataURL and fire dataURL requests', async({page, server}) => { + const requests = []; + page.on('request', request => !utils.isFavicon(request) && requests.push(request)); + const dataURL = 'data:text/html,
yo
'; + const response = await page.goto(dataURL); + expect(response.status()).toBe(200); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + }); + it.skip(FFOX || WEBKIT)('should navigate to URL with hash and fire requests without hash', async({page, server}) => { + const requests = []; + page.on('request', request => !utils.isFavicon(request) && requests.push(request)); + const response = await page.goto(server.EMPTY_PAGE + '#hash'); + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + }); + it.skip(WEBKIT)('should work with self requesting page', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/self-request.html'); + expect(response.status()).toBe(200); + expect(response.url()).toContain('self-request.html'); + }); + it.skip(WEBKIT)('should fail when navigating and show the url at the error message', async function({page, server, httpsServer}) { + const url = httpsServer.PREFIX + '/redirect/1.html'; + let error = null; + try { + await page.goto(url); + } catch (e) { + error = e; + } + expect(error.message).toContain(url); + }); + it.skip(WEBKIT)('should send referer', async({page, server}) => { + const [request1, request2] = await Promise.all([ + server.waitForRequest('/grid.html'), + server.waitForRequest('/digits/1.png'), + page.goto(server.PREFIX + '/grid.html', { + referer: 'http://google.com/', + }), + ]); + expect(request1.headers['referer']).toBe('http://google.com/'); + // Make sure subresources do not inherit referer. + expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + }); + }); + + describe('Page.waitForNavigation', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.evaluate(url => window.location.href = url, server.PREFIX + '/grid.html') + ]); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('grid.html'); + }); + it.skip(WEBKIT)('should work with both domcontentloaded and load', async({page, server}) => { + let response = null; + server.setRoute('/one-style.css', (req, res) => response = res); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html'); + const domContentLoadedPromise = page.waitForNavigation({ + waitUntil: 'domcontentloaded' + }); + + let bothFired = false; + const bothFiredPromise = page.waitForNavigation({ + waitUntil: ['load', 'domcontentloaded'] + }).then(() => bothFired = true); + + await server.waitForRequest('/one-style.css'); + await domContentLoadedPromise; + expect(bothFired).toBe(false); + response.end(); + await bothFiredPromise; + await navigationPromise; + }); + it('should work with clicking on anchor links', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(`foobar`); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar'); + }); + it('should work with history.pushState()', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + SPA + + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/wow.html'); + }); + it('should work with history.replaceState()', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + SPA + + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/replaced.html'); + }); + it.skip(FFOX)('should work with DOM history.back()/history.forward()', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + back + forward + + `); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + const [backResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#back'), + ]); + expect(backResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + const [forwardResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#forward'), + ]); + expect(forwardResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + }); + it.skip(WEBKIT)('should work when subframe issues window.stop()', async({page, server}) => { + server.setRoute('/frames/style.css', (req, res) => {}); + const navigationPromise = page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = await utils.waitEvent(page, 'frameattached'); + await new Promise(fulfill => { + page.on('framenavigated', f => { + if (f === frame) + fulfill(); + }); + }); + await Promise.all([ + frame.evaluate(() => window.stop()), + navigationPromise + ]); + }); + }); + + describe('Page.goBack', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/grid.html'); + + let response = await page.goBack(); + expect(response.ok()).toBe(true); + expect(response.url()).toContain(server.EMPTY_PAGE); + + response = await page.goForward(); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('/grid.html'); + + response = await page.goForward(); + expect(response).toBe(null); + }); + it.skip(WEBKIT)('should work with HistoryAPI', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + history.pushState({}, '', '/first.html'); + history.pushState({}, '', '/second.html'); + }); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + + await page.goBack(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + await page.goBack(); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goForward(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + }); + }); + + describe('Frame.goto', function() { + it.skip(WEBKIT)('should navigate subframes', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames()[0].url()).toContain('/frames/one-frame.html'); + expect(page.frames()[1].url()).toContain('/frames/frame.html'); + + const response = await page.frames()[1].goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + expect(response.frame()).toBe(page.frames()[1]); + }); + it.skip(WEBKIT)('should reject when frame detaches', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/one-frame.html'); + + server.setRoute('/empty.html', () => {}); + const navigationPromise = page.frames()[1].goto(server.EMPTY_PAGE).catch(e => e); + await server.waitForRequest('/empty.html'); + + await page.$eval('iframe', frame => frame.remove()); + const error = await navigationPromise; + expect(error.message).toBe('Navigating frame was detached'); + }); + it.skip(WEBKIT)('should return matching responses', async({page, server}) => { + // Disable cache: otherwise, chromium will cache similar requests. + await page.setCacheEnabled(false); + await page.goto(server.EMPTY_PAGE); + // Attach three frames. + const frames = await Promise.all([ + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame2', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame3', server.EMPTY_PAGE), + ]); + // Navigate all frames to the same URL. + const serverResponses = []; + server.setRoute('/one-style.html', (req, res) => serverResponses.push(res)); + const navigations = []; + for (let i = 0; i < 3; ++i) { + navigations.push(frames[i].goto(server.PREFIX + '/one-style.html')); + await server.waitForRequest('/one-style.html'); + } + // Respond from server out-of-order. + const serverResponseTexts = ['AAA', 'BBB', 'CCC']; + for (const i of [1, 2, 0]) { + serverResponses[i].end(serverResponseTexts[i]); + const response = await navigations[i]; + expect(response.frame()).toBe(frames[i]); + expect(await response.text()).toBe(serverResponseTexts[i]); + } + }); + }); + + describe('Frame.waitForNavigation', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]; + const [response] = await Promise.all([ + frame.waitForNavigation(), + frame.evaluate(url => window.location.href = url, server.PREFIX + '/grid.html') + ]); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('grid.html'); + expect(response.frame()).toBe(frame); + expect(page.url()).toContain('/frames/one-frame.html'); + }); + it.skip(WEBKIT)('should fail when frame detaches', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]; + + server.setRoute('/empty.html', () => {}); + let error = null; + const navigationPromise = frame.waitForNavigation().catch(e => error = e); + await Promise.all([ + server.waitForRequest('/empty.html'), + frame.evaluate(() => window.location = '/empty.html') + ]); + await page.$eval('iframe', frame => frame.remove()); + await navigationPromise; + expect(error.message).toBe('Navigating frame was detached'); + }); + }); + + describe('Page.reload', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => window._foo = 10); + await page.reload(); + expect(await page.evaluate(() => window._foo)).toBe(undefined); + }); + }); +}; diff --git a/test/network.spec.js b/test/network.spec.js new file mode 100644 index 0000000000..36f8eb44d8 --- /dev/null +++ b/test/network.spec.js @@ -0,0 +1,445 @@ +/** + * 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 fs = require('fs'); +const path = require('path'); +const utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.Events.Request', function() { + it('should fire for navigation requests', async({page, server}) => { + const requests = []; + page.on('request', request => !utils.isFavicon(request) && requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + }); + it('should fire for iframes', async({page, server}) => { + const requests = []; + page.on('request', request => !utils.isFavicon(request) && requests.push(request)); + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(2); + }); + it('should fire for fetches', async({page, server}) => { + const requests = []; + page.on('request', request => !utils.isFavicon(request) && requests.push(request)); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => fetch('/empty.html')); + expect(requests.length).toBe(2); + }); + }); + + describe('Request.frame', function() { + it('should work for main frame navigation request', async({page, server}) => { + const requests = []; + page.on('request', request => !utils.isFavicon(request) && requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.mainFrame()); + }); + // FIXME: needs frameAttached event, otherwise it introduces too many hacks in the lib. + it.skip(WEBKIT)('should work for subframe navigation request', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const requests = []; + page.on('request', request => !utils.isFavicon(request) && requests.push(request)); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.frames()[1]); + }); + it('should work for fetch requests', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + let requests = []; + page.on('request', request => !utils.isFavicon(request) && requests.push(request)); + await page.evaluate(() => fetch('/digits/1.png')); + requests = requests.filter(request => !request.url().includes('favicon')); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.mainFrame()); + }); + }); + + describe('Request.headers', function() { + it('should work', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + if (CHROME) + expect(response.request().headers()['user-agent']).toContain('Chrome'); + else if (FFOX) + expect(response.request().headers()['user-agent']).toContain('Firefox'); + else if (WEBKIT) + expect(response.request().headers()['user-agent']).toContain('WebKit'); + }); + }); + + describe('Response.headers', function() { + it('should work', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('foo', 'bar'); + res.end(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.headers()['foo']).toBe('bar'); + }); + }); + + describe.skip(FFOX)('Response.fromCache', function() { + it.skip(WEBKIT)('should return |false| for non-cached content', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.fromCache()).toBe(false); + }); + + it.skip(WEBKIT)('should work', async({page, server}) => { + const responses = new Map(); + page.on('response', r => !utils.isFavicon(r.request()) && responses.set(r.url().split('/').pop(), r)); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('one-style.css').status()).toBe(200); + expect(responses.get('one-style.css').fromCache()).toBe(true); + expect(responses.get('one-style.html').status()).toBe(304); + expect(responses.get('one-style.html').fromCache()).toBe(false); + }); + }); + + describe.skip(FFOX)('Response.fromServiceWorker', function() { + it.skip(WEBKIT)('should return |false| for non-service-worker content', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.fromServiceWorker()).toBe(false); + }); + + // FIXME: WebKit responses contain sw.js + it.skip(WEBKIT)('Response.fromServiceWorker', async({page, server}) => { + const responses = new Map(); + page.on('response', r => responses.set(r.url().split('/').pop(), r)); + + // Load and re-load to make sure serviceworker is installed and running. + await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {waitUntil: 'networkidle2'}); + await page.evaluate(async() => await window.activationPromise); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('sw.html').status()).toBe(200); + expect(responses.get('sw.html').fromServiceWorker()).toBe(true); + expect(responses.get('style.css').status()).toBe(200); + expect(responses.get('style.css').fromServiceWorker()).toBe(true); + }); + }); + + describe('Request.postData', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (req, res) => res.end()); + let request = null; + page.on('request', r => request = r); + await page.evaluate(() => fetch('./post', { method: 'POST', body: JSON.stringify({foo: 'bar'})})); + expect(request).toBeTruthy(); + expect(request.postData()).toBe('{"foo":"bar"}'); + }); + it('should be |undefined| when there is no post data', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.request().postData()).toBe(undefined); + }); + }); + + describe('Response.text', function() { + it('should work', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(await response.text()).toBe('{"foo": "bar"}\n'); + }); + it('should return uncompressed text', async({page, server}) => { + server.enableGzip('/simple.json'); + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(response.headers()['content-encoding']).toBe('gzip'); + expect(await response.text()).toBe('{"foo": "bar"}\n'); + }); + it('should throw when requesting body of redirected response', async({page, server}) => { + server.setRedirect('/foo.html', '/empty.html'); + const response = await page.goto(server.PREFIX + '/foo.html'); + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(1); + const redirected = redirectChain[0].response(); + expect(redirected.status()).toBe(302); + let error = null; + await redirected.text().catch(e => error = e); + expect(error.message).toContain('Response body is unavailable for redirect responses'); + }); + it('should wait until response completes', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + // Setup server to trap request. + let serverResponse = null; + server.setRoute('/get', (req, res) => { + serverResponse = res; + // In Firefox, |fetch| will be hanging until it receives |Content-Type| header + // from server. + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.write('hello '); + }); + // Setup page to trap response. + let requestFinished = false; + page.on('requestfinished', r => requestFinished = requestFinished || r.url().includes('/get')); + // send request and wait for server response + const [pageResponse] = await Promise.all([ + page.waitForResponse(r => !utils.isFavicon(r.request())), + page.evaluate(() => fetch('./get', { method: 'GET'})), + server.waitForRequest('/get'), + ]); + + expect(serverResponse).toBeTruthy(); + expect(pageResponse).toBeTruthy(); + expect(pageResponse.status()).toBe(200); + expect(requestFinished).toBe(false); + + const responseText = pageResponse.text(); + // Write part of the response and wait for it to be flushed. + await new Promise(x => serverResponse.write('wor', x)); + // Finish response. + await new Promise(x => serverResponse.end('ld!', x)); + expect(await responseText).toBe('hello world!'); + }); + }); + + describe('Response.json', function() { + it('should work', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(await response.json()).toEqual({foo: 'bar'}); + }); + }); + + describe('Response.buffer', function() { + it('should work', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/pptr.png'); + const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png')); + const responseBuffer = await response.buffer(); + expect(responseBuffer.equals(imageBuffer)).toBe(true); + }); + it('should work with compression', async({page, server}) => { + server.enableGzip('/pptr.png'); + const response = await page.goto(server.PREFIX + '/pptr.png'); + const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png')); + const responseBuffer = await response.buffer(); + expect(responseBuffer.equals(imageBuffer)).toBe(true); + }); + }); + + describe('Response.statusText', function() { + it('should work', async({page, server}) => { + server.setRoute('/cool', (req, res) => { + res.writeHead(200, 'cool!'); + res.end(); + }); + const response = await page.goto(server.PREFIX + '/cool'); + expect(response.statusText()).toBe('cool!'); + }); + }); + + describe('Network Events', function() { + it('Page.Events.Request', async({page, server}) => { + const requests = []; + page.on('request', request => requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + expect(requests[0].resourceType()).toBe('document'); + expect(requests[0].method()).toBe('GET'); + expect(requests[0].response()).toBeTruthy(); + expect(requests[0].frame() === page.mainFrame()).toBe(true); + expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE); + }); + // FIXME: WebKit doesn't provide remoteIPAddress in the response. + it.skip(WEBKIT)('Page.Events.Response', async({page, server}) => { + const responses = []; + page.on('response', response => responses.push(response)); + await page.goto(server.EMPTY_PAGE); + expect(responses.length).toBe(1); + expect(responses[0].url()).toBe(server.EMPTY_PAGE); + expect(responses[0].status()).toBe(200); + expect(responses[0].ok()).toBe(true); + expect(responses[0].request()).toBeTruthy(); + const remoteAddress = responses[0].remoteAddress(); + // Either IPv6 or IPv4, depending on environment. + expect(remoteAddress.ip.includes('::1') || remoteAddress.ip === '127.0.0.1').toBe(true); + expect(remoteAddress.port).toBe(server.PORT); + }); + + // FIXME: requires request interception. + it.skip(WEBKIT)('Page.Events.RequestFailed', async({page, server}) => { + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('css')) + request.abort(); + else + request.continue(); + }); + const failedRequests = []; + page.on('requestfailed', request => failedRequests.push(request)); + await page.goto(server.PREFIX + '/one-style.html'); + expect(failedRequests.length).toBe(1); + expect(failedRequests[0].url()).toContain('one-style.css'); + expect(failedRequests[0].response()).toBe(null); + expect(failedRequests[0].resourceType()).toBe('stylesheet'); + if (CHROME || WEBKIT) + expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED'); + else + expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE'); + expect(failedRequests[0].frame()).toBeTruthy(); + }); + // FIXME: WebKit requestfinished comes after goto. + it.skip(WEBKIT)('Page.Events.RequestFinished', async({page, server}) => { + const requests = []; + page.on('requestfinished', request => requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + expect(requests[0].response()).toBeTruthy(); + expect(requests[0].frame() === page.mainFrame()).toBe(true); + expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE); + }); + it.skip(WEBKIT)('should fire events in proper order', async({page, server}) => { + const events = []; + page.on('request', request => events.push('request')); + page.on('response', response => events.push('response')); + page.on('requestfinished', request => events.push('requestfinished')); + await page.goto(server.EMPTY_PAGE); + expect(events).toEqual(['request', 'response', 'requestfinished']); + }); + it.skip(WEBKIT)('should support redirects', async({page, server}) => { + const events = []; + page.on('request', request => events.push(`${request.method()} ${request.url()}`)); + page.on('response', response => events.push(`${response.status()} ${response.url()}`)); + page.on('requestfinished', request => events.push(`DONE ${request.url()}`)); + page.on('requestfailed', request => events.push(`FAIL ${request.url()}`)); + server.setRedirect('/foo.html', '/empty.html'); + const FOO_URL = server.PREFIX + '/foo.html'; + const response = await page.goto(FOO_URL); + expect(events).toEqual([ + `GET ${FOO_URL}`, + `302 ${FOO_URL}`, + `DONE ${FOO_URL}`, + `GET ${server.EMPTY_PAGE}`, + `200 ${server.EMPTY_PAGE}`, + `DONE ${server.EMPTY_PAGE}` + ]); + + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(1); + expect(redirectChain[0].url()).toContain('/foo.html'); + expect(redirectChain[0].response().remoteAddress().port).toBe(server.PORT); + }); + }); + + describe('Request.isNavigationRequest', () => { + it('should work', async({page, server}) => { + const requests = new Map(); + page.on('request', request => requests.set(request.url().split('/').pop(), request)); + server.setRedirect('/rrredirect', '/frames/one-frame.html'); + await page.goto(server.PREFIX + '/rrredirect'); + expect(requests.get('rrredirect').isNavigationRequest()).toBe(true); + expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('script.js').isNavigationRequest()).toBe(false); + expect(requests.get('style.css').isNavigationRequest()).toBe(false); + }); + it.skip(WEBKIT)('should work with request interception', async({page, server}) => { + const requests = new Map(); + page.on('request', request => { + requests.set(request.url().split('/').pop(), request); + request.continue(); + }); + await page.setRequestInterception(true); + server.setRedirect('/rrredirect', '/frames/one-frame.html'); + await page.goto(server.PREFIX + '/rrredirect'); + expect(requests.get('rrredirect').isNavigationRequest()).toBe(true); + expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('script.js').isNavigationRequest()).toBe(false); + expect(requests.get('style.css').isNavigationRequest()).toBe(false); + }); + it('should work when navigating to image', async({page, server}) => { + const requests = []; + page.on('request', request => requests.push(request)); + await page.goto(server.PREFIX + '/pptr.png'); + expect(requests[0].isNavigationRequest()).toBe(true); + }); + }); + + describe('Page.setExtraHTTPHeaders', function() { + it('should work', async({page, server}) => { + await page.setExtraHTTPHeaders({ + foo: 'bar' + }); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should throw for non-string header values', async({page, server}) => { + let error = null; + try { + await page.setExtraHTTPHeaders({ 'foo': 1 }); + } catch (e) { + error = e; + } + expect(error.message).toBe('Expected value of header "foo" to be String, but "number" is found.'); + }); + }); + + // FIXME: WebKit doesn't support network interception. + describe.skip(FFOX || WEBKIT)('Page.authenticate', function() { + it('should work', async({page, server}) => { + server.setAuth('/empty.html', 'user', 'pass'); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + await page.authenticate({ + username: 'user', + password: 'pass' + }); + response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should fail if wrong credentials', async({page, server}) => { + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/empty.html', 'user2', 'pass2'); + await page.authenticate({ + username: 'foo', + password: 'bar' + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + }); + it('should allow disable authentication', async({page, server}) => { + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/empty.html', 'user3', 'pass3'); + await page.authenticate({ + username: 'user3', + password: 'pass3' + }); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + await page.authenticate(null); + // Navigate to a different origin to bust Chrome's credential caching. + response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response.status()).toBe(401); + }); + }); +}; + diff --git a/test/oopif.spec.js b/test/oopif.spec.js new file mode 100644 index 0000000000..0bc5496799 --- /dev/null +++ b/test/oopif.spec.js @@ -0,0 +1,61 @@ +/** + * 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.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('OOPIF', function() { + beforeAll(async function(state) { + state.browser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat(['--site-per-process']), + })); + }); + beforeEach(async function(state) { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + afterEach(async function(state) { + await state.context.close(); + state.page = null; + state.context = null; + }); + afterAll(async function(state) { + await state.browser.close(); + state.browser = null; + }); + xit('should report oopif frames', async function({page, server, context}) { + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + expect(oopifs(context).length).toBe(1); + expect(page.frames().length).toBe(2); + }); + it('should load oopif iframes with subresources and request interception', async function({page, server, context}) { + await page.setRequestInterception(true); + page.on('request', request => request.continue()); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + expect(oopifs(context).length).toBe(1); + }); + }); +}; + + +/** + * @param {!Playwright.BrowserContext} context + */ +function oopifs(context) { + return context.targets().filter(target => target._targetInfo.type === 'iframe'); +} diff --git a/test/page.spec.js b/test/page.spec.js new file mode 100644 index 0000000000..ec9de5a6d9 --- /dev/null +++ b/test/page.spec.js @@ -0,0 +1,1265 @@ +/** + * 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 fs = require('fs'); +const path = require('path'); +const utils = require('./utils'); +const {waitEvent} = utils; + +module.exports.addTests = function({testRunner, expect, headless, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.close', function() { + it('should reject all promises when page is closed', async({context}) => { + const newPage = await context.newPage(); + let error = null; + await Promise.all([ + newPage.evaluate(() => new Promise(r => {})).catch(e => error = e), + newPage.close(), + ]); + expect(error.message).toContain('Protocol error'); + }); + it('should not be visible in browser.pages', async({browser}) => { + const newPage = await browser.newPage(); + expect(await browser.pages()).toContain(newPage); + await newPage.close(); + expect(await browser.pages()).not.toContain(newPage); + }); + it.skip(WEBKIT)('should run beforeunload if asked for', async({context, server}) => { + const newPage = await context.newPage(); + await newPage.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await newPage.click('body'); + const pageClosingPromise = newPage.close({ runBeforeUnload: true }); + const dialog = await waitEvent(newPage, 'dialog'); + expect(dialog.type()).toBe('beforeunload'); + expect(dialog.defaultValue()).toBe(''); + if (CHROME || WEBKIT) + expect(dialog.message()).toBe(''); + else + expect(dialog.message()).toBe('This page is asking you to confirm that you want to leave - data you have entered may not be saved.'); + await dialog.accept(); + await pageClosingPromise; + }); + it.skip(WEBKIT)('should *not* run beforeunload by default', async({context, server}) => { + const newPage = await context.newPage(); + await newPage.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await newPage.click('body'); + await newPage.close(); + }); + it('should set the page close state', async({context}) => { + const newPage = await context.newPage(); + expect(newPage.isClosed()).toBe(false); + await newPage.close(); + expect(newPage.isClosed()).toBe(true); + }); + it.skip(FFOX || WEBKIT)('should terminate network waiters', async({context, server}) => { + const newPage = await context.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e), + newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e), + newPage.close() + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + }); + }); + + describe('Page.Events.Load', function() { + it('should fire when expected', async({page, server}) => { + await Promise.all([ + page.goto('about:blank'), + utils.waitEvent(page, 'load'), + ]); + }); + }); + + describe('Async stacks', () => { + it.skip(WEBKIT)('should work', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + let error = null; + await page.goto(server.EMPTY_PAGE).catch(e => error = e); + expect(error).not.toBe(null); + expect(error.stack).toContain(__filename); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.Events.error', function() { + it('should throw when page crashes', async({page}) => { + let error = null; + page.on('error', err => error = err); + page.goto('chrome://crash').catch(e => {}); + await waitEvent(page, 'error'); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describe.skip(WEBKIT)('Page.Events.Popup', function() { + it('should work', async({page}) => { + const [popup] = await Promise.all([ + new Promise(x => page.once('popup', x)), + page.evaluate(() => window.open('about:blank')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + }); + it('should work with noopener', async({page}) => { + const [popup] = await Promise.all([ + new Promise(x => page.once('popup', x)), + page.evaluate(() => window.open('about:blank', null, 'noopener')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + it('should work with clicking target=_blank', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + new Promise(x => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + }); + it('should work with fake-clicking target=_blank and rel=noopener', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + new Promise(x => page.once('popup', x)), + page.$eval('a', a => a.click()), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + it('should work with clicking target=_blank and rel=noopener', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + new Promise(x => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + }); + + // Permissions API is not implemented in WebKit (see https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) + describe('BrowserContext.overridePermissions', function() { + function getPermission(page, name) { + return page.evaluate(name => navigator.permissions.query({name}).then(result => result.state), name); + } + + it.skip(WEBKIT)('should be prompt by default', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + it.skip(WEBKIT)('should deny permission when not listed', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + }); + it.skip(WEBKIT)('should fail when bad permission is given', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + let error = null; + await context.overridePermissions(server.EMPTY_PAGE, ['foo']).catch(e => error = e); + expect(error.message).toBe('Unknown permission: foo'); + }); + it.skip(WEBKIT)('should grant permission when listed', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + }); + it.skip(WEBKIT)('should reset permissions', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + it.skip(WEBKIT)('should trigger permission onchange', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + window.events = []; + return navigator.permissions.query({name: 'geolocation'}).then(function(result) { + window.events.push(result.state); + result.onchange = function() { + window.events.push(result.state); + }; + }); + }); + expect(await page.evaluate(() => window.events)).toEqual(['prompt']); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied']); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied', 'granted']); + await context.clearPermissionOverrides(); + expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied', 'granted', 'prompt']); + }); + it.skip(WEBKIT)('should isolate permissions between browser contexs', async({page, server, context, browser}) => { + await page.goto(server.EMPTY_PAGE); + const otherContext = await browser.createIncognitoBrowserContext(); + const otherPage = await otherContext.newPage(); + await otherPage.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + expect(await getPermission(otherPage, 'geolocation')).toBe('prompt'); + + await context.overridePermissions(server.EMPTY_PAGE, []); + await otherContext.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + + await otherContext.close(); + }); + }); + + // FIXME: not supported in WebKit (as well as Emulation domain in general). + // It was removed from WebKit in https://webkit.org/b/126630 + describe.skip(FFOX || WEBKIT)('Page.setGeolocation', function() { + it('should work', async({page, server, context}) => { + await context.overridePermissions(server.PREFIX, ['geolocation']); + await page.goto(server.EMPTY_PAGE); + await page.setGeolocation({longitude: 10, latitude: 10}); + const geolocation = await page.evaluate(() => new Promise(resolve => navigator.geolocation.getCurrentPosition(position => { + resolve({latitude: position.coords.latitude, longitude: position.coords.longitude}); + }))); + expect(geolocation).toEqual({ + latitude: 10, + longitude: 10 + }); + }); + it('should throw when invalid longitude', async({page, server, context}) => { + let error = null; + try { + await page.setGeolocation({longitude: 200, latitude: 10}); + } catch (e) { + error = e; + } + expect(error.message).toContain('Invalid longitude "200"'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.setOfflineMode', function() { + it('should work', async({page, server}) => { + await page.setOfflineMode(true); + let error = null; + await page.goto(server.EMPTY_PAGE).catch(e => error = e); + expect(error).toBeTruthy(); + await page.setOfflineMode(false); + const response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should emulate navigator.onLine', async({page, server}) => { + expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); + await page.setOfflineMode(true); + expect(await page.evaluate(() => window.navigator.onLine)).toBe(false); + await page.setOfflineMode(false); + expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); + }); + }); + + describe.skip(FFOX || WEBKIT)('ExecutionContext.queryObjects', function() { + it('should work', async({page, server}) => { + // Instantiate an object + await page.evaluate(() => window.set = new Set(['hello', 'world'])); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate(objects => objects.length, objectsHandle); + expect(count).toBe(1); + const values = await page.evaluate(objects => Array.from(objects[0].values()), objectsHandle); + expect(values).toEqual(['hello', 'world']); + }); + it('should work for non-blank page', async({page, server}) => { + // Instantiate an object + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => window.set = new Set(['hello', 'world'])); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate(objects => objects.length, objectsHandle); + expect(count).toBe(1); + }); + it('should fail for disposed handles', async({page, server}) => { + const prototypeHandle = await page.evaluateHandle(() => HTMLBodyElement.prototype); + await prototypeHandle.dispose(); + let error = null; + await page.queryObjects(prototypeHandle).catch(e => error = e); + expect(error.message).toBe('Prototype JSHandle is disposed!'); + }); + it('should fail primitive values as prototypes', async({page, server}) => { + const prototypeHandle = await page.evaluateHandle(() => 42); + let error = null; + await page.queryObjects(prototypeHandle).catch(e => error = e); + expect(error.message).toBe('Prototype JSHandle must not be referencing primitive value'); + }); + }); + + describe('Page.Events.Console', function() { + it('should work', async({page, server}) => { + let message = null; + page.once('console', m => message = m); + await Promise.all([ + page.evaluate(() => console.log('hello', 5, {foo: 'bar'})), + waitEvent(page, 'console') + ]); + expect(message.text()).toEqual('hello 5 JSHandle@object'); + expect(message.type()).toEqual('log'); + expect(await message.args()[0].jsonValue()).toEqual('hello'); + expect(await message.args()[1].jsonValue()).toEqual(5); + expect(await message.args()[2].jsonValue()).toEqual({foo: 'bar'}); + }); + it('should work for different console API calls', async({page, server}) => { + const messages = []; + page.on('console', msg => messages.push(msg)); + // All console events will be reported before `page.evaluate` is finished. + await page.evaluate(() => { + // A pair of time/timeEnd generates only one Console API call. + console.time('calling console.time'); + console.timeEnd('calling console.time'); + console.trace('calling console.trace'); + console.dir('calling console.dir'); + console.warn('calling console.warn'); + console.error('calling console.error'); + console.log(Promise.resolve('should not wait until resolved!')); + }); + expect(messages.map(msg => msg.type())).toEqual([ + 'timeEnd', 'trace', 'dir', 'warning', 'error', 'log' + ]); + expect(messages[0].text()).toContain('calling console.time'); + expect(messages.slice(1).map(msg => msg.text())).toEqual([ + 'calling console.trace', + 'calling console.dir', + 'calling console.warn', + 'calling console.error', + 'JSHandle@promise', + ]); + }); + it('should not fail for window object', async({page, server}) => { + let message = null; + page.once('console', msg => message = msg); + await Promise.all([ + page.evaluate(() => console.error(window)), + waitEvent(page, 'console') + ]); + expect(message.text()).toBe('JSHandle@object'); + }); + it('should trigger correct Log', async({page, server}) => { + await page.goto('about:blank'); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(async url => fetch(url).catch(e => {}), server.EMPTY_PAGE) + ]); + expect(message.text()).toContain('Access-Control-Allow-Origin'); + if (CHROME || WEBKIT) + expect(message.type()).toEqual('error'); + else + expect(message.type()).toEqual('warn'); + }); + it.skip(FFOX || WEBKIT)('should have location when fetch fails', async({page, server}) => { + // The point of this test is to make sure that we report console messages from + // Log domain: https://vanilla.aslushnikov.com/?Log.entryAdded + await page.goto(server.EMPTY_PAGE); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.setContent(``), + ]); + expect(message.text()).toContain(`ERR_NAME_NOT_RESOLVED`); + expect(message.type()).toEqual('error'); + expect(message.location()).toEqual({ + url: 'http://wat/', + lineNumber: undefined + }); + }); + it.skip(WEBKIT)('should have location for console API calls', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.goto(server.PREFIX + '/consolelog.html'), + ]); + expect(message.text()).toBe('yellow'); + expect(message.type()).toBe('log'); + expect(message.location()).toEqual({ + url: server.PREFIX + '/consolelog.html', + lineNumber: 7, + columnNumber: 14, + }); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/3865 + it.skip(FFOX || WEBKIT)('should not throw when there are console messages in detached iframes', async({browser, page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(async() => { + // 1. Create a popup that Playwright is not connected to. + const win = window.open(window.location.href, 'Title', 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=200,top=0,left=0'); + await new Promise(x => win.onload = x); + // 2. In this popup, create an iframe that console.logs a message. + win.document.body.innerHTML = ``; + const frame = win.document.querySelector('iframe'); + await new Promise(x => frame.onload = x); + // 3. After that, remove the iframe. + frame.remove(); + }); + const popupTarget = page.browserContext().targets().find(target => target !== page.target()); + // 4. Connect to the popup and make sure it doesn't throw. + await popupTarget.page(); + }); + }); + + describe('Page.Events.DOMContentLoaded', function() { + it('should fire when expected', async({page, server}) => { + page.goto('about:blank'); + await waitEvent(page, 'domcontentloaded'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.metrics', function() { + it('should get metrics from a page', async({page, server}) => { + await page.goto('about:blank'); + const metrics = await page.metrics(); + checkMetrics(metrics); + }); + it('metrics event fired on console.timeStamp', async({page, server}) => { + const metricsPromise = new Promise(fulfill => page.once('metrics', fulfill)); + await page.evaluate(() => console.timeStamp('test42')); + const metrics = await metricsPromise; + expect(metrics.title).toBe('test42'); + checkMetrics(metrics.metrics); + }); + function checkMetrics(metrics) { + const metricsToCheck = new Set([ + 'Timestamp', + 'Documents', + 'Frames', + 'JSEventListeners', + 'Nodes', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'TaskDuration', + 'JSHeapUsedSize', + 'JSHeapTotalSize', + ]); + for (const name in metrics) { + expect(metricsToCheck.has(name)).toBeTruthy(); + expect(metrics[name]).toBeGreaterThanOrEqual(0); + metricsToCheck.delete(name); + } + expect(metricsToCheck.size).toBe(0); + } + }); + + describe('Page.waitForRequest', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }) + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with predicate', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(request => request.url() === server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }) + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async({page, server}) => { + let error = null; + await page.waitForRequest(() => false, {timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should respect default timeout', async({page, server}) => { + let error = null; + page.setDefaultTimeout(1); + await page.waitForRequest(() => false).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should work with no timeout', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png', {timeout: 0}), + page.evaluate(() => setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50)) + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.waitForResponse', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }) + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async({page, server}) => { + let error = null; + await page.waitForResponse(() => false, {timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should respect default timeout', async({page, server}) => { + let error = null; + page.setDefaultTimeout(1); + await page.waitForResponse(() => false).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should work with predicate', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(response => response.url() === server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }) + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with no timeout', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png', {timeout: 0}), + page.evaluate(() => setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50)) + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.exposeFunction', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.exposeFunction('compute', function(a, b) { + return a * b; + }); + const result = await page.evaluate(async function() { + return await compute(9, 4); + }); + expect(result).toBe(36); + }); + it.skip(WEBKIT)('should throw exception in page context', async({page, server}) => { + await page.exposeFunction('woof', function() { + throw new Error('WOOF WOOF'); + }); + const {message, stack} = await page.evaluate(async() => { + try { + await woof(); + } catch (e) { + return {message: e.message, stack: e.stack}; + } + }); + expect(message).toBe('WOOF WOOF'); + expect(stack).toContain(__filename); + }); + it.skip(WEBKIT)('should support throwing "null"', async({page, server}) => { + await page.exposeFunction('woof', function() { + throw null; + }); + const thrown = await page.evaluate(async() => { + try { + await woof(); + } catch (e) { + return e; + } + }); + expect(thrown).toBe(null); + }); + it.skip(WEBKIT)('should be callable from-inside evaluateOnNewDocument', async({page, server}) => { + let called = false; + await page.exposeFunction('woof', function() { + called = true; + }); + await page.evaluateOnNewDocument(() => woof()); + await page.reload(); + expect(called).toBe(true); + }); + it.skip(WEBKIT)('should survive navigation', async({page, server}) => { + await page.exposeFunction('compute', function(a, b) { + return a * b; + }); + + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async function() { + return await compute(9, 4); + }); + expect(result).toBe(36); + }); + it.skip(WEBKIT)('should await returned promise', async({page, server}) => { + await page.exposeFunction('compute', function(a, b) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function() { + return await compute(3, 5); + }); + expect(result).toBe(15); + }); + it.skip(WEBKIT)('should work on frames', async({page, server}) => { + await page.exposeFunction('compute', function(a, b) { + return Promise.resolve(a * b); + }); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const frame = page.frames()[1]; + const result = await frame.evaluate(async function() { + return await compute(3, 5); + }); + expect(result).toBe(15); + }); + it.skip(WEBKIT)('should work on frames before navigation', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + await page.exposeFunction('compute', function(a, b) { + return Promise.resolve(a * b); + }); + + const frame = page.frames()[1]; + const result = await frame.evaluate(async function() { + return await compute(3, 5); + }); + expect(result).toBe(15); + }); + it.skip(WEBKIT)('should work with complex objects', async({page, server}) => { + await page.exposeFunction('complexObject', function(a, b) { + return {x: a.x + b.x}; + }); + const result = await page.evaluate(async() => complexObject({x: 5}, {x: 2})); + expect(result.x).toBe(7); + }); + }); + + describe('Page.Events.PageError', function() { + it.skip(WEBKIT)('should fire', async({page, server}) => { + let error = null; + page.once('pageerror', e => error = e); + await Promise.all([ + page.goto(server.PREFIX + '/error.html'), + waitEvent(page, 'pageerror') + ]); + expect(error.message).toContain('Fancy'); + }); + }); + + describe('Page.setUserAgent', function() { + it('should work', async({page, server}) => { + expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should work for subframes', async({page, server}) => { + expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it.skip(WEBKIT)('should emulate device user-agent', async({page, server}) => { + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => navigator.userAgent)).not.toContain('iPhone'); + await page.setUserAgent(playwright.devices['iPhone 6'].userAgent); + expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); + }); + }); + + describe('Page.setContent', function() { + const expectedOutput = '
hello
'; + it('should work', async({page, server}) => { + await page.setContent('
hello
'); + const result = await page.content(); + expect(result).toBe(expectedOutput); + }); + it('should work with doctype', async({page, server}) => { + const doctype = ''; + await page.setContent(`${doctype}
hello
`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should work with HTML 4 doctype', async({page, server}) => { + const doctype = ''; + await page.setContent(`${doctype}
hello
`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it.skip(FFOX)('should respect timeout', async({page, server}) => { + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, (req, res) => {}); + let error = null; + await page.setContent(``, {timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it.skip(FFOX)('should respect default navigation timeout', async({page, server}) => { + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, (req, res) => {}); + let error = null; + await page.setContent(``).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it.skip(FFOX)('should await resources to load', async({page, server}) => { + const imgPath = '/img.png'; + let imgResponse = null; + server.setRoute(imgPath, (req, res) => imgResponse = res); + let loaded = false; + const contentPromise = page.setContent(``).then(() => loaded = true); + await server.waitForRequest(imgPath); + expect(loaded).toBe(false); + imgResponse.end(); + await contentPromise; + }); + it('should work fast enough', async({page, server}) => { + for (let i = 0; i < 20; ++i) + await page.setContent('
yo
'); + }); + it('should work with tricky content', async({page, server}) => { + await page.setContent('
hello world
' + '\x7F'); + expect(await page.$eval('div', div => div.textContent)).toBe('hello world'); + }); + it('should work with accents', async({page, server}) => { + await page.setContent('
aberración
'); + expect(await page.$eval('div', div => div.textContent)).toBe('aberración'); + }); + it('should work with emojis', async({page, server}) => { + await page.setContent('
🐥
'); + expect(await page.$eval('div', div => div.textContent)).toBe('🐥'); + }); + it('should work with newline', async({page, server}) => { + await page.setContent('
\n
'); + expect(await page.$eval('div', div => div.textContent)).toBe('\n'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.setBypassCSP', function() { + it('should bypass CSP meta tag', async({page, server}) => { + // Make sure CSP prohibits addScriptTag. + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await page.evaluate(() => window.__injected)).toBe(undefined); + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + }); + + it('should bypass CSP header', async({page, server}) => { + // Make sure CSP prohibits addScriptTag. + server.setCSP('/empty.html', 'default-src "self"'); + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await page.evaluate(() => window.__injected)).toBe(undefined); + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + }); + + it('should bypass after cross-process navigation', async({page, server}) => { + await page.setBypassCSP(true); + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + + await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + }); + it('should bypass CSP in iframes as well', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + { + // Make sure CSP prohibits addScriptTag in an iframe. + const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); + await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await frame.evaluate(() => window.__injected)).toBe(undefined); + } + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + + { + const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); + await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await frame.evaluate(() => window.__injected)).toBe(42); + } + }); + }); + + describe('Page.addScriptTag', function() { + it('should throw an error if no options are provided', async({page, server}) => { + let error = null; + try { + await page.addScriptTag('/injectedfile.js'); + } catch (e) { + error = e; + } + expect(error.message).toBe('Provide an object with a `url`, `path` or `content` property'); + }); + + it('should work with a url', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ url: '/injectedfile.js' }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => __injected)).toBe(42); + }); + + it('should work with a url and type=module', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ url: '/es6/es6import.js', type: 'module' }); + expect(await page.evaluate(() => __es6injected)).toBe(42); + }); + + it('should work with a path and type=module', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ path: path.join(__dirname, 'assets/es6/es6pathimport.js'), type: 'module' }); + await page.waitForFunction('window.__es6injected'); + expect(await page.evaluate(() => __es6injected)).toBe(42); + }); + + it('should work with a content and type=module', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ content: `import num from '/es6/es6module.js';window.__es6injected = num;`, type: 'module' }); + await page.waitForFunction('window.__es6injected'); + expect(await page.evaluate(() => __es6injected)).toBe(42); + }); + + it('should throw an error if loading from url fail', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addScriptTag({ url: '/nonexistfile.js' }); + } catch (e) { + error = e; + } + expect(error.message).toBe('Loading script from /nonexistfile.js failed'); + }); + + it('should work with a path', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ path: path.join(__dirname, 'assets/injectedfile.js') }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => __injected)).toBe(42); + }); + + it.skip(WEBKIT)('should include sourcemap when path is provided', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ path: path.join(__dirname, 'assets/injectedfile.js') }); + const result = await page.evaluate(() => __injectedError.stack); + expect(result).toContain(path.join('assets', 'injectedfile.js')); + }); + + it('should work with content', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ content: 'window.__injected = 35;' }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => __injected)).toBe(35); + }); + + // @see https://github.com/GoogleChrome/puppeteer/issues/4840 + xit('should throw when added with content to the CSP page', async({page, server}) => { + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page.addScriptTag({ content: 'window.__injected = 35;' }).catch(e => error = e); + expect(error).toBeTruthy(); + }); + + it('should throw when added with URL to the CSP page', async({page, server}) => { + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page.addScriptTag({ url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js' }).catch(e => error = e); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.addStyleTag', function() { + it('should throw an error if no options are provided', async({page, server}) => { + let error = null; + try { + await page.addStyleTag('/injectedstyle.css'); + } catch (e) { + error = e; + } + expect(error.message).toBe('Provide an object with a `url`, `path` or `content` property'); + }); + + it('should work with a url', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ url: '/injectedstyle.css' }); + expect(styleHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(`window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`)).toBe('rgb(255, 0, 0)'); + }); + + it('should throw an error if loading from url fail', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addStyleTag({ url: '/nonexistfile.js' }); + } catch (e) { + error = e; + } + expect(error.message).toBe('Loading style from /nonexistfile.js failed'); + }); + + it('should work with a path', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ path: path.join(__dirname, 'assets/injectedstyle.css') }); + expect(styleHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(`window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`)).toBe('rgb(255, 0, 0)'); + }); + + it('should include sourcemap when path is provided', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.addStyleTag({ path: path.join(__dirname, 'assets/injectedstyle.css') }); + const styleHandle = await page.$('style'); + const styleContent = await page.evaluate(style => style.innerHTML, styleHandle); + expect(styleContent).toContain(path.join('assets', 'injectedstyle.css')); + }); + + it('should work with content', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ content: 'body { background-color: green; }' }); + expect(styleHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(`window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`)).toBe('rgb(0, 128, 0)'); + }); + + it.skip(FFOX || WEBKIT)('should throw when added with content to the CSP page', async({page, server}) => { + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page.addStyleTag({ content: 'body { background-color: green; }' }).catch(e => error = e); + expect(error).toBeTruthy(); + }); + + it.skip(WEBKIT)('should throw when added with URL to the CSP page', async({page, server}) => { + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page.addStyleTag({ url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css' }).catch(e => error = e); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.url', function() { + it('should work', async({page, server}) => { + expect(page.url()).toBe('about:blank'); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + }); + + describe('Page.setJavaScriptEnabled', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.setJavaScriptEnabled(false); + await page.goto('data:text/html, '); + let error = null; + await page.evaluate('something').catch(e => error = e); + expect(error.message).toContain('something is not defined'); + + await page.setJavaScriptEnabled(true); + await page.goto('data:text/html, '); + expect(await page.evaluate('something')).toBe('forbidden'); + }); + }); + + describe('Page.setCacheEnabled', function() { + // FIXME: 'if-modified-since' is not set for some reason even if cache is on. + it.skip(WEBKIT)('should enable or disable the cache based on the state passed', async({page, server}) => { + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [cachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + // Rely on "if-modified-since" caching in our test server. + expect(cachedRequest.headers['if-modified-since']).not.toBe(undefined); + + await page.setCacheEnabled(false); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + }); + it.skip(WEBKIT)('should stay disabled when toggling request interception on/off', async({page, server}) => { + await page.setCacheEnabled(false); + await page.setRequestInterception(true); + await page.setRequestInterception(false); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + }); + }); + + // Printing to pdf is currently only supported in headless + describe.skip(FFOX || WEBKIT || !headless)('Page.pdf', function() { + it('should be able to save file', async({page, server}) => { + const outputFile = __dirname + '/assets/output.pdf'; + await page.pdf({path: outputFile}); + expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0); + fs.unlinkSync(outputFile); + }); + }); + + describe('Page.title', function() { + it('should return the page title', async({page, server}) => { + await page.goto(server.PREFIX + '/title.html'); + expect(await page.title()).toBe('Woof-Woof'); + }); + }); + + describe('Page.select', function() { + it('should select single option', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect(await page.evaluate(() => result.onInput)).toEqual(['blue']); + expect(await page.evaluate(() => result.onChange)).toEqual(['blue']); + }); + it('should select only first option', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => result.onInput)).toEqual(['blue']); + expect(await page.evaluate(() => result.onChange)).toEqual(['blue']); + }); + it.skip(FFOX)('should not throw when select causes navigation', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.$eval('select', select => select.addEventListener('input', () => window.location = '/empty.html')); + await Promise.all([ + page.select('select', 'blue'), + page.waitForNavigation(), + ]); + expect(page.url()).toContain('empty.html'); + }); + it('should select multiple options', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => makeMultiple()); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => result.onInput)).toEqual(['blue', 'green', 'red']); + expect(await page.evaluate(() => result.onChange)).toEqual(['blue', 'green', 'red']); + }); + it('should respect event bubbling', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect(await page.evaluate(() => result.onBubblingInput)).toEqual(['blue']); + expect(await page.evaluate(() => result.onBubblingChange)).toEqual(['blue']); + }); + it('should throw when element is not a element.'); + }); + it('should return [] on no matched values', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select','42','abc'); + expect(result).toEqual([]); + }); + it('should return an array of matched values', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => makeMultiple()); + const result = await page.select('select','blue','black','magenta'); + expect(result.reduce((accumulator,current) => ['blue', 'black', 'magenta'].includes(current) && accumulator, true)).toEqual(true); + }); + it('should return an array of one element when multiple is not set', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select','42','blue','black','magenta'); + expect(result.length).toEqual(1); + }); + it('should return [] on no values',async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select'); + expect(result).toEqual([]); + }); + it('should deselect all options when passed no values for a multiple select',async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => makeMultiple()); + await page.select('select','blue','black','magenta'); + await page.select('select'); + expect(await page.$eval('select', select => Array.from(select.options).every(option => !option.selected))).toEqual(true); + }); + it('should deselect all options when passed no values for a select without multiple',async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select','blue','black','magenta'); + await page.select('select'); + expect(await page.$eval('select', select => Array.from(select.options).every(option => !option.selected))).toEqual(true); + }); + it('should throw if passed in non-strings', async({page, server}) => { + await page.setContent(''); + let error = null; + try { + await page.select('select', 12); + } catch (e) { + error = e; + } + expect(error.message).toContain('Values must be strings'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/3327 + it.skip(FFOX || WEBKIT)('should work when re-defining top-level Event class', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => window.Event = null); + await page.select('select', 'blue'); + expect(await page.evaluate(() => result.onInput)).toEqual(['blue']); + expect(await page.evaluate(() => result.onChange)).toEqual(['blue']); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.fill', function() { + it('should fill textarea', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.fill('textarea', 'some value'); + expect(await page.evaluate(() => result)).toBe('some value'); + }); + it('should fill input', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.fill('input', 'some value'); + expect(await page.evaluate(() => result)).toBe('some value'); + }); + it('should throw on non-text inputs', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + for (const type of ['email', 'number', 'date']) { + await page.$eval('input', (input, type) => input.setAttribute('type', type), type); + let error = null; + await page.fill('input', '').catch(e => error = e); + expect(error.message).toContain('Cannot fill input of type'); + } + }); + it('should fill different input types', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + for (const type of ['password', 'search', 'tel', 'text', 'url']) { + await page.$eval('input', (input, type) => input.setAttribute('type', type), type); + await page.fill('input', 'text ' + type); + expect(await page.evaluate(() => result)).toBe('text ' + type); + } + }); + it('should fill contenteditable', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.fill('div[contenteditable]', 'some value'); + expect(await page.$eval('div[contenteditable]', div => div.textContent)).toBe('some value'); + }); + it('should fill elements with existing value and selection', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + + await page.$eval('input', input => input.value = 'value one'); + await page.fill('input', 'another value'); + expect(await page.evaluate(() => result)).toBe('another value'); + + await page.$eval('input', input => { + input.selectionStart = 1; + input.selectionEnd = 2; + }); + await page.fill('input', 'maybe this one'); + expect(await page.evaluate(() => result)).toBe('maybe this one'); + + await page.$eval('div[contenteditable]', div => { + div.innerHTML = 'some text some more text and even more text'; + const range = document.createRange(); + range.selectNodeContents(div.querySelector('span')); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }); + await page.fill('div[contenteditable]', 'replace with this'); + expect(await page.$eval('div[contenteditable]', div => div.textContent)).toBe('replace with this'); + }); + it('should throw when element is not an ,