mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-11 20:37:54 +03:00
devops: remake downloading logic (#1419)
This patch: - removes `browserType.downloadBrowserIfNeeded()` method. The method turned out to be ill-behaving and cannot not be used as we'd like to (see #1085) - adds a `browserType.setExecutablePath` method to set a browser exectuable. With this patch, we take the following approach towards managing browser downloads: - `playwright-core` doesn't download any browsers. In `playwright-core`, `playwright.chromium.executablePath()` returns `null` (same for firefox and webkit). - clients of `playwright-core` (e.g. `playwright` and others) download browsers one way or another. They can then configure `playwright` with executable paths and re-export the `playwright` object to their clients. - `playwright`, `playwright-firefox`, `playwright-chromium` and `playwright-webkit` download browsers. Once browsers are downloaded, their executable paths are saved to a `.downloaded-browsers.json` file. This file is read in `playwright/index.js` to configure browser executable paths and re-export the API. - special case is `install-from-github.js` that also cleans up old browsers.
This commit is contained in:
parent
2af07ce475
commit
f5ecbff16e
@ -15,9 +15,7 @@ jobs:
|
||||
- save_cache:
|
||||
key: dependency-cache-{{ checksum "package.json" }}
|
||||
paths:
|
||||
- ./.local-chromium
|
||||
- ./.local-firefox
|
||||
- ./.local-webkit
|
||||
- ./.local-browsers
|
||||
|
||||
- run:
|
||||
command: |
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -3,11 +3,10 @@
|
||||
/test/output-firefox
|
||||
/test/output-webkit
|
||||
/test/test-user-data-dir*
|
||||
/.local-chromium/
|
||||
/.local-firefox/
|
||||
/.local-webkit/
|
||||
/.local-browsers/
|
||||
/.dev_profile*
|
||||
.DS_Store
|
||||
.downloaded-browsers.json
|
||||
*.swp
|
||||
*.pyc
|
||||
.vscode
|
||||
|
@ -3645,7 +3645,6 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
|
||||
|
||||
<!-- GEN:toc -->
|
||||
- [browserType.connect(options)](#browsertypeconnectoptions)
|
||||
- [browserType.downloadBrowserIfNeeded([progress])](#browsertypedownloadbrowserifneededprogress)
|
||||
- [browserType.executablePath()](#browsertypeexecutablepath)
|
||||
- [browserType.launch([options])](#browsertypelaunchoptions)
|
||||
- [browserType.launchPersistentContext(userDataDir, [options])](#browsertypelaunchpersistentcontextuserdatadir-options)
|
||||
@ -3661,14 +3660,8 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
|
||||
|
||||
This methods attaches Playwright to an existing browser instance.
|
||||
|
||||
#### browserType.downloadBrowserIfNeeded([progress])
|
||||
- `progress` <[function]> If download is initiated, this function is called with two parameters: `downloadedBytes` and `totalBytes`.
|
||||
- returns: <[Promise]> promise that resolves when browser is successfully downloaded.
|
||||
|
||||
Download browser binary if it is missing.
|
||||
|
||||
#### browserType.executablePath()
|
||||
- returns: <[string]> A path where Playwright expects to find a bundled browser.
|
||||
- returns: <[string]> A path where Playwright expects to find a bundled browser executable.
|
||||
|
||||
#### browserType.launch([options])
|
||||
- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields:
|
||||
|
@ -164,7 +164,7 @@ done only once per host environment:
|
||||
|
||||
```bash
|
||||
# cd to the downloaded instance
|
||||
cd <project-dir-path>/node_modules/playwright/.local-chromium/linux-<revision>/chrome-linux/
|
||||
cd <project-dir-path>/node_modules/playwright/.local-browsers/chromium-<revision>/
|
||||
sudo chown root:root chrome_sandbox
|
||||
sudo chmod 4755 chrome_sandbox
|
||||
# copy sandbox executable to a shared location
|
||||
|
@ -13,37 +13,39 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const browserFetcher = require('./lib/server/browserFetcher.js');
|
||||
const packageJSON = require('./package.json');
|
||||
|
||||
async function downloadBrowser(browserType) {
|
||||
const browser = browserType.name();
|
||||
async function downloadBrowserWithProgressBar(downloadPath, browser, version = '') {
|
||||
let progressBar = null;
|
||||
let lastDownloadedBytes = 0;
|
||||
function onProgress(downloadedBytes, totalBytes) {
|
||||
const revision = packageJSON.playwright[`${browser}_revision`];
|
||||
function progress(downloadedBytes, totalBytes) {
|
||||
if (!progressBar) {
|
||||
const ProgressBar = require('progress');
|
||||
progressBar = new ProgressBar(`Downloading ${browser} ${browserType._revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
|
||||
progressBar = new ProgressBar(`Downloading ${browser} r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
|
||||
complete: '=',
|
||||
incomplete: ' ',
|
||||
width: 20,
|
||||
total: totalBytes,
|
||||
host: getFromENV('PLAYWRIGHT_DOWNLOAD_HOST'),
|
||||
});
|
||||
}
|
||||
const delta = downloadedBytes - lastDownloadedBytes;
|
||||
lastDownloadedBytes = downloadedBytes;
|
||||
progressBar.tick(delta);
|
||||
}
|
||||
|
||||
const fetcher = browserType._createBrowserFetcher();
|
||||
const revisionInfo = fetcher.revisionInfo();
|
||||
// Do nothing if the revision is already downloaded.
|
||||
if (revisionInfo.local)
|
||||
return revisionInfo;
|
||||
await browserType.downloadBrowserIfNeeded(onProgress);
|
||||
logPolitely(`${browser} downloaded to ${revisionInfo.folderPath}`);
|
||||
return revisionInfo;
|
||||
const executablePath = await browserFetcher.downloadBrowser({
|
||||
downloadPath,
|
||||
browser,
|
||||
revision,
|
||||
progress,
|
||||
});
|
||||
logPolitely(`${browser} downloaded to ${downloadPath}`);
|
||||
return executablePath;
|
||||
}
|
||||
|
||||
|
||||
function toMegabytes(bytes) {
|
||||
const mb = bytes / 1024 / 1024;
|
||||
return `${Math.round(mb * 10) / 10} Mb`;
|
||||
@ -57,4 +59,11 @@ function logPolitely(toBeLogged) {
|
||||
console.log(toBeLogged);
|
||||
}
|
||||
|
||||
module.exports = {downloadBrowser};
|
||||
function getFromENV(name) {
|
||||
let value = process.env[name];
|
||||
value = value || process.env[`npm_config_${name.toLowerCase()}`];
|
||||
value = value || process.env[`npm_package_config_${name.toLowerCase()}`];
|
||||
return value;
|
||||
}
|
||||
|
||||
module.exports = {downloadBrowserWithProgressBar};
|
||||
|
14
index.js
14
index.js
@ -15,9 +15,17 @@
|
||||
*/
|
||||
const {Playwright} = require('./lib/server/playwright.js');
|
||||
|
||||
module.exports = new Playwright({
|
||||
downloadPath: __dirname,
|
||||
const playwright = new Playwright({
|
||||
browsers: ['webkit', 'chromium', 'firefox'],
|
||||
respectEnvironmentVariables: false,
|
||||
});
|
||||
|
||||
try {
|
||||
const downloadedBrowsers = require('./.downloaded-browsers.json');
|
||||
playwright.chromium._executablePath = downloadedBrowsers.crExecutablePath;
|
||||
playwright.firefox._executablePath = downloadedBrowsers.ffExecutablePath;
|
||||
playwright.webkit._executablePath = downloadedBrowsers.wkExecutablePath;
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
module.exports = playwright;
|
||||
|
||||
|
@ -24,41 +24,69 @@ try {
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
const {downloadBrowser} = require('./download-browser');
|
||||
const playwright = require('.');
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const rmAsync = util.promisify(require('rimraf'));
|
||||
const existsAsync = path => fs.promises.access(path).then(() => true, e => false);
|
||||
const {downloadBrowserWithProgressBar} = require('./download-browser');
|
||||
const protocolGenerator = require('./utils/protocol-types-generator');
|
||||
const packageJSON = require('./package.json');
|
||||
|
||||
const DOWNLOADED_BROWSERS_JSON_PATH = path.join(__dirname, '.downloaded-browsers.json');
|
||||
const DOWNLOAD_PATHS = {
|
||||
chromium: path.join(__dirname, '.local-browsers', `chromium-${packageJSON.playwright.chromium_revision}`),
|
||||
firefox: path.join(__dirname, '.local-browsers', `firefox-${packageJSON.playwright.firefox_revision}`),
|
||||
webkit: path.join(__dirname, '.local-browsers', `webkit-${packageJSON.playwright.webkit_revision}`),
|
||||
};
|
||||
|
||||
(async function() {
|
||||
const protocolGenerator = require('./utils/protocol-types-generator');
|
||||
const downloadedBrowsersJSON = await fs.promises.readFile(DOWNLOADED_BROWSERS_JSON_PATH, 'utf8').then(json => JSON.parse(json)).catch(() => ({}));
|
||||
try {
|
||||
const chromeRevision = await downloadAndCleanup(playwright.chromium);
|
||||
await protocolGenerator.generateChromiunProtocol(chromeRevision);
|
||||
if (!(await existsAsync(DOWNLOAD_PATHS.chromium))) {
|
||||
const crExecutablePath = await downloadBrowserWithProgressBar(DOWNLOAD_PATHS.chromium, 'chromium');
|
||||
downloadedBrowsersJSON.crExecutablePath = crExecutablePath;
|
||||
await protocolGenerator.generateChromiumProtocol(crExecutablePath);
|
||||
await fs.promises.writeFile(DOWNLOADED_BROWSERS_JSON_PATH, JSON.stringify(downloadedBrowsersJSON));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
}
|
||||
try {
|
||||
if (!(await existsAsync(DOWNLOAD_PATHS.firefox))) {
|
||||
const ffExecutablePath = await downloadBrowserWithProgressBar(DOWNLOAD_PATHS.firefox, 'firefox');
|
||||
downloadedBrowsersJSON.ffExecutablePath = ffExecutablePath;
|
||||
await protocolGenerator.generateFirefoxProtocol(ffExecutablePath);
|
||||
await fs.promises.writeFile(DOWNLOADED_BROWSERS_JSON_PATH, JSON.stringify(downloadedBrowsersJSON));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
}
|
||||
try {
|
||||
if (!(await existsAsync(DOWNLOAD_PATHS.webkit))) {
|
||||
const wkExecutablePath = await downloadBrowserWithProgressBar(DOWNLOAD_PATHS.webkit, 'webkit');
|
||||
downloadedBrowsersJSON.wkExecutablePath = wkExecutablePath;
|
||||
await protocolGenerator.generateWebKitProtocol(path.dirname(wkExecutablePath));
|
||||
await fs.promises.writeFile(DOWNLOADED_BROWSERS_JSON_PATH, JSON.stringify(downloadedBrowsersJSON));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const firefoxRevision = await downloadAndCleanup(playwright.firefox);
|
||||
await protocolGenerator.generateFirefoxProtocol(firefoxRevision);
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
}
|
||||
// Cleanup stale revisions.
|
||||
const directories = new Set(await readdirAsync(path.join(__dirname, '.local-browsers')));
|
||||
directories.delete(DOWNLOAD_PATHS.chromium);
|
||||
directories.delete(DOWNLOAD_PATHS.firefox);
|
||||
directories.delete(DOWNLOAD_PATHS.webkit);
|
||||
// cleanup old browser directories.
|
||||
directories.add(path.join(__dirname, '.local-chromium'));
|
||||
directories.add(path.join(__dirname, '.local-firefox'));
|
||||
directories.add(path.join(__dirname, '.local-webkit'));
|
||||
await Promise.all([...directories].map(directory => rmAsync(directory)));
|
||||
|
||||
try {
|
||||
const webkitRevision = await downloadAndCleanup(playwright.webkit);
|
||||
await protocolGenerator.generateWebKitProtocol(webkitRevision);
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
async function readdirAsync(dirpath) {
|
||||
return fs.promises.readdir(dirpath).then(dirs => dirs.map(dir => path.join(dirpath, dir)));
|
||||
}
|
||||
})();
|
||||
|
||||
async function downloadAndCleanup(browserType) {
|
||||
const revisionInfo = await downloadBrowser(browserType);
|
||||
|
||||
// Remove previous revisions.
|
||||
const fetcher = browserType._createBrowserFetcher();
|
||||
const localRevisions = await fetcher.localRevisions();
|
||||
const cleanupOldVersions = localRevisions.filter(revision => revision !== revisionInfo.revision).map(revision => fetcher.remove(revision));
|
||||
await Promise.all([...cleanupOldVersions]);
|
||||
|
||||
return revisionInfo;
|
||||
}
|
||||
|
@ -13,12 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const {Playwright} = require('playwright-core/lib/server/playwright.js');
|
||||
|
||||
module.exports = new Playwright({
|
||||
downloadPath: __dirname,
|
||||
const playwright = new Playwright({
|
||||
browsers: ['chromium'],
|
||||
respectEnvironmentVariables: true,
|
||||
});
|
||||
module.exports = playwright;
|
||||
|
||||
try {
|
||||
const downloadedBrowsers = require('./.downloaded-browsers.json');
|
||||
playwright.chromium._executablePath = downloadedBrowsers.crExecutablePath;
|
||||
} catch (e) {
|
||||
throw new Error('playwright-chromium has not downloaded Chromium.');
|
||||
}
|
||||
|
@ -13,6 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const {downloadBrowser} = require('playwright-core/download-browser');
|
||||
const playwright = require('.');
|
||||
downloadBrowser(playwright.chromium);
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {downloadBrowserWithProgressBar} = require('playwright-core/download-browser');
|
||||
(async function() {
|
||||
const crExecutablePath = await downloadBrowserWithProgressBar(path.join(__dirname, '.local-browsers', 'chromium'), 'chromium');
|
||||
await fs.promises.writeFile(path.join(__dirname, '.downloaded-browsers.json'), JSON.stringify({crExecutablePath}));
|
||||
})();
|
||||
|
@ -13,12 +13,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const {Playwright} = require('playwright-core/lib/server/playwright.js');
|
||||
|
||||
module.exports = new Playwright({
|
||||
downloadPath: __dirname,
|
||||
const playwright = new Playwright({
|
||||
browsers: ['firefox'],
|
||||
respectEnvironmentVariables: true,
|
||||
});
|
||||
module.exports = playwright;
|
||||
|
||||
try {
|
||||
const downloadedBrowsers = require(path.join(__dirname, '.downloaded-browsers.json'));
|
||||
playwright.firefox._executablePath = downloadedBrowsers.ffExecutablePath;
|
||||
} catch (e) {
|
||||
throw new Error('playwright-firefox has not downloaded Firefox.');
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const {downloadBrowser} = require('playwright-core/download-browser');
|
||||
const playwright = require('.');
|
||||
downloadBrowser(playwright.firefox);
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {downloadBrowserWithProgressBar} = require('playwright-core/download-browser');
|
||||
|
||||
(async function() {
|
||||
const ffExecutablePath = await downloadBrowserWithProgressBar(path.join(__dirname, '.local-browsers', 'firefox'), 'firefox');
|
||||
await fs.promises.writeFile(path.join(__dirname, '.downloaded-browsers.json'), JSON.stringify({ffExecutablePath, }));
|
||||
})();
|
||||
|
@ -13,12 +13,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const {Playwright} = require('playwright-core/lib/server/playwright.js');
|
||||
|
||||
module.exports = new Playwright({
|
||||
downloadPath: __dirname,
|
||||
const playwright = new Playwright({
|
||||
browsers: ['webkit'],
|
||||
respectEnvironmentVariables: true,
|
||||
});
|
||||
module.exports = playwright;
|
||||
|
||||
try {
|
||||
const downloadedBrowsers = require(path.join(__dirname, '.downloaded-browsers.json'));
|
||||
playwright.webkit._executablePath = downloadedBrowsers.wkExecutablePath;
|
||||
} catch (e) {
|
||||
throw new Error('playwright-webkit has not downloaded WebKit.');
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const {downloadBrowser} = require('playwright-core/download-browser');
|
||||
const playwright = require('.');
|
||||
downloadBrowser(playwright.webkit);
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {downloadBrowserWithProgressBar} = require('playwright-core/download-browser');
|
||||
|
||||
(async function() {
|
||||
const wkExecutablePath = await downloadBrowserWithProgressBar(path.join(__dirname, '.local-browsers', 'webkit'), 'webkit');
|
||||
await fs.promises.writeFile(path.join(__dirname, '.downloaded-browsers.json'), JSON.stringify({wkExecutablePath, }));
|
||||
})();
|
||||
|
@ -13,11 +13,21 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const path = require('path');
|
||||
const {Playwright} = require('playwright-core/lib/server/playwright.js');
|
||||
|
||||
module.exports = new Playwright({
|
||||
downloadPath: __dirname,
|
||||
const playwright = new Playwright({
|
||||
browsers: ['webkit', 'chromium', 'firefox'],
|
||||
respectEnvironmentVariables: true,
|
||||
});
|
||||
module.exports = playwright;
|
||||
|
||||
try {
|
||||
const downloadedBrowsers = require(path.join(__dirname, '.downloaded-browsers.json'));
|
||||
playwright.chromium._executablePath = downloadedBrowsers.crExecutablePath;
|
||||
playwright.firefox._executablePath = downloadedBrowsers.ffExecutablePath;
|
||||
playwright.webkit._executablePath = downloadedBrowsers.wkExecutablePath;
|
||||
} catch (e) {
|
||||
throw new Error('ERROR: Playwright did not download browsers');
|
||||
}
|
||||
|
||||
|
||||
|
@ -13,10 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const {downloadBrowser} = require('playwright-core/download-browser');
|
||||
const playwright = require('.');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {downloadBrowserWithProgressBar} = require('playwright-core/download-browser');
|
||||
|
||||
(async function() {
|
||||
await downloadBrowser(playwright.chromium);
|
||||
await downloadBrowser(playwright.firefox);
|
||||
await downloadBrowser(playwright.webkit);
|
||||
const crExecutablePath = await downloadBrowserWithProgressBar(path.join(__dirname, '.local-browsers', 'chromium'), 'chromium');
|
||||
const ffExecutablePath = await downloadBrowserWithProgressBar(path.join(__dirname, '.local-browsers', 'firefox'), 'firefox');
|
||||
const wkExecutablePath = await downloadBrowserWithProgressBar(path.join(__dirname, '.local-browsers', 'webkit'), 'webkit');
|
||||
await fs.promises.writeFile(path.join(__dirname, '.downloaded-browsers.json'), JSON.stringify({crExecutablePath, ffExecutablePath, wkExecutablePath, }));
|
||||
})();
|
||||
|
@ -17,110 +17,152 @@
|
||||
|
||||
import * as extract from 'extract-zip';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as util from 'util';
|
||||
import { execSync } from 'child_process';
|
||||
import * as ProxyAgent from 'https-proxy-agent';
|
||||
import * as path from 'path';
|
||||
import * as platform from '../platform';
|
||||
import { getProxyForUrl } from 'proxy-from-env';
|
||||
import * as removeRecursive from 'rimraf';
|
||||
import * as URL from 'url';
|
||||
import { assert } from '../helper';
|
||||
import * as platform from '../platform';
|
||||
|
||||
const readdirAsync = platform.promisify(fs.readdir.bind(fs));
|
||||
const mkdirAsync = platform.promisify(fs.mkdir.bind(fs));
|
||||
const unlinkAsync = platform.promisify(fs.unlink.bind(fs));
|
||||
const chmodAsync = platform.promisify(fs.chmod.bind(fs));
|
||||
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
||||
|
||||
function existsAsync(filePath: string): Promise<boolean> {
|
||||
let fulfill: (exists: boolean) => void;
|
||||
const promise = new Promise<boolean>(x => fulfill = x);
|
||||
fs.access(filePath, err => fulfill(!err));
|
||||
return promise;
|
||||
}
|
||||
const DEFAULT_DOWNLOAD_HOSTS = {
|
||||
chromium: 'https://storage.googleapis.com',
|
||||
firefox: 'https://playwright.azureedge.net',
|
||||
webkit: 'https://playwright.azureedge.net',
|
||||
};
|
||||
|
||||
type ParamsGetter = (platform: string, revision: string) => { downloadUrl: string, executablePath: string };
|
||||
const DOWNLOAD_URLS = {
|
||||
chromium: {
|
||||
'linux': '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip',
|
||||
'mac10.14': '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip',
|
||||
'mac10.15': '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip',
|
||||
'win32': '%s/chromium-browser-snapshots/Win/%d/chrome-win.zip',
|
||||
'win64': '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win.zip',
|
||||
},
|
||||
firefox: {
|
||||
'linux': '%s/builds/firefox/%s/firefox-linux.zip',
|
||||
'mac10.14': '%s/builds/firefox/%s/firefox-mac.zip',
|
||||
'mac10.15': '%s/builds/firefox/%s/firefox-mac.zip',
|
||||
'win32': '%s/builds/firefox/%s/firefox-win32.zip',
|
||||
'win64': '%s/builds/firefox/%s/firefox-win64.zip',
|
||||
},
|
||||
webkit: {
|
||||
'linux': '%s/builds/webkit/%s/minibrowser-gtk-wpe.zip',
|
||||
'mac10.14': '%s/builds/webkit/%s/minibrowser-mac-10.14.zip',
|
||||
'mac10.15': '%s/builds/webkit/%s/minibrowser-mac-10.15.zip',
|
||||
'win32': '%s/builds/webkit/%s/minibrowser-win64.zip',
|
||||
'win64': '%s/builds/webkit/%s/minibrowser-win64.zip',
|
||||
},
|
||||
};
|
||||
|
||||
const RELATIVE_EXECUTABLE_PATHS = {
|
||||
chromium: {
|
||||
'linux': ['chrome-linux', 'chrome'],
|
||||
'mac10.14': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'],
|
||||
'mac10.15': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'],
|
||||
'win32': ['chrome-win', 'chrome.exe'],
|
||||
'win64': ['chrome-win', 'chrome.exe'],
|
||||
},
|
||||
firefox: {
|
||||
'linux': ['firefox', 'firefox'],
|
||||
'mac10.14': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'],
|
||||
'mac10.15': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'],
|
||||
'win32': ['firefox', 'firefox.exe'],
|
||||
'win64': ['firefox', 'firefox.exe'],
|
||||
},
|
||||
webkit: {
|
||||
'linux': ['pw_run.sh'],
|
||||
'mac10.14': ['pw_run.sh'],
|
||||
'mac10.15': ['pw_run.sh'],
|
||||
'win32': ['MiniBrowser.exe'],
|
||||
'win64': ['MiniBrowser.exe'],
|
||||
},
|
||||
};
|
||||
|
||||
export type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void;
|
||||
export type BrowserName = ('chromium'|'webkit'|'firefox');
|
||||
export type BrowserPlatform = ('win32'|'win64'|'mac10.14'|'mac10.15'|'linux');
|
||||
|
||||
export class BrowserFetcher {
|
||||
private _downloadsFolder: string;
|
||||
private _platform: string;
|
||||
private _preferredRevision: string;
|
||||
private _params: ParamsGetter;
|
||||
export type DownloadOptions = {
|
||||
browser: BrowserName,
|
||||
revision: string,
|
||||
downloadPath: string,
|
||||
platform?: BrowserPlatform,
|
||||
host?: string,
|
||||
progress?: OnProgressCallback,
|
||||
};
|
||||
|
||||
constructor(downloadsFolder: string, platform: string, preferredRevision: string, params: ParamsGetter) {
|
||||
this._downloadsFolder = downloadsFolder;
|
||||
this._platform = platform;
|
||||
this._preferredRevision = preferredRevision;
|
||||
this._params = params;
|
||||
const CURRENT_HOST_PLATFORM = ((): string => {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin') {
|
||||
const macVersion = execSync('sw_vers -productVersion').toString('utf8').trim().split('.').slice(0, 2).join('.');
|
||||
return `mac${macVersion}`;
|
||||
}
|
||||
if (platform === 'linux')
|
||||
return 'linux';
|
||||
if (platform === 'win32')
|
||||
return os.arch() === 'x64' ? 'win64' : 'win32';
|
||||
return platform;
|
||||
})();
|
||||
|
||||
canDownload(revision: string = this._preferredRevision): Promise<boolean> {
|
||||
const url = this._params(this._platform, revision).downloadUrl;
|
||||
let resolve: (result: boolean) => void = () => {};
|
||||
const promise = new Promise<boolean>(x => resolve = x);
|
||||
const request = httpRequest(url, 'HEAD', response => {
|
||||
resolve(response.statusCode === 200);
|
||||
});
|
||||
request.on('error', (error: any) => {
|
||||
console.error(error);
|
||||
resolve(false);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
async download(revision: string = this._preferredRevision, progressCallback?: OnProgressCallback): Promise<BrowserFetcherRevisionInfo> {
|
||||
const url = this._params(this._platform, revision).downloadUrl;
|
||||
const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
if (await existsAsync(folderPath))
|
||||
return this.revisionInfo(revision);
|
||||
if (!(await existsAsync(this._downloadsFolder)))
|
||||
await mkdirAsync(this._downloadsFolder);
|
||||
try {
|
||||
await downloadFile(url, zipPath, progressCallback);
|
||||
await extractZip(zipPath, folderPath);
|
||||
} finally {
|
||||
if (await existsAsync(zipPath))
|
||||
await unlinkAsync(zipPath);
|
||||
}
|
||||
const revisionInfo = this.revisionInfo(revision);
|
||||
if (revisionInfo)
|
||||
await chmodAsync(revisionInfo.executablePath, 0o755);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
async localRevisions(): Promise<string[]> {
|
||||
if (!await existsAsync(this._downloadsFolder))
|
||||
return [];
|
||||
const fileNames: string[] = await readdirAsync(this._downloadsFolder);
|
||||
return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry!.revision);
|
||||
}
|
||||
|
||||
async remove(revision: string = this._preferredRevision) {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
|
||||
await new Promise(fulfill => removeRecursive(folderPath, fulfill));
|
||||
}
|
||||
|
||||
revisionInfo(revision: string = this._preferredRevision): BrowserFetcherRevisionInfo {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
const params = this._params(this._platform, revision);
|
||||
const local = fs.existsSync(folderPath);
|
||||
return {revision, executablePath: path.join(folderPath, params.executablePath), folderPath, local, url: params.downloadUrl};
|
||||
}
|
||||
|
||||
_getFolderPath(revision: string): string {
|
||||
return path.join(this._downloadsFolder, this._platform + '-' + revision);
|
||||
}
|
||||
function revisionURL(options: DownloadOptions): string {
|
||||
const {
|
||||
browser,
|
||||
revision,
|
||||
platform = CURRENT_HOST_PLATFORM,
|
||||
host = DEFAULT_DOWNLOAD_HOSTS[browser],
|
||||
} = options;
|
||||
assert(revision, `'revision' must be specified`);
|
||||
assert(DOWNLOAD_URLS[browser], 'Unsupported browser: ' + browser);
|
||||
const urlTemplate = (DOWNLOAD_URLS[browser] as any)[platform];
|
||||
assert(urlTemplate, `ERROR: Playwright does not support ${browser} on ${platform}`);
|
||||
return util.format(urlTemplate, host, revision);
|
||||
}
|
||||
|
||||
function parseFolderPath(folderPath: string): { platform: string; revision: string; } | null {
|
||||
const name = path.basename(folderPath);
|
||||
const splits = name.split('-');
|
||||
if (splits.length !== 2)
|
||||
return null;
|
||||
const [platform, revision] = splits;
|
||||
return {platform, revision};
|
||||
export async function downloadBrowser(options: DownloadOptions): Promise<string> {
|
||||
const {
|
||||
browser,
|
||||
revision,
|
||||
downloadPath,
|
||||
platform = CURRENT_HOST_PLATFORM,
|
||||
progress,
|
||||
} = options;
|
||||
assert(downloadPath, '`downloadPath` must be provided');
|
||||
const url = revisionURL(options);
|
||||
const zipPath = path.join(os.tmpdir(), `playwright-download-${browser}-${platform}-${revision}.zip`);
|
||||
if (await existsAsync(downloadPath))
|
||||
throw new Error('ERROR: downloadPath folder already exists!');
|
||||
try {
|
||||
await downloadFile(url, zipPath, progress);
|
||||
// await mkdirAsync(downloadPath, {recursive: true});
|
||||
await extractZip(zipPath, downloadPath);
|
||||
} finally {
|
||||
if (await existsAsync(zipPath))
|
||||
await unlinkAsync(zipPath);
|
||||
}
|
||||
const executablePath = path.join(downloadPath, ...RELATIVE_EXECUTABLE_PATHS[browser][platform as BrowserPlatform]);
|
||||
await chmodAsync(executablePath, 0o755);
|
||||
return executablePath;
|
||||
}
|
||||
|
||||
export async function canDownload(options: DownloadOptions): Promise<boolean> {
|
||||
const url = revisionURL(options);
|
||||
let resolve: (result: boolean) => void = () => {};
|
||||
const promise = new Promise<boolean>(x => resolve = x);
|
||||
const request = httpRequest(url, 'HEAD', response => {
|
||||
resolve(response.statusCode === 200);
|
||||
});
|
||||
request.on('error', (error: any) => {
|
||||
console.error(error);
|
||||
resolve(false);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
function downloadFile(url: string, destinationPath: string, progressCallback: OnProgressCallback | undefined): Promise<any> {
|
||||
@ -199,17 +241,3 @@ function httpRequest(url: string, method: string, response: (r: any) => void) {
|
||||
request.end();
|
||||
return request;
|
||||
}
|
||||
|
||||
export type BrowserFetcherOptions = {
|
||||
platform?: string,
|
||||
path?: string,
|
||||
host?: string,
|
||||
};
|
||||
|
||||
export type BrowserFetcherRevisionInfo = {
|
||||
folderPath: string,
|
||||
executablePath: string,
|
||||
url: string,
|
||||
local: boolean,
|
||||
revision: string,
|
||||
};
|
||||
|
@ -17,7 +17,6 @@
|
||||
import { Browser, ConnectOptions } from '../browser';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { BrowserServer } from './browserServer';
|
||||
import { OnProgressCallback } from './browserFetcher';
|
||||
|
||||
export type BrowserArgOptions = {
|
||||
headless?: boolean,
|
||||
@ -47,5 +46,4 @@ export interface BrowserType {
|
||||
launchServer(options?: LaunchOptions & { port?: number }): Promise<BrowserServer>;
|
||||
launchPersistentContext(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext>;
|
||||
connect(options: ConnectOptions): Promise<Browser>;
|
||||
downloadBrowserIfNeeded(progress?: OnProgressCallback): Promise<void>;
|
||||
}
|
||||
|
@ -18,9 +18,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import { BrowserFetcher, OnProgressCallback, BrowserFetcherOptions } from '../server/browserFetcher';
|
||||
import { assert, helper } from '../helper';
|
||||
import { helper } from '../helper';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import * as platform from '../platform';
|
||||
import { TimeoutError } from '../errors';
|
||||
@ -35,14 +33,12 @@ import { ConnectionTransport } from '../transport';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
|
||||
export class Chromium implements BrowserType {
|
||||
private _downloadPath: string;
|
||||
private _downloadHost: string;
|
||||
readonly _revision: string;
|
||||
private _executablePath: (string|undefined);
|
||||
|
||||
constructor(downloadPath: string, downloadHost: (string|undefined), preferredRevision: string) {
|
||||
this._downloadPath = downloadPath;
|
||||
this._downloadHost = downloadHost || 'https://storage.googleapis.com';
|
||||
this._revision = preferredRevision;
|
||||
executablePath(): string {
|
||||
if (!this._executablePath)
|
||||
throw new Error('No executable path!');
|
||||
return this._executablePath;
|
||||
}
|
||||
|
||||
name() {
|
||||
@ -97,16 +93,12 @@ export class Chromium implements BrowserType {
|
||||
else
|
||||
chromeArguments.push(...args);
|
||||
|
||||
let chromeExecutable = executablePath;
|
||||
if (!executablePath) {
|
||||
const {missingText, executablePath} = this._resolveExecutablePath();
|
||||
if (missingText)
|
||||
throw new Error(missingText);
|
||||
chromeExecutable = executablePath;
|
||||
}
|
||||
const chromeExecutable = executablePath || this._executablePath;
|
||||
if (!chromeExecutable)
|
||||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
|
||||
let browserServer: BrowserServer | undefined = undefined;
|
||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||
executablePath: chromeExecutable!,
|
||||
executablePath: chromeExecutable,
|
||||
args: chromeArguments,
|
||||
env,
|
||||
handleSIGINT,
|
||||
@ -134,7 +126,7 @@ export class Chromium implements BrowserType {
|
||||
let transport: PipeTransport | undefined;
|
||||
let browserWSEndpoint: string | undefined;
|
||||
if (launchType === 'server') {
|
||||
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chromium! The only Chromium revision guaranteed to work is r${this._revision}`);
|
||||
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chromium!`);
|
||||
const match = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError);
|
||||
browserWSEndpoint = match[1];
|
||||
browserServer = new BrowserServer(launchedProcess, gracefullyClose, browserWSEndpoint);
|
||||
@ -153,10 +145,6 @@ export class Chromium implements BrowserType {
|
||||
});
|
||||
}
|
||||
|
||||
executablePath(): string {
|
||||
return this._resolveExecutablePath().executablePath;
|
||||
}
|
||||
|
||||
private _defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string, port: number): string[] {
|
||||
const {
|
||||
devtools = false,
|
||||
@ -193,71 +181,6 @@ export class Chromium implements BrowserType {
|
||||
|
||||
return chromeArguments;
|
||||
}
|
||||
|
||||
async downloadBrowserIfNeeded(onProgress?: OnProgressCallback) {
|
||||
const fetcher = this._createBrowserFetcher();
|
||||
const revisionInfo = fetcher.revisionInfo();
|
||||
// Do nothing if the revision is already downloaded.
|
||||
if (revisionInfo.local)
|
||||
return;
|
||||
await fetcher.download(revisionInfo.revision, onProgress);
|
||||
}
|
||||
|
||||
_createBrowserFetcher(options: BrowserFetcherOptions = {}): BrowserFetcher {
|
||||
const downloadURLs = {
|
||||
linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip',
|
||||
mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip',
|
||||
win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip',
|
||||
win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip',
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
path: path.join(this._downloadPath, '.local-chromium'),
|
||||
host: this._downloadHost,
|
||||
platform: (() => {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin')
|
||||
return 'mac';
|
||||
if (platform === 'linux')
|
||||
return 'linux';
|
||||
if (platform === 'win32')
|
||||
return os.arch() === 'x64' ? 'win64' : 'win32';
|
||||
return platform;
|
||||
})()
|
||||
};
|
||||
options = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
assert(!!(downloadURLs as any)[options.platform!], 'Unsupported platform: ' + options.platform);
|
||||
|
||||
return new BrowserFetcher(options.path!, options.platform!, this._revision, (platform: string, revision: string) => {
|
||||
let archiveName = '';
|
||||
let executablePath = '';
|
||||
if (platform === 'linux') {
|
||||
archiveName = 'chrome-linux';
|
||||
executablePath = path.join(archiveName, 'chrome');
|
||||
} else if (platform === 'mac') {
|
||||
archiveName = 'chrome-mac';
|
||||
executablePath = path.join(archiveName, 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
|
||||
} else if (platform === 'win32' || platform === 'win64') {
|
||||
// Windows archive name changed at r591479.
|
||||
archiveName = parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
|
||||
executablePath = path.join(archiveName, 'chrome.exe');
|
||||
}
|
||||
return {
|
||||
downloadUrl: util.format((downloadURLs as any)[platform], options.host, revision, archiveName),
|
||||
executablePath
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_resolveExecutablePath(): { executablePath: string; missingText: string | null; } {
|
||||
const browserFetcher = this._createBrowserFetcher();
|
||||
const revisionInfo = browserFetcher.revisionInfo();
|
||||
const missingText = !revisionInfo.local ? `Chromium revision is not downloaded. Run "npm install"` : null;
|
||||
return { executablePath: revisionInfo.executablePath, missingText };
|
||||
}
|
||||
}
|
||||
|
||||
const mkdtempAsync = platform.promisify(fs.mkdtemp);
|
||||
|
@ -18,16 +18,14 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import { ConnectOptions, LaunchType } from '../browser';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { TimeoutError } from '../errors';
|
||||
import { Events } from '../events';
|
||||
import { FFBrowser } from '../firefox/ffBrowser';
|
||||
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
|
||||
import { assert, helper } from '../helper';
|
||||
import { helper } from '../helper';
|
||||
import * as platform from '../platform';
|
||||
import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback } from './browserFetcher';
|
||||
import { BrowserServer } from './browserServer';
|
||||
import { BrowserArgOptions, BrowserType, LaunchOptions } from './browserType';
|
||||
import { launchProcess, waitForLine } from './processLauncher';
|
||||
@ -35,23 +33,12 @@ import { launchProcess, waitForLine } from './processLauncher';
|
||||
const mkdtempAsync = platform.promisify(fs.mkdtemp);
|
||||
|
||||
export class Firefox implements BrowserType {
|
||||
private _downloadPath: string;
|
||||
private _downloadHost: string;
|
||||
readonly _revision: string;
|
||||
private _executablePath: (string|undefined);
|
||||
|
||||
constructor(downloadPath: string, downloadHost: (string|undefined), preferredRevision: string) {
|
||||
this._downloadPath = downloadPath;
|
||||
this._downloadHost = downloadHost || 'https://playwright.azureedge.net';
|
||||
this._revision = preferredRevision;
|
||||
}
|
||||
|
||||
async downloadBrowserIfNeeded(onProgress?: OnProgressCallback) {
|
||||
const fetcher = this._createBrowserFetcher();
|
||||
const revisionInfo = fetcher.revisionInfo();
|
||||
// Do nothing if the revision is already downloaded.
|
||||
if (revisionInfo.local)
|
||||
return;
|
||||
await fetcher.download(revisionInfo.revision, onProgress);
|
||||
executablePath(): string {
|
||||
if (!this._executablePath)
|
||||
throw new Error('No executable path!');
|
||||
return this._executablePath;
|
||||
}
|
||||
|
||||
name() {
|
||||
@ -116,13 +103,9 @@ export class Firefox implements BrowserType {
|
||||
else
|
||||
firefoxArguments.push(...args);
|
||||
|
||||
let firefoxExecutable = executablePath;
|
||||
if (!firefoxExecutable) {
|
||||
const {missingText, executablePath} = this._resolveExecutablePath();
|
||||
if (missingText)
|
||||
throw new Error(missingText);
|
||||
firefoxExecutable = executablePath;
|
||||
}
|
||||
const firefoxExecutable = executablePath || this._executablePath;
|
||||
if (!firefoxExecutable)
|
||||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
|
||||
|
||||
let browserServer: BrowserServer | undefined = undefined;
|
||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||
@ -168,10 +151,6 @@ export class Firefox implements BrowserType {
|
||||
});
|
||||
}
|
||||
|
||||
executablePath(): string {
|
||||
return this._resolveExecutablePath().executablePath;
|
||||
}
|
||||
|
||||
private _defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string, port: number): string[] {
|
||||
const {
|
||||
devtools = false,
|
||||
@ -204,55 +183,5 @@ export class Firefox implements BrowserType {
|
||||
firefoxArguments.push('about:blank');
|
||||
return firefoxArguments;
|
||||
}
|
||||
|
||||
_createBrowserFetcher(options: BrowserFetcherOptions = {}): BrowserFetcher {
|
||||
const downloadURLs = {
|
||||
linux: '%s/builds/firefox/%s/firefox-linux.zip',
|
||||
mac: '%s/builds/firefox/%s/firefox-mac.zip',
|
||||
win32: '%s/builds/firefox/%s/firefox-win32.zip',
|
||||
win64: '%s/builds/firefox/%s/firefox-win64.zip',
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
path: path.join(this._downloadPath, '.local-firefox'),
|
||||
host: this._downloadHost,
|
||||
platform: (() => {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin')
|
||||
return 'mac';
|
||||
if (platform === 'linux')
|
||||
return 'linux';
|
||||
if (platform === 'win32')
|
||||
return os.arch() === 'x64' ? 'win64' : 'win32';
|
||||
return platform;
|
||||
})()
|
||||
};
|
||||
options = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
assert(!!(downloadURLs as any)[options.platform!], 'Unsupported platform: ' + options.platform);
|
||||
|
||||
return new BrowserFetcher(options.path!, options.platform!, this._revision, (platform: string, revision: string) => {
|
||||
let executablePath = '';
|
||||
if (platform === 'linux')
|
||||
executablePath = path.join('firefox', 'firefox');
|
||||
else if (platform === 'mac')
|
||||
executablePath = path.join('firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox');
|
||||
else if (platform === 'win32' || platform === 'win64')
|
||||
executablePath = path.join('firefox', 'firefox.exe');
|
||||
return {
|
||||
downloadUrl: util.format((downloadURLs as any)[platform], options.host, revision),
|
||||
executablePath
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_resolveExecutablePath() {
|
||||
const browserFetcher = this._createBrowserFetcher();
|
||||
const revisionInfo = browserFetcher.revisionInfo();
|
||||
const missingText = !revisionInfo.local ? `Firefox revision is not downloaded. Run "npm install"` : null;
|
||||
return { executablePath: revisionInfo.executablePath, missingText };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,17 +23,13 @@ import { Chromium } from './chromium';
|
||||
import { WebKit } from './webkit';
|
||||
import { Firefox } from './firefox';
|
||||
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
for (const className in api) {
|
||||
if (typeof (api as any)[className] === 'function')
|
||||
helper.installApiHooks(className[0].toLowerCase() + className.substring(1), (api as any)[className]);
|
||||
}
|
||||
|
||||
type PlaywrightOptions = {
|
||||
downloadPath: string,
|
||||
browsers: Array<('firefox'|'webkit'|'chromium')>,
|
||||
respectEnvironmentVariables: boolean,
|
||||
};
|
||||
|
||||
export class Playwright {
|
||||
@ -46,25 +42,15 @@ export class Playwright {
|
||||
|
||||
constructor(options: PlaywrightOptions) {
|
||||
const {
|
||||
downloadPath,
|
||||
browsers,
|
||||
respectEnvironmentVariables,
|
||||
} = options;
|
||||
this.devices = DeviceDescriptors;
|
||||
this.errors = { TimeoutError };
|
||||
const downloadHost = respectEnvironmentVariables ? getFromENV('PLAYWRIGHT_DOWNLOAD_HOST') : undefined;
|
||||
if (browsers.includes('chromium'))
|
||||
this.chromium = new Chromium(downloadPath, downloadHost, packageJSON.playwright.chromium_revision);
|
||||
this.chromium = new Chromium();
|
||||
if (browsers.includes('webkit'))
|
||||
this.webkit = new WebKit(downloadPath, downloadHost, packageJSON.playwright.webkit_revision);
|
||||
this.webkit = new WebKit();
|
||||
if (browsers.includes('firefox'))
|
||||
this.firefox = new Firefox(downloadPath, downloadHost, packageJSON.playwright.firefox_revision);
|
||||
this.firefox = new Firefox();
|
||||
}
|
||||
}
|
||||
|
||||
function getFromENV(name: string): (string|undefined) {
|
||||
let value = process.env[name];
|
||||
value = value || process.env[`npm_config_${name.toLowerCase()}`];
|
||||
value = value || process.env[`npm_package_config_${name.toLowerCase()}`];
|
||||
return value;
|
||||
}
|
||||
|
@ -15,17 +15,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BrowserFetcher, OnProgressCallback, BrowserFetcherOptions } from './browserFetcher';
|
||||
import { WKBrowser } from '../webkit/wkBrowser';
|
||||
import { execSync } from 'child_process';
|
||||
import { PipeTransport } from './pipeTransport';
|
||||
import { launchProcess } from './processLauncher';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as platform from '../platform';
|
||||
import * as util from 'util';
|
||||
import * as os from 'os';
|
||||
import { assert, helper } from '../helper';
|
||||
import { helper } from '../helper';
|
||||
import { kBrowserCloseMessageId } from '../webkit/wkConnection';
|
||||
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
|
||||
import { ConnectionTransport } from '../transport';
|
||||
@ -36,29 +33,18 @@ import { Events } from '../events';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
|
||||
export class WebKit implements BrowserType {
|
||||
private _downloadPath: string;
|
||||
private _downloadHost: string;
|
||||
readonly _revision: string;
|
||||
private _executablePath: (string|undefined);
|
||||
|
||||
constructor(downloadPath: string, downloadHost: (string|undefined), preferredRevision: string) {
|
||||
this._downloadPath = downloadPath;
|
||||
this._downloadHost = downloadHost || 'https://playwright.azureedge.net';
|
||||
this._revision = preferredRevision;
|
||||
executablePath(): string {
|
||||
if (!this._executablePath)
|
||||
throw new Error('No executable path!');
|
||||
return this._executablePath;
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'webkit';
|
||||
}
|
||||
|
||||
async downloadBrowserIfNeeded(onProgress?: OnProgressCallback) {
|
||||
const fetcher = this._createBrowserFetcher();
|
||||
const revisionInfo = fetcher.revisionInfo();
|
||||
// Do nothing if the revision is already downloaded.
|
||||
if (revisionInfo.local)
|
||||
return;
|
||||
await fetcher.download(revisionInfo.revision, onProgress);
|
||||
}
|
||||
|
||||
async launch(options?: LaunchOptions & { slowMo?: number }): Promise<WKBrowser> {
|
||||
if (options && (options as any).userDataDir)
|
||||
throw new Error('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||
@ -106,18 +92,14 @@ export class WebKit implements BrowserType {
|
||||
else
|
||||
webkitArguments.push(...args);
|
||||
|
||||
let webkitExecutable = executablePath;
|
||||
if (!executablePath) {
|
||||
const {missingText, executablePath} = this._resolveExecutablePath();
|
||||
if (missingText)
|
||||
throw new Error(missingText);
|
||||
webkitExecutable = executablePath;
|
||||
}
|
||||
const webkitExecutable = executablePath || this._executablePath;
|
||||
if (!webkitExecutable)
|
||||
throw new Error(`No executable path is specified.`);
|
||||
|
||||
let transport: ConnectionTransport | undefined = undefined;
|
||||
let browserServer: BrowserServer | undefined = undefined;
|
||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||
executablePath: webkitExecutable!,
|
||||
executablePath: webkitExecutable,
|
||||
args: webkitArguments,
|
||||
env: { ...env, CURL_COOKIE_JAR_PATH: path.join(userDataDir!, 'cookiejar.db') },
|
||||
handleSIGINT,
|
||||
@ -155,10 +137,6 @@ export class WebKit implements BrowserType {
|
||||
});
|
||||
}
|
||||
|
||||
executablePath(): string {
|
||||
return this._resolveExecutablePath().executablePath;
|
||||
}
|
||||
|
||||
_defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string, port: number): string[] {
|
||||
const {
|
||||
devtools = false,
|
||||
@ -182,67 +160,12 @@ export class WebKit implements BrowserType {
|
||||
webkitArguments.push(...args);
|
||||
return webkitArguments;
|
||||
}
|
||||
|
||||
_createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher {
|
||||
const downloadURLs = {
|
||||
linux: '%s/builds/webkit/%s/minibrowser-gtk-wpe.zip',
|
||||
mac: '%s/builds/webkit/%s/minibrowser-mac-%s.zip',
|
||||
win64: '%s/builds/webkit/%s/minibrowser-win64.zip',
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
path: path.join(this._downloadPath, '.local-webkit'),
|
||||
host: this._downloadHost,
|
||||
platform: (() => {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin')
|
||||
return 'mac';
|
||||
if (platform === 'linux')
|
||||
return 'linux';
|
||||
if (platform === 'win32')
|
||||
return 'win64';
|
||||
return platform;
|
||||
})()
|
||||
};
|
||||
options = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
assert(!!(downloadURLs as any)[options.platform!], 'Unsupported platform: ' + options.platform);
|
||||
|
||||
return new BrowserFetcher(options.path!, options.platform!, this._revision, (platform: string, revision: string) => {
|
||||
return {
|
||||
downloadUrl: (platform === 'mac') ?
|
||||
util.format(downloadURLs[platform], options!.host, revision, getMacVersion()) :
|
||||
util.format((downloadURLs as any)[platform], options!.host, revision),
|
||||
executablePath: platform.startsWith('win') ? 'MiniBrowser.exe' : 'pw_run.sh',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_resolveExecutablePath(): { executablePath: string; missingText: string | null; } {
|
||||
const browserFetcher = this._createBrowserFetcher();
|
||||
const revisionInfo = browserFetcher.revisionInfo();
|
||||
const missingText = !revisionInfo.local ? `WebKit revision is not downloaded. Run "npm install"` : null;
|
||||
return { executablePath: revisionInfo.executablePath, missingText };
|
||||
}
|
||||
}
|
||||
|
||||
const mkdtempAsync = platform.promisify(fs.mkdtemp);
|
||||
|
||||
const WEBKIT_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');
|
||||
|
||||
let cachedMacVersion: string | undefined = undefined;
|
||||
|
||||
function getMacVersion(): string {
|
||||
if (!cachedMacVersion) {
|
||||
const [major, minor] = execSync('sw_vers -productVersion').toString('utf8').trim().split('.');
|
||||
assert(+major === 10 && +minor >= 14, 'Error: unsupported macOS version, macOS 10.14 and newer are supported');
|
||||
cachedMacVersion = major + '.' + minor;
|
||||
}
|
||||
return cachedMacVersion;
|
||||
}
|
||||
|
||||
class SequenceNumberMixer<V> {
|
||||
static _lastSequenceNumber = 1;
|
||||
private _values = new Map<number, V>();
|
||||
|
@ -91,36 +91,6 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, b
|
||||
});
|
||||
});
|
||||
|
||||
describe('BrowserFetcher', function() {
|
||||
it('should download and extract linux binary', async({server}) => {
|
||||
const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
|
||||
const browserFetcher = browserType._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 = WIN ? 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('BrowserContext', function() {
|
||||
it('should not create pages automatically', async function() {
|
||||
const browser = await browserType.launch();
|
||||
|
@ -285,9 +285,8 @@ const utils = module.exports = {
|
||||
makeUserDataDir: async function() {
|
||||
return await mkdtempAsync(path.join(os.tmpdir(), 'playwright_dev_profile-'));
|
||||
},
|
||||
|
||||
|
||||
removeUserDataDir: async function(dir) {
|
||||
await removeFolderAsync(dir).catch(e => {});
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -16,11 +16,11 @@
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const playwright = require('..').chromium;
|
||||
const browserFetcher = require('../lib/server/browserFetcher.js');
|
||||
const https = require('https');
|
||||
const SUPPORTER_PLATFORMS = ['linux', 'mac', 'win32', 'win64'];
|
||||
|
||||
const fetchers = SUPPORTER_PLATFORMS.map(platform => playwright._createBrowserFetcher({platform}));
|
||||
const fetcherOptions = SUPPORTER_PLATFORMS.map(platform => ({platform: platform === 'mac' ? 'mac10.15' : platform, browser: 'chromium'}));
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
@ -100,7 +100,7 @@ async function checkRangeAvailability(fromRevision, toRevision, stopWhenAllAvail
|
||||
* @return {boolean}
|
||||
*/
|
||||
async function checkAndDrawRevisionAvailability(table, name, revision) {
|
||||
const promises = fetchers.map(fetcher => fetcher.canDownload(revision));
|
||||
const promises = fetcherOptions.map(options => browserFetcher.canDownload({...options, revision}));
|
||||
const availability = await Promise.all(promises);
|
||||
const allAvailable = availability.every(e => !!e);
|
||||
const values = [name + ' ' + (allAvailable ? colors.green + revision + colors.reset : revision)];
|
||||
|
@ -4,29 +4,26 @@ const fs = require('fs');
|
||||
const StreamZip = require('node-stream-zip');
|
||||
const vm = require('vm');
|
||||
const os = require('os');
|
||||
const util = require('util');
|
||||
|
||||
async function generateChromiunProtocol(revision) {
|
||||
async function generateChromiumProtocol(executablePath) {
|
||||
const outputPath = path.join(__dirname, '..', '..', 'src', 'chromium', 'protocol.ts');
|
||||
if (revision.local && fs.existsSync(outputPath))
|
||||
return;
|
||||
const playwright = await require('../../index').chromium;
|
||||
const browserServer = await playwright.launchServer({ executablePath: revision.executablePath, args: ['--no-sandbox'] });
|
||||
const browserServer = await playwright.launchServer({ executablePath, args: ['--no-sandbox'] });
|
||||
const origin = browserServer.wsEndpoint().match(/ws:\/\/([0-9A-Za-z:\.]*)\//)[1];
|
||||
const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`http://${origin}/json/protocol`);
|
||||
const json = JSON.parse(await page.evaluate(() => document.documentElement.innerText));
|
||||
await browserServer.close();
|
||||
fs.writeFileSync(outputPath, jsonToTS(json));
|
||||
await fs.promises.writeFile(outputPath, jsonToTS(json));
|
||||
console.log(`Wrote protocol.ts to ${path.relative(process.cwd(), outputPath)}`);
|
||||
}
|
||||
|
||||
async function generateWebKitProtocol(revision) {
|
||||
async function generateWebKitProtocol(folderPath) {
|
||||
const outputPath = path.join(__dirname, '..', '..', 'src', 'webkit', 'protocol.ts');
|
||||
if (revision.local && fs.existsSync(outputPath))
|
||||
return;
|
||||
const json = JSON.parse(fs.readFileSync(path.join(revision.folderPath, 'protocol.json'), 'utf8'));
|
||||
fs.writeFileSync(outputPath, jsonToTS({domains: json}));
|
||||
const json = JSON.parse(await fs.promises.readFile(path.join(folderPath, 'protocol.json'), 'utf8'));
|
||||
await fs.promises.writeFile(outputPath, jsonToTS({domains: json}));
|
||||
console.log(`Wrote protocol.ts for WebKit to ${path.relative(process.cwd(), outputPath)}`);
|
||||
}
|
||||
|
||||
@ -118,13 +115,11 @@ function typeOfProperty(property, domain) {
|
||||
return property.type;
|
||||
}
|
||||
|
||||
async function generateFirefoxProtocol(revision) {
|
||||
async function generateFirefoxProtocol(executablePath) {
|
||||
const outputPath = path.join(__dirname, '..', '..', 'src', 'firefox', 'protocol.ts');
|
||||
if (revision.local && fs.existsSync(outputPath))
|
||||
return;
|
||||
const omnija = os.platform() === 'darwin' ?
|
||||
path.join(revision.executablePath, '..', '..', 'Resources', 'omni.ja') :
|
||||
path.join(revision.executablePath, '..', 'omni.ja');
|
||||
path.join(executablePath, '..', '..', 'Resources', 'omni.ja') :
|
||||
path.join(executablePath, '..', 'omni.ja');
|
||||
const zip = new StreamZip({file: omnija, storeEntries: true});
|
||||
// @ts-ignore
|
||||
await new Promise(x => zip.on('ready', x));
|
||||
@ -164,7 +159,7 @@ async function generateFirefoxProtocol(revision) {
|
||||
}
|
||||
}
|
||||
const json = vm.runInContext(`(${inject})();${protocolJSCode}; this.protocol;`, ctx);
|
||||
fs.writeFileSync(outputPath, firefoxJSONToTS(json));
|
||||
await fs.promises.writeFile(outputPath, firefoxJSONToTS(json));
|
||||
console.log(`Wrote protocol.ts for Firefox to ${path.relative(process.cwd(), outputPath)}`);
|
||||
}
|
||||
|
||||
@ -216,4 +211,4 @@ function firefoxTypeToString(type, indent=' ') {
|
||||
return type['$type'];
|
||||
}
|
||||
|
||||
module.exports = {generateChromiunProtocol, generateFirefoxProtocol, generateWebKitProtocol};
|
||||
module.exports = {generateChromiumProtocol, generateFirefoxProtocol, generateWebKitProtocol};
|
||||
|
Loading…
Reference in New Issue
Block a user