chore: reuse BrowserFetcher between browsers (#177)
This commit is contained in:
parent
ba4cfe908c
commit
f38ab5d80f
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,7 +4,7 @@
|
|||
/test/output-webkit
|
||||
/test/test-user-data-dir*
|
||||
/.local-chromium/
|
||||
/.local-browser/
|
||||
/.local-firefox/
|
||||
/.local-webkit/
|
||||
/.dev_profile*
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -18,36 +18,12 @@
|
|||
import * as extract from 'extract-zip';
|
||||
import * as fs from 'fs';
|
||||
import * as ProxyAgent from 'https-proxy-agent';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
// @ts-ignore
|
||||
import { getProxyForUrl } from 'proxy-from-env';
|
||||
import * as removeRecursive from 'rimraf';
|
||||
import * as URL from 'url';
|
||||
import * as util from 'util';
|
||||
import { assert, helper } from '../helper';
|
||||
import {execSync} from 'child_process';
|
||||
|
||||
const DEFAULT_DOWNLOAD_HOST = 'https://playwrightaccount.blob.core.windows.net';
|
||||
|
||||
const supportedPlatforms = ['linux', 'mac'];
|
||||
const downloadURLs = {
|
||||
linux: '%s/builds/webkit/%s/minibrowser-linux.zip',
|
||||
mac: '%s/builds/webkit/%s/minibrowser-mac-%s.zip',
|
||||
};
|
||||
let cachedMacVersion = undefined;
|
||||
function getMacVersion() {
|
||||
if (!cachedMacVersion) {
|
||||
const [major, minor] = execSync('sw_vers -productVersion').toString('utf8').trim().split('.');
|
||||
cachedMacVersion = major + '.' + minor;
|
||||
}
|
||||
return cachedMacVersion;
|
||||
}
|
||||
|
||||
function downloadURL(platform: string, host: string, revision: string): string {
|
||||
if (platform === 'mac')
|
||||
return util.format(downloadURLs['mac'], host, revision, getMacVersion());
|
||||
return util.format(downloadURLs[platform], host, revision);
|
||||
}
|
||||
import { assert, helper } from './helper';
|
||||
|
||||
const readdirAsync = helper.promisify(fs.readdir.bind(fs));
|
||||
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
||||
|
|
@ -61,34 +37,23 @@ function existsAsync(filePath) {
|
|||
return promise;
|
||||
}
|
||||
|
||||
type ParamsGetter = (platform: string, revision: string) => { downloadUrl: string, executablePath: string };
|
||||
|
||||
export type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void;
|
||||
|
||||
export class BrowserFetcher {
|
||||
private _downloadsFolder: string;
|
||||
private _downloadHost: string;
|
||||
private _platform: string;
|
||||
private _params: ParamsGetter;
|
||||
|
||||
constructor(projectRoot: string, options: BrowserFetcherOptions = {}) {
|
||||
this._downloadsFolder = options.path || path.join(projectRoot, '.local-webkit');
|
||||
this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
|
||||
this._platform = options.platform || '';
|
||||
if (!this._platform) {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin')
|
||||
this._platform = 'mac';
|
||||
else if (platform === 'linux')
|
||||
this._platform = 'linux';
|
||||
else if (platform === 'win32')
|
||||
this._platform = 'linux'; // Windows gets linux binaries and uses WSL
|
||||
assert(this._platform, 'Unsupported platform: ' + os.platform());
|
||||
}
|
||||
assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
|
||||
}
|
||||
|
||||
platform(): string {
|
||||
return this._platform;
|
||||
constructor(downloadsFolder: string, platform: string, params: ParamsGetter) {
|
||||
this._downloadsFolder = downloadsFolder;
|
||||
this._platform = platform;
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
canDownload(revision: string): Promise<boolean> {
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
const url = this._params(this._platform, revision).downloadUrl;
|
||||
let resolve;
|
||||
const promise = new Promise<boolean>(x => resolve = x);
|
||||
const request = httpRequest(url, 'HEAD', response => {
|
||||
|
|
@ -100,8 +65,9 @@ export class BrowserFetcher {
|
|||
});
|
||||
return promise;
|
||||
}
|
||||
async download(revision: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise<BrowserFetcherRevisionInfo> {
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
|
||||
async download(revision: string, progressCallback: OnProgressCallback | null): 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))
|
||||
|
|
@ -136,14 +102,9 @@ export class BrowserFetcher {
|
|||
|
||||
revisionInfo(revision: string): BrowserFetcherRevisionInfo {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
let executablePath = '';
|
||||
if (this._platform === 'linux' || this._platform === 'mac')
|
||||
executablePath = path.join(folderPath, 'pw_run.sh');
|
||||
else
|
||||
throw new Error('Unsupported platform: ' + this._platform);
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
const params = this._params(this._platform, revision);
|
||||
const local = fs.existsSync(folderPath);
|
||||
return {revision, executablePath, folderPath, local, url};
|
||||
return {revision, executablePath: path.join(folderPath, params.executablePath), folderPath, local, url: params.downloadUrl};
|
||||
}
|
||||
|
||||
_getFolderPath(revision: string): string {
|
||||
|
|
@ -157,12 +118,10 @@ function parseFolderPath(folderPath: string): { platform: string; revision: stri
|
|||
if (splits.length !== 2)
|
||||
return null;
|
||||
const [platform, revision] = splits;
|
||||
if (!supportedPlatforms.includes(platform))
|
||||
return null;
|
||||
return {platform, revision};
|
||||
}
|
||||
|
||||
function downloadFile(url: string, destinationPath: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise<any> {
|
||||
function downloadFile(url: string, destinationPath: string, progressCallback: OnProgressCallback | null): Promise<any> {
|
||||
let fulfill, reject;
|
||||
let downloadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
|
|
@ -244,7 +203,7 @@ export type BrowserFetcherOptions = {
|
|||
host ?: string,
|
||||
};
|
||||
|
||||
type BrowserFetcherRevisionInfo = {
|
||||
export type BrowserFetcherRevisionInfo = {
|
||||
folderPath: string,
|
||||
executablePath: string,
|
||||
url: string,
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as extract from 'extract-zip';
|
||||
import * as fs from 'fs';
|
||||
import * as ProxyAgent from 'https-proxy-agent';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
// @ts-ignore
|
||||
import { getProxyForUrl } from 'proxy-from-env';
|
||||
import * as removeRecursive from 'rimraf';
|
||||
import * as URL from 'url';
|
||||
import * as util from 'util';
|
||||
import { assert, helper } from '../helper';
|
||||
|
||||
const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
|
||||
|
||||
const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
|
||||
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',
|
||||
};
|
||||
|
||||
function archiveName(platform: string, revision: string): string {
|
||||
if (platform === 'linux')
|
||||
return 'chrome-linux';
|
||||
if (platform === 'mac')
|
||||
return 'chrome-mac';
|
||||
if (platform === 'win32' || platform === 'win64') {
|
||||
// Windows archive name changed at r591479.
|
||||
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function downloadURL(platform: string, host: string, revision: string): string {
|
||||
return util.format(downloadURLs[platform], host, revision, archiveName(platform, revision));
|
||||
}
|
||||
|
||||
const readdirAsync = helper.promisify(fs.readdir.bind(fs));
|
||||
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
||||
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
|
||||
const chmodAsync = helper.promisify(fs.chmod.bind(fs));
|
||||
|
||||
function existsAsync(filePath) {
|
||||
let fulfill = null;
|
||||
const promise = new Promise(x => fulfill = x);
|
||||
fs.access(filePath, err => fulfill(!err));
|
||||
return promise;
|
||||
}
|
||||
|
||||
export class BrowserFetcher {
|
||||
private _downloadsFolder: string;
|
||||
private _downloadHost: string;
|
||||
private _platform: string;
|
||||
|
||||
constructor(projectRoot: string, options: BrowserFetcherOptions = {}) {
|
||||
this._downloadsFolder = options.path || path.join(projectRoot, '.local-chromium');
|
||||
this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
|
||||
this._platform = options.platform || '';
|
||||
if (!this._platform) {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin')
|
||||
this._platform = 'mac';
|
||||
else if (platform === 'linux')
|
||||
this._platform = 'linux';
|
||||
else if (platform === 'win32')
|
||||
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
|
||||
assert(this._platform, 'Unsupported platform: ' + os.platform());
|
||||
}
|
||||
assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
|
||||
}
|
||||
|
||||
platform(): string {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
canDownload(revision: string): Promise<boolean> {
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
let resolve;
|
||||
const promise = new Promise<boolean>(x => resolve = x);
|
||||
const request = httpRequest(url, 'HEAD', response => {
|
||||
resolve(response.statusCode === 200);
|
||||
});
|
||||
request.on('error', error => {
|
||||
console.error(error);
|
||||
resolve(false);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
async download(revision: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise<BrowserFetcherRevisionInfo> {
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
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 = 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) {
|
||||
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): BrowserFetcherRevisionInfo {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
let executablePath = '';
|
||||
if (this._platform === 'mac')
|
||||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
|
||||
else if (this._platform === 'linux')
|
||||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome');
|
||||
else if (this._platform === 'win32' || this._platform === 'win64')
|
||||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome.exe');
|
||||
else
|
||||
throw new Error('Unsupported platform: ' + this._platform);
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
const local = fs.existsSync(folderPath);
|
||||
return {revision, executablePath, folderPath, local, url};
|
||||
}
|
||||
|
||||
_getFolderPath(revision: string): string {
|
||||
return path.join(this._downloadsFolder, this._platform + '-' + 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;
|
||||
if (!supportedPlatforms.includes(platform))
|
||||
return null;
|
||||
return {platform, revision};
|
||||
}
|
||||
|
||||
function downloadFile(url: string, destinationPath: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise<any> {
|
||||
let fulfill, reject;
|
||||
let downloadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
const promise = new Promise((x, y) => { fulfill = x; reject = y; });
|
||||
|
||||
const request = httpRequest(url, 'GET', response => {
|
||||
if (response.statusCode !== 200) {
|
||||
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
|
||||
// consume response data to free up memory
|
||||
response.resume();
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath);
|
||||
file.on('finish', () => fulfill());
|
||||
file.on('error', error => reject(error));
|
||||
response.pipe(file);
|
||||
totalBytes = parseInt(response.headers['content-length'], 10);
|
||||
if (progressCallback)
|
||||
response.on('data', onData);
|
||||
});
|
||||
request.on('error', error => reject(error));
|
||||
return promise;
|
||||
|
||||
function onData(chunk) {
|
||||
downloadedBytes += chunk.length;
|
||||
progressCallback(downloadedBytes, totalBytes);
|
||||
}
|
||||
}
|
||||
|
||||
function extractZip(zipPath: string, folderPath: string): Promise<Error | null> {
|
||||
return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
fulfill();
|
||||
}));
|
||||
}
|
||||
|
||||
function httpRequest(url: string, method: string, response: (r: any) => void) {
|
||||
let options: any = URL.parse(url);
|
||||
options.method = method;
|
||||
|
||||
const proxyURL = getProxyForUrl(url);
|
||||
if (proxyURL) {
|
||||
if (url.startsWith('http:')) {
|
||||
const proxy = URL.parse(proxyURL);
|
||||
options = {
|
||||
path: options.href,
|
||||
host: proxy.hostname,
|
||||
port: proxy.port,
|
||||
};
|
||||
} else {
|
||||
const parsedProxyURL: any = URL.parse(proxyURL);
|
||||
parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
|
||||
|
||||
options.agent = new ProxyAgent(parsedProxyURL);
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
}
|
||||
|
||||
const requestCallback = res => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
|
||||
httpRequest(res.headers.location, method, response);
|
||||
else
|
||||
response(res);
|
||||
};
|
||||
const request = options.protocol === 'https:' ?
|
||||
require('https').request(options, requestCallback) :
|
||||
require('http').request(options, requestCallback);
|
||||
request.end();
|
||||
return request;
|
||||
}
|
||||
|
||||
export type BrowserFetcherOptions = {
|
||||
platform?: string,
|
||||
path?: string,
|
||||
host ?: string,
|
||||
};
|
||||
|
||||
type BrowserFetcherRevisionInfo = {
|
||||
folderPath: string,
|
||||
executablePath: string,
|
||||
url: string,
|
||||
local: boolean,
|
||||
revision: string,
|
||||
};
|
||||
|
|
@ -24,7 +24,7 @@ import * as readline from 'readline';
|
|||
import * as removeFolder from 'rimraf';
|
||||
import * as URL from 'url';
|
||||
import { Browser } from './Browser';
|
||||
import { BrowserFetcher } from './BrowserFetcher';
|
||||
import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher';
|
||||
import { Connection } from './Connection';
|
||||
import { TimeoutError } from '../Errors';
|
||||
import { assert, debugError, helper } from '../helper';
|
||||
|
|
@ -32,6 +32,7 @@ import * as types from '../types';
|
|||
import { PipeTransport } from './PipeTransport';
|
||||
import { WebSocketTransport } from './WebSocketTransport';
|
||||
import { ConnectionTransport } from '../ConnectionTransport';
|
||||
import * as util from 'util';
|
||||
|
||||
const mkdtempAsync = helper.promisify(fs.mkdtemp);
|
||||
const removeFolderAsync = helper.promisify(removeFolder);
|
||||
|
|
@ -289,7 +290,7 @@ export class Launcher {
|
|||
}
|
||||
|
||||
_resolveExecutablePath(): { executablePath: string; missingText: string | null; } {
|
||||
const browserFetcher = new BrowserFetcher(this._projectRoot);
|
||||
const browserFetcher = createBrowserFetcher(this._projectRoot);
|
||||
const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
|
||||
const missingText = !revisionInfo.local ? `Chromium revision is not downloaded. Run "npm install" or "yarn install"` : null;
|
||||
return {executablePath: revisionInfo.executablePath, missingText};
|
||||
|
|
@ -395,3 +396,52 @@ export type LauncherBrowserOptions = {
|
|||
defaultViewport?: types.Viewport | null,
|
||||
slowMo?: number,
|
||||
};
|
||||
|
||||
export function createBrowserFetcher(projectRoot: string, 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(projectRoot, '.local-chromium'),
|
||||
host: 'https://storage.googleapis.com',
|
||||
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[options.platform], 'Unsupported platform: ' + options.platform);
|
||||
|
||||
return new BrowserFetcher(options.path, options.platform, (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[platform], options.host, revision, archiveName),
|
||||
executablePath
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,24 +15,28 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import { Browser } from './Browser';
|
||||
import { BrowserFetcher, BrowserFetcherOptions } from './BrowserFetcher';
|
||||
import { BrowserFetcher, BrowserFetcherOptions, BrowserFetcherRevisionInfo, OnProgressCallback } from '../browserFetcher';
|
||||
import { ConnectionTransport } from '../ConnectionTransport';
|
||||
import { DeviceDescriptors } from '../DeviceDescriptors';
|
||||
import * as Errors from '../Errors';
|
||||
import { Launcher, LauncherBrowserOptions, LauncherChromeArgOptions, LauncherLaunchOptions } from './Launcher';
|
||||
import {download, RevisionInfo} from '../download';
|
||||
import { Launcher, LauncherBrowserOptions, LauncherChromeArgOptions, LauncherLaunchOptions, createBrowserFetcher } from './Launcher';
|
||||
|
||||
export class Playwright {
|
||||
private _projectRoot: string;
|
||||
private _launcher: Launcher;
|
||||
readonly _revision: string;
|
||||
downloadBrowser: (options?: { onProgress?: (downloadedBytes: number, totalBytes: number) => void; }) => Promise<RevisionInfo>;
|
||||
|
||||
constructor(projectRoot: string, preferredRevision: string) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._launcher = new Launcher(projectRoot, preferredRevision);
|
||||
this._revision = preferredRevision;
|
||||
this.downloadBrowser = download.bind(null, this.createBrowserFetcher(), preferredRevision, 'Chromium');
|
||||
}
|
||||
|
||||
async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise<BrowserFetcherRevisionInfo> {
|
||||
const fetcher = this.createBrowserFetcher(options);
|
||||
const revisionInfo = fetcher.revisionInfo(this._revision);
|
||||
await fetcher.download(this._revision, options ? options.onProgress : undefined);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
launch(options?: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise<Browser> {
|
||||
|
|
@ -65,7 +69,7 @@ export class Playwright {
|
|||
return this._launcher.defaultArgs(options);
|
||||
}
|
||||
|
||||
createBrowserFetcher(options?: BrowserFetcherOptions | undefined): BrowserFetcher {
|
||||
return new BrowserFetcher(this._projectRoot, options);
|
||||
createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher {
|
||||
return createBrowserFetcher(this._projectRoot, options);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export { ExecutionContext, JSHandle } from '../javascript';
|
|||
export { Request, Response } from '../network';
|
||||
export { Browser } from './Browser';
|
||||
export { BrowserContext } from './BrowserContext';
|
||||
export { BrowserFetcher } from './BrowserFetcher';
|
||||
export { BrowserFetcher } from '../browserFetcher';
|
||||
export { CDPSession } from './Connection';
|
||||
export { Accessibility } from './features/accessibility';
|
||||
export { Chromium } from './features/chromium';
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
export async function download(
|
||||
browserFetcher:
|
||||
import('./chromium/BrowserFetcher').BrowserFetcher |
|
||||
import('./firefox/BrowserFetcher').BrowserFetcher |
|
||||
import('./webkit/BrowserFetcher').BrowserFetcher,
|
||||
revision: string,
|
||||
browserName: string,
|
||||
{onProgress}: {onProgress?: (downloadedBytes: number, totalBytes: number) => void} = {}) : Promise<RevisionInfo> {
|
||||
const revisionInfo = browserFetcher.revisionInfo(revision);
|
||||
await browserFetcher.download(revision, onProgress);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
export type RevisionInfo = {
|
||||
folderPath: string,
|
||||
executablePath: string,
|
||||
url: string,
|
||||
local: boolean,
|
||||
revision: string,
|
||||
};
|
||||
|
|
@ -1,283 +0,0 @@
|
|||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as extract from 'extract-zip';
|
||||
import * as util from 'util';
|
||||
import * as URL from 'url';
|
||||
import {helper, assert} from '../helper';
|
||||
import * as removeRecursive from 'rimraf';
|
||||
// @ts-ignore
|
||||
import * as ProxyAgent from 'https-proxy-agent';
|
||||
// @ts-ignore
|
||||
import {getProxyForUrl} from 'proxy-from-env';
|
||||
const DEFAULT_DOWNLOAD_HOST = 'https://playwrightaccount.blob.core.windows.net/builds';
|
||||
|
||||
const downloadURLs = {
|
||||
chromium: {
|
||||
linux: '%s/chromium-browser-snapshots/Linux_x64/%s/%s.zip',
|
||||
mac: '%s/chromium-browser-snapshots/Mac/%s/%s.zip',
|
||||
win32: '%s/chromium-browser-snapshots/Win/%s/%s.zip',
|
||||
win64: '%s/chromium-browser-snapshots/Win_x64/%s/%s.zip',
|
||||
},
|
||||
firefox: {
|
||||
linux: '%s/firefox/%s/%s.zip',
|
||||
mac: '%s/firefox/%s/%s.zip',
|
||||
win32: '%s/firefox/%s/%s.zip',
|
||||
win64: '%s/firefox/%s/%s.zip',
|
||||
},
|
||||
};
|
||||
|
||||
function archiveName(product: string, platform: string, revision: string): string {
|
||||
if (product === 'chromium') {
|
||||
if (platform === 'linux')
|
||||
return 'chrome-linux';
|
||||
if (platform === 'mac')
|
||||
return 'chrome-mac';
|
||||
if (platform === 'win32' || platform === 'win64') {
|
||||
// Windows archive name changed at r591479.
|
||||
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
|
||||
}
|
||||
} else if (product === 'firefox') {
|
||||
if (platform === 'linux')
|
||||
return 'firefox-linux';
|
||||
if (platform === 'mac')
|
||||
return 'firefox-mac';
|
||||
if (platform === 'win32' || platform === 'win64')
|
||||
return 'firefox-' + platform;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function downloadURL(product: string, platform: string, host: string, revision: string): string {
|
||||
return util.format(downloadURLs[product][platform], host, revision, archiveName(product, platform, revision));
|
||||
}
|
||||
|
||||
const readdirAsync = helper.promisify(fs.readdir.bind(fs));
|
||||
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
||||
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
|
||||
const chmodAsync = helper.promisify(fs.chmod.bind(fs));
|
||||
|
||||
function existsAsync(filePath) {
|
||||
let fulfill = null;
|
||||
const promise = new Promise(x => fulfill = x);
|
||||
fs.access(filePath, err => fulfill(!err));
|
||||
return promise;
|
||||
}
|
||||
|
||||
export class BrowserFetcher {
|
||||
_product: string;
|
||||
_downloadsFolder: string;
|
||||
_downloadHost: string;
|
||||
_platform: string;
|
||||
constructor(projectRoot: string, options: BrowserFetcherOptions | undefined = {}) {
|
||||
this._product = (options.browser || 'chromium').toLowerCase();
|
||||
assert(this._product === 'chromium' || this._product === 'firefox', `Unkown product: "${options.browser}"`);
|
||||
this._downloadsFolder = options.path || path.join(projectRoot, '.local-browser');
|
||||
this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
|
||||
this._platform = options.platform || '';
|
||||
if (!this._platform) {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin')
|
||||
this._platform = 'mac';
|
||||
else if (platform === 'linux')
|
||||
this._platform = 'linux';
|
||||
else if (platform === 'win32')
|
||||
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
|
||||
assert(this._platform, 'Unsupported platform: ' + os.platform());
|
||||
}
|
||||
assert(downloadURLs[this._product][this._platform], 'Unsupported platform: ' + this._platform);
|
||||
}
|
||||
|
||||
platform(): string {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
canDownload(revision: string): Promise<boolean> {
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
let resolve;
|
||||
const promise = new Promise<boolean>(x => resolve = x);
|
||||
const request = httpRequest(url, 'HEAD', response => {
|
||||
resolve(response.statusCode === 200);
|
||||
});
|
||||
request.on('error', error => {
|
||||
console.error(error);
|
||||
resolve(false);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
async download(revision: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise<RevisionInfo> {
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
const zipPath = path.join(this._downloadsFolder, `download-${this._product}-${this._platform}-${revision}.zip`);
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
if (await existsAsync(folderPath))
|
||||
return this.revisionInfo(revision);
|
||||
if (!(await existsAsync(this._downloadsFolder)))
|
||||
await mkdirAsync(this._downloadsFolder);
|
||||
try {
|
||||
await downloadFile(url, zipPath, progressCallback);
|
||||
await extractZip(zipPath, folderPath);
|
||||
} finally {
|
||||
if (await existsAsync(zipPath))
|
||||
await unlinkAsync(zipPath);
|
||||
}
|
||||
const revisionInfo = this.revisionInfo(revision);
|
||||
if (revisionInfo)
|
||||
await chmodAsync(revisionInfo.executablePath, 0o755);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
async localRevisions(): Promise<Array<string>> {
|
||||
if (!await existsAsync(this._downloadsFolder))
|
||||
return [];
|
||||
const fileNames = await readdirAsync(this._downloadsFolder);
|
||||
return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
|
||||
}
|
||||
|
||||
async remove(revision: string) {
|
||||
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): RevisionInfo {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
let executablePath = '';
|
||||
if (this._product === 'chromium') {
|
||||
if (this._platform === 'mac')
|
||||
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
|
||||
else if (this._platform === 'linux')
|
||||
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome');
|
||||
else if (this._platform === 'win32' || this._platform === 'win64')
|
||||
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome.exe');
|
||||
else
|
||||
throw new Error('Unsupported platform: ' + this._platform);
|
||||
} else if (this._product === 'firefox') {
|
||||
if (this._platform === 'mac')
|
||||
executablePath = path.join(folderPath, 'firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox');
|
||||
else if (this._platform === 'linux')
|
||||
executablePath = path.join(folderPath, 'firefox', 'firefox');
|
||||
else if (this._platform === 'win32' || this._platform === 'win64')
|
||||
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
|
||||
else
|
||||
throw new Error('Unsupported platform: ' + this._platform);
|
||||
}
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
const local = fs.existsSync(folderPath);
|
||||
return {revision, executablePath, folderPath, local, url};
|
||||
}
|
||||
|
||||
_getFolderPath(revision: string): string {
|
||||
return path.join(this._downloadsFolder, this._product + '-' + this._platform + '-' + revision);
|
||||
}
|
||||
}
|
||||
|
||||
function parseFolderPath(folderPath: string): { platform: string; revision: string; } | null {
|
||||
const name = path.basename(folderPath);
|
||||
const splits = name.split('-');
|
||||
if (splits.length !== 3)
|
||||
return null;
|
||||
const [product, platform, revision] = splits;
|
||||
if (!downloadURLs[product][platform])
|
||||
return null;
|
||||
return {platform, revision};
|
||||
}
|
||||
|
||||
function downloadFile(url: string, destinationPath: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise<any> {
|
||||
let fulfill, reject;
|
||||
let downloadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
const promise = new Promise((x, y) => { fulfill = x; reject = y; });
|
||||
|
||||
const request = httpRequest(url, 'GET', response => {
|
||||
if (response.statusCode !== 200) {
|
||||
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
|
||||
// consume response data to free up memory
|
||||
response.resume();
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath);
|
||||
file.on('finish', () => fulfill());
|
||||
file.on('error', error => reject(error));
|
||||
response.pipe(file);
|
||||
totalBytes = parseInt(response.headers['content-length'], 10);
|
||||
if (progressCallback)
|
||||
response.on('data', onData);
|
||||
});
|
||||
request.on('error', error => reject(error));
|
||||
return promise;
|
||||
|
||||
function onData(chunk) {
|
||||
downloadedBytes += chunk.length;
|
||||
progressCallback(downloadedBytes, totalBytes);
|
||||
}
|
||||
}
|
||||
|
||||
function extractZip(zipPath: string, folderPath: string): Promise<Error | null> {
|
||||
return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
fulfill();
|
||||
}));
|
||||
}
|
||||
|
||||
function httpRequest(url: string, method: string, onResponse: (response: any) => void) {
|
||||
const options: any = URL.parse(url);
|
||||
options.method = method;
|
||||
|
||||
const proxyURL = getProxyForUrl(url);
|
||||
if (proxyURL) {
|
||||
const parsedProxyURL: any = URL.parse(proxyURL);
|
||||
parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
|
||||
|
||||
options.agent = new ProxyAgent(parsedProxyURL);
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
const requestCallback = res => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
|
||||
httpRequest(res.headers.location, method, onResponse);
|
||||
else
|
||||
onResponse(res);
|
||||
};
|
||||
const request = options.protocol === 'https:' ?
|
||||
require('https').request(options, requestCallback) :
|
||||
require('http').request(options, requestCallback);
|
||||
request.end();
|
||||
return request;
|
||||
}
|
||||
|
||||
interface BrowserFetcherOptions {
|
||||
browser?: string;
|
||||
platform?: string;
|
||||
path?: string;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
interface RevisionInfo {
|
||||
folderPath: string;
|
||||
executablePath: string;
|
||||
url: string;
|
||||
local: boolean;
|
||||
revision: string;
|
||||
}
|
||||
|
|
@ -20,11 +20,11 @@ import * as removeFolder from 'rimraf';
|
|||
import * as childProcess from 'child_process';
|
||||
import {Connection} from './Connection';
|
||||
import {Browser} from './Browser';
|
||||
import {BrowserFetcher} from './BrowserFetcher';
|
||||
import {BrowserFetcher, BrowserFetcherOptions} from '../browserFetcher';
|
||||
import * as readline from 'readline';
|
||||
import * as fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import {helper, debugError} from '../helper';
|
||||
import {helper, debugError, assert} from '../helper';
|
||||
import {TimeoutError} from '../Errors';
|
||||
import {WebSocketTransport} from './WebSocketTransport';
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ export class Launcher {
|
|||
}
|
||||
|
||||
_resolveExecutablePath() {
|
||||
const browserFetcher = new BrowserFetcher(this._projectRoot, { browser: 'firefox' });
|
||||
const browserFetcher = createBrowserFetcher(this._projectRoot);
|
||||
const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
|
||||
const missingText = !revisionInfo.local ? `Firefox revision is not downloaded. Run "npm install" or "yarn install"` : null;
|
||||
return {executablePath: revisionInfo.executablePath, missingText};
|
||||
|
|
@ -275,3 +275,46 @@ function waitForWSEndpoint(firefoxProcess: import('child_process').ChildProcess,
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function createBrowserFetcher(projectRoot: string, 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(projectRoot, '.local-firefox'),
|
||||
host: 'https://playwrightaccount.blob.core.windows.net',
|
||||
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[options.platform], 'Unsupported platform: ' + options.platform);
|
||||
|
||||
return new BrowserFetcher(options.path, options.platform, (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[platform], options.host, revision),
|
||||
executablePath
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,24 +15,28 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import { Browser } from './Browser';
|
||||
import { BrowserFetcher } from './BrowserFetcher';
|
||||
import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../browserFetcher';
|
||||
import { ConnectionTransport } from '../ConnectionTransport';
|
||||
import { DeviceDescriptors } from '../DeviceDescriptors';
|
||||
import * as Errors from '../Errors';
|
||||
import { Launcher } from './Launcher';
|
||||
import {download, RevisionInfo} from '../download';
|
||||
import { Launcher, createBrowserFetcher } from './Launcher';
|
||||
|
||||
export class Playwright {
|
||||
private _projectRoot: string;
|
||||
private _launcher: Launcher;
|
||||
readonly _revision: string;
|
||||
downloadBrowser: (options?: { onProgress?: (downloadedBytes: number, totalBytes: number) => void; }) => Promise<RevisionInfo>;
|
||||
|
||||
constructor(projectRoot: string, preferredRevision: string) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._launcher = new Launcher(projectRoot, preferredRevision);
|
||||
this._revision = preferredRevision;
|
||||
this.downloadBrowser = download.bind(null, this.createBrowserFetcher(), preferredRevision, 'Chromium');
|
||||
}
|
||||
|
||||
async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise<BrowserFetcherRevisionInfo> {
|
||||
const fetcher = this.createBrowserFetcher(options);
|
||||
const revisionInfo = fetcher.revisionInfo(this._revision);
|
||||
await fetcher.download(this._revision, options ? options.onProgress : undefined);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
launch(options: any): Promise<Browser> {
|
||||
|
|
@ -65,7 +69,7 @@ export class Playwright {
|
|||
return this._launcher.defaultArgs(options);
|
||||
}
|
||||
|
||||
createBrowserFetcher(options?: any | undefined): BrowserFetcher {
|
||||
return new BrowserFetcher(this._projectRoot, { browser: 'firefox', ...options });
|
||||
createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher {
|
||||
return createBrowserFetcher(this._projectRoot, options);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
export { TimeoutError } from '../Errors';
|
||||
export { Keyboard, Mouse } from '../input';
|
||||
export { Browser, BrowserContext } from './Browser';
|
||||
export { BrowserFetcher } from './BrowserFetcher';
|
||||
export { BrowserFetcher } from '../browserFetcher';
|
||||
export { Dialog } from '../dialog';
|
||||
export { ExecutionContext, JSHandle } from '../javascript';
|
||||
export { ElementHandle } from '../dom';
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import * as childProcess from 'child_process';
|
||||
import { debugError, helper } from '../helper';
|
||||
import { debugError, helper, assert } from '../helper';
|
||||
import { Browser } from './Browser';
|
||||
import { BrowserFetcher } from './BrowserFetcher';
|
||||
import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher';
|
||||
import { Connection } from './Connection';
|
||||
import * as types from '../types';
|
||||
import { PipeTransport } from './PipeTransport';
|
||||
import { execSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import * as os from 'os';
|
||||
|
||||
const DEFAULT_ARGS = [
|
||||
];
|
||||
|
|
@ -168,7 +172,7 @@ export class Launcher {
|
|||
}
|
||||
|
||||
_resolveExecutablePath(): { executablePath: string; missingText: string | null; } {
|
||||
const browserFetcher = new BrowserFetcher(this._projectRoot);
|
||||
const browserFetcher = createBrowserFetcher(this._projectRoot);
|
||||
const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
|
||||
const missingText = !revisionInfo.local ? `WebKit revision is not downloaded. Run "npm install" or "yarn install"` : null;
|
||||
return {executablePath: revisionInfo.executablePath, missingText};
|
||||
|
|
@ -189,3 +193,48 @@ export type LauncherLaunchOptions = {
|
|||
defaultViewport?: types.Viewport | null,
|
||||
slowMo?: number,
|
||||
};
|
||||
|
||||
let cachedMacVersion = undefined;
|
||||
function getMacVersion() {
|
||||
if (!cachedMacVersion) {
|
||||
const [major, minor] = execSync('sw_vers -productVersion').toString('utf8').trim().split('.');
|
||||
cachedMacVersion = major + '.' + minor;
|
||||
}
|
||||
return cachedMacVersion;
|
||||
}
|
||||
|
||||
export function createBrowserFetcher(projectRoot: string, options: BrowserFetcherOptions = {}): BrowserFetcher {
|
||||
const downloadURLs = {
|
||||
linux: '%s/builds/webkit/%s/minibrowser-linux.zip',
|
||||
mac: '%s/builds/webkit/%s/minibrowser-mac-%s.zip',
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
path: path.join(projectRoot, '.local-webkit'),
|
||||
host: 'https://playwrightaccount.blob.core.windows.net',
|
||||
platform: (() => {
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin')
|
||||
return 'mac';
|
||||
if (platform === 'linux')
|
||||
return 'linux';
|
||||
if (platform === 'win32')
|
||||
return 'linux'; // Windows gets linux binaries and uses WSL
|
||||
return platform;
|
||||
})()
|
||||
};
|
||||
options = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
assert(!!downloadURLs[options.platform], 'Unsupported platform: ' + options.platform);
|
||||
|
||||
return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => {
|
||||
return {
|
||||
downloadUrl: (platform === 'mac') ?
|
||||
util.format(downloadURLs[platform], options.host, revision, getMacVersion()) :
|
||||
util.format(downloadURLs[platform], options.host, revision),
|
||||
executablePath: 'pw_run.sh',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,23 +15,27 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import { Browser } from './Browser';
|
||||
import { BrowserFetcher, BrowserFetcherOptions } from './BrowserFetcher';
|
||||
import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../browserFetcher';
|
||||
import { DeviceDescriptors } from '../DeviceDescriptors';
|
||||
import * as Errors from '../Errors';
|
||||
import { Launcher, LauncherLaunchOptions } from './Launcher';
|
||||
import { download, RevisionInfo } from '../download';
|
||||
import { Launcher, LauncherLaunchOptions, createBrowserFetcher } from './Launcher';
|
||||
|
||||
export class Playwright {
|
||||
private _projectRoot: string;
|
||||
private _launcher: Launcher;
|
||||
readonly _revision: string;
|
||||
downloadBrowser: (options?: { onProgress?: (downloadedBytes: number, totalBytes: number) => void; }) => Promise<RevisionInfo>;
|
||||
|
||||
constructor(projectRoot: string, preferredRevision: string) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._launcher = new Launcher(projectRoot, preferredRevision);
|
||||
this._revision = preferredRevision;
|
||||
this.downloadBrowser = download.bind(null, this.createBrowserFetcher(), preferredRevision, 'WebKit');
|
||||
}
|
||||
|
||||
async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise<BrowserFetcherRevisionInfo> {
|
||||
const fetcher = this.createBrowserFetcher(options);
|
||||
const revisionInfo = fetcher.revisionInfo(this._revision);
|
||||
await fetcher.download(this._revision, options ? options.onProgress : undefined);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
launch(options: (LauncherLaunchOptions) | undefined): Promise<Browser> {
|
||||
|
|
@ -57,7 +61,7 @@ export class Playwright {
|
|||
return this._launcher.defaultArgs(options);
|
||||
}
|
||||
|
||||
createBrowserFetcher(options?: BrowserFetcherOptions | undefined): BrowserFetcher {
|
||||
return new BrowserFetcher(this._projectRoot, options);
|
||||
createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher {
|
||||
return createBrowserFetcher(this._projectRoot, options);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
export { TimeoutError } from '../Errors';
|
||||
export { Browser, BrowserContext } from './Browser';
|
||||
export { BrowserFetcher } from './BrowserFetcher';
|
||||
export { BrowserFetcher } from '../browserFetcher';
|
||||
export { ExecutionContext, JSHandle } from '../javascript';
|
||||
export { ElementHandle } from '../dom';
|
||||
export { Frame } from '../frames';
|
||||
|
|
|
|||
Loading…
Reference in a new issue