Skip to content

Commit ec41f20

Browse files
author
Benjamin Pasero
authored
Web - run smoke tests using playwright (microsoft#89918)
* playwright - initial version * browser - use existing page and not create new context * macOS: document how to remove the security flag * smoke test - allow to run against server build with --build option * do not rely on args * fix path for windows * smoke test - smoke 💄 and -ci option
1 parent 16c7551 commit ec41f20

12 files changed

Lines changed: 246 additions & 187 deletions

File tree

test/automation/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@
2222
"devDependencies": {
2323
"@types/mkdirp": "0.5.1",
2424
"@types/ncp": "2.0.1",
25-
"@types/node": "8.0.33",
26-
"@types/puppeteer": "^1.19.0",
25+
"@types/debug": "4.1.5",
26+
"@types/node": "^12.11.7",
2727
"@types/tmp": "0.1.0",
2828
"concurrently": "^3.5.1",
2929
"cpx": "^1.5.0",
30-
"typescript": "2.9.2",
30+
"typescript": "3.7.5",
3131
"watch": "^1.0.2"
3232
},
3333
"dependencies": {
3434
"mkdirp": "^0.5.1",
3535
"ncp": "^2.0.0",
36-
"puppeteer": "^1.19.0",
36+
"playwright": "0.10.0",
3737
"tmp": "0.1.0",
3838
"vscode-uri": "^2.0.3"
3939
}

test/automation/src/application.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export class Application {
126126
extraArgs,
127127
remote: this.options.remote,
128128
web: this.options.web,
129+
browser: this.options.browser,
129130
headless: this.options.headless
130131
});
131132

test/automation/src/code.ts

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as fs from 'fs';
1010
import * as mkdirp from 'mkdirp';
1111
import { tmpName } from 'tmp';
1212
import { IDriver, connect as connectElectronDriver, IDisposable, IElement, Thenable } from './driver';
13-
import { connect as connectPuppeteerDriver, launch } from './puppeteerDriver';
13+
import { connect as connectPlaywrightDriver, launch } from './playwrightDriver';
1414
import { Logger } from './logger';
1515
import { ncp } from 'ncp';
1616
import { URI } from 'vscode-uri';
@@ -101,6 +101,8 @@ export interface SpawnOptions {
101101
remote?: boolean;
102102
/** Run in the web */
103103
web?: boolean;
104+
/** A specific browser to use (requires web: true) */
105+
browser?: 'chromium' | 'webkit' | 'firefox';
104106
/** Run in headless mode (only applies when web is true) */
105107
headless?: boolean;
106108
}
@@ -120,68 +122,69 @@ export async function spawn(options: SpawnOptions): Promise<Code> {
120122
const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath();
121123
const handle = await createDriverHandle();
122124

123-
const args = [
124-
options.workspacePath,
125-
'--skip-getting-started',
126-
'--skip-release-notes',
127-
'--sticky-quickopen',
128-
'--disable-telemetry',
129-
'--disable-updates',
130-
'--disable-crash-reporter',
131-
`--extensions-dir=${options.extensionsPath}`,
132-
`--user-data-dir=${options.userDataDir}`,
133-
'--driver', handle
134-
];
135-
136-
const env = process.env;
137-
138-
if (options.remote) {
139-
// Replace workspace path with URI
140-
args[0] = `--${options.workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(options.workspacePath).path}`;
141-
142-
if (codePath) {
143-
// running against a build: copy the test resolver extension
144-
const testResolverExtPath = path.join(options.extensionsPath, 'vscode-test-resolver');
145-
if (!fs.existsSync(testResolverExtPath)) {
146-
const orig = path.join(repoPath, 'extensions', 'vscode-test-resolver');
147-
await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c()));
125+
let child: cp.ChildProcess | undefined;
126+
let connectDriver: typeof connectElectronDriver;
127+
128+
if (options.web) {
129+
await launch(options.userDataDir, options.workspacePath, options.codePath);
130+
connectDriver = connectPlaywrightDriver.bind(connectPlaywrightDriver, !!options.headless, options.browser);
131+
} else {
132+
const env = process.env;
133+
134+
const args = [
135+
options.workspacePath,
136+
'--skip-getting-started',
137+
'--skip-release-notes',
138+
'--sticky-quickopen',
139+
'--disable-telemetry',
140+
'--disable-updates',
141+
'--disable-crash-reporter',
142+
`--extensions-dir=${options.extensionsPath}`,
143+
`--user-data-dir=${options.userDataDir}`,
144+
'--driver', handle
145+
];
146+
147+
if (options.remote) {
148+
// Replace workspace path with URI
149+
args[0] = `--${options.workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(options.workspacePath).path}`;
150+
151+
if (codePath) {
152+
// running against a build: copy the test resolver extension
153+
const testResolverExtPath = path.join(options.extensionsPath, 'vscode-test-resolver');
154+
if (!fs.existsSync(testResolverExtPath)) {
155+
const orig = path.join(repoPath, 'extensions', 'vscode-test-resolver');
156+
await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c()));
157+
}
148158
}
159+
args.push('--enable-proposed-api=vscode.vscode-test-resolver');
160+
const remoteDataDir = `${options.userDataDir}-server`;
161+
mkdirp.sync(remoteDataDir);
162+
env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir;
149163
}
150-
args.push('--enable-proposed-api=vscode.vscode-test-resolver');
151-
const remoteDataDir = `${options.userDataDir}-server`;
152-
mkdirp.sync(remoteDataDir);
153-
env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir;
154-
}
155-
156-
if (!codePath) {
157-
args.unshift(repoPath);
158-
}
159164

160-
if (options.verbose) {
161-
args.push('--driver-verbose');
162-
}
165+
if (!codePath) {
166+
args.unshift(repoPath);
167+
}
163168

164-
if (options.log) {
165-
args.push('--log', options.log);
166-
}
169+
if (options.verbose) {
170+
args.push('--driver-verbose');
171+
}
167172

168-
if (options.extraArgs) {
169-
args.push(...options.extraArgs);
170-
}
173+
if (options.log) {
174+
args.push('--log', options.log);
175+
}
171176

172-
let child: cp.ChildProcess | undefined;
173-
let connectDriver: typeof connectElectronDriver;
177+
if (options.extraArgs) {
178+
args.push(...options.extraArgs);
179+
}
174180

175-
if (options.web) {
176-
await launch(args);
177-
connectDriver = connectPuppeteerDriver.bind(connectPuppeteerDriver, !!options.headless);
178-
} else {
179181
const spawnOptions: cp.SpawnOptions = { env };
180182
child = cp.spawn(electronPath, args, spawnOptions);
181183
instances.add(child);
182184
child.once('exit', () => instances.delete(child!));
183185
connectDriver = connectElectronDriver;
184186
}
187+
185188
return connect(connectDriver, child, outPath, handle, options.logger);
186189
}
187190

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as puppeteer from 'puppeteer';
6+
import * as playwright from 'playwright';
77
import { ChildProcess, spawn } from 'child_process';
88
import { join } from 'path';
99
import { mkdir } from 'fs';
1010
import { promisify } from 'util';
1111
import { IDriver, IDisposable } from './driver';
12+
import { URI } from 'vscode-uri';
1213

1314
const width = 1200;
1415
const height = 800;
1516

16-
const vscodeToPuppeteerKey: { [key: string]: string } = {
17+
const vscodeToPlaywrightKey: { [key: string]: string } = {
1718
cmd: 'Meta',
1819
ctrl: 'Control',
1920
shift: 'Shift',
@@ -26,7 +27,7 @@ const vscodeToPuppeteerKey: { [key: string]: string } = {
2627
home: 'Home'
2728
};
2829

29-
function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver {
30+
function buildDriver(browser: playwright.Browser, page: playwright.Page): IDriver {
3031
const driver: IDriver = {
3132
_serviceBrand: undefined,
3233
getWindowIds: () => {
@@ -45,8 +46,8 @@ function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver
4546
const keys = chord.split('+');
4647
const keysDown: string[] = [];
4748
for (let i = 0; i < keys.length; i++) {
48-
if (keys[i] in vscodeToPuppeteerKey) {
49-
keys[i] = vscodeToPuppeteerKey[keys[i]];
49+
if (keys[i] in vscodeToPlaywrightKey) {
50+
keys[i] = vscodeToPlaywrightKey[keys[i]];
5051
}
5152
await page.keyboard.down(keys[i]);
5253
keysDown.push(keys[i]);
@@ -68,7 +69,7 @@ function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver
6869
await driver.click(windowId, selector, 0, 0);
6970
await timeout(100);
7071
},
71-
setValue: async (windowId, selector, text) => page.evaluate(`window.driver.setValue('${selector}', '${text}')`),
72+
setValue: async (windowId, selector, text) => page.evaluate(`window.driver.setValue('${selector}', '${text}')`).then(undefined),
7273
getTitle: (windowId) => page.evaluate(`window.driver.getTitle()`),
7374
isActiveElement: (windowId, selector) => page.evaluate(`window.driver.isActiveElement('${selector}')`),
7475
getElements: (windowId, selector, recursive) => page.evaluate(`window.driver.getElements('${selector}', ${recursive})`),
@@ -86,31 +87,32 @@ function timeout(ms: number): Promise<void> {
8687

8788
// function runInDriver(call: string, args: (string | boolean)[]): Promise<any> {}
8889

89-
let args: string[] | undefined;
9090
let server: ChildProcess | undefined;
9191
let endpoint: string | undefined;
92+
let workspacePath: string | undefined;
9293

93-
export async function launch(_args: string[]): Promise<void> {
94-
args = _args;
95-
const agentFolder = args.filter(e => e.includes('--user-data-dir='))[0].replace('--user-data-dir=', '');
94+
export async function launch(userDataDir: string, _workspacePath: string, codeServerPath = process.env.VSCODE_REMOTE_SERVER_PATH): Promise<void> {
95+
workspacePath = _workspacePath;
96+
const agentFolder = userDataDir;
9697
await promisify(mkdir)(agentFolder);
9798
const env = {
9899
VSCODE_AGENT_FOLDER: agentFolder,
100+
VSCODE_REMOTE_SERVER_PATH: codeServerPath,
99101
...process.env
100102
};
101103
let serverLocation: string | undefined;
102-
if (process.env.VSCODE_REMOTE_SERVER_PATH) {
103-
serverLocation = join(process.env.VSCODE_REMOTE_SERVER_PATH, `server.${process.platform === 'win32' ? 'cmd' : 'sh'}`);
104+
if (codeServerPath) {
105+
serverLocation = join(codeServerPath, `server.${process.platform === 'win32' ? 'cmd' : 'sh'}`);
104106
} else {
105-
serverLocation = join(args[0], `resources/server/web.${process.platform === 'win32' ? 'bat' : 'sh'}`);
107+
serverLocation = join(__dirname, '..', '..', '..', `resources/server/web.${process.platform === 'win32' ? 'bat' : 'sh'}`);
106108
}
107109
server = spawn(
108110
serverLocation,
109111
['--browser', 'none', '--driver', 'web'],
110112
{ env }
111113
);
112-
server.stderr.on('data', e => console.log('Server stderr: ' + e));
113-
server.stdout.on('data', e => console.log('Server stdout: ' + e));
114+
server.stderr?.on('data', e => console.log('Server stderr: ' + e));
115+
server.stdout?.on('data', e => console.log('Server stdout: ' + e));
114116
process.on('exit', teardown);
115117
process.on('SIGINT', teardown);
116118
process.on('SIGTERM', teardown);
@@ -126,7 +128,7 @@ function teardown(): void {
126128

127129
function waitForEndpoint(): Promise<string> {
128130
return new Promise<string>(r => {
129-
server!.stdout.on('data', (d: Buffer) => {
131+
server!.stdout?.on('data', (d: Buffer) => {
130132
const matches = d.toString('ascii').match(/Web UI available at (.+)/);
131133
if (matches !== null) {
132134
r(matches[1]);
@@ -135,20 +137,18 @@ function waitForEndpoint(): Promise<string> {
135137
});
136138
}
137139

138-
export function connect(headless: boolean, outPath: string, handle: string): Promise<{ client: IDisposable, driver: IDriver }> {
140+
export function connect(headless: boolean, engine: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> {
139141
return new Promise(async (c) => {
140-
const browser = await puppeteer.launch({
142+
const browser = await playwright[engine].launch({
141143
// Run in Edge dev on macOS
142144
// executablePath: '/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev',
143-
headless,
144-
slowMo: 80,
145-
args: [`--window-size=${width},${height}`]
145+
headless
146146
});
147-
const page = (await browser.pages())[0];
147+
const page = (await browser.defaultContext().pages())[0];
148148
await page.setViewport({ width, height });
149-
await page.goto(`${endpoint}&folder=vscode-remote://localhost:9888${args![1]}`);
149+
await page.goto(`${endpoint}&folder=vscode-remote://localhost:9888${URI.file(workspacePath!).path}`);
150150
const result = {
151-
client: { dispose: () => teardown },
151+
client: { dispose: () => teardown() },
152152
driver: buildDriver(browser, page)
153153
};
154154
c(result);

test/automation/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
"exclude": [
1717
"node_modules",
1818
"out",
19-
"tools",
19+
"tools"
2020
]
2121
}

0 commit comments

Comments
 (0)