Browse Source

feat: support `npx playwright install msedge` (#6861)

pull/6932/head
Andrey Lushnikov 1 week ago
committed by GitHub
parent
commit
cfd49b5c01
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      .github/workflows/tests_secondary.yml
  2. 1
      bin/reinstall_chrome_beta_linux.sh
  3. 3
      bin/reinstall_chrome_beta_mac.sh
  4. 21
      bin/reinstall_chrome_beta_win.ps1
  5. 1
      bin/reinstall_chrome_stable_linux.sh
  6. 4
      bin/reinstall_chrome_stable_mac.sh
  7. 26
      bin/reinstall_chrome_stable_win.ps1
  8. 11
      bin/reinstall_msedge_stable_mac.sh
  9. 21
      bin/reinstall_msedge_stable_win.ps1
  10. 89
      src/cli/cli.ts
  11. 89
      src/install/browserFetcher.ts
  12. 3
      src/server/browserType.ts
  13. 4
      src/server/supplements/recorder/recorderApp.ts
  14. 107
      src/utils/utils.ts
  15. 1
      tests/headful.spec.ts

27
.github/workflows/tests_secondary.yml

@ -333,6 +333,31 @@ jobs:
name: firefox-stable-mac-test-results
path: test-results
edge_stable_mac:
name: "Edge Stable (Mac)"
runs-on: macos-10.15
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: node lib/cli/cli install msedge
- run: npm run ctest
env:
PWTEST_CHANNEL: msedge
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
- uses: actions/upload-artifact@v1
if: ${{ always() }}
with:
name: msedge-stable-mac-test-results
path: test-results
edge_stable_win:
name: "Edge Stable (Win)"
runs-on: windows-latest
@ -345,7 +370,7 @@ jobs:
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: node lib/cli/cli install ffmpeg
- run: node lib/cli/cli install msedge
- run: npm run ctest
shell: bash
env:

1
bin/reinstall_chrome_beta_linux.sh

@ -13,3 +13,4 @@ wget https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb
sudo apt-get install -y ./google-chrome-beta_current_amd64.deb
rm -rf ./google-chrome-beta_current_amd64.deb
cd -
google-chrome-beta --version

3
bin/reinstall_chrome_beta_mac.sh

@ -8,3 +8,6 @@ curl -o ./googlechromebeta.dmg -k https://dl.google.com/chrome/mac/beta/googlech
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg
cp -rf "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications
hdiutil detach /Volumes/googlechromebeta.dmg
rm -rf /tmp/googlechromebeta.dmg
/Applications/Google\ Chrome\ Beta.app/Contents/MacOS/Google\ Chrome\ Beta --version

21
bin/reinstall_chrome_beta_win.ps1

@ -4,17 +4,22 @@ if ([Environment]::Is64BitProcess) {
$url = 'https://dl.google.com/tag/s/dl/chrome/install/beta/googlechromebetastandaloneenterprise64.msi'
}
$app = Get-WmiObject -Class Win32_Product | Where-Object {
$_.Name -match "Google Chrome Beta"
}
if ($app) {
$app.Uninstall()
}
Write-Host "Downloading Google Chrome Beta"
$wc = New-Object net.webclient
$msiInstaller = "$env:temp\google-chrome-beta.msi"
Remove-Item $msiInstaller
$wc.Downloadfile($url, $msiInstaller)
Write-Host "Installing Google Chrome Beta"
$arguments = "/i `"$msiInstaller`" /quiet"
Start-Process msiexec.exe -ArgumentList $arguments -Wait
Remove-Item $msiInstaller
$suffix = "\\Google\\Chrome Beta\\Application\\chrome.exe"
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
} else {
write-host "ERROR: failed to install Google Chrome Beta"
exit 1
}

1
bin/reinstall_chrome_stable_linux.sh

@ -14,3 +14,4 @@ wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt-get install -y ./google-chrome-stable_current_amd64.deb
rm -rf ./google-chrome-stable_current_amd64.deb
cd -
google-chrome --version

4
bin/reinstall_chrome_stable_mac.sh

@ -4,7 +4,9 @@ set -x
rm -rf "/Applications/Google Chrome.app"
cd /tmp
curl -o ./googlechrome.dmg -k https://dl.google.com/chrome/mac/beta/googlechrome.dmg
curl -o ./googlechrome.dmg -k https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg
cp -rf "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications
hdiutil detach /Volumes/googlechrome.dmg
rm -rf /tmp/googlechrome.dmg
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version

26
bin/reinstall_chrome_stable_win.ps1

@ -1,20 +1,26 @@
$url = 'https://dl.google.com/tag/s/dl/chrome/install/beta/googlechromestandaloneenterprise.msi';
$url = 'https://dl.google.com/tag/s/dl/chrome/install/googlechromestandaloneenterprise.msi';
if ([Environment]::Is64BitProcess) {
$url = 'https://dl.google.com/tag/s/dl/chrome/install/beta/googlechromestandaloneenterprise64.msi'
}
$app = Get-WmiObject -Class Win32_Product | Where-Object {
$_.Name -eq "Google Chrome"
}
if ($app) {
$app.Uninstall()
$url = 'https://dl.google.com/tag/s/dl/chrome/install/googlechromestandaloneenterprise64.msi'
}
$wc = New-Object net.webclient
$msiInstaller = "$env:temp\google-chrome.msi"
Remove-Item $msiInstaller
Write-Host "Downloading Google Chrome"
$wc.Downloadfile($url, $msiInstaller)
Write-Host "Installing Google Chrome"
$arguments = "/i `"$msiInstaller`" /quiet"
Start-Process msiexec.exe -ArgumentList $arguments -Wait
Remove-Item $msiInstaller
$suffix = "\\Google\\Chrome\\Application\\chrome.exe"
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
} else {
write-host "ERROR: failed to install Google Chrome"
exit 1
}

11
bin/reinstall_msedge_stable_mac.sh

@ -0,0 +1,11 @@
#!/bin/bash
set -e
set -x
cd /tmp
curl -o ./msedge_stable.pkg -k "$1"
# Note: there's no way to uninstall previously installed MSEdge.
# However, running PKG again seems to update installation.
sudo installer -pkg /tmp/msedge_stable.pkg -target /
rm -rf /tmp/msedge_stable.pkg
/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --version

21
bin/reinstall_msedge_stable_win.ps1

@ -0,0 +1,21 @@
$url = $args[0]
Write-Host "Downloading Microsoft Edge"
$wc = New-Object net.webclient
$msiInstaller = "$env:temp\microsoft-edge-stable.msi"
$wc.Downloadfile($url, $msiInstaller)
Write-Host "Installing Microsoft Edge"
$arguments = "/i `"$msiInstaller`" /quiet"
Start-Process msiexec.exe -ArgumentList $arguments -Wait
Remove-Item $msiInstaller
$suffix = "\\Microsoft\\Edge\\Application\\msedge.exe"
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
} else {
write-host "ERROR: failed to install Microsoft Edge"
exit 1
}

89
src/cli/cli.ts

@ -39,10 +39,34 @@ import * as utils from '../utils/utils';
const SCRIPTS_DIRECTORY = path.join(__dirname, '..', '..', 'bin');
type BrowserChannel = 'chrome-beta'|'chrome';
const allBrowserChannels: Set<BrowserChannel> = new Set(['chrome-beta', 'chrome']);
type BrowserChannel = 'chrome-beta'|'chrome'|'msedge';
const allBrowserChannels: Set<BrowserChannel> = new Set(['chrome-beta', 'chrome', 'msedge']);
const packageJSON = require('../../package.json');
const ChannelName = {
'chrome-beta': 'Google Chrome Beta',
'chrome': 'Google Chrome',
'msedge': 'Microsoft Edge',
};
const InstallationScriptName = {
'chrome-beta': {
'linux': 'reinstall_chrome_beta_linux.sh',
'darwin': 'reinstall_chrome_beta_mac.sh',
'win32': 'reinstall_chrome_beta_win.ps1',
},
'chrome': {
'linux': 'reinstall_chrome_stable_linux.sh',
'darwin': 'reinstall_chrome_stable_mac.sh',
'win32': 'reinstall_chrome_stable_win.ps1',
},
'msedge': {
'darwin': 'reinstall_msedge_stable_mac.sh',
'win32': 'reinstall_msedge_stable_win.ps1',
},
};
program
.version('Version ' + packageJSON.version)
.name(process.env.PW_CLI_NAME || 'npx playwright');
@ -107,47 +131,46 @@ program
console.log(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${[...allBrowserNames, ...allBrowserChannels].map(name => `'${name}'`).join(', ')}`);
process.exit(1);
}
if (browserNames.has('chromium') || browserChannels.has('chrome-beta') || browserChannels.has('chrome'))
if (browserNames.has('chromium') || browserChannels.has('chrome-beta') || browserChannels.has('chrome') || browserChannels.has('msedge'))
browserNames.add('ffmpeg');
if (browserNames.size)
await installBrowsers([...browserNames]);
for (const browserChannel of browserChannels) {
if (browserChannel === 'chrome-beta' || browserChannel === 'chrome')
await installChromeChannel(browserChannel);
else
throw new Error(`ERROR: no installation instructions for '${browserChannel}' channel.`);
}
for (const browserChannel of browserChannels)
await installBrowserChannel(browserChannel);
} catch (e) {
console.log(`Failed to install browsers\n${e}`);
process.exit(1);
}
});
async function installChromeChannel(channel: string) {
const platform: string = os.platform();
const shell: (string|undefined) = {
'linux': 'bash',
'darwin': 'bash',
'win32': 'powershell.exe',
}[platform];
const scriptName: (string|undefined) = ({
'chrome-beta': {
'linux': 'reinstall_chrome_beta_linux.sh',
'darwin': 'reinstall_chrome_beta_mac.sh',
'win32': 'reinstall_chrome_beta_win.ps1',
},
'chrome': {
'linux': 'reinstall_chrome_stable_linux.sh',
'darwin': 'reinstall_chrome_stable_mac.sh',
'win32': 'reinstall_chrome_stable_win.ps1',
},
}[channel] as any)[platform];
if (!shell || !scriptName)
throw new Error(`Cannot install chrome-beta on ${platform}`);
const {code} = await utils.spawnAsync(shell, [path.join(SCRIPTS_DIRECTORY, scriptName)], { cwd: SCRIPTS_DIRECTORY, stdio: 'inherit' });
async function installBrowserChannel(channel: BrowserChannel) {
const platform = os.platform();
const scriptName: (string|undefined) = (InstallationScriptName[channel] as any)[platform];
if (!scriptName)
throw new Error(`Cannot install ${ChannelName[channel]} on ${platform}`);
const scriptArgs = [];
if (channel === 'msedge') {
const products = JSON.parse(await utils.fetchData('https://edgeupdates.microsoft.com/api/products'));
const stable = products.find((product: any) => product.Product === 'Stable');
if (platform === 'win32') {
const arch = os.arch() === 'x64' ? 'x64' : 'x86';
const release = stable.Releases.find((release: any) => release.Platform === 'Windows' && release.Architecture === arch);
const artifact = release.Artifacts.find((artifact: any) => artifact.ArtifactName === 'msi');
scriptArgs.push(artifact.Location /* url */);
} else if (platform === 'darwin') {
const release = stable.Releases.find((release: any) => release.Platform === 'MacOS' && release.Architecture === 'universal');
const artifact = release.Artifacts.find((artifact: any) => artifact.ArtifactName === 'pkg');
scriptArgs.push(artifact.Location /* url */);
} else {
throw new Error(`Cannot install ${ChannelName[channel]} on ${platform}`);
}
}
const shell = scriptName.endsWith('.ps1') ? 'powershell.exe' : 'bash';
const {code} = await utils.spawnAsync(shell, [path.join(SCRIPTS_DIRECTORY, scriptName), ...scriptArgs], { cwd: SCRIPTS_DIRECTORY, stdio: 'inherit' });
if (code !== 0)
throw new Error('Failed to install chrome-beta');
throw new Error(`Failed to install ${ChannelName[channel]}`);
}
program

89
src/install/browserFetcher.ts

@ -20,24 +20,10 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import ProgressBar from 'progress';
import { getProxyForUrl } from 'proxy-from-env';
import * as URL from 'url';
import { BrowserName, Registry, hostPlatform } from '../utils/registry';
import { downloadFile, existsAsync } from '../utils/utils';
import { debugLogger } from '../utils/debugLogger';
// `https-proxy-agent` v5 is written in TypeScript and exposes generated types.
// However, as of June 2020, its types are generated with tsconfig that enables
// `esModuleInterop` option.
//
// As a result, we can't depend on the package unless we enable the option
// for our codebase. Instead of doing this, we abuse "require" to import module
// without types.
const ProxyAgent = require('https-proxy-agent');
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
export type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void;
export async function downloadBrowserWithProgressBar(registry: Registry, browserName: BrowserName): Promise<boolean> {
const browserDirectory = registry.browserDirectory(browserName);
const progressBarName = `${browserName} v${registry.revision(browserName)}`;
@ -71,7 +57,7 @@ export async function downloadBrowserWithProgressBar(registry: Registry, browser
try {
for (let attempt = 1, N = 3; attempt <= N; ++attempt) {
debugLogger.log('install', `downloading ${progressBarName} - attempt #${attempt}`);
const {error} = await downloadFile(url, zipPath, progress);
const {error} = await downloadFile(url, zipPath, {progressCallback: progress, log: debugLogger.log.bind(debugLogger, 'install')});
if (!error) {
debugLogger.log('install', `SUCCESS downloading ${progressBarName}`);
break;
@ -111,77 +97,6 @@ function toMegabytes(bytes: number) {
return `${Math.round(mb * 10) / 10} Mb`;
}
function downloadFile(url: string, destinationPath: string, progressCallback: OnProgressCallback | undefined): Promise<{error: any}> {
debugLogger.log('install', `running download:`);
debugLogger.log('install', `-- from url: ${url}`);
debugLogger.log('install', `-- to location: ${destinationPath}`);
let fulfill: ({error}: {error: any}) => void = ({error}) => {};
let downloadedBytes = 0;
let totalBytes = 0;
const promise: Promise<{error: any}> = new Promise(x => { fulfill = x; });
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();
fulfill({error});
return;
}
const file = fs.createWriteStream(destinationPath);
file.on('finish', () => fulfill({error: null}));
file.on('error', error => fulfill({error}));
response.pipe(file);
totalBytes = parseInt(response.headers['content-length'], 10);
debugLogger.log('install', `-- total bytes: ${totalBytes}`);
if (progressCallback)
response.on('data', onData);
});
request.on('error', (error: any) => fulfill({error}));
return promise;
function onData(chunk: string) {
downloadedBytes += chunk.length;
progressCallback!(downloadedBytes, totalBytes);
}
}
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: any) => {
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 function logPolitely(toBeLogged: string) {
const logLevel = process.env.npm_config_loglevel;
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel || '') > -1;

3
src/server/browserType.ts

@ -27,12 +27,11 @@ import { Progress, ProgressController } from './progress';
import * as types from './types';
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../utils/timeoutSettings';
import { validateHostRequirements } from './validateDependencies';
import { debugMode } from '../utils/utils';
import { debugMode, existsAsync } from '../utils/utils';
import { helper } from './helper';
import { RecentLogsCollector } from '../utils/debugLogger';
import { CallMetadata, SdkObject } from './instrumentation';
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-');
export abstract class BrowserType extends SdkObject {

4
src/server/supplements/recorder/recorderApp.ts

@ -23,9 +23,7 @@ import { EventEmitter } from 'events';
import { internalCallMetadata } from '../../instrumentation';
import type { CallLog, EventData, Mode, Source } from './recorderTypes';
import { BrowserContext } from '../../browserContext';
import { isUnderTest } from '../../../utils/utils';
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
import { existsAsync, isUnderTest } from '../../../utils/utils';
declare global {
interface Window {

107
src/utils/utils.ts

@ -20,6 +20,113 @@ import removeFolder from 'rimraf';
import * as crypto from 'crypto';
import os from 'os';
import { spawn } from 'child_process';
import { getProxyForUrl } from 'proxy-from-env';
import * as URL from 'url';
// `https-proxy-agent` v5 is written in TypeScript and exposes generated types.
// However, as of June 2020, its types are generated with tsconfig that enables
// `esModuleInterop` option.
//
// As a result, we can't depend on the package unless we enable the option
// for our codebase. Instead of doing this, we abuse "require" to import module
// without types.
const ProxyAgent = require('https-proxy-agent');
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
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: any) => {
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 function fetchData(url: string): Promise<string> {
return new Promise((resolve, reject) => {
httpRequest(url, 'GET', function(response){
if (response.statusCode !== 200) {
reject(new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${url}`));
return;
}
let body = '';
response.on('data', (chunk: string) => body += chunk);
response.on('error', (error: any) => reject(error));
response.on('end', () => resolve(body));
}).on('error', (error: any) => reject(error));
});
}
type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void;
type DownloadFileLogger = (message: string) => void;
export function downloadFile(url: string, destinationPath: string, options : {progressCallback?: OnProgressCallback, log?: DownloadFileLogger} = {}): Promise<{error: any}> {
const {
progressCallback,
log = () => {},
} = options;
log(`running download:`);
log(`-- from url: ${url}`);
log(`-- to location: ${destinationPath}`);
let fulfill: ({error}: {error: any}) => void = ({error}) => {};
let downloadedBytes = 0;
let totalBytes = 0;
const promise: Promise<{error: any}> = new Promise(x => { fulfill = x; });
const request = httpRequest(url, 'GET', response => {
log(`-- response status code: ${response.statusCode}`);
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();
fulfill({error});
return;
}
const file = fs.createWriteStream(destinationPath);
file.on('finish', () => fulfill({error: null}));
file.on('error', error => fulfill({error}));
response.pipe(file);
totalBytes = parseInt(response.headers['content-length'], 10);
log(`-- total bytes: ${totalBytes}`);
if (progressCallback)
response.on('data', onData);
});
request.on('error', (error: any) => fulfill({error}));
return promise;
function onData(chunk: string) {
downloadedBytes += chunk.length;
progressCallback!(downloadedBytes, totalBytes);
}
}
export function spawnAsync(cmd: string, args: string[], options: any): Promise<{stdout: string, stderr: string, code: number, error?: Error}> {
const process = spawn(cmd, args, options);

1
tests/headful.spec.ts

@ -148,6 +148,7 @@ it('Page.bringToFront should work', async ({browserType, browserOptions}) => {
});
it('focused input should produce the same screenshot', async ({browserType, browserOptions, browserName, platform, channel}, testInfo) => {
it.fail(channel === 'msedge' && platform === 'darwin', 'focus ring is black on MSEdge');
it.fail(browserName === 'firefox' && platform === 'darwin', 'headless has thinner outline');
it.fail(browserName === 'firefox' && platform === 'linux', 'headless has no outline');
it.skip(browserName === 'webkit' && platform === 'linux', 'gtk vs wpe');

Loading…
Cancel
Save