@salesforce/salesforcedx-vscode-test-tools
Version:
Test automation framework for Salesforce Extensions for VS Code
911 lines (782 loc) • 33.9 kB
text/typescript
import { ExTester } from 'vscode-extension-tester';
import { EnvironmentSettings } from './environmentSettings';
import path from 'path';
import fs from 'fs/promises';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { ReleaseQuality } from 'vscode-extension-tester/out/util/codeUtil';
import { expect } from 'chai';
import { log } from 'console';
import { orgLoginSfdxUrl, setAlias } from './system-operations/cliCommands';
import { createOrOverwriteFile, getVsixFilesFromDir } from './system-operations';
import { TestConfig, ExtensionConfig } from './core/types';
import { createDefaultTestConfig, validateTestConfig, normalizePath } from './core/helpers';
import { verifyAliasAndUserName } from './salesforce-components/authorization';
import { retryOperation } from './retryUtils';
// Set Ubuntu Chrome arguments immediately when module loads
if (process.platform === 'linux') {
const uniqueId = `${Date.now()}-${process.pid}`;
const tempDir = path.join(process.cwd(), 'test-resources', 'chrome-user-data', uniqueId);
const ubuntuChromeArgs = [
`--user-data-dir=${tempDir}`,
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-features=VizDisplayCompositor',
'--disable-gpu',
'--disable-software-rasterizer',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-field-trial-config',
'--disable-ipc-flooding-protection',
'--single-process',
'--no-zygote'
].join(' ');
process.env.VSCODE_EXTENSION_TESTER_CHROMEDRIVER_ARGS = ubuntuChromeArgs;
process.env.CHROME_OPTIONS = ubuntuChromeArgs;
process.env.CHROMIUM_FLAGS = ubuntuChromeArgs;
log(`Early Ubuntu Chrome setup: ${ubuntuChromeArgs}`);
}
// Set Chrome-related environment variables early
process.env.CHROME_OPTIONS =
'--no-sandbox --disable-dev-shm-usage --disable-gpu --remote-debugging-port=9222 --disable-web-security --disable-features=VizDisplayCompositor --single-process --no-zygote --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --user-data-dir=/tmp/chrome-user-data-' +
Date.now();
process.env.CHROMIUM_FLAGS =
'--no-sandbox --disable-dev-shm-usage --disable-gpu --remote-debugging-port=9222 --disable-web-security --disable-features=VizDisplayCompositor --single-process --no-zygote --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --user-data-dir=/tmp/chrome-user-data-' +
Date.now();
process.env.CHROME_BIN = '/usr/bin/google-chrome';
process.env.CHROMIUM_BIN = '/usr/bin/google-chrome';
// Skip Husky installation during test setup
process.env.HUSKY_SKIP_INSTALL = '1';
// Set NPM to use legacy peer dependencies
process.env.NPM_CONFIG_LEGACY_PEER_DEPS = 'true';
// For Ubuntu CI environments
if (process.platform === 'linux') {
// Set up virtual display for headless operation
process.env.DISPLAY = ':99';
process.env.CHROME_NO_SANDBOX = 'true';
process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = 'true';
process.env.PUPPETEER_EXECUTABLE_PATH = '/usr/bin/google-chrome';
// Additional Chrome flags for CI
process.env.CHROME_DEVEL_SANDBOX = '/usr/lib/chromium-browser/chrome-sandbox';
}
class TestSetupAndRunner extends ExTester {
protected static _exTester: TestSetupAndRunner;
private testConfig: TestConfig;
private spec: string | string[] | undefined;
private static readonly MAX_RETRIES = 3;
private static readonly RETRY_DELAY = 5000;
constructor(testConfig?: Partial<TestConfig>, spec?: string | string[] | undefined) {
log(`Init testConfig with testConfig: ${JSON.stringify(testConfig)}`);
log(`Init testConfig with spec: ${spec}`);
// Create config with defaults and overrides
const config = createDefaultTestConfig(testConfig);
// Validate config and set defaults for missing values
validateTestConfig(config);
super(config?.testResources, ReleaseQuality.Stable);
this.testConfig = config;
this.spec = spec;
}
/**
* Helper method to log important test information using standard terminology
*/
private logTestEnvironment(): void {
log(`Setting up test environment with:`);
log(`- Test Resources: ${this.testConfig.testResources}`);
log(`- Extensions Folder: ${this.testConfig.extensionsFolder}`);
log(`- VS Code Version: ${this.testConfig.codeVersion}`);
// Log Chrome driver arguments if available
const chromeDriverArgs = EnvironmentSettings.getInstance().chromeDriverArgs;
if (chromeDriverArgs) {
log(`- ChromeDriver Arguments: ${chromeDriverArgs}`);
}
// Log the throttle factor
const throttleFactor = EnvironmentSettings.getInstance().throttleFactor;
log(`- Throttle Factor: ${throttleFactor}`);
}
/**
* Sets up Ubuntu-specific Chrome driver arguments to avoid user data directory conflicts
*/
private async setupUbuntuChromeArgs(): Promise<void> {
// Only apply Ubuntu-specific settings if we're on Linux and no custom args are already set
if (process.platform !== 'linux') {
console.log(`Not on Linux, skipping setupUbuntuChromeArgs: ${process.platform}`);
return;
}
const existingArgs = EnvironmentSettings.getInstance().chromeDriverArgs;
if (existingArgs) {
log(`Chrome driver arguments already set: ${existingArgs}`);
return;
}
// Generate a unique user data directory for this test run
const uniqueId = `${Date.now()}-${process.pid}`;
const tempDir = path.join(this.testConfig.testResources || 'test-resources', 'chrome-user-data', uniqueId);
// Ensure the directory exists
try {
await fs.mkdir(tempDir, { recursive: true });
log(`Created Chrome user data directory: ${tempDir}`);
} catch (error) {
log(`Warning: Could not create Chrome user data directory: ${error}`);
}
const ubuntuChromeArgs = [
`--user-data-dir=${tempDir}`,
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-features=VizDisplayCompositor',
'--disable-gpu',
'--disable-software-rasterizer',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-field-trial-config',
'--disable-ipc-flooding-protection',
'--single-process',
'--no-zygote'
].join(' ');
// Set the environment variable for vscode-extension-tester to pick up
process.env.VSCODE_EXTENSION_TESTER_CHROMEDRIVER_ARGS = ubuntuChromeArgs;
// Also try other possible environment variables
process.env.CHROME_OPTIONS = ubuntuChromeArgs;
process.env.CHROMIUM_FLAGS = ubuntuChromeArgs;
process.env.GOOGLE_CHROME_OPTS = ubuntuChromeArgs;
log(`Set Ubuntu Chrome driver arguments: ${ubuntuChromeArgs}`);
log(
`Environment variable VSCODE_EXTENSION_TESTER_CHROMEDRIVER_ARGS: ${process.env.VSCODE_EXTENSION_TESTER_CHROMEDRIVER_ARGS}`
);
// Also try setting the user data dir in a more explicit way
const userDataDir = tempDir;
process.env.CHROME_USER_DATA_DIR = userDataDir;
process.env.CHROMIUM_USER_DATA_DIR = userDataDir;
log(`Set CHROME_USER_DATA_DIR: ${userDataDir}`);
}
/**
* Kills existing Chrome processes that might be using the user data directory
*/
private async killExistingChromeProcesses(): Promise<void> {
try {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
log('Attempting to kill existing Chrome processes...');
// Kill Chrome processes
const commands = [
'pkill -f "chrome.*--test-type"',
'pkill -f "chromium.*--test-type"',
'pkill -f "google-chrome.*--test-type"',
'pkill -f "chrome.*--user-data-dir"',
'pkill -f "chromium.*--user-data-dir"'
];
for (const command of commands) {
try {
await execAsync(command);
log(`Executed: ${command}`);
} catch (error) {
// It's normal for pkill to exit with code 1 if no processes are found
log(`Command ${command} completed (processes may not have been running)`);
}
}
// Wait a moment for processes to fully terminate
await new Promise(resolve => setTimeout(resolve, 2000));
log('Chrome process cleanup completed');
} catch (error) {
log(`Warning: Could not kill Chrome processes: ${error}`);
}
}
/**
* Kills existing VS Code processes on Windows to prevent file locking issues during extension installation
*/
private async killExistingVSCodeProcesses(): Promise<void> {
if (process.platform !== 'win32') {
return;
}
try {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
log('Attempting to kill existing VS Code processes on Windows...');
// Kill VS Code processes on Windows
const commands = [
'taskkill /f /im Code.exe',
'taskkill /f /im code.exe',
'taskkill /f /im VSCode.exe',
'taskkill /f /im electron.exe /fi "WINDOWTITLE eq Visual Studio Code*"',
'taskkill /f /im node.exe /fi "COMMANDLINE eq *vscode*"'
];
for (const command of commands) {
try {
await execAsync(command);
log(`Executed: ${command}`);
} catch (error) {
// It's normal for taskkill to exit with code 1 if no processes are found
log(`Command ${command} completed (processes may not have been running)`);
}
}
// Wait a moment for processes to fully terminate and release file locks
await new Promise(resolve => setTimeout(resolve, 3000));
log('VS Code process cleanup completed');
} catch (error) {
log(`Warning: Could not kill VS Code processes: ${error}`);
}
}
/**
* Cleans up Windows VS Code extension temporary files that may cause locking issues
*/
private async cleanupWindowsExtensionTempFiles(): Promise<void> {
if (process.platform !== 'win32') {
return;
}
try {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
log('Cleaning up Windows VS Code extension temporary files...');
// Clean up VS Code extension temporary files
const cleanupCommands = [
'del /f /q "%USERPROFILE%\\.vscode\\extensions\\*.vsctmp" 2>nul',
'del /f /q "%USERPROFILE%\\.vscode\\extensions\\*.tmp" 2>nul',
'rmdir /s /q "%TEMP%\\vscode-*" 2>nul',
'rmdir /s /q "%TEMP%\\Code-*" 2>nul'
];
for (const command of cleanupCommands) {
try {
await execAsync(command);
log(`Executed cleanup: ${command}`);
} catch (error) {
// It's normal for cleanup commands to fail if files don't exist
log(`Cleanup command completed: ${command}`);
}
}
log('Windows extension temp file cleanup completed');
} catch (error) {
log(`Warning: Could not cleanup Windows extension temp files: ${error}`);
}
}
/**
* Creates a Chrome wrapper script that forces Ubuntu-specific arguments
*/
private async createChromeWrapper(): Promise<void> {
try {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
// Find Chrome executable
const chromeCommands = [
'which google-chrome',
'which google-chrome-stable',
'which chromium-browser',
'which chromium'
];
let chromeExePath = '';
for (const command of chromeCommands) {
try {
const result = await execAsync(command);
chromeExePath = result.stdout.trim();
if (chromeExePath) {
log(`Found Chrome at: ${chromeExePath}`);
break;
}
} catch (error) {
// Command failed, try next one
}
}
if (!chromeExePath) {
log('Warning: Could not find Chrome executable for wrapper');
return;
}
// Create unique user data directory
const uniqueId = `${Date.now()}-${process.pid}`;
const tempDir = path.join(this.testConfig.testResources || 'test-resources', 'chrome-user-data', uniqueId);
await fs.mkdir(tempDir, { recursive: true });
// Create wrapper script
const wrapperPath = path.join(this.testConfig.testResources || 'test-resources', 'chrome-wrapper.sh');
const wrapperScript = `#!/bin/bash
# Chrome wrapper script for Ubuntu testing
exec "${chromeExePath}" \\
--user-data-dir="${tempDir}" \\
--no-sandbox \\
--disable-dev-shm-usage \\
--disable-web-security \\
--disable-features=VizDisplayCompositor \\
--disable-gpu \\
--disable-software-rasterizer \\
--disable-background-timer-throttling \\
--disable-backgrounding-occluded-windows \\
--disable-renderer-backgrounding \\
--disable-field-trial-config \\
--disable-ipc-flooding-protection \\
--single-process \\
--no-zygote \\
"$@"
`;
createOrOverwriteFile(wrapperPath, wrapperScript);
await execAsync(`chmod +x "${wrapperPath}"`);
// Override Chrome path in environment
process.env.CHROME_BIN = wrapperPath;
process.env.CHROMIUM_BIN = wrapperPath;
process.env.GOOGLE_CHROME_BIN = wrapperPath;
// Create wrapper scripts for common Chrome names in our own directory
const wrapperDir = path.join(this.testConfig.testResources || 'test-resources', 'chrome-wrappers');
await fs.mkdir(wrapperDir, { recursive: true });
const chromeNames = ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium'];
for (const name of chromeNames) {
const nameWrapperPath = path.join(wrapperDir, name);
createOrOverwriteFile(nameWrapperPath, wrapperScript);
await execAsync(`chmod +x "${nameWrapperPath}"`);
log(`Created ${name} wrapper at: ${nameWrapperPath}`);
}
// Prepend our wrapper directory to PATH so our wrappers are found first
process.env.PATH = `${wrapperDir}:${process.env.PATH}`;
log(`Prepended ${wrapperDir} to PATH`);
log(`Created Chrome wrapper at: ${wrapperPath}`);
log(`Set CHROME_BIN to: ${wrapperPath}`);
} catch (error) {
log(`Warning: Could not create Chrome wrapper: ${error}`);
}
}
public async setup(): Promise<void> {
// Log the test environment configuration
this.logTestEnvironment();
// Set Ubuntu-specific Chrome driver arguments if needed
await this.setupUbuntuChromeArgs();
// Kill any existing Chrome processes on Linux
if (process.platform === 'linux') {
await this.killExistingChromeProcesses();
}
await this.downloadCode(this.testConfig.codeVersion);
await this.downloadChromeDriver(this.testConfig.codeVersion);
// Create Chrome wrapper script for Ubuntu
if (process.platform === 'linux') {
await this.createChromeWrapper();
}
// Kill any existing VS Code processes on Windows before installing extensions
if (process.platform === 'win32') {
await this.killExistingVSCodeProcesses();
await this.cleanupWindowsExtensionTempFiles();
}
try {
await this.installExtensions();
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Warning: Failed to install extensions: ${errorMessage}. Continuing setup.`);
}
await this.setupAndAuthorizeOrg();
}
public async runTests(): Promise<number> {
const useExistingProject = EnvironmentSettings.getInstance().useExistingProject;
const resources = useExistingProject ? [useExistingProject] : [];
log(`starting runTests with useExistingProject: ${useExistingProject}`);
log(`starting runTests with resources: ${resources}`);
log(`starting runTests with spec: ${this.spec}`);
// Set Ubuntu Chrome arguments just before running tests
const isLinux = process.platform === 'linux';
if (isLinux) {
await this.setupUbuntuChromeArgs();
await this.killExistingChromeProcesses();
// Use Ubuntu-specific test runner
log('Using Ubuntu-specific test runner...');
return await this.runTestsUbuntu();
}
// Try to pass additional launch arguments for Ubuntu
const runOptions: { resources: string[]; launchArgs?: string[] } = { resources };
if (isLinux) {
runOptions.launchArgs = [
'--disable-web-security',
'--disable-features=VizDisplayCompositor',
'--no-sandbox',
'--disable-dev-shm-usage'
];
}
const exitCode = await super.runTests(this.spec || EnvironmentSettings.getInstance().specFiles, runOptions);
await this.validateTestExecution(exitCode);
return exitCode;
}
public async installExtension(extension: string): Promise<void> {
log(`SetUp - Started Install extension ${path.basename(extension)}`);
await this.installVsix({ useYarn: false, vsixFile: extension });
}
/**
* Installs a single VSIX file with platform-specific handling
* @param vsixPath - Path to the VSIX file to install
*/
private async installSingleVsix(vsixPath: string): Promise<void> {
let resolvedVsixPath = path.resolve(vsixPath);
// Avoid Windows paths being parsed as URLs by the tester
if (process.platform === 'win32') {
resolvedVsixPath = resolvedVsixPath.replace(/\\/g, '/');
if (/^[a-zA-Z]:\//.test(resolvedVsixPath)) {
resolvedVsixPath = '/' + resolvedVsixPath; // Prevent `new URL()` from treating it as a URL
}
}
// Use retry logic for Windows to handle file locking issues
if (process.platform === 'win32') {
await retryOperation(
async () => {
await new Promise(resolve => setTimeout(resolve, TestSetupAndRunner.RETRY_DELAY));
await this.installExtension(resolvedVsixPath);
},
TestSetupAndRunner.MAX_RETRIES,
`Failed to install extension ${path.basename(resolvedVsixPath)} after retries due to Windows file locking issues`
);
} else {
await this.installExtension(resolvedVsixPath);
}
}
/**
* Installs extensions from VSIX files
* @param excludeExtensions - Array of extension IDs to exclude from installation
* @param extensionConfigs - Optional array of extension configurations for validation and control
*/
public async installExtensions(
excludeExtensions: string[] = [],
extensionConfigs?: ExtensionConfig[]
): Promise<void> {
const vsixToInstallDir = this.testConfig.vsixToInstallDir || EnvironmentSettings.getInstance().vsixToInstallDir;
log(`Installing extension from vsixToInstallDir: ${vsixToInstallDir}`);
if (!vsixToInstallDir) {
log(`No VSIX_TO_INSTALL directory specified, the tests will run without installing any extensions`);
return;
}
// Check if the directory exists
try {
await fs.access(vsixToInstallDir);
} catch (error) {
throw new Error(`VSIX_TO_INSTALL directory does not exist or is not accessible: ${vsixToInstallDir}`);
}
// If extension configs are provided, check for already installed extensions
if (extensionConfigs && extensionConfigs.length > 0) {
const extensionPattern = /^(?<publisher>.+?)\.(?<extensionId>.+?)-(?<version>\d+\.\d+\.\d+)(?:\.\d+)*$/;
const extensionsDirEntries = (await fs.readdir(vsixToInstallDir)).map(entry =>
path.resolve(normalizePath(path.join(vsixToInstallDir, entry)))
);
const foundInstalledExtensions = await Promise.all(
extensionsDirEntries
.filter(async entry => {
try {
const stats = await fs.stat(entry);
return stats.isDirectory();
} catch (e) {
log(`stat failed for file ${entry}`);
return false;
}
})
.map(entry => {
const match = path.basename(entry).match(extensionPattern);
if (match?.groups) {
return {
publisher: match.groups.publisher,
extensionId: match.groups.extensionId,
version: match.groups.version,
path: entry
};
}
return null;
})
.filter(Boolean)
.filter(ext =>
extensionConfigs.find(refExt => {
return refExt.extensionId === ext?.extensionId;
})
)
);
if (
foundInstalledExtensions.length > 0 &&
foundInstalledExtensions.every(ext => extensionConfigs.find(refExt => refExt.extensionId === ext?.extensionId))
) {
log(`Found the following pre-installed extensions in dir ${vsixToInstallDir}, skipping installation of vsix`);
foundInstalledExtensions.forEach(ext => {
log(`Extension ${ext?.extensionId} version ${ext?.version}`);
});
return;
}
}
const extensionsVsixs = getVsixFilesFromDir(vsixToInstallDir);
if (extensionsVsixs.length === 0) {
log(`No vsix files were found in dir ${vsixToInstallDir}, skipping extension installation`);
return;
}
log(`VSIX files count: ${extensionsVsixs.length} found in dir ${vsixToInstallDir}`);
// Build the merged exclude list
const mergeExcluded = Array.from(
new Set([
...excludeExtensions,
...(extensionConfigs?.filter(ext => ext.shouldInstall === 'never').map(ext => ext.extensionId) || [])
])
);
// If extension configs are provided, use them for validation and filtering
if (extensionConfigs && extensionConfigs.length > 0) {
// Create a mutable copy of extension configs to update with vsixPath
const extensionConfigsMap = new Map(extensionConfigs.map(ext => [ext.extensionId, { ...ext }]));
// Match VSIX files against extension configs and update vsixPath
extensionsVsixs.forEach(vsix => {
const match = path.basename(vsix).match(/^(?<extension>.*?)(-(?<version>\d+\.\d+\.\d+))?\.vsix$/);
if (match?.groups) {
const { extension, version } = match.groups;
const foundExtension = extensionConfigsMap.get(extension);
if (foundExtension) {
foundExtension.vsixPath = vsix;
// Assign 'never' to this extension if its id is included in excludedExtensions
foundExtension.shouldInstall = mergeExcluded.includes(foundExtension.extensionId)
? 'never'
: foundExtension.shouldInstall || 'always';
// If not installing, don't verify, otherwise use default value
foundExtension.shouldVerifyActivation =
foundExtension.shouldInstall === 'never' ? false : foundExtension.shouldVerifyActivation;
log(`SetUp - Found extension ${extension} version ${version} with vsixPath ${foundExtension.vsixPath}`);
} else {
log(`SetUp - Extension ${extension} version ${version} not in configured extensions list`);
}
}
});
// Install extensions that have vsixPath and shouldInstall !== 'never'
for (const extensionConfig of extensionConfigsMap.values()) {
if (extensionConfig.vsixPath && extensionConfig.shouldInstall !== 'never') {
await this.installSingleVsix(extensionConfig.vsixPath);
}
}
} else {
// No extension configs provided - install all VSIX files except excluded ones
for (const vsixPath of extensionsVsixs) {
// Extract extension ID from VSIX filename
const match = path.basename(vsixPath).match(/^(?<extension>.*?)(-(?<version>\d+\.\d+\.\d+))?\.vsix$/);
if (match?.groups) {
const { extension, version } = match.groups;
// Skip if this extension is in the exclude list
if (mergeExcluded.includes(extension)) {
log(`SetUp - Skipping excluded extension ${extension} version ${version}`);
continue;
}
log(`SetUp - Installing extension ${extension} version ${version}`);
}
await this.installSingleVsix(vsixPath);
}
}
}
public async setupAndAuthorizeOrg() {
const environmentSettings = EnvironmentSettings.getInstance();
const devHubUserName = environmentSettings.devHubUserName;
const devHubAliasName = environmentSettings.devHubAliasName;
const SFDX_AUTH_URL = environmentSettings.sfdxAuthUrl;
try {
// First try to verify if the org with alias and username exists
log('Checking if org with matching alias and username is already authenticated...');
await verifyAliasAndUserName();
log(`Org with alias ${devHubAliasName} and username ${devHubUserName} is already authenticated`);
return;
} catch (error) {
// If verification fails, continue with SFDX_AUTH_URL
const errorMessage = error instanceof Error ? error.message : String(error);
log(`No existing authenticated org found: ${errorMessage}`);
// If no SFDX_AUTH_URL is provided, rethrow the error
if (!SFDX_AUTH_URL) {
throw new Error('No SFDX_AUTH_URL provided and no existing authentication found. Unable to authorize org.');
}
}
// The verifyAliasAndUserName function already checks that both alias and username are set,
// so at this point we know they are defined (or we would have thrown earlier).
// TypeScript doesn't know this though, so we'll add the non-null assertion
if (!devHubUserName || !devHubAliasName) {
throw new Error(
'DevHub username or alias name is not set. This should not happen as verifyAliasAndUserName should have caught this.'
);
}
// At this point, neither alias nor username matched an existing org,
// and we have an SFDX_AUTH_URL to use
log('Authenticating using SFDX_AUTH_URL from environment variable...');
// Step 1: Authorize to Testing Org
const authorizeOrg = await orgLoginSfdxUrl();
expect(authorizeOrg.stdout).to.contain(`Successfully authorized ${devHubUserName}`);
log(`Successfully authorized ${devHubUserName}`);
// Step 2: Set Alias for the Org
const setAliasResult = await setAlias(devHubAliasName, devHubUserName);
expect(setAliasResult.stdout).to.contain(devHubAliasName);
expect(setAliasResult.stdout).to.contain(devHubUserName);
expect(setAliasResult.stdout).to.contain('true');
log(`Successfully setAliasResult ${setAliasResult.stdout}`);
}
async downloadCode(version = 'latest'): Promise<void> {
await super.downloadCode(version);
}
static get exTester(): TestSetupAndRunner {
if (TestSetupAndRunner.exTester) {
return TestSetupAndRunner._exTester;
}
TestSetupAndRunner._exTester = new TestSetupAndRunner();
return TestSetupAndRunner._exTester;
}
private async setupVirtualDisplay(): Promise<void> {
if (process.platform !== 'linux') {
return;
}
try {
console.log('Setting up virtual display for Ubuntu...');
// Start Xvfb virtual display
await this.execShellCommand('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &').catch(() => {
console.log('Xvfb might already be running or not available');
});
// Wait for display to be ready
await new Promise(resolve => setTimeout(resolve, 2000));
// Verify display is working
await this.execShellCommand('export DISPLAY=:99 && xdpyinfo > /dev/null 2>&1').catch(() => {
console.log('Virtual display verification failed, continuing anyway');
});
console.log('Virtual display setup completed');
} catch (error) {
console.warn('Failed to setup virtual display:', error);
}
}
private async execShellCommand(command: string): Promise<void> {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
await execAsync(command);
}
private async runTestsUbuntu(): Promise<number> {
// Setup virtual display for Ubuntu
await this.setupVirtualDisplay();
console.log('Running tests...');
// Kill any existing Chrome processes before running
if (process.platform === 'linux') {
await this.killChromeProcesses();
}
// Create a unique user data directory
const uniqueUserDataDir = `/tmp/vscode-chrome-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
// Update environment variables with unique directory
process.env.CHROME_USER_DATA_DIR = uniqueUserDataDir;
// Call the existing runTests method
const useExistingProject = EnvironmentSettings.getInstance().useExistingProject;
const resources = useExistingProject ? [useExistingProject] : [];
const exitCode = await super.runTests(this.spec || EnvironmentSettings.getInstance().specFiles, { resources });
await this.validateTestExecution(exitCode);
if (exitCode === 0) {
console.log('Tests completed successfully!');
}
return exitCode;
}
/**
* Validates that tests were actually executed by checking for test output
* Throws an error if no tests were found or executed
*/
private async validateTestExecution(exitCode: number): Promise<void> {
if (exitCode !== 0) {
return; // If tests failed, don't check for test count (tests were found but failed)
}
// Check if any spec files were provided
const specFiles = this.spec || EnvironmentSettings.getInstance().specFiles;
if (!specFiles || (Array.isArray(specFiles) && specFiles.length === 0)) {
throw new Error('No E2E test spec files were provided. Please specify test files to run.');
}
// Check if spec files actually exist
const specsToCheck = Array.isArray(specFiles) ? specFiles : [specFiles];
let foundTestFiles = false;
for (const spec of specsToCheck) {
try {
// Check if the spec file/pattern matches any files
if (spec.includes('*') || spec.includes('?')) {
// It's a glob pattern - we can't easily check without implementing glob matching
// For now, assume it's valid if it contains wildcards
log(`Spec pattern provided: ${spec}`);
foundTestFiles = true;
} else {
// It's a specific file path
await fs.access(spec);
foundTestFiles = true;
log(`Found test file: ${spec}`);
}
} catch (error) {
log(`Test file not found: ${spec}`);
}
}
if (!foundTestFiles && exitCode === 0) {
throw new Error(
`No E2E test files were found for the provided spec: ${JSON.stringify(specFiles)}. This likely indicates a configuration issue - tests should not pass when no test files exist.`
);
}
log('Test execution validation completed - test files were found and executed');
}
private async killChromeProcesses(): Promise<void> {
try {
console.log('Killing existing Chrome processes...');
const commands = [
'pkill -f "chrome.*--test-type"',
'pkill -f "chrome.*--remote-debugging-port"',
'pkill -f "chrome.*--user-data-dir"',
'pkill -f "chrome.*--disable-dev-shm-usage"',
'pkill -f chromium',
'pkill -f google-chrome',
'pkill -f chrome',
'pkill -f "Code.*--extensionDevelopmentPath"',
'pkill -f "code.*--extensionDevelopmentPath"'
];
for (const cmd of commands) {
try {
await this.execShellCommand(cmd);
} catch (error) {
// Ignore errors - processes might not exist
}
}
// Clean up any leftover user data directories and lock files
await this.execShellCommand(
'rm -rf /tmp/chrome-user-data-* /tmp/vscode-chrome-* ~/.config/Code/SingletonLock ~/.vscode/extensions/*/node_modules/.bin/chromedriver'
).catch(() => {
console.log('Failed to clean up leftover files, continuing anyway');
});
// Wait a moment for processes to fully terminate
await new Promise(resolve => setTimeout(resolve, 3000));
} catch (error) {
console.warn('Failed to kill Chrome processes:', error);
}
}
}
// Parse command-line arguments
const argv = yargs(hideBin(process.argv))
.option('spec', {
alias: 's',
type: 'string',
description: 'Glob pattern for test files or list of test files',
demandOption: false,
array: true
})
.option('workspace-path', {
alias: 'w',
type: 'string',
description: 'Path to workspace directory',
demandOption: false
})
.option('vscode-version', {
alias: 'v',
type: 'string',
description: 'VS Code version to use (e.g., latest, stable, 1.85.0)',
demandOption: false
})
.help().argv as {
spec: string | string[] | undefined;
workspacePath?: string;
vscodeVersion?: string;
};
// Create test config from command line arguments
const testConfig: Partial<TestConfig> = {};
if (argv.workspacePath) {
testConfig.workspacePath = argv.workspacePath;
testConfig.extensionsPath = path.join(argv.workspacePath, 'extensions');
}
if (argv.vscodeVersion) {
testConfig.codeVersion = argv.vscodeVersion;
}
if (argv.spec) {
log(`Spec passed in: ${argv.spec}`);
}
const testSetupAndRunner = new TestSetupAndRunner(testConfig, argv.spec);
async function run() {
try {
await testSetupAndRunner.setup();
const result = await testSetupAndRunner.runTests();
console.log(result);
process.exit(result);
} catch (error) {
console.error(error);
process.exit(1);
}
}
void run();