Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/integration/browser/src/index.ts
3520 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as path from 'path';
7
import * as cp from 'child_process';
8
import * as playwright from '@playwright/test';
9
import * as url from 'url';
10
import * as tmp from 'tmp';
11
import * as rimraf from 'rimraf';
12
import { URI } from 'vscode-uri';
13
import * as kill from 'tree-kill';
14
import * as minimist from 'minimist';
15
import { promisify } from 'util';
16
import { promises } from 'fs';
17
18
const root = path.join(__dirname, '..', '..', '..', '..');
19
const logsPath = path.join(root, '.build', 'logs', 'integration-tests-browser');
20
21
const args = minimist(process.argv.slice(2), {
22
string: [
23
// path to the workspace (folder or *.code-workspace file) to open in the test
24
'workspacePath',
25
// path to the extension to test
26
'extensionDevelopmentPath',
27
// path to the extension tests
28
'extensionTestsPath',
29
// browser in which integration tests should run
30
'browser',
31
],
32
boolean: [
33
'help',
34
// do not run browsers headless
35
'debug',
36
],
37
alias: {
38
h: 'help'
39
},
40
default: {
41
'browser': 'chromium'
42
}
43
});
44
45
if (args.help) {
46
console.error(`Integration test runner for VS Code in the browser
47
Usage: node integration-tests-browser/out/index.js [options]
48
49
--workspacePath <path> Path to the workspace (folder or *.code-workspace file) to open in the test
50
--extensionDevelopmentPath <path> Path to the extension to test
51
--extensionTestsPath <path> Path to the extension tests
52
--browser <browser> Browser in which integration tests should run. separate the channel with a dash, e.g. 'chromium-msedge' or 'chromium-chrome'
53
--debug Do not run browsers headless
54
--help Print this help message
55
`);
56
57
process.exit(1);
58
}
59
60
const width = 1440;
61
const height = 900;
62
63
type BrowserType = 'chromium' | 'firefox' | 'webkit';
64
type BrowserChannel = 'msedge' | 'chrome';
65
66
async function runTestsInBrowser(browserType: BrowserType, browserChannel: BrowserChannel, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise<void> {
67
const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug), channel: browserChannel });
68
const context = await browser.newContext();
69
70
const page = await context.newPage();
71
await page.setViewportSize({ width, height });
72
73
page.on('pageerror', async error => console.error(`Playwright ERROR: page error: ${error}`));
74
page.on('crash', page => console.error('Playwright ERROR: page crash'));
75
page.on('response', async response => {
76
if (response.status() >= 400) {
77
console.error(`Playwright ERROR: HTTP status ${response.status()} for ${response.url()}`);
78
}
79
});
80
page.on('console', async msg => {
81
try {
82
if (msg.type() === 'error' || msg.type() === 'warning') {
83
consoleLogFn(msg)(msg.text(), await Promise.all(msg.args().map(async arg => await arg.jsonValue())));
84
}
85
} catch (err) {
86
console.error('Error logging console', err);
87
}
88
});
89
page.on('requestfailed', e => {
90
console.error('Request Failed', e.url(), e.failure()?.errorText);
91
});
92
93
await page.exposeFunction('codeAutomationLog', (type: string, args: any[]) => {
94
console[type](...args);
95
});
96
97
await page.exposeFunction('codeAutomationExit', async (code: number, logs: Array<{ readonly relativePath: string; readonly contents: string }>) => {
98
try {
99
for (const log of logs) {
100
const absoluteLogsPath = path.join(logsPath, log.relativePath);
101
102
await promises.mkdir(path.dirname(absoluteLogsPath), { recursive: true });
103
await promises.writeFile(absoluteLogsPath, log.contents);
104
}
105
} catch (error) {
106
console.error(`Error saving web client logs (${error})`);
107
}
108
109
if (args.debug) {
110
return;
111
}
112
113
try {
114
await browser.close();
115
} catch (error) {
116
console.error(`Error when closing browser: ${error}`);
117
}
118
119
try {
120
await promisify(kill)(server.pid!);
121
} catch (error) {
122
console.error(`Error when killing server process tree (pid: ${server.pid}): ${error}`);
123
}
124
125
process.exit(code);
126
});
127
128
const host = endpoint.host;
129
const protocol = 'vscode-remote';
130
131
const testWorkspacePath = URI.file(path.resolve(args.workspacePath)).path;
132
const testExtensionUri = url.format({ pathname: URI.file(path.resolve(args.extensionDevelopmentPath)).path, protocol, host, slashes: true });
133
const testFilesUri = url.format({ pathname: URI.file(path.resolve(args.extensionTestsPath)).path, protocol, host, slashes: true });
134
135
const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","ef65ac1ba57f57f2a3961bfe94aa20481caca4c6"],["skipWelcome","true"]]`;
136
137
if (path.extname(testWorkspacePath) === '.code-workspace') {
138
await page.goto(`${endpoint.href}&workspace=${testWorkspacePath}&payload=${payloadParam}`);
139
} else {
140
await page.goto(`${endpoint.href}&folder=${testWorkspacePath}&payload=${payloadParam}`);
141
}
142
}
143
144
function consoleLogFn(msg: playwright.ConsoleMessage) {
145
const type = msg.type();
146
const candidate = console[type];
147
if (candidate) {
148
return candidate;
149
}
150
151
if (type === 'warning') {
152
return console.warn;
153
}
154
155
return console.log;
156
}
157
158
async function launchServer(browserType: BrowserType, browserChannel: BrowserChannel): Promise<{ endpoint: url.UrlWithStringQuery; server: cp.ChildProcess }> {
159
160
// Ensure a tmp user-data-dir is used for the tests
161
const tmpDir = tmp.dirSync({ prefix: 't' });
162
const testDataPath = tmpDir.name;
163
process.once('exit', () => rimraf.sync(testDataPath));
164
165
const userDataDir = path.join(testDataPath, 'd');
166
167
const env = {
168
VSCODE_BROWSER: browserChannel ? `${browserType}-${browserChannel}` : browserType,
169
...process.env
170
};
171
172
const serverArgs = ['--enable-proposed-api', '--disable-telemetry', '--disable-experiments', '--server-data-dir', userDataDir, '--accept-server-license-terms', '--disable-workspace-trust'];
173
174
let serverLocation: string;
175
if (process.env.VSCODE_REMOTE_SERVER_PATH) {
176
const { serverApplicationName } = require(path.join(process.env.VSCODE_REMOTE_SERVER_PATH, 'product.json'));
177
serverLocation = path.join(process.env.VSCODE_REMOTE_SERVER_PATH, 'bin', `${serverApplicationName}${process.platform === 'win32' ? '.cmd' : ''}`);
178
179
if (args.debug) {
180
console.log(`Starting built server from '${serverLocation}'`);
181
}
182
} else {
183
serverLocation = path.join(root, `scripts/code-server.${process.platform === 'win32' ? 'bat' : 'sh'}`);
184
process.env.VSCODE_DEV = '1';
185
186
if (args.debug) {
187
console.log(`Starting server out of sources from '${serverLocation}'`);
188
}
189
}
190
191
const serverLogsPath = path.join(logsPath, 'server');
192
console.log(`Storing log files into '${serverLogsPath}'`);
193
serverArgs.push('--logsPath', serverLogsPath);
194
195
const stdio: cp.StdioOptions = args.debug ? 'pipe' : ['ignore', 'pipe', 'ignore'];
196
const shell: boolean = (process.platform === 'win32');
197
const serverProcess = cp.spawn(
198
serverLocation,
199
serverArgs,
200
{ env, stdio, shell }
201
);
202
203
if (args.debug) {
204
serverProcess.stderr!.on('data', error => console.log(`Server stderr: ${error}`));
205
serverProcess.stdout!.on('data', data => console.log(`Server stdout: ${data}`));
206
}
207
208
process.on('exit', () => serverProcess.kill());
209
process.on('SIGINT', () => {
210
serverProcess.kill();
211
process.exit(128 + 2); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events
212
});
213
process.on('SIGTERM', () => {
214
serverProcess.kill();
215
process.exit(128 + 15); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events
216
});
217
218
return new Promise(c => {
219
serverProcess.stdout!.on('data', data => {
220
const matches = data.toString('ascii').match(/Web UI available at (.+)/);
221
if (matches !== null) {
222
c({ endpoint: url.parse(matches[1]), server: serverProcess });
223
}
224
});
225
});
226
}
227
228
const [browserType, browserChannel] = args.browser.split('-');
229
launchServer(browserType, browserChannel).then(async ({ endpoint, server }) => {
230
return runTestsInBrowser(browserType, browserChannel, endpoint, server);
231
}, error => {
232
console.error(error);
233
process.exit(1);
234
});
235
236