Implement more sanity tests (#286914)

This commit is contained in:
Dmitriy Vasyura
2026-01-10 17:05:21 +01:00
committed by GitHub
parent 7eb5eb5d99
commit 4c0056e0ff
6 changed files with 456 additions and 125 deletions
+12 -20
View File
@@ -92,47 +92,39 @@ export function setup(context: TestContext) {
process.chdir(workspaceDir);
context.log(`Changed current directory to: ${workspaceDir}`);
const cliDataDir = context.createTempDir();
const userDataDir = context.createTempDir();
const serverDataDir = context.createTempDir();
const extensionsDir = context.createTempDir();
const args = [
'--cli-data-dir', cliDataDir,
'--user-data-dir', userDataDir,
'--cli-data-dir', context.createTempDir(),
'--user-data-dir', context.createTempDir(),
'tunnel',
'--accept-server-license-terms',
'--server-data-dir', serverDataDir,
'--extensions-dir', extensionsDir,
'--server-data-dir', context.createTempDir(),
'--extensions-dir', context.createTempDir(),
];
context.log(`Running CLI ${entryPoint} with args: ${args.join(' ')}`);
const cli = spawn(entryPoint, args);
context.log(`Running CLI ${entryPoint} with args ${args.join(' ')}`);
const cli = spawn(entryPoint, args, { detached: true });
cli.stderr.on('data', (data) => {
context.error(`[CLI Error] ${data.toString().trim()}`);
});
let tunnelUrl: string | undefined = undefined;
cli.stdout.on('data', (data) => {
const text = data.toString();
text.trim().split('\n').forEach((line: string) => {
const text = data.toString().trim();
text.split('\n').forEach((line: string) => {
context.log(`[CLI Output] ${line}`);
});
const match = /Open this link in your browser (https:\/\/.+)/.exec(text);
const match = /Using GitHub for authentication/.exec(text);
if (match !== null) {
tunnelUrl = context.getTunnelUrl(match[1]);
context.log(`Tunnel URL: ${tunnelUrl}`);
cli.kill();
context.log(`CLI started successfully and is waiting for authentication`);
context.killProcessTree(cli.pid!);
}
});
await new Promise<void>((resolve, reject) => {
cli.on('error', reject);
cli.on('exit', () => resolve());
cli.on('exit', resolve);
});
assert.ok(tunnelUrl, 'Expected to receive a tunnel URL from the CLI');
}
});
}
+109 -9
View File
@@ -9,6 +9,7 @@ import fs from 'fs';
import fetch, { Response } from 'node-fetch';
import os from 'os';
import path from 'path';
import { Browser, chromium } from 'playwright';
/**
* Response from https://update.code.visualstudio.com/api/versions/commit:<commit>/<target>/<quality>
@@ -282,6 +283,43 @@ export class TestContext {
return result;
}
/**
* Kills a process and all its child processes.
* @param pid The process ID to kill.
*/
public killProcessTree(pid: number): void {
this.log(`Killing process tree for PID: ${pid}`);
if (os.platform() === 'win32') {
spawnSync('taskkill', ['/T', '/F', '/PID', pid.toString()]);
} else {
process.kill(-pid, 'SIGKILL');
}
this.log(`Killed process tree for PID: ${pid}`);
}
/**
* Returns the Windows installation directory for VS Code based on the installation type and quality.
* @param type The type of installation ('user' or 'system').
* @returns The path to the VS Code installation directory.
*/
private getWindowsInstallDir(type: 'user' | 'system'): string {
let parentDir: string;
if (type === 'system') {
parentDir = process.env['PROGRAMFILES'] || '';
} else {
parentDir = path.join(process.env['LOCALAPPDATA'] || '', 'Programs');
}
switch (this.quality) {
case 'stable':
return path.join(parentDir, 'Microsoft VS Code');
case 'insider':
return path.join(parentDir, 'Microsoft VS Code Insiders');
case 'exploration':
return path.join(parentDir, 'Microsoft VS Code Exploration');
}
}
/**
* Installs a Microsoft Installer package silently.
* @param installerPath The path to the installer executable.
@@ -292,22 +330,17 @@ export class TestContext {
this.runNoErrors(installerPath, '/silent', '/mergetasks=!runcode');
this.log(`Installed ${installerPath} successfully`);
const varName = type === 'system' ? 'PROGRAMFILES' : 'LOCALAPPDATA';
const parentDir = process.env[varName];
if (parentDir === undefined) {
this.error(`Environment variable ${varName} is not defined`);
}
const appDir = this.getWindowsInstallDir(type);
let entryPoint: string;
switch (this.quality) {
case 'stable':
entryPoint = path.join(parentDir, 'Microsoft VS Code', 'Code.exe');
entryPoint = path.join(appDir, 'Code.exe');
break;
case 'insider':
entryPoint = path.join(parentDir, 'Microsoft VS Code Insiders', 'Code - Insiders.exe');
entryPoint = path.join(appDir, 'Code - Insiders.exe');
break;
case 'exploration':
entryPoint = path.join(parentDir, 'Microsoft VS Code Exploration', 'Code - Exploration.exe');
entryPoint = path.join(appDir, 'Code - Exploration.exe');
break;
}
@@ -319,6 +352,27 @@ export class TestContext {
return entryPoint;
}
/**
* Uninstalls a Windows application silently.
* @param type The type of installation ('user' or 'system').
*/
public async uninstallWindowsApp(type: 'user' | 'system'): Promise<void> {
const appDir = this.getWindowsInstallDir(type);
const uninstallerPath = path.join(appDir, 'unins000.exe');
if (!fs.existsSync(uninstallerPath)) {
this.error(`Uninstaller does not exist: ${uninstallerPath}`);
}
this.log(`Uninstalling VS Code from ${appDir} in silent mode`);
this.runNoErrors(uninstallerPath, '/silent');
this.log(`Uninstalled VS Code from ${appDir} successfully`);
await new Promise(resolve => setTimeout(resolve, 2000));
if (fs.existsSync(appDir)) {
this.error(`Installation directory still exists after uninstall: ${appDir}`);
}
}
/**
* Prepares a macOS .app bundle for execution by removing the quarantine attribute.
* @param bundleDir The directory containing the .app bundle.
@@ -426,6 +480,42 @@ export class TestContext {
return filePath;
}
/**
* Returns the entry point executable for the VS Code server in the specified directory.
* @param dir The directory containing unpacked server files.
* @returns The path to the server entry point executable.
*/
public getServerEntryPoint(dir: string): string {
const serverDir = fs.readdirSync(dir, { withFileTypes: true }).filter(o => o.isDirectory()).at(0)?.name;
if (!serverDir) {
this.error(`No subdirectories found in server directory: ${dir}`);
}
let filename: string;
switch (this.quality) {
case 'stable':
filename = 'code-server';
break;
case 'insider':
filename = 'code-server-insiders';
break;
case 'exploration':
filename = 'code-server-exploration';
break;
}
if (os.platform() === 'win32') {
filename += '.cmd';
}
const entryPoint = path.join(dir, serverDir, 'bin', filename);
if (!fs.existsSync(entryPoint)) {
this.error(`Server entry point does not exist: ${entryPoint}`);
}
return entryPoint;
}
/**
* Returns the tunnel URL for the VS Code server including vscode-version parameter.
* @param baseUrl The base URL for the VS Code server.
@@ -436,4 +526,14 @@ export class TestContext {
url.searchParams.set('vscode-version', this.commit);
return url.toString();
}
/**
* Launches a web browser for UI testing.
* @returns The launched Browser instance.
*/
public async launchBrowser(): Promise<Browser> {
this.log(`Launching web browser`);
const channel = os.platform() === 'win32' ? 'msedge' : 'chrome';
return await chromium.launch({ channel, headless: false });
}
}
+36 -73
View File
@@ -3,16 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { _electron } from 'playwright';
import { TestContext } from './context';
import { UITest } from './uiTest';
export function setup(context: TestContext) {
describe('Desktop', () => {
if (context.platform === 'darwin-x64') {
it('darwin', async () => {
it('desktop-darwin', async () => {
const dir = await context.downloadAndUnpack('darwin');
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint);
@@ -20,7 +19,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'darwin-arm64') {
it('darwin-arm64', async () => {
it('desktop-darwin-arm64', async () => {
const dir = await context.downloadAndUnpack('darwin-arm64');
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint);
@@ -28,7 +27,7 @@ export function setup(context: TestContext) {
}
if (context.platform.startsWith('darwin-')) {
it('darwin-universal', async () => {
it('desktop-darwin-universal', async () => {
const dir = await context.downloadAndUnpack('darwin-universal');
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint);
@@ -36,7 +35,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-arm64') {
it('linux-arm64', async () => {
it('desktop-linux-arm64', async () => {
const dir = await context.downloadAndUnpack('linux-arm64');
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
@@ -44,7 +43,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-arm') {
it('linux-armhf', async () => {
it('desktop-linux-armhf', async () => {
const dir = await context.downloadAndUnpack('linux-armhf');
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
@@ -52,7 +51,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-arm64') {
it('linux-deb-arm64', async () => {
it('desktop-linux-deb-arm64', async () => {
const packagePath = await context.downloadTarget('linux-deb-arm64');
const entryPoint = context.installDeb(packagePath);
await testDesktopApp(entryPoint);
@@ -60,7 +59,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-arm') {
it('linux-deb-armhf', async () => {
it('desktop-linux-deb-armhf', async () => {
const packagePath = await context.downloadTarget('linux-deb-armhf');
const entryPoint = context.installDeb(packagePath);
await testDesktopApp(entryPoint);
@@ -68,7 +67,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-x64') {
it('linux-deb-x64', async () => {
it('desktop-linux-deb-x64', async () => {
const packagePath = await context.downloadTarget('linux-deb-x64');
const entryPoint = context.installDeb(packagePath);
await testDesktopApp(entryPoint);
@@ -76,7 +75,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-arm64') {
it('linux-rpm-arm64', async () => {
it('desktop-linux-rpm-arm64', async () => {
const packagePath = await context.downloadTarget('linux-rpm-arm64');
const entryPoint = context.installRpm(packagePath);
await testDesktopApp(entryPoint);
@@ -84,7 +83,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-arm') {
it('linux-rpm-armhf', async () => {
it('desktop-linux-rpm-armhf', async () => {
const packagePath = await context.downloadTarget('linux-rpm-armhf');
const entryPoint = context.installRpm(packagePath);
await testDesktopApp(entryPoint);
@@ -92,7 +91,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-x64') {
it('linux-rpm-x64', async () => {
it('desktop-linux-rpm-x64', async () => {
const packagePath = await context.downloadTarget('linux-rpm-x64');
const entryPoint = context.installRpm(packagePath);
await testDesktopApp(entryPoint);
@@ -100,7 +99,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-x64') {
it('linux-snap-x64', async () => {
it('desktop-linux-snap-x64', async () => {
const packagePath = await context.downloadTarget('linux-snap-x64');
const entryPoint = context.installSnap(packagePath);
await testDesktopApp(entryPoint);
@@ -108,7 +107,7 @@ export function setup(context: TestContext) {
}
if (context.platform === 'linux-x64') {
it('linux-x64', async () => {
it('desktop-linux-x64', async () => {
const dir = await context.downloadAndUnpack('linux-x64');
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
@@ -116,17 +115,18 @@ export function setup(context: TestContext) {
}
if (context.platform === 'win32-arm64') {
it('win32-arm64', async () => {
it('desktop-win32-arm64', async () => {
const packagePath = await context.downloadTarget('win32-arm64');
context.validateSignature(packagePath);
const entryPoint = context.installWindowsApp('system', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
await context.uninstallWindowsApp('system');
});
}
if (context.platform === 'win32-arm64') {
it('win32-arm64-archive', async () => {
it('desktop-win32-arm64-archive', async () => {
const dir = await context.downloadAndUnpack('win32-arm64-archive');
context.validateAllSignatures(dir);
const entryPoint = context.getEntryPoint('desktop', dir);
@@ -135,27 +135,29 @@ export function setup(context: TestContext) {
}
if (context.platform === 'win32-arm64') {
it('win32-arm64-user', async () => {
it('desktop-win32-arm64-user', async () => {
const packagePath = await context.downloadTarget('win32-arm64-user');
context.validateSignature(packagePath);
const entryPoint = context.installWindowsApp('user', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
await context.uninstallWindowsApp('user');
});
}
if (context.platform === 'win32-x64') {
it('win32-x64', async () => {
it('desktop-win32-x64', async () => {
const packagePath = await context.downloadTarget('win32-x64');
context.validateSignature(packagePath);
const entryPoint = context.installWindowsApp('system', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
await context.uninstallWindowsApp('system');
});
}
if (context.platform === 'win32-x64') {
it('win32-x64-archive', async () => {
it('desktop-win32-x64-archive', async () => {
const dir = await context.downloadAndUnpack('win32-x64-archive');
context.validateAllSignatures(dir);
const entryPoint = context.getEntryPoint('desktop', dir);
@@ -164,73 +166,34 @@ export function setup(context: TestContext) {
}
if (context.platform === 'win32-x64') {
it('win32-x64-user', async () => {
it('desktop-win32-x64-user', async () => {
const packagePath = await context.downloadTarget('win32-x64-user');
context.validateSignature(packagePath);
const entryPoint = context.installWindowsApp('user', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
await context.uninstallWindowsApp('user');
});
}
async function testDesktopApp(executablePath: string) {
const extensionsDir = context.createTempDir();
const dataDir = context.createTempDir();
const workspaceDir = context.createTempDir();
const args = ['--extensions-dir', extensionsDir, '--user-data-dir', dataDir, workspaceDir];
async function testDesktopApp(entryPoint: string) {
const test = new UITest(context);
const args = [
'--extensions-dir', test.extensionsDir,
'--user-data-dir', test.userDataDir,
test.workspaceDir
];
context.log(`Start VS Code: ${executablePath} with args: ${args.join(' ')}`);
const app = await _electron.launch({ executablePath, args });
context.log(`Starting VS Code ${entryPoint} with args ${args.join(' ')}`);
const app = await _electron.launch({ executablePath: entryPoint, args });
const window = await app.firstWindow();
context.log('Dismiss workspace trust dialog');
await window.getByText('Yes, I trust the authors').click();
await test.run(window);
context.log('Focus Explorer view');
await window.keyboard.press('Control+Shift+E');
context.log('Click New File button');
await window.getByLabel('New File...').click();
context.log('Type file name');
await window.locator('input').fill('helloWorld.txt');
await window.keyboard.press('Enter');
context.log('Focus the code editor');
await window.getByText(/Start typing/).focus();
context.log('Type some content into the file');
await window.keyboard.type('Hello, World!');
context.log('Save the file');
await window.keyboard.press('Control+S');
context.log('Open Extensions view');
await window.keyboard.press('Control+Shift+X');
await window.waitForSelector('.extension-list-item');
context.log('Type extension name to search for');
await window.keyboard.type('GitHub Pull Requests');
await new Promise(resolve => setTimeout(resolve, 2000));
context.log('Click Install on the first extension in the list');
await window.locator('.extension-action:not(.disabled)', { hasText: /Install/ }).first().click();
context.log('Wait for extension to be installed');
await window.locator('.extension-action:not(.disabled)', { hasText: /Uninstall/ }).waitFor();
context.log('Close the application');
context.log('Closing the application');
await app.close();
context.log('Verify file contents');
const filePath = `${workspaceDir}/helloWorld.txt`;
const fileContents = fs.readFileSync(filePath, 'utf-8');
assert.strictEqual(fileContents, 'Hello, World!');
context.log('Verify extension is installed');
const extensions = fs.readdirSync(extensionsDir);
const hasExtension = extensions.some(ext => ext.startsWith('github.vscode-pull-request-github'));
assert.strictEqual(hasExtension, true);
test.validate();
}
});
}
+67 -7
View File
@@ -3,49 +3,65 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { spawn } from 'child_process';
import { TestContext } from './context';
export function setup(context: TestContext) {
describe('Server', () => {
if (context.platform === 'linux-arm64') {
it('server-alpine-arm64', async () => {
await context.downloadAndUnpack('server-alpine-arm64');
const dir = await context.downloadAndUnpack('server-alpine-arm64');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('server-alpine-x64', async () => {
await context.downloadAndUnpack('server-linux-alpine');
const dir = await context.downloadAndUnpack('server-linux-alpine');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'darwin-arm64') {
it('server-darwin-arm64', async () => {
await context.downloadAndUnpack('server-darwin-arm64');
const dir = await context.downloadAndUnpack('server-darwin-arm64');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'darwin-x64') {
it('server-darwin-x64', async () => {
await context.downloadAndUnpack('server-darwin');
const dir = await context.downloadAndUnpack('server-darwin');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'linux-arm64') {
it('server-linux-arm64', async () => {
await context.downloadAndUnpack('server-linux-arm64');
const dir = await context.downloadAndUnpack('server-linux-arm64');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'linux-arm') {
it('server-linux-armhf', async () => {
await context.downloadAndUnpack('server-linux-armhf');
const dir = await context.downloadAndUnpack('server-linux-armhf');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('server-linux-x64', async () => {
await context.downloadAndUnpack('server-linux-x64');
const dir = await context.downloadAndUnpack('server-linux-x64');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
@@ -53,6 +69,8 @@ export function setup(context: TestContext) {
it('server-win32-arm64', async () => {
const dir = await context.downloadAndUnpack('server-win32-arm64');
context.validateAllSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
@@ -60,6 +78,48 @@ export function setup(context: TestContext) {
it('server-win32-x64', async () => {
const dir = await context.downloadAndUnpack('server-win32-x64');
context.validateAllSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
async function testServer(entryPoint: string) {
const args = ['--accept-server-license-terms', '--connection-token', '12345'];
context.log(`Starting server ${entryPoint} with args ${args.join(' ')}`);
const server = spawn(entryPoint, args, { shell: true });
server.stderr.on('data', (data) => {
context.error(`[Server Error] ${data.toString().trim()}`);
});
server.stdout.on('data', (data) => {
const text = data.toString().trim();
text.split('\n').forEach((line: string) => {
context.log(`[Server Output] ${line}`);
});
const port = /Extension host agent listening on (\d+)/.exec(text)?.[1];
if (port) {
(async function () {
try {
const url = `http://localhost:${port}/version`;
context.log(`Fetching ${url}`);
const response = await fetch(url);
assert.equal(response.status, 200);
assert.equal(await response.text(), context.commit);
} catch (error) {
assert.fail(error instanceof Error ? error.message : String(error));
} finally {
context.killProcessTree(server.pid!);
}
})();
}
});
await new Promise<void>((resolve, reject) => {
server.on('error', reject);
server.on('exit', resolve);
});
}
});
+100 -16
View File
@@ -3,63 +3,147 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { spawn } from 'child_process';
import path from 'path';
import { TestContext } from './context';
import { UITest } from './uiTest';
export function setup(context: TestContext) {
describe('Server Web', () => {
if (context.platform === 'linux-arm64') {
it('server-alpine-arm64-web', async () => {
await context.downloadAndUnpack('server-alpine-arm64-web');
it('server-web-alpine-arm64', async () => {
const dir = await context.downloadAndUnpack('server-alpine-arm64-web');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('server-alpine-x64-web', async () => {
await context.downloadAndUnpack('server-linux-alpine-web');
it('server-web-alpine-x64', async () => {
const dir = await context.downloadAndUnpack('server-linux-alpine-web');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'darwin-arm64') {
it('server-darwin-arm64-web', async () => {
await context.downloadAndUnpack('server-darwin-arm64-web');
it('server-web-darwin-arm64', async () => {
const dir = await context.downloadAndUnpack('server-darwin-arm64-web');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'darwin-x64') {
it('server-darwin-x64-web', async () => {
await context.downloadAndUnpack('server-darwin-web');
it('server-web-darwin-x64', async () => {
const dir = await context.downloadAndUnpack('server-darwin-web');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'linux-arm64') {
it('server-linux-arm64-web', async () => {
await context.downloadAndUnpack('server-linux-arm64-web');
it('server-web-linux-arm64', async () => {
const dir = await context.downloadAndUnpack('server-linux-arm64-web');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'linux-arm') {
it('server-linux-armhf-web', async () => {
await context.downloadAndUnpack('server-linux-armhf-web');
it('server-web-linux-armhf', async () => {
const dir = await context.downloadAndUnpack('server-linux-armhf-web');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('server-linux-x64-web', async () => {
await context.downloadAndUnpack('server-linux-x64-web');
it('server-web-linux-x64', async () => {
const dir = await context.downloadAndUnpack('server-linux-x64-web');
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'win32-arm64') {
it('server-win32-arm64-web', async () => {
it('server-web-win32-arm64', async () => {
const dir = await context.downloadAndUnpack('server-win32-arm64-web');
context.validateAllSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
if (context.platform === 'win32-x64') {
it('server-win32-x64-web', async () => {
it('server-web-win32-x64', async () => {
const dir = await context.downloadAndUnpack('server-win32-x64-web');
context.validateAllSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
}
async function testServer(entryPoint: string) {
const token = '12345';
const test = new UITest(context);
const args = [
'--accept-server-license-terms',
'--connection-token', token,
'--server-data-dir', context.createTempDir(),
'--extensions-dir', test.extensionsDir,
'--user-data-dir', test.userDataDir
];
context.log(`Starting server ${entryPoint} with args ${args.join(' ')}`);
const server = spawn(entryPoint, args, { shell: true });
server.stderr.on('data', (data) => {
context.error(`[Server Error] ${data.toString().trim()}`);
});
server.stdout.on('data', (data) => {
const text = data.toString().trim();
text.split('\n').forEach((line: string) => {
context.log(`[Server Output] ${line}`);
});
const port = /Extension host agent listening on (\d+)/.exec(text)?.[1];
if (port) {
(async function () {
try {
const browser = await context.launchBrowser();
const page = await browser.newPage();
const url = `http://localhost:${port}?tkn=${token}&folder=/${test.workspaceDir.replaceAll(path.sep, '/')}`;
context.log(`Navigating to ${url}`);
await page.goto(url, { waitUntil: 'networkidle' });
context.log('Waiting for the workbench to load');
await page.waitForSelector('.monaco-workbench');
context.log('Verifying page title contains "Visual Studio Code"');
assert.match(await page.title(), /Visual Studio Code/);
await test.run(page);
context.log('Closing browser');
await browser.close();
test.validate();
} catch (error) {
assert.fail(error instanceof Error ? error.message : String(error));
} finally {
context.killProcessTree(server.pid!);
}
})();
}
});
await new Promise<void>((resolve, reject) => {
server.on('error', reject);
server.on('exit', resolve);
});
}
});
+132
View File
@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import fs from 'fs';
import { Page } from 'playwright';
import { TestContext } from './context';
/**
* UI Test helper class to perform common UI actions and verifications.
*/
export class UITest {
private _extensionsDir: string | undefined;
private _workspaceDir: string | undefined;
private _userDataDir: string | undefined;
constructor(private readonly context: TestContext) {
}
/**
* The directory where extensions are installed.
*/
public get extensionsDir(): string {
return this._extensionsDir ??= this.context.createTempDir();
}
/**
* The workspace directory used for testing.
*/
public get workspaceDir(): string {
return this._workspaceDir ??= this.context.createTempDir();
}
/**
* The user data directory used for testing.
*/
public get userDataDir(): string {
return this._userDataDir ??= this.context.createTempDir();
}
/**
* Run the UI test actions.
*/
public async run(page: Page) {
await this.dismissWorkspaceTrustDialog(page);
await this.createTextFile(page);
await this.installExtension(page);
}
/**
* Validate the results of the UI test actions.
*/
public async validate() {
this.verifyTextFileCreated();
this.verifyExtensionInstalled();
}
/**
* Dismiss the workspace trust dialog.
*/
private async dismissWorkspaceTrustDialog(page: Page) {
this.context.log('Dismissing workspace trust dialog');
await page.getByText('Yes, I trust the authors').click();
await page.waitForTimeout(500);
}
/**
* Create a new text file in the editor with some content and save it.
*/
private async createTextFile(page: Page) {
this.context.log('Focusing Explorer view');
await page.keyboard.press('Control+Shift+E');
this.context.log('Clicking New File button');
await page.getByLabel('New File...').click();
this.context.log('Typing file name');
await page.locator('input').fill('helloWorld.txt');
await page.keyboard.press('Enter');
this.context.log('Focusing the code editor');
await page.getByText(/Start typing/).focus();
this.context.log('Typing some content into the file');
await page.keyboard.type('Hello, World!');
this.context.log('Saving the file');
await page.keyboard.press('Control+S');
await page.waitForTimeout(1000);
}
/**
* Verify that the text file was created with the expected content.
*/
private verifyTextFileCreated() {
this.context.log('Verifying file contents');
const filePath = `${this.workspaceDir}/helloWorld.txt`;
const fileContents = fs.readFileSync(filePath, 'utf-8');
assert.strictEqual(fileContents, 'Hello, World!');
}
/**
* Install GitHub Pull Requests extension from the Extensions view.
*/
private async installExtension(page: Page) {
this.context.log('Opening Extensions view');
await page.keyboard.press('Control+Shift+X');
await page.waitForSelector('.extension-list-item');
this.context.log('Typing extension name to search for');
await page.keyboard.type('GitHub Pull Requests');
await page.waitForTimeout(2000);
this.context.log('Clicking Install on the first extension in the list');
await page.locator('.extension-action:not(.disabled)', { hasText: /Install/ }).first().click();
this.context.log('Waiting for extension to be installed');
await page.locator('.extension-action:not(.disabled)', { hasText: /Uninstall/ }).waitFor();
}
/**
* Verify that the GitHub Pull Requests extension is installed.
*/
private verifyExtensionInstalled() {
this.context.log('Verifying extension is installed');
const extensions = fs.readdirSync(this.extensionsDir);
const hasExtension = extensions.some(ext => ext.startsWith('github.vscode-pull-request-github'));
assert.strictEqual(hasExtension, true);
}
}