vscode-extension-tester
Version:
ExTester is a package that is designed to help you run UI tests for your Visual Studio Code extensions using selenium-webdriver.
497 lines • 20 kB
JavaScript
"use strict";
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License", destination); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://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.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeUtil = exports.DEFAULT_RUN_OPTIONS = exports.ReleaseQuality = void 0;
const childProcess = __importStar(require("child_process"));
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const vsce = __importStar(require("@vscode/vsce"));
const runner_1 = require("../suite/runner");
const unpack_1 = require("./unpack");
const selenium_webdriver_1 = require("selenium-webdriver");
const download_1 = require("./download");
const extester_1 = require("../extester");
const compare_versions_1 = require("compare-versions");
var ReleaseQuality;
(function (ReleaseQuality) {
ReleaseQuality["Stable"] = "stable";
ReleaseQuality["Insider"] = "insider";
})(ReleaseQuality || (exports.ReleaseQuality = ReleaseQuality = {}));
/** defaults for the [[RunOptions]] */
exports.DEFAULT_RUN_OPTIONS = {
vscodeVersion: 'latest',
settings: '',
logLevel: selenium_webdriver_1.logging.Level.INFO,
offline: false,
resources: [],
noCache: false,
};
/**
* Handles the VS Code instance used for testing.
* Includes downloading, unpacking, launching, and version checks.
*/
class CodeUtil {
codeFolder;
downloadPlatform;
downloadFolder;
releaseType;
executablePath;
cliPath;
cliEnv;
availableVersions;
extensionsFolder;
coverage;
env = { ...process.env };
/**
* Create an instance of code handler
* @param folder Path to folder where all the artifacts will be stored.
* @param extensionsFolder Path to use as extensions directory by VS Code
*/
constructor(folder = extester_1.DEFAULT_STORAGE_FOLDER, type = ReleaseQuality.Stable, extensionsFolder, coverage) {
this.availableVersions = [];
this.downloadPlatform = this.getPlatform();
this.downloadFolder = path.resolve(folder);
this.extensionsFolder = extensionsFolder ? path.resolve(extensionsFolder) : undefined;
this.coverage = coverage;
this.releaseType = type;
if (type === ReleaseQuality.Stable) {
this.codeFolder = path.join(this.downloadFolder, process.platform === 'darwin' ? 'Visual Studio Code.app' : `VSCode-${this.downloadPlatform}`);
}
else {
this.codeFolder = path.join(this.downloadFolder, process.platform === 'darwin' ? 'Visual Studio Code - Insiders.app' : `VSCode-${this.downloadPlatform}-insider`);
}
this.findExecutables();
// remove unsafe env variables from current process to avoid spam messages like:
// Node.js environment variables are disabled because this process is invoked by other apps.
// See https://github.com/microsoft/vscode/issues/204005
for (const key in this.env) {
if (key.startsWith('NODE_')) {
delete this.env[key];
}
}
}
/**
* Get all versions for the given release stream
*/
async getVSCodeVersions() {
const apiUrl = `https://update.code.visualstudio.com/api/releases/${this.releaseType}`;
const json = await download_1.Download.getText(apiUrl);
return json;
}
/**
* Download and unpack VS Code for testing purposes
*
* @param version VS Code version to get, default latest
* @param noCache whether to skip using cached version
*/
async downloadVSCode(version = 'latest', noCache = false) {
await this.checkCodeVersion(version);
const literalVersion = version === 'latest' ? this.availableVersions[0] : version;
if (this.releaseType === ReleaseQuality.Stable && literalVersion !== this.availableVersions[0]) {
console.log('\x1b[33m%s\x1b[0m', `\n\nWARNING: You are using the outdated VS Code version '${literalVersion}'. The latest stable version is '${this.availableVersions[0]}'.\n\n`);
}
console.log(`Downloading VS Code: ${literalVersion} / ${this.releaseType}`);
if (!fs.existsSync(this.executablePath) || this.getExistingCodeVersion() !== literalVersion || noCache) {
fs.mkdirpSync(this.downloadFolder);
const url = ['https://update.code.visualstudio.com', version, this.downloadPlatform, this.releaseType].join('/');
const isTarGz = this.downloadPlatform.indexOf('linux') > -1;
const fileExtension = isTarGz ? 'tar.gz' : 'zip';
let versionPart = '';
if (this.releaseType === ReleaseQuality.Stable) {
versionPart = `-${this.releaseType}`;
}
let fileName;
if (noCache) {
fileName = this.releaseType + '.' + fileExtension;
}
else {
fileName = literalVersion + versionPart + '.' + fileExtension;
}
const zipPath = path.join(this.downloadFolder, fileName);
if (!noCache && fs.existsSync(zipPath)) {
console.log(`VS Code archive ${fileName} already exists in storage folder, skipping download`);
}
else {
console.log(`Downloading VS Code from: ${url}`);
await download_1.Download.getFile(url, zipPath, true);
console.log(`Downloaded VS Code into ${zipPath}`);
}
const tempPrefix = path.join(this.downloadFolder, 'vscode-temp-');
console.log(`Unpacking VS Code into ${this.downloadFolder}`);
const target = await fs.mkdtemp(tempPrefix);
try {
await unpack_1.Unpack.unpack(zipPath, target);
let rootDir = target;
const files = await fs.readdir(target);
if (files.length === 1) {
rootDir = path.join(target, files[0]);
}
await fs.move(rootDir, this.codeFolder, { overwrite: true });
console.log('Success!');
if (noCache) {
await fs.remove(zipPath);
console.log('Removed downloaded archive as --no_cache is active');
}
}
finally {
await fs.remove(target);
}
}
else {
console.log('VS Code exists in local cache, skipping download');
}
}
/**
* Install your extension into the test instance of VS Code
*/
installExtension(vsix, id, preRelease) {
const pjson = require(path.resolve('package.json'));
if (id) {
return this.installExt(id, preRelease);
}
const vsixPath = path.resolve(vsix ? vsix : `${pjson.name}-${pjson.version}.vsix`);
this.installExt(vsixPath);
}
/**
* Install extension dependencies from marketplace
*/
installDependencies() {
const pjson = require(path.resolve('package.json'));
const deps = pjson.extensionDependencies;
if (!deps) {
return;
}
for (const id of deps) {
this.installExt(id);
}
}
getCliInitCommand() {
const cli = `${this.cliEnv} "${this.executablePath}" "${this.cliPath}"`;
if ((0, compare_versions_1.satisfies)(this.getExistingCodeVersion(), '>=1.86.0')) {
return cli;
}
else {
return `${cli} --ms-enable-electron-run-as-node`;
}
}
installExt(pathOrID, preRelease) {
let command = `${this.getCliInitCommand()} --force --install-extension "${pathOrID}"`;
if (preRelease) {
command += ' --pre-release';
}
if (this.extensionsFolder) {
command += ` --extensions-dir=${this.extensionsFolder}`;
}
childProcess.execSync(command, { stdio: 'inherit' });
}
/**
* Open files/folders in running vscode
* @param paths vararg paths to files or folders to open
*/
open(...paths) {
const segments = paths.map((f) => `"${f}"`).join(' ');
const command = `${this.getCliInitCommand()} -r ${segments} --user-data-dir="${path.join(this.downloadFolder, 'settings')}"`;
childProcess.execSync(command);
}
/**
* Download a vsix file
* @param vsixURL URL of the vsix file
*/
async downloadExtension(vsixURL) {
fs.mkdirpSync(this.downloadFolder);
const fileName = path.basename(vsixURL);
const target = path.join(this.downloadFolder, fileName);
if (!fileName.endsWith('.vsix')) {
throw new Error('The URL does not point to a vsix file');
}
console.log(`Downloading ${fileName}`);
await download_1.Download.getFile(vsixURL, target);
console.log('Success!');
return target;
}
/**
* Package extension into a vsix file
* @param useYarn false to use npm as packaging system, true to use yarn instead
*/
async packageExtension(useYarn) {
await vsce.createVSIX({
useYarn,
});
}
/**
* Uninstall the tested extension from the test instance of VS Code
*
* @param cleanup remove the extension's directory as well.
*/
uninstallExtension(cleanup) {
const pjson = require(path.resolve('package.json'));
const extension = `${pjson.publisher}.${pjson.name}`;
if (cleanup) {
let command = `${this.getCliInitCommand()} --uninstall-extension "${extension}"`;
if (this.extensionsFolder) {
command += ` --extensions-dir=${this.extensionsFolder}`;
}
childProcess.execSync(command, { stdio: 'inherit' });
}
}
/**
* Run tests in your test environment using mocha
*
* @param testFilesPattern glob pattern of test files to run
* @param runOptions additional options for customizing the test run
*
* @return The exit code of the mocha process
*/
async runTests(testFilesPattern, runOptions = exports.DEFAULT_RUN_OPTIONS) {
if (!runOptions.offline) {
await this.checkCodeVersion(runOptions.vscodeVersion ?? exports.DEFAULT_RUN_OPTIONS.vscodeVersion);
}
else {
this.availableVersions = [this.getExistingCodeVersion()];
}
const literalVersion = runOptions.vscodeVersion === undefined || runOptions.vscodeVersion === 'latest' ? this.availableVersions[0] : runOptions.vscodeVersion;
// add chromedriver to process' path
const finalEnv = {};
Object.assign(finalEnv, process.env);
const key = 'PATH';
finalEnv[key] = [this.downloadFolder, process.env[key]].join(path.delimiter);
process.env = finalEnv;
process.env.TEST_RESOURCES = this.downloadFolder;
process.env.EXTENSIONS_FOLDER = this.extensionsFolder;
process.env.EXTENSION_DEV_PATH = this.coverage ? process.cwd() : undefined;
const runner = new runner_1.VSRunner(this.executablePath, literalVersion, this.parseSettings(runOptions.settings ?? exports.DEFAULT_RUN_OPTIONS.settings), runOptions.cleanup, this.releaseType, runOptions.config);
return await runner.runTests(testFilesPattern, this, runOptions.logLevel, runOptions.resources);
}
/**
* Finds the version of chromium used for given VS Code version.
* Works only for versions 1.30+, older versions need to be checked manually
*
* @param codeVersion version of VS Code, default latest
* @param quality release stream, default stable
*/
async getChromiumVersion(codeVersion = 'latest') {
await this.checkCodeVersion(codeVersion);
const literalVersion = codeVersion === 'latest' ? this.availableVersions[0] : codeVersion;
let revision = literalVersion;
if (literalVersion.endsWith('-insider')) {
if (codeVersion === 'latest') {
revision = 'main';
}
else {
revision = literalVersion.substring(0, literalVersion.indexOf('-insider'));
revision = `release/${revision.substring(0, revision.lastIndexOf('.'))}`;
}
}
else {
revision = `release/${revision.substring(0, revision.lastIndexOf('.'))}`;
}
const fileName = 'manifest.json';
const url = `https://raw.githubusercontent.com/Microsoft/vscode/${revision}/cgmanifest.json`;
await download_1.Download.getFile(url, path.join(this.downloadFolder, fileName));
try {
const manifest = require(path.join(this.downloadFolder, fileName));
return manifest.registrations[0].version;
}
catch (err) {
let version = '';
if (await fs.pathExists(this.codeFolder)) {
version = this.getChromiumVersionOffline();
}
if (version === '') {
throw new Error('Unable to determine required ChromeDriver version');
}
return version;
}
}
/**
* Check if VS Code exists in local cache along with an appropriate version of chromedriver
* without internet connection
*/
checkOfflineRequirements() {
try {
this.getExistingCodeVersion();
}
catch (err) {
console.log('ERROR: Cannot find a local copy of VS Code in offline mode, exiting.');
throw err;
}
return this.getChromiumVersionOffline();
}
/**
* Attempt to get chromium version from a downloaded copy of vs code
*/
getChromiumVersionOffline() {
const manifestPath = path.join(this.codeFolder, 'resources', 'app', 'ThirdPartyNotices.txt');
const text = fs.readFileSync(manifestPath).toString();
const matches = text.match(/chromium\sversion\s(.*)\s\(/);
if (matches && matches[1]) {
return matches[1];
}
return '';
}
/**
* Get the root folder of VS Code instance
*/
getCodeFolder() {
return this.codeFolder;
}
/**
* Getter for coverage enablement option
*/
get coverageEnabled() {
return this.coverage;
}
/**
* Check if given version is available in the given stream
*/
async checkCodeVersion(vscodeVersion) {
if (this.availableVersions.length < 1) {
this.availableVersions = await this.getVSCodeVersions();
}
if (vscodeVersion !== 'latest' && this.availableVersions.indexOf(vscodeVersion) < 0) {
throw new Error(`Version ${vscodeVersion} is not available in ${this.releaseType} stream`);
}
}
/**
* Check what VS Code version is present in the testing folder
*/
getExistingCodeVersion() {
const command = `${this.cliEnv} "${this.executablePath}" "${this.cliPath}"`;
let out;
try {
out = childProcess.execSync(`${command} -v`, { env: this.env });
}
catch (error) {
out = childProcess.execSync(`${command} --ms-enable-electron-run-as-node -v`, { env: this.env });
}
return out.toString().split('\n')[0];
}
/**
* Construct the platform string based on OS
*/
getPlatform() {
let platform = process.platform;
const arch = process.arch;
this.cliEnv = 'ELECTRON_RUN_AS_NODE=1';
if (platform === 'linux') {
platform += arch === 'ia32' ? '-ia32' : `-${arch}`;
}
else if (platform === 'win32') {
switch (arch) {
case 'arm64': {
platform += '-arm64';
break;
}
case 'x64': {
platform += '-x64';
break;
}
default: {
throw new Error(`Unknown Platform: ${arch}`);
}
}
platform += '-archive';
this.cliEnv = `set ${this.cliEnv} &&`;
}
else if (platform === 'darwin') {
platform += '-universal';
}
return platform;
}
/**
* Setup paths specific to used OS
*/
findExecutables() {
this.cliPath = path.join(this.codeFolder, 'resources', 'app', 'out', 'cli.js');
switch (process.platform) {
case 'darwin':
this.executablePath = path.join(this.codeFolder, 'Contents', 'MacOS', 'Electron');
this.cliPath = path.join(this.codeFolder, 'Contents', 'Resources', 'app', 'out', 'cli.js');
break;
case 'win32':
this.executablePath = path.join(this.codeFolder, 'Code.exe');
if (this.releaseType === ReleaseQuality.Insider) {
this.executablePath = path.join(this.codeFolder, 'Code - Insiders.exe');
}
break;
case 'linux':
this.executablePath = path.join(this.codeFolder, 'code');
if (this.releaseType === ReleaseQuality.Insider) {
this.executablePath = path.join(this.codeFolder, 'code-insiders');
}
break;
}
}
/**
* Parse JSON from a file
* @param path path to json file
*/
parseSettings(path) {
if (!path) {
return {};
}
let text = '';
try {
text = fs.readFileSync(path).toString();
}
catch (err) {
throw new Error(`Unable to read settings from ${path}:\n ${err}`);
}
try {
return JSON.parse(text);
}
catch (err) {
throw new Error(`Error parsing the settings file from ${path}:\n ${err}`);
}
}
}
exports.CodeUtil = CodeUtil;
//# sourceMappingURL=codeUtil.js.map