chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:42 +08:00
commit 2d485ba600
8326 changed files with 2641263 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/mocharc",
"ui": "tdd",
"timeout": 10000
}
+9
View File
@@ -0,0 +1,9 @@
# VSCode Tests
## Contents
This folder contains the various test runners for VSCode. Please refer to the documentation within for how to run them:
* `unit`: our suite of unit tests ([README](unit/README.md))
* `integration`: our suite of API tests ([README](integration/browser/README.md))
* `smoke`: our suite of automated UI tests ([README](smoke/README.md))
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
npm-debug.log
Thumbs.db
node_modules/
out/
keybindings.*.json
src/driver.d.ts
*.tgz
+6
View File
@@ -0,0 +1,6 @@
!/out
/src
/tools
.gitignore
tsconfig.json
*.tgz
+3
View File
@@ -0,0 +1,3 @@
# VS Code Automation Package
This package contains functionality for automating various components of the VS Code UI, via an automation "driver" that connects from a separate process. It is used by the `smoke` tests.
+1313
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
{
"name": "vscode-automation",
"version": "1.71.0",
"description": "VS Code UI automation driver",
"author": {
"name": "Microsoft Corporation"
},
"license": "MIT",
"main": "./out/index.js",
"private": true,
"scripts": {
"compile": "npm run copy-driver-definition && node ../../node_modules/typescript/bin/tsc",
"watch": "npm-run-all -lp watch-driver-definition watch-tsc",
"watch-tsc": "node ../../node_modules/typescript/bin/tsc --watch --preserveWatchOutput",
"copy-driver-definition": "node tools/copy-driver-definition.js",
"watch-driver-definition": "nodemon --watch ../../src/vs/workbench/services/driver/common/driver.ts tools/copy-driver-definition.js",
"copy-package-version": "node tools/copy-package-version.js",
"prepublishOnly": "npm run copy-package-version"
},
"dependencies": {
"ncp": "^2.0.0",
"tmp": "0.2.1",
"tree-kill": "1.2.2",
"vscode-uri": "3.0.2"
},
"devDependencies": {
"@types/ncp": "2.0.1",
"@types/node": "20.x",
"@types/tmp": "0.2.2",
"cpx2": "3.0.0",
"nodemon": "^3.1.9",
"npm-run-all": "^4.1.5"
}
}
+30
View File
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
export const enum ActivityBarPosition {
LEFT = 0,
RIGHT = 1
}
export class ActivityBar {
constructor(private code: Code) { }
async waitForActivityBar(position: ActivityBarPosition): Promise<void> {
let positionClass: string;
if (position === ActivityBarPosition.LEFT) {
positionClass = 'left';
} else if (position === ActivityBarPosition.RIGHT) {
positionClass = 'right';
} else {
throw new Error('No such position for activity bar defined.');
}
await this.code.waitForElement(`.part.activitybar.${positionClass}`);
}
}
+147
View File
@@ -0,0 +1,147 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Workbench } from './workbench';
import { Code, launch, LaunchOptions } from './code';
import { Logger, measureAndLog } from './logger';
import { Profiler } from './profiler';
export const enum Quality {
Dev,
Insiders,
Stable,
Exploration,
OSS
}
export interface ApplicationOptions extends LaunchOptions {
quality: Quality;
readonly workspacePath: string;
}
export class Application {
constructor(private options: ApplicationOptions) {
this._userDataPath = options.userDataDir;
this._workspacePathOrFolder = options.workspacePath;
}
private _code: Code | undefined;
get code(): Code { return this._code!; }
private _workbench: Workbench | undefined;
get workbench(): Workbench { return this._workbench!; }
get quality(): Quality {
return this.options.quality;
}
get logger(): Logger {
return this.options.logger;
}
get remote(): boolean {
return !!this.options.remote;
}
get web(): boolean {
return !!this.options.web;
}
private _workspacePathOrFolder: string;
get workspacePathOrFolder(): string {
return this._workspacePathOrFolder;
}
get extensionsPath(): string {
return this.options.extensionsPath;
}
private _userDataPath: string;
get userDataPath(): string {
return this._userDataPath;
}
private _profiler: Profiler | undefined;
get profiler(): Profiler { return this._profiler!; }
async start(): Promise<void> {
await this._start();
await this.code.waitForElement('.explorer-folders-view');
}
async restart(options?: { workspaceOrFolder?: string; extraArgs?: string[] }): Promise<void> {
await measureAndLog(() => (async () => {
await this.stop();
await this._start(options?.workspaceOrFolder, options?.extraArgs);
})(), 'Application#restart()', this.logger);
}
private async _start(workspaceOrFolder = this.workspacePathOrFolder, extraArgs: string[] = []): Promise<void> {
this._workspacePathOrFolder = workspaceOrFolder;
// Launch Code...
const code = await this.startApplication(extraArgs);
// ...and make sure the window is ready to interact
await measureAndLog(() => this.checkWindowReady(code), 'Application#checkWindowReady()', this.logger);
}
async stop(): Promise<void> {
if (this._code) {
try {
await this._code.exit();
} finally {
this._code = undefined;
}
}
}
async startTracing(name: string): Promise<void> {
await this._code?.startTracing(name);
}
async stopTracing(name: string, persist: boolean): Promise<void> {
await this._code?.stopTracing(name, persist);
}
private async startApplication(extraArgs: string[] = []): Promise<Code> {
const code = this._code = await launch({
...this.options,
extraArgs: [...(this.options.extraArgs || []), ...extraArgs],
});
this._workbench = new Workbench(this._code);
this._profiler = new Profiler(this.code);
return code;
}
private async checkWindowReady(code: Code): Promise<void> {
// We need a rendered workbench
await measureAndLog(() => code.didFinishLoad(), 'Application#checkWindowReady: wait for navigation to be committed', this.logger);
await measureAndLog(() => code.waitForElement('.monaco-workbench'), 'Application#checkWindowReady: wait for .monaco-workbench element', this.logger);
await measureAndLog(() => code.whenWorkbenchRestored(), 'Application#checkWorkbenchRestored', this.logger);
// Remote but not web: wait for a remote connection state change
if (this.remote) {
await measureAndLog(() => code.waitForTextContent('.monaco-workbench .statusbar-item[id="status.host"]', undefined, statusHostLabel => {
this.logger.log(`checkWindowReady: remote indicator text is ${statusHostLabel}`);
// The absence of "Opening Remote" is not a strict
// indicator for a successful connection, but we
// want to avoid hanging here until timeout because
// this method is potentially called from a location
// that has no tracing enabled making it hard to
// diagnose this. As such, as soon as the connection
// state changes away from the "Opening Remote..." one
// we return.
return !statusHostLabel.includes('Opening Remote');
}, 300 /* = 30s of retry */), 'Application#checkWindowReady: wait for remote indicator', this.logger);
}
}
}
+349
View File
@@ -0,0 +1,349 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import * as os from 'os';
import * as treekill from 'tree-kill';
import { IElement, ILocaleInfo, ILocalizedStrings, ILogFile } from './driver';
import { Logger, measureAndLog } from './logger';
import { launch as launchPlaywrightBrowser } from './playwrightBrowser';
import { PlaywrightDriver } from './playwrightDriver';
import { launch as launchPlaywrightElectron } from './playwrightElectron';
import { teardown } from './processes';
import { Quality } from './application';
export interface LaunchOptions {
codePath?: string;
readonly workspacePath: string;
userDataDir: string;
readonly extensionsPath: string;
readonly logger: Logger;
logsPath: string;
crashesPath: string;
readonly verbose?: boolean;
readonly extraArgs?: string[];
readonly remote?: boolean;
readonly web?: boolean;
readonly tracing?: boolean;
snapshots?: boolean;
readonly headless?: boolean;
readonly browser?: 'chromium' | 'webkit' | 'firefox';
readonly quality: Quality;
}
interface ICodeInstance {
kill: () => Promise<void>;
}
const instances = new Set<ICodeInstance>();
function registerInstance(process: cp.ChildProcess, logger: Logger, type: string) {
const instance = { kill: () => teardown(process, logger) };
instances.add(instance);
process.stdout?.on('data', data => logger.log(`[${type}] stdout: ${data}`));
process.stderr?.on('data', error => logger.log(`[${type}] stderr: ${error}`));
process.once('exit', (code, signal) => {
logger.log(`[${type}] Process terminated (pid: ${process.pid}, code: ${code}, signal: ${signal})`);
instances.delete(instance);
});
}
async function teardownAll(signal?: number) {
stopped = true;
for (const instance of instances) {
await instance.kill();
}
if (typeof signal === 'number') {
process.exit(signal);
}
}
let stopped = false;
process.on('exit', () => teardownAll());
process.on('SIGINT', () => teardownAll(128 + 2)); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events
process.on('SIGTERM', () => teardownAll(128 + 15)); // same as above
export async function launch(options: LaunchOptions): Promise<Code> {
if (stopped) {
throw new Error('Smoke test process has terminated, refusing to spawn Code');
}
// Browser smoke tests
if (options.web) {
const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger);
registerInstance(serverProcess, options.logger, 'server');
return new Code(driver, options.logger, serverProcess, options.quality);
}
// Electron smoke tests (playwright)
else {
const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger);
registerInstance(electronProcess, options.logger, 'electron');
return new Code(driver, options.logger, electronProcess, options.quality);
}
}
export class Code {
readonly driver: PlaywrightDriver;
constructor(
driver: PlaywrightDriver,
readonly logger: Logger,
private readonly mainProcess: cp.ChildProcess,
readonly quality: Quality
) {
this.driver = new Proxy(driver, {
get(target, prop) {
if (typeof prop === 'symbol') {
throw new Error('Invalid usage');
}
const targetProp = (target as any)[prop];
if (typeof targetProp !== 'function') {
return targetProp;
}
return function (this: any, ...args: any[]) {
logger.log(`${prop}`, ...args.filter(a => typeof a === 'string'));
return targetProp.apply(this, args);
};
}
});
}
async startTracing(name: string): Promise<void> {
return await this.driver.startTracing(name);
}
async stopTracing(name: string, persist: boolean): Promise<void> {
return await this.driver.stopTracing(name, persist);
}
async sendKeybinding(keybinding: string, accept?: () => Promise<void> | void): Promise<void> {
await this.driver.sendKeybinding(keybinding, accept);
}
async didFinishLoad(): Promise<void> {
return this.driver.didFinishLoad();
}
async exit(): Promise<void> {
return measureAndLog(() => new Promise<void>(resolve => {
const pid = this.mainProcess.pid!;
let done = false;
// Start the exit flow via driver
this.driver.exitApplication();
// Await the exit of the application
(async () => {
let retries = 0;
while (!done) {
retries++;
switch (retries) {
// after 5 / 10 seconds: try to exit gracefully again
case 10:
case 20: {
this.logger.log('Smoke test exit call did not terminate process after 5-10s, gracefully trying to exit the application again...');
this.driver.exitApplication();
break;
}
// after 20 seconds: forcefully kill
case 40: {
this.logger.log('Smoke test exit call did not terminate process after 20s, forcefully exiting the application...');
// no need to await since we're polling for the process to die anyways
treekill(pid, err => {
try {
process.kill(pid, 0); // throws an exception if the process doesn't exist anymore
this.logger.log('Failed to kill Electron process tree:', err?.message);
} catch (error) {
// Expected when process is gone
}
});
break;
}
// after 30 seconds: give up
case 60: {
done = true;
this.logger.log('Smoke test exit call did not terminate process after 30s, giving up');
resolve();
}
}
try {
process.kill(pid, 0); // throws an exception if the process doesn't exist anymore.
await this.wait(500);
} catch (error) {
done = true;
resolve();
}
}
})();
}), 'Code#exit()', this.logger);
}
async getElement(selector: string): Promise<IElement | undefined> {
return (await this.driver.getElements(selector))?.[0];
}
async getElements(selector: string, recursive: boolean): Promise<IElement[] | undefined> {
return this.driver.getElements(selector, recursive);
}
async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean, retryCount?: number): Promise<string> {
accept = accept || (result => textContent !== undefined ? textContent === result : !!result);
return await this.poll(
() => this.driver.getElements(selector).then(els => els.length > 0 ? Promise.resolve(els[0].textContent) : Promise.reject(new Error('Element not found for textContent'))),
s => accept!(typeof s === 'string' ? s : ''),
`get text content '${selector}'`,
retryCount
);
}
async waitAndClick(selector: string, xoffset?: number, yoffset?: number, retryCount: number = 200): Promise<void> {
await this.poll(() => this.driver.click(selector, xoffset, yoffset), () => true, `click '${selector}'`, retryCount);
}
async waitForSetValue(selector: string, value: string): Promise<void> {
await this.poll(() => this.driver.setValue(selector, value), () => true, `set value '${selector}'`);
}
async waitForElements(selector: string, recursive: boolean, accept: (result: IElement[]) => boolean = result => result.length > 0): Promise<IElement[]> {
return await this.poll(() => this.driver.getElements(selector, recursive), accept, `get elements '${selector}'`);
}
async waitForElement(selector: string, accept: (result: IElement | undefined) => boolean = result => !!result, retryCount: number = 200): Promise<IElement> {
return await this.poll<IElement>(() => this.driver.getElements(selector).then(els => els[0]), accept, `get element '${selector}'`, retryCount);
}
async waitForActiveElement(selector: string, retryCount: number = 200): Promise<void> {
await this.poll(() => this.driver.isActiveElement(selector), r => r, `is active element '${selector}'`, retryCount);
}
async waitForTitle(accept: (title: string) => boolean): Promise<void> {
await this.poll(() => this.driver.getTitle(), accept, `get title`);
}
async waitForTypeInEditor(selector: string, text: string): Promise<void> {
await this.poll(() => this.driver.typeInEditor(selector, text), () => true, `type in editor '${selector}'`);
}
async waitForEditorSelection(selector: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise<void> {
await this.poll(() => this.driver.getEditorSelection(selector), accept, `get editor selection '${selector}'`);
}
async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise<void> {
await this.poll(() => this.driver.getTerminalBuffer(selector), accept, `get terminal buffer '${selector}'`);
}
async writeInTerminal(selector: string, value: string): Promise<void> {
await this.poll(() => this.driver.writeInTerminal(selector, value), () => true, `writeInTerminal '${selector}'`);
}
async whenWorkbenchRestored(): Promise<void> {
await this.poll(() => this.driver.whenWorkbenchRestored(), () => true, `when workbench restored`);
}
getLocaleInfo(): Promise<ILocaleInfo> {
return this.driver.getLocaleInfo();
}
getLocalizedStrings(): Promise<ILocalizedStrings> {
return this.driver.getLocalizedStrings();
}
getLogs(): Promise<ILogFile[]> {
return this.driver.getLogs();
}
wait(millis: number): Promise<void> {
return this.driver.wait(millis);
}
private async poll<T>(
fn: () => Promise<T>,
acceptFn: (result: T) => boolean,
timeoutMessage: string,
retryCount = 200,
retryInterval = 100 // millis
): Promise<T> {
let trial = 1;
let lastError: string = '';
while (true) {
if (trial > retryCount) {
this.logger.log('Timeout!');
this.logger.log(lastError);
this.logger.log(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`);
throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`);
}
let result;
try {
result = await fn();
if (acceptFn(result)) {
return result;
} else {
lastError = 'Did not pass accept function';
}
} catch (e: any) {
lastError = Array.isArray(e.stack) ? e.stack.join(os.EOL) : e.stack;
}
await this.wait(retryInterval);
trial++;
}
}
}
export function findElement(element: IElement, fn: (element: IElement) => boolean): IElement | null {
const queue = [element];
while (queue.length > 0) {
const element = queue.shift()!;
if (fn(element)) {
return element;
}
queue.push(...element.children);
}
return null;
}
export function findElements(element: IElement, fn: (element: IElement) => boolean): IElement[] {
const result: IElement[] = [];
const queue = [element];
while (queue.length > 0) {
const element = queue.shift()!;
if (fn(element)) {
result.push(element);
}
queue.push(...element.children);
}
return result;
}
+161
View File
@@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Viewlet } from './viewlet';
import { Commands } from './workbench';
import { Code, findElement } from './code';
import { Editors } from './editors';
import { Editor } from './editor';
import { IElement } from './driver';
import { Quality } from './application';
const VIEWLET = 'div[id="workbench.view.debug"]';
const DEBUG_VIEW = `${VIEWLET}`;
const CONFIGURE = `div[id="workbench.parts.sidebar"] .actions-container .codicon-gear`;
const STOP = `.debug-toolbar .action-label[title*="Stop"]`;
const STEP_OVER = `.debug-toolbar .action-label[title*="Step Over"]`;
const STEP_IN = `.debug-toolbar .action-label[title*="Step Into"]`;
const STEP_OUT = `.debug-toolbar .action-label[title*="Step Out"]`;
const CONTINUE = `.debug-toolbar .action-label[title*="Continue"]`;
const GLYPH_AREA = '.margin-view-overlays>:nth-child';
const BREAKPOINT_GLYPH = '.codicon-debug-breakpoint';
const PAUSE = `.debug-toolbar .action-label[title*="Pause"]`;
const DEBUG_STATUS_BAR = `.statusbar.debugging`;
const NOT_DEBUG_STATUS_BAR = `.statusbar:not(debugging)`;
const TOOLBAR_HIDDEN = `.debug-toolbar[aria-hidden="true"]`;
const STACK_FRAME = `${VIEWLET} .monaco-list-row .stack-frame`;
const SPECIFIC_STACK_FRAME = (filename: string) => `${STACK_FRAME} .file[title*="${filename}"]`;
const VARIABLE = `${VIEWLET} .debug-variables .monaco-list-row .expression`;
const CONSOLE_OUTPUT = `.repl .output.expression .value`;
const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`;
const CONSOLE_LINK = `.repl .value a.link`;
const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context';
const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea';
export interface IStackFrame {
name: string;
lineNumber: number;
}
function toStackFrame(element: IElement): IStackFrame {
const name = findElement(element, e => /\bfile-name\b/.test(e.className))!;
const line = findElement(element, e => /\bline-number\b/.test(e.className))!;
const lineNumber = line.textContent ? parseInt(line.textContent.split(':').shift() || '0') : 0;
return {
name: name.textContent || '',
lineNumber
};
}
export class Debug extends Viewlet {
constructor(code: Code, private commands: Commands, private editors: Editors, private editor: Editor) {
super(code);
}
async openDebugViewlet(): Promise<any> {
const accept = async () => {
await this.code.waitForElement(DEBUG_VIEW);
};
if (process.platform === 'darwin') {
await this.code.sendKeybinding('cmd+shift+d', accept);
} else {
await this.code.sendKeybinding('ctrl+shift+d', accept);
}
await this.code.waitForElement(DEBUG_VIEW);
}
async configure(): Promise<any> {
await this.code.waitAndClick(CONFIGURE);
await this.editors.waitForEditorFocus('launch.json');
}
async setBreakpointOnLine(lineNumber: number): Promise<any> {
await this.code.waitForElement(`${GLYPH_AREA}(${lineNumber})`);
await this.code.waitAndClick(`${GLYPH_AREA}(${lineNumber})`, 5, 5);
await this.code.waitForElement(BREAKPOINT_GLYPH);
}
async startDebugging(): Promise<number> {
await this.code.sendKeybinding('f5', async () => {
await this.code.waitForElement(PAUSE);
await this.code.waitForElement(DEBUG_STATUS_BAR);
});
const portPrefix = 'Port: ';
const output = await this.waitForOutput(output => output.some(line => line.indexOf(portPrefix) >= 0));
const lastOutput = output.filter(line => line.indexOf(portPrefix) >= 0)[0];
return lastOutput ? parseInt(lastOutput.substr(portPrefix.length)) : 3000;
}
async stepOver(): Promise<any> {
await this.code.waitAndClick(STEP_OVER);
}
async stepIn(): Promise<any> {
await this.code.waitAndClick(STEP_IN);
}
async stepOut(): Promise<any> {
await this.code.waitAndClick(STEP_OUT);
}
async continue(): Promise<any> {
await this.code.waitAndClick(CONTINUE);
await this.waitForStackFrameLength(0);
}
async stopDebugging(): Promise<any> {
await this.code.waitAndClick(STOP);
await this.code.waitForElement(TOOLBAR_HIDDEN);
await this.code.waitForElement(NOT_DEBUG_STATUS_BAR);
}
async waitForStackFrame(func: (stackFrame: IStackFrame) => boolean, message: string): Promise<IStackFrame> {
const elements = await this.code.waitForElements(STACK_FRAME, true, elements => elements.some(e => func(toStackFrame(e))));
return elements.map(toStackFrame).filter(s => func(s))[0];
}
async waitForStackFrameLength(length: number): Promise<any> {
await this.code.waitForElements(STACK_FRAME, false, result => result.length === length);
}
async focusStackFrame(name: string, message: string): Promise<any> {
await this.code.waitAndClick(SPECIFIC_STACK_FRAME(name), 0, 0);
await this.editors.waitForTab(name);
}
async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise<void> {
await this.commands.runCommand('Debug: Focus on Debug Console View');
const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT;
await this.code.waitForActiveElement(selector);
await this.code.waitForSetValue(selector, text);
// Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed
await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0);
await this.code.sendKeybinding('enter', async () => {
await this.code.waitForElements(CONSOLE_EVALUATION_RESULT, false,
elements => !!elements.length && accept(elements[elements.length - 1].textContent));
});
}
// Different node versions give different number of variables. As a workaround be more relaxed when checking for variable count
async waitForVariableCount(count: number, alternativeCount: number): Promise<void> {
await this.code.waitForElements(VARIABLE, false, els => els.length === count || els.length === alternativeCount);
}
async waitForLink(): Promise<void> {
await this.code.waitForElement(CONSOLE_LINK);
}
private async waitForOutput(fn: (output: string[]) => boolean): Promise<string[]> {
const elements = await this.code.waitForElements(CONSOLE_OUTPUT, false, elements => fn(elements.map(e => e.textContent)));
return elements.map(e => e.textContent);
}
}
+137
View File
@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { References } from './peek';
import { Commands } from './workbench';
import { Code } from './code';
import { Quality } from './application';
const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box';
const RENAME_INPUT = `${RENAME_BOX} .rename-input`;
const EDITOR = (filename: string) => `.monaco-editor[data-uri$="${filename}"]`;
const VIEW_LINES = (filename: string) => `${EDITOR(filename)} .view-lines`;
const LINE_NUMBERS = (filename: string) => `${EDITOR(filename)} .margin .margin-view-overlays .line-numbers`;
export class Editor {
private static readonly FOLDING_EXPANDED = '.monaco-editor .margin .margin-view-overlays>:nth-child(${INDEX}) .folding';
private static readonly FOLDING_COLLAPSED = `${Editor.FOLDING_EXPANDED}.collapsed`;
constructor(private code: Code, private commands: Commands) { }
async findReferences(filename: string, term: string, line: number): Promise<References> {
await this.clickOnTerm(filename, term, line);
await this.commands.runCommand('Peek References');
const references = new References(this.code);
await references.waitUntilOpen();
return references;
}
async rename(filename: string, line: number, from: string, to: string): Promise<void> {
await this.clickOnTerm(filename, from, line);
await this.commands.runCommand('Rename Symbol');
await this.code.waitForActiveElement(RENAME_INPUT);
await this.code.waitForSetValue(RENAME_INPUT, to);
await this.code.sendKeybinding('enter');
}
async gotoDefinition(filename: string, term: string, line: number): Promise<void> {
await this.clickOnTerm(filename, term, line);
await this.commands.runCommand('Go to Implementations');
}
async peekDefinition(filename: string, term: string, line: number): Promise<References> {
await this.clickOnTerm(filename, term, line);
await this.commands.runCommand('Peek Definition');
const peek = new References(this.code);
await peek.waitUntilOpen();
return peek;
}
private async getSelector(filename: string, term: string, line: number): Promise<string> {
const lineIndex = await this.getViewLineIndex(filename, line);
const classNames = await this.getClassSelectors(filename, term, lineIndex);
return `${VIEW_LINES(filename)}>:nth-child(${lineIndex}) span span.${classNames[0]}`;
}
async foldAtLine(filename: string, line: number): Promise<any> {
const lineIndex = await this.getViewLineIndex(filename, line);
await this.code.waitAndClick(Editor.FOLDING_EXPANDED.replace('${INDEX}', '' + lineIndex));
await this.code.waitForElement(Editor.FOLDING_COLLAPSED.replace('${INDEX}', '' + lineIndex));
}
async unfoldAtLine(filename: string, line: number): Promise<any> {
const lineIndex = await this.getViewLineIndex(filename, line);
await this.code.waitAndClick(Editor.FOLDING_COLLAPSED.replace('${INDEX}', '' + lineIndex));
await this.code.waitForElement(Editor.FOLDING_EXPANDED.replace('${INDEX}', '' + lineIndex));
}
private async clickOnTerm(filename: string, term: string, line: number): Promise<void> {
const selector = await this.getSelector(filename, term, line);
await this.code.waitAndClick(selector);
}
async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise<void> {
const editor = [selectorPrefix || '', EDITOR(filename)].join(' ');
const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`;
const editContext = `${editor} ${this._editContextSelector()}`;
await this.code.waitAndClick(line, 1, 1);
await this.code.waitForActiveElement(editContext);
}
async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise<any> {
if (text.includes('\n')) {
throw new Error('waitForTypeInEditor does not support new lines, use either a long single line or dispatchKeybinding(\'Enter\')');
}
const editor = [selectorPrefix || '', EDITOR(filename)].join(' ');
await this.code.waitForElement(editor);
const editContext = `${editor} ${this._editContextSelector()}`;
await this.code.waitForActiveElement(editContext);
await this.code.waitForTypeInEditor(editContext, text);
await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix);
}
async waitForEditorSelection(filename: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise<void> {
const selector = `${EDITOR(filename)} ${this._editContextSelector()}`;
await this.code.waitForEditorSelection(selector, accept);
}
private _editContextSelector() {
return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context';
}
async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise<any> {
const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' ');
return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' ')));
}
private async getClassSelectors(filename: string, term: string, viewline: number): Promise<string[]> {
const elements = await this.code.waitForElements(`${VIEW_LINES(filename)}>:nth-child(${viewline}) span span`, false, els => els.some(el => el.textContent === term));
const { className } = elements.filter(r => r.textContent === term)[0];
return className.split(/\s/g);
}
private async getViewLineIndex(filename: string, line: number): Promise<number> {
const elements = await this.code.waitForElements(LINE_NUMBERS(filename), false, els => {
return els.some(el => el.textContent === `${line}`);
});
for (let index = 0; index < elements.length; index++) {
if (elements[index].textContent === `${line}`) {
return index + 1;
}
}
throw new Error('Line not found');
}
}
+72
View File
@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Quality } from './application';
import { Code } from './code';
export class Editors {
constructor(private code: Code) { }
async saveOpenedFile(): Promise<any> {
if (process.platform === 'darwin') {
await this.code.sendKeybinding('cmd+s');
} else {
await this.code.sendKeybinding('ctrl+s');
}
}
async selectTab(fileName: string): Promise<void> {
// Selecting a tab and making an editor have keyboard focus
// is critical to almost every test. As such, we try our
// best to retry this task in case some other component steals
// focus away from the editor while we attempt to get focus
let error: unknown | undefined = undefined;
let retries = 0;
while (retries < 10) {
await this.code.waitAndClick(`.tabs-container div.tab[data-resource-name$="${fileName}"]`);
try {
await this.code.sendKeybinding(process.platform === 'darwin' ? 'cmd+1' : 'ctrl+1', () => this.waitForEditorFocus(fileName, 50 /* 50 retries * 100ms delay = 5s */));
return;
} catch (e) {
error = e;
retries++;
}
}
// We failed after 10 retries
throw error;
}
async waitForEditorFocus(fileName: string, retryCount?: number): Promise<void> {
await this.waitForActiveTab(fileName, undefined, retryCount);
await this.waitForActiveEditor(fileName, retryCount);
}
async waitForActiveTab(fileName: string, isDirty: boolean = false, retryCount?: number): Promise<void> {
await this.code.waitForElement(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][data-resource-name$="${fileName}"]`, undefined, retryCount);
}
async waitForActiveEditor(fileName: string, retryCount?: number): Promise<any> {
const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`;
return this.code.waitForActiveElement(selector, retryCount);
}
async waitForTab(fileName: string, isDirty: boolean = false): Promise<void> {
await this.code.waitForElement(`.tabs-container div.tab${isDirty ? '.dirty' : ''}[data-resource-name$="${fileName}"]`);
}
async newUntitledFile(): Promise<void> {
const accept = () => this.waitForEditorFocus('Untitled-1');
if (process.platform === 'darwin') {
await this.code.sendKeybinding('cmd+n', accept);
} else {
await this.code.sendKeybinding('ctrl+n', accept);
}
}
}
+139
View File
@@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'path';
import * as fs from 'fs';
import { copyExtension } from './extensions';
import { URI } from 'vscode-uri';
import { measureAndLog } from './logger';
import type { LaunchOptions } from './code';
const root = join(__dirname, '..', '..', '..');
export interface IElectronConfiguration {
readonly electronPath: string;
readonly args: string[];
readonly env?: NodeJS.ProcessEnv;
}
export async function resolveElectronConfiguration(options: LaunchOptions): Promise<IElectronConfiguration> {
const { codePath, workspacePath, extensionsPath, userDataDir, remote, logger, logsPath, crashesPath, extraArgs } = options;
const env = { ...process.env };
const args = [
workspacePath,
'--skip-release-notes',
'--skip-welcome',
'--disable-telemetry',
'--no-cached-data',
'--disable-updates',
'--use-inmemory-secretstorage',
`--crash-reporter-directory=${crashesPath}`,
'--disable-workspace-trust',
`--extensions-dir=${extensionsPath}`,
`--user-data-dir=${userDataDir}`,
`--logsPath=${logsPath}`
];
if (options.verbose) {
args.push('--verbose');
}
if (process.platform === 'linux') {
// --disable-dev-shm-usage: when run on docker containers where size of /dev/shm
// partition < 64MB which causes OOM failure for chromium compositor that uses
// this partition for shared memory.
// Refs https://github.com/microsoft/vscode/issues/152143
args.push('--disable-dev-shm-usage');
// Refs https://github.com/microsoft/vscode/issues/192206
args.push('--disable-gpu');
}
if (process.platform === 'darwin') {
// On macOS force software based rendering since we are seeing GPU process
// hangs when initializing GL context. This is very likely possible
// that there are new displays available in the CI hardware and
// the relevant drivers couldn't be loaded via the GPU sandbox.
// TODO(deepak1556): remove this switch with Electron update.
args.push('--use-gl=swiftshader');
}
if (remote) {
// Replace workspace path with URI
args[0] = `--${workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(workspacePath).path}`;
if (codePath) {
// running against a build: copy the test resolver extension
await measureAndLog(() => copyExtension(root, extensionsPath, 'vscode-test-resolver'), 'copyExtension(vscode-test-resolver)', logger);
}
args.push('--enable-proposed-api=vscode.vscode-test-resolver');
const remoteDataDir = `${userDataDir}-server`;
fs.mkdirSync(remoteDataDir, { recursive: true });
env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir;
env['TESTRESOLVER_LOGS_FOLDER'] = join(logsPath, 'server');
if (options.verbose) {
env['TESTRESOLVER_LOG_LEVEL'] = 'trace';
}
}
if (!codePath) {
args.unshift(root);
}
if (extraArgs) {
args.push(...extraArgs);
}
const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath();
return {
env,
args,
electronPath
};
}
export function getDevElectronPath(): string {
const buildPath = join(root, '.build');
const product = require(join(root, 'product.json'));
switch (process.platform) {
case 'darwin':
return join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron');
case 'linux':
return join(buildPath, 'electron', `${product.applicationName}`);
case 'win32':
return join(buildPath, 'electron', `${product.nameShort}.exe`);
default:
throw new Error('Unsupported platform.');
}
}
export function getBuildElectronPath(root: string): string {
switch (process.platform) {
case 'darwin':
return join(root, 'Contents', 'MacOS', 'Electron');
case 'linux': {
const product = require(join(root, 'resources', 'app', 'product.json'));
return join(root, product.applicationName);
}
case 'win32': {
const product = require(join(root, 'resources', 'app', 'product.json'));
return join(root, `${product.nameShort}.exe`);
}
default:
throw new Error('Unsupported platform.');
}
}
export function getBuildVersion(root: string): string {
switch (process.platform) {
case 'darwin':
return require(join(root, 'Contents', 'Resources', 'app', 'package.json')).version;
default:
return require(join(root, 'resources', 'app', 'package.json')).version;
}
}
+42
View File
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Viewlet } from './viewlet';
import { Code } from './code';
export class Explorer extends Viewlet {
private static readonly EXPLORER_VIEWLET = 'div[id="workbench.view.explorer"]';
private static readonly OPEN_EDITORS_VIEW = `${Explorer.EXPLORER_VIEWLET} .split-view-view:nth-child(1) .title`;
constructor(code: Code) {
super(code);
}
async openExplorerView(): Promise<any> {
if (process.platform === 'darwin') {
await this.code.sendKeybinding('cmd+shift+e');
} else {
await this.code.sendKeybinding('ctrl+shift+e');
}
}
async waitForOpenEditorsViewTitle(fn: (title: string) => boolean): Promise<void> {
await this.code.waitForTextContent(Explorer.OPEN_EDITORS_VIEW, undefined, fn);
}
getExtensionSelector(fileName: string): string {
const extension = fileName.split('.')[1];
if (extension === 'js') {
return 'js-ext-file-icon ext-file-icon javascript-lang-file-icon';
} else if (extension === 'json') {
return 'json-ext-file-icon ext-file-icon json-lang-file-icon';
} else if (extension === 'md') {
return 'md-ext-file-icon ext-file-icon markdown-lang-file-icon';
}
throw new Error('No class defined for this file extension');
}
}
+84
View File
@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Viewlet } from './viewlet';
import { Code } from './code';
import { ncp } from 'ncp';
import { promisify } from 'util';
import { Commands } from './workbench';
import { Quality } from './application';
import path = require('path');
import fs = require('fs');
export class Extensions extends Viewlet {
constructor(code: Code, private commands: Commands) {
super(code);
}
async searchForExtension(id: string): Promise<any> {
await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true });
await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`);
await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace');
let retrials = 1;
while (retrials++ < 10) {
try {
return await this.code.waitForElement(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"]`, undefined, 100);
} catch (error) {
this.code.logger.log(`Extension '${id}' is not found. Retrying count: ${retrials}`);
await this.commands.runCommand('workbench.extensions.action.refreshExtension');
}
}
throw new Error(`Extension ${id} is not found`);
}
async openExtension(id: string): Promise<any> {
await this.searchForExtension(id);
await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"]`);
}
async closeExtension(title: string): Promise<any> {
try {
await this.code.waitAndClick(`.tabs-container div.tab[aria-label="Extension: ${title}, preview"] div.tab-actions a.action-label.codicon.codicon-close`);
} catch (e) {
this.code.logger.log(`Extension '${title}' not opened as preview. Trying without 'preview'.`);
await this.code.waitAndClick(`.tabs-container div.tab[aria-label="Extension: ${title}"] div.tab-actions a.action-label.codicon.codicon-close`);
}
}
async installExtension(id: string, waitUntilEnabled: boolean): Promise<void> {
await this.searchForExtension(id);
// try to install extension 3 times
let attempt = 1;
while (true) {
await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"] .extension-list-item .monaco-action-bar .action-item:not(.disabled) .extension-action.install`);
try {
await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`);
break;
} catch (err) {
if (attempt++ === 3) {
throw err;
}
}
}
if (waitUntilEnabled) {
await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) a[aria-label="Disable this extension"]`);
}
}
}
export async function copyExtension(repoPath: string, extensionsPath: string, extId: string): Promise<void> {
const dest = path.join(extensionsPath, extId);
if (!fs.existsSync(dest)) {
const orig = path.join(repoPath, 'extensions', extId);
return promisify(ncp)(orig, dest);
}
}
+29
View File
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export * from './activityBar';
export * from './application';
export * from './code';
export * from './debug';
export * from './editor';
export * from './editors';
export * from './explorer';
export * from './extensions';
export * from './keybindings';
export * from './logger';
export * from './peek';
export * from './problems';
export * from './quickinput';
export * from './quickaccess';
export * from './scm';
export * from './search';
export * from './settings';
export * from './statusbar';
export * from './terminal';
export * from './viewlet';
export * from './localization';
export * from './workbench';
export * from './task';
export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electron';
+32
View File
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
const SEARCH_INPUT = '.keybindings-header .settings-search-input input';
export class KeybindingsEditor {
constructor(private code: Code) { }
async updateKeybinding(command: string, commandName: string | undefined, keybinding: string, keybindingTitle: string): Promise<any> {
const accept = () => this.code.waitForActiveElement(SEARCH_INPUT);
if (process.platform === 'darwin') {
await this.code.sendKeybinding('cmd+k cmd+s', accept);
} else {
await this.code.sendKeybinding('ctrl+k ctrl+s', accept);
}
await this.code.waitForActiveElement(SEARCH_INPUT);
await this.code.waitForSetValue(SEARCH_INPUT, `@command:${command}`);
const commandTitle = commandName ? `${commandName} (${command})` : command;
await this.code.waitAndClick(`.keybindings-table-container .monaco-list-row .command[aria-label="${commandTitle}"]`);
await this.code.waitForElement(`.keybindings-table-container .monaco-list-row.focused.selected .command[aria-label="${commandTitle}"]`);
await this.code.sendKeybinding('enter', () => this.code.waitForActiveElement('.defineKeybindingWidget .monaco-inputbox input'));
await this.code.sendKeybinding(keybinding);
await this.code.sendKeybinding('enter', async () => { await this.code.waitForElement(`.keybindings-table-container .keybinding-label div[aria-label="${keybindingTitle}"]`); });
}
}
+19
View File
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
import { ILocalizedStrings, ILocaleInfo } from './driver';
export class Localization {
constructor(private code: Code) { }
async getLocaleInfo(): Promise<ILocaleInfo> {
return this.code.getLocaleInfo();
}
async getLocalizedStrings(): Promise<ILocalizedStrings> {
return this.code.getLocalizedStrings();
}
}
+65
View File
@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { appendFileSync, writeFileSync } from 'fs';
import { format } from 'util';
import { EOL } from 'os';
export interface Logger {
log(message: string, ...args: any[]): void;
}
export class ConsoleLogger implements Logger {
log(message: string, ...args: any[]): void {
console.log('**', message, ...args);
}
}
export class FileLogger implements Logger {
constructor(private path: string) {
writeFileSync(path, '');
}
log(message: string, ...args: any[]): void {
const date = new Date().toISOString();
appendFileSync(this.path, `[${date}] ${format(message, ...args)}${EOL}`);
}
}
export class MultiLogger implements Logger {
constructor(private loggers: Logger[]) { }
log(message: string, ...args: any[]): void {
for (const logger of this.loggers) {
logger.log(message, ...args);
}
}
}
export async function measureAndLog<T>(promiseFactory: () => Promise<T>, name: string, logger: Logger): Promise<T> {
const now = Date.now();
logger.log(`Starting operation '${name}'...`);
let res: T | undefined = undefined;
let e: unknown;
try {
res = await promiseFactory();
} catch (error) {
e = error;
}
if (e) {
logger.log(`Finished operation '${name}' with error ${e} after ${Date.now() - now}ms`);
throw e;
}
logger.log(`Finished operation '${name}' successfully after ${Date.now() - now}ms`);
return res as unknown as T;
}
+100
View File
@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Quality } from './application';
import { Code } from './code';
import { QuickAccess } from './quickaccess';
import { QuickInput } from './quickinput';
const activeRowSelector = `.notebook-editor .monaco-list-row.focused`;
export class Notebook {
constructor(
private readonly quickAccess: QuickAccess,
private readonly quickInput: QuickInput,
private readonly code: Code) {
}
async openNotebook() {
await this.quickAccess.openFileQuickAccessAndWait('notebook.ipynb', 1);
await this.quickInput.selectQuickInputElement(0);
await this.code.waitForElement(activeRowSelector);
await this.focusFirstCell();
}
async focusNextCell() {
await this.code.sendKeybinding('down');
}
async focusFirstCell() {
await this.quickAccess.runCommand('notebook.focusTop');
}
async editCell() {
await this.code.sendKeybinding('enter');
}
async stopEditingCell() {
await this.quickAccess.runCommand('notebook.cell.quitEdit');
}
async waitForTypeInEditor(text: string): Promise<any> {
const editor = `${activeRowSelector} .monaco-editor`;
await this.code.waitForElement(editor);
const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`;
await this.code.waitForActiveElement(editContext);
await this.code.waitForTypeInEditor(editContext, text);
await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1);
}
async waitForActiveCellEditorContents(contents: string): Promise<any> {
return this._waitForActiveCellEditorContents(str => str === contents);
}
private async _waitForActiveCellEditorContents(accept: (contents: string) => boolean): Promise<any> {
const selector = `${activeRowSelector} .monaco-editor .view-lines`;
return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' ')));
}
async waitForMarkdownContents(markdownSelector: string, text: string): Promise<void> {
const selector = `${activeRowSelector} .markdown ${markdownSelector}`;
await this.code.waitForTextContent(selector, text);
}
async insertNotebookCell(kind: 'markdown' | 'code'): Promise<void> {
if (kind === 'markdown') {
await this.quickAccess.runCommand('notebook.cell.insertMarkdownCellBelow');
} else {
await this.quickAccess.runCommand('notebook.cell.insertCodeCellBelow');
}
}
async deleteActiveCell(): Promise<void> {
await this.quickAccess.runCommand('notebook.cell.delete');
}
async focusInCellOutput(): Promise<void> {
await this.quickAccess.runCommand('notebook.cell.focusInOutput');
await this.code.waitForActiveElement('webview, .webview');
}
async focusOutCellOutput(): Promise<void> {
await this.quickAccess.runCommand('notebook.cell.focusOutOutput');
}
async executeActiveCell(): Promise<void> {
await this.quickAccess.runCommand('notebook.cell.execute');
}
async executeCellAction(selector: string): Promise<void> {
await this.code.waitAndClick(selector);
}
}
+51
View File
@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
export class References {
private static readonly REFERENCES_WIDGET = '.monaco-editor .zone-widget .zone-widget-container.peekview-widget.reference-zone-widget.results-loaded';
private static readonly REFERENCES_TITLE_FILE_NAME = `${References.REFERENCES_WIDGET} .head .peekview-title .filename`;
private static readonly REFERENCES_TITLE_COUNT = `${References.REFERENCES_WIDGET} .head .peekview-title .meta`;
private static readonly REFERENCES = `${References.REFERENCES_WIDGET} .body .ref-tree.inline .monaco-list-row .highlight`;
constructor(private code: Code) { }
async waitUntilOpen(): Promise<void> {
await this.code.waitForElement(References.REFERENCES_WIDGET);
}
async waitForReferencesCountInTitle(count: number): Promise<void> {
await this.code.waitForTextContent(References.REFERENCES_TITLE_COUNT, undefined, titleCount => {
const matches = titleCount.match(/\d+/);
return matches ? parseInt(matches[0]) === count : false;
});
}
async waitForReferencesCount(count: number): Promise<void> {
await this.code.waitForElements(References.REFERENCES, false, result => result && result.length === count);
}
async waitForFile(file: string): Promise<void> {
await this.code.waitForTextContent(References.REFERENCES_TITLE_FILE_NAME, file);
}
async close(): Promise<void> {
// Sometimes someone else eats up the `Escape` key
let count = 0;
while (true) {
try {
await this.code.sendKeybinding('escape', async () => { await this.code.waitForElement(References.REFERENCES_WIDGET, el => !el, 10); });
return;
} catch (err) {
if (++count > 5) {
throw err;
}
}
}
}
}
+177
View File
@@ -0,0 +1,177 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as playwright from '@playwright/test';
import { ChildProcess, spawn } from 'child_process';
import { join } from 'path';
import * as fs from 'fs';
import { URI } from 'vscode-uri';
import { Logger, measureAndLog } from './logger';
import type { LaunchOptions } from './code';
import { PlaywrightDriver } from './playwrightDriver';
const root = join(__dirname, '..', '..', '..');
let port = 9000;
export async function launch(options: LaunchOptions): Promise<{ serverProcess: ChildProcess; driver: PlaywrightDriver }> {
// Launch server
const { serverProcess, endpoint } = await launchServer(options);
// Launch browser
const { browser, context, page, pageLoadedPromise } = await launchBrowser(options, endpoint);
return {
serverProcess,
driver: new PlaywrightDriver(browser, context, page, serverProcess, pageLoadedPromise, options)
};
}
async function launchServer(options: LaunchOptions) {
const { userDataDir, codePath, extensionsPath, logger, logsPath } = options;
const serverLogsPath = join(logsPath, 'server');
const codeServerPath = codePath ?? process.env.VSCODE_REMOTE_SERVER_PATH;
const agentFolder = userDataDir;
await measureAndLog(() => fs.promises.mkdir(agentFolder, { recursive: true }), `mkdirp(${agentFolder})`, logger);
const env = {
VSCODE_REMOTE_SERVER_PATH: codeServerPath,
...process.env
};
const args = [
'--disable-telemetry',
'--disable-workspace-trust',
`--port=${port++}`,
'--enable-smoke-test-driver',
`--extensions-dir=${extensionsPath}`,
`--server-data-dir=${agentFolder}`,
'--accept-server-license-terms',
`--logsPath=${serverLogsPath}`
];
if (options.verbose) {
args.push('--log=trace');
}
let serverLocation: string | undefined;
if (codeServerPath) {
const { serverApplicationName } = require(join(codeServerPath, 'product.json'));
serverLocation = join(codeServerPath, 'bin', `${serverApplicationName}${process.platform === 'win32' ? '.cmd' : ''}`);
logger.log(`Starting built server from '${serverLocation}'`);
} else {
serverLocation = join(root, `scripts/code-server.${process.platform === 'win32' ? 'bat' : 'sh'}`);
logger.log(`Starting server out of sources from '${serverLocation}'`);
}
logger.log(`Storing log files into '${serverLogsPath}'`);
logger.log(`Command line: '${serverLocation}' ${args.join(' ')}`);
const shell: boolean = (process.platform === 'win32');
const serverProcess = spawn(
serverLocation,
args,
{ env, shell }
);
logger.log(`Started server for browser smoke tests (pid: ${serverProcess.pid})`);
return {
serverProcess,
endpoint: await measureAndLog(() => waitForEndpoint(serverProcess, logger), 'waitForEndpoint(serverProcess)', logger)
};
}
async function launchBrowser(options: LaunchOptions, endpoint: string) {
const { logger, workspacePath, tracing, snapshots, headless } = options;
const browser = await measureAndLog(() => playwright[options.browser ?? 'chromium'].launch({
headless: headless ?? false,
timeout: 0
}), 'playwright#launch', logger);
browser.on('disconnected', () => logger.log(`Playwright: browser disconnected`));
const context = await measureAndLog(() => browser.newContext(), 'browser.newContext', logger);
if (tracing) {
try {
await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots }), 'context.tracing.start()', logger);
} catch (error) {
logger.log(`Playwright (Browser): Failed to start playwright tracing (${error})`); // do not fail the build when this fails
}
}
const page = await measureAndLog(() => context.newPage(), 'context.newPage()', logger);
await measureAndLog(() => page.setViewportSize({ width: 1200, height: 800 }), 'page.setViewportSize', logger);
if (options.verbose) {
context.on('page', () => logger.log(`Playwright (Browser): context.on('page')`));
context.on('requestfailed', e => logger.log(`Playwright (Browser): context.on('requestfailed') [${e.failure()?.errorText} for ${e.url()}]`));
page.on('console', e => logger.log(`Playwright (Browser): window.on('console') [${e.text()}]`));
page.on('dialog', () => logger.log(`Playwright (Browser): page.on('dialog')`));
page.on('domcontentloaded', () => logger.log(`Playwright (Browser): page.on('domcontentloaded')`));
page.on('load', () => logger.log(`Playwright (Browser): page.on('load')`));
page.on('popup', () => logger.log(`Playwright (Browser): page.on('popup')`));
page.on('framenavigated', () => logger.log(`Playwright (Browser): page.on('framenavigated')`));
page.on('requestfailed', e => logger.log(`Playwright (Browser): page.on('requestfailed') [${e.failure()?.errorText} for ${e.url()}]`));
}
page.on('pageerror', async (error) => logger.log(`Playwright (Browser) ERROR: page error: ${error}`));
page.on('crash', () => logger.log('Playwright (Browser) ERROR: page crash'));
page.on('close', () => logger.log('Playwright (Browser): page close'));
page.on('response', async (response) => {
if (response.status() >= 400) {
logger.log(`Playwright (Browser) ERROR: HTTP status ${response.status()} for ${response.url()}`);
}
});
const payloadParam = `[${[
'["enableProposedApi",""]',
'["skipWelcome", "true"]',
'["skipReleaseNotes", "true"]',
`["logLevel","${options.verbose ? 'trace' : 'info'}"]`
].join(',')}]`;
const gotoPromise = measureAndLog(() => page.goto(`${endpoint}&${workspacePath.endsWith('.code-workspace') ? 'workspace' : 'folder'}=${URI.file(workspacePath!).path}&payload=${payloadParam}`), 'page.goto()', logger);
const pageLoadedPromise = page.waitForLoadState('load');
await gotoPromise;
return { browser, context, page, pageLoadedPromise };
}
function waitForEndpoint(server: ChildProcess, logger: Logger): Promise<string> {
return new Promise<string>((resolve, reject) => {
let endpointFound = false;
server.stdout?.on('data', data => {
if (!endpointFound) {
logger.log(`[server] stdout: ${data}`); // log until endpoint found to diagnose issues
}
const matches = data.toString('ascii').match(/Web UI available at (.+)/);
if (matches !== null) {
endpointFound = true;
resolve(matches[1]);
}
});
server.stderr?.on('data', error => {
if (!endpointFound) {
logger.log(`[server] stderr: ${error}`); // log until endpoint found to diagnose issues
}
if (error.toString().indexOf('EADDRINUSE') !== -1) {
reject(new Error(error));
}
});
});
}
+336
View File
@@ -0,0 +1,336 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as playwright from '@playwright/test';
import type { Protocol } from 'playwright-core/types/protocol';
import { dirname, join } from 'path';
import { promises } from 'fs';
import { IWindowDriver } from './driver';
import { PageFunction } from 'playwright-core/types/structs';
import { measureAndLog } from './logger';
import { LaunchOptions } from './code';
import { teardown } from './processes';
import { ChildProcess } from 'child_process';
export class PlaywrightDriver {
private static traceCounter = 1;
private static screenShotCounter = 1;
private static readonly vscodeToPlaywrightKey: { [key: string]: string } = {
cmd: 'Meta',
ctrl: 'Control',
shift: 'Shift',
enter: 'Enter',
escape: 'Escape',
right: 'ArrowRight',
up: 'ArrowUp',
down: 'ArrowDown',
left: 'ArrowLeft',
home: 'Home',
esc: 'Escape'
};
constructor(
private readonly application: playwright.Browser | playwright.ElectronApplication,
private readonly context: playwright.BrowserContext,
private readonly page: playwright.Page,
private readonly serverProcess: ChildProcess | undefined,
private readonly whenLoaded: Promise<unknown>,
private readonly options: LaunchOptions
) {
}
async startTracing(name: string): Promise<void> {
if (!this.options.tracing) {
return; // tracing disabled
}
try {
await measureAndLog(() => this.context.tracing.startChunk({ title: name }), `startTracing for ${name}`, this.options.logger);
} catch (error) {
// Ignore
}
}
async stopTracing(name: string, persist: boolean): Promise<void> {
if (!this.options.tracing) {
return; // tracing disabled
}
try {
let persistPath: string | undefined = undefined;
if (persist) {
persistPath = join(this.options.logsPath, `playwright-trace-${PlaywrightDriver.traceCounter++}-${name.replace(/\s+/g, '-')}.zip`);
}
await measureAndLog(() => this.context.tracing.stopChunk({ path: persistPath }), `stopTracing for ${name}`, this.options.logger);
// To ensure we have a screenshot at the end where
// it failed, also trigger one explicitly. Tracing
// does not guarantee to give us a screenshot unless
// some driver action ran before.
if (persist) {
await this.takeScreenshot(name);
}
} catch (error) {
// Ignore
}
}
async didFinishLoad(): Promise<void> {
await this.whenLoaded;
}
private _cdpSession: playwright.CDPSession | undefined;
async startCDP() {
if (this._cdpSession) {
return;
}
this._cdpSession = await this.page.context().newCDPSession(this.page);
}
async collectGarbage() {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
await this._cdpSession.send('HeapProfiler.collectGarbage');
}
async evaluate(options: Protocol.Runtime.evaluateParameters): Promise<Protocol.Runtime.evaluateReturnValue> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
return await this._cdpSession.send('Runtime.evaluate', options);
}
async releaseObjectGroup(parameters: Protocol.Runtime.releaseObjectGroupParameters): Promise<void> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
await this._cdpSession.send('Runtime.releaseObjectGroup', parameters);
}
async queryObjects(parameters: Protocol.Runtime.queryObjectsParameters): Promise<Protocol.Runtime.queryObjectsReturnValue> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
return await this._cdpSession.send('Runtime.queryObjects', parameters);
}
async callFunctionOn(parameters: Protocol.Runtime.callFunctionOnParameters): Promise<Protocol.Runtime.callFunctionOnReturnValue> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
return await this._cdpSession.send('Runtime.callFunctionOn', parameters);
}
async takeHeapSnapshot(): Promise<string> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
let snapshot = '';
const listener = (c: { chunk: string }) => {
snapshot += c.chunk;
};
this._cdpSession.addListener('HeapProfiler.addHeapSnapshotChunk', listener);
await this._cdpSession.send('HeapProfiler.takeHeapSnapshot');
this._cdpSession.removeListener('HeapProfiler.addHeapSnapshotChunk', listener);
return snapshot;
}
async getProperties(parameters: Protocol.Runtime.getPropertiesParameters): Promise<Protocol.Runtime.getPropertiesReturnValue> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
return await this._cdpSession.send('Runtime.getProperties', parameters);
}
private async takeScreenshot(name: string): Promise<void> {
try {
const persistPath = join(this.options.logsPath, `playwright-screenshot-${PlaywrightDriver.screenShotCounter++}-${name.replace(/\s+/g, '-')}.png`);
await measureAndLog(() => this.page.screenshot({ path: persistPath, type: 'png' }), 'takeScreenshot', this.options.logger);
} catch (error) {
// Ignore
}
}
async reload() {
await this.page.reload();
}
async exitApplication() {
// Stop tracing
try {
if (this.options.tracing) {
await measureAndLog(() => this.context.tracing.stop(), 'stop tracing', this.options.logger);
}
} catch (error) {
// Ignore
}
// Web: Extract client logs
if (this.options.web) {
try {
await measureAndLog(() => this.saveWebClientLogs(), 'saveWebClientLogs()', this.options.logger);
} catch (error) {
this.options.logger.log(`Error saving web client logs (${error})`);
}
}
// Web: exit via `close` method
if (this.options.web) {
try {
await measureAndLog(() => this.application.close(), 'playwright.close()', this.options.logger);
} catch (error) {
this.options.logger.log(`Error closing appliction (${error})`);
}
}
// Desktop: exit via `driver.exitApplication`
else {
try {
await measureAndLog(() => this.evaluateWithDriver(([driver]) => driver.exitApplication()), 'driver.exitApplication()', this.options.logger);
} catch (error) {
this.options.logger.log(`Error exiting appliction (${error})`);
}
}
// Server: via `teardown`
if (this.serverProcess) {
await measureAndLog(() => teardown(this.serverProcess!, this.options.logger), 'teardown server process', this.options.logger);
}
}
private async saveWebClientLogs(): Promise<void> {
const logs = await this.getLogs();
for (const log of logs) {
const absoluteLogsPath = join(this.options.logsPath, log.relativePath);
await promises.mkdir(dirname(absoluteLogsPath), { recursive: true });
await promises.writeFile(absoluteLogsPath, log.contents);
}
}
async sendKeybinding(keybinding: string, accept?: () => Promise<void> | void) {
const chords = keybinding.split(' ');
for (let i = 0; i < chords.length; i++) {
const chord = chords[i];
if (i > 0) {
await this.wait(100);
}
if (keybinding.startsWith('Alt') || keybinding.startsWith('Control') || keybinding.startsWith('Backspace')) {
await this.page.keyboard.press(keybinding);
return;
}
const keys = chord.split('+');
const keysDown: string[] = [];
for (let i = 0; i < keys.length; i++) {
if (keys[i] in PlaywrightDriver.vscodeToPlaywrightKey) {
keys[i] = PlaywrightDriver.vscodeToPlaywrightKey[keys[i]];
}
await this.page.keyboard.down(keys[i]);
keysDown.push(keys[i]);
}
while (keysDown.length > 0) {
await this.page.keyboard.up(keysDown.pop()!);
}
}
if (accept) {
await accept();
}
}
async click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined) {
const { x, y } = await this.getElementXY(selector, xoffset, yoffset);
await this.page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0));
}
async setValue(selector: string, text: string) {
return this.page.evaluate(([driver, selector, text]) => driver.setValue(selector, text), [await this.getDriverHandle(), selector, text] as const);
}
async getTitle() {
return this.page.title();
}
async isActiveElement(selector: string) {
return this.page.evaluate(([driver, selector]) => driver.isActiveElement(selector), [await this.getDriverHandle(), selector] as const);
}
async getElements(selector: string, recursive: boolean = false) {
return this.page.evaluate(([driver, selector, recursive]) => driver.getElements(selector, recursive), [await this.getDriverHandle(), selector, recursive] as const);
}
async getElementXY(selector: string, xoffset?: number, yoffset?: number) {
return this.page.evaluate(([driver, selector, xoffset, yoffset]) => driver.getElementXY(selector, xoffset, yoffset), [await this.getDriverHandle(), selector, xoffset, yoffset] as const);
}
async typeInEditor(selector: string, text: string) {
return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this.getDriverHandle(), selector, text] as const);
}
async getEditorSelection(selector: string) {
return this.page.evaluate(([driver, selector]) => driver.getEditorSelection(selector), [await this.getDriverHandle(), selector] as const);
}
async getTerminalBuffer(selector: string) {
return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this.getDriverHandle(), selector] as const);
}
async writeInTerminal(selector: string, text: string) {
return this.page.evaluate(([driver, selector, text]) => driver.writeInTerminal(selector, text), [await this.getDriverHandle(), selector, text] as const);
}
async getLocaleInfo() {
return this.evaluateWithDriver(([driver]) => driver.getLocaleInfo());
}
async getLocalizedStrings() {
return this.evaluateWithDriver(([driver]) => driver.getLocalizedStrings());
}
async getLogs() {
return this.page.evaluate(([driver]) => driver.getLogs(), [await this.getDriverHandle()] as const);
}
private async evaluateWithDriver<T>(pageFunction: PageFunction<IWindowDriver[], T>) {
return this.page.evaluate(pageFunction, [await this.getDriverHandle()]);
}
wait(ms: number): Promise<void> {
return wait(ms);
}
whenWorkbenchRestored(): Promise<void> {
return this.evaluateWithDriver(([driver]) => driver.whenWorkbenchRestored());
}
private async getDriverHandle(): Promise<playwright.JSHandle<IWindowDriver>> {
return this.page.evaluateHandle('window.driver');
}
}
export function wait(ms: number): Promise<void> {
return new Promise<void>(resolve => setTimeout(resolve, ms));
}
+80
View File
@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as playwright from '@playwright/test';
import type { LaunchOptions } from './code';
import { PlaywrightDriver } from './playwrightDriver';
import { IElectronConfiguration, resolveElectronConfiguration } from './electron';
import { measureAndLog } from './logger';
import { ChildProcess } from 'child_process';
export async function launch(options: LaunchOptions): Promise<{ electronProcess: ChildProcess; driver: PlaywrightDriver }> {
// Resolve electron config and update
const { electronPath, args, env } = await resolveElectronConfiguration(options);
args.push('--enable-smoke-test-driver');
// Launch electron via playwright
const { electron, context, page } = await launchElectron({ electronPath, args, env }, options);
const electronProcess = electron.process();
return {
electronProcess,
driver: new PlaywrightDriver(electron, context, page, undefined /* no server process */, Promise.resolve() /* Window is open already */, options)
};
}
async function launchElectron(configuration: IElectronConfiguration, options: LaunchOptions) {
const { logger, tracing, snapshots } = options;
const electron = await measureAndLog(() => playwright._electron.launch({
executablePath: configuration.electronPath,
args: configuration.args,
env: configuration.env as { [key: string]: string },
timeout: 0
}), 'playwright-electron#launch', logger);
let window = electron.windows()[0];
if (!window) {
window = await measureAndLog(() => electron.waitForEvent('window', { timeout: 0 }), 'playwright-electron#firstWindow', logger);
}
const context = window.context();
if (tracing) {
try {
await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots }), 'context.tracing.start()', logger);
} catch (error) {
logger.log(`Playwright (Electron): Failed to start playwright tracing (${error})`); // do not fail the build when this fails
}
}
if (options.verbose) {
electron.on('window', () => logger.log(`Playwright (Electron): electron.on('window')`));
electron.on('close', () => logger.log(`Playwright (Electron): electron.on('close')`));
context.on('page', () => logger.log(`Playwright (Electron): context.on('page')`));
context.on('requestfailed', e => logger.log(`Playwright (Electron): context.on('requestfailed') [${e.failure()?.errorText} for ${e.url()}]`));
window.on('dialog', () => logger.log(`Playwright (Electron): window.on('dialog')`));
window.on('domcontentloaded', () => logger.log(`Playwright (Electron): window.on('domcontentloaded')`));
window.on('load', () => logger.log(`Playwright (Electron): window.on('load')`));
window.on('popup', () => logger.log(`Playwright (Electron): window.on('popup')`));
window.on('framenavigated', () => logger.log(`Playwright (Electron): window.on('framenavigated')`));
window.on('requestfailed', e => logger.log(`Playwright (Electron): window.on('requestfailed') [${e.failure()?.errorText} for ${e.url()}]`));
}
window.on('console', e => logger.log(`Playwright (Electron): window.on('console') [${e.text()}]`));
window.on('pageerror', async (error) => logger.log(`Playwright (Electron) ERROR: page error: ${error}`));
window.on('crash', () => logger.log('Playwright (Electron) ERROR: page crash'));
window.on('close', () => logger.log('Playwright (Electron): page close'));
window.on('response', async (response) => {
if (response.status() >= 400) {
logger.log(`Playwright (Electron) ERROR: HTTP status ${response.status()} for ${response.url()}`);
}
});
return { electron, context, page: window };
}
+43
View File
@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
import { QuickAccess } from './quickaccess';
export const enum ProblemSeverity {
WARNING = 0,
ERROR = 1
}
export class Problems {
static PROBLEMS_VIEW_SELECTOR = '.panel .markers-panel';
constructor(private code: Code, private quickAccess: QuickAccess) { }
async showProblemsView(): Promise<any> {
await this.quickAccess.runCommand('workbench.panel.markers.view.focus');
await this.waitForProblemsView();
}
async hideProblemsView(): Promise<any> {
await this.quickAccess.runCommand('workbench.actions.view.problems');
await this.code.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR, el => !el);
}
async waitForProblemsView(): Promise<void> {
await this.code.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR);
}
static getSelectorInProblemsView(problemType: ProblemSeverity): string {
const selector = problemType === ProblemSeverity.WARNING ? 'codicon-warning' : 'codicon-error';
return `div[id="workbench.panel.markers"] .monaco-tl-contents .marker-icon .${selector}`;
}
static getSelectorInEditor(problemType: ProblemSeverity): string {
const selector = problemType === ProblemSeverity.WARNING ? 'squiggly-warning' : 'squiggly-error';
return `.view-overlays .cdr.${selector}`;
}
}
+34
View File
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChildProcess } from 'child_process';
import { promisify } from 'util';
import * as treekill from 'tree-kill';
import { Logger } from './logger';
export async function teardown(p: ChildProcess, logger: Logger, retryCount = 3): Promise<void> {
const pid = p.pid;
if (typeof pid !== 'number') {
return;
}
let retries = 0;
while (retries < retryCount) {
retries++;
try {
return await promisify(treekill)(pid);
} catch (error) {
try {
process.kill(pid, 0); // throws an exception if the process doesn't exist anymore
logger.log(`Error tearing down process (pid: ${pid}, attempt: ${retries}): ${error}`);
} catch (error) {
return; // Expected when process is gone
}
}
}
logger.log(`Gave up tearing down process client after ${retries} attempts...`);
}
+213
View File
@@ -0,0 +1,213 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const { decode_bytes } = require('@vscode/v8-heap-parser');
import { Code } from './code';
import { PlaywrightDriver } from './playwrightDriver';
export class Profiler {
constructor(private readonly code: Code) {
}
async checkObjectLeaks(classNames: string | string[], fn: () => Promise<void>): Promise<void> {
await this.code.driver.startCDP();
const classNamesArray = Array.isArray(classNames) ? classNames : [classNames];
const countsBefore = await getInstances(this.code.driver, classNamesArray);
await fn();
const countAfter = await getInstances(this.code.driver, classNamesArray);
const leaks: string[] = [];
for (const className of classNamesArray) {
const count = countAfter[className] ?? 0;
const countBefore = countsBefore[className] ?? 0;
if (count !== countBefore) {
leaks.push(`Leaked ${count - countBefore} ${className}`);
}
}
if (leaks.length > 0) {
throw new Error(leaks.join('\n'));
}
}
async checkHeapLeaks(classNames: string | string[], fn: () => Promise<void>): Promise<void> {
await this.code.driver.startCDP();
await fn();
const heapSnapshotAfter = await this.code.driver.takeHeapSnapshot();
const buff = Buffer.from(heapSnapshotAfter);
const graph = await decode_bytes(buff);
const counts: number[] = Array.from(graph.get_class_counts(classNames));
const leaks: string[] = [];
for (let i = 0; i < classNames.length; i++) {
if (counts[i] > 0) {
leaks.push(`Leaked ${counts[i]} ${classNames[i]}`);
}
}
if (leaks.length > 0) {
throw new Error(leaks.join('\n'));
}
}
}
/**
* Copied from src/vs/base/common/uuid.ts
*/
export function generateUuid(): string {
// use `randomUUID` if possible
if (typeof crypto.randomUUID === 'function') {
// see https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto
// > Although crypto is available on all windows, the returned Crypto object only has one
// > usable feature in insecure contexts: the getRandomValues() method.
// > In general, you should use this API only in secure contexts.
return crypto.randomUUID.bind(crypto)();
}
// prep-work
const _data = new Uint8Array(16);
const _hex: string[] = [];
for (let i = 0; i < 256; i++) {
_hex.push(i.toString(16).padStart(2, '0'));
}
// get data
crypto.getRandomValues(_data);
// set version bits
_data[6] = (_data[6] & 0x0f) | 0x40;
_data[8] = (_data[8] & 0x3f) | 0x80;
// print as string
let i = 0;
let result = '';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
return result;
}
/*---------------------------------------------------------------------------------------------
* The MIT License (MIT)
* Copyright (c) 2023-present, Simon Siefke
*
* This code is derived from https://github.com/SimonSiefke/vscode-memory-leak-finder
*--------------------------------------------------------------------------------------------*/
const getInstances = async (driver: PlaywrightDriver, classNames: string[]): Promise<{ [key: string]: number }> => {
await driver.collectGarbage();
const objectGroup = `og:${generateUuid()}`;
const prototypeDescriptor = await driver.evaluate({
expression: 'Object.prototype',
returnByValue: false,
objectGroup,
});
const objects = await driver.queryObjects({
prototypeObjectId: prototypeDescriptor.result.objectId!,
objectGroup,
});
const fnResult1 = await driver.callFunctionOn({
functionDeclaration: `function(){
const objects = this
const classNames = ${JSON.stringify(classNames)}
const nativeConstructors = [
Object,
Array,
Function,
Set,
Map,
WeakMap,
WeakSet,
RegExp,
Node,
HTMLScriptElement,
DOMRectReadOnly,
DOMRect,
HTMLHtmlElement,
Node,
DOMTokenList,
HTMLUListElement,
HTMLStyleElement,
HTMLDivElement,
HTMLCollection,
FocusEvent,
Promise,
HTMLLinkElement,
HTMLLIElement,
HTMLAnchorElement,
HTMLSpanElement,
ArrayBuffer,
Uint16Array,
HTMLLabelElement,
TrustedTypePolicy,
Uint8Array,
Uint32Array,
HTMLHeadingElement,
MediaQueryList,
HTMLDocument,
TextDecoder,
TextEncoder,
HTMLInputElement,
HTMLCanvasElement,
HTMLIFrameElement,
Int32Array,
CSSStyleDeclaration
]
const isNativeConstructor = object => {
return nativeConstructors.includes(object.constructor) ||
object.constructor.name === 'AsyncFunction' ||
object.constructor.name === 'GeneratorFunction' ||
object.constructor.name === 'AsyncGeneratorFunction'
}
const isInstance = (object) => {
return object && !isNativeConstructor(object)
}
const instances = objects.filter(isInstance)
const counts = Object.create(null)
for(const instance of instances){
const name=instance.constructor.name
if(classNames.includes(name)){
counts[name]||= 0
counts[name]++
}
}
return counts
}`,
objectId: objects.objects.objectId,
returnByValue: true,
objectGroup,
});
const returnObject = fnResult1.result.value;
await driver.releaseObjectGroup({ objectGroup: objectGroup });
return returnObject;
};
+242
View File
@@ -0,0 +1,242 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Editors } from './editors';
import { Code } from './code';
import { QuickInput } from './quickinput';
import { basename, isAbsolute } from 'path';
enum QuickAccessKind {
Files = 1,
Commands,
Symbols
}
export class QuickAccess {
constructor(private code: Code, private editors: Editors, private quickInput: QuickInput) { }
async openFileQuickAccessAndWait(searchValue: string, expectedFirstElementNameOrExpectedResultCount: string | number): Promise<void> {
// make sure the file quick access is not "polluted"
// with entries from the editor history when opening
await this.runCommand('workbench.action.clearEditorHistory');
const PollingStrategy = {
Stop: true,
Continue: false
};
let retries = 0;
let success = false;
while (++retries < 10) {
let retry = false;
try {
await this.openQuickAccessWithRetry(QuickAccessKind.Files, searchValue);
await this.quickInput.waitForQuickInputElements(elementNames => {
this.code.logger.log('QuickAccess: resulting elements: ', elementNames);
// Quick access seems to be still running -> retry
if (elementNames.length === 0) {
this.code.logger.log('QuickAccess: file search returned 0 elements, will continue polling...');
return PollingStrategy.Continue;
}
// Quick access does not seem healthy/ready -> retry
const firstElementName = elementNames[0];
if (firstElementName === 'No matching results') {
this.code.logger.log(`QuickAccess: file search returned "No matching results", will retry...`);
retry = true;
return PollingStrategy.Stop;
}
// Expected: number of results
if (typeof expectedFirstElementNameOrExpectedResultCount === 'number') {
if (elementNames.length === expectedFirstElementNameOrExpectedResultCount) {
success = true;
return PollingStrategy.Stop;
}
this.code.logger.log(`QuickAccess: file search returned ${elementNames.length} results but was expecting ${expectedFirstElementNameOrExpectedResultCount}, will retry...`);
retry = true;
return PollingStrategy.Stop;
}
// Expected: string
else {
if (firstElementName === expectedFirstElementNameOrExpectedResultCount) {
success = true;
return PollingStrategy.Stop;
}
this.code.logger.log(`QuickAccess: file search returned ${firstElementName} as first result but was expecting ${expectedFirstElementNameOrExpectedResultCount}, will retry...`);
retry = true;
return PollingStrategy.Stop;
}
});
} catch (error) {
this.code.logger.log(`QuickAccess: file search waitForQuickInputElements threw an error ${error}, will retry...`);
retry = true;
}
if (!retry) {
break;
}
await this.quickInput.closeQuickInput();
}
if (!success) {
if (typeof expectedFirstElementNameOrExpectedResultCount === 'string') {
throw new Error(`Quick open file search was unable to find '${expectedFirstElementNameOrExpectedResultCount}' after 10 attempts, giving up.`);
} else {
throw new Error(`Quick open file search was unable to find ${expectedFirstElementNameOrExpectedResultCount} result items after 10 attempts, giving up.`);
}
}
}
async openFile(path: string): Promise<void> {
if (!isAbsolute(path)) {
// we require absolute paths to get a single
// result back that is unique and avoid hitting
// the search process to reduce chances of
// search needing longer.
throw new Error('QuickAccess.openFile requires an absolute path');
}
const fileName = basename(path);
// quick access shows files with the basename of the path
await this.openFileQuickAccessAndWait(path, basename(path));
// open first element
await this.quickInput.selectQuickInputElement(0);
// wait for editor being focused
await this.editors.waitForActiveTab(fileName);
await this.editors.selectTab(fileName);
}
private async openQuickAccessWithRetry(kind: QuickAccessKind, value?: string): Promise<void> {
let retries = 0;
// Other parts of code might steal focus away from quickinput :(
while (retries < 5) {
try {
// Await for quick input widget opened
const accept = () => this.quickInput.waitForQuickInputOpened(10);
// Open via keybinding
switch (kind) {
case QuickAccessKind.Files:
await this.code.sendKeybinding(process.platform === 'darwin' ? 'cmd+p' : 'ctrl+p', accept);
break;
case QuickAccessKind.Symbols:
await this.code.sendKeybinding(process.platform === 'darwin' ? 'cmd+shift+o' : 'ctrl+shift+o', accept);
break;
case QuickAccessKind.Commands:
await this.code.sendKeybinding(process.platform === 'darwin' ? 'cmd+shift+p' : 'ctrl+shift+p', accept);
break;
}
break;
} catch (err) {
if (++retries > 5) {
throw new Error(`QuickAccess.openQuickAccessWithRetry(kind: ${kind}) failed: ${err}`);
}
// Retry
await this.code.sendKeybinding('escape');
}
}
// Type value if any
if (value) {
await this.quickInput.type(value);
}
}
async runCommand(commandId: string, options?: { keepOpen?: boolean; exactLabelMatch?: boolean }): Promise<void> {
const keepOpen = options?.keepOpen;
const exactLabelMatch = options?.exactLabelMatch;
const openCommandPalletteAndTypeCommand = async (): Promise<boolean> => {
// open commands picker
await this.openQuickAccessWithRetry(QuickAccessKind.Commands, `>${commandId}`);
// wait for best choice to be focused
await this.quickInput.waitForQuickInputElementFocused();
// Retry for as long as the command not found
const text = await this.quickInput.waitForQuickInputElementText();
if (text === 'No matching commands' || (exactLabelMatch && text !== commandId)) {
return false;
}
return true;
};
let hasCommandFound = await openCommandPalletteAndTypeCommand();
if (!hasCommandFound) {
this.code.logger.log(`QuickAccess: No matching commands, will retry...`);
await this.quickInput.closeQuickInput();
let retries = 0;
while (++retries < 5) {
hasCommandFound = await openCommandPalletteAndTypeCommand();
if (hasCommandFound) {
break;
} else {
this.code.logger.log(`QuickAccess: No matching commands, will retry...`);
await this.quickInput.closeQuickInput();
await this.code.wait(1000);
}
}
if (!hasCommandFound) {
throw new Error(`QuickAccess.runCommand(commandId: ${commandId}) failed to find command.`);
}
}
// wait and click on best choice
await this.quickInput.selectQuickInputElement(0, keepOpen);
}
async openQuickOutline(): Promise<void> {
let retries = 0;
while (++retries < 10) {
// open quick outline via keybinding
await this.openQuickAccessWithRetry(QuickAccessKind.Symbols);
const text = await this.quickInput.waitForQuickInputElementText();
// Retry for as long as no symbols are found
if (text === 'No symbol information for the file') {
this.code.logger.log(`QuickAccess: openQuickOutline indicated 'No symbol information for the file', will retry...`);
// close and retry
await this.quickInput.closeQuickInput();
continue;
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
export class QuickInput {
private static QUICK_INPUT = '.quick-input-widget';
private static QUICK_INPUT_INPUT = `${QuickInput.QUICK_INPUT} .quick-input-box input`;
private static QUICK_INPUT_ROW = `${QuickInput.QUICK_INPUT} .quick-input-list .monaco-list-row`;
private static QUICK_INPUT_FOCUSED_ELEMENT = `${QuickInput.QUICK_INPUT_ROW}.focused .monaco-highlighted-label`;
// Note: this only grabs the label and not the description or detail
private static QUICK_INPUT_ENTRY_LABEL = `${QuickInput.QUICK_INPUT_ROW} .quick-input-list-row > .monaco-icon-label .label-name`;
private static QUICK_INPUT_ENTRY_LABEL_SPAN = `${QuickInput.QUICK_INPUT_ROW} .monaco-highlighted-label`;
constructor(private code: Code) { }
async waitForQuickInputOpened(retryCount?: number): Promise<void> {
await this.code.waitForActiveElement(QuickInput.QUICK_INPUT_INPUT, retryCount);
}
async type(value: string): Promise<void> {
await this.code.waitForSetValue(QuickInput.QUICK_INPUT_INPUT, value);
}
async waitForQuickInputElementFocused(): Promise<void> {
await this.code.waitForTextContent(QuickInput.QUICK_INPUT_FOCUSED_ELEMENT);
}
async waitForQuickInputElementText(): Promise<string> {
return this.code.waitForTextContent(QuickInput.QUICK_INPUT_ENTRY_LABEL_SPAN);
}
async closeQuickInput(): Promise<void> {
await this.code.sendKeybinding('escape', () => this.waitForQuickInputClosed());
}
async waitForQuickInputElements(accept: (names: string[]) => boolean): Promise<void> {
await this.code.waitForElements(QuickInput.QUICK_INPUT_ENTRY_LABEL, false, els => accept(els.map(e => e.textContent)));
}
async waitForQuickInputClosed(): Promise<void> {
await this.code.waitForElement(QuickInput.QUICK_INPUT, r => !!r && r.attributes.style.indexOf('display: none;') !== -1);
}
async selectQuickInputElement(index: number, keepOpen?: boolean): Promise<void> {
await this.waitForQuickInputOpened();
for (let from = 0; from < index; from++) {
await this.code.sendKeybinding('down');
}
await this.code.sendKeybinding('enter', async () => {
if (!keepOpen) {
await this.waitForQuickInputClosed();
}
});
}
}
+84
View File
@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Viewlet } from './viewlet';
import { IElement } from './driver';
import { findElement, findElements, Code } from './code';
import { Quality } from './application';
const VIEWLET = 'div[id="workbench.view.scm"]';
const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`;
const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`;
const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`;
const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`;
const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`;
const SCM_RESOURCE_CLICK = (name: string) => `${SCM_RESOURCE} .monaco-icon-label[aria-label*="${name}"] .label-name`;
const SCM_RESOURCE_ACTION_CLICK = (name: string, actionName: string) => `${SCM_RESOURCE} .monaco-icon-label[aria-label*="${name}"] .actions .action-label[aria-label="${actionName}"]`;
interface Change {
name: string;
type: string;
actions: string[];
}
function toChange(element: IElement): Change {
const name = findElement(element, e => /\blabel-name\b/.test(e.className))!;
const type = element.attributes['data-tooltip'] || '';
const actionElementList = findElements(element, e => /\baction-label\b/.test(e.className));
const actions = actionElementList.map(e => e.attributes['title']);
return {
name: name.textContent || '',
type,
actions
};
}
export class SCM extends Viewlet {
constructor(code: Code) {
super(code);
}
async openSCMViewlet(): Promise<any> {
await this.code.sendKeybinding('ctrl+shift+g', async () => { await this.code.waitForElement(this._editContextSelector()); });
}
async waitForChange(name: string, type?: string): Promise<void> {
const func = (change: Change) => change.name === name && (!type || change.type === type);
await this.code.waitForElements(SCM_RESOURCE, true, elements => elements.some(e => func(toChange(e))));
}
async refreshSCMViewlet(): Promise<any> {
await this.code.waitAndClick(REFRESH_COMMAND);
}
async openChange(name: string): Promise<void> {
await this.code.waitAndClick(SCM_RESOURCE_CLICK(name));
}
async stage(name: string): Promise<void> {
await this.code.waitAndClick(SCM_RESOURCE_ACTION_CLICK(name, 'Stage Changes'));
await this.waitForChange(name, 'Index Modified');
}
async unstage(name: string): Promise<void> {
await this.code.waitAndClick(SCM_RESOURCE_ACTION_CLICK(name, 'Unstage Changes'));
await this.waitForChange(name, 'Modified');
}
async commit(message: string): Promise<void> {
await this.code.waitAndClick(this._editContextSelector());
await this.code.waitForActiveElement(this._editContextSelector());
await this.code.waitForSetValue(this._editContextSelector(), message);
await this.code.waitAndClick(COMMIT_COMMAND);
}
private _editContextSelector(): string {
return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT;
}
}
+158
View File
@@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Viewlet } from './viewlet';
import { Code } from './code';
const VIEWLET = '.search-view';
const INPUT = `${VIEWLET} .search-widget .search-container .monaco-inputbox textarea`;
const INCLUDE_INPUT = `${VIEWLET} .query-details .file-types.includes .monaco-inputbox input`;
const FILE_MATCH = (filename: string) => `${VIEWLET} .results .filematch[data-resource$="${filename}"]`;
async function retry(setup: () => Promise<any>, attempt: () => Promise<any>) {
let count = 0;
while (true) {
await setup();
try {
await attempt();
return;
} catch (err) {
if (++count > 5) {
throw err;
}
}
}
}
export class Search extends Viewlet {
constructor(code: Code) {
super(code);
}
async clearSearchResults(): Promise<void> {
await retry(
() => this.code.waitAndClick(`.sidebar .title-actions .codicon-search-clear-results`),
() => this.waitForNoResultText(10));
}
async openSearchViewlet(): Promise<any> {
const accept = () => this.waitForInputFocus(INPUT);
if (process.platform === 'darwin') {
await this.code.sendKeybinding('cmd+shift+f', accept);
} else {
await this.code.sendKeybinding('ctrl+shift+f', accept);
}
}
async getSearchTooltip(): Promise<any> {
const icon = await this.code.waitForElement(`.activitybar .action-label.codicon.codicon-search-view-icon`, (el) => !!el?.attributes?.['title']);
return icon.attributes['title'];
}
async searchFor(text: string): Promise<void> {
await this.clearSearchResults();
await this.waitForInputFocus(INPUT);
await this.code.waitForSetValue(INPUT, text);
await this.submitSearch();
}
async hasActivityBarMoved() {
await this.code.waitForElement('.activitybar');
const elementBoundingBox = await this.code.driver.getElementXY('.activitybar');
return elementBoundingBox !== null && elementBoundingBox.x === 48 && elementBoundingBox.y === 375;
}
async waitForPageUp(): Promise<void> {
await this.code.sendKeybinding('PageUp');
}
async waitForPageDown(): Promise<void> {
await this.code.sendKeybinding('PageDown');
}
async submitSearch(): Promise<void> {
await this.waitForInputFocus(INPUT);
await this.code.sendKeybinding('enter', async () => { await this.code.waitForElement(`${VIEWLET} .messages`); });
}
async setFilesToIncludeText(text: string): Promise<void> {
await this.waitForInputFocus(INCLUDE_INPUT);
await this.code.waitForSetValue(INCLUDE_INPUT, text || '');
}
async showQueryDetails(): Promise<void> {
await this.code.waitAndClick(`${VIEWLET} .query-details .more`);
}
async hideQueryDetails(): Promise<void> {
await this.code.waitAndClick(`${VIEWLET} .query-details.more .more`);
}
async removeFileMatch(filename: string, expectedText: string): Promise<void> {
const fileMatch = FILE_MATCH(filename);
// Retry this because the click can fail if the search tree is rerendered at the same time
await retry(
async () => {
await this.code.waitAndClick(fileMatch);
await this.code.waitAndClick(`${fileMatch} .action-label.codicon-search-remove`);
},
async () => this.waitForResultText(expectedText, 10));
}
async expandReplace(): Promise<void> {
await this.code.waitAndClick(`${VIEWLET} .search-widget .monaco-button.toggle-replace-button.codicon-search-hide-replace`);
}
async collapseReplace(): Promise<void> {
await this.code.waitAndClick(`${VIEWLET} .search-widget .monaco-button.toggle-replace-button.codicon-search-show-replace`);
}
async setReplaceText(text: string): Promise<void> {
await this.code.waitForSetValue(`${VIEWLET} .search-widget .replace-container .monaco-inputbox textarea[aria-label="Replace"]`, text);
}
async replaceFileMatch(filename: string, expectedText: string): Promise<void> {
const fileMatch = FILE_MATCH(filename);
// Retry this because the click can fail if the search tree is rerendered at the same time
await retry(
async () => {
await this.code.waitAndClick(fileMatch);
await this.code.waitAndClick(`${fileMatch} .action-label.codicon.codicon-search-replace-all`);
},
() => this.waitForResultText(expectedText, 10));
}
async waitForResultText(text: string, retryCount?: number): Promise<void> {
// The label can end with " - " depending on whether the search editor is enabled
await this.code.waitForTextContent(`${VIEWLET} .messages .message`, undefined, result => result.startsWith(text), retryCount);
}
async waitForNoResultText(retryCount?: number): Promise<void> {
await this.code.waitForTextContent(`${VIEWLET} .messages`, undefined, text => text === '' || text.startsWith('Search was canceled before any results could be found'), retryCount);
}
private async waitForInputFocus(selector: string): Promise<void> {
let retries = 0;
// other parts of code might steal focus away from input boxes :(
while (retries < 5) {
await this.code.waitAndClick(INPUT, 2, 2);
try {
await this.code.waitForActiveElement(INPUT, 10);
break;
} catch (err) {
if (++retries > 5) {
throw err;
}
}
}
}
}
+95
View File
@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Editor } from './editor';
import { Editors } from './editors';
import { Code } from './code';
import { QuickAccess } from './quickaccess';
import { Quality } from './application';
const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context';
const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea';
export class SettingsEditor {
constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { }
/**
* Write a single setting key value pair.
*
* Warning: You may need to set `editor.wordWrap` to `"on"` if this is called with a really long
* setting.
*/
async addUserSetting(setting: string, value: string): Promise<void> {
await this.openUserSettingsFile();
await this.code.sendKeybinding('right', () =>
this.editor.waitForEditorSelection('settings.json', (s) => this._acceptEditorSelection(this.code.quality, s)));
await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value},`);
await this.editors.saveOpenedFile();
}
/**
* Write several settings faster than multiple calls to {@link addUserSetting}.
*
* Warning: You will likely also need to set `editor.wordWrap` to `"on"` if `addUserSetting` is
* called after this in the test.
*/
async addUserSettings(settings: [key: string, value: string][]): Promise<void> {
await this.openUserSettingsFile();
await this.code.sendKeybinding('right', () =>
this.editor.waitForEditorSelection('settings.json', (s) => this._acceptEditorSelection(this.code.quality, s)));
await this.editor.waitForTypeInEditor('settings.json', settings.map(v => `"${v[0]}": ${v[1]},`).join(''));
await this.editors.saveOpenedFile();
}
async clearUserSettings(): Promise<void> {
await this.openUserSettingsFile();
await this.quickaccess.runCommand('editor.action.selectAll');
await this.code.sendKeybinding('Delete', async () => {
await this.editor.waitForEditorContents('settings.json', contents => contents === '');
});
await this.editor.waitForTypeInEditor('settings.json', `{`); // will auto close }
await this.editors.saveOpenedFile();
await this.quickaccess.runCommand('workbench.action.closeActiveEditor');
}
async openUserSettingsFile(): Promise<void> {
await this.quickaccess.runCommand('workbench.action.openSettingsJson');
await this.editor.waitForEditorFocus('settings.json', 1);
}
async openUserSettingsUI(): Promise<void> {
await this.quickaccess.runCommand('workbench.action.openSettings2');
await this.code.waitForActiveElement(this._editContextSelector());
}
async searchSettingsUI(query: string): Promise<void> {
await this.openUserSettingsUI();
await this.code.waitAndClick(this._editContextSelector());
if (process.platform === 'darwin') {
await this.code.sendKeybinding('cmd+a');
} else {
await this.code.sendKeybinding('ctrl+a');
}
await this.code.sendKeybinding('Delete', async () => {
await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent));
});
await this.code.waitForTypeInEditor(this._editContextSelector(), query);
await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found'));
}
private _editContextSelector() {
return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT;
}
private _acceptEditorSelection(quality: Quality, s: { selectionStart: number; selectionEnd: number }): boolean {
if (quality === Quality.Stable) {
return true;
}
return s.selectionStart === 1 && s.selectionEnd === 1;
}
}
+63
View File
@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
export const enum StatusBarElement {
BRANCH_STATUS = 0,
SYNC_STATUS = 1,
PROBLEMS_STATUS = 2,
SELECTION_STATUS = 3,
INDENTATION_STATUS = 4,
ENCODING_STATUS = 5,
EOL_STATUS = 6,
LANGUAGE_STATUS = 7
}
export class StatusBar {
private readonly mainSelector = 'footer[id="workbench.parts.statusbar"]';
constructor(private code: Code) { }
async waitForStatusbarElement(element: StatusBarElement): Promise<void> {
await this.code.waitForElement(this.getSelector(element));
}
async clickOn(element: StatusBarElement): Promise<void> {
await this.code.waitAndClick(this.getSelector(element));
}
async waitForEOL(eol: string): Promise<string> {
return this.code.waitForTextContent(this.getSelector(StatusBarElement.EOL_STATUS), eol);
}
async waitForStatusbarText(title: string, text: string): Promise<void> {
await this.code.waitForTextContent(`${this.mainSelector} .statusbar-item[aria-label="${title}"]`, text);
}
private getSelector(element: StatusBarElement): string {
switch (element) {
case StatusBarElement.BRANCH_STATUS:
return `.statusbar-item[id^="status.scm."] .codicon.codicon-git-branch`;
case StatusBarElement.SYNC_STATUS:
return `.statusbar-item[id^="status.scm."] .codicon.codicon-sync`;
case StatusBarElement.PROBLEMS_STATUS:
return `.statusbar-item[id="status.problems"]`;
case StatusBarElement.SELECTION_STATUS:
return `.statusbar-item[id="status.editor.selection"]`;
case StatusBarElement.INDENTATION_STATUS:
return `.statusbar-item[id="status.editor.indentation"]`;
case StatusBarElement.ENCODING_STATUS:
return `.statusbar-item[id="status.editor.encoding"]`;
case StatusBarElement.EOL_STATUS:
return `.statusbar-item[id="status.editor.eol"]`;
case StatusBarElement.LANGUAGE_STATUS:
return `.statusbar-item[id="status.editor.mode"]`;
default:
throw new Error(element);
}
}
}
+93
View File
@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Editor } from './editor';
import { Code } from './code';
import { QuickAccess } from './quickaccess';
import { Editors } from './editors';
import { QuickInput } from './quickinput';
import { Terminal } from './terminal';
import { wait } from './playwrightDriver';
interface ITaskConfigurationProperties {
label?: string;
type?: string;
command?: string;
identifier?: string;
group?: string;
isBackground?: boolean;
promptOnClose?: boolean;
icon?: { id?: string; color?: string };
hide?: boolean;
}
export enum TaskCommandId {
TerminalRename = 'workbench.action.terminal.rename'
}
export class Task {
constructor(private code: Code, private editor: Editor, private editors: Editors, private quickaccess: QuickAccess, private quickinput: QuickInput, private terminal: Terminal) {
}
async assertTasks(filter: string, expected: ITaskConfigurationProperties[], type: 'run' | 'configure') {
await this.code.sendKeybinding('right');
// TODO https://github.com/microsoft/vscode/issues/242535
await wait(100);
await this.editors.saveOpenedFile();
type === 'run' ? await this.quickaccess.runCommand('workbench.action.tasks.runTask', { keepOpen: true }) : await this.quickaccess.runCommand('workbench.action.tasks.configureTask', { keepOpen: true });
if (expected.length === 0) {
await this.quickinput.waitForQuickInputElements(e => e.length > 1 && e.every(label => label.trim() !== filter.trim()));
} else {
await this.quickinput.waitForQuickInputElements(e => e.length > 1 && e.some(label => label.trim() === filter.trim()));
}
if (expected.length > 0 && !expected[0].hide) {
// select the expected task
await this.quickinput.selectQuickInputElement(0, true);
// Continue without scanning the output
await this.quickinput.selectQuickInputElement(0);
if (expected[0].icon) {
await this.terminal.assertSingleTab({ color: expected[0].icon.color, icon: expected[0].icon.id || 'tools' });
}
}
await this.quickinput.closeQuickInput();
}
async configureTask(properties: ITaskConfigurationProperties) {
await this.quickaccess.openFileQuickAccessAndWait('tasks.json', 'tasks.json');
await this.quickinput.selectQuickInputElement(0);
await this.quickaccess.runCommand('editor.action.selectAll');
await this.code.sendKeybinding('Delete');
// TODO https://github.com/microsoft/vscode/issues/242535
await wait(100);
const taskStringLines: string[] = [
'{', // Brackets auto close
'"version": "2.0.0",',
'"tasks": [{' // Brackets auto close
];
for (let [key, value] of Object.entries(properties)) {
if (typeof value === 'object') {
value = JSON.stringify(value);
} else if (typeof value === 'boolean') {
value = value;
} else if (typeof value === 'string') {
value = `"${value}"`;
} else {
throw new Error('Unsupported task property value type');
}
taskStringLines.push(`"${key}": ${value},`);
}
for (const [i, line] of taskStringLines.entries()) {
await this.editor.waitForTypeInEditor('tasks.json', `${line}`);
if (i !== taskStringLines.length - 1) {
await this.code.sendKeybinding('Enter');
// TODO https://github.com/microsoft/vscode/issues/242535
await wait(100);
}
}
await this.editors.saveOpenedFile();
}
}
+339
View File
@@ -0,0 +1,339 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { QuickInput } from './quickinput';
import { Code } from './code';
import { QuickAccess } from './quickaccess';
import { IElement } from './driver';
import { wait } from './playwrightDriver';
export enum Selector {
TerminalView = `#terminal`,
CommandDecorationPlaceholder = `.terminal-command-decoration.codicon-terminal-decoration-incomplete`,
CommandDecorationSuccess = `.terminal-command-decoration.codicon-terminal-decoration-success`,
CommandDecorationError = `.terminal-command-decoration.codicon-terminal-decoration-error`,
Xterm = `#terminal .terminal-wrapper`,
XtermEditor = `.editor-instance .terminal-wrapper`,
TabsEntry = '.terminal-tabs-entry',
Description = '.label-description',
XtermFocused = '.terminal.xterm.focus',
PlusButton = '.codicon-plus',
EditorGroups = '.editor .split-view-view',
EditorTab = '.terminal-tab',
SingleTab = '.single-terminal-tab',
Tabs = '.tabs-list .monaco-list-row',
SplitButton = '.editor .codicon-split-horizontal',
XtermSplitIndex0 = '#terminal .terminal-groups-container .split-view-view:nth-child(1) .terminal-wrapper',
XtermSplitIndex1 = '#terminal .terminal-groups-container .split-view-view:nth-child(2) .terminal-wrapper',
Hide = '.hide'
}
/**
* Terminal commands that accept a value in a quick input.
*/
export enum TerminalCommandIdWithValue {
Rename = 'workbench.action.terminal.rename',
ChangeColor = 'workbench.action.terminal.changeColor',
ChangeIcon = 'workbench.action.terminal.changeIcon',
NewWithProfile = 'workbench.action.terminal.newWithProfile',
SelectDefaultProfile = 'workbench.action.terminal.selectDefaultShell',
AttachToSession = 'workbench.action.terminal.attachToSession',
WriteDataToTerminal = 'workbench.action.terminal.writeDataToTerminal'
}
/**
* Terminal commands that do not present a quick input.
*/
export enum TerminalCommandId {
Split = 'workbench.action.terminal.split',
KillAll = 'workbench.action.terminal.killAll',
Unsplit = 'workbench.action.terminal.unsplit',
Join = 'workbench.action.terminal.join',
Show = 'workbench.action.terminal.toggleTerminal',
CreateNewEditor = 'workbench.action.createTerminalEditor',
SplitEditor = 'workbench.action.createTerminalEditorSide',
MoveToPanel = 'workbench.action.terminal.moveToTerminalPanel',
MoveToEditor = 'workbench.action.terminal.moveToEditor',
NewWithProfile = 'workbench.action.terminal.newWithProfile',
SelectDefaultProfile = 'workbench.action.terminal.selectDefaultShell',
DetachSession = 'workbench.action.terminal.detachSession',
CreateNew = 'workbench.action.terminal.new'
}
interface TerminalLabel {
name?: string;
description?: string;
icon?: string;
color?: string;
}
type TerminalGroup = TerminalLabel[];
interface ICommandDecorationCounts {
placeholder: number;
success: number;
error: number;
}
export class Terminal {
constructor(private code: Code, private quickaccess: QuickAccess, private quickinput: QuickInput) { }
async runCommand(commandId: TerminalCommandId, expectedLocation?: 'editor' | 'panel'): Promise<void> {
const keepOpen = commandId === TerminalCommandId.Join;
await this.quickaccess.runCommand(commandId, { keepOpen });
if (keepOpen) {
await this.code.sendKeybinding('enter');
// TODO https://github.com/microsoft/vscode/issues/242535
await wait(100);
await this.quickinput.waitForQuickInputClosed();
}
switch (commandId) {
case TerminalCommandId.Show:
case TerminalCommandId.CreateNewEditor:
case TerminalCommandId.CreateNew:
case TerminalCommandId.NewWithProfile:
await this._waitForTerminal(expectedLocation === 'editor' || commandId === TerminalCommandId.CreateNewEditor ? 'editor' : 'panel');
break;
case TerminalCommandId.KillAll:
// HACK: Attempt to kill all terminals to clean things up, this is known to be flaky
// but the reason why isn't known. This is typically called in the after each hook,
// Since it's not actually required that all terminals are killed just continue on
// after 2 seconds.
await Promise.race([
this.code.waitForElements(Selector.Xterm, true, e => e.length === 0),
this.code.wait(2000)
]);
break;
}
}
async runCommandWithValue(commandId: TerminalCommandIdWithValue, value?: string, altKey?: boolean): Promise<void> {
const keepOpen = !!value || commandId === TerminalCommandIdWithValue.NewWithProfile || commandId === TerminalCommandIdWithValue.Rename || (commandId === TerminalCommandIdWithValue.SelectDefaultProfile && value !== 'PowerShell');
await this.quickaccess.runCommand(commandId, { keepOpen });
// Running the command should hide the quick input in the following frame, this next wait
// ensures that the quick input is opened again before proceeding to avoid a race condition
// where the enter keybinding below would close the quick input if it's triggered before the
// new quick input shows.
await this.quickinput.waitForQuickInputOpened();
if (value) {
await this.quickinput.type(value);
} else if (commandId === TerminalCommandIdWithValue.Rename) {
// Reset
await this.code.sendKeybinding('Backspace');
// TODO https://github.com/microsoft/vscode/issues/242535
await wait(100);
}
await this.code.wait(100);
await this.code.sendKeybinding(altKey ? 'Alt+Enter' : 'enter');
// TODO https://github.com/microsoft/vscode/issues/242535
await wait(100);
await this.quickinput.waitForQuickInputClosed();
if (commandId === TerminalCommandIdWithValue.NewWithProfile) {
await this._waitForTerminal();
}
}
async runCommandInTerminal(commandText: string, skipEnter?: boolean): Promise<void> {
await this.code.writeInTerminal(Selector.Xterm, commandText);
if (!skipEnter) {
await this.code.sendKeybinding('enter');
// TODO https://github.com/microsoft/vscode/issues/242535
await wait(100);
}
}
/**
* Creates a terminal using the new terminal command.
* @param expectedLocation The location to check the terminal for, defaults to panel.
*/
async createTerminal(expectedLocation?: 'editor' | 'panel'): Promise<void> {
await this.runCommand(TerminalCommandId.CreateNew, expectedLocation);
await this._waitForTerminal(expectedLocation);
}
/**
* Creates an empty terminal by opening a regular terminal and resetting its state such that it
* essentially acts like an Pseudoterminal extension API-based terminal. This can then be paired
* with `TerminalCommandIdWithValue.WriteDataToTerminal` to make more reliable tests.
*/
async createEmptyTerminal(expectedLocation?: 'editor' | 'panel'): Promise<void> {
await this.createTerminal(expectedLocation);
// Run a command to ensure the shell has started, this is used to ensure the shell's data
// does not leak into the "empty terminal"
await this.runCommandInTerminal('echo "initialized"');
await this.waitForTerminalText(buffer => buffer.some(line => line.startsWith('initialized')));
// Erase all content and reset cursor to top
await this.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${csi('2J')}${csi('H')}`);
// Force windows pty mode off; assume all sequences are rendered in correct position
if (process.platform === 'win32') {
await this.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${vsc('P;IsWindows=False')}`);
}
}
async assertEditorGroupCount(count: number): Promise<void> {
await this.code.waitForElements(Selector.EditorGroups, true, editorGroups => editorGroups && editorGroups.length === count);
}
async assertSingleTab(label: TerminalLabel, editor?: boolean): Promise<void> {
let regex = undefined;
if (label.name && label.description) {
regex = new RegExp(label.name + ' - ' + label.description);
} else if (label.name) {
regex = new RegExp(label.name);
}
await this.assertTabExpected(editor ? Selector.EditorTab : Selector.SingleTab, undefined, regex, label.icon, label.color);
}
async assertTerminalGroups(expectedGroups: TerminalGroup[]): Promise<void> {
let expectedCount = 0;
expectedGroups.forEach(g => expectedCount += g.length);
let index = 0;
while (index < expectedCount) {
for (let groupIndex = 0; groupIndex < expectedGroups.length; groupIndex++) {
const terminalsInGroup = expectedGroups[groupIndex].length;
let indexInGroup = 0;
const isSplit = terminalsInGroup > 1;
while (indexInGroup < terminalsInGroup) {
const instance = expectedGroups[groupIndex][indexInGroup];
const nameRegex = instance.name && isSplit ? new RegExp('\\s*[├┌└]\\s*' + instance.name) : instance.name ? new RegExp(/^\s*/ + instance.name) : undefined;
await this.assertTabExpected(undefined, index, nameRegex, instance.icon, instance.color, instance.description);
indexInGroup++;
index++;
}
}
}
}
async getTerminalGroups(): Promise<TerminalGroup[]> {
const tabCount = (await this.code.waitForElements(Selector.Tabs, true)).length;
const groups: TerminalGroup[] = [];
for (let i = 0; i < tabCount; i++) {
const title = await this.code.waitForElement(`${Selector.Tabs}[data-index="${i}"] ${Selector.TabsEntry}`, e => e?.textContent?.length ? e?.textContent?.length > 1 : false);
const description: IElement | undefined = await this.code.waitForElement(`${Selector.Tabs}[data-index="${i}"] ${Selector.TabsEntry} ${Selector.Description}`, () => true);
const label: TerminalLabel = {
name: title.textContent.replace(/^[├┌└]\s*/, ''),
description: description?.textContent
};
// It's a new group if the tab does not start with ├ or └
if (title.textContent.match(/^[├└]/)) {
groups[groups.length - 1].push(label);
} else {
groups.push([label]);
}
}
return groups;
}
async getSingleTabName(): Promise<string> {
const tab = await this.code.waitForElement(Selector.SingleTab, singleTab => !!singleTab && singleTab?.textContent.length > 1);
return tab.textContent;
}
private async assertTabExpected(selector?: string, listIndex?: number, nameRegex?: RegExp, icon?: string, color?: string, description?: string): Promise<void> {
if (listIndex) {
if (nameRegex) {
await this.code.waitForElement(`${Selector.Tabs}[data-index="${listIndex}"] ${Selector.TabsEntry}`, entry => !!entry && !!entry?.textContent.match(nameRegex));
if (description) {
await this.code.waitForElement(`${Selector.Tabs}[data-index="${listIndex}"] ${Selector.TabsEntry} ${Selector.Description}`, e => !!e && e.textContent === description);
}
}
if (color) {
await this.code.waitForElement(`${Selector.Tabs}[data-index="${listIndex}"] ${Selector.TabsEntry} .monaco-icon-label.terminal-icon-terminal_ansi${color}`);
}
if (icon) {
await this.code.waitForElement(`${Selector.Tabs}[data-index="${listIndex}"] ${Selector.TabsEntry} .codicon-${icon}`);
}
} else if (selector) {
if (nameRegex) {
await this.code.waitForElement(`${selector}`, singleTab => !!singleTab && !!singleTab?.textContent.match(nameRegex));
}
if (color) {
await this.code.waitForElement(`${selector}`, singleTab => !!singleTab && !!singleTab.className.includes(`terminal-icon-terminal_ansi${color}`));
}
if (icon) {
selector = selector === Selector.EditorTab ? selector : `${selector} .codicon`;
await this.code.waitForElement(`${selector}`, singleTab => !!singleTab && !!singleTab.className.includes(icon));
}
}
}
async assertTerminalViewHidden(): Promise<void> {
await this.code.waitForElement(Selector.TerminalView, result => result === undefined);
}
async assertCommandDecorations(expectedCounts?: ICommandDecorationCounts, customIcon?: { updatedIcon: string; count: number }, showDecorations?: 'both' | 'gutter' | 'overviewRuler' | 'never'): Promise<void> {
if (expectedCounts) {
const placeholderSelector = showDecorations === 'overviewRuler' ? `${Selector.CommandDecorationPlaceholder}${Selector.Hide}` : Selector.CommandDecorationPlaceholder;
await this.code.waitForElements(placeholderSelector, true, decorations => decorations && decorations.length === expectedCounts.placeholder);
const successSelector = showDecorations === 'overviewRuler' ? `${Selector.CommandDecorationSuccess}${Selector.Hide}` : Selector.CommandDecorationSuccess;
await this.code.waitForElements(successSelector, true, decorations => decorations && decorations.length === expectedCounts.success);
const errorSelector = showDecorations === 'overviewRuler' ? `${Selector.CommandDecorationError}${Selector.Hide}` : Selector.CommandDecorationError;
await this.code.waitForElements(errorSelector, true, decorations => decorations && decorations.length === expectedCounts.error);
}
if (customIcon) {
await this.code.waitForElements(`.terminal-command-decoration.codicon-${customIcon.updatedIcon}`, true, decorations => decorations && decorations.length === customIcon.count);
}
}
async clickPlusButton(): Promise<void> {
await this.code.waitAndClick(Selector.PlusButton);
}
async clickSplitButton(): Promise<void> {
await this.code.waitAndClick(Selector.SplitButton);
}
async clickSingleTab(): Promise<void> {
await this.code.waitAndClick(Selector.SingleTab);
}
async waitForTerminalText(accept: (buffer: string[]) => boolean, message?: string, splitIndex?: 0 | 1): Promise<void> {
try {
let selector: string = Selector.Xterm;
if (splitIndex !== undefined) {
selector = splitIndex === 0 ? Selector.XtermSplitIndex0 : Selector.XtermSplitIndex1;
}
await this.code.waitForTerminalBuffer(selector, accept);
} catch (err: any) {
if (message) {
throw new Error(`${message} \n\nInner exception: \n${err.message} `);
}
throw err;
}
}
async getPage(): Promise<any> {
return (this.code.driver as any).page;
}
/**
* Waits for the terminal to be focused and to contain content.
* @param expectedLocation The location to check the terminal for, defaults to panel.
*/
private async _waitForTerminal(expectedLocation?: 'editor' | 'panel'): Promise<void> {
await this.code.waitForElement(Selector.XtermFocused);
await this.code.waitForTerminalBuffer(expectedLocation === 'editor' ? Selector.XtermEditor : Selector.Xterm, lines => lines.some(line => line.length > 0));
}
}
function vsc(data: string) {
return setTextParams(`633;${data}`);
}
function setTextParams(data: string) {
return osc(`${data}\\x07`);
}
function osc(data: string) {
return `\\x1b]${data}`;
}
function csi(data: string) {
return `\\x1b[${data}`;
}
+15
View File
@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
export abstract class Viewlet {
constructor(protected code: Code) { }
async waitForTitle(fn: (title: string) => boolean): Promise<void> {
await this.code.waitForTextContent('.monaco-workbench .part.sidebar > .title > .title-label > h2', undefined, fn);
}
}
+71
View File
@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Explorer } from './explorer';
import { ActivityBar } from './activityBar';
import { QuickAccess } from './quickaccess';
import { QuickInput } from './quickinput';
import { Extensions } from './extensions';
import { Search } from './search';
import { Editor } from './editor';
import { SCM } from './scm';
import { Debug } from './debug';
import { StatusBar } from './statusbar';
import { Problems } from './problems';
import { SettingsEditor } from './settings';
import { KeybindingsEditor } from './keybindings';
import { Editors } from './editors';
import { Code } from './code';
import { Terminal } from './terminal';
import { Notebook } from './notebook';
import { Localization } from './localization';
import { Task } from './task';
export interface Commands {
runCommand(command: string, options?: { exactLabelMatch?: boolean }): Promise<any>;
}
export class Workbench {
readonly quickaccess: QuickAccess;
readonly quickinput: QuickInput;
readonly editors: Editors;
readonly explorer: Explorer;
readonly activitybar: ActivityBar;
readonly search: Search;
readonly extensions: Extensions;
readonly editor: Editor;
readonly scm: SCM;
readonly debug: Debug;
readonly statusbar: StatusBar;
readonly problems: Problems;
readonly settingsEditor: SettingsEditor;
readonly keybindingsEditor: KeybindingsEditor;
readonly terminal: Terminal;
readonly notebook: Notebook;
readonly localization: Localization;
readonly task: Task;
constructor(code: Code) {
this.editors = new Editors(code);
this.quickinput = new QuickInput(code);
this.quickaccess = new QuickAccess(code, this.editors, this.quickinput);
this.explorer = new Explorer(code);
this.activitybar = new ActivityBar(code);
this.search = new Search(code);
this.extensions = new Extensions(code, this.quickaccess);
this.editor = new Editor(code, this.quickaccess);
this.scm = new SCM(code);
this.debug = new Debug(code, this.quickaccess, this.editors, this.editor);
this.statusbar = new StatusBar(code);
this.problems = new Problems(code, this.quickaccess);
this.settingsEditor = new SettingsEditor(code, this.editors, this.editor, this.quickaccess);
this.keybindingsEditor = new KeybindingsEditor(code);
this.terminal = new Terminal(code, this.quickaccess, this.quickinput);
this.notebook = new Notebook(this.quickaccess, this.quickinput, code);
this.localization = new Localization(code);
this.task = new Task(code, this.editor, this.editors, this.quickaccess, this.quickinput, this.terminal);
}
}
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const fs = require('fs');
const path = require('path');
const root = path.dirname(path.dirname(path.dirname(__dirname)));
const driverPath = path.join(root, 'src/vs/workbench/services/driver/common/driver.ts');
let contents = fs.readFileSync(driverPath, 'utf8');
contents = /\/\/\*START([\s\S]*)\/\/\*END/mi.exec(contents)[1].trim();
contents = contents.replace(/\bTPromise\b/g, 'Promise');
contents = `/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
${contents}
`;
const srcPath = path.join(path.dirname(__dirname), 'src');
const outPath = path.join(path.dirname(__dirname), 'out');
if (!fs.existsSync(outPath)) {
fs.mkdirSync(outPath);
}
fs.writeFileSync(path.join(srcPath, 'driver.d.ts'), contents);
fs.writeFileSync(path.join(outPath, 'driver.d.ts'), contents);
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const fs = require('fs');
const path = require('path');
const packageDir = path.dirname(__dirname);
const root = path.dirname(path.dirname(path.dirname(__dirname)));
const rootPackageJsonFile = path.join(root, 'package.json');
const thisPackageJsonFile = path.join(packageDir, 'package.json');
const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonFile, 'utf8'));
const thisPackageJson = JSON.parse(fs.readFileSync(thisPackageJsonFile, 'utf8'));
thisPackageJson.version = rootPackageJson.version;
fs.writeFileSync(thisPackageJsonFile, JSON.stringify(thisPackageJson, null, ' '));
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"strict": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"outDir": "out",
"sourceMap": true,
"declaration": true,
"skipLibCheck": true,
"lib": [
"esnext", // for #201187
"dom"
]
},
"exclude": [
"node_modules",
"out",
"tools"
]
}
+43
View File
@@ -0,0 +1,43 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "Jxck/assert",
"repositoryUrl": "https://github.com/Jxck/assert",
"commitHash": "a617d24d4e752e4299a6de4f78b1c23bfa9c49e8"
}
},
"licenseDetail": [
"The MIT License (MIT)",
"",
"Copyright (c) 2011 Jxck",
"",
"Originally from node.js (http://nodejs.org)",
"Copyright Joyent, Inc.",
"",
"Permission is hereby granted, free of charge, to any person obtaining a copy",
"of this software and associated documentation files (the \"Software\"), to deal",
"in the Software without restriction, including without limitation the rights",
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
"copies of the Software, and to permit persons to whom the Software is",
"furnished to do so, subject to the following conditions:",
"",
"The above copyright notice and this permission notice shall be included in all",
"copies or substantial portions of the Software.",
"",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
],
"developmentDependency": true,
"license": "MIT",
"version": "1.0.0"
}
],
"version": 1
}
+5
View File
@@ -0,0 +1,5 @@
.DS_Store
npm-debug.log
Thumbs.db
node_modules/
out/
+29
View File
@@ -0,0 +1,29 @@
# Integration test
## Compile
Make sure to run the following commands to compile and install dependencies:
cd test/integration/browser
npm i
npm run compile
## Run (inside Electron)
scripts/test-integration.[sh|bat]
All integration tests run in an Electron instance. You can specify to run the tests against a real build by setting the environment variables `INTEGRATION_TEST_ELECTRON_PATH` and `VSCODE_REMOTE_SERVER_PATH` (if you want to include remote tests).
## Run (inside browser)
scripts/test-web-integration.[sh|bat] --browser [chromium|webkit] [--debug]
All integration tests run in a browser instance as specified by the command line arguments.
Add the `--debug` flag to see a browser window with the tests running.
**Note**: you can enable verbose logging of playwright library by setting a `DEBUG` environment variable before running the tests (<https://playwright.dev/docs/debug#verbose-api-logs>)
## Debug
All integration tests can be run and debugged from within VSCode (both Electron and Web) simply by selecting the related launch configuration and running them.
+227
View File
@@ -0,0 +1,227 @@
{
"name": "code-oss-dev-integration-test",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "code-oss-dev-integration-test",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"@types/node": "20.x",
"@types/rimraf": "^2.0.4",
"@types/tmp": "0.1.0",
"rimraf": "^2.6.1",
"tmp": "0.0.33",
"tree-kill": "1.2.2",
"vscode-uri": "^3.0.2"
}
},
"node_modules/@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
"node_modules/@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
"integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
"dev": true,
"dependencies": {
"@types/events": "*",
"@types/minimatch": "*",
"@types/node": "*"
}
},
"node_modules/@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"node_modules/@types/node": {
"version": "20.11.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
"integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/rimraf": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-2.0.4.tgz",
"integrity": "sha512-8gBudvllD2A/c0CcEX/BivIDorHFt5UI5m46TsNj8DjWCCTTZT74kEe4g+QsY7P/B9WdO98d82zZgXO/RQzu2Q==",
"dev": true,
"dependencies": {
"@types/glob": "*",
"@types/node": "*"
}
},
"node_modules/@types/tmp": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz",
"integrity": "sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c= sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18= sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/vscode-uri": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.3.tgz",
"integrity": "sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==",
"dev": true
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "code-oss-dev-integration-test",
"version": "0.1.0",
"license": "MIT",
"main": "./index.js",
"scripts": {
"compile": "node ../../../node_modules/typescript/bin/tsc"
},
"devDependencies": {
"@types/node": "20.x",
"@types/rimraf": "^2.0.4",
"@types/tmp": "0.1.0",
"rimraf": "^2.6.1",
"tmp": "0.0.33",
"tree-kill": "1.2.2",
"vscode-uri": "^3.0.2"
}
}
+232
View File
@@ -0,0 +1,232 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as cp from 'child_process';
import * as playwright from '@playwright/test';
import * as url from 'url';
import * as tmp from 'tmp';
import * as rimraf from 'rimraf';
import { URI } from 'vscode-uri';
import * as kill from 'tree-kill';
import * as minimist from 'minimist';
import { promisify } from 'util';
import { promises } from 'fs';
const root = path.join(__dirname, '..', '..', '..', '..');
const logsPath = path.join(root, '.build', 'logs', 'integration-tests-browser');
const args = minimist(process.argv.slice(2), {
string: [
// path to the workspace (folder or *.code-workspace file) to open in the test
'workspacePath',
// path to the extension to test
'extensionDevelopmentPath',
// path to the extension tests
'extensionTestsPath',
// browser in which integration tests should run
'browser',
],
boolean: [
'help',
// do not run browsers headless
'debug',
],
alias: {
h: 'help'
},
default: {
'browser': 'chromium'
}
});
if (args.help) {
console.error(`Integration test runner for VS Code in the browser
Usage: node integration-tests-browser/out/index.js [options]
--workspacePath <path> Path to the workspace (folder or *.code-workspace file) to open in the test
--extensionDevelopmentPath <path> Path to the extension to test
--extensionTestsPath <path> Path to the extension tests
--browser <browser> Browser in which integration tests should run
--debug Do not run browsers headless
--help Print this help message
`);
process.exit(1);
}
const width = 1200;
const height = 800;
type BrowserType = 'chromium' | 'firefox' | 'webkit';
async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise<void> {
const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug) });
const context = await browser.newContext();
const page = await context.newPage();
await page.setViewportSize({ width, height });
page.on('pageerror', async error => console.error(`Playwright ERROR: page error: ${error}`));
page.on('crash', page => console.error('Playwright ERROR: page crash'));
page.on('response', async response => {
if (response.status() >= 400) {
console.error(`Playwright ERROR: HTTP status ${response.status()} for ${response.url()}`);
}
});
page.on('console', async msg => {
try {
if (msg.type() === 'error' || msg.type() === 'warning') {
consoleLogFn(msg)(msg.text(), await Promise.all(msg.args().map(async arg => await arg.jsonValue())));
}
} catch (err) {
console.error('Error logging console', err);
}
});
page.on('requestfailed', e => {
console.error('Request Failed', e.url(), e.failure()?.errorText);
});
await page.exposeFunction('codeAutomationLog', (type: string, args: any[]) => {
console[type](...args);
});
await page.exposeFunction('codeAutomationExit', async (code: number, logs: Array<{ readonly relativePath: string; readonly contents: string }>) => {
try {
for (const log of logs) {
const absoluteLogsPath = path.join(logsPath, log.relativePath);
await promises.mkdir(path.dirname(absoluteLogsPath), { recursive: true });
await promises.writeFile(absoluteLogsPath, log.contents);
}
} catch (error) {
console.error(`Error saving web client logs (${error})`);
}
if (args.debug) {
return;
}
try {
await browser.close();
} catch (error) {
console.error(`Error when closing browser: ${error}`);
}
try {
await promisify(kill)(server.pid!);
} catch (error) {
console.error(`Error when killing server process tree (pid: ${server.pid}): ${error}`);
}
process.exit(code);
});
const host = endpoint.host;
const protocol = 'vscode-remote';
const testWorkspacePath = URI.file(path.resolve(args.workspacePath)).path;
const testExtensionUri = url.format({ pathname: URI.file(path.resolve(args.extensionDevelopmentPath)).path, protocol, host, slashes: true });
const testFilesUri = url.format({ pathname: URI.file(path.resolve(args.extensionTestsPath)).path, protocol, host, slashes: true });
const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","ef65ac1ba57f57f2a3961bfe94aa20481caca4c6"],["skipWelcome","true"]]`;
if (path.extname(testWorkspacePath) === '.code-workspace') {
await page.goto(`${endpoint.href}&workspace=${testWorkspacePath}&payload=${payloadParam}`);
} else {
await page.goto(`${endpoint.href}&folder=${testWorkspacePath}&payload=${payloadParam}`);
}
}
function consoleLogFn(msg: playwright.ConsoleMessage) {
const type = msg.type();
const candidate = console[type];
if (candidate) {
return candidate;
}
if (type === 'warning') {
return console.warn;
}
return console.log;
}
async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.UrlWithStringQuery; server: cp.ChildProcess }> {
// Ensure a tmp user-data-dir is used for the tests
const tmpDir = tmp.dirSync({ prefix: 't' });
const testDataPath = tmpDir.name;
process.once('exit', () => rimraf.sync(testDataPath));
const userDataDir = path.join(testDataPath, 'd');
const env = {
VSCODE_BROWSER: browserType,
...process.env
};
const serverArgs = ['--enable-proposed-api', '--disable-telemetry', '--server-data-dir', userDataDir, '--accept-server-license-terms', '--disable-workspace-trust'];
let serverLocation: string;
if (process.env.VSCODE_REMOTE_SERVER_PATH) {
const { serverApplicationName } = require(path.join(process.env.VSCODE_REMOTE_SERVER_PATH, 'product.json'));
serverLocation = path.join(process.env.VSCODE_REMOTE_SERVER_PATH, 'bin', `${serverApplicationName}${process.platform === 'win32' ? '.cmd' : ''}`);
if (args.debug) {
console.log(`Starting built server from '${serverLocation}'`);
}
} else {
serverLocation = path.join(root, `scripts/code-server.${process.platform === 'win32' ? 'bat' : 'sh'}`);
process.env.VSCODE_DEV = '1';
if (args.debug) {
console.log(`Starting server out of sources from '${serverLocation}'`);
}
}
const serverLogsPath = path.join(logsPath, 'server');
console.log(`Storing log files into '${serverLogsPath}'`);
serverArgs.push('--logsPath', serverLogsPath);
const stdio: cp.StdioOptions = args.debug ? 'pipe' : ['ignore', 'pipe', 'ignore'];
const shell: boolean = (process.platform === 'win32');
const serverProcess = cp.spawn(
serverLocation,
serverArgs,
{ env, stdio, shell }
);
if (args.debug) {
serverProcess.stderr!.on('data', error => console.log(`Server stderr: ${error}`));
serverProcess.stdout!.on('data', data => console.log(`Server stdout: ${data}`));
}
process.on('exit', () => serverProcess.kill());
process.on('SIGINT', () => {
serverProcess.kill();
process.exit(128 + 2); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events
});
process.on('SIGTERM', () => {
serverProcess.kill();
process.exit(128 + 15); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events
});
return new Promise(c => {
serverProcess.stdout!.on('data', data => {
const matches = data.toString('ascii').match(/Web UI available at (.+)/);
if (matches !== null) {
c({ endpoint: url.parse(matches[1]), server: serverProcess });
}
});
});
}
launchServer(args.browser).then(async ({ endpoint, server }) => {
return runTestsInBrowser(args.browser, endpoint, server);
}, error => {
console.error(error);
process.exit(1);
});
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": false,
"removeComments": false,
"preserveConstEnums": true,
"target": "es2020",
"strict": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"outDir": "out",
"sourceMap": true,
"skipLibCheck": true,
"lib": [
"esnext", // for #201187
"dom"
]
},
"exclude": [
"node_modules"
]
}
+10
View File
@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MochaOptions } from 'mocha';
export function configure(opts: MochaOptions): void;
export function run(testsRoot: string[], clb: (error: Error | undefined, failures: number | undefined) => void): void;
+51
View File
@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const paths = require('path');
const glob = require('glob');
// Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY
// Since we are not running in a tty environment, we just implement the method statically
const tty = require('tty');
// @ts-ignore
if (!tty.getWindowSize) {
// @ts-ignore
tty.getWindowSize = function () { return [80, 75]; };
}
const Mocha = require('mocha');
let mocha = new Mocha({
ui: 'tdd',
color: true
});
exports.configure = function configure(opts) {
mocha = new Mocha(opts);
};
exports.run = function run(testsRoot, clb) {
// Enable source map support
require('source-map-support').install();
// Glob test files
glob('**/**.test.js', { cwd: testsRoot }, function (error, files) {
if (error) {
return clb(error);
}
try {
// Fill into Mocha
files.forEach(function (f) { return mocha.addFile(paths.join(testsRoot, f)); });
// Run the tests
mocha.run(function (failures) {
clb(null, failures);
});
}
catch (error) {
return clb(error);
}
});
};
+40
View File
@@ -0,0 +1,40 @@
<html>
<head>
<meta charset="utf-8">
<title>Leak Test Bed</title>
</head>
<body>
<button id="alloc">Alloc</button>
<button id="dealloc">Dealloc</button>
<script src="/static/vs/loader.js"></script>
<script>
require.config({ baseUrl: '/static' });
require(['vs/base/browser/event'], ({ domEvent }) => {
let event;
let listener;
function alloc() {
event = domEvent(document.body, 'mousemove');
listener = event(e => console.log(e));
}
function dealloc() {
listener.dispose();
listener = null;
event = null;
}
const allocBtn = document.getElementById('alloc');
allocBtn.onclick = alloc;
const deallocBtn = document.getElementById('dealloc');
deallocBtn.onclick = dealloc;
});
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
{
"name": "leaks",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"koa": "^2.13.1",
"koa-mount": "^4.0.0",
"koa-static": "^5.0.0"
}
}
+16
View File
@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const Koa = require('koa');
const serve = require('koa-static');
const mount = require('koa-mount');
const app = new Koa();
app.use(serve('.'));
app.use(mount('/static', serve('../../out')));
app.listen(3000);
console.log('👉 http://localhost:3000');
+4
View File
@@ -0,0 +1,4 @@
/dist/**/*.js
/dist/**/*.ttf
/out/
/esm-check/out/
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/mocharc",
"ui": "bdd",
"timeout": 10000
}
+2
View File
@@ -0,0 +1,2 @@
legacy-peer-deps="true"
timeout=180000
+13
View File
@@ -0,0 +1,13 @@
# Monaco Editor Test
This directory contains scripts that are used to smoke test the Monaco Editor distribution.
## Setup & Bundle
$test/monaco> npm i
$test/monaco> npm run bundle
## Compile and run tests
$test/monaco> npm run compile
$test/monaco> npm run test
+30
View File
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as monaco from 'monaco-editor-core';
self.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
return './editorWebWorkerMain.bundle.js';
}
};
window.instance = monaco.editor.create(document.getElementById('container'), {
value: [
'from banana import *',
'',
'class Monkey:',
' # Bananas the monkey can eat.',
' capacity = 10',
' def eat(self, N):',
' \'\'\'Make the monkey eat N bananas!\'\'\'',
' capacity = capacity - N*banana.size',
'',
' def feeding_frenzy(self):',
' eat(9.25)',
' return "Yum yum"',
].join('\n'),
language: 'python'
});
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
<div id="container" style="width:800px;height:600px;border:1px solid #ccc"></div>
<script src="./core.bundle.js"></script>
</body>
</html>
+96
View File
@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
const fs = require('fs');
const path = require('path');
const util = require('../../../build/lib/util');
const playwright = require('@playwright/test');
const yaserver = require('yaserver');
const http = require('http');
const DEBUG_TESTS = false;
const SRC_DIR = path.join(__dirname, '../../../out-monaco-editor-core/esm');
const DST_DIR = path.join(__dirname, './out');
const PORT = 8562;
run();
async function run() {
await extractSourcesWithoutCSS();
const server = await startServer();
const browser = await playwright['chromium'].launch({
headless: !DEBUG_TESTS,
devtools: DEBUG_TESTS
// slowMo: DEBUG_TESTS ? 2000 : 0
});
const page = await browser.newPage({
viewport: {
width: 800,
height: 600
}
});
page.on('pageerror', (e) => {
console.error(`[esm-check] A page error occurred:`);
console.error(e);
process.exit(1);
});
const URL = `http://127.0.0.1:${PORT}/index.html`;
console.log(`[esm-check] Navigating to ${URL}`);
const response = await page.goto(URL);
if (!response) {
console.error(`[esm-check] Missing response.`);
process.exit(1);
}
if (response.status() !== 200) {
console.error(`[esm-check] Response status ${response.status()} is not 200 .`);
process.exit(1);
}
console.log(`[esm-check] All appears good.`);
await page.close();
await browser.close();
server.close();
}
/**
* @returns {Promise<http.Server>}
*/
async function startServer() {
const staticServer = await yaserver.createServer({ rootDir: __dirname });
return new Promise((resolve, reject) => {
const server = http.createServer((request, response) => {
return staticServer.handle(request, response);
});
server.listen(PORT, '127.0.0.1', () => {
resolve(server);
});
});
}
async function extractSourcesWithoutCSS() {
await util.rimraf(DST_DIR);
const files = util.rreddir(SRC_DIR);
for (const file of files) {
const srcFilename = path.join(SRC_DIR, file);
if (!/\.js$/.test(srcFilename)) {
continue;
}
const dstFilename = path.join(DST_DIR, file);
let contents = fs.readFileSync(srcFilename).toString();
contents = contents.replace(/import '[^']+\.css';/g, '');
util.ensureDir(path.dirname(dstFilename));
fs.writeFileSync(dstFilename, contents);
}
}
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
<div id="container" style="width: 800px; height: 600px; border: 1px solid #ccc"></div>
<script type="module" src="index.js"></script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// eslint-disable-next-line local/code-no-standalone-editor
import * as monaco from './out/vs/editor/editor.main.js';
monaco.editor.create(document.getElementById('container'), {
value: 'Hello world'
});
+138
View File
@@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as playwright from '@playwright/test';
import { assert } from 'chai';
const PORT = 8563;
const TIMEOUT = 20 * 1000;
const APP = `http://127.0.0.1:${PORT}/dist/core.html`;
let browser: playwright.Browser;
let page: playwright.Page;
type BrowserType = 'chromium' | 'firefox' | 'webkit';
const browserType: BrowserType = process.env.BROWSER as BrowserType || 'chromium';
before(async function () {
this.timeout(TIMEOUT);
console.log(`Starting browser: ${browserType}`);
browser = await playwright[browserType].launch({
headless: process.argv.includes('--headless'),
});
});
after(async function () {
this.timeout(TIMEOUT);
await browser.close();
});
const pageErrors: any[] = [];
beforeEach(async function () {
this.timeout(TIMEOUT);
page = await browser.newPage({
viewport: {
width: 800,
height: 600
}
});
pageErrors.length = 0;
page.on('pageerror', (e) => {
console.log(e);
pageErrors.push(e);
});
page.on('pageerror', (e) => {
console.log(e);
pageErrors.push(e);
});
});
afterEach(async () => {
await page.close();
for (const e of pageErrors) {
throw e;
}
});
describe('API Integration Tests', function (): void {
this.timeout(TIMEOUT);
beforeEach(async () => {
await page.goto(APP);
});
it('`monaco` is not exposed as global', async function (): Promise<any> {
assert.strictEqual(await page.evaluate(`typeof monaco`), 'undefined');
});
it('Focus and Type', async function (): Promise<any> {
await page.evaluate(`
(function () {
instance.focus();
instance.trigger('keyboard', 'cursorHome');
instance.trigger('keyboard', 'type', {
text: 'a'
});
})()
`);
assert.strictEqual(await page.evaluate(`instance.getModel().getLineContent(1)`), 'afrom banana import *');
});
it('Type and Undo', async function (): Promise<any> {
await page.evaluate(`
(function () {
instance.focus();
instance.trigger('keyboard', 'cursorHome');
instance.trigger('keyboard', 'type', {
text: 'a'
});
instance.getModel().undo();
})()
`);
assert.strictEqual(await page.evaluate(`instance.getModel().getLineContent(1)`), 'from banana import *');
});
it('Multi Cursor', async function (): Promise<any> {
await page.evaluate(`
(function () {
instance.focus();
instance.trigger('keyboard', 'editor.action.insertCursorBelow');
instance.trigger('keyboard', 'editor.action.insertCursorBelow');
instance.trigger('keyboard', 'editor.action.insertCursorBelow');
instance.trigger('keyboard', 'editor.action.insertCursorBelow');
instance.trigger('keyboard', 'editor.action.insertCursorBelow');
instance.trigger('keyboard', 'type', {
text: '# '
});
instance.focus();
})()
`);
await page.waitForTimeout(1000);
assert.deepStrictEqual(await page.evaluate(`
[
instance.getModel().getLineContent(1),
instance.getModel().getLineContent(2),
instance.getModel().getLineContent(3),
instance.getModel().getLineContent(4),
instance.getModel().getLineContent(5),
instance.getModel().getLineContent(6),
instance.getModel().getLineContent(7),
]
`), [
'# from banana import *',
'# ',
'# class Monkey:',
'# # Bananas the monkey can eat.',
'# capacity = 10',
'# def eat(self, N):',
'\t\t\'\'\'Make the monkey eat N bananas!\'\'\''
]);
});
});
+107
View File
@@ -0,0 +1,107 @@
{
"name": "test-monaco",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "test-monaco",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@types/chai": "^4.2.14",
"chai": "^4.2.0",
"warnings-to-errors-webpack-plugin": "^2.3.0"
}
},
"node_modules/@types/chai": {
"version": "4.2.14",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.14.tgz",
"integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==",
"dev": true
},
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/chai": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
"dev": true,
"dependencies": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^3.0.1",
"get-func-name": "^2.0.0",
"pathval": "^1.1.0",
"type-detect": "^4.0.5"
},
"engines": {
"node": ">=4"
}
},
"node_modules/check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"dependencies": {
"type-detect": "^4.0.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/warnings-to-errors-webpack-plugin": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/warnings-to-errors-webpack-plugin/-/warnings-to-errors-webpack-plugin-2.3.0.tgz",
"integrity": "sha512-fzOyw+Ctr2MqJ+4UXKobDiOLzMMSBAdvGMWvZ4NRgTBCofAL2mmdfr6/Of3Bqz9Sq6R6xT5dLs6nnLLCtmppeQ==",
"dev": true,
"peerDependencies": {
"webpack": "^2.2.0-rc || ^3 || ^4 || ^5"
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "test-monaco",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"scripts": {
"compile": "node ../../node_modules/typescript/bin/tsc",
"bundle-webpack": "node ../../node_modules/webpack/bin/webpack --config ./webpack.config.js --bail",
"esm-check": "node esm-check/esm-check.js",
"test": "node runner.js"
},
"devDependencies": {
"@types/chai": "^4.2.14",
"chai": "^4.2.0",
"warnings-to-errors-webpack-plugin": "^2.3.0"
}
}
+52
View File
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const yaserver = require('yaserver');
const http = require('http');
const cp = require('child_process');
const PORT = 8563;
yaserver.createServer({
rootDir: __dirname
}).then((staticServer) => {
const server = http.createServer((request, response) => {
return staticServer.handle(request, response);
});
server.listen(PORT, '127.0.0.1', () => {
runTests().then(() => {
console.log(`All good`);
process.exit(0);
}, (err) => {
console.error(err);
process.exit(1);
});
});
});
function runTests() {
return (
runTest('chromium')
.then(() => runTest('firefox'))
// .then(() => runTest('webkit'))
);
}
function runTest(browser) {
return new Promise((resolve, reject) => {
const proc = cp.spawn('node', ['../../node_modules/mocha/bin/mocha', 'out/*.test.js', '--headless'], {
env: { BROWSER: browser, ...process.env },
stdio: 'inherit'
});
proc.on('error', reject);
proc.on('exit', (code) => {
if (code === 0) {
resolve();
} else {
reject(code);
}
});
});
}
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2016",
"strict": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"outDir": "out",
"sourceMap": true,
"skipLibCheck": true,
"declaration": true,
"lib": [
"esnext", // for #201187
"dom"
]
},
"exclude": [
"node_modules",
"out",
"tools"
]
}
+62
View File
@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const WarningsToErrorsPlugin = require('warnings-to-errors-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
'core': './core.js',
'editorWebWorkerMain': '../../out-monaco-editor-core/esm/vs/editor/common/services/editorWebWorkerMain.js',
},
output: {
globalObject: 'self',
filename: '[name].bundle.js',
path: path.resolve(__dirname, './dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/'
}
}
]
}
]
},
resolve: {
alias: {
'monaco-editor-core': path.resolve(__dirname, '../../out-monaco-editor-core/esm/vs/editor/editor.main.js'),
}
},
stats: {
all: false,
modules: true,
errors: true,
warnings: true,
// our additional options
moduleTrace: true,
errorDetails: true,
chunks: true
},
plugins: [
new WarningsToErrorsPlugin()
],
optimization: {
// Without it, CI fails, which indicates a webpack minification bug.
minimize: false,
},
};
+3
View File
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
+9
View File
@@ -0,0 +1,9 @@
.DS_Store
npm-debug.log
Thumbs.db
node_modules/
out/
keybindings.*.json
test_data/
src/vscode/driver.d.ts
vscode-server*/
+15
View File
@@ -0,0 +1,15 @@
# VS Code Smoke Tests Failures History
This file contains a history of smoke test failures which could be avoided if particular techniques were used in the test (e.g. binding test elements with HTML5 `data-*` attribute).
To better understand what can be employed in smoke test to ensure its stability, it is important to understand patterns that led to smoke test breakage. This markdown is a result of work on [this issue](https://github.com/microsoft/vscode/issues/27906).
## Log
1. This following change led to the smoke test failure because DOM element's attribute `a[title]` was changed:
[eac49a3](https://github.com/microsoft/vscode/commit/eac49a321b84cb9828430e9dcd3f34243a3480f7)
This attribute was used in the smoke test to grab the contents of SCM part in status bar:
[0aec2d6](https://github.com/microsoft/vscode/commit/0aec2d6838b5e65cc74c33b853ffbd9fa191d636)
2. To be continued...
+86
View File
@@ -0,0 +1,86 @@
# VS Code Smoke Test
Make sure you are on **Node v12.x**.
## Quick Overview
```bash
# Build extensions in the VS Code repo (if needed)
npm i && npm run compile
# Dev (Electron)
npm run smoketest
# Dev (Web - Must be run on distro)
npm run smoketest -- --web --browser [chromium|webkit]
# Build (Electron)
npm run smoketest -- --build <path to latest version>
example: npm run smoketest -- --build /Applications/Visual\ Studio\ Code\ -\ Insiders.app
# Build (Web - read instructions below)
npm run smoketest -- --build <path to server web build (ends in -web)> --web --browser [chromium|webkit]
# Remote (Electron)
npm run smoketest -- --build <path to latest version> --remote
```
\* This step is necessary only when running without `--build` and OSS doesn't already exist in the `.build/electron` directory.
### Running for a release (Endgame)
You must always run the smoketest version that matches the release you are testing. So, if you want to run the smoketest for a release build (e.g. `release/1.22`), you need to check out that version of the smoke tests too:
```bash
git fetch
git checkout release/1.22
npm i && npm run compile
cd test/smoke
npm i
```
#### Web
There is no support for testing an old version to a new one yet.
Instead, simply configure the `--build` command line argument to point to the absolute path of the extracted server web build folder (e.g. `<rest of path here>/vscode-server-darwin-x64-web` for macOS). The server web build is available from the builds page (see previous subsection).
**macOS**: if you have downloaded the server with web bits, make sure to run the following command before unzipping it to avoid security issues on startup:
```bash
xattr -d com.apple.quarantine <path to server with web folder zip>
```
**Note**: make sure to point to the server that includes the client bits!
### Debug
- `--verbose` logs all the low level driver calls made to Code;
- `-f PATTERN` (alias `-g PATTERN`) filters the tests to be run. You can also use pretty much any mocha argument;
- `--headless` will run playwright in headless mode when `--web` is used.
**Note**: you can enable verbose logging of playwright library by setting a `DEBUG` environment variable before running the tests (<https://playwright.dev/docs/debug#verbose-api-logs>), for example to `pw:browser`.
### Develop
```bash
cd test/smoke
npm run watch
```
## Troubleshooting
### Error: Could not get a unique tmp filename, max tries reached
On Windows, check for the folder `C:\Users\<username>\AppData\Local\Temp\t`. If this folder exists, the `tmp` module can't run properly, resulting in the error above. In this case, delete the `t` folder.
## Pitfalls
- Beware of workbench **state**. The tests within a single suite will share the same state.
- Beware of **singletons**. This evil can, and will, manifest itself under the form of FS paths, TCP ports, IPC handles. Whenever writing a test, or setting up more smoke test architecture, make sure it can run simultaneously with any other tests and even itself. All test suites should be able to run many times in parallel.
- Beware of **focus**. **Never** depend on DOM elements having focus using `.focused` classes or `:focus` pseudo-classes, since they will lose that state as soon as another window appears on top of the running VS Code window. A safe approach which avoids this problem is to use the `waitForActiveElement` API. Many tests use this whenever they need to wait for a specific element to _have focus_.
- Beware of **timing**. You need to read from or write to the DOM... but is it the right time to do that? Can you 100% guarantee that `input` box will be visible at that point in time? Or are you just hoping that it will be so? Hope is your worst enemy in UI tests. Example: just because you triggered Quick Access with `F1`, it doesn't mean that it's open and you can just start typing; you must first wait for the input element to be in the DOM as well as be the current active element.
- Beware of **waiting**. **Never** wait longer than a couple of seconds for anything, unless it's justified. Think of it as a human using Code. Would a human take 10 minutes to run through the Search viewlet smoke test? Then, the computer should even be faster. **Don't** use `setTimeout` just because. Think about what you should wait for in the DOM to be ready and wait for that instead.
+962
View File
@@ -0,0 +1,962 @@
{
"name": "code-oss-dev-smoke-test",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "code-oss-dev-smoke-test",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"ncp": "^2.0.0",
"node-fetch": "^2.6.7",
"rimraf": "3.0.2"
},
"devDependencies": {
"@types/mocha": "^9.1.1",
"@types/ncp": "2.0.1",
"@types/node": "20.x",
"@types/node-fetch": "^2.5.10",
"@types/rimraf": "3.0.2",
"npm-run-all": "^4.1.5"
}
},
"node_modules/@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
"node_modules/@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
"integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
"dev": true,
"dependencies": {
"@types/events": "*",
"@types/minimatch": "*",
"@types/node": "*"
}
},
"node_modules/@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"node_modules/@types/mocha": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz",
"integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==",
"dev": true
},
"node_modules/@types/ncp": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/ncp/-/ncp-2.0.1.tgz",
"integrity": "sha512-TeiJ7uvv/92ugSqZ0v9l0eNXzutlki0aK+R1K5bfA5SYUil46ITlxLW4iNTCf55P4L5weCmaOdtxGeGWvudwPg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "20.11.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
"integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/node-fetch": {
"version": "2.5.10",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.10.tgz",
"integrity": "sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"form-data": "^3.0.0"
}
},
"node_modules/@types/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
"dev": true,
"dependencies": {
"@types/glob": "*",
"@types/node": "*"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c= sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/call-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz",
"integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"dev": true,
"dependencies": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
},
"engines": {
"node": ">=4.8"
}
},
"node_modules/define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"dependencies": {
"object-keys": "^1.0.12"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"dev": true,
"dependencies": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"dev": true,
"dependencies": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
"is-symbol": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"node_modules/get-intrinsic": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz",
"integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
"dev": true
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0= sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/has-symbols": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"dev": true
},
"node_modules/is-callable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-core-module": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
"integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-date-object": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
"integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-negative-zero": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
"integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-symbol": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"node_modules/load-json-file": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
"integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs= sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"parse-json": "^4.0.0",
"pify": "^3.0.0",
"strip-bom": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
"integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI= sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
"dev": true,
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/mime-db": {
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
"integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.31",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
"integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
"dev": true,
"dependencies": {
"mime-db": "1.48.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==",
"bin": {
"ncp": "bin/ncp"
}
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"dev": true,
"dependencies": {
"hosted-git-info": "^2.1.4",
"resolve": "^1.10.0",
"semver": "2 || 3 || 4 || 5",
"validate-npm-package-license": "^3.0.1"
}
},
"node_modules/npm-run-all": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
"integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"chalk": "^2.4.1",
"cross-spawn": "^6.0.5",
"memorystream": "^0.3.1",
"minimatch": "^3.0.4",
"pidtree": "^0.3.0",
"read-pkg": "^3.0.0",
"shell-quote": "^1.6.1",
"string.prototype.padend": "^3.0.0"
},
"bin": {
"npm-run-all": "bin/npm-run-all/index.js",
"run-p": "bin/run-p/index.js",
"run-s": "bin/run-s/index.js"
},
"engines": {
"node": ">= 4"
}
},
"node_modules/object-inspect": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
"dev": true,
"dependencies": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18= sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
"integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
"dev": true,
"dependencies": {
"pify": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pidtree": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
"integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
"dev": true,
"bin": {
"pidtree": "bin/pidtree.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/read-pkg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
"integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
"dev": true,
"dependencies": {
"load-json-file": "^4.0.0",
"normalize-package-data": "^2.3.2",
"path-type": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/resolve": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
"dev": true,
"dependencies": {
"is-core-module": "^2.1.0",
"path-parse": "^1.0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
"dev": true,
"dependencies": {
"shebang-regex": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shell-quote": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"node_modules/spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
"integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
"dev": true,
"dependencies": {
"spdx-expression-parse": "^3.0.0",
"spdx-license-ids": "^3.0.0"
}
},
"node_modules/spdx-exceptions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
"dev": true
},
"node_modules/spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
"dependencies": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
}
},
"node_modules/spdx-license-ids": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz",
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
"dev": true
},
"node_modules/string.prototype.padend": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.1.tgz",
"integrity": "sha512-eCzTASPnoCr5Ht+Vn1YXgm8SB015hHKgEIMu9Nr9bQmLhRBxKRfmzSj/IQsxDFc8JInJDDFA0qXwK+xxI7wDkg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.trimend": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz",
"integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.trimstart": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz",
"integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"dev": true,
"dependencies": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"which": "bin/which"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "code-oss-dev-smoke-test",
"version": "0.1.0",
"license": "MIT",
"main": "./src/main.js",
"scripts": {
"compile": "cd ../automation && npm run compile && cd ../smoke && node ../../node_modules/typescript/bin/tsc",
"watch-automation": "cd ../automation && npm run watch",
"watch-smoke": "node ../../node_modules/typescript/bin/tsc --watch --preserveWatchOutput",
"watch": "npm-run-all -lp watch-automation watch-smoke",
"mocha": "node ../node_modules/mocha/bin/mocha"
},
"dependencies": {
"ncp": "^2.0.0",
"node-fetch": "^2.6.7",
"rimraf": "3.0.2"
},
"devDependencies": {
"@types/mocha": "^9.1.1",
"@types/ncp": "2.0.1",
"@types/node": "20.x",
"@types/node-fetch": "^2.5.10",
"@types/rimraf": "3.0.2",
"npm-run-all": "^4.1.5"
}
}
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
export function setup(logger: Logger) {
describe('Extensions', () => {
// Shared before/after handling
installAllHandlers(logger, opts => {
opts.snapshots = true; // enable network tab in devtools for tracing since we install an extension
return opts;
});
it('install and enable vscode-smoketest-check extension', async function () {
const app = this.app as Application;
await app.workbench.extensions.installExtension('ms-vscode.vscode-smoketest-check', true);
// Close extension editor because keybindings dispatch is not working when web views are opened and focused
// https://github.com/microsoft/vscode/issues/110276
await app.workbench.extensions.closeExtension('vscode-smoketest-check');
await app.workbench.quickaccess.runCommand('Smoke Test Check');
});
});
}
@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'path';
import { Application, ProblemSeverity, Problems, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
export function setup(logger: Logger) {
describe('Language Features', () => {
// Shared before/after handling
installAllHandlers(logger);
it('verifies quick outline (js)', async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'bin', 'www'));
await app.workbench.quickaccess.openQuickOutline();
await app.workbench.quickinput.waitForQuickInputElements(names => names.length >= 6);
await app.workbench.quickinput.closeQuickInput();
});
it('verifies quick outline (css)', async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'public', 'stylesheets', 'style.css'));
await app.workbench.quickaccess.openQuickOutline();
await app.workbench.quickinput.waitForQuickInputElements(names => names.length === 2);
await app.workbench.quickinput.closeQuickInput();
});
it('verifies problems view (css)', async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'public', 'stylesheets', 'style.css'));
await app.workbench.editor.waitForTypeInEditor('style.css', '.foo{}');
await app.code.waitForElement(Problems.getSelectorInEditor(ProblemSeverity.WARNING));
await app.workbench.problems.showProblemsView();
await app.code.waitForElement(Problems.getSelectorInProblemsView(ProblemSeverity.WARNING));
await app.workbench.problems.hideProblemsView();
});
it('verifies settings (css)', async function () {
const app = this.app as Application;
await app.workbench.settingsEditor.addUserSetting('css.lint.emptyRules', '"error"');
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'public', 'stylesheets', 'style.css'));
await app.code.waitForElement(Problems.getSelectorInEditor(ProblemSeverity.ERROR));
await app.workbench.problems.showProblemsView();
await app.code.waitForElement(Problems.getSelectorInProblemsView(ProblemSeverity.ERROR));
await app.workbench.problems.hideProblemsView();
});
});
}
@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { Application, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
function toUri(path: string): string {
if (process.platform === 'win32') {
return `${path.replace(/\\/g, '/')}`;
}
return `${path}`;
}
function createWorkspaceFile(workspacePath: string): string {
const workspaceFilePath = join(dirname(workspacePath), 'smoketest.code-workspace');
const workspace = {
folders: [
{ path: toUri(join(workspacePath, 'public')) },
{ path: toUri(join(workspacePath, 'routes')) },
{ path: toUri(join(workspacePath, 'views')) }
],
settings: {
'workbench.startupEditor': 'none',
'workbench.enableExperiments': false,
'typescript.disableAutomaticTypeAcquisition': true,
'json.schemaDownload.enable': false,
'npm.fetchOnlinePackageInfo': false,
'npm.autoDetect': 'off',
'workbench.editor.languageDetection': false,
"workbench.localHistory.enabled": false
}
};
writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, '\t'));
return workspaceFilePath;
}
export function setup(logger: Logger) {
describe('Multiroot', () => {
// Shared before/after handling
installAllHandlers(logger, opts => {
const workspacePath = createWorkspaceFile(opts.workspacePath);
return { ...opts, workspacePath };
});
it('shows results from all folders', async function () {
const app = this.app as Application;
const expectedNames = [
'index.js',
'users.js',
'style.css',
'error.pug',
'index.pug',
'layout.pug'
];
await app.workbench.quickaccess.openFileQuickAccessAndWait('*.*', 6);
await app.workbench.quickinput.waitForQuickInputElements(names => expectedNames.every(expectedName => names.some(name => expectedName === name)));
await app.workbench.quickinput.closeQuickInput();
});
it('shows workspace name in title', async function () {
const app = this.app as Application;
await app.code.waitForTitle(title => /smoketest \(Workspace\)/i.test(title));
});
});
}
@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import { Application, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
export function setup(logger: Logger) {
describe('Notebooks', () => { // https://github.com/microsoft/vscode/issues/140575
// Shared before/after handling
installAllHandlers(logger);
afterEach(async function () {
const app = this.app as Application;
await app.workbench.quickaccess.runCommand('workbench.action.files.save');
await app.workbench.quickaccess.runCommand('workbench.action.closeActiveEditor');
});
after(async function () {
const app = this.app as Application;
cp.execSync('git checkout . --quiet', { cwd: app.workspacePathOrFolder });
cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder });
});
it.skip('check heap leaks', async function () {
const app = this.app as Application;
await app.profiler.checkHeapLeaks(['NotebookTextModel', 'NotebookCellTextModel', 'NotebookEventDispatcher'], async () => {
await app.workbench.notebook.openNotebook();
await app.workbench.quickaccess.runCommand('workbench.action.files.save');
await app.workbench.quickaccess.runCommand('workbench.action.closeActiveEditor');
});
});
it('check object leaks', async function () {
const app = this.app as Application;
await app.profiler.checkObjectLeaks(['NotebookTextModel', 'NotebookCellTextModel', 'NotebookEventDispatcher'], async () => {
await app.workbench.notebook.openNotebook();
await app.workbench.quickaccess.runCommand('workbench.action.files.save');
await app.workbench.quickaccess.runCommand('workbench.action.closeActiveEditor');
});
});
it.skip('inserts/edits code cell', async function () {
const app = this.app as Application;
await app.workbench.notebook.openNotebook();
await app.workbench.notebook.focusNextCell();
await app.workbench.notebook.insertNotebookCell('code');
await app.workbench.notebook.waitForTypeInEditor('// some code');
await app.workbench.notebook.stopEditingCell();
});
it.skip('inserts/edits markdown cell', async function () {
const app = this.app as Application;
await app.workbench.notebook.openNotebook();
await app.workbench.notebook.focusNextCell();
await app.workbench.notebook.insertNotebookCell('markdown');
await app.workbench.notebook.waitForTypeInEditor('## hello2! ');
await app.workbench.notebook.stopEditingCell();
await app.workbench.notebook.waitForMarkdownContents('h2', 'hello2!');
});
it.skip('moves focus as it inserts/deletes a cell', async function () {
const app = this.app as Application;
await app.workbench.notebook.openNotebook();
await app.workbench.notebook.insertNotebookCell('code');
await app.workbench.notebook.waitForActiveCellEditorContents('');
await app.workbench.notebook.stopEditingCell();
await app.workbench.notebook.deleteActiveCell();
await app.workbench.notebook.waitForMarkdownContents('p', 'Markdown Cell');
});
it.skip('moves focus in and out of output', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/139270
const app = this.app as Application;
await app.workbench.notebook.openNotebook();
await app.workbench.notebook.executeActiveCell();
await app.workbench.notebook.focusInCellOutput();
await app.workbench.notebook.focusOutCellOutput();
await app.workbench.notebook.waitForActiveCellEditorContents('code()');
});
it.skip('cell action execution', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/139270
const app = this.app as Application;
await app.workbench.notebook.openNotebook();
await app.workbench.notebook.insertNotebookCell('code');
await app.workbench.notebook.executeCellAction('.notebook-editor .monaco-list-row.focused div.monaco-toolbar .codicon-debug');
await app.workbench.notebook.waitForActiveCellEditorContents('test');
});
});
}
@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, ActivityBarPosition, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
export function setup(logger: Logger) {
describe('Preferences', () => {
// Shared before/after handling
installAllHandlers(logger);
it('turns off editor line numbers and verifies the live change', async function () {
const app = this.app as Application;
await app.workbench.settingsEditor.openUserSettingsFile();
await app.code.waitForElements('.line-numbers', false, elements => !!elements.length);
await app.workbench.settingsEditor.addUserSetting('editor.lineNumbers', '"off"');
await app.code.waitForElements('.line-numbers', false, elements => !elements || elements.length === 0);
});
it.skip('changes "workbench.action.toggleSidebarPosition" command key binding and verifies it', async function () {
const app = this.app as Application;
await app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.LEFT);
await app.workbench.keybindingsEditor.updateKeybinding('workbench.action.toggleSidebarPosition', 'View: Toggle Primary Side Bar Position', 'ctrl+u', 'Control+U');
await app.code.sendKeybinding('ctrl+u', () => app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.RIGHT));
});
});
describe('Settings editor', () => {
// Shared before/after handling
installAllHandlers(logger);
it('shows a modified indicator on a modified setting', async function () {
const app = this.app as Application;
await app.workbench.settingsEditor.searchSettingsUI('@id:editor.tabSize');
await app.code.waitForSetValue('.settings-editor .setting-item-contents .setting-item-control input', '6');
await app.code.waitForElement('.settings-editor .setting-item-contents .setting-item-modified-indicator');
await app.code.waitForSetValue('.settings-editor .setting-item-contents .setting-item-control input', '4');
});
// Skipping test due to it being flaky.
it.skip('turns off editor line numbers and verifies the live change', async function () {
const app = this.app as Application;
await app.workbench.editors.newUntitledFile();
await app.code.sendKeybinding('enter', async () => {
await app.code.waitForElements('.line-numbers', false, elements => !!elements.length);
});
// Turn off line numbers
await app.workbench.settingsEditor.searchSettingsUI('editor.lineNumbers');
await app.code.waitAndClick('.settings-editor .monaco-list-rows .setting-item-control select', 2, 2);
await app.code.waitAndClick('.context-view .option-text', 2, 2);
await app.workbench.editors.selectTab('Untitled-1');
await app.code.waitForElements('.line-numbers', false, elements => !elements || elements.length === 0);
});
// Skipping test due to it being flaky.
it.skip('hides the toc when searching depending on the search behavior', async function () {
const app = this.app as Application;
// Hide ToC when searching
await app.workbench.settingsEditor.searchSettingsUI('workbench.settings.settingsSearchTocBehavior');
await app.code.waitAndClick('.settings-editor .monaco-list-rows .setting-item-control select', 2, 2);
await app.code.waitAndClick('.context-view .monaco-list-row:nth-child(1) .option-text', 2, 2);
await app.workbench.settingsEditor.searchSettingsUI('test');
await app.code.waitForElements('.settings-editor .settings-toc-container', false, elements => elements.length === 1 && elements[0].attributes['style'].includes('width: 0px'));
await app.code.waitForElements('.settings-editor .settings-body .monaco-sash', false, elements => elements.length === 1 && elements[0].className.includes('disabled'));
// Show ToC when searching
await app.workbench.settingsEditor.searchSettingsUI('workbench.settings.settingsSearchTocBehavior');
await app.code.waitAndClick('.settings-editor .monaco-list-rows .setting-item-control select', 2, 2);
await app.code.waitAndClick('.context-view .monaco-list-row:nth-child(2) .option-text', 2, 2);
await app.workbench.settingsEditor.searchSettingsUI('test');
await app.code.waitForElements('.settings-editor .settings-toc-container', false, elements => elements.length === 1 && !elements[0].attributes['style'].includes('width: 0px'));
await app.code.waitForElements('.settings-editor .settings-body .monaco-sash', false, elements => elements.length === 1 && !elements[0].className.includes('disabled'));
});
});
}
+117
View File
@@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import { Application, Logger } from '../../../../automation';
import { installAllHandlers, retry } from '../../utils';
export function setup(logger: Logger) {
describe('Search', () => {
// Shared before/after handling
installAllHandlers(logger);
after(function () {
const app = this.app as Application;
retry(async () => cp.execSync('git checkout . --quiet', { cwd: app.workspacePathOrFolder }), 0, 5);
retry(async () => cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder }), 0, 5);
});
it('verifies the sidebar moves to the right', async function () {
const app = this.app as Application;
await app.workbench.search.openSearchViewlet();
await app.code.sendKeybinding('PageUp', async () => {
await app.workbench.search.hasActivityBarMoved();
});
await app.code.sendKeybinding('PageUp', async () => {
await app.workbench.search.hasActivityBarMoved();
});
});
it('searches for body & checks for correct result number', async function () {
const app = this.app as Application;
await app.workbench.search.openSearchViewlet();
await app.workbench.search.searchFor('body');
await app.workbench.search.waitForResultText('6 results in 3 files');
});
it('searches only for *.js files & checks for correct result number', async function () {
const app = this.app as Application;
try {
await app.workbench.search.setFilesToIncludeText('*.js');
await app.workbench.search.searchFor('body');
await app.workbench.search.showQueryDetails();
await app.workbench.search.waitForResultText('4 results in 1 file');
} finally {
await app.workbench.search.setFilesToIncludeText('');
await app.workbench.search.hideQueryDetails();
}
});
it('dismisses result & checks for correct result number', async function () {
const app = this.app as Application;
await app.workbench.search.searchFor('body');
await app.workbench.search.waitForResultText('6 results in 3 files');
await app.workbench.search.removeFileMatch('app.js', '2 results in 2 files');
});
it.skip('replaces first search result with a replace term', async function () { // TODO@roblourens https://github.com/microsoft/vscode/issues/137195
const app = this.app as Application;
await app.workbench.search.searchFor('body');
await app.workbench.search.waitForResultText('6 results in 3 files');
await app.workbench.search.expandReplace();
await app.workbench.search.setReplaceText('ydob');
await app.workbench.search.replaceFileMatch('app.js', '2 results in 2 files');
await app.workbench.search.searchFor('ydob');
await app.workbench.search.waitForResultText('4 results in 1 file');
await app.workbench.search.setReplaceText('body');
await app.workbench.search.replaceFileMatch('app.js', '0 results in 0 files');
await app.workbench.search.waitForResultText('0 results in 0 files');
});
});
describe('Quick Open', () => {
// Shared before/after handling
installAllHandlers(logger);
it('quick open search produces correct result', async function () {
const app = this.app as Application;
const expectedNames = [
'.eslintrc.json',
'tasks.json',
'settings.json',
'app.js',
'index.js',
'users.js',
'package.json',
'jsconfig.json'
];
await app.workbench.quickaccess.openFileQuickAccessAndWait('.js', 8);
await app.workbench.quickinput.waitForQuickInputElements(names => expectedNames.every(expectedName => names.some(name => expectedName === name)));
await app.workbench.quickinput.closeQuickInput();
});
it('quick open respects fuzzy matching', async function () {
const app = this.app as Application;
const expectedNames = [
'tasks.json',
'app.js',
'package.json'
];
await app.workbench.quickaccess.openFileQuickAccessAndWait('a.s', 3);
await app.workbench.quickinput.waitForQuickInputElements(names => expectedNames.every(expectedName => names.some(name => expectedName === name)));
await app.workbench.quickinput.closeQuickInput();
});
});
}
@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'path';
import { Application, StatusBarElement, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
export function setup(logger: Logger) {
describe('Statusbar', () => {
// Shared before/after handling
installAllHandlers(logger);
it('verifies presence of all default status bar elements', async function () {
const app = this.app as Application;
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.BRANCH_STATUS);
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.SYNC_STATUS);
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.PROBLEMS_STATUS);
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'readme.md'));
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.ENCODING_STATUS);
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.EOL_STATUS);
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.INDENTATION_STATUS);
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.LANGUAGE_STATUS);
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.SELECTION_STATUS);
});
it(`verifies that 'quick input' opens when clicking on status bar elements`, async function () {
const app = this.app as Application;
await app.workbench.statusbar.clickOn(StatusBarElement.BRANCH_STATUS);
await app.workbench.quickinput.waitForQuickInputOpened();
await app.workbench.quickinput.closeQuickInput();
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'readme.md'));
await app.workbench.statusbar.clickOn(StatusBarElement.INDENTATION_STATUS);
await app.workbench.quickinput.waitForQuickInputOpened();
await app.workbench.quickinput.closeQuickInput();
await app.workbench.statusbar.clickOn(StatusBarElement.ENCODING_STATUS);
await app.workbench.quickinput.waitForQuickInputOpened();
await app.workbench.quickinput.closeQuickInput();
await app.workbench.statusbar.clickOn(StatusBarElement.EOL_STATUS);
await app.workbench.quickinput.waitForQuickInputOpened();
await app.workbench.quickinput.closeQuickInput();
await app.workbench.statusbar.clickOn(StatusBarElement.LANGUAGE_STATUS);
await app.workbench.quickinput.waitForQuickInputOpened();
await app.workbench.quickinput.closeQuickInput();
});
it(`verifies that 'Problems View' appears when clicking on 'Problems' status element`, async function () {
const app = this.app as Application;
await app.workbench.statusbar.clickOn(StatusBarElement.PROBLEMS_STATUS);
await app.workbench.problems.waitForProblemsView();
});
it(`verifies if changing EOL is reflected in the status bar`, async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'readme.md'));
await app.workbench.statusbar.clickOn(StatusBarElement.EOL_STATUS);
await app.workbench.quickinput.selectQuickInputElement(1);
await app.workbench.statusbar.waitForEOL('CRLF');
});
});
}
@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Task, Terminal, TerminalCommandId } from '../../../../automation/';
export function setup(options?: { skipSuite: boolean }) {
describe('Task Quick Pick', () => {
let app: Application;
let task: Task;
let terminal: Terminal;
// Acquire automation API
before(async function () {
app = this.app as Application;
task = app.workbench.task;
terminal = app.workbench.terminal;
});
afterEach(async () => {
// Kill all terminals between every test for a consistent testing environment
await terminal.runCommand(TerminalCommandId.KillAll);
});
describe('Tasks: Run Task', () => {
const label = "name";
const type = "shell";
const command = "echo 'test'";
it('hide property - true', async () => {
await task.configureTask({ type, command, label, hide: true });
await task.assertTasks(label, [], 'run');
});
it('hide property - false', async () => {
await task.configureTask({ type, command, label, hide: false });
await task.assertTasks(label, [{ label }], 'run');
});
it('hide property - undefined', async () => {
await task.configureTask({ type, command, label });
await task.assertTasks(label, [{ label }], 'run');
});
(options?.skipSuite ? it.skip : it)('icon - icon only', async () => {
const config = { label, type, command, icon: { id: "lightbulb" } };
await task.configureTask(config);
await task.assertTasks(label, [config], 'run');
});
(options?.skipSuite ? it.skip : it)('icon - color only', async () => {
const config = { label, type, command, icon: { color: "terminal.ansiRed" } };
await task.configureTask(config);
await task.assertTasks(label, [{ label, type, command, icon: { color: "Red" } }], 'run');
});
(options?.skipSuite ? it.skip : it)('icon - icon & color', async () => {
const config = { label, type, command, icon: { id: "lightbulb", color: "terminal.ansiRed" } };
await task.configureTask(config);
await task.assertTasks(label, [{ label, type, command, icon: { id: "lightbulb", color: "Red" } }], 'run');
});
});
//TODO: why won't this command run
describe.skip('Tasks: Configure Task', () => {
const label = "name";
const type = "shell";
const command = "echo 'test'";
describe('hide', () => {
it('true should still show the task', async () => {
await task.configureTask({ type, command, label, hide: true });
await task.assertTasks(label, [{ label }], 'configure');
});
});
});
});
}
+24
View File
@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
import { setup as setupTaskQuickPickTests } from './task-quick-pick.test';
export function setup(logger: Logger) {
describe('Task', function () {
// Retry tests 3 times to minimize build failures due to any flakiness
this.retries(3);
// Shared before/after handling
installAllHandlers(logger);
// Refs https://github.com/microsoft/vscode/issues/225250
// Pty spawning fails with invalid fd error in product CI while development CI
// works fine, we need additional logging to investigate.
setupTaskQuickPickTests({ skipSuite: process.platform === 'linux' });
});
}
@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, TerminalCommandId, TerminalCommandIdWithValue, SettingsEditor } from '../../../../automation';
import { setTerminalTestSettings } from './terminal-helpers';
export function setup(options?: { skipSuite: boolean }) {
(options?.skipSuite ? describe.skip : describe)('Terminal Editors', () => {
let app: Application;
let terminal: Terminal;
let settingsEditor: SettingsEditor;
// Acquire automation API
before(async function () {
app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
await setTerminalTestSettings(app);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
it('should update color of the tab', async () => {
await terminal.runCommand(TerminalCommandId.CreateNewEditor);
const color = 'Cyan';
await terminal.runCommandWithValue(TerminalCommandIdWithValue.ChangeColor, color);
await terminal.assertSingleTab({ color }, true);
});
it('should rename the tab', async () => {
await terminal.runCommand(TerminalCommandId.CreateNewEditor);
const name = 'my terminal name';
await terminal.runCommandWithValue(TerminalCommandIdWithValue.Rename, name);
await terminal.assertSingleTab({ name }, true);
});
it('should show the panel when the terminal is moved there and close the editor', async () => {
await terminal.runCommand(TerminalCommandId.CreateNewEditor);
await terminal.runCommand(TerminalCommandId.MoveToPanel);
await terminal.assertSingleTab({});
});
it('should open a terminal in a new group for open to the side', async () => {
await terminal.runCommand(TerminalCommandId.CreateNewEditor);
await terminal.runCommand(TerminalCommandId.SplitEditor);
await terminal.assertEditorGroupCount(2);
});
it('should open a terminal in a new group when the split button is pressed', async () => {
await terminal.runCommand(TerminalCommandId.CreateNewEditor);
await terminal.clickSplitButton();
await terminal.assertEditorGroupCount(2);
});
it('should create new terminals in the active editor group via command', async () => {
await terminal.runCommand(TerminalCommandId.CreateNewEditor);
await terminal.runCommand(TerminalCommandId.CreateNewEditor);
await terminal.assertEditorGroupCount(1);
});
it('should create new terminals in the active editor group via plus button', async () => {
await terminal.runCommand(TerminalCommandId.CreateNewEditor);
await terminal.clickPlusButton();
await terminal.assertEditorGroupCount(1);
});
it('should create a terminal in the editor area by default', async () => {
await app.workbench.settingsEditor.addUserSetting('terminal.integrated.defaultLocation', '"editor"');
// Close the settings editor
await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors');
await terminal.createTerminal('editor');
await terminal.assertEditorGroupCount(1);
await terminal.assertTerminalViewHidden();
await app.workbench.settingsEditor.clearUserSettings();
});
});
}
@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application } from '../../../../automation';
export async function setTerminalTestSettings(app: Application, additionalSettings: [key: string, value: string][] = []) {
await app.workbench.settingsEditor.addUserSettings([
// Work wrap is required when calling settingsEditor.addUserSetting multiple times or the
// click to focus will fail
['editor.wordWrap', '"on"'],
// Always show tabs to make getting terminal groups easier
['terminal.integrated.tabs.hideCondition', '"never"'],
// Use the DOM renderer for smoke tests so they can be inspected in the playwright trace
// viewer
['terminal.integrated.gpuAcceleration', '"off"'],
...additionalSettings
]);
// Close the settings editor
await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors');
}
@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, SettingsEditor } from '../../../../automation';
import { setTerminalTestSettings } from './terminal-helpers';
export function setup(options?: { skipSuite: boolean }) {
(options?.skipSuite ? describe.skip : describe)('Terminal Input', () => {
let terminal: Terminal;
let settingsEditor: SettingsEditor;
// Acquire automation API
before(async function () {
const app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
await setTerminalTestSettings(app);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
describe('Auto replies', function () {
// HACK: Retry this suite only on Windows because conpty can rarely lead to unexpected behavior which would
// cause flakiness. If this does happen, the feature is expected to fail.
if (process.platform === 'win32') {
this.retries(3);
}
async function writeTextForAutoReply(text: string): Promise<void> {
// Put the matching word in quotes to avoid powershell coloring the first word and
// on a new line to avoid cursor move/line switching sequences
await terminal.runCommandInTerminal(`"\r${text}`, true);
}
it('should automatically reply to a custom entry', async () => {
await settingsEditor.addUserSetting('terminal.integrated.autoReplies', '{ "foo": "bar" }');
await terminal.createTerminal();
await writeTextForAutoReply('foo');
await terminal.waitForTerminalText(buffer => buffer.some(line => line.match(/foo.*bar/)));
});
});
});
}
@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, TerminalCommandId, TerminalCommandIdWithValue, SettingsEditor } from '../../../../automation';
import { setTerminalTestSettings } from './terminal-helpers';
export function setup(options?: { skipSuite: boolean }) {
(options?.skipSuite ? describe.skip : describe)('Terminal Persistence', () => {
// Acquire automation API
let terminal: Terminal;
let settingsEditor: SettingsEditor;
before(async function () {
const app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
await setTerminalTestSettings(app);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
describe('detach/attach', () => {
// https://github.com/microsoft/vscode/issues/137799
it('should support basic reconnection', async () => {
await terminal.createTerminal();
// TODO: Handle passing in an actual regex, not string
await terminal.assertTerminalGroups([
[{ name: '.*' }]
]);
// Get the terminal name
await terminal.assertTerminalGroups([
[{ name: '.*' }]
]);
const name = (await terminal.getTerminalGroups())[0][0].name!;
// Detach
await terminal.runCommand(TerminalCommandId.DetachSession);
await terminal.assertTerminalViewHidden();
// Attach
await terminal.runCommandWithValue(TerminalCommandIdWithValue.AttachToSession, name);
await terminal.assertTerminalGroups([
[{ name }]
]);
});
it.skip('should persist buffer content', async () => {
await terminal.createTerminal();
// TODO: Handle passing in an actual regex, not string
await terminal.assertTerminalGroups([
[{ name: '.*' }]
]);
// Get the terminal name
await terminal.assertTerminalGroups([
[{ name: '.*' }]
]);
const name = (await terminal.getTerminalGroups())[0][0].name!;
// Write in terminal
await terminal.runCommandInTerminal('echo terminal_test_content');
await terminal.waitForTerminalText(buffer => buffer.some(e => e.includes('terminal_test_content')));
// Detach
await terminal.runCommand(TerminalCommandId.DetachSession);
await terminal.assertTerminalViewHidden();
// Attach
await terminal.runCommandWithValue(TerminalCommandIdWithValue.AttachToSession, name);
await terminal.assertTerminalGroups([
[{ name }]
]);
// There can be line wrapping, so remove newlines and carriage returns #216464
await terminal.waitForTerminalText(buffer => buffer.some(e => e.replaceAll(/[\r\n]/g, '').includes('terminal_test_content')));
});
});
});
}
@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, TerminalCommandId, TerminalCommandIdWithValue, SettingsEditor } from '../../../../automation';
import { setTerminalTestSettings } from './terminal-helpers';
const CONTRIBUTED_PROFILE_NAME = `JavaScript Debug Terminal`;
const ANY_PROFILE_NAME = '^((?!JavaScript Debug Terminal).)*$';
export function setup(options?: { skipSuite: boolean }) {
(options?.skipSuite ? describe.skip : describe)('Terminal Profiles', () => {
// Acquire automation API
let terminal: Terminal;
let settingsEditor: SettingsEditor;
before(async function () {
const app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
await setTerminalTestSettings(app);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
it('should launch the default profile', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.assertSingleTab({ name: ANY_PROFILE_NAME });
});
it('should set the default profile to a contributed one', async () => {
await terminal.runCommandWithValue(TerminalCommandIdWithValue.SelectDefaultProfile, CONTRIBUTED_PROFILE_NAME);
await terminal.createTerminal();
await terminal.assertSingleTab({ name: CONTRIBUTED_PROFILE_NAME });
});
it('should use the default contributed profile on panel open and for splitting', async () => {
await terminal.runCommandWithValue(TerminalCommandIdWithValue.SelectDefaultProfile, CONTRIBUTED_PROFILE_NAME);
await terminal.runCommand(TerminalCommandId.Show);
await terminal.runCommand(TerminalCommandId.Split);
await terminal.assertTerminalGroups([[{ name: CONTRIBUTED_PROFILE_NAME }, { name: CONTRIBUTED_PROFILE_NAME }]]);
});
it('should set the default profile', async () => {
await terminal.runCommandWithValue(TerminalCommandIdWithValue.SelectDefaultProfile, process.platform === 'win32' ? 'PowerShell' : undefined);
await terminal.createTerminal();
await terminal.assertSingleTab({ name: ANY_PROFILE_NAME });
});
it('should use the default profile on panel open and for splitting', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.assertSingleTab({ name: ANY_PROFILE_NAME });
await terminal.runCommand(TerminalCommandId.Split);
await terminal.assertTerminalGroups([[{}, {}]]);
});
it('createWithProfile command should create a terminal with a profile', async () => {
await terminal.runCommandWithValue(TerminalCommandIdWithValue.NewWithProfile);
await terminal.assertSingleTab({ name: ANY_PROFILE_NAME });
});
it('createWithProfile command should create a terminal with a contributed profile', async () => {
await terminal.runCommandWithValue(TerminalCommandIdWithValue.NewWithProfile, CONTRIBUTED_PROFILE_NAME);
await terminal.assertSingleTab({ name: CONTRIBUTED_PROFILE_NAME });
});
it('createWithProfile command should create a split terminal with a profile', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.runCommandWithValue(TerminalCommandIdWithValue.NewWithProfile, undefined, true);
await terminal.assertTerminalGroups([[{}, {}]]);
});
it('createWithProfile command should create a split terminal with a contributed profile', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.assertSingleTab({});
await terminal.runCommandWithValue(TerminalCommandIdWithValue.NewWithProfile, CONTRIBUTED_PROFILE_NAME, true);
await terminal.assertTerminalGroups([[{ name: ANY_PROFILE_NAME }, { name: CONTRIBUTED_PROFILE_NAME }]]);
});
});
}
@@ -0,0 +1,153 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, SettingsEditor, TerminalCommandIdWithValue, TerminalCommandId } from '../../../../automation';
import { setTerminalTestSettings } from './terminal-helpers';
export function setup(options?: { skipSuite: boolean }) {
(options?.skipSuite ? describe.skip : describe)('Terminal Shell Integration', () => {
let terminal: Terminal;
let settingsEditor: SettingsEditor;
let app: Application;
// Acquire automation API
before(async function () {
app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
});
afterEach(async function () {
await app.workbench.terminal.runCommand(TerminalCommandId.KillAll);
});
async function createShellIntegrationProfile() {
await terminal.runCommandWithValue(TerminalCommandIdWithValue.NewWithProfile, process.platform === 'win32' ? 'PowerShell' : 'bash');
}
// TODO: Some agents may not have pwsh installed?
(process.platform === 'win32' ? describe.skip : describe)(`Process-based tests`, function () {
before(async function () {
await setTerminalTestSettings(app, [['terminal.integrated.shellIntegration.enabled', 'true']]);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
describe('Decorations', function () {
describe('Should show default icons', function () {
it('Placeholder', async () => {
await createShellIntegrationProfile();
await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 });
});
it('Success', async () => {
await createShellIntegrationProfile();
await terminal.runCommandInTerminal(`echo "success"`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 0 });
});
it('Error', async () => {
await createShellIntegrationProfile();
await terminal.runCommandInTerminal(`false`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 1 });
});
});
describe('terminal.integrated.shellIntegration.decorationsEnabled should determine gutter and overview ruler decoration visibility', function () {
beforeEach(async () => {
await settingsEditor.clearUserSettings();
await setTerminalTestSettings(app, [['terminal.integrated.shellIntegration.enabled', 'true']]);
await createShellIntegrationProfile();
await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 });
await terminal.runCommandInTerminal(`echo "foo"`);
await terminal.runCommandInTerminal(`bar`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 1 });
});
afterEach(async () => {
await app.workbench.terminal.runCommand(TerminalCommandId.KillAll);
});
it('never', async () => {
await settingsEditor.addUserSetting('terminal.integrated.shellIntegration.decorationsEnabled', '"never"');
await terminal.assertCommandDecorations({ placeholder: 0, success: 0, error: 0 }, undefined, 'never');
});
it('both', async () => {
await settingsEditor.addUserSetting('terminal.integrated.shellIntegration.decorationsEnabled', '"both"');
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 1 }, undefined, 'both');
});
it('gutter', async () => {
await settingsEditor.addUserSetting('terminal.integrated.shellIntegration.decorationsEnabled', '"gutter"');
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 1 }, undefined, 'gutter');
});
it('overviewRuler', async () => {
await settingsEditor.addUserSetting('terminal.integrated.shellIntegration.decorationsEnabled', '"overviewRuler"');
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 1 }, undefined, 'overviewRuler');
});
});
});
});
// These are integration tests that only test the UI side by simulating process writes.
// Because of this, they do not test the shell integration scripts, only what the scripts
// are expected to write.
describe('Write data-based tests', () => {
before(async function () {
await setTerminalTestSettings(app);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
beforeEach(async function () {
// Use the simplest profile to get as little process interaction as possible
await terminal.createEmptyTerminal();
// Erase all content and reset cursor to top
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${csi('2J')}${csi('H')}`);
});
describe('VS Code sequences', () => {
it('should handle the simple case', async () => {
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${vsc('A')}Prompt> ${vsc('B')}exitcode 0`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `\\r\\n${vsc('C')}Success\\r\\n${vsc('D;0')}`);
await terminal.assertCommandDecorations({ placeholder: 0, success: 1, error: 0 });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${vsc('A')}Prompt> ${vsc('B')}exitcode 1`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 0 });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `\\r\\n${vsc('C')}Failure\\r\\n${vsc('D;1')}`);
await terminal.assertCommandDecorations({ placeholder: 0, success: 1, error: 1 });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${vsc('A')}Prompt> ${vsc('B')}`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 1 });
});
});
describe('Final Term sequences', () => {
it('should handle the simple case', async () => {
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${ft('A')}Prompt> ${ft('B')}exitcode 0`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `\\r\\n${ft('C')}Success\\r\\n${ft('D;0')}`);
await terminal.assertCommandDecorations({ placeholder: 0, success: 1, error: 0 });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${ft('A')}Prompt> ${ft('B')}exitcode 1`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 0 });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `\\r\\n${ft('C')}Failure\\r\\n${ft('D;1')}`);
await terminal.assertCommandDecorations({ placeholder: 0, success: 1, error: 1 });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${ft('A')}Prompt> ${ft('B')}exitcode 1`);
await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 1 });
});
});
});
});
}
function ft(data: string) {
return setTextParams(`133;${data}`);
}
function vsc(data: string) {
return setTextParams(`633;${data}`);
}
function setTextParams(data: string) {
return osc(`${data}\\x07`);
}
function osc(data: string) {
return `\\x1b]${data}`;
}
function csi(data: string) {
return `\\x1b[${data}`;
}
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, SettingsEditor } from '../../../../automation';
import { setTerminalTestSettings } from './terminal-helpers';
export function setup(options?: { skipSuite: boolean }) {
(options?.skipSuite ? describe.skip : describe)('Terminal splitCwd', () => {
// Acquire automation API
let terminal: Terminal;
let settingsEditor: SettingsEditor;
before(async function () {
const app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
await setTerminalTestSettings(app, [
['terminal.integrated.splitCwd', '"inherited"']
]);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
it('should inherit cwd when split and update the tab description - alt click', async () => {
await terminal.createTerminal();
const cwd = 'test';
await terminal.runCommandInTerminal(`mkdir ${cwd}`);
await terminal.runCommandInTerminal(`cd ${cwd}`);
const page = await terminal.getPage();
page.keyboard.down('Alt');
await terminal.clickSingleTab();
page.keyboard.up('Alt');
await terminal.assertTerminalGroups([[{ description: cwd }, { description: cwd }]]);
});
});
}
@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, SettingsEditor, TerminalCommandIdWithValue } from '../../../../automation';
import { setTerminalTestSettings } from './terminal-helpers';
export function setup(options?: { skipSuite: boolean }) {
(options?.skipSuite ? describe.skip : describe)('Terminal stickyScroll', () => {
// Acquire automation API
let app: Application;
let terminal: Terminal;
let settingsEditor: SettingsEditor;
before(async function () {
app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
await setTerminalTestSettings(app, [
['terminal.integrated.stickyScroll.enabled', 'true']
]);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
// A polling approach is used to avoid test flakiness. While it's not ideal that this
// occurs, the main purpose of the tests is to verify sticky scroll shows and updates,
// not edge case race conditions on terminal start up
async function checkCommandAndOutput(
command: string,
exitCode: number,
prompt: string = 'Prompt> ',
expectedLineCount: number = 1
): Promise<void> {
const data = generateCommandAndOutput(prompt, command, exitCode);
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, data);
// Verify line count
await app.code.waitForElements('.terminal-sticky-scroll .xterm-rows > *', true, e => e.length === expectedLineCount);
// Verify content
const element = await app.code.getElement('.terminal-sticky-scroll .xterm-rows');
if (
element &&
// New lines don't come through in textContent
element.textContent.indexOf(`${prompt.replace(/\\r\\n/g, '')}${command}`) >= 0
) {
return;
}
throw new Error(`Failed for command ${command}, exitcode ${exitCode}, text content ${element?.textContent}`);
}
beforeEach(async () => {
// Create the simplest system profile to get as little process interaction as possible
await terminal.createEmptyTerminal();
});
it('should show sticky scroll when appropriate', async () => {
// Write prompt, fill viewport, finish command, print new prompt, verify sticky scroll
await checkCommandAndOutput('sticky scroll 1', 0);
// And again with a failed command
await checkCommandAndOutput('sticky scroll 2', 1);
});
it('should support multi-line prompt', async () => {
// Standard multi-line prompt
await checkCommandAndOutput('sticky scroll 1', 0, "Multi-line\\r\\nPrompt> ", 2);
// New line before prompt
await checkCommandAndOutput('sticky scroll 2', 0, "\\r\\nMulti-line Prompt> ", 1);
// New line before multi-line prompt
await checkCommandAndOutput('sticky scroll 3', 0, "\\r\\nMulti-line\\r\\nPrompt> ", 2);
});
});
}
function generateCommandAndOutput(prompt: string, command: string, exitCode: number): string {
return [
`${vsc('A')}${prompt}${vsc('B')}${command}`,
`\\r\\n${vsc('C')}`,
`\\r\\ndata`.repeat(50),
`\\r\\n${vsc(`D;${exitCode}`)}`,
].join('');
}
function vsc(data: string) {
return setTextParams(`633;${data}`);
}
function setTextParams(data: string) {
return osc(`${data}\\x07`);
}
function osc(data: string) {
return `\\x1b]${data}`;
}
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, TerminalCommandId, TerminalCommandIdWithValue, SettingsEditor } from '../../../../automation';
import { setTerminalTestSettings } from './terminal-helpers';
export function setup(options?: { skipSuite: boolean }) {
(options?.skipSuite ? describe.skip : describe)('Terminal Tabs', () => {
// Acquire automation API
let terminal: Terminal;
let settingsEditor: SettingsEditor;
before(async function () {
const app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
await setTerminalTestSettings(app);
});
after(async function () {
await settingsEditor.clearUserSettings();
});
it('clicking the plus button should create a terminal and display the tabs view showing no split decorations', async () => {
await terminal.createTerminal();
await terminal.clickPlusButton();
await terminal.assertTerminalGroups([[{}], [{}]]);
});
it('should rename the single tab', async () => {
await terminal.createTerminal();
const name = 'my terminal name';
await terminal.runCommandWithValue(TerminalCommandIdWithValue.Rename, name);
await terminal.assertSingleTab({ name });
});
// DEBT: Flaky https://github.com/microsoft/vscode/issues/216564
it.skip('should reset the tab name to the default value when no name is provided', async () => {
await terminal.createTerminal();
const defaultName = await terminal.getSingleTabName();
const name = 'my terminal name';
await terminal.runCommandWithValue(TerminalCommandIdWithValue.Rename, name);
await terminal.assertSingleTab({ name });
await terminal.runCommandWithValue(TerminalCommandIdWithValue.Rename, undefined);
await terminal.assertSingleTab({ name: defaultName });
});
it('should rename the tab in the tabs list', async () => {
await terminal.createTerminal();
await terminal.runCommand(TerminalCommandId.Split);
const name = 'my terminal name';
await terminal.runCommandWithValue(TerminalCommandIdWithValue.Rename, name);
await terminal.assertTerminalGroups([[{}, { name }]]);
});
it('should create a split terminal when single tab is alt clicked', async () => {
await terminal.createTerminal();
const page = await terminal.getPage();
page.keyboard.down('Alt');
await terminal.clickSingleTab();
page.keyboard.up('Alt');
await terminal.assertTerminalGroups([[{}, {}]]);
});
it('should do nothing when join tabs is run with only one terminal', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.runCommand(TerminalCommandId.Join);
await terminal.assertTerminalGroups([[{}]]);
});
it('should do nothing when join tabs is run with only split terminals', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.runCommand(TerminalCommandId.Split);
await terminal.runCommand(TerminalCommandId.Join);
await terminal.assertTerminalGroups([[{}], [{}]]);
});
it('should join tabs when more than one non-split terminal', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.createTerminal();
await terminal.runCommand(TerminalCommandId.Join);
await terminal.assertTerminalGroups([[{}, {}]]);
});
it('should do nothing when unsplit tabs called with no splits', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.createTerminal();
await terminal.assertTerminalGroups([[{}], [{}]]);
await terminal.runCommand(TerminalCommandId.Unsplit);
await terminal.assertTerminalGroups([[{}], [{}]]);
});
it('should unsplit tabs', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.runCommand(TerminalCommandId.Split);
await terminal.assertTerminalGroups([[{}, {}]]);
await terminal.runCommand(TerminalCommandId.Unsplit);
await terminal.assertTerminalGroups([[{}], [{}]]);
});
it('should move the terminal to the editor area', async () => {
await terminal.runCommand(TerminalCommandId.Show);
await terminal.assertSingleTab({});
await terminal.runCommand(TerminalCommandId.MoveToEditor);
await terminal.assertEditorGroupCount(1);
});
});
}
@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, TerminalCommandId, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
import { setup as setupTerminalEditorsTests } from './terminal-editors.test';
import { setup as setupTerminalInputTests } from './terminal-input.test';
import { setup as setupTerminalPersistenceTests } from './terminal-persistence.test';
import { setup as setupTerminalProfileTests } from './terminal-profiles.test';
import { setup as setupTerminalTabsTests } from './terminal-tabs.test';
import { setup as setupTerminalSplitCwdTests } from './terminal-splitCwd.test';
import { setup as setupTerminalStickyScrollTests } from './terminal-stickyScroll.test';
import { setup as setupTerminalShellIntegrationTests } from './terminal-shellIntegration.test';
export function setup(logger: Logger) {
describe('Terminal', function () {
// Retry tests 3 times to minimize build failures due to any flakiness
this.retries(3);
// Shared before/after handling
installAllHandlers(logger);
let app: Application;
let terminal: Terminal;
before(async function () {
// Fetch terminal automation API
app = this.app as Application;
terminal = app.workbench.terminal;
});
afterEach(async () => {
// Kill all terminals between every test for a consistent testing environment
await terminal.runCommand(TerminalCommandId.KillAll);
});
// https://github.com/microsoft/vscode/issues/216564
// The pty host can crash on Linux in smoke tests for an unknown reason. We need more user
// reports to investigate
setupTerminalEditorsTests({ skipSuite: process.platform === 'linux' });
setupTerminalInputTests({ skipSuite: process.platform === 'linux' });
setupTerminalPersistenceTests({ skipSuite: process.platform === 'linux' });
setupTerminalProfileTests({ skipSuite: process.platform === 'linux' });
setupTerminalTabsTests({ skipSuite: process.platform === 'linux' });
setupTerminalShellIntegrationTests({ skipSuite: process.platform === 'linux' });
setupTerminalStickyScrollTests({ skipSuite: process.platform === 'linux' });
// https://github.com/microsoft/vscode/pull/141974
// Windows is skipped here as well as it was never enabled from the start
setupTerminalSplitCwdTests({ skipSuite: process.platform === 'linux' || process.platform === 'win32' });
});
}
@@ -0,0 +1,275 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'path';
import { Application, ApplicationOptions, Logger, Quality } from '../../../../automation';
import { createApp, timeout, installDiagnosticsHandler, installAppAfterHandler, getRandomUserDataDir, suiteLogsPath, suiteCrashPath } from '../../utils';
export function setup(ensureStableCode: () => string | undefined, logger: Logger) {
describe('Data Loss (insiders -> insiders)', function () {
// Double the timeout since these tests involve 2 startups
this.timeout(4 * 60 * 1000);
let app: Application | undefined = undefined;
// Shared before/after handling
installDiagnosticsHandler(logger, () => app);
installAppAfterHandler(() => app);
it('verifies opened editors are restored', async function () {
app = createApp({
...this.defaultOptions,
logsPath: suiteLogsPath(this.defaultOptions, 'test_verifies_opened_editors_are_restored'),
crashesPath: suiteCrashPath(this.defaultOptions, 'test_verifies_opened_editors_are_restored')
});
await app.start();
// Open 3 editors
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'bin', 'www'));
await app.workbench.quickaccess.runCommand('View: Keep Editor');
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'app.js'));
await app.workbench.quickaccess.runCommand('View: Keep Editor');
await app.workbench.editors.newUntitledFile();
await app.restart();
// Verify 3 editors are open
await app.workbench.editors.selectTab('Untitled-1');
await app.workbench.editors.selectTab('app.js');
await app.workbench.editors.selectTab('www');
await app.stop();
app = undefined;
});
it('verifies editors can save and restore', async function () {
app = createApp({
...this.defaultOptions,
logsPath: suiteLogsPath(this.defaultOptions, 'test_verifies_editors_can_save_and_restore'),
crashesPath: suiteCrashPath(this.defaultOptions, 'test_verifies_editors_can_save_and_restore')
});
await app.start();
const textToType = 'Hello, Code';
// open editor and type
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'app.js'));
await app.workbench.editor.waitForTypeInEditor('app.js', textToType);
await app.workbench.editors.waitForTab('app.js', true);
// save
await app.workbench.editors.saveOpenedFile();
await app.workbench.editors.waitForTab('app.js', false);
// restart
await app.restart();
// verify contents
await app.workbench.editor.waitForEditorContents('app.js', contents => contents.indexOf(textToType) > -1);
await app.stop();
app = undefined;
});
it('verifies that "hot exit" works for dirty files (without delay)', function () {
return testHotExit.call(this, 'test_verifies_that_hot_exit_works_for_dirty_files_without_delay', undefined, undefined);
});
it('verifies that "hot exit" works for dirty files (with delay)', function () {
return testHotExit.call(this, 'test_verifies_that_hot_exit_works_for_dirty_files_with_delay', 2000, undefined);
});
it('verifies that auto save triggers on shutdown', function () {
return testHotExit.call(this, 'test_verifies_that_auto_save_triggers_on_shutdown', undefined, true);
});
async function testHotExit(this: import('mocha').Context, title: string, restartDelay: number | undefined, autoSave: boolean | undefined) {
app = createApp({
...this.defaultOptions,
logsPath: suiteLogsPath(this.defaultOptions, title),
crashesPath: suiteCrashPath(this.defaultOptions, title)
});
await app.start();
if (autoSave) {
await app.workbench.settingsEditor.addUserSetting('files.autoSave', '"afterDelay"');
}
const textToTypeInUntitled = 'Hello from Untitled';
await app.workbench.editors.newUntitledFile();
await app.workbench.editor.waitForTypeInEditor('Untitled-1', textToTypeInUntitled);
await app.workbench.editors.waitForTab('Untitled-1', true);
const textToType = 'Hello, Code';
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'readme.md'));
await app.workbench.editor.waitForTypeInEditor('readme.md', textToType);
await app.workbench.editors.waitForTab('readme.md', !autoSave);
if (typeof restartDelay === 'number') {
// this is an OK use of a timeout in a smoke test:
// we want to simulate a user having typed into
// the editor and pausing for a moment before
// terminating
await timeout(restartDelay);
}
await app.restart();
await app.workbench.editors.waitForTab('readme.md', !autoSave);
await app.workbench.editors.waitForTab('Untitled-1', true);
await app.workbench.editors.selectTab('readme.md');
await app.workbench.editor.waitForEditorContents('readme.md', contents => contents.indexOf(textToType) > -1);
await app.workbench.editors.selectTab('Untitled-1');
await app.workbench.editor.waitForEditorContents('Untitled-1', contents => contents.indexOf(textToTypeInUntitled) > -1);
await app.stop();
app = undefined;
}
});
describe('Data Loss (stable -> insiders)', function () {
// Double the timeout since these tests involve 2 startups
this.timeout(4 * 60 * 1000);
let insidersApp: Application | undefined = undefined;
let stableApp: Application | undefined = undefined;
// Shared before/after handling
installDiagnosticsHandler(logger, () => insidersApp ?? stableApp);
installAppAfterHandler(() => insidersApp ?? stableApp, async () => stableApp?.stop());
it('verifies opened editors are restored', async function () {
const stableCodePath = ensureStableCode();
if (!stableCodePath) {
this.skip();
}
// macOS: the first launch of stable Code will trigger
// additional checks in the OS (notarization validation)
// so it can take a very long time. as such we install
// a retry handler to make sure we do not fail as a
// consequence.
if (process.platform === 'darwin') {
this.retries(2);
}
const userDataDir = getRandomUserDataDir(this.defaultOptions);
const logsPath = suiteLogsPath(this.defaultOptions, 'test_verifies_opened_editors_are_restored_from_stable');
const crashesPath = suiteCrashPath(this.defaultOptions, 'test_verifies_opened_editors_are_restored_from_stable');
const stableOptions: ApplicationOptions = Object.assign({}, this.defaultOptions);
stableOptions.codePath = stableCodePath;
stableOptions.userDataDir = userDataDir;
stableOptions.quality = Quality.Stable;
stableOptions.logsPath = logsPath;
stableOptions.crashesPath = crashesPath;
stableApp = new Application(stableOptions);
await stableApp.start();
// Open 3 editors
await stableApp.workbench.quickaccess.openFile(join(stableApp.workspacePathOrFolder, 'bin', 'www'));
await stableApp.workbench.quickaccess.runCommand('View: Keep Editor');
await stableApp.workbench.quickaccess.openFile(join(stableApp.workspacePathOrFolder, 'app.js'));
await stableApp.workbench.quickaccess.runCommand('View: Keep Editor');
await stableApp.workbench.editors.newUntitledFile();
await stableApp.stop();
stableApp = undefined;
const insiderOptions: ApplicationOptions = Object.assign({}, this.defaultOptions);
insiderOptions.userDataDir = userDataDir;
insiderOptions.logsPath = logsPath;
insiderOptions.crashesPath = crashesPath;
insidersApp = new Application(insiderOptions);
await insidersApp.start();
// Verify 3 editors are open
await insidersApp.workbench.editors.selectTab('Untitled-1');
await insidersApp.workbench.editors.selectTab('app.js');
await insidersApp.workbench.editors.selectTab('www');
await insidersApp.stop();
insidersApp = undefined;
});
it('verifies that "hot exit" works for dirty files (without delay)', async function () {
return testHotExit.call(this, `test_verifies_that_hot_exit_works_for_dirty_files_without_delay_from_stable`, undefined);
});
it('verifies that "hot exit" works for dirty files (with delay)', async function () {
return testHotExit.call(this, `test_verifies_that_hot_exit_works_for_dirty_files_with_delay_from_stable`, 2000);
});
async function testHotExit(this: import('mocha').Context, title: string, restartDelay: number | undefined) {
const stableCodePath = ensureStableCode();
if (!stableCodePath) {
this.skip();
}
const userDataDir = getRandomUserDataDir(this.defaultOptions);
const logsPath = suiteLogsPath(this.defaultOptions, title);
const crashesPath = suiteCrashPath(this.defaultOptions, title);
const stableOptions: ApplicationOptions = Object.assign({}, this.defaultOptions);
stableOptions.codePath = stableCodePath;
stableOptions.userDataDir = userDataDir;
stableOptions.quality = Quality.Stable;
stableOptions.logsPath = logsPath;
stableOptions.crashesPath = crashesPath;
stableApp = new Application(stableOptions);
await stableApp.start();
const textToTypeInUntitled = 'Hello from Untitled';
await stableApp.workbench.editors.newUntitledFile();
await stableApp.workbench.editor.waitForTypeInEditor('Untitled-1', textToTypeInUntitled);
await stableApp.workbench.editors.waitForTab('Untitled-1', true);
const textToType = 'Hello, Code';
await stableApp.workbench.quickaccess.openFile(join(stableApp.workspacePathOrFolder, 'readme.md'));
await stableApp.workbench.editor.waitForTypeInEditor('readme.md', textToType);
await stableApp.workbench.editors.waitForTab('readme.md', true);
if (typeof restartDelay === 'number') {
// this is an OK use of a timeout in a smoke test
// we want to simulate a user having typed into
// the editor and pausing for a moment before
// terminating
await timeout(restartDelay);
}
await stableApp.stop();
stableApp = undefined;
const insiderOptions: ApplicationOptions = Object.assign({}, this.defaultOptions);
insiderOptions.userDataDir = userDataDir;
insiderOptions.logsPath = logsPath;
insiderOptions.crashesPath = crashesPath;
insidersApp = new Application(insiderOptions);
await insidersApp.start();
await insidersApp.workbench.editors.waitForTab('readme.md', true);
await insidersApp.workbench.editors.waitForTab('Untitled-1', true);
await insidersApp.workbench.editors.selectTab('readme.md');
await insidersApp.workbench.editor.waitForEditorContents('readme.md', contents => contents.indexOf(textToType) > -1);
await insidersApp.workbench.editors.selectTab('Untitled-1');
await insidersApp.workbench.editor.waitForEditorContents('Untitled-1', contents => contents.indexOf(textToTypeInUntitled) > -1);
await insidersApp.stop();
insidersApp = undefined;
}
});
}
@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'path';
import { Application, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
export function setup(logger: Logger) {
describe('Launch', () => {
// Shared before/after handling
installAllHandlers(logger, opts => ({ ...opts, userDataDir: join(opts.userDataDir, 'ø') }));
it('verifies that application launches when user data directory has non-ascii characters', async function () {
const app = this.app as Application;
await app.workbench.explorer.openExplorerView();
});
});
}
@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Logger, Application } from '../../../../automation';
import { installAllHandlers } from '../../utils';
export function setup(logger: Logger) {
describe('Localization', () => {
// Shared before/after handling
installAllHandlers(logger, opts => {
opts.snapshots = true; // enable network tab in devtools for tracing since we install an extension
return opts;
});
it('starts with "DE" locale and verifies title and viewlets text is in German', async function () {
const app = this.app as Application;
await app.workbench.extensions.installExtension('ms-ceintl.vscode-language-pack-de', false);
await app.restart({ extraArgs: ['--locale=DE'] });
const result = await app.workbench.localization.getLocalizedStrings();
const localeInfo = await app.workbench.localization.getLocaleInfo();
if (localeInfo.locale === undefined || localeInfo.locale.toLowerCase() !== 'de') {
throw new Error(`The requested locale for VS Code was not German. The received value is: ${localeInfo.locale === undefined ? 'not set' : localeInfo.locale}`);
}
if (localeInfo.language.toLowerCase() !== 'de') {
throw new Error(`The UI language is not German. It is ${localeInfo.language}`);
}
if (result.open.toLowerCase() !== 'öffnen' || result.close.toLowerCase() !== 'schließen' || result.find.toLowerCase() !== 'finden') {
throw new Error(`Received wrong German localized strings: ${JSON.stringify(result, undefined, 0)}`);
}
});
});
}
+411
View File
@@ -0,0 +1,411 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { gracefulify } from 'graceful-fs';
import * as cp from 'child_process';
import * as path from 'path';
import * as os from 'os';
import * as minimist from 'minimist';
import * as rimraf from 'rimraf';
import * as vscodetest from '@vscode/test-electron';
import fetch from 'node-fetch';
import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog, getDevElectronPath, getBuildElectronPath, getBuildVersion } from '../../automation';
import { retry, timeout } from './utils';
import { setup as setupDataLossTests } from './areas/workbench/data-loss.test';
import { setup as setupPreferencesTests } from './areas/preferences/preferences.test';
import { setup as setupSearchTests } from './areas/search/search.test';
import { setup as setupNotebookTests } from './areas/notebook/notebook.test';
import { setup as setupLanguagesTests } from './areas/languages/languages.test';
import { setup as setupStatusbarTests } from './areas/statusbar/statusbar.test';
import { setup as setupExtensionTests } from './areas/extensions/extensions.test';
import { setup as setupMultirootTests } from './areas/multiroot/multiroot.test';
import { setup as setupLocalizationTests } from './areas/workbench/localization.test';
import { setup as setupLaunchTests } from './areas/workbench/launch.test';
import { setup as setupTerminalTests } from './areas/terminal/terminal.test';
import { setup as setupTaskTests } from './areas/task/task.test';
const rootPath = path.join(__dirname, '..', '..', '..');
const [, , ...args] = process.argv;
const opts = minimist(args, {
string: [
'browser',
'build',
'stable-build',
'wait-time',
'test-repo',
'electronArgs'
],
boolean: [
'verbose',
'remote',
'web',
'headless',
'tracing'
],
default: {
verbose: false
}
}) as {
verbose?: boolean;
remote?: boolean;
headless?: boolean;
web?: boolean;
tracing?: boolean;
build?: string;
'stable-build'?: string;
browser?: string;
electronArgs?: string;
};
const logsRootPath = (() => {
const logsParentPath = path.join(rootPath, '.build', 'logs');
let logsName: string;
if (opts.web) {
logsName = 'smoke-tests-browser';
} else if (opts.remote) {
logsName = 'smoke-tests-remote';
} else {
logsName = 'smoke-tests-electron';
}
return path.join(logsParentPath, logsName);
})();
const crashesRootPath = (() => {
const crashesParentPath = path.join(rootPath, '.build', 'crashes');
let crashesName: string;
if (opts.web) {
crashesName = 'smoke-tests-browser';
} else if (opts.remote) {
crashesName = 'smoke-tests-remote';
} else {
crashesName = 'smoke-tests-electron';
}
return path.join(crashesParentPath, crashesName);
})();
const logger = createLogger();
function createLogger(): Logger {
const loggers: Logger[] = [];
// Log to console if verbose
if (opts.verbose) {
loggers.push(new ConsoleLogger());
}
// Prepare logs rot path
fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 3 });
fs.mkdirSync(logsRootPath, { recursive: true });
// Always log to log file
loggers.push(new FileLogger(path.join(logsRootPath, 'smoke-test-runner.log')));
return new MultiLogger(loggers);
}
try {
gracefulify(fs);
} catch (error) {
logger.log(`Error enabling graceful-fs: ${error}`);
}
const testDataPath = path.join(os.tmpdir(), 'vscsmoke');
if (fs.existsSync(testDataPath)) {
rimraf.sync(testDataPath);
}
fs.mkdirSync(testDataPath, { recursive: true });
process.once('exit', () => {
try {
rimraf.sync(testDataPath);
} catch {
// noop
}
});
const testRepoUrl = 'https://github.com/microsoft/vscode-smoketest-express';
const workspacePath = path.join(testDataPath, 'vscode-smoketest-express');
const extensionsPath = path.join(testDataPath, 'extensions-dir');
fs.mkdirSync(extensionsPath, { recursive: true });
function fail(errorMessage): void {
logger.log(errorMessage);
if (!opts.verbose) {
console.error(errorMessage);
}
process.exit(1);
}
let quality: Quality;
let version: string | undefined;
function parseVersion(version: string): { major: number; minor: number; patch: number } {
const [, major, minor, patch] = /^(\d+)\.(\d+)\.(\d+)/.exec(version)!;
return { major: parseInt(major), minor: parseInt(minor), patch: parseInt(patch) };
}
function parseQuality(): Quality {
if (process.env.VSCODE_DEV === '1') {
return Quality.Dev;
}
const quality = process.env.VSCODE_QUALITY ?? '';
switch (quality) {
case 'stable':
return Quality.Stable;
case 'insider':
return Quality.Insiders;
case 'exploration':
return Quality.Exploration;
case 'oss':
return Quality.OSS;
default:
return Quality.Dev;
}
}
//
// #### Electron Smoke Tests ####
//
if (!opts.web) {
let testCodePath = opts.build;
let electronPath: string | undefined;
if (testCodePath) {
electronPath = getBuildElectronPath(testCodePath);
version = getBuildVersion(testCodePath);
} else {
testCodePath = getDevElectronPath();
electronPath = testCodePath;
process.env.VSCODE_REPOSITORY = rootPath;
process.env.VSCODE_DEV = '1';
process.env.VSCODE_CLI = '1';
}
if (!fs.existsSync(electronPath || '')) {
fail(`Cannot find VSCode at ${electronPath}. Please run VSCode once first (scripts/code.sh, scripts\\code.bat) and try again.`);
}
quality = parseQuality();
if (opts.remote) {
logger.log(`Running desktop remote smoke tests against ${electronPath}`);
} else {
logger.log(`Running desktop smoke tests against ${electronPath}`);
}
}
//
// #### Web Smoke Tests ####
//
else {
const testCodeServerPath = opts.build || process.env.VSCODE_REMOTE_SERVER_PATH;
if (typeof testCodeServerPath === 'string') {
if (!fs.existsSync(testCodeServerPath)) {
fail(`Cannot find Code server at ${testCodeServerPath}.`);
} else {
logger.log(`Running web smoke tests against ${testCodeServerPath}`);
}
}
if (!testCodeServerPath) {
process.env.VSCODE_REPOSITORY = rootPath;
process.env.VSCODE_DEV = '1';
process.env.VSCODE_CLI = '1';
logger.log(`Running web smoke out of sources`);
}
quality = parseQuality();
}
logger.log(`VS Code product quality: ${quality}.`);
const userDataDir = path.join(testDataPath, 'd');
async function setupRepository(): Promise<void> {
if (opts['test-repo']) {
logger.log('Copying test project repository:', opts['test-repo']);
rimraf.sync(workspacePath);
// not platform friendly
if (process.platform === 'win32') {
cp.execSync(`xcopy /E "${opts['test-repo']}" "${workspacePath}"\\*`);
} else {
cp.execSync(`cp -R "${opts['test-repo']}" "${workspacePath}"`);
}
} else {
if (!fs.existsSync(workspacePath)) {
logger.log('Cloning test project repository...');
const res = cp.spawnSync('git', ['clone', testRepoUrl, workspacePath], { stdio: 'inherit' });
if (!fs.existsSync(workspacePath)) {
throw new Error(`Clone operation failed: ${res.stderr.toString()}`);
}
} else {
logger.log('Cleaning test project repository...');
cp.spawnSync('git', ['fetch'], { cwd: workspacePath, stdio: 'inherit' });
cp.spawnSync('git', ['reset', '--hard', 'FETCH_HEAD'], { cwd: workspacePath, stdio: 'inherit' });
cp.spawnSync('git', ['clean', '-xdf'], { cwd: workspacePath, stdio: 'inherit' });
}
}
}
async function ensureStableCode(): Promise<void> {
let stableCodePath = opts['stable-build'];
if (!stableCodePath) {
const current = parseVersion(version!);
const versionsReq = await retry(() => measureAndLog(() => fetch('https://update.code.visualstudio.com/api/releases/stable'), 'versionReq', logger), 1000, 20);
if (!versionsReq.ok) {
throw new Error('Could not fetch releases from update server');
}
const versions: string[] = await measureAndLog(() => versionsReq.json(), 'versionReq.json()', logger);
const stableVersion = versions.find(raw => {
const version = parseVersion(raw);
return version.major < current.major || (version.major === current.major && version.minor < current.minor);
});
if (!stableVersion) {
throw new Error(`Could not find suitable stable version for ${version}`);
}
logger.log(`Found VS Code v${version}, downloading previous VS Code version ${stableVersion}...`);
let lastProgressMessage: string | undefined = undefined;
let lastProgressReportedAt = 0;
const stableCodeDestination = path.join(testDataPath, 's');
const stableCodeExecutable = await retry(() => measureAndLog(() => vscodetest.download({
cachePath: stableCodeDestination,
version: stableVersion,
extractSync: true,
reporter: {
report: report => {
let progressMessage = `download stable code progress: ${report.stage}`;
const now = Date.now();
if (progressMessage !== lastProgressMessage || now - lastProgressReportedAt > 10000) {
lastProgressMessage = progressMessage;
lastProgressReportedAt = now;
if (report.stage === 'downloading') {
progressMessage += ` (${report.bytesSoFar}/${report.totalBytes})`;
}
logger.log(progressMessage);
}
},
error: error => logger.log(`download stable code error: ${error}`)
}
}), 'download stable code', logger), 1000, 3, () => new Promise<void>((resolve, reject) => {
rimraf(stableCodeDestination, { maxBusyTries: 10 }, error => {
if (error) {
reject(error);
} else {
resolve();
}
});
}));
if (process.platform === 'darwin') {
// Visual Studio Code.app/Contents/MacOS/Electron
stableCodePath = path.dirname(path.dirname(path.dirname(stableCodeExecutable)));
} else {
// VSCode/Code.exe (Windows) | VSCode/code (Linux)
stableCodePath = path.dirname(stableCodeExecutable);
}
}
if (!fs.existsSync(stableCodePath)) {
throw new Error(`Cannot find Stable VSCode at ${stableCodePath}.`);
}
logger.log(`Using stable build ${stableCodePath} for migration tests`);
opts['stable-build'] = stableCodePath;
}
async function setup(): Promise<void> {
logger.log('Test data path:', testDataPath);
logger.log('Preparing smoketest setup...');
if (!opts.web && !opts.remote && opts.build) {
// only enabled when running with --build and not in web or remote
await measureAndLog(() => ensureStableCode(), 'ensureStableCode', logger);
}
await measureAndLog(() => setupRepository(), 'setupRepository', logger);
logger.log('Smoketest setup done!\n');
}
// Before all tests run setup
before(async function () {
this.timeout(5 * 60 * 1000); // increase since we download VSCode
this.defaultOptions = {
quality,
codePath: opts.build,
workspacePath,
userDataDir,
extensionsPath,
logger,
logsPath: path.join(logsRootPath, 'suite_unknown'),
crashesPath: path.join(crashesRootPath, 'suite_unknown'),
verbose: opts.verbose,
remote: opts.remote,
web: opts.web,
tracing: opts.tracing,
headless: opts.headless,
browser: opts.browser,
extraArgs: (opts.electronArgs || '').split(' ').map(arg => arg.trim()).filter(arg => !!arg)
};
await setup();
});
// After main suite (after all tests)
after(async function () {
try {
let deleted = false;
await measureAndLog(() => Promise.race([
new Promise<void>((resolve, reject) => rimraf(testDataPath, { maxBusyTries: 10 }, error => {
if (error) {
reject(error);
} else {
deleted = true;
resolve();
}
})),
timeout(30000).then(() => {
if (!deleted) {
throw new Error('giving up after 30s');
}
})
]), 'rimraf(testDataPath)', logger);
} catch (error) {
logger.log(`Unable to delete smoke test workspace: ${error}. This indicates some process is locking the workspace folder.`);
}
});
describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => {
if (!opts.web) { setupDataLossTests(() => opts['stable-build'] /* Do not change, deferred for a reason! */, logger); }
setupPreferencesTests(logger);
setupSearchTests(logger);
if (!opts.web) { setupNotebookTests(logger); }
setupLanguagesTests(logger);
setupTerminalTests(logger);
setupTaskTests(logger);
setupStatusbarTests(logger);
if (quality !== Quality.Dev && quality !== Quality.OSS) { setupExtensionTests(logger); }
setupMultirootTests(logger);
if (!opts.web && !opts.remote && quality !== Quality.Dev && quality !== Quality.OSS) { setupLocalizationTests(logger); }
if (!opts.web && !opts.remote) { setupLaunchTests(logger); }
});
+186
View File
@@ -0,0 +1,186 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Suite, Context } from 'mocha';
import { dirname, join } from 'path';
import { Application, ApplicationOptions, Logger } from '../../automation';
export function describeRepeat(n: number, description: string, callback: (this: Suite) => void): void {
for (let i = 0; i < n; i++) {
describe(`${description} (iteration ${i})`, callback);
}
}
export function itRepeat(n: number, description: string, callback: (this: Context) => any): void {
for (let i = 0; i < n; i++) {
it(`${description} (iteration ${i})`, callback);
}
}
export function installAllHandlers(logger: Logger, optionsTransform?: (opts: ApplicationOptions) => ApplicationOptions) {
installDiagnosticsHandler(logger);
installAppBeforeHandler(optionsTransform);
installAppAfterHandler();
}
export function installDiagnosticsHandler(logger: Logger, appFn?: () => Application | undefined) {
// Before each suite
before(async function () {
const suiteTitle = this.currentTest?.parent?.title;
logger.log('');
logger.log(`>>> Suite start: '${suiteTitle ?? 'unknown'}' <<<`);
logger.log('');
});
// Before each test
beforeEach(async function () {
const testTitle = this.currentTest?.title;
logger.log('');
logger.log(`>>> Test start: '${testTitle ?? 'unknown'}' <<<`);
logger.log('');
const app: Application = appFn?.() ?? this.app;
await app?.startTracing(testTitle ?? 'unknown');
});
// After each test
afterEach(async function () {
const currentTest = this.currentTest;
if (!currentTest) {
return;
}
const failed = currentTest.state === 'failed';
const testTitle = currentTest.title;
logger.log('');
if (failed) {
logger.log(`>>> !!! FAILURE !!! Test end: '${testTitle}' !!! FAILURE !!! <<<`);
} else {
logger.log(`>>> Test end: '${testTitle}' <<<`);
}
logger.log('');
const app: Application = appFn?.() ?? this.app;
await app?.stopTracing(testTitle.replace(/[^a-z0-9\-]/ig, '_'), failed);
});
}
let logsCounter = 1;
let crashCounter = 1;
export function suiteLogsPath(options: ApplicationOptions, suiteName: string): string {
return join(dirname(options.logsPath), `${logsCounter++}_suite_${suiteName.replace(/[^a-z0-9\-]/ig, '_')}`);
}
export function suiteCrashPath(options: ApplicationOptions, suiteName: string): string {
return join(dirname(options.crashesPath), `${crashCounter++}_suite_${suiteName.replace(/[^a-z0-9\-]/ig, '_')}`);
}
function installAppBeforeHandler(optionsTransform?: (opts: ApplicationOptions) => ApplicationOptions) {
before(async function () {
const suiteName = this.test?.parent?.title ?? 'unknown';
this.app = createApp({
...this.defaultOptions,
logsPath: suiteLogsPath(this.defaultOptions, suiteName),
crashesPath: suiteCrashPath(this.defaultOptions, suiteName)
}, optionsTransform);
await this.app.start();
});
}
export function installAppAfterHandler(appFn?: () => Application | undefined, joinFn?: () => Promise<unknown>) {
after(async function () {
const app: Application = appFn?.() ?? this.app;
if (app) {
await app.stop();
}
if (joinFn) {
await joinFn();
}
});
}
export function createApp(options: ApplicationOptions, optionsTransform?: (opts: ApplicationOptions) => ApplicationOptions): Application {
if (optionsTransform) {
options = optionsTransform({ ...options });
}
const app = new Application({
...options,
userDataDir: getRandomUserDataDir(options)
});
return app;
}
export function getRandomUserDataDir(options: ApplicationOptions): string {
// Pick a random user data dir suffix that is not
// too long to not run into max path length issues
// https://github.com/microsoft/vscode/issues/34988
const userDataPathSuffix = [...Array(8)].map(() => Math.random().toString(36)[3]).join('');
return options.userDataDir.concat(`-${userDataPathSuffix}`);
}
export function timeout(i: number) {
return new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, i);
});
}
export async function retryWithRestart(app: Application, testFn: () => Promise<unknown>, retries = 3, timeoutMs = 20000): Promise<unknown> {
let lastError: Error | undefined = undefined;
for (let i = 0; i < retries; i++) {
const result = await Promise.race([
testFn().then(() => true, error => {
lastError = error;
return false;
}),
timeout(timeoutMs).then(() => false)
]);
if (result) {
return;
}
await app.restart();
}
throw lastError ?? new Error('retryWithRestart failed with an unknown error');
}
export interface ITask<T> {
(): T;
}
export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number, onBeforeRetry?: () => Promise<unknown>): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i < retries; i++) {
try {
if (i > 0 && typeof onBeforeRetry === 'function') {
try {
await onBeforeRetry();
} catch (error) {
console.warn(`onBeforeRetry failed with: ${error}`);
}
}
return await task();
} catch (error) {
lastError = error as Error;
await timeout(delay);
}
}
throw lastError;
}
+75
View File
@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const { join } = require('path');
const Mocha = require('mocha');
const minimist = require('minimist');
const [, , ...args] = process.argv;
const opts = minimist(args, {
boolean: ['web'],
string: ['f', 'g']
});
const suite = opts['web'] ? 'Browser Smoke Tests' : 'Desktop Smoke Tests';
const options = {
color: true,
timeout: 2 * 60 * 1000,
slow: 30 * 1000,
grep: opts['f'] || opts['g']
};
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
const mocha = new Mocha(options);
mocha.addFile('out/main.js');
mocha.run(failures => {
// Indicate location of log files for further diagnosis
if (failures) {
const rootPath = join(__dirname, '..', '..', '..');
const logPath = join(rootPath, '.build', 'logs');
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
console.log(`
###################################################################
# #
# Logs are attached as build artefact and can be downloaded #
# from the build Summary page (Summary -> Related -> N published) #
# #
# Show playwright traces on: https://trace.playwright.dev/ #
# #
###################################################################
`);
} else {
console.log(`
#############################################
#
# Log files of client & server are stored into
# '${logPath}'.
#
# Logs of the smoke test runner are stored into
# 'smoke-test-runner.log' in respective folder.
#
#############################################
`);
}
}
process.exit(failures ? -1 : 0);
});
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": false,
"removeComments": false,
"preserveConstEnums": true,
"target": "es2020",
"strict": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"outDir": "out",
"sourceMap": true,
"skipLibCheck": true,
"lib": [
"esnext", // for #201187
"dom"
]
},
"exclude": [
"node_modules"
]
}

Some files were not shown because too many files have changed in this diff Show More