Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/unit/browser/index.js
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
//@ts-check
7
'use strict';
8
9
const path = require('path');
10
const glob = require('glob');
11
const events = require('events');
12
const mocha = require('mocha');
13
const createStatsCollector = require('mocha/lib/stats-collector');
14
const MochaJUnitReporter = require('mocha-junit-reporter');
15
const url = require('url');
16
const minimatch = require('minimatch');
17
const fs = require('fs');
18
const playwright = require('@playwright/test');
19
const { applyReporter } = require('../reporter');
20
const yaserver = require('yaserver');
21
const http = require('http');
22
const { randomBytes } = require('crypto');
23
const minimist = require('minimist');
24
const { promisify } = require('node:util');
25
26
/**
27
* @type {{
28
* run: string;
29
* grep: string;
30
* runGlob: string;
31
* browser: string;
32
* reporter: string;
33
* 'reporter-options': string;
34
* tfs: string;
35
* build: boolean;
36
* debug: boolean;
37
* sequential: boolean;
38
* help: boolean;
39
* }}
40
*/
41
const args = minimist(process.argv.slice(2), {
42
boolean: ['build', 'debug', 'sequential', 'help'],
43
string: ['run', 'grep', 'runGlob', 'browser', 'reporter', 'reporter-options', 'tfs'],
44
default: {
45
build: false,
46
browser: ['chromium', 'firefox', 'webkit'],
47
reporter: process.platform === 'win32' ? 'list' : 'spec',
48
'reporter-options': ''
49
},
50
alias: {
51
grep: ['g', 'f'],
52
runGlob: ['glob', 'runGrep'],
53
debug: ['debug-browser'],
54
help: 'h'
55
},
56
describe: {
57
build: 'run with build output (out-build)',
58
run: 'only run tests matching <relative_file_path>',
59
grep: 'only run tests matching <pattern>',
60
debug: 'do not run browsers headless',
61
sequential: 'only run suites for a single browser at a time',
62
browser: 'browsers in which tests should run',
63
reporter: 'the mocha reporter',
64
'reporter-options': 'the mocha reporter options',
65
tfs: 'tfs',
66
help: 'show the help'
67
}
68
});
69
70
if (args.help) {
71
console.log(`Usage: node ${process.argv[1]} [options]
72
73
Options:
74
--build run with build output (out-build)
75
--run <relative_file_path> only run tests matching <relative_file_path>
76
--grep, -g, -f <pattern> only run tests matching <pattern>
77
--debug, --debug-browser do not run browsers headless
78
--sequential only run suites for a single browser at a time
79
--browser <browser> browsers in which tests should run. separate the channel with a dash, e.g. 'chromium-msedge' or 'chromium-chrome'
80
--reporter <reporter> the mocha reporter
81
--reporter-options <reporter-options> the mocha reporter options
82
--tfs <tfs> tfs
83
--help, -h show the help`);
84
process.exit(0);
85
}
86
87
const isDebug = !!args.debug;
88
89
const withReporter = (function () {
90
if (args.tfs) {
91
{
92
const testResultsRoot = process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE;
93
return (browserType, runner) => {
94
new mocha.reporters.Spec(runner);
95
new MochaJUnitReporter(runner, {
96
reporterOptions: {
97
testsuitesTitle: `${args.tfs} ${process.platform}`,
98
mochaFile: testResultsRoot ? path.join(testResultsRoot, `test-results/${process.platform}-${process.arch}-${browserType}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined
99
}
100
});
101
};
102
}
103
} else {
104
return (_, runner) => applyReporter(runner, args);
105
}
106
})();
107
108
const outdir = args.build ? 'out-build' : 'out';
109
const rootDir = path.resolve(__dirname, '..', '..', '..');
110
const out = path.join(rootDir, `${outdir}`);
111
112
function ensureIsArray(a) {
113
return Array.isArray(a) ? a : [a];
114
}
115
116
const testModules = (async function () {
117
118
const excludeGlob = '**/{node,electron-browser,electron-main,electron-utility}/**/*.test.js';
119
let isDefaultModules = true;
120
let promise;
121
122
if (args.run) {
123
// use file list (--run)
124
isDefaultModules = false;
125
promise = Promise.resolve(ensureIsArray(args.run).map(file => {
126
file = file.replace(/^src/, 'out');
127
file = file.replace(/\.ts$/, '.js');
128
return path.relative(out, file);
129
}));
130
131
} else {
132
// glob patterns (--glob)
133
const defaultGlob = '**/*.test.js';
134
const pattern = args.runGlob || defaultGlob;
135
isDefaultModules = pattern === defaultGlob;
136
137
promise = new Promise((resolve, reject) => {
138
glob(pattern, { cwd: out }, (err, files) => {
139
if (err) {
140
reject(err);
141
} else {
142
resolve(files);
143
}
144
});
145
});
146
}
147
148
return promise.then(files => {
149
const modules = [];
150
for (const file of files) {
151
if (!minimatch(file, excludeGlob)) {
152
modules.push(file.replace(/\.js$/, ''));
153
154
} else if (!isDefaultModules) {
155
console.warn(`DROPPING ${file} because it cannot be run inside a browser`);
156
}
157
}
158
return modules;
159
});
160
})();
161
162
function consoleLogFn(msg) {
163
const type = msg.type();
164
const candidate = console[type];
165
if (candidate) {
166
return candidate;
167
}
168
169
if (type === 'warning') {
170
return console.warn;
171
}
172
173
return console.log;
174
}
175
176
async function createServer() {
177
// Demand a prefix to avoid issues with other services on the
178
// machine being able to access the test server.
179
const prefix = '/' + randomBytes(16).toString('hex');
180
const serveStatic = await yaserver.createServer({ rootDir });
181
182
/** Handles a request for a remote method call, invoking `fn` and returning the result */
183
const remoteMethod = async (req, response, fn) => {
184
const params = await new Promise((resolve, reject) => {
185
const body = [];
186
req.on('data', chunk => body.push(chunk));
187
req.on('end', () => resolve(JSON.parse(Buffer.concat(body).toString())));
188
req.on('error', reject);
189
});
190
try {
191
const result = await fn(...params);
192
response.writeHead(200, { 'Content-Type': 'application/json' });
193
response.end(JSON.stringify(result));
194
} catch (err) {
195
response.writeHead(500);
196
response.end(err.message);
197
}
198
};
199
200
const server = http.createServer((request, response) => {
201
if (!request.url?.startsWith(prefix)) {
202
return response.writeHead(404).end();
203
}
204
205
// rewrite the URL so the static server can handle the request correctly
206
request.url = request.url.slice(prefix.length);
207
208
function massagePath(p) {
209
// TODO@jrieken FISHY but it enables snapshot
210
// in ESM browser tests
211
p = String(p).replace(/\\/g, '/').replace(prefix, rootDir);
212
return p;
213
}
214
215
switch (request.url) {
216
case '/remoteMethod/__readFileInTests':
217
return remoteMethod(request, response, p => fs.promises.readFile(massagePath(p), 'utf-8'));
218
case '/remoteMethod/__writeFileInTests':
219
return remoteMethod(request, response, (p, contents) => fs.promises.writeFile(massagePath(p), contents));
220
case '/remoteMethod/__readDirInTests':
221
return remoteMethod(request, response, p => fs.promises.readdir(massagePath(p)));
222
case '/remoteMethod/__unlinkInTests':
223
return remoteMethod(request, response, p => fs.promises.unlink(massagePath(p)));
224
case '/remoteMethod/__mkdirPInTests':
225
return remoteMethod(request, response, p => fs.promises.mkdir(massagePath(p), { recursive: true }));
226
default:
227
return serveStatic.handle(request, response);
228
}
229
});
230
231
return new Promise((resolve, reject) => {
232
server.listen(0, 'localhost', () => {
233
resolve({
234
dispose: () => server.close(),
235
// @ts-ignore
236
url: `http://localhost:${server.address().port}${prefix}`
237
});
238
});
239
server.on('error', reject);
240
});
241
}
242
243
async function runTestsInBrowser(testModules, browserType, browserChannel) {
244
const server = await createServer();
245
const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug), devtools: Boolean(args.debug), channel: browserChannel });
246
const context = await browser.newContext();
247
const page = await context.newPage();
248
const target = new URL(server.url + '/test/unit/browser/renderer.html');
249
target.searchParams.set('baseUrl', url.pathToFileURL(path.join(rootDir, 'src')).toString());
250
if (args.build) {
251
target.searchParams.set('build', 'true');
252
}
253
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) {
254
target.searchParams.set('ci', 'true');
255
}
256
257
// append CSS modules as query-param
258
await promisify(require('glob'))('**/*.css', { cwd: out }).then(async cssModules => {
259
const cssData = await new Response((await new Response(cssModules.join(',')).blob()).stream().pipeThrough(new CompressionStream('gzip'))).arrayBuffer();
260
target.searchParams.set('_devCssData', Buffer.from(cssData).toString('base64'));
261
});
262
263
const emitter = new events.EventEmitter();
264
await page.exposeFunction('mocha_report', (type, data1, data2) => {
265
emitter.emit(type, data1, data2);
266
});
267
268
await page.goto(target.href);
269
270
if (args.build) {
271
const nlsMessages = await fs.promises.readFile(path.join(out, 'nls.messages.json'), 'utf8');
272
await page.evaluate(value => {
273
// when running from `out-build`, ensure to load the default
274
// messages file, because all `nls.localize` calls have their
275
// english values removed and replaced by an index.
276
// @ts-ignore
277
globalThis._VSCODE_NLS_MESSAGES = JSON.parse(value);
278
}, nlsMessages);
279
}
280
281
page.on('console', async msg => {
282
consoleLogFn(msg)(msg.text(), await Promise.all(msg.args().map(async arg => await arg.jsonValue())));
283
});
284
285
withReporter(browserType, new EchoRunner(emitter, browserChannel ? `${browserType.toUpperCase()}-${browserChannel.toUpperCase()}` : browserType.toUpperCase()));
286
287
// collection failures for console printing
288
const failingModuleIds = [];
289
const failingTests = [];
290
emitter.on('fail', (test, err) => {
291
failingTests.push({ title: test.fullTitle, message: err.message });
292
293
if (err.stack) {
294
const regex = /(vs\/.*\.test)\.js/;
295
for (const line of String(err.stack).split('\n')) {
296
const match = regex.exec(line);
297
if (match) {
298
failingModuleIds.push(match[1]);
299
return;
300
}
301
}
302
}
303
});
304
305
try {
306
// @ts-expect-error
307
await page.evaluate(opts => loadAndRun(opts), {
308
modules: testModules,
309
grep: args.grep,
310
});
311
} catch (err) {
312
console.error(err);
313
}
314
if (!isDebug) {
315
server?.dispose();
316
await browser.close();
317
}
318
319
if (failingTests.length > 0) {
320
let res = `The followings tests are failing:\n - ${failingTests.map(({ title, message }) => `${title} (reason: ${message})`).join('\n - ')}`;
321
322
if (failingModuleIds.length > 0) {
323
res += `\n\nTo DEBUG, open ${browserType.toUpperCase()} and navigate to ${target.href}?${failingModuleIds.map(module => `m=${module}`).join('&')}`;
324
}
325
326
return `${res}\n`;
327
}
328
}
329
330
class EchoRunner extends events.EventEmitter {
331
332
constructor(event, title = '') {
333
super();
334
createStatsCollector(this);
335
event.on('start', () => this.emit('start'));
336
event.on('end', () => this.emit('end'));
337
event.on('suite', (suite) => this.emit('suite', EchoRunner.deserializeSuite(suite, title)));
338
event.on('suite end', (suite) => this.emit('suite end', EchoRunner.deserializeSuite(suite, title)));
339
event.on('test', (test) => this.emit('test', EchoRunner.deserializeRunnable(test)));
340
event.on('test end', (test) => this.emit('test end', EchoRunner.deserializeRunnable(test)));
341
event.on('hook', (hook) => this.emit('hook', EchoRunner.deserializeRunnable(hook)));
342
event.on('hook end', (hook) => this.emit('hook end', EchoRunner.deserializeRunnable(hook)));
343
event.on('pass', (test) => this.emit('pass', EchoRunner.deserializeRunnable(test)));
344
event.on('fail', (test, err) => this.emit('fail', EchoRunner.deserializeRunnable(test, title), EchoRunner.deserializeError(err)));
345
event.on('pending', (test) => this.emit('pending', EchoRunner.deserializeRunnable(test)));
346
}
347
348
static deserializeSuite(suite, titleExtra) {
349
return {
350
root: suite.root,
351
suites: suite.suites,
352
tests: suite.tests,
353
title: titleExtra && suite.title ? `${suite.title} - /${titleExtra}/` : suite.title,
354
titlePath: () => suite.titlePath,
355
fullTitle: () => suite.fullTitle,
356
timeout: () => suite.timeout,
357
retries: () => suite.retries,
358
slow: () => suite.slow,
359
bail: () => suite.bail
360
};
361
}
362
363
static deserializeRunnable(runnable, titleExtra) {
364
return {
365
title: runnable.title,
366
fullTitle: () => titleExtra && runnable.fullTitle ? `${runnable.fullTitle} - /${titleExtra}/` : runnable.fullTitle,
367
titlePath: () => runnable.titlePath,
368
async: runnable.async,
369
slow: () => runnable.slow,
370
speed: runnable.speed,
371
duration: runnable.duration,
372
currentRetry: () => runnable.currentRetry,
373
};
374
}
375
376
static deserializeError(err) {
377
const inspect = err.inspect;
378
err.inspect = () => inspect;
379
return err;
380
}
381
}
382
383
testModules.then(async modules => {
384
385
// run tests in selected browsers
386
const browsers = Array.isArray(args.browser)
387
? args.browser : [args.browser];
388
389
let messages = [];
390
let didFail = false;
391
392
try {
393
if (args.sequential) {
394
for (const browser of browsers) {
395
const [browserType, browserChannel] = browser.split('-');
396
messages.push(await runTestsInBrowser(modules, browserType, browserChannel));
397
}
398
} else {
399
messages = await Promise.all(browsers.map(async browser => {
400
const [browserType, browserChannel] = browser.split('-');
401
return await runTestsInBrowser(modules, browserType, browserChannel);
402
}));
403
}
404
} catch (err) {
405
console.error(err);
406
if (!isDebug) {
407
process.exit(1);
408
}
409
}
410
411
// aftermath
412
for (const msg of messages) {
413
if (msg) {
414
didFail = true;
415
console.log(msg);
416
}
417
}
418
if (!isDebug) {
419
process.exit(didFail ? 1 : 0);
420
}
421
422
}).catch(err => {
423
console.error(err);
424
});
425
426