diff --git a/index-for-dev.js b/index-for-dev.js index 8194796c7f..5dbc13a174 100644 --- a/index-for-dev.js +++ b/index-for-dev.js @@ -15,7 +15,10 @@ */ const { Playwright } = require('./lib/server/playwright'); +const { Electron } = require('./lib/server/electron'); const path = require('path'); const playwrightRoot = path.join(__dirname, 'packages', 'playwright'); -module.exports = new Playwright(playwrightRoot, require(path.join(playwrightRoot, 'browsers.json'))['browsers']); +const playwright = new Playwright(playwrightRoot, require(path.join(playwrightRoot, 'browsers.json'))['browsers']); +playwright.electron = new Electron(); +module.exports = playwright; diff --git a/package-lock.json b/package-lock.json index bcb0e22b96..ae3b0d16ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,38 @@ "js-tokens": "^4.0.0" } }, + "@electron/get": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.12.2.tgz", + "integrity": "sha512-vAuHUbfvBQpYTJ5wB7uVIDq5c/Ry0fiTBMs7lnEYAo/qXXppIVcWdfBr57u6eRnKdVso7KSiH6p/LbQAG6Izrg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "global-agent": "^2.0.2", + "global-tunnel-ng": "^2.7.1", + "got": "^9.6.0", + "progress": "^2.0.3", + "sanitize-filename": "^1.6.2", + "sumchecker": "^3.0.1" + } + }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -810,6 +842,13 @@ "integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==", "dev": true }, + "boolean": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz", + "integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==", + "dev": true, + "optional": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1012,6 +1051,29 @@ "unset-value": "^1.0.0" } }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + } + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1250,6 +1312,15 @@ } } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -1328,6 +1399,17 @@ "typedarray": "^0.0.6" } }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, + "optional": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -1371,6 +1453,13 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "dev": true, + "optional": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1497,12 +1586,37 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "optional": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -1560,6 +1674,13 @@ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true, + "optional": true + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -1594,6 +1715,12 @@ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -1606,6 +1733,52 @@ "stream-shift": "^1.0.0" } }, + "electron": { + "version": "9.0.0-beta.24", + "resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0-beta.24.tgz", + "integrity": "sha512-25L3XMqm/1CCaV5CgU5ZkhKXw9830WeipJrTW0+VC5XTKp/3xHwhxyQ5G1kQnOTJd7IGwOamvw237D6e1YKnng==", + "dev": true, + "requires": { + "@electron/get": "^1.0.1", + "@types/node": "^12.0.12", + "extract-zip": "^1.0.3" + }, + "dependencies": { + "@types/node": { + "version": "12.12.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.38.tgz", + "integrity": "sha512-75eLjX0pFuTcUXnnWmALMzzkYorjND0ezNEycaKesbUBg9eGZp4GHPuDmkRc4mQQvIpe29zrzATNRA6hkYqwmA==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "dev": true, + "requires": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "elliptic": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", @@ -1641,6 +1814,13 @@ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true, + "optional": true + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1666,6 +1846,12 @@ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", "dev": true }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "dev": true + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -1675,6 +1861,13 @@ "prr": "~1.0.1" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -2317,6 +2510,17 @@ "readable-stream": "^2.0.0" } }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -2393,6 +2597,31 @@ "is-glob": "^4.0.1" } }, + "global-agent": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.8.tgz", + "integrity": "sha512-VpBe/rhY6Rw2VDOTszAMNambg+4Qv8j0yiTNDYEXXXxkUNGWLHp8A3ztK4YDBbFNcWF4rgsec6/5gPyryya/+A==", + "dev": true, + "optional": true, + "requires": { + "boolean": "^3.0.0", + "core-js": "^3.6.4", + "es6-error": "^4.1.1", + "matcher": "^2.1.0", + "roarr": "^2.15.2", + "semver": "^7.1.2", + "serialize-error": "^5.0.0" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true, + "optional": true + } + } + }, "global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -2428,6 +2657,19 @@ "which": "^1.2.14" } }, + "global-tunnel-ng": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", + "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", + "dev": true, + "optional": true, + "requires": { + "encodeurl": "^1.0.2", + "lodash": "^4.17.10", + "npm-conf": "^1.1.3", + "tunnel": "^0.0.6" + } + }, "globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", @@ -2437,6 +2679,46 @@ "type-fest": "^0.8.1" } }, + "globalthis": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.1.tgz", + "integrity": "sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==", + "dev": true, + "optional": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -2561,6 +2843,12 @@ "parse-passwd": "^1.0.0" } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -2932,6 +3220,12 @@ "esprima": "^4.0.0" } }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -2950,6 +3244,13 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", @@ -2959,6 +3260,24 @@ "minimist": "^1.2.0" } }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -3017,6 +3336,12 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3068,6 +3393,25 @@ "object-visit": "^1.0.0" } }, + "matcher": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-2.1.0.tgz", + "integrity": "sha512-o+nZr+vtJtgPNklyeUKkkH42OsK8WAfdgaJE2FNxcjLPg+5QbeEoT6vRj8Xq/iv18JlQ9cmKsEu0b94ixWf1YQ==", + "dev": true, + "optional": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "optional": true + } + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -3145,6 +3489,12 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -3356,6 +3706,32 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, + "npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "optional": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "optional": true + } + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -3402,6 +3778,13 @@ } } }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "optional": true + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -3474,6 +3857,12 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -3670,6 +4059,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -3693,6 +4088,13 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "dev": true, + "optional": true + }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -4024,6 +4426,15 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -4058,6 +4469,30 @@ "inherits": "^2.0.1" } }, + "roarr": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.3.tgz", + "integrity": "sha512-AEjYvmAhlyxOeB9OqPUzQCo3kuAkNfuDk/HqWbZdFsqDFpapkTjiw+p4svNEoRLvuqNTxqfL+s+gtD4eDgZ+CA==", + "dev": true, + "optional": true, + "requires": { + "boolean": "^3.0.0", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true, + "optional": true + } + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -4103,6 +4538,15 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -4120,6 +4564,23 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true, + "optional": true + }, + "serialize-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-5.0.0.tgz", + "integrity": "sha512-/VtpuyzYf82mHYTtI4QKtwHa79vAdU5OQpNPAmE/0UDdlGT0ZxHwC+J6gXkw29wwoVI8fMPsfcVHOwXtUQYYQA==", + "dev": true, + "optional": true, + "requires": { + "type-fest": "^0.8.0" + } + }, "serialize-javascript": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", @@ -4532,6 +4993,15 @@ "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", "dev": true }, + "sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "requires": { + "debug": "^4.1.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4700,6 +5170,12 @@ } } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", @@ -4721,6 +5197,15 @@ "is-number": "^7.0.0" } }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "dev": true, + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, "ts-loader": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.2.tgz", @@ -4755,6 +5240,13 @@ "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "dev": true }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "optional": true + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -4812,6 +5304,12 @@ "imurmurhash": "^0.1.4" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -4891,12 +5389,27 @@ } } }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=", + "dev": true + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/package.json b/package.json index eeb621b696..1f94000d75 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "colors": "^1.4.0", "commonmark": "^0.28.1", "cross-env": "^5.0.5", + "electron": "^9.0.0-beta.24", "eslint": "^6.6.0", "esprima": "^4.0.0", "formidable": "^1.2.1", diff --git a/packages/playwright-electron/README.md b/packages/playwright-electron/README.md new file mode 100644 index 0000000000..2a478430fd --- /dev/null +++ b/packages/playwright-electron/README.md @@ -0,0 +1,2 @@ +# playwright-electron +This package contains the [Electron](https://www.electronjs.org/) flavor of [Playwright](http://github.com/microsoft/playwright). diff --git a/packages/playwright-electron/browsers.json b/packages/playwright-electron/browsers.json new file mode 100644 index 0000000000..4e02babf0c --- /dev/null +++ b/packages/playwright-electron/browsers.json @@ -0,0 +1,3 @@ +{ + "browsers": [] +} diff --git a/packages/playwright-electron/index.d.ts b/packages/playwright-electron/index.d.ts new file mode 100644 index 0000000000..e87c71ad84 --- /dev/null +++ b/packages/playwright-electron/index.d.ts @@ -0,0 +1,20 @@ +/** + * 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. + */ + +import * as types from 'playwright-core/types/types'; + +export * from 'playwright-core/types/types'; +export const electron: types.BrowserType; diff --git a/packages/playwright-electron/index.js b/packages/playwright-electron/index.js new file mode 100644 index 0000000000..fe9fa4bdd3 --- /dev/null +++ b/packages/playwright-electron/index.js @@ -0,0 +1,22 @@ +/** + * 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 { Playwright } = require('playwright-core/lib/server/playwright'); +const { Electron } = require('playwright-core/lib/server/electron'); + +const playwright = new Playwright(__dirname, require('./browsers.json')['browsers']); +playwright.electron = new Electron(); +module.exports = playwright; diff --git a/packages/playwright-electron/package.json b/packages/playwright-electron/package.json new file mode 100644 index 0000000000..911b7b37c8 --- /dev/null +++ b/packages/playwright-electron/package.json @@ -0,0 +1,16 @@ +{ + "name": "playwright-electron", + "version": "0.1.0", + "description": "A high-level API to automate Electron", + "repository": "github:Microsoft/playwright", + "homepage": "https://playwright.dev", + "main": "index.js", + "scripts": {}, + "author": { + "name": "Microsoft Corporation" + }, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "^1.0.1" + } +} diff --git a/src/server/electron.ts b/src/server/electron.ts new file mode 100644 index 0000000000..4ae0281cce --- /dev/null +++ b/src/server/electron.ts @@ -0,0 +1,198 @@ +/** + * 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. + */ + +import * as path from 'path'; +import { CRBrowser, CRBrowserContext } from '../chromium/crBrowser'; +import { CRConnection, CRSession } from '../chromium/crConnection'; +import { CRExecutionContext } from '../chromium/crExecutionContext'; +import { TimeoutError } from '../errors'; +import { Events } from '../events'; +import { ExtendedEventEmitter } from '../extendedEventEmitter'; +import { helper } from '../helper'; +import * as js from '../javascript'; +import { InnerLogger, Logger, RootLogger } from '../logger'; +import { Page } from '../page'; +import { TimeoutSettings } from '../timeoutSettings'; +import { WebSocketTransport } from '../transport'; +import * as types from '../types'; +import { BrowserServer } from './browserServer'; +import { launchProcess, waitForLine } from './processLauncher'; +import { BrowserContext } from '../browserContext'; + +type ElectronLaunchOptions = { + args?: string[], + cwd?: string, + env?: {[key: string]: string|number|boolean}, + handleSIGINT?: boolean, + handleSIGTERM?: boolean, + handleSIGHUP?: boolean, + timeout?: number, + logger?: Logger, +}; + +export const ElectronEvents = { + ElectronApplication: { + Close: 'close', + Window: 'window', + } +}; + +export class ElectronApplication extends ExtendedEventEmitter { + private _logger: InnerLogger; + private _browserContext: CRBrowserContext; + private _nodeConnection: CRConnection; + private _nodeSession: CRSession; + private _nodeExecutionContext: js.ExecutionContext | undefined; + private _nodeElectronHandle: js.JSHandle | undefined; + private _windows = new Set(); + private _lastWindowId = 0; + readonly _timeoutSettings = new TimeoutSettings(); + + constructor(logger: InnerLogger, browser: CRBrowser, nodeConnection: CRConnection) { + super(); + this._logger = logger; + this._browserContext = browser._defaultContext!; + this._browserContext.on(Events.BrowserContext.Close, () => this.emit(ElectronEvents.ElectronApplication.Close)); + this._browserContext.on(Events.BrowserContext.Page, event => this._onPage(event)); + this._nodeConnection = nodeConnection; + this._nodeSession = nodeConnection.rootSession; + } + + private async _onPage(page: Page) { + // Needs to be sync. + const windowId = ++this._lastWindowId; + // Can be async. + const handle = await this._nodeElectronHandle!.evaluateHandle(({ BrowserWindow }, windowId) => BrowserWindow.fromId(windowId), windowId); + (page as any).browserWindow = handle; + (page as any)._browserWindowId = windowId; + page.on(Events.Page.Close, () => { + (page as any).browserWindow.dispose(); + this._windows.delete(page); + }); + this._windows.add(page); + this.emit(ElectronEvents.ElectronApplication.Window, page); + } + + windows(): Page[] { + return [...this._windows]; + } + + async newBrowserWindow(options: any): Promise { + const windowId = await this.evaluate(async ({ BrowserWindow }, options) => { + const win = new BrowserWindow(options); + win.loadURL('about:blank'); + return win.id; + }, options); + + for (const page of this._windows) { + if ((page as any)._browserWindowId === windowId) + return page; + } + + return await this.waitForEvent(ElectronEvents.ElectronApplication.Window, (page: Page) => (page as any)._browserWindowId === windowId); + } + + context(): BrowserContext { + return this._browserContext; + } + + async close() { + await this.evaluate(({ app }) => app.quit()); + this._nodeConnection.close(); + } + + async _init() { + this._nodeSession.once('Runtime.executionContextCreated', event => { + this._nodeExecutionContext = new js.ExecutionContext(new CRExecutionContext(this._nodeSession, event.context), this._logger); + }); + await this._nodeSession.send('Runtime.enable', {}).catch(e => {}); + this._nodeElectronHandle = await this._nodeExecutionContext!.evaluateHandleInternal(() => { + // Resolving the race between the debugger and the boot-time script. + if ((global as any)._playwrightRun) + return (global as any)._playwrightRun(); + return new Promise(f => (global as any)._playwrightRunCallback = f); + }); + } + + async evaluate(pageFunction: types.FuncOn, arg: Arg): Promise; + async evaluate(pageFunction: types.FuncOn, arg?: any): Promise; + async evaluate(pageFunction: types.FuncOn, arg: Arg): Promise { + return this._nodeElectronHandle!.evaluate(pageFunction, arg); + } + + async evaluateHandle(pageFunction: types.FuncOn, arg: Arg): Promise>; + async evaluateHandle(pageFunction: types.FuncOn, arg?: any): Promise>; + async evaluateHandle(pageFunction: types.FuncOn, arg: Arg): Promise> { + return this._nodeElectronHandle!.evaluateHandle(pageFunction, arg); + } + + protected _computeDeadline(options?: types.TimeoutOptions): number { + return this._timeoutSettings.computeDeadline(options); + } +} + +export class Electron { + async launch(executablePath: string, options: ElectronLaunchOptions = {}): Promise { + const { + args = [], + env = process.env, + handleSIGINT = true, + handleSIGTERM = true, + handleSIGHUP = true, + timeout = 30000, + } = options; + const deadline = TimeoutSettings.computeDeadline(timeout); + + let app: ElectronApplication | undefined = undefined; + + const logger = new RootLogger(options.logger); + const electronArguments = ['--inspect=0', '--remote-debugging-port=0', '--require', path.join(__dirname, 'electronLoader.js'), ...args]; + const { launchedProcess, gracefullyClose } = await launchProcess({ + executablePath, + args: electronArguments, + env, + handleSIGINT, + handleSIGTERM, + handleSIGHUP, + logger, + pipe: true, + cwd: options.cwd, + omitDownloads: true, + attemptToGracefullyClose: () => app!.close(), + onkill: (exitCode, signal) => { + if (app) + app.emit(ElectronEvents.ElectronApplication.Close, exitCode, signal); + }, + }); + + const timeoutError = new TimeoutError(`Timed out while trying to connect to Electron!`); + const nodeMatch = await waitForLine(launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/, helper.timeUntilDeadline(deadline), timeoutError); + const nodeConnection = await WebSocketTransport.connect(nodeMatch[1], transport => { + return new CRConnection(transport, logger); + }); + + const chromeMatch = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, helper.timeUntilDeadline(deadline), timeoutError); + const chromeTransport = await WebSocketTransport.connect(chromeMatch[1], transport => { + return transport; + }); + const browserServer = new BrowserServer(launchedProcess, gracefullyClose, null); + const browser = await CRBrowser.connect(chromeTransport, true, logger, { ...options, viewport: null }); + browser._ownedServer = browserServer; + app = new ElectronApplication(logger, browser, nodeConnection); + await app._init(); + return app; + } +} diff --git a/src/server/electronLoader.ts b/src/server/electronLoader.ts new file mode 100644 index 0000000000..178c77c752 --- /dev/null +++ b/src/server/electronLoader.ts @@ -0,0 +1,72 @@ +/** + * 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. + */ + +import * as electron from 'electron'; + +// Electron loader is passed to Electron as --require electronLoader.js and +// defers the app ready event until remote debugging is established. + +(async () => { + const app = electron.app; + const originalEmitMethod = app.emit.bind(app); + const originalIsReadyMethod = app.isReady.bind(app); + const originalWhenReadyMethod = app.whenReady.bind(app); + + const deferredEmits: { event: string | symbol, args: any[] }[] = []; + let attached = false; + let isReady = false; + let readyCallback: () => void; + const readyPromise = new Promise(f => readyCallback = f); + + app.isReady = () => { + if (attached) + return originalIsReadyMethod(); + return isReady; + }; + + app.whenReady = async () => { + if (attached) + return originalWhenReadyMethod(); + await readyPromise; + }; + + app.emit = (event: string | symbol, ...args: any[]): boolean => { + if (attached) + return originalEmitMethod(event, ...args); + deferredEmits.push({ event, args }); + return true; + }; + + const playwrightRun = () => { + while (deferredEmits.length) { + const emit = deferredEmits.shift(); + if (emit!.event === 'ready') { + isReady = true; + readyCallback(); + } + originalEmitMethod(emit!.event, ...emit!.args); + } + attached = true; + return electron; + }; + + // Resolving the race between the debugger and the boot-time script. + if ((global as any)._playwrightRunCallback) { + (global as any)._playwrightRunCallback(playwrightRun()); + return; + } + (global as any)._playwrightRun = playwrightRun; +})(); diff --git a/test/electron/electron.spec.js b/test/electron/electron.spec.js new file mode 100644 index 0000000000..b1acc3c8a4 --- /dev/null +++ b/test/electron/electron.spec.js @@ -0,0 +1,165 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const path = require('path'); +const config = require('../test.config'); + +const electronName = process.platform === 'win32' ? 'electron.cmd' : 'electron'; + +describe('Electron', function() { + beforeEach(async (state, testRun) => { + const electronPath = path.join(__dirname, '..', '..', 'node_modules', '.bin', electronName); + state.application = await playwright.electron.launch(electronPath, { + args: [path.join(__dirname, 'testApp.js')], + logger: { + isEnabled: (name, severity) => { + return name === 'browser' || + (name === 'protocol' && config.dumpProtocolOnFailure); + }, + log: (name, severity, message, args) => { + if (name === 'browser') { + if (severity === 'warning') + testRun.log(`\x1b[31m[browser]\x1b[0m ${message}`) + else + testRun.log(`\x1b[33m[browser]\x1b[0m ${message}`) + } else if (name === 'protocol' && config.dumpProtocolOnFailure) { + testRun.log(`\x1b[32m[protocol]\x1b[0m ${message}`) + } + } + } + }); + }); + afterEach(async (state, testRun) => { + await state.application.close(); + if (config.dumpProtocolOnFailure) { + if (testRun.ok()) + testRun.output().splice(0); + } + }); + it('should script application', async ({ application }) => { + const appPath = await application.evaluate(async ({ app }) => app.getAppPath()); + expect(appPath).toContain('electron'); + }); + it('should create window', async ({ application }) => { + const [ page ] = await Promise.all([ + application.waitForEvent('window'), + application.evaluate(({ BrowserWindow }) => { + const window = new BrowserWindow({ width: 800, height: 600 }); + window.loadURL('data:text/html,Hello World 1'); + }) + ]); + await page.waitForLoadState('domcontentloaded'); + expect(await page.title()).toBe('Hello World 1'); + }); + it('should create window 2', async ({ application }) => { + const page = await application.newBrowserWindow({ width: 800, height: 600 }); + await page.goto('data:text/html,Hello World 2'); + expect(await page.title()).toBe('Hello World 2'); + }); + it('should create multiple windows', async ({ application }) => { + const createPage = async ordinal => { + const page = await application.newBrowserWindow({ width: 800, height: 600 }); + await Promise.all([ + page.waitForNavigation(), + page.browserWindow.evaluate((window, ordinal) => window.loadURL(`data:text/html,Hello World ${ordinal}`), ordinal) + ]); + return page; + }; + + const page1 = await createPage(1); + const page2 = await createPage(2); + const page3 = await createPage(3); + await page1.close(); + const page4 = await createPage(4); + const titles = []; + for (const window of application.windows()) + titles.push(await window.title()); + expect(titles).toEqual(['Hello World 2', 'Hello World 3', 'Hello World 4']); + }); + it('should route network', async ({ application }) => { + await application.context().route('**/empty.html', (route, request) => { + route.fulfill({ + status: 200, + contentType: 'text/html', + body: 'Hello World', + }) + }); + const page = await application.newBrowserWindow({ width: 800, height: 600 }); + await page.goto('https://localhost:1000/empty.html'); + expect(await page.title()).toBe('Hello World'); + }); + it('should support init script', async ({ application }) => { + await application.context().addInitScript('window.magic = 42;') + const page = await application.newBrowserWindow({ width: 800, height: 600 }); + await page.goto('data:text/html,'); + expect(await page.evaluate(() => copy)).toBe(42); + }); + it('should expose function', async ({ application }) => { + const result = new Promise(f => callback = f); + const t = Date.now(); + await application.context().exposeFunction('add', (a, b) => a + b); + const page = await application.newBrowserWindow({ width: 800, height: 600 }); + await page.goto('data:text/html,'); + expect(await page.evaluate(() => result)).toBe(42); + }); +}); + +describe('Electron window', function() { + beforeAll(async state => { + const electronPath = path.join(__dirname, '..', '..', 'node_modules', '.bin', electronName); + state.application = await playwright.electron.launch(electronPath, { + args: [path.join(__dirname, 'testApp.js')] + }); + }); + + afterAll(async state => { + await state.application.close(); + }); + + beforeEach(async state => { + state.page = await state.application.newBrowserWindow({ width: 800, height: 600 }); + }); + + afterEach(async state => { + await state.page.close(); + }); + + 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 check the box', async({page}) => { + await page.setContent(``); + await page.check('input'); + expect(await page.evaluate(() => checkbox.checked)).toBe(true); + }); + it('should not check the checked box', async({page}) => { + await page.setContent(``); + await page.check('input'); + expect(await page.evaluate(() => checkbox.checked)).toBe(true); + }); + 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); + }); +}); diff --git a/test/electron/testApp.js b/test/electron/testApp.js new file mode 100644 index 0000000000..493b45a009 --- /dev/null +++ b/test/electron/testApp.js @@ -0,0 +1,3 @@ +const { app } = require('electron'); + +app.on('window-all-closed', e => e.preventDefault()); \ No newline at end of file diff --git a/test/test.config.js b/test/test.config.js index 00c6c12015..7b6a12a7df 100644 --- a/test/test.config.js +++ b/test/test.config.js @@ -229,6 +229,15 @@ module.exports = { environments: [customEnvironment], }, + { + files: [ + './electron/electron.spec.js', + ], + browsers: ['chromium'], + title: '[Electron]', + environments: [customEnvironment], + }, + { files: [ './apicoverage.spec.js',