Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

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))
* `ui`: our suite of manual UI tests

8
lib/vscode/test/automation/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
npm-debug.log
Thumbs.db
node_modules/
out/
keybindings.*.json
src/driver.d.ts
*.tgz

View File

@ -0,0 +1,6 @@
!/out
/src
/tools
.gitignore
tsconfig.json
*.tgz

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.

View File

@ -0,0 +1,40 @@
{
"name": "vscode-automation",
"version": "1.39.0",
"description": "VS Code UI automation driver",
"author": {
"name": "Microsoft Corporation"
},
"license": "MIT",
"main": "./out/index.js",
"private": true,
"scripts": {
"prepare": "npm run compile",
"compile": "npm run copy-driver && npm run copy-driver-definition && tsc",
"watch": "concurrently \"npm run watch-driver\" \"npm run watch-driver-definition\" \"tsc --watch\"",
"copy-driver": "cpx src/driver.js out/",
"watch-driver": "cpx src/driver.js out/ -w",
"copy-driver-definition": "node tools/copy-driver-definition.js",
"watch-driver-definition": "watch \"node tools/copy-driver-definition.js\" ../../src/vs/platform/driver/node",
"copy-package-version": "node tools/copy-package-version.js",
"prepublishOnly": "npm run copy-package-version"
},
"devDependencies": {
"@types/debug": "4.1.5",
"@types/mkdirp": "0.5.1",
"@types/ncp": "2.0.1",
"@types/node": "^12.11.7",
"@types/tmp": "0.1.0",
"concurrently": "^3.5.1",
"cpx": "^1.5.0",
"typescript": "3.7.5",
"watch": "^1.0.2",
"tree-kill": "1.2.2"
},
"dependencies": {
"mkdirp": "^0.5.1",
"ncp": "^2.0.0",
"tmp": "0.1.0",
"vscode-uri": "^2.0.3"
}
}

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}`);
}
}

View File

@ -0,0 +1,152 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import { Workbench } from './workbench';
import { Code, spawn, SpawnOptions } from './code';
import { Logger } from './logger';
export const enum Quality {
Dev,
Insiders,
Stable
}
export interface ApplicationOptions extends SpawnOptions {
quality: Quality;
workspacePath: string;
waitTime: number;
screenshotsPath: string | null;
}
export class Application {
private _code: Code | undefined;
private _workbench: Workbench | undefined;
constructor(private options: ApplicationOptions) {
this._workspacePathOrFolder = options.workspacePath;
}
get quality(): Quality {
return this.options.quality;
}
get code(): Code {
return this._code!;
}
get workbench(): Workbench {
return this._workbench!;
}
get logger(): Logger {
return this.options.logger;
}
get remote(): boolean {
return !!this.options.remote;
}
private _workspacePathOrFolder: string;
get workspacePathOrFolder(): string {
return this._workspacePathOrFolder;
}
get extensionsPath(): string {
return this.options.extensionsPath;
}
get userDataPath(): string {
return this.options.userDataDir;
}
async start(expectWalkthroughPart = true): Promise<any> {
await this._start();
await this.code.waitForElement('.explorer-folders-view');
if (expectWalkthroughPart) {
await this.code.waitForActiveElement(`.editor-instance[data-editor-id="workbench.editor.walkThroughPart"] > div > div[tabIndex="0"]`);
}
}
async restart(options: { workspaceOrFolder?: string, extraArgs?: string[] }): Promise<any> {
await this.stop();
await new Promise(c => setTimeout(c, 1000));
await this._start(options.workspaceOrFolder, options.extraArgs);
}
private async _start(workspaceOrFolder = this.workspacePathOrFolder, extraArgs: string[] = []): Promise<any> {
this._workspacePathOrFolder = workspaceOrFolder;
await this.startApplication(extraArgs);
await this.checkWindowReady();
}
async reload(): Promise<any> {
this.code.reload()
.catch(err => null); // ignore the connection drop errors
// needs to be enough to propagate the 'Reload Window' command
await new Promise(c => setTimeout(c, 1500));
await this.checkWindowReady();
}
async stop(): Promise<any> {
if (this._code) {
await this._code.exit();
this._code.dispose();
this._code = undefined;
}
}
async captureScreenshot(name: string): Promise<void> {
if (this.options.screenshotsPath) {
const raw = await this.code.capturePage();
const buffer = Buffer.from(raw, 'base64');
const screenshotPath = path.join(this.options.screenshotsPath, `${name}.png`);
if (this.options.log) {
this.logger.log('*** Screenshot recorded:', screenshotPath);
}
fs.writeFileSync(screenshotPath, buffer);
}
}
private async startApplication(extraArgs: string[] = []): Promise<any> {
this._code = await spawn({
codePath: this.options.codePath,
workspacePath: this.workspacePathOrFolder,
userDataDir: this.options.userDataDir,
extensionsPath: this.options.extensionsPath,
logger: this.options.logger,
verbose: this.options.verbose,
log: this.options.log,
extraArgs,
remote: this.options.remote,
web: this.options.web,
browser: this.options.browser
});
this._workbench = new Workbench(this._code, this.userDataPath);
}
private async checkWindowReady(): Promise<any> {
if (!this.code) {
console.error('No code instance found');
return;
}
await this.code.waitForWindowIds(ids => ids.length > 0);
await this.code.waitForElement('.monaco-workbench');
if (this.remote) {
await this.code.waitForTextContent('.monaco-workbench .statusbar-item[id="status.host"]', ' TestResolver');
}
// wait a bit, since focus might be stolen off widgets
// as soon as they open (e.g. quick access)
await new Promise(c => setTimeout(c, 1000));
}
}

View File

@ -0,0 +1,391 @@
/*---------------------------------------------------------------------------------------------
* 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 os from 'os';
import * as fs from 'fs';
import * as mkdirp from 'mkdirp';
import { tmpName } from 'tmp';
import { IDriver, connect as connectElectronDriver, IDisposable, IElement, Thenable } from './driver';
import { connect as connectPlaywrightDriver, launch } from './playwrightDriver';
import { Logger } from './logger';
import { ncp } from 'ncp';
import { URI } from 'vscode-uri';
const repoPath = path.join(__dirname, '../../..');
function getDevElectronPath(): string {
const buildPath = path.join(repoPath, '.build');
const product = require(path.join(repoPath, 'product.json'));
switch (process.platform) {
case 'darwin':
return path.join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron');
case 'linux':
return path.join(buildPath, 'electron', `${product.applicationName}`);
case 'win32':
return path.join(buildPath, 'electron', `${product.nameShort}.exe`);
default:
throw new Error('Unsupported platform.');
}
}
function getBuildElectronPath(root: string): string {
switch (process.platform) {
case 'darwin':
return path.join(root, 'Contents', 'MacOS', 'Electron');
case 'linux': {
const product = require(path.join(root, 'resources', 'app', 'product.json'));
return path.join(root, product.applicationName);
}
case 'win32': {
const product = require(path.join(root, 'resources', 'app', 'product.json'));
return path.join(root, `${product.nameShort}.exe`);
}
default:
throw new Error('Unsupported platform.');
}
}
function getDevOutPath(): string {
return path.join(repoPath, 'out');
}
function getBuildOutPath(root: string): string {
switch (process.platform) {
case 'darwin':
return path.join(root, 'Contents', 'Resources', 'app', 'out');
default:
return path.join(root, 'resources', 'app', 'out');
}
}
async function connect(connectDriver: typeof connectElectronDriver, child: cp.ChildProcess | undefined, outPath: string, handlePath: string, logger: Logger): Promise<Code> {
let errCount = 0;
while (true) {
try {
const { client, driver } = await connectDriver(outPath, handlePath);
return new Code(client, driver, logger);
} catch (err) {
if (++errCount > 50) {
if (child) {
child.kill();
}
throw err;
}
// retry
await new Promise(c => setTimeout(c, 100));
}
}
}
// Kill all running instances, when dead
const instances = new Set<cp.ChildProcess>();
process.once('exit', () => instances.forEach(code => code.kill()));
export interface SpawnOptions {
codePath?: string;
workspacePath: string;
userDataDir: string;
extensionsPath: string;
logger: Logger;
verbose?: boolean;
extraArgs?: string[];
log?: string;
/** Run in the test resolver */
remote?: boolean;
/** Run in the web */
web?: boolean;
/** A specific browser to use (requires web: true) */
browser?: 'chromium' | 'webkit' | 'firefox';
}
async function createDriverHandle(): Promise<string> {
if ('win32' === os.platform()) {
const name = [...Array(15)].map(() => Math.random().toString(36)[3]).join('');
return `\\\\.\\pipe\\${name}`;
} else {
return await new Promise<string>((c, e) => tmpName((err, handlePath) => err ? e(err) : c(handlePath)));
}
}
export async function spawn(options: SpawnOptions): Promise<Code> {
const handle = await createDriverHandle();
let child: cp.ChildProcess | undefined;
let connectDriver: typeof connectElectronDriver;
copyExtension(options, 'vscode-notebook-tests');
if (options.web) {
await launch(options.userDataDir, options.workspacePath, options.codePath, options.extensionsPath);
connectDriver = connectPlaywrightDriver.bind(connectPlaywrightDriver, options.browser);
return connect(connectDriver, child, '', handle, options.logger);
}
const env = process.env;
const codePath = options.codePath;
const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath();
const args = [
options.workspacePath,
'--skip-release-notes',
'--disable-telemetry',
'--no-cached-data',
'--disable-updates',
'--disable-crash-reporter',
`--extensions-dir=${options.extensionsPath}`,
`--user-data-dir=${options.userDataDir}`,
'--driver', handle
];
if (options.remote) {
// Replace workspace path with URI
args[0] = `--${options.workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(options.workspacePath).path}`;
if (codePath) {
// running against a build: copy the test resolver extension
copyExtension(options, 'vscode-test-resolver');
}
args.push('--enable-proposed-api=vscode.vscode-test-resolver');
const remoteDataDir = `${options.userDataDir}-server`;
mkdirp.sync(remoteDataDir);
env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir;
}
args.push('--enable-proposed-api=vscode.vscode-notebook-tests');
if (!codePath) {
args.unshift(repoPath);
}
if (options.verbose) {
args.push('--driver-verbose');
}
if (options.log) {
args.push('--log', options.log);
}
if (options.extraArgs) {
args.push(...options.extraArgs);
}
const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath();
const spawnOptions: cp.SpawnOptions = { env };
child = cp.spawn(electronPath, args, spawnOptions);
instances.add(child);
child.once('exit', () => instances.delete(child!));
connectDriver = connectElectronDriver;
return connect(connectDriver, child, outPath, handle, options.logger);
}
async function copyExtension(options: SpawnOptions, extId: string): Promise<void> {
const testResolverExtPath = path.join(options.extensionsPath, extId);
if (!fs.existsSync(testResolverExtPath)) {
const orig = path.join(repoPath, 'extensions', extId);
await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c()));
}
}
async function poll<T>(
fn: () => Thenable<T>,
acceptFn: (result: T) => boolean,
timeoutMessage: string,
retryCount: number = 200,
retryInterval: number = 100 // millis
): Promise<T> {
let trial = 1;
let lastError: string = '';
while (true) {
if (trial > retryCount) {
console.error('** Timeout!');
console.error(lastError);
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) {
lastError = Array.isArray(e.stack) ? e.stack.join(os.EOL) : e.stack;
}
await new Promise(resolve => setTimeout(resolve, retryInterval));
trial++;
}
}
export class Code {
private _activeWindowId: number | undefined = undefined;
private driver: IDriver;
constructor(
private client: IDisposable,
driver: IDriver,
readonly logger: Logger
) {
this.driver = new Proxy(driver, {
get(target, prop, receiver) {
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 capturePage(): Promise<string> {
const windowId = await this.getActiveWindowId();
return await this.driver.capturePage(windowId);
}
async waitForWindowIds(fn: (windowIds: number[]) => boolean): Promise<void> {
await poll(() => this.driver.getWindowIds(), fn, `get window ids`);
}
async dispatchKeybinding(keybinding: string): Promise<void> {
const windowId = await this.getActiveWindowId();
await this.driver.dispatchKeybinding(windowId, keybinding);
}
async reload(): Promise<void> {
const windowId = await this.getActiveWindowId();
await this.driver.reloadWindow(windowId);
}
async exit(): Promise<void> {
await this.driver.exitApplication();
}
async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean): Promise<string> {
const windowId = await this.getActiveWindowId();
accept = accept || (result => textContent !== undefined ? textContent === result : !!result);
return await poll(
() => this.driver.getElements(windowId, 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}'`
);
}
async waitAndClick(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
const windowId = await this.getActiveWindowId();
await poll(() => this.driver.click(windowId, selector, xoffset, yoffset), () => true, `click '${selector}'`);
}
async waitAndDoubleClick(selector: string): Promise<void> {
const windowId = await this.getActiveWindowId();
await poll(() => this.driver.doubleClick(windowId, selector), () => true, `double click '${selector}'`);
}
async waitForSetValue(selector: string, value: string): Promise<void> {
const windowId = await this.getActiveWindowId();
await poll(() => this.driver.setValue(windowId, selector, value), () => true, `set value '${selector}'`);
}
async waitForElements(selector: string, recursive: boolean, accept: (result: IElement[]) => boolean = result => result.length > 0): Promise<IElement[]> {
const windowId = await this.getActiveWindowId();
return await poll(() => this.driver.getElements(windowId, selector, recursive), accept, `get elements '${selector}'`);
}
async waitForElement(selector: string, accept: (result: IElement | undefined) => boolean = result => !!result, retryCount: number = 200): Promise<IElement> {
const windowId = await this.getActiveWindowId();
return await poll<IElement>(() => this.driver.getElements(windowId, selector).then(els => els[0]), accept, `get element '${selector}'`, retryCount);
}
async waitForActiveElement(selector: string, retryCount: number = 200): Promise<void> {
const windowId = await this.getActiveWindowId();
await poll(() => this.driver.isActiveElement(windowId, selector), r => r, `is active element '${selector}'`, retryCount);
}
async waitForTitle(fn: (title: string) => boolean): Promise<void> {
const windowId = await this.getActiveWindowId();
await poll(() => this.driver.getTitle(windowId), fn, `get title`);
}
async waitForTypeInEditor(selector: string, text: string): Promise<void> {
const windowId = await this.getActiveWindowId();
await poll(() => this.driver.typeInEditor(windowId, selector, text), () => true, `type in editor '${selector}'`);
}
async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise<void> {
const windowId = await this.getActiveWindowId();
await poll(() => this.driver.getTerminalBuffer(windowId, selector), accept, `get terminal buffer '${selector}'`);
}
async writeInTerminal(selector: string, value: string): Promise<void> {
const windowId = await this.getActiveWindowId();
await poll(() => this.driver.writeInTerminal(windowId, selector, value), () => true, `writeInTerminal '${selector}'`);
}
private async getActiveWindowId(): Promise<number> {
if (typeof this._activeWindowId !== 'number') {
const windows = await this.driver.getWindowIds();
this._activeWindowId = windows[0];
}
return this._activeWindowId;
}
dispose(): void {
this.client.dispose();
}
}
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;
}

View File

@ -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 { Viewlet } from './viewlet';
import { Commands } from './workbench';
import { Code, findElement } from './code';
import { Editors } from './editors';
import { Editor } from './editor';
import { IElement } from '../src/driver';
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 = '.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> {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+shift+d');
} else {
await this.code.dispatchKeybinding('ctrl+shift+d');
}
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.dispatchKeybinding('f5');
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');
await this.code.waitForActiveElement(REPL_FOCUSED);
await this.code.waitForSetValue(REPL_FOCUSED, text);
// Wait for the keys to be picked up by the editor model such that repl evalutes what just got typed
await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0);
await this.code.dispatchKeybinding('enter');
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);
}
}

View File

@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* 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');
exports.connect = function (outPath, handle) {
const bootstrapPath = path.join(outPath, 'bootstrap-amd.js');
const { load } = require(bootstrapPath);
return new Promise((c, e) => load('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(c, e), e));
};

View File

@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* 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';
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.dispatchKeybinding('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;
}
async waitForHighlightingLine(filename: string, line: number): Promise<void> {
const currentLineIndex = await this.getViewLineIndex(filename, line);
if (currentLineIndex) {
await this.code.waitForElement(`.monaco-editor .view-overlays>:nth-child(${currentLineIndex}) .current-line`);
return;
}
throw new Error('Cannot find line ' + line);
}
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 textarea = `${editor} textarea`;
await this.code.waitAndClick(line, 1, 1);
await this.code.waitForActiveElement(textarea);
}
async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise<any> {
const editor = [selectorPrefix || '', EDITOR(filename)].join(' ');
await this.code.waitForElement(editor);
const textarea = `${editor} textarea`;
await this.code.waitForActiveElement(textarea);
await this.code.waitForTypeInEditor(textarea, text);
await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix);
}
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');
}
}

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.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
export class Editors {
constructor(private code: Code) { }
async saveOpenedFile(): Promise<any> {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+s');
} else {
await this.code.dispatchKeybinding('ctrl+s');
}
}
async selectTab(fileName: string): Promise<void> {
await this.code.waitAndClick(`.tabs-container div.tab[data-resource-name$="${fileName}"]`);
await this.waitForEditorFocus(fileName);
}
async waitForActiveEditor(fileName: string): Promise<any> {
const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`;
return this.code.waitForActiveElement(selector);
}
async waitForEditorFocus(fileName: string): Promise<void> {
await this.waitForActiveTab(fileName);
await this.waitForActiveEditor(fileName);
}
async waitForActiveTab(fileName: string, isDirty: boolean = false): Promise<void> {
await this.code.waitForElement(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][data-resource-name$="${fileName}"]`);
}
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> {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+n');
} else {
await this.code.dispatchKeybinding('ctrl+n');
}
await this.waitForEditorFocus('Untitled-1');
}
}

View File

@ -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 { Viewlet } from './viewlet';
import { Editors } from './editors';
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, private editors: Editors) {
super(code);
}
async openExplorerView(): Promise<any> {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+shift+e');
} else {
await this.code.dispatchKeybinding('ctrl+shift+e');
}
}
async waitForOpenEditorsViewTitle(fn: (title: string) => boolean): Promise<void> {
await this.code.waitForTextContent(Explorer.OPEN_EDITORS_VIEW, undefined, fn);
}
async openFile(fileName: string): Promise<any> {
await this.code.waitAndDoubleClick(`div[class="monaco-icon-label file-icon ${fileName}-name-file-icon ${this.getExtensionSelector(fileName)} explorer-item"]`);
await this.editors.waitForEditorFocus(fileName);
}
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');
}
}

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';
const SEARCH_BOX = 'div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea';
export class Extensions extends Viewlet {
constructor(code: Code) {
super(code);
}
async openExtensionsViewlet(): Promise<any> {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+shift+x');
} else {
await this.code.dispatchKeybinding('ctrl+shift+x');
}
await this.code.waitForActiveElement(SEARCH_BOX);
}
async waitForExtensionsViewlet(): Promise<any> {
await this.code.waitForElement(SEARCH_BOX);
}
async searchForExtension(id: string): Promise<any> {
await this.code.waitAndClick(SEARCH_BOX);
await this.code.waitForActiveElement(SEARCH_BOX);
await this.code.waitForTypeInEditor(SEARCH_BOX, `@id:${id}`);
}
async installExtension(id: string): Promise<void> {
await this.searchForExtension(id);
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`);
await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`);
}
}

View File

@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* 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 './workbench';
export * from './driver';

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 { Code } from './code';
const SEARCH_INPUT = '.keybindings-header .settings-search-input input';
export class KeybindingsEditor {
constructor(private code: Code) { }
async updateKeybinding(command: string, keybinding: string, title: string): Promise<any> {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+k cmd+s');
} else {
await this.code.dispatchKeybinding('ctrl+k ctrl+s');
}
await this.code.waitForActiveElement(SEARCH_INPUT);
await this.code.waitForSetValue(SEARCH_INPUT, command);
await this.code.waitAndClick('.keybindings-list-container .monaco-list-row.keybinding-item');
await this.code.waitForElement('.keybindings-list-container .monaco-list-row.keybinding-item.focused.selected');
await this.code.waitAndClick('.keybindings-list-container .monaco-list-row.keybinding-item .action-item .codicon.codicon-add');
await this.code.waitForActiveElement('.defineKeybindingWidget .monaco-inputbox input');
await this.code.dispatchKeybinding(keybinding);
await this.code.dispatchKeybinding('enter');
await this.code.waitForElement(`.keybindings-list-container .keybinding-label div[title="${title}"]`);
}
}

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 { 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);
}
}
}

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.
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
import { QuickAccess } from './quickaccess';
const activeRowSelector = `.notebook-editor .monaco-list-row.focused`;
export class Notebook {
constructor(
private readonly quickAccess: QuickAccess,
private readonly code: Code) {
}
async openNotebook() {
await this.quickAccess.runCommand('vscode-notebook-tests.createNewNotebook');
await this.code.waitForElement(activeRowSelector);
await this.focusFirstCell();
await this.waitForActiveCellEditorContents('code()');
}
async focusNextCell() {
await this.code.dispatchKeybinding('down');
}
async focusFirstCell() {
await this.quickAccess.runCommand('notebook.focusTop');
}
async editCell() {
await this.code.dispatchKeybinding('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 textarea = `${editor} textarea`;
await this.code.waitForActiveElement(textarea);
await this.code.waitForTypeInEditor(textarea, 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);
}
}

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.
*--------------------------------------------------------------------------------------------*/
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) {
await this.code.dispatchKeybinding('escape');
try {
await this.code.waitForElement(References.REFERENCES_WIDGET, el => !el, 10);
return;
} catch (err) {
if (++count > 5) {
throw err;
}
}
}
}
}

View File

@ -0,0 +1,157 @@
/*---------------------------------------------------------------------------------------------
* 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';
import { ChildProcess, spawn } from 'child_process';
import { join } from 'path';
import { mkdir } from 'fs';
import { promisify } from 'util';
import { IDriver, IDisposable } from './driver';
import { URI } from 'vscode-uri';
import * as kill from 'tree-kill';
const width = 1200;
const height = 800;
const 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'
};
function buildDriver(browser: playwright.Browser, page: playwright.Page): IDriver {
const driver: IDriver = {
_serviceBrand: undefined,
getWindowIds: () => {
return Promise.resolve([1]);
},
capturePage: () => Promise.resolve(''),
reloadWindow: (windowId) => Promise.resolve(),
exitApplication: () => browser.close(),
dispatchKeybinding: async (windowId, keybinding) => {
const chords = keybinding.split(' ');
for (let i = 0; i < chords.length; i++) {
const chord = chords[i];
if (i > 0) {
await timeout(100);
}
const keys = chord.split('+');
const keysDown: string[] = [];
for (let i = 0; i < keys.length; i++) {
if (keys[i] in vscodeToPlaywrightKey) {
keys[i] = vscodeToPlaywrightKey[keys[i]];
}
await page.keyboard.down(keys[i]);
keysDown.push(keys[i]);
}
while (keysDown.length > 0) {
await page.keyboard.up(keysDown.pop()!);
}
}
await timeout(100);
},
click: async (windowId, selector, xoffset, yoffset) => {
const { x, y } = await driver.getElementXY(windowId, selector, xoffset, yoffset);
await page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0));
},
doubleClick: async (windowId, selector) => {
await driver.click(windowId, selector, 0, 0);
await timeout(60);
await driver.click(windowId, selector, 0, 0);
await timeout(100);
},
setValue: async (windowId, selector, text) => page.evaluate(`window.driver.setValue('${selector}', '${text}')`).then(undefined),
getTitle: (windowId) => page.evaluate(`window.driver.getTitle()`),
isActiveElement: (windowId, selector) => page.evaluate(`window.driver.isActiveElement('${selector}')`),
getElements: (windowId, selector, recursive) => page.evaluate(`window.driver.getElements('${selector}', ${recursive})`),
getElementXY: (windowId, selector, xoffset?, yoffset?) => page.evaluate(`window.driver.getElementXY('${selector}', ${xoffset}, ${yoffset})`),
typeInEditor: (windowId, selector, text) => page.evaluate(`window.driver.typeInEditor('${selector}', '${text}')`),
getTerminalBuffer: (windowId, selector) => page.evaluate(`window.driver.getTerminalBuffer('${selector}')`),
writeInTerminal: (windowId, selector, text) => page.evaluate(`window.driver.writeInTerminal('${selector}', '${text}')`)
};
return driver;
}
function timeout(ms: number): Promise<void> {
return new Promise<void>(r => setTimeout(r, ms));
}
let server: ChildProcess | undefined;
let endpoint: string | undefined;
let workspacePath: string | undefined;
export async function launch(userDataDir: string, _workspacePath: string, codeServerPath = process.env.VSCODE_REMOTE_SERVER_PATH, extPath: string): Promise<void> {
workspacePath = _workspacePath;
const agentFolder = userDataDir;
await promisify(mkdir)(agentFolder);
const env = {
VSCODE_AGENT_FOLDER: agentFolder,
VSCODE_REMOTE_SERVER_PATH: codeServerPath,
...process.env
};
let serverLocation: string | undefined;
if (codeServerPath) {
serverLocation = join(codeServerPath, `server.${process.platform === 'win32' ? 'cmd' : 'sh'}`);
console.log(`Starting built server from '${serverLocation}'`);
} else {
serverLocation = join(__dirname, '..', '..', '..', `resources/server/web.${process.platform === 'win32' ? 'bat' : 'sh'}`);
console.log(`Starting server out of sources from '${serverLocation}'`);
}
server = spawn(
serverLocation,
['--browser', 'none', '--driver', 'web', '--extensions-dir', extPath],
{ env }
);
server.stderr?.on('data', error => console.log(`Server stderr: ${error}`));
server.stdout?.on('data', data => console.log(`Server stdout: ${data}`));
process.on('exit', teardown);
process.on('SIGINT', teardown);
process.on('SIGTERM', teardown);
endpoint = await waitForEndpoint();
}
function teardown(): void {
if (server) {
kill(server.pid);
server = undefined;
}
}
function waitForEndpoint(): Promise<string> {
return new Promise<string>(r => {
server!.stdout?.on('data', (d: Buffer) => {
const matches = d.toString('ascii').match(/Web UI available at (.+)/);
if (matches !== null) {
r(matches[1]);
}
});
});
}
export function connect(browserType: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> {
return new Promise(async (c) => {
const browser = await playwright[browserType].launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
await page.setViewportSize({ width, height });
const payloadParam = `[["enableProposedApi",""]]`;
await page.goto(`${endpoint}&folder=vscode-remote://localhost:9888${URI.file(workspacePath!).path}&payload=${payloadParam}`);
const result = {
client: { dispose: () => browser.close() && teardown() },
driver: buildDriver(browser, page)
};
c(result);
});
}

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) { }
public async showProblemsView(): Promise<any> {
await this.quickAccess.runCommand('workbench.panel.markers.view.focus');
await this.waitForProblemsView();
}
public async hideProblemsView(): Promise<any> {
await this.quickAccess.runCommand('workbench.actions.view.problems');
await this.code.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR, el => !el);
}
public async waitForProblemsView(): Promise<void> {
await this.code.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR);
}
public static getSelectorInProblemsView(problemType: ProblemSeverity): string {
let selector = problemType === ProblemSeverity.WARNING ? 'codicon-warning' : 'codicon-error';
return `div[id="workbench.panel.markers"] .monaco-tl-contents .marker-icon.${selector}`;
}
public static getSelectorInEditor(problemType: ProblemSeverity): string {
let selector = problemType === ProblemSeverity.WARNING ? 'squiggly-warning' : 'squiggly-error';
return `.view-overlays .cdr.${selector}`;
}
}

View File

@ -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 { Editors } from './editors';
import { Code } from './code';
import { QuickInput } from './quickinput';
export class QuickAccess {
constructor(private code: Code, private editors: Editors, private quickInput: QuickInput) { }
async openQuickAccess(value: string): Promise<void> {
let retries = 0;
// other parts of code might steal focus away from quickinput :(
while (retries < 5) {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+p');
} else {
await this.code.dispatchKeybinding('ctrl+p');
}
try {
await this.quickInput.waitForQuickInputOpened(10);
break;
} catch (err) {
if (++retries > 5) {
throw err;
}
await this.code.dispatchKeybinding('escape');
}
}
if (value) {
await this.code.waitForSetValue(QuickInput.QUICK_INPUT_INPUT, value);
}
}
async openFile(fileName: string): Promise<void> {
await this.openQuickAccess(fileName);
await this.quickInput.waitForQuickInputElements(names => names[0] === fileName);
await this.code.dispatchKeybinding('enter');
await this.editors.waitForActiveTab(fileName);
await this.editors.waitForEditorFocus(fileName);
}
async runCommand(commandId: string): Promise<void> {
await this.openQuickAccess(`>${commandId}`);
// wait for best choice to be focused
await this.code.waitForTextContent(QuickInput.QUICK_INPUT_FOCUSED_ELEMENT);
// wait and click on best choice
await this.quickInput.selectQuickInputElement(0);
}
async openQuickOutline(): Promise<void> {
let retries = 0;
while (++retries < 10) {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+shift+o');
} else {
await this.code.dispatchKeybinding('ctrl+shift+o');
}
const text = await this.code.waitForTextContent(QuickInput.QUICK_INPUT_ENTRY_LABEL_SPAN);
if (text !== 'No symbol information for the file') {
return;
}
await this.quickInput.closeQuickInput();
await new Promise(c => setTimeout(c, 250));
}
}
}

View File

@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* 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 {
static QUICK_INPUT = '.quick-input-widget';
static QUICK_INPUT_INPUT = `${QuickInput.QUICK_INPUT} .quick-input-box input`;
static QUICK_INPUT_ROW = `${QuickInput.QUICK_INPUT} .quick-input-list .monaco-list-row`;
static QUICK_INPUT_FOCUSED_ELEMENT = `${QuickInput.QUICK_INPUT_ROW}.focused .monaco-highlighted-label`;
static QUICK_INPUT_ENTRY_LABEL = `${QuickInput.QUICK_INPUT_ROW} .label-name`;
static QUICK_INPUT_ENTRY_LABEL_SPAN = `${QuickInput.QUICK_INPUT_ROW} .monaco-highlighted-label span`;
constructor(private code: Code) { }
async submit(text: string): Promise<void> {
await this.code.waitForSetValue(QuickInput.QUICK_INPUT_INPUT, text);
await this.code.dispatchKeybinding('enter');
await this.waitForQuickInputClosed();
}
async closeQuickInput(): Promise<void> {
await this.code.dispatchKeybinding('escape');
await this.waitForQuickInputClosed();
}
async waitForQuickInputOpened(retryCount?: number): Promise<void> {
await this.code.waitForActiveElement(QuickInput.QUICK_INPUT_INPUT, retryCount);
}
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): Promise<void> {
await this.waitForQuickInputOpened();
for (let from = 0; from < index; from++) {
await this.code.dispatchKeybinding('down');
}
await this.code.dispatchKeybinding('enter');
await this.waitForQuickInputClosed();
}
}

View File

@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* 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 '../src/driver';
import { findElement, findElements, Code } from './code';
const VIEWLET = 'div[id="workbench.view.scm"]';
const SCM_INPUT = `${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[title="Refresh"]`;
const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[title="Commit"]`;
const SCM_RESOURCE_CLICK = (name: string) => `${SCM_RESOURCE} .monaco-icon-label[title*="${name}"] .label-name`;
const SCM_RESOURCE_ACTION_CLICK = (name: string, actionName: string) => `${SCM_RESOURCE} .monaco-icon-label[title*="${name}"] .actions .action-label[title="${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.dispatchKeybinding('ctrl+shift+g');
await this.code.waitForElement(SCM_INPUT);
}
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(SCM_INPUT);
await this.code.waitForActiveElement(SCM_INPUT);
await this.code.waitForSetValue(SCM_INPUT, message);
await this.code.waitAndClick(COMMIT_COMMAND);
}
}

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 { 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 openSearchViewlet(): Promise<any> {
if (process.platform === 'darwin') {
await this.code.dispatchKeybinding('cmd+shift+f');
} else {
await this.code.dispatchKeybinding('ctrl+shift+f');
}
await this.waitForInputFocus(INPUT);
}
async searchFor(text: string): Promise<void> {
await this.waitForInputFocus(INPUT);
await this.code.waitForSetValue(INPUT, text);
await this.submitSearch();
}
async submitSearch(): Promise<void> {
await this.waitForInputFocus(INPUT);
await this.code.dispatchKeybinding('enter');
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): Promise<void> {
const fileMatch = FILE_MATCH(filename);
await retry(
() => this.code.waitAndClick(fileMatch),
() => this.code.waitForElement(`${fileMatch} .action-label.codicon-search-remove`, el => !!el && el.top > 0 && el.left > 0, 10)
);
// ¯\_(ツ)_/¯
await new Promise(c => setTimeout(c, 500));
await this.code.waitAndClick(`${fileMatch} .action-label.codicon-search-remove`);
await this.code.waitForElement(fileMatch, el => !el);
}
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[title="Replace"]`, text);
}
async replaceFileMatch(filename: string): Promise<void> {
const fileMatch = FILE_MATCH(filename);
await retry(
() => this.code.waitAndClick(fileMatch),
() => this.code.waitForElement(`${fileMatch} .action-label.codicon.codicon-search-replace-all`, el => !!el && el.top > 0 && el.left > 0, 10)
);
// ¯\_(ツ)_/¯
await new Promise(c => setTimeout(c, 500));
await this.code.waitAndClick(`${fileMatch} .action-label.codicon.codicon-search-replace-all`);
}
async waitForResultText(text: string): Promise<void> {
// The label can end with " - " depending on whether the search editor is enabled
await this.code.waitForTextContent(`${VIEWLET} .messages .message>span`, undefined, result => result.startsWith(text));
}
async waitForNoResultText(): Promise<void> {
await this.code.waitForTextContent(`${VIEWLET} .messages`, '');
}
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;
}
}
}
}
}

View File

@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import { Editor } from './editor';
import { Editors } from './editors';
import { Code } from './code';
import { QuickAccess } from './quickaccess';
export class SettingsEditor {
constructor(private code: Code, private userDataPath: string, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { }
async addUserSetting(setting: string, value: string): Promise<void> {
await this.openSettings();
await this.editor.waitForEditorFocus('settings.json', 1);
await this.code.dispatchKeybinding('right');
await this.editor.waitForTypeInEditor('settings.json', `"${setting}": ${value}`);
await this.editors.saveOpenedFile();
}
async clearUserSettings(): Promise<void> {
const settingsPath = path.join(this.userDataPath, 'User', 'settings.json');
await new Promise((c, e) => fs.writeFile(settingsPath, '{\n}', 'utf8', err => err ? e(err) : c()));
await this.openSettings();
await this.editor.waitForEditorContents('settings.json', c => c === '{}');
}
private async openSettings(): Promise<void> {
await this.quickaccess.runCommand('workbench.action.openSettingsJson');
}
}

View File

@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* 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,
FEEDBACK_ICON = 8
}
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[title="${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"]`;
case StatusBarElement.FEEDBACK_ICON:
return `.statusbar-item[id="status.feedback"]`;
default:
throw new Error(element);
}
}
}

View File

@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* 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';
const PANEL_SELECTOR = 'div[id="workbench.panel.terminal"]';
const XTERM_SELECTOR = `${PANEL_SELECTOR} .terminal-wrapper`;
const XTERM_TEXTAREA = `${XTERM_SELECTOR} textarea.xterm-helper-textarea`;
export class Terminal {
constructor(private code: Code, private quickaccess: QuickAccess) { }
async showTerminal(): Promise<void> {
await this.quickaccess.runCommand('workbench.action.terminal.toggleTerminal');
await this.code.waitForActiveElement(XTERM_TEXTAREA);
await this.code.waitForTerminalBuffer(XTERM_SELECTOR, lines => lines.some(line => line.length > 0));
}
async runCommand(commandText: string): Promise<void> {
await this.code.writeInTerminal(XTERM_SELECTOR, commandText);
// hold your horses
await new Promise(c => setTimeout(c, 500));
await this.code.dispatchKeybinding('enter');
}
async waitForTerminalText(accept: (buffer: string[]) => boolean): Promise<void> {
await this.code.waitForTerminalBuffer(XTERM_SELECTOR, accept);
}
}

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);
}
}

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 { 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';
export interface Commands {
runCommand(command: string): 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;
constructor(code: Code, userDataPath: string) {
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.editors);
this.activitybar = new ActivityBar(code);
this.search = new Search(code);
this.extensions = new Extensions(code);
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, userDataPath, this.editors, this.editor, this.quickaccess);
this.keybindingsEditor = new KeybindingsEditor(code);
this.terminal = new Terminal(code, this.quickaccess);
this.notebook = new Notebook(this.quickaccess, code);
}
}

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.
*--------------------------------------------------------------------------------------------*/
const fs = require('fs');
const path = require('path');
const root = path.dirname(path.dirname(path.dirname(__dirname)));
const driverPath = path.join(root, 'src/vs/platform/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.
*--------------------------------------------------------------------------------------------*/
/**
* Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
* and others. This API makes no assumption about what promise library is being used which
* enables reusing existing code without migrating to a specific promise implementation. Still,
* we recommend the use of native promises which are available in this editor.
*/
interface Thenable<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
}
${contents}
export interface IDisposable {
dispose(): void;
}
export function connect(outPath: string, handle: string): Promise<{ client: IDisposable, driver: IDriver }>;
`;
const srcPath = path.join(path.dirname(__dirname), 'src');
const outPath = path.join(path.dirname(__dirname), 'out');
fs.writeFileSync(path.join(srcPath, 'driver.d.ts'), contents);
fs.writeFileSync(path.join(outPath, 'driver.d.ts'), contents);

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.
*--------------------------------------------------------------------------------------------*/
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, ' '));

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2016",
"strict": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"outDir": "out",
"sourceMap": true,
"declaration": true,
"lib": [
"es2016",
"dom"
]
},
"exclude": [
"node_modules",
"out",
"tools"
]
}

File diff suppressed because it is too large Load Diff

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
}

View File

@ -0,0 +1,5 @@
.DS_Store
npm-debug.log
Thumbs.db
node_modules/
out/

View File

@ -0,0 +1,25 @@
# Integration test
## Compile
Make sure to run the following command to compile and install dependencies:
yarn --cwd test/integration/browser
## 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)
resources/server/test/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.
## 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.

View File

@ -0,0 +1,21 @@
{
"name": "code-oss-dev-integration-test",
"version": "0.1.0",
"main": "./index.js",
"scripts": {
"postinstall": "npm run compile",
"compile": "yarn tsc"
},
"devDependencies": {
"@types/mkdirp": "0.5.1",
"@types/node": "^12.11.7",
"@types/optimist": "0.0.29",
"@types/rimraf": "2.0.2",
"@types/tmp": "^0.1.0",
"rimraf": "^2.6.1",
"tmp": "0.0.33",
"tree-kill": "1.2.2",
"typescript": "3.7.5",
"vscode-uri": "2.1.1"
}
}

View File

@ -0,0 +1,141 @@
/*---------------------------------------------------------------------------------------------
* 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';
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 optimistLib from 'optimist';
const optimist = optimistLib
.describe('workspacePath', 'path to the workspace to open in the test').string('workspacePath')
.describe('extensionDevelopmentPath', 'path to the extension to test').string('extensionDevelopmentPath')
.describe('extensionTestsPath', 'path to the extension tests').string('extensionTestsPath')
.describe('debug', 'do not run browsers headless').boolean('debug')
.describe('browser', 'browser in which integration tests should run').string('browser').default('browser', 'chromium')
.describe('help', 'show the help').alias('help', 'h');
if (optimist.argv.help) {
optimist.showHelp();
process.exit(0);
}
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 args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros
const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug), args });
const context = await browser.newContext();
const page = await context.newPage();
await page.setViewportSize({ width, height });
const host = endpoint.host;
const protocol = 'vscode-remote';
const testWorkspaceUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.workspacePath)).path, protocol, host, slashes: true });
const testExtensionUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionDevelopmentPath)).path, protocol, host, slashes: true });
const testFilesUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionTestsPath)).path, protocol, host, slashes: true });
const folderParam = testWorkspaceUri;
const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""]]`;
await page.goto(`${endpoint.href}&folder=${folderParam}&payload=${payloadParam}`);
await page.exposeFunction('codeAutomationLog', (type: string, args: any[]) => {
console[type](...args);
});
page.on('console', async (msg: playwright.ConsoleMessage) => {
const msgText = msg.text();
if (msgText.indexOf('vscode:exit') >= 0) {
try {
await browser.close();
} catch (error) {
console.error(`Error when closing browser: ${error}`);
}
try {
await pkill(server.pid);
} catch (error) {
console.error(`Error when killing server process tree: ${error}`);
}
process.exit(msgText === 'vscode:exit 0' ? 0 : 1);
}
});
}
function pkill(pid: number): Promise<void> {
return new Promise((c, e) => {
kill(pid, error => error ? e(error) : c());
});
}
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_AGENT_FOLDER: userDataDir,
VSCODE_BROWSER: browserType,
...process.env
};
let serverLocation: string;
if (process.env.VSCODE_REMOTE_SERVER_PATH) {
serverLocation = path.join(process.env.VSCODE_REMOTE_SERVER_PATH, `server.${process.platform === 'win32' ? 'cmd' : 'sh'}`);
console.log(`Starting built server from '${serverLocation}'`);
} else {
serverLocation = path.join(__dirname, '..', '..', '..', '..', `resources/server/web.${process.platform === 'win32' ? 'bat' : 'sh'}`);
process.env.VSCODE_DEV = '1';
console.log(`Starting server out of sources from '${serverLocation}'`);
}
let serverProcess = cp.spawn(
serverLocation,
['--browser', 'none', '--driver', 'web', '--enable-proposed-api'],
{ env }
);
serverProcess?.stderr?.on('data', error => console.log(`Server stderr: ${error}`));
if (optimist.argv.debug) {
serverProcess?.stdout?.on('data', data => console.log(`Server stdout: ${data}`));
}
process.on('exit', () => serverProcess.kill());
process.on('SIGINT', () => serverProcess.kill());
process.on('SIGTERM', () => serverProcess.kill());
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(optimist.argv.browser).then(async ({ endpoint, server }) => {
return runTestsInBrowser(optimist.argv.browser, endpoint, server);
}, error => {
console.error(error);
process.exit(1);
});

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": false,
"removeComments": false,
"preserveConstEnums": true,
"target": "es2017",
"strictNullChecks": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"outDir": "out",
"sourceMap": true,
"lib": [
"es2016",
"dom"
]
},
"exclude": [
"node_modules"
]
}

View File

@ -0,0 +1,163 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/glob@*":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/mkdirp@0.5.1":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.1.tgz#ea887cd024f691c1ca67cce20b7606b053e43b0f"
integrity sha512-XA4vNO6GCBz8Smq0hqSRo4yRWMqr4FPQrWjhJt6nKskzly4/p87SfuJMFYGRyYb6jo2WNIQU2FDBsY5r1BibUA==
dependencies:
"@types/node" "*"
"@types/node@*":
version "13.7.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4"
integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==
"@types/node@^12.11.7":
version "12.12.26"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.26.tgz#213e153babac0ed169d44a6d919501e68f59dea9"
integrity sha512-UmUm94/QZvU5xLcUlNR8hA7Ac+fGpO1EG/a8bcWVz0P0LqtxFmun9Y2bbtuckwGboWJIT70DoWq1r3hb56n3DA==
"@types/optimist@0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/optimist/-/optimist-0.0.29.tgz#a8873580b3a84b69ac1e687323b15fbbeb90479a"
integrity sha1-qIc1gLOoS2msHmhzI7Ffu+uQR5o=
"@types/rimraf@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e"
integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ==
dependencies:
"@types/glob" "*"
"@types/node" "*"
"@types/tmp@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd"
integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
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"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
rimraf@^2.6.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
dependencies:
glob "^7.1.3"
tmp@0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
dependencies:
os-tmpdir "~1.0.2"
tree-kill@1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
typescript@3.7.5:
version "3.7.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
vscode-uri@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.1.tgz#5aa1803391b6ebdd17d047f51365cf62c38f6e90"
integrity sha512-eY9jmGoEnVf8VE8xr5znSah7Qt1P/xsCdErz+g8HYZtJ7bZqKH5E3d+6oVNm1AC/c6IHUDokbmVXKOi4qPAC9A==
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

View File

@ -0,0 +1,2 @@
--ui tdd
--timeout 10000

9
lib/vscode/test/smoke/.gitignore vendored Normal file
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*/

View File

@ -0,0 +1,13 @@
# 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...

View File

@ -0,0 +1,86 @@
# VS Code Smoke Test
Make sure you are on **Node v12.x**.
### Run
```bash
# Build extensions in repo (if needed)
yarn && yarn compile
# Install Dependencies and Compile
yarn --cwd test/smoke
# Dev (Electron)
yarn smoketest
# Dev (Web)
yarn smoketest --web --browser [chromium|webkit]
# Build (Electron)
yarn smoketest --build <path latest built version> --stable-build <path to previous stable version>
# Build (Web - read instructions below)
yarn smoketest --build <path to web server folder> --web --browser [chromium|webkit]
# Remote (Electron)
yarn smoketest --build <path latest built version> --remote
```
### Run for a release
You must always run the smoketest version which 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 that version of the smoke tests too:
```bash
git checkout release/1.22
yarn && yarn compile
yarn --cwd test/smoke
```
#### Electron
In addition to the new build to be released you will need the previous stable build so that the smoketest can test the data migration.
The recommended way to make these builds available for the smoketest is by downloading their archive version (\*.zip) and extracting
them into two folders. Pass the folder paths to the smoketest as follows:
```bash
yarn smoketest --build <path latest built version> --stable-build <path to previous stable version>
```
#### Web
**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>
```
There is no support for testing an old version to a new one yet, so simply configure the `--build` command line argument to point to
the web server folder which includes the web client bits (e.g. `vscode-server-darwin-web` for macOS).
**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;
- `--screenshots SCREENSHOT_DIR` captures screenshots when tests fail.
### Develop
```bash
cd test/smoke
yarn watch
```
## 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.

View File

@ -0,0 +1,33 @@
{
"name": "code-oss-dev-smoke-test",
"version": "0.1.0",
"main": "./src/main.js",
"scripts": {
"postinstall": "npm run compile",
"compile": "yarn --cwd ../automation compile && tsc",
"watch": "concurrently \"yarn --cwd ../automation watch --preserveWatchOutput\" \"tsc --watch --preserveWatchOutput\"",
"mocha": "mocha"
},
"devDependencies": {
"@types/htmlparser2": "3.7.29",
"@types/mkdirp": "0.5.1",
"@types/mocha": "2.2.41",
"@types/ncp": "2.0.1",
"@types/node": "^12.11.7",
"@types/rimraf": "2.0.2",
"concurrently": "^3.5.1",
"cpx": "^1.5.0",
"htmlparser2": "^3.9.2",
"mkdirp": "^0.5.1",
"mocha": "^6.1.4",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"ncp": "^2.0.0",
"portastic": "^1.0.1",
"rimraf": "^2.6.1",
"strip-json-comments": "^2.0.1",
"tmp": "0.0.33",
"typescript": "3.7.5",
"watch": "^1.0.2"
}
}

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 { Application } from '../../../../automation';
export function setup() {
describe('Editor', () => {
it('shows correct quick outline', async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile('www');
await app.workbench.quickaccess.openQuickOutline();
await app.workbench.quickinput.waitForQuickInputElements(names => names.length >= 6);
});
// it('folds/unfolds the code correctly', async function () {
// await app.workbench.quickaccess.openFile('www');
// // Fold
// await app.workbench.editor.foldAtLine(3);
// await app.workbench.editor.waitUntilShown(3);
// await app.workbench.editor.waitUntilHidden(4);
// await app.workbench.editor.waitUntilHidden(5);
// // Unfold
// await app.workbench.editor.unfoldAtLine(3);
// await app.workbench.editor.waitUntilShown(3);
// await app.workbench.editor.waitUntilShown(4);
// await app.workbench.editor.waitUntilShown(5);
// });
});
}

View File

@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Quality } from '../../../../automation';
export function setup() {
describe('Extensions', () => {
it(`install and activate vscode-smoketest-check extension`, async function () {
const app = this.app as Application;
if (app.quality === Quality.Dev) {
this.skip();
return;
}
await app.workbench.extensions.openExtensionsViewlet();
await app.workbench.extensions.installExtension('michelkaporin.vscode-smoketest-check');
await app.workbench.extensions.waitForExtensionsViewlet();
if (app.remote) {
await app.reload();
}
await app.workbench.quickaccess.runCommand('Smoke Test Check');
await app.workbench.statusbar.waitForStatusbarText('smoke test', 'VS Code Smoke Test Check');
});
});
}

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 { Application, ProblemSeverity, Problems } from '../../../../automation/out';
export function setup() {
describe('Language Features', () => {
it('verifies quick outline', async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile('style.css');
await app.workbench.quickaccess.openQuickOutline();
await app.workbench.quickinput.waitForQuickInputElements(names => names.length === 2);
});
it('verifies problems view', async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile('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', async function () {
const app = this.app as Application;
await app.workbench.settingsEditor.addUserSetting('css.lint.emptyRules', '"error"');
await app.workbench.quickaccess.openFile('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();
});
});
}

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 * as fs from 'fs';
import * as path from 'path';
import { Application } from '../../../../automation';
function toUri(path: string): string {
if (process.platform === 'win32') {
return `${path.replace(/\\/g, '/')}`;
}
return `${path}`;
}
async function createWorkspaceFile(workspacePath: string): Promise<string> {
const workspaceFilePath = path.join(path.dirname(workspacePath), 'smoketest.code-workspace');
const workspace = {
folders: [
{ path: toUri(path.join(workspacePath, 'public')) },
{ path: toUri(path.join(workspacePath, 'routes')) },
{ path: toUri(path.join(workspacePath, 'views')) }
]
};
fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, '\t'));
return workspaceFilePath;
}
export function setup() {
describe('Multiroot', () => {
before(async function () {
const app = this.app as Application;
const workspaceFilePath = await createWorkspaceFile(app.workspacePathOrFolder);
// restart with preventing additional windows from restoring
// to ensure the window after restart is the multi-root workspace
await app.restart({ workspaceOrFolder: workspaceFilePath });
});
it('shows results from all folders', async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openQuickAccess('*.*');
await app.workbench.quickinput.waitForQuickInputElements(names => names.length === 6);
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));
});
});
}

View File

@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* 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 } from '../../../../automation';
// function wait(ms: number): Promise<void> {
// return new Promise(r => setTimeout(r, ms));
// }
export function setup() {
describe('Notebooks', () => {
after(async function () {
const app = this.app as Application;
cp.execSync('git checkout . --quiet', { cwd: app.workspacePathOrFolder });
cp.execSync('git reset --hard origin/master --quiet', { cwd: app.workspacePathOrFolder });
});
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');
});
it('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('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('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 () {
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 () {
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');
});
});
}

View File

@ -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, ActivityBarPosition } from '../../../../automation';
export function setup() {
describe('Preferences', () => {
it('turns off editor line numbers and verifies the live change', async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile('app.js');
await app.code.waitForElements('.line-numbers', false, elements => !!elements.length);
await app.workbench.settingsEditor.addUserSetting('editor.lineNumbers', '"off"');
await app.workbench.editors.selectTab('app.js');
await app.code.waitForElements('.line-numbers', false, result => !result || result.length === 0);
});
it(`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', 'ctrl+u', 'Control+U');
await app.code.dispatchKeybinding('ctrl+u');
await app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.RIGHT);
});
after(async function () {
const app = this.app as Application;
await app.workbench.settingsEditor.clearUserSettings();
// Wait for settings to be applied, which will happen after the settings file is empty
await app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.LEFT);
});
});
}

View File

@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* 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 } from '../../../../automation';
export function setup() {
describe('Search', () => {
after(function () {
const app = this.app as Application;
cp.execSync('git checkout . --quiet', { cwd: app.workspacePathOrFolder });
cp.execSync('git reset --hard origin/master --quiet', { cwd: app.workspacePathOrFolder });
});
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('16 results in 5 files');
});
it('searches only for *.js files & checks for correct result number', async function () {
const app = this.app as Application;
await app.workbench.search.searchFor('body');
await app.workbench.search.showQueryDetails();
await app.workbench.search.setFilesToIncludeText('*.js');
await app.workbench.search.submitSearch();
await app.workbench.search.waitForResultText('4 results in 1 file');
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.removeFileMatch('app.js');
await app.workbench.search.waitForResultText('12 results in 4 files');
});
it('replaces first search result with a replace term', async function () {
const app = this.app as Application;
await app.workbench.search.searchFor('body');
await app.workbench.search.expandReplace();
await app.workbench.search.setReplaceText('ydob');
await app.workbench.search.replaceFileMatch('app.js');
await app.workbench.search.waitForResultText('12 results in 4 files');
await app.workbench.search.searchFor('ydob');
await app.workbench.search.setReplaceText('body');
await app.workbench.search.replaceFileMatch('app.js');
await app.workbench.search.waitForNoResultText();
});
});
describe('Quick Access', () => {
it('quick access search produces correct result', async function () {
const app = this.app as Application;
const expectedNames = [
'.eslintrc.json',
'tasks.json',
'app.js',
'index.js',
'users.js',
'package.json',
'jsconfig.json'
];
await app.workbench.quickaccess.openQuickAccess('.js');
await app.workbench.quickinput.waitForQuickInputElements(names => expectedNames.every(n => names.some(m => n === m)));
await app.code.dispatchKeybinding('escape');
});
it('quick access respects fuzzy matching', async function () {
const app = this.app as Application;
const expectedNames = [
'tasks.json',
'app.js',
'package.json'
];
await app.workbench.quickaccess.openQuickAccess('a.s');
await app.workbench.quickinput.waitForQuickInputElements(names => expectedNames.every(n => names.some(m => n === m)));
await app.code.dispatchKeybinding('escape');
});
});
}

View File

@ -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, Quality, StatusBarElement } from '../../../../automation';
export function setup(isWeb) {
describe('Statusbar', () => {
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);
if (app.quality !== Quality.Dev) {
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.FEEDBACK_ICON);
}
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.SYNC_STATUS);
await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.PROBLEMS_STATUS);
await app.workbench.quickaccess.openFile('app.js');
if (!isWeb) {
// Encoding picker currently hidden in web (only UTF-8 supported)
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('app.js');
await app.workbench.statusbar.clickOn(StatusBarElement.INDENTATION_STATUS);
await app.workbench.quickinput.waitForQuickInputOpened();
await app.workbench.quickinput.closeQuickInput();
if (!isWeb) {
// Encoding picker currently hidden in web (only UTF-8 supported)
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 that 'Tweet us feedback' pop-up appears when clicking on 'Feedback' icon`, async function () {
const app = this.app as Application;
if (app.quality === Quality.Dev) {
return this.skip();
}
await app.workbench.statusbar.clickOn(StatusBarElement.FEEDBACK_ICON);
await app.code.waitForElement('.feedback-form');
});
it(`checks if 'Go to Line' works if called from the status bar`, async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile('app.js');
await app.workbench.statusbar.clickOn(StatusBarElement.SELECTION_STATUS);
await app.workbench.quickinput.waitForQuickInputOpened();
await app.workbench.quickinput.submit(':15');
await app.workbench.editor.waitForHighlightingLine('app.js', 15);
});
it(`verifies if changing EOL is reflected in the status bar`, async function () {
const app = this.app as Application;
await app.workbench.quickaccess.openFile('app.js');
await app.workbench.statusbar.clickOn(StatusBarElement.EOL_STATUS);
await app.workbench.quickinput.waitForQuickInputOpened();
await app.workbench.quickinput.selectQuickInputElement(1);
await app.workbench.statusbar.waitForEOL('CRLF');
});
});
}

View File

@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* 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 function setup() {
describe('Dataloss', () => {
it(`verifies that 'hot exit' works for dirty files`, async function () {
const app = this.app as Application;
await app.workbench.editors.newUntitledFile();
const untitled = 'Untitled-1';
const textToTypeInUntitled = 'Hello from Untitled';
await app.workbench.editor.waitForTypeInEditor(untitled, textToTypeInUntitled);
const readmeMd = 'readme.md';
const textToType = 'Hello, Code';
await app.workbench.quickaccess.openFile(readmeMd);
await app.workbench.editor.waitForTypeInEditor(readmeMd, textToType);
await app.reload();
await app.workbench.editors.waitForActiveTab(readmeMd, true);
await app.workbench.editor.waitForEditorContents(readmeMd, c => c.indexOf(textToType) > -1);
await app.workbench.editors.waitForTab(untitled);
await app.workbench.editors.selectTab(untitled);
await app.workbench.editor.waitForEditorContents(untitled, c => c.indexOf(textToTypeInUntitled) > -1);
});
});
}

View File

@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, ApplicationOptions } from '../../../../automation';
import { join } from 'path';
export function setup(stableCodePath: string, testDataPath: string) {
describe('Datamigration', () => {
it(`verifies opened editors are restored`, async function () {
if (!stableCodePath) {
this.skip();
}
const userDataDir = join(testDataPath, 'd2'); // different data dir from the other tests
const stableOptions: ApplicationOptions = Object.assign({}, this.defaultOptions);
stableOptions.codePath = stableCodePath;
stableOptions.userDataDir = userDataDir;
const stableApp = new Application(stableOptions);
await stableApp!.start();
// Open 3 editors and pin 2 of them
await stableApp.workbench.quickaccess.openFile('www');
await stableApp.workbench.quickaccess.runCommand('View: Keep Editor');
await stableApp.workbench.quickaccess.openFile('app.js');
await stableApp.workbench.quickaccess.runCommand('View: Keep Editor');
await stableApp.workbench.editors.newUntitledFile();
await stableApp.stop();
const insiderOptions: ApplicationOptions = Object.assign({}, this.defaultOptions);
insiderOptions.userDataDir = userDataDir;
const insidersApp = new Application(insiderOptions);
await insidersApp!.start(false /* not expecting walkthrough path */);
// Verify 3 editors are open
await insidersApp.workbench.editors.waitForEditorFocus('Untitled-1');
await insidersApp.workbench.editors.selectTab('app.js');
await insidersApp.workbench.editors.selectTab('www');
await insidersApp.stop();
});
it(`verifies that 'hot exit' works for dirty files`, async function () {
if (!stableCodePath) {
this.skip();
}
const userDataDir = join(testDataPath, 'd3'); // different data dir from the other tests
const stableOptions: ApplicationOptions = Object.assign({}, this.defaultOptions);
stableOptions.codePath = stableCodePath;
stableOptions.userDataDir = userDataDir;
const stableApp = new Application(stableOptions);
await stableApp!.start();
await stableApp.workbench.editors.newUntitledFile();
const untitled = 'Untitled-1';
const textToTypeInUntitled = 'Hello from Untitled';
await stableApp.workbench.editor.waitForTypeInEditor(untitled, textToTypeInUntitled);
const readmeMd = 'readme.md';
const textToType = 'Hello, Code';
await stableApp.workbench.quickaccess.openFile(readmeMd);
await stableApp.workbench.editor.waitForTypeInEditor(readmeMd, textToType);
await stableApp.stop();
const insiderOptions: ApplicationOptions = Object.assign({}, this.defaultOptions);
insiderOptions.userDataDir = userDataDir;
const insidersApp = new Application(insiderOptions);
await insidersApp!.start(false /* not expecting walkthrough path */);
await insidersApp.workbench.editors.waitForActiveTab(readmeMd, true);
await insidersApp.workbench.editor.waitForEditorContents(readmeMd, c => c.indexOf(textToType) > -1);
await insidersApp.workbench.editors.waitForTab(untitled, true);
await insidersApp.workbench.editors.selectTab(untitled);
await insidersApp.workbench.editor.waitForEditorContents(untitled, c => c.indexOf(textToTypeInUntitled) > -1);
await insidersApp.stop();
});
});
}

View File

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* 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 { Application, ApplicationOptions } from '../../../../automation';
export function setup() {
describe('Launch', () => {
let app: Application;
after(async function () {
if (app) {
await app.stop();
}
});
afterEach(async function () {
if (app) {
if (this.currentTest.state === 'failed') {
const name = this.currentTest.fullTitle().replace(/[^a-z0-9\-]/ig, '_');
await app.captureScreenshot(name);
}
}
});
it(`verifies that application launches when user data directory has non-ascii characters`, async function () {
const defaultOptions = this.defaultOptions as ApplicationOptions;
const options: ApplicationOptions = { ...defaultOptions, userDataDir: path.join(defaultOptions.userDataDir, 'abcdø') };
app = new Application(options);
await app.start();
});
});
}

View File

@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Quality } from '../../../../automation';
export function setup() {
describe('Localization', () => {
before(async function () {
const app = this.app as Application;
if (app.quality === Quality.Dev) {
return;
}
await app.workbench.extensions.openExtensionsViewlet();
await app.workbench.extensions.installExtension('ms-ceintl.vscode-language-pack-de');
await app.restart({ extraArgs: ['--locale=DE'] });
});
it(`starts with 'DE' locale and verifies title and viewlets text is in German`, async function () {
const app = this.app as Application;
if (app.quality === Quality.Dev) {
this.skip();
return;
}
await app.workbench.explorer.waitForOpenEditorsViewTitle(title => /geöffnete editoren/i.test(title));
await app.workbench.search.openSearchViewlet();
await app.workbench.search.waitForTitle(title => /suchen/i.test(title));
// await app.workbench.scm.openSCMViewlet();
// await app.workbench.scm.waitForTitle(title => /quellcodeverwaltung/i.test(title));
// See https://github.com/microsoft/vscode/issues/93462
// await app.workbench.debug.openDebugViewlet();
// await app.workbench.debug.waitForTitle(title => /starten/i.test(title));
// await app.workbench.extensions.openExtensionsViewlet();
// await app.workbench.extensions.waitForTitle(title => /extensions/i.test(title));
});
});
}

View File

@ -0,0 +1,329 @@
/*---------------------------------------------------------------------------------------------
* 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 * as cp from 'child_process';
import * as path from 'path';
import * as minimist from 'minimist';
import * as tmp from 'tmp';
import * as rimraf from 'rimraf';
import * as mkdirp from 'mkdirp';
import { ncp } from 'ncp';
import {
Application,
Quality,
ApplicationOptions,
MultiLogger,
Logger,
ConsoleLogger,
FileLogger,
} from '../../automation';
import { setup as setupDataMigrationTests } from './areas/workbench/data-migration.test';
import { setup as setupDataLossTests } from './areas/workbench/data-loss.test';
import { setup as setupDataPreferencesTests } from './areas/preferences/preferences.test';
import { setup as setupDataSearchTests } from './areas/search/search.test';
import { setup as setupDataNotebookTests } from './areas/notebook/notebook.test';
import { setup as setupDataLanguagesTests } from './areas/languages/languages.test';
import { setup as setupDataEditorTests } from './areas/editor/editor.test';
import { setup as setupDataStatusbarTests } from './areas/statusbar/statusbar.test';
import { setup as setupDataExtensionTests } from './areas/extensions/extensions.test';
import { setup as setupDataMultirootTests } from './areas/multiroot/multiroot.test';
import { setup as setupDataLocalizationTests } from './areas/workbench/localization.test';
import { setup as setupLaunchTests } from './areas/workbench/launch.test';
if (!/^v10/.test(process.version) && !/^v12/.test(process.version)) {
console.error('Error: Smoketest must be run using Node 10/12. Currently running', process.version);
process.exit(1);
}
const tmpDir = tmp.dirSync({ prefix: 't' }) as { name: string; removeCallback: Function; };
const testDataPath = tmpDir.name;
process.once('exit', () => rimraf.sync(testDataPath));
const [, , ...args] = process.argv;
const opts = minimist(args, {
string: [
'browser',
'build',
'stable-build',
'wait-time',
'test-repo',
'screenshots',
'log'
],
boolean: [
'verbose',
'remote',
'web'
],
default: {
verbose: false
}
});
const testRepoUrl = 'https://github.com/microsoft/vscode-smoketest-express';
const workspacePath = path.join(testDataPath, 'vscode-smoketest-express');
const extensionsPath = path.join(testDataPath, 'extensions-dir');
mkdirp.sync(extensionsPath);
const screenshotsPath = opts.screenshots ? path.resolve(opts.screenshots) : null;
if (screenshotsPath) {
mkdirp.sync(screenshotsPath);
}
function fail(errorMessage): void {
console.error(errorMessage);
process.exit(1);
}
const repoPath = path.join(__dirname, '..', '..', '..');
let quality: Quality;
//
// #### Electron Smoke Tests ####
//
if (!opts.web) {
function getDevElectronPath(): string {
const buildPath = path.join(repoPath, '.build');
const product = require(path.join(repoPath, 'product.json'));
switch (process.platform) {
case 'darwin':
return path.join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron');
case 'linux':
return path.join(buildPath, 'electron', `${product.applicationName}`);
case 'win32':
return path.join(buildPath, 'electron', `${product.nameShort}.exe`);
default:
throw new Error('Unsupported platform.');
}
}
function getBuildElectronPath(root: string): string {
switch (process.platform) {
case 'darwin':
return path.join(root, 'Contents', 'MacOS', 'Electron');
case 'linux': {
const product = require(path.join(root, 'resources', 'app', 'product.json'));
return path.join(root, product.applicationName);
}
case 'win32': {
const product = require(path.join(root, 'resources', 'app', 'product.json'));
return path.join(root, `${product.nameShort}.exe`);
}
default:
throw new Error('Unsupported platform.');
}
}
let testCodePath = opts.build;
let stableCodePath = opts['stable-build'];
let electronPath: string;
let stablePath: string | undefined = undefined;
if (testCodePath) {
electronPath = getBuildElectronPath(testCodePath);
if (stableCodePath) {
stablePath = getBuildElectronPath(stableCodePath);
}
} else {
testCodePath = getDevElectronPath();
electronPath = testCodePath;
process.env.VSCODE_REPOSITORY = repoPath;
process.env.VSCODE_DEV = '1';
process.env.VSCODE_CLI = '1';
}
if (!fs.existsSync(electronPath || '')) {
fail(`Can't find VSCode at ${electronPath}.`);
}
if (typeof stablePath === 'string' && !fs.existsSync(stablePath)) {
fail(`Can't find Stable VSCode at ${stablePath}.`);
}
if (process.env.VSCODE_DEV === '1') {
quality = Quality.Dev;
} else if (electronPath.indexOf('Code - Insiders') >= 0 /* macOS/Windows */ || electronPath.indexOf('code-insiders') /* Linux */ >= 0) {
quality = Quality.Insiders;
} else {
quality = Quality.Stable;
}
console.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(`Can't find Code server at ${testCodeServerPath}.`);
} else {
console.log(`Running web smoke tests against ${testCodeServerPath}`);
}
}
if (!testCodeServerPath) {
process.env.VSCODE_REPOSITORY = repoPath;
process.env.VSCODE_DEV = '1';
process.env.VSCODE_CLI = '1';
console.log(`Running web smoke out of sources`);
}
if (process.env.VSCODE_DEV === '1') {
quality = Quality.Dev;
} else {
quality = Quality.Insiders;
}
}
const userDataDir = path.join(testDataPath, 'd');
async function setupRepository(): Promise<void> {
if (opts['test-repo']) {
console.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)) {
console.log('*** Cloning test project repository...');
cp.spawnSync('git', ['clone', testRepoUrl, workspacePath]);
} else {
console.log('*** Cleaning test project repository...');
cp.spawnSync('git', ['fetch'], { cwd: workspacePath });
cp.spawnSync('git', ['reset', '--hard', 'FETCH_HEAD'], { cwd: workspacePath });
cp.spawnSync('git', ['clean', '-xdf'], { cwd: workspacePath });
}
console.log('*** Running yarn...');
cp.execSync('yarn', { cwd: workspacePath, stdio: 'inherit' });
}
}
async function setup(): Promise<void> {
console.log('*** Test data:', testDataPath);
console.log('*** Preparing smoketest setup...');
await setupRepository();
console.log('*** Smoketest setup done!\n');
}
function createOptions(): ApplicationOptions {
const loggers: Logger[] = [];
if (opts.verbose) {
loggers.push(new ConsoleLogger());
}
let log: string | undefined = undefined;
if (opts.log) {
loggers.push(new FileLogger(opts.log));
log = 'trace';
}
return {
quality,
codePath: opts.build,
workspacePath,
userDataDir,
extensionsPath,
waitTime: parseInt(opts['wait-time'] || '0') || 20,
logger: new MultiLogger(loggers),
verbose: opts.verbose,
log,
screenshotsPath,
remote: opts.remote,
web: opts.web,
browser: opts.browser
};
}
before(async function () {
this.timeout(2 * 60 * 1000); // allow two minutes for setup
await setup();
this.defaultOptions = createOptions();
});
after(async function () {
await new Promise(c => setTimeout(c, 500)); // wait for shutdown
if (opts.log) {
const logsDir = path.join(userDataDir, 'logs');
const destLogsDir = path.join(path.dirname(opts.log), 'logs');
await new Promise((c, e) => ncp(logsDir, destLogsDir, err => err ? e(err) : c()));
}
await new Promise((c, e) => rimraf(testDataPath, { maxBusyTries: 10 }, err => err ? e(err) : c()));
});
describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => {
if (screenshotsPath) {
afterEach(async function () {
if (this.currentTest.state !== 'failed') {
return;
}
const app = this.app as Application;
const name = this.currentTest.fullTitle().replace(/[^a-z0-9\-]/ig, '_');
await app.captureScreenshot(name);
});
}
if (opts.log) {
beforeEach(async function () {
const app = this.app as Application;
const title = this.currentTest.fullTitle();
app.logger.log('*** Test start:', title);
});
}
if (!opts.web && opts['stable-build']) {
describe(`Stable vs Insiders Smoke Tests: This test MUST run before releasing by providing the --stable-build command line argument`, () => {
setupDataMigrationTests(opts['stable-build'], testDataPath);
});
}
describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => {
before(async function () {
const app = new Application(this.defaultOptions);
await app!.start(opts.web ? false : undefined);
this.app = app;
});
after(async function () {
await this.app.stop();
});
if (!opts.web) { setupDataLossTests(); }
if (!opts.web) { setupDataPreferencesTests(); }
setupDataSearchTests();
setupDataNotebookTests();
setupDataLanguagesTests();
setupDataEditorTests();
setupDataStatusbarTests(!!opts.web);
if (!opts.web) { setupDataExtensionTests(); }
if (!opts.web) { setupDataMultirootTests(); }
if (!opts.web) { setupDataLocalizationTests(); }
if (!opts.web) { setupLaunchTests(); }
});
});

View File

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISuiteCallbackContext, ITestCallbackContext } from 'mocha';
export function describeRepeat(n: number, description: string, callback: (this: ISuiteCallbackContext) => void): void {
for (let i = 0; i < n; i++) {
describe(`${description} (iteration ${i})`, callback);
}
}
export function itRepeat(n: number, description: string, callback: (this: ITestCallbackContext, done: MochaDone) => any): void {
for (let i = 0; i < n; i++) {
it(`${description} (iteration ${i})`, callback);
}
}

View File

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* 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 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' : 'Smoke Tests';
const options = {
color: true,
timeout: 60000,
slow: 30000,
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: path.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 => process.exit(failures ? -1 : 0));

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": false,
"removeComments": false,
"preserveConstEnums": true,
"target": "es2017",
"strictNullChecks": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"outDir": "out",
"sourceMap": true,
"lib": [
"es2016",
"dom"
]
},
"exclude": [
"node_modules"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
{
"name": "splitview",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"koa": "^2.5.1",
"koa-mount": "^3.0.0",
"koa-route": "^3.2.0",
"koa-static": "^5.0.0",
"mz": "^2.7.0"
}
}

View File

@ -0,0 +1,260 @@
<html>
<head>
<meta charset="utf-8">
<title>Splitview</title>
<style>
#container {
width: 800px;
height: 600px;
border: 1px solid black;
}
.view {
width: 100%;
height: 100%;
text-align: center;
vertical-align: middle;
color: white;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: bold;
font-size: 20px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div id="buttons"></div>
<div id="container"></div>
<script src="/static/vs/loader.js"></script>
<script>
require.config({ baseUrl: '/static' });
const params = location.search.split(/[?&]/);
const testGrid = params.findIndex(p => p === 'grid') >= 0;
const testDeserialization = params.findIndex(p => p === 'deserialize') >= 0;
if (testGrid) {
require(['vs/base/browser/ui/grid/grid', 'vs/base/common/event'], ({ Grid, Sizing, Direction, SerializableGrid }, { Event }) => {
const buttons = document.getElementById('buttons');
let grid;
class View {
static ID = 0;
constructor(label, minimumWidth, maximumWidth, minimumHeight, maximumHeight, snap) {
this.id = View.ID++;
this.label = label;
this.minimumWidth = minimumWidth;
this.maximumWidth = maximumWidth;
this.minimumHeight = minimumHeight;
this.maximumHeight = maximumHeight;
this.snap = snap;
this.onDidChange = Event.None;
this.element = document.createElement('div');
this.element.className = 'view';
this.element.style.backgroundColor = `hsl(${(this.id * 41) % 360}, 50%, 70%)`;
this.element.textContent = this.label;
this.button = document.createElement('button');
this.button.onclick = () => grid.setViewVisible(this, !grid.isViewVisible(this));
buttons.appendChild(this.button);
this.setVisible(true);
}
layout(width, height, top, left) {
this.element.innerHTML = `(${top}, ${left})<br />(${width}, ${height})`;
}
setVisible(visible) {
this.button.textContent = visible ? `hide ${this.label}` : `show ${this.label}`;
}
}
const editor = new View('editor', 200, Number.POSITIVE_INFINITY, 200, Number.POSITIVE_INFINITY);
const statusbar = new View('status', 0, Number.POSITIVE_INFINITY, 20, 20);
const activitybar = new View('activity', 50, 50, 0, Number.POSITIVE_INFINITY);
const sidebar = new View('sidebar', 200, Number.POSITIVE_INFINITY, 0, Number.POSITIVE_INFINITY, true);
const panel = new View('panel', 0, Number.POSITIVE_INFINITY, 200, Number.POSITIVE_INFINITY, true);
if (testDeserialization) {
const viewMap = {
['activitybar']: activitybar,
['editor']: editor,
['panel']: panel,
['sidebar']: sidebar,
['statusbar']: statusbar
};
const gridViewDescriptor = {
root: {
type: 'branch',
size: 800,
data: [
{
type: 'branch',
size: 580,
data: [
{
type: 'leaf',
size: activitybar.maximumWidth,
data: { type: 'activitybar' },
visible: true
},
{
type: 'leaf',
size: 200,
data: { type: 'sidebar' },
visible: true
},
{
type: 'branch',
size: 550,
data: [
{
type: 'leaf',
size: 380,
data: { type: 'editor' },
visible: true
},
{
type: 'leaf',
size: 200,
data: { type: 'panel' },
visible: true
}
]
}
]
},
{
type: 'leaf',
size: statusbar.maximumHeight,
data: { type: "statusbar" },
visible: true
}
]
},
orientation: 0,
width: 800,
height: 600
};
const fromJSON = ({ type }) => viewMap[type];
grid = SerializableGrid.deserialize(
gridViewDescriptor,
{ fromJSON },
{ proportionalLayout: false }
);
container.appendChild(grid.element);
grid.layout(800, 600);
} else {
grid = new Grid(editor, {});
const container = document.getElementById('container');
container.appendChild(grid.element);
grid.layout(800, 600);
grid.addView(statusbar, Sizing.Distribute, editor, Direction.Down);
grid.addView(activitybar, Sizing.Distribute, editor, Direction.Left);
grid.addView(sidebar, 250, editor, Direction.Left);
grid.addView(panel, 200, editor, Direction.Down);
}
});
}
else {
require(['vs/base/browser/ui/splitview/splitview', 'vs/base/common/event'], ({ SplitView, Sizing }, { Event }) => {
const buttons = document.getElementById('buttons');
let splitview;
class View {
static ID = 0;
constructor(minimumSize, maximumSize) {
this.id = View.ID++;
this.element = document.createElement('div');
this.element.className = 'view';
this.element.style.backgroundColor = `hsl(${(this.id * 41) % 360}, 50%, 70%)`;
this.element.textContent = `${this.id}`;
this.minimumSize = typeof minimumSize === 'number' ? minimumSize : 100;
this.maximumSize = typeof maximumSize === 'number' ? maximumSize : Number.POSITIVE_INFINITY;
this.onDidChange = Event.None;
this.snap = !minimumSize && !maximumSize;
this.button = document.createElement('button');
this.button.onclick = () => splitview.setViewVisible(this.id, !splitview.isViewVisible(this.id));
buttons.appendChild(this.button);
this.setVisible(true);
}
layout(size, orientation) {
this.element.style.lineHeight = `${size}px`;
}
setVisible(visible) {
this.button.textContent = visible ? `hide ${this.id}` : `show ${this.id}`;
}
}
const container = document.getElementById('container');
const views = [new View(100, 100), new View(), new View(), new View(), new View()];
if (testDeserialization) {
const splitViewDescriptor = {
views: [
{
view: views[0],
size: 100,
visible: true
},
{
view: views[1],
size: 100,
visible: false
},
{
view: views[2],
size: 100,
visible: true
},
{
view: views[3],
size: 200,
visible: true
},
{
view: views[4],
size: 200,
visible: true
}
]
};
splitview = new SplitView(container, { descriptor: splitViewDescriptor, orientation: 0 });
splitview.layout(600);
} else {
// Create a new split view from scratch
splitview = new SplitView(container, {});
splitview.layout(600);
splitview.addView(views[0], Sizing.Distribute);
splitview.addView(views[1], Sizing.Distribute);
splitview.addView(views[2], Sizing.Distribute);
splitview.addView(views[3], Sizing.Distribute);
splitview.addView(views[4], Sizing.Distribute);
}
});
}
</script>
</body>
</html>

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.
*--------------------------------------------------------------------------------------------*/
const fs = require('mz/fs');
const path = require('path');
const Koa = require('koa');
const _ = require('koa-route');
const serve = require('koa-static');
const mount = require('koa-mount');
const app = new Koa();
app.use(serve('public'));
app.use(mount('/static', serve('../../out')));
app.listen(3000);
console.log('http://localhost:3000');

View File

@ -0,0 +1,341 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@^1.2.2:
version "1.3.5"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I=
dependencies:
mime-types "~2.1.18"
negotiator "0.6.1"
any-promise@^1.0.0, any-promise@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
content-disposition@~0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
content-type@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookies@~0.7.0:
version "0.7.1"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b"
integrity sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=
dependencies:
depd "~1.1.1"
keygrip "~1.0.2"
debug@*, debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^2.6.1:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
depd@^1.1.0, depd@~1.1.1, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
error-inject@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=
escape-html@~1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
fresh@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-assert@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a"
integrity sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=
dependencies:
deep-equal "~1.0.1"
http-errors "~1.6.1"
http-errors@^1.2.8, http-errors@^1.6.3, http-errors@~1.6.1, http-errors@~1.6.2:
version "1.6.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
is-generator-function@^1.0.3:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522"
integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
keygrip@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91"
integrity sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=
koa-compose@^3.0.0, koa-compose@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=
dependencies:
any-promise "^1.1.0"
koa-compose@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
koa-convert@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=
dependencies:
co "^4.6.0"
koa-compose "^3.0.0"
koa-is-json@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
koa-mount@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/koa-mount/-/koa-mount-3.0.0.tgz#08cab3b83d31442ed8b7e75c54b1abeb922ec197"
integrity sha1-CMqzuD0xRC7Yt+dcVLGr65IuwZc=
dependencies:
debug "^2.6.1"
koa-compose "^3.2.1"
koa-route@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/koa-route/-/koa-route-3.2.0.tgz#76298b99a6bcfa9e38cab6fe5c79a8733e758bce"
integrity sha1-dimLmaa8+p44yrb+XHmocz51i84=
dependencies:
debug "*"
methods "~1.1.0"
path-to-regexp "^1.2.0"
koa-send@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.0.tgz#5e8441e07ef55737734d7ced25b842e50646e7eb"
integrity sha512-90ZotV7t0p3uN9sRwW2D484rAaKIsD8tAVtypw/aBU+ryfV+fR2xrcAwhI8Wl6WRkojLUs/cB9SBSCuIb+IanQ==
dependencies:
debug "^3.1.0"
http-errors "^1.6.3"
mz "^2.7.0"
resolve-path "^1.4.0"
koa-static@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943"
integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==
dependencies:
debug "^3.1.0"
koa-send "^5.0.0"
koa@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c"
integrity sha512-cchwbMeG2dv3E2xTAmheDAuvR53tPgJZN/Hf1h7bTzJLSPcFZp8/t5+bNKJ6GaQZoydhZQ+1GNruhKdj3lIrug==
dependencies:
accepts "^1.2.2"
content-disposition "~0.5.0"
content-type "^1.0.0"
cookies "~0.7.0"
debug "*"
delegates "^1.0.0"
depd "^1.1.0"
destroy "^1.0.3"
error-inject "~1.0.0"
escape-html "~1.0.1"
fresh "^0.5.2"
http-assert "^1.1.0"
http-errors "^1.2.8"
is-generator-function "^1.0.3"
koa-compose "^4.0.0"
koa-convert "^1.2.0"
koa-is-json "^1.0.0"
mime-types "^2.0.7"
on-finished "^2.1.0"
only "0.0.2"
parseurl "^1.3.0"
statuses "^1.2.0"
type-is "^1.5.5"
vary "^1.0.0"
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
methods@~1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
mime-types@^2.0.7, mime-types@~2.1.18:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
dependencies:
mime-db "~1.33.0"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
dependencies:
any-promise "^1.0.0"
object-assign "^4.0.1"
thenify-all "^1.0.0"
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=
object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
on-finished@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
only@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
parseurl@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=
path-is-absolute@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-to-regexp@^1.2.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=
dependencies:
isarray "0.0.1"
resolve-path@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=
dependencies:
http-errors "~1.6.2"
path-is-absolute "1.0.1"
setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
"statuses@>= 1.4.0 < 2", statuses@^1.2.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
dependencies:
thenify ">= 3.1.0 < 4"
"thenify@>= 3.1.0 < 4":
version "3.3.0"
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=
dependencies:
any-promise "^1.0.0"
type-is@^1.5.5:
version "1.6.16"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.18"
vary@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

View File

@ -0,0 +1,13 @@
{
"name": "tree",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"koa": "^2.5.1",
"koa-mount": "^3.0.0",
"koa-route": "^3.2.0",
"koa-static": "^5.0.0",
"mz": "^2.7.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,403 @@
<html>
<head>
<meta charset="utf-8">
<title>Tree</title>
<style>
#container {
width: 400;
height: 600;
border: 1px solid black;
}
.monaco-scrollable-element>.scrollbar>.slider {
background: rgba(100, 100, 100, .4);
}
.tl-contents {
flex: 1;
}
.monaco-list-row:hover:not(.selected):not(.focused) {
background: gainsboro !important;
}
</style>
</head>
<body>
<input type="text" id="filter" />
<button id="expandall">Expand All</button>
<button id="collapseall">Collapse All</button>
<button id="renderwidth">Render Width</button>
<button id="refresh">Refresh</button>
<div id="container"></div>
<script src="/static/vs/loader.js"></script>
<script>
function perf(name, fn) {
performance.mark('before ' + name);
const start = performance.now();
fn();
console.log(name + ' took', performance.now() - start);
performance.mark('after ' + name);
}
require.config({ baseUrl: '/static' });
require(['vs/base/browser/ui/tree/indexTree', 'vs/base/browser/ui/tree/objectTree', 'vs/base/browser/ui/tree/asyncDataTree', 'vs/base/browser/ui/tree/dataTree', 'vs/base/browser/ui/tree/tree', 'vs/base/common/iterator'], ({ IndexTree }, { CompressibleObjectTree }, { AsyncDataTree }, { DataTree }, { TreeVisibility }, { iter }) => {
function createIndexTree(opts) {
opts = opts || {};
const delegate = {
getHeight() { return 22; },
getTemplateId() { return 'template'; },
hasDynamicHeight() { return true; }
};
const renderer = {
templateId: 'template',
renderTemplate(container) { return container; },
renderElement(element, index, container) {
if (opts.supportDynamicHeights) {
let v = [];
for (let i = 1; i <= 5; i++) {
v.push(element.element);
}
container.innerHTML = v.join('<br />');
} else {
container.innerHTML = element.element;
}
},
disposeElement() { },
disposeTemplate() { }
};
const treeFilter = new class {
constructor() {
this.pattern = null;
let timeout;
filter.oninput = () => {
clearTimeout(timeout);
timeout = setTimeout(() => this.updatePattern(), 300);
};
}
updatePattern() {
if (!filter.value) {
this.pattern = null;
} else {
this.pattern = new RegExp(filter.value, 'i');
}
perf('refilter', () => tree.refilter());
}
filter(el) {
return (this.pattern ? this.pattern.test(el) : true) ? TreeVisibility.Visible : TreeVisibility.Recurse;
}
};
const tree = new IndexTree('test', container, delegate, [renderer], null, { ...opts, filter: treeFilter, setRowLineHeight: false });
return { tree, treeFilter };
}
function createCompressedObjectTree(opts) {
opts = opts || {};
const delegate = {
getHeight() { return 22; },
getTemplateId() { return 'template'; },
hasDynamicHeight() { return true; }
};
const renderer = {
templateId: 'template',
renderTemplate(container) { return container; },
renderElement(element, index, container) {
container.innerHTML = element.element.name;
},
renderCompressedElements(node, index, container, height) {
container.innerHTML = `🙈 ${node.element.elements.map(el => el.name).join('/')}`;
},
disposeElement() { },
disposeTemplate() { }
};
const treeFilter = new class {
constructor() {
this.pattern = null;
let timeout;
filter.oninput = () => {
clearTimeout(timeout);
timeout = setTimeout(() => this.updatePattern(), 300);
};
}
updatePattern() {
if (!filter.value) {
this.pattern = null;
} else {
this.pattern = new RegExp(filter.value, 'i');
}
perf('refilter', () => tree.refilter());
}
filter(el) {
return (this.pattern ? this.pattern.test(el) : true) ? TreeVisibility.Visible : TreeVisibility.Recurse;
}
};
const tree = new CompressibleObjectTree('test', container, delegate, [renderer], { ...opts, filter: treeFilter, setRowLineHeight: false, collapseByDefault: true, setRowLineHeight: true });
return { tree, treeFilter };
}
function createAsyncDataTree() {
const delegate = {
getHeight() { return 22; },
getTemplateId() { return 'template'; }
};
const renderer = {
templateId: 'template',
renderTemplate(container) { return container; },
renderElement(node, index, container) { container.textContent = node.element.element.name; },
disposeElement() { },
disposeTemplate() { }
};
const treeFilter = new class {
constructor() {
this.pattern = null;
let timeout;
filter.oninput = () => {
clearTimeout(timeout);
timeout = setTimeout(() => this.updatePattern(), 300);
};
}
updatePattern() {
if (!filter.value) {
this.pattern = null;
} else {
this.pattern = new RegExp(filter.value, 'i');
}
perf('refilter', () => tree.refilter());
}
filter(el) {
return (this.pattern ? this.pattern.test(el.name) : true) ? TreeVisibility.Visible : TreeVisibility.Recurse;
}
};
const sorter = new class {
compare(a, b) {
if (a.collapsible === b.collapsible) {
return a.name < b.name ? -1 : 1;
}
return a.collapsible ? -1 : 1;
}
};
const dataSource = new class {
hasChildren(element) {
return element === null || element.element.type === 'dir';
}
getChildren(element) {
return new Promise((c, e) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', element ? `/api/readdir?path=${element.element.path}` : '/api/readdir');
xhr.send();
xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
const els = JSON.parse(this.responseText).map(element => ({
element,
collapsible: element.type === 'dir'
}));
c(els);
}
};
});
}
}
const identityProvider = {
getId(node) {
return node.element.path;
}
};
const tree = new AsyncDataTree('test', container, delegate, [renderer], dataSource, { filter: treeFilter, sorter, identityProvider });
return { tree, treeFilter };
}
function createDataTree() {
const delegate = {
getHeight() { return 22; },
getTemplateId() { return 'template'; }
};
const renderer = {
templateId: 'template',
renderTemplate(container) { return container; },
renderElement(node, index, container) { container.textContent = node.element.name; },
disposeElement() { },
disposeTemplate() { }
};
const treeFilter = new class {
constructor() {
this.pattern = null;
let timeout;
filter.oninput = () => {
clearTimeout(timeout);
timeout = setTimeout(() => this.updatePattern(), 300);
};
}
updatePattern() {
if (!filter.value) {
this.pattern = null;
} else {
this.pattern = new RegExp(filter.value, 'i');
}
perf('refilter', () => tree.refilter());
}
filter(el) {
return (this.pattern ? this.pattern.test(el.name) : true) ? TreeVisibility.Visible : TreeVisibility.Recurse;
}
};
const dataSource = new class {
getChildren(element) {
return element.children || [];
}
};
const identityProvider = {
getId(node) {
return node.name;
}
};
const tree = new DataTree('test', container, delegate, [renderer], dataSource, { filter: treeFilter, identityProvider });
tree.setInput({
children: [
{ name: 'A', children: [{ name: 'AA' }, { name: 'AB' }] },
{ name: 'B', children: [{ name: 'BA', children: [{ name: 'BAA' }] }, { name: 'BB' }] },
{ name: 'C' }
]
});
return { tree, treeFilter };
}
switch (location.search) {
case '?problems': {
const { tree, treeFilter } = createIndexTree();
expandall.onclick = () => perf('expand all', () => tree.expandAll());
collapseall.onclick = () => perf('collapse all', () => tree.collapseAll());
renderwidth.onclick = () => perf('renderwidth', () => tree.layoutWidth(Math.random()));
const files = [];
for (let i = 0; i < 100000; i++) {
const errors = [];
for (let j = 1; j <= 3; j++) {
errors.push({ element: `error #${j} ` });
}
files.push({ element: `file #${i}`, children: errors });
}
perf('splice', () => tree.splice([0], 0, files));
break;
}
case '?data': {
const { tree, treeFilter } = createAsyncDataTree();
expandall.onclick = () => perf('expand all', () => tree.expandAll());
collapseall.onclick = () => perf('collapse all', () => tree.collapseAll());
renderwidth.onclick = () => perf('renderwidth', () => tree.layoutWidth(Math.random()));
refresh.onclick = () => perf('refresh', () => tree.updateChildren());
tree.setInput(null);
break;
}
case '?objectdata': {
const { tree, treeFilter } = createDataTree();
expandall.onclick = () => perf('expand all', () => tree.expandAll());
collapseall.onclick = () => perf('collapse all', () => tree.collapseAll());
renderwidth.onclick = () => perf('renderwidth', () => tree.layoutWidth(Math.random()));
refresh.onclick = () => perf('refresh', () => tree.updateChildren());
break;
}
case '?compressed': {
const { tree, treeFilter } = createCompressedObjectTree();
expandall.onclick = () => perf('expand all', () => tree.expandAll());
collapseall.onclick = () => perf('collapse all', () => tree.collapseAll());
const xhr = new XMLHttpRequest();
xhr.open('GET', '/compressed.json');
xhr.send();
xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
tree.setChildren(null, JSON.parse(this.responseText));
}
};
break;
}
case '?height': {
const { tree, treeFilter } = createIndexTree({ supportDynamicHeights: true });
expandall.onclick = () => perf('expand all', () => tree.expandAll());
collapseall.onclick = () => perf('collapse all', () => tree.collapseAll());
renderwidth.onclick = () => perf('renderwidth', () => tree.layoutWidth(Math.random()));
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/ls?path=');
xhr.send();
xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
perf('splice', () => tree.splice([0], 0, [JSON.parse(this.responseText)]));
treeFilter.updatePattern();
}
};
// container.
break;
}
default: {
const { tree, treeFilter } = createIndexTree();
expandall.onclick = () => perf('expand all', () => tree.expandAll());
collapseall.onclick = () => perf('collapse all', () => tree.collapseAll());
renderwidth.onclick = () => perf('renderwidth', () => tree.layoutWidth(Math.random()));
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/ls?path=');
xhr.send();
xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
perf('splice', () => tree.splice([0], 0, [JSON.parse(this.responseText)]));
treeFilter.updatePattern();
}
};
}
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const fs = require('mz/fs');
const path = require('path');
const Koa = require('koa');
const _ = require('koa-route');
const serve = require('koa-static');
const mount = require('koa-mount');
const app = new Koa();
const root = path.dirname(path.dirname(__dirname));
async function getTree(fsPath, level) {
const element = path.basename(fsPath);
const stat = await fs.stat(fsPath);
if (!stat.isDirectory() || element === '.git' || element === '.build' || level >= 4) {
return { element };
}
const childNames = await fs.readdir(fsPath);
const children = await Promise.all(childNames.map(async childName => await getTree(path.join(fsPath, childName), level + 1)));
return { element, collapsible: true, collapsed: false, children };
}
async function readdir(relativePath) {
const absolutePath = relativePath ? path.join(root, relativePath) : root;
const childNames = await fs.readdir(absolutePath);
const childStats = await Promise.all(childNames.map(async name => await fs.stat(path.join(absolutePath, name))));
const result = [];
for (let i = 0; i < childNames.length; i++) {
const name = childNames[i];
const path = relativePath ? `${relativePath}/${name}` : name;
const stat = childStats[i];
if (stat.isFile()) {
result.push({ type: 'file', name, path });
} else if (!stat.isDirectory() || name === '.git' || name === '.build') {
continue;
} else {
result.push({ type: 'dir', name, path });
}
}
return result;
}
app.use(serve('public'));
app.use(mount('/static', serve('../../out')));
app.use(_.get('/api/ls', async ctx => {
const relativePath = ctx.query.path;
const absolutePath = path.join(root, relativePath);
ctx.body = await getTree(absolutePath, 0);
}));
app.use(_.get('/api/readdir', async ctx => {
const relativePath = ctx.query.path;
ctx.body = await readdir(relativePath);
}));
app.listen(3000);
console.log('http://localhost:3000');

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.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const fs = require('fs');
function collect(location) {
const element = { name: path.basename(location) };
const stat = fs.statSync(location);
if (!stat.isDirectory()) {
return { element, incompressible: true };
}
const children = fs.readdirSync(location)
.map(child => path.join(location, child))
.map(collect);
return { element, children };
}
console.log(JSON.stringify(collect(process.cwd())));

View File

@ -0,0 +1,341 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@^1.2.2:
version "1.3.5"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I=
dependencies:
mime-types "~2.1.18"
negotiator "0.6.1"
any-promise@^1.0.0, any-promise@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
content-disposition@~0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
content-type@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookies@~0.7.0:
version "0.7.1"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b"
integrity sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=
dependencies:
depd "~1.1.1"
keygrip "~1.0.2"
debug@*, debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^2.6.1:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
depd@^1.1.0, depd@~1.1.1, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
error-inject@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=
escape-html@~1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
fresh@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-assert@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a"
integrity sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=
dependencies:
deep-equal "~1.0.1"
http-errors "~1.6.1"
http-errors@^1.2.8, http-errors@^1.6.3, http-errors@~1.6.1, http-errors@~1.6.2:
version "1.6.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
is-generator-function@^1.0.3:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522"
integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
keygrip@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91"
integrity sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=
koa-compose@^3.0.0, koa-compose@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=
dependencies:
any-promise "^1.1.0"
koa-compose@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
koa-convert@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=
dependencies:
co "^4.6.0"
koa-compose "^3.0.0"
koa-is-json@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
koa-mount@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/koa-mount/-/koa-mount-3.0.0.tgz#08cab3b83d31442ed8b7e75c54b1abeb922ec197"
integrity sha1-CMqzuD0xRC7Yt+dcVLGr65IuwZc=
dependencies:
debug "^2.6.1"
koa-compose "^3.2.1"
koa-route@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/koa-route/-/koa-route-3.2.0.tgz#76298b99a6bcfa9e38cab6fe5c79a8733e758bce"
integrity sha1-dimLmaa8+p44yrb+XHmocz51i84=
dependencies:
debug "*"
methods "~1.1.0"
path-to-regexp "^1.2.0"
koa-send@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.0.tgz#5e8441e07ef55737734d7ced25b842e50646e7eb"
integrity sha512-90ZotV7t0p3uN9sRwW2D484rAaKIsD8tAVtypw/aBU+ryfV+fR2xrcAwhI8Wl6WRkojLUs/cB9SBSCuIb+IanQ==
dependencies:
debug "^3.1.0"
http-errors "^1.6.3"
mz "^2.7.0"
resolve-path "^1.4.0"
koa-static@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943"
integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==
dependencies:
debug "^3.1.0"
koa-send "^5.0.0"
koa@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c"
integrity sha512-cchwbMeG2dv3E2xTAmheDAuvR53tPgJZN/Hf1h7bTzJLSPcFZp8/t5+bNKJ6GaQZoydhZQ+1GNruhKdj3lIrug==
dependencies:
accepts "^1.2.2"
content-disposition "~0.5.0"
content-type "^1.0.0"
cookies "~0.7.0"
debug "*"
delegates "^1.0.0"
depd "^1.1.0"
destroy "^1.0.3"
error-inject "~1.0.0"
escape-html "~1.0.1"
fresh "^0.5.2"
http-assert "^1.1.0"
http-errors "^1.2.8"
is-generator-function "^1.0.3"
koa-compose "^4.0.0"
koa-convert "^1.2.0"
koa-is-json "^1.0.0"
mime-types "^2.0.7"
on-finished "^2.1.0"
only "0.0.2"
parseurl "^1.3.0"
statuses "^1.2.0"
type-is "^1.5.5"
vary "^1.0.0"
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
methods@~1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
mime-types@^2.0.7, mime-types@~2.1.18:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
dependencies:
mime-db "~1.33.0"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
dependencies:
any-promise "^1.0.0"
object-assign "^4.0.1"
thenify-all "^1.0.0"
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=
object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
on-finished@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
only@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
parseurl@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=
path-is-absolute@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-to-regexp@^1.2.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=
dependencies:
isarray "0.0.1"
resolve-path@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=
dependencies:
http-errors "~1.6.2"
path-is-absolute "1.0.1"
setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
"statuses@>= 1.4.0 < 2", statuses@^1.2.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
dependencies:
thenify ">= 3.1.0 < 4"
"thenify@>= 3.1.0 < 4":
version "3.3.0"
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=
dependencies:
any-promise "^1.0.0"
type-is@^1.5.5:
version "1.6.16"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.18"
vary@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

View File

@ -0,0 +1,40 @@
# Unit Tests
## Run (inside Electron)
./scripts/test.[sh|bat]
All unit tests are run inside a electron-browser environment which access to DOM and Nodejs api. This is the closest to the environment in which VS Code itself ships. Notes:
- use the `--debug` to see an electron window with dev tools which allows for debugging
- to run only a subset of tests use the `--run` or `--glob` options
For instance, `./scripts/test.sh --debug --glob **/extHost*.test.js` runs all tests from `extHost`-files and enables you to debug them.
## Run (inside browser)
yarn test-browser --browser webkit --browser chromium
Unit tests from layers `common` and `browser` are run inside `chromium`, `webkit`, and (soonish) `firefox` (using playwright). This complements our electron-based unit test runner and adds more coverage of supported platforms. Notes:
- these tests are part of the continuous build, that means you might have test failures that only happen with webkit on _windows_ or _chromium_ on linux
- you can run these tests locally via yarn `test-browser --browser chromium --browser webkit`
- to debug, open `<vscode>/test/unit/browser/renderer.html` inside a browser and use the `?m=<amd_module>`-query to specify what AMD module to load, e.g `file:///Users/jrieken/Code/vscode/test/unit/browser/renderer.html?m=vs/base/test/common/strings.test` runs all tests from `strings.test.ts`
- to run only a subset of tests use the `--run` or `--glob` options
## Run (with node)
yarn run mocha --run src/vs/editor/test/browser/controller/cursor.test.ts
## Coverage
The following command will create a `coverage` folder at the root of the workspace:
**OS X and Linux**
./scripts/test.sh --coverage
**Windows**
scripts\test --coverage

View File

@ -0,0 +1,471 @@
// http://wiki.commonjs.org/wiki/Unit_Testing/1.0
//
// THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8!
//
// 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 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.
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory); // AMD
} else if (typeof exports === 'object') {
module.exports = factory(); // CommonJS
} else {
root.assert = factory(); // Global
}
})(this, function() {
// UTILITY
// Object.create compatible in IE
var create = Object.create || function(p) {
if (!p) throw Error('no type');
function f() {};
f.prototype = p;
return new f();
};
// UTILITY
var util = {
inherits: function(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
},
isArray: function(ar) {
return Array.isArray(ar);
},
isBoolean: function(arg) {
return typeof arg === 'boolean';
},
isNull: function(arg) {
return arg === null;
},
isNullOrUndefined: function(arg) {
return arg == null;
},
isNumber: function(arg) {
return typeof arg === 'number';
},
isString: function(arg) {
return typeof arg === 'string';
},
isSymbol: function(arg) {
return typeof arg === 'symbol';
},
isUndefined: function(arg) {
return arg === undefined;
},
isRegExp: function(re) {
return util.isObject(re) && util.objectToString(re) === '[object RegExp]';
},
isObject: function(arg) {
return typeof arg === 'object' && arg !== null;
},
isDate: function(d) {
return util.isObject(d) && util.objectToString(d) === '[object Date]';
},
isError: function(e) {
return isObject(e) &&
(objectToString(e) === '[object Error]' || e instanceof Error);
},
isFunction: function(arg) {
return typeof arg === 'function';
},
isPrimitive: function(arg) {
return arg === null ||
typeof arg === 'boolean' ||
typeof arg === 'number' ||
typeof arg === 'string' ||
typeof arg === 'symbol' || // ES6 symbol
typeof arg === 'undefined';
},
objectToString: function(o) {
return Object.prototype.toString.call(o);
}
};
var pSlice = Array.prototype.slice;
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
var Object_keys = typeof Object.keys === 'function' ? Object.keys : (function() {
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function(obj) {
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
var result = [], prop, i;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
})();
// 1. The assert module provides functions that throw
// AssertionError's when particular conditions are not met. The
// assert module must conform to the following interface.
var assert = ok;
// 2. The AssertionError is defined in assert.
// new assert.AssertionError({ message: message,
// actual: actual,
// expected: expected })
assert.AssertionError = function AssertionError(options) {
this.name = 'AssertionError';
this.actual = options.actual;
this.expected = options.expected;
this.operator = options.operator;
if (options.message) {
this.message = options.message;
this.generatedMessage = false;
} else {
this.message = getMessage(this);
this.generatedMessage = true;
}
var stackStartFunction = options.stackStartFunction || fail;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, stackStartFunction);
} else {
// try to throw an error now, and from the stack property
// work out the line that called in to assert.js.
try {
this.stack = (new Error).stack.toString();
} catch (e) {}
}
};
// assert.AssertionError instanceof Error
util.inherits(assert.AssertionError, Error);
function replacer(key, value) {
if (util.isUndefined(value)) {
return '' + value;
}
if (util.isNumber(value) && (isNaN(value) || !isFinite(value))) {
return value.toString();
}
if (util.isFunction(value) || util.isRegExp(value)) {
return value.toString();
}
return value;
}
function truncate(s, n) {
if (util.isString(s)) {
return s.length < n ? s : s.slice(0, n);
} else {
return s;
}
}
function getMessage(self) {
return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' +
self.operator + ' ' +
truncate(JSON.stringify(self.expected, replacer), 128);
}
// At present only the three keys mentioned above are used and
// understood by the spec. Implementations or sub modules can pass
// other keys to the AssertionError's constructor - they will be
// ignored.
// 3. All of the following functions must throw an AssertionError
// when a corresponding condition is not met, with a message that
// may be undefined if not provided. All assertion methods provide
// both the actual and expected values to the assertion error for
// display purposes.
function fail(actual, expected, message, operator, stackStartFunction) {
throw new assert.AssertionError({
message: message,
actual: actual,
expected: expected,
operator: operator,
stackStartFunction: stackStartFunction
});
}
// EXTENSION! allows for well behaved errors defined elsewhere.
assert.fail = fail;
// 4. Pure assertion tests whether a value is truthy, as determined
// by !!guard.
// assert.ok(guard, message_opt);
// This statement is equivalent to assert.equal(true, !!guard,
// message_opt);. To test strictly for the value true, use
// assert.strictEqual(true, guard, message_opt);.
function ok(value, message) {
if (!value) fail(value, true, message, '==', assert.ok);
}
assert.ok = ok;
// 5. The equality assertion tests shallow, coercive equality with
// ==.
// assert.equal(actual, expected, message_opt);
assert.equal = function equal(actual, expected, message) {
if (actual != expected) fail(actual, expected, message, '==', assert.equal);
};
// 6. The non-equality assertion tests for whether two objects are not equal
// with != assert.notEqual(actual, expected, message_opt);
assert.notEqual = function notEqual(actual, expected, message) {
if (actual == expected) {
fail(actual, expected, message, '!=', assert.notEqual);
}
};
// 7. The equivalence assertion tests a deep equality relation.
// assert.deepEqual(actual, expected, message_opt);
assert.deepEqual = function deepEqual(actual, expected, message) {
if (!_deepEqual(actual, expected, false)) {
fail(actual, expected, message, 'deepEqual', assert.deepEqual);
}
};
assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
if (!_deepEqual(actual, expected, true)) {
fail(actual, expected, message, 'deepStrictEqual', assert.deepStrictEqual);
}
};
function _deepEqual(actual, expected, strict) {
// 7.1. All identical values are equivalent, as determined by ===.
if (actual === expected) {
return true;
// } else if (actual instanceof Buffer && expected instanceof Buffer) {
// return compare(actual, expected) === 0;
// 7.2. If the expected value is a Date object, the actual value is
// equivalent if it is also a Date object that refers to the same time.
} else if (util.isDate(actual) && util.isDate(expected)) {
return actual.getTime() === expected.getTime();
// 7.3 If the expected value is a RegExp object, the actual value is
// equivalent if it is also a RegExp object with the same source and
// properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
} else if (util.isRegExp(actual) && util.isRegExp(expected)) {
return actual.source === expected.source &&
actual.global === expected.global &&
actual.multiline === expected.multiline &&
actual.lastIndex === expected.lastIndex &&
actual.ignoreCase === expected.ignoreCase;
// 7.4. Other pairs that do not both pass typeof value == 'object',
// equivalence is determined by ==.
} else if ((actual === null || typeof actual !== 'object') &&
(expected === null || typeof expected !== 'object')) {
return strict ? actual === expected : actual == expected;
// 7.5 For all other Object pairs, including Array objects, equivalence is
// determined by having the same number of owned properties (as verified
// with Object.prototype.hasOwnProperty.call), the same set of keys
// (although not necessarily the same order), equivalent values for every
// corresponding key, and an identical 'prototype' property. Note: this
// accounts for both named and indexed properties on Arrays.
} else {
return objEquiv(actual, expected, strict);
}
}
function isArguments(object) {
return Object.prototype.toString.call(object) == '[object Arguments]';
}
function objEquiv(a, b, strict) {
if (a === null || a === undefined || b === null || b === undefined)
return false;
// if one is a primitive, the other must be same
if (util.isPrimitive(a) || util.isPrimitive(b))
return a === b;
if (strict && Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
return false;
var aIsArgs = isArguments(a),
bIsArgs = isArguments(b);
if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs))
return false;
if (aIsArgs) {
a = pSlice.call(a);
b = pSlice.call(b);
return _deepEqual(a, b, strict);
}
var ka = Object.keys(a),
kb = Object.keys(b),
key, i;
// having the same number of owned properties (keys incorporates
// hasOwnProperty)
if (ka.length !== kb.length)
return false;
//the same set of keys (although not necessarily the same order),
ka.sort();
kb.sort();
//~~~cheap key test
for (i = ka.length - 1; i >= 0; i--) {
if (ka[i] !== kb[i])
return false;
}
//equivalent values for every corresponding key, and
//~~~possibly expensive deep test
for (i = ka.length - 1; i >= 0; i--) {
key = ka[i];
if (!_deepEqual(a[key], b[key], strict)) return false;
}
return true;
}
// 8. The non-equivalence assertion tests for any deep inequality.
// assert.notDeepEqual(actual, expected, message_opt);
assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
if (_deepEqual(actual, expected, false)) {
fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual);
}
};
assert.notDeepStrictEqual = notDeepStrictEqual;
function notDeepStrictEqual(actual, expected, message) {
if (_deepEqual(actual, expected, true)) {
fail(actual, expected, message, 'notDeepStrictEqual', notDeepStrictEqual);
}
}
// 9. The strict equality assertion tests strict equality, as determined by ===.
// assert.strictEqual(actual, expected, message_opt);
assert.strictEqual = function strictEqual(actual, expected, message) {
if (actual !== expected) {
fail(actual, expected, message, '===', assert.strictEqual);
}
};
// 10. The strict non-equality assertion tests for strict inequality, as
// determined by !==. assert.notStrictEqual(actual, expected, message_opt);
assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
if (actual === expected) {
fail(actual, expected, message, '!==', assert.notStrictEqual);
}
};
function expectedException(actual, expected) {
if (!actual || !expected) {
return false;
}
if (Object.prototype.toString.call(expected) == '[object RegExp]') {
return expected.test(actual);
} else if (actual instanceof expected) {
return true;
} else if (expected.call({}, actual) === true) {
return true;
}
return false;
}
function _throws(shouldThrow, block, expected, message) {
var actual;
if (typeof block !== 'function') {
throw new TypeError('block must be a function');
}
if (typeof expected === 'string') {
message = expected;
expected = null;
}
try {
block();
} catch (e) {
actual = e;
}
message = (expected && expected.name ? ' (' + expected.name + ').' : '.') +
(message ? ' ' + message : '.');
if (shouldThrow && !actual) {
fail(actual, expected, 'Missing expected exception' + message);
}
if (!shouldThrow && expectedException(actual, expected)) {
fail(actual, expected, 'Got unwanted exception' + message);
}
if ((shouldThrow && actual && expected &&
!expectedException(actual, expected)) || (!shouldThrow && actual)) {
throw actual;
}
}
// 11. Expected to throw an error:
// assert.throws(block, Error_opt, message_opt);
assert.throws = function(block, /*optional*/error, /*optional*/message) {
_throws.apply(this, [true].concat(pSlice.call(arguments)));
};
// EXTENSION! This is annoying to write outside this module.
assert.doesNotThrow = function(block, /*optional*/message) {
_throws.apply(this, [false].concat(pSlice.call(arguments)));
};
assert.ifError = function(err) { if (err) {throw err;}};
return assert;
});

View File

@ -0,0 +1,263 @@
/*---------------------------------------------------------------------------------------------
* 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 path = require('path');
const glob = require('glob');
const fs = require('fs');
const events = require('events');
const mocha = require('mocha');
const MochaJUnitReporter = require('mocha-junit-reporter');
const url = require('url');
const minimatch = require('minimatch');
const playwright = require('playwright');
// opts
const defaultReporterName = process.platform === 'win32' ? 'list' : 'spec';
const optimist = require('optimist')
// .describe('grep', 'only run tests matching <pattern>').alias('grep', 'g').alias('grep', 'f').string('grep')
.describe('build', 'run with build output (out-build)').boolean('build')
.describe('run', 'only run tests matching <relative_file_path>').string('run')
.describe('glob', 'only run tests matching <glob_pattern>').string('glob')
.describe('debug', 'do not run browsers headless').boolean('debug')
.describe('browser', 'browsers in which tests should run').string('browser').default('browser', ['chromium', 'firefox', 'webkit'])
.describe('reporter', 'the mocha reporter').string('reporter').default('reporter', defaultReporterName)
.describe('reporter-options', 'the mocha reporter options').string('reporter-options').default('reporter-options', '')
.describe('tfs', 'tfs').string('tfs')
.describe('help', 'show the help').alias('help', 'h');
// logic
const argv = optimist.argv;
if (argv.help) {
optimist.showHelp();
process.exit(0);
}
const withReporter = (function () {
if (argv.tfs) {
{
return (browserType, runner) => {
new mocha.reporters.Spec(runner);
new MochaJUnitReporter(runner, {
reporterOptions: {
testsuitesTitle: `${argv.tfs} ${process.platform}`,
mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${browserType}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined
}
});
}
}
} else {
const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', argv.reporter);
let ctor;
try {
ctor = require(reporterPath);
} catch (err) {
try {
ctor = require(argv.reporter);
} catch (err) {
ctor = process.platform === 'win32' ? mocha.reporters.List : mocha.reporters.Spec;
console.warn(`could not load reporter: ${argv.reporter}, using ${ctor.name}`);
}
}
function parseReporterOption(value) {
let r = /^([^=]+)=(.*)$/.exec(value);
return r ? { [r[1]]: r[2] } : {};
}
let reporterOptions = argv['reporter-options'];
reporterOptions = typeof reporterOptions === 'string' ? [reporterOptions] : reporterOptions;
reporterOptions = reporterOptions.reduce((r, o) => Object.assign(r, parseReporterOption(o)), {});
return (_, runner) => new ctor(runner, { reporterOptions })
}
})()
const outdir = argv.build ? 'out-build' : 'out';
const out = path.join(__dirname, `../../../${outdir}`);
function ensureIsArray(a) {
return Array.isArray(a) ? a : [a];
}
const testModules = (async function () {
const excludeGlob = '**/{node,electron-sandbox,electron-browser,electron-main}/**/*.test.js';
let isDefaultModules = true;
let promise;
if (argv.run) {
// use file list (--run)
isDefaultModules = false;
promise = Promise.resolve(ensureIsArray(argv.run).map(file => {
file = file.replace(/^src/, 'out');
file = file.replace(/\.ts$/, '.js');
return path.relative(out, file);
}));
} else {
// glob patterns (--glob)
const defaultGlob = '**/*.test.js';
const pattern = argv.glob || defaultGlob
isDefaultModules = pattern === defaultGlob;
promise = new Promise((resolve, reject) => {
glob(pattern, { cwd: out }, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files)
}
});
});
}
return promise.then(files => {
const modules = [];
for (let file of files) {
if (!minimatch(file, excludeGlob)) {
modules.push(file.replace(/\.js$/, ''));
} else if (!isDefaultModules) {
console.warn(`DROPPONG ${file} because it cannot be run inside a browser`);
}
}
return modules;
})
})();
async function runTestsInBrowser(testModules, browserType) {
const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros
const browser = await playwright[browserType].launch({ headless: !Boolean(argv.debug), args });
const context = await browser.newContext();
const page = await context.newPage();
const target = url.pathToFileURL(path.join(__dirname, 'renderer.html'));
if (argv.build) {
target.search = `?build=true`;
}
await page.goto(target.href);
const emitter = new events.EventEmitter();
await page.exposeFunction('mocha_report', (type, data1, data2) => {
emitter.emit(type, data1, data2)
});
page.on('console', async msg => {
console[msg.type()](msg.text(), await Promise.all(msg.args().map(async arg => await arg.jsonValue())));
});
withReporter(browserType, new EchoRunner(emitter, browserType.toUpperCase()));
// collection failures for console printing
const fails = [];
emitter.on('fail', (test, err) => {
if (err.stack) {
const regex = /(vs\/.*\.test)\.js/;
for (let line of String(err.stack).split('\n')) {
const match = regex.exec(line);
if (match) {
fails.push(match[1]);
break;
}
}
}
});
try {
// @ts-expect-error
await page.evaluate(modules => loadAndRun(modules), testModules);
} catch (err) {
console.error(err);
}
await browser.close();
if (fails.length > 0) {
return `to DEBUG, open ${browserType.toUpperCase()} and navigate to ${target.href}?${fails.map(module => `m=${module}`).join('&')}`;
}
}
class EchoRunner extends events.EventEmitter {
constructor(event, title = '') {
super();
event.on('start', () => this.emit('start'));
event.on('end', () => this.emit('end'));
event.on('suite', (suite) => this.emit('suite', EchoRunner.deserializeSuite(suite, title)));
event.on('suite end', (suite) => this.emit('suite end', EchoRunner.deserializeSuite(suite, title)));
event.on('test', (test) => this.emit('test', EchoRunner.deserializeRunnable(test)));
event.on('test end', (test) => this.emit('test end', EchoRunner.deserializeRunnable(test)));
event.on('hook', (hook) => this.emit('hook', EchoRunner.deserializeRunnable(hook)));
event.on('hook end', (hook) => this.emit('hook end', EchoRunner.deserializeRunnable(hook)));
event.on('pass', (test) => this.emit('pass', EchoRunner.deserializeRunnable(test)));
event.on('fail', (test, err) => this.emit('fail', EchoRunner.deserializeRunnable(test, title), EchoRunner.deserializeError(err)));
event.on('pending', (test) => this.emit('pending', EchoRunner.deserializeRunnable(test)));
}
static deserializeSuite(suite, titleExtra) {
return {
root: suite.root,
suites: suite.suites,
tests: suite.tests,
title: titleExtra && suite.title ? `${suite.title} - /${titleExtra}/` : suite.title,
fullTitle: () => suite.fullTitle,
timeout: () => suite.timeout,
retries: () => suite.retries,
enableTimeouts: () => suite.enableTimeouts,
slow: () => suite.slow,
bail: () => suite.bail
};
}
static deserializeRunnable(runnable, titleExtra) {
return {
title: runnable.title,
fullTitle: () => titleExtra && runnable.fullTitle ? `${runnable.fullTitle} - /${titleExtra}/` : runnable.fullTitle,
async: runnable.async,
slow: () => runnable.slow,
speed: runnable.speed,
duration: runnable.duration
};
}
static deserializeError(err) {
const inspect = err.inspect;
err.inspect = () => inspect;
return err;
}
}
testModules.then(async modules => {
// run tests in selected browsers
const browserTypes = Array.isArray(argv.browser)
? argv.browser : [argv.browser];
const promises = browserTypes.map(async browserType => {
try {
return await runTestsInBrowser(modules, browserType);
} catch (err) {
console.error(err);
process.exit(1);
}
});
// aftermath
let didFail = false;
const messages = await Promise.all(promises);
for (let msg of messages) {
if (msg) {
didFail = true;
console.log(msg);
}
}
process.exit(didFail ? 1 : 0);
}).catch(err => {
console.error(err);
});

View File

@ -0,0 +1,148 @@
<html>
<head>
<meta charset="utf-8">
<title>VSCode Tests</title>
<link href="../../../node_modules/mocha/mocha.css" rel="stylesheet" />
</head>
<body>
<div id="mocha"></div>
<script src="../../../node_modules/mocha/mocha.js"></script>
<script>
// !!! DO NOT CHANGE !!!
// Our unit tests may run in environments without
// display (e.g. from builds) and tests may by
// accident bring up native dialogs or even open
// windows. This we cannot allow as it may crash
// the test run.
// !!! DO NOT CHANGE !!!
window.open = function () { throw new Error('window.open() is not supported in tests!'); };
window.alert = function () { throw new Error('window.alert() is not supported in tests!'); }
window.confirm = function () { throw new Error('window.confirm() is not supported in tests!'); }
// Ignore uncaught cancelled promise errors
window.addEventListener('unhandledrejection', e => {
const name = e && e.reason && e.reason.name;
if (name === 'Canceled') {
e.preventDefault();
e.stopPropagation();
}
});
mocha.setup({
ui: 'tdd',
timeout: 5000
});
</script>
<!-- Depending on --build or not, load loader from known locations -->
<script src="../../../out/vs/loader.js"></script>
<script src="../../../out-build/vs/loader.js"></script>
<script>
const urlParams = new URLSearchParams(window.location.search);
const isBuild = urlParams.get('build');
// configure loader
const baseUrl = window.location.href;
require.config({
catchError: true,
baseUrl: new URL('../../../src', baseUrl).href,
paths: {
vs: new URL(`../../../${!!isBuild ? 'out-build' : 'out'}/vs`, baseUrl).href,
assert: new URL('../assert.js', baseUrl).href,
sinon: new URL('../../../node_modules/sinon/pkg/sinon-1.17.7.js', baseUrl).href,
xterm: new URL('../../../node_modules/xterm/lib/xterm.js', baseUrl).href,
'iconv-lite-umd': new URL('../../../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js', baseUrl).href,
jschardet: new URL('../../../node_modules/jschardet/dist/jschardet.min.js', baseUrl).href
}
});
</script>
<script>
function serializeSuite(suite) {
return {
root: suite.root,
suites: suite.suites.map(serializeSuite),
tests: suite.tests.map(serializeRunnable),
title: suite.title,
fullTitle: suite.fullTitle(),
timeout: suite.timeout(),
retries: suite.retries(),
enableTimeouts: suite.enableTimeouts(),
slow: suite.slow(),
bail: suite.bail()
};
}
function serializeRunnable(runnable) {
return {
title: runnable.title,
fullTitle: runnable.fullTitle(),
async: runnable.async,
slow: runnable.slow(),
speed: runnable.speed,
duration: runnable.duration
};
}
function serializeError(err) {
return {
message: err.message,
stack: err.stack,
actual: err.actual,
expected: err.expected,
uncaught: err.uncaught,
showDiff: err.showDiff,
inspect: typeof err.inspect === 'function' ? err.inspect() : ''
};
}
function PlaywrightReporter(runner) {
runner.on('start', () => window.mocha_report('start'));
runner.on('end', () => window.mocha_report('end'));
runner.on('suite', suite => window.mocha_report('suite', serializeSuite(suite)));
runner.on('suite end', suite => window.mocha_report('suite end', serializeSuite(suite)));
runner.on('test', test => window.mocha_report('test', serializeRunnable(test)));
runner.on('test end', test => window.mocha_report('test end', serializeRunnable(test)));
runner.on('hook', hook => window.mocha_report('hook', serializeRunnable(hook)));
runner.on('hook end', hook => window.mocha_report('hook end', serializeRunnable(hook)));
runner.on('pass', test => window.mocha_report('pass', serializeRunnable(test)));
runner.on('fail', (test, err) => window.mocha_report('fail', serializeRunnable(test), serializeError(err)));
runner.on('pending', test => window.mocha_report('pending', serializeRunnable(test)));
};
window.loadAndRun = async function loadAndRun(modules, manual = false) {
// load
await Promise.all(modules.map(module => new Promise((resolve, reject) => {
require([module], resolve, err => {
console.log("BAD " + module + JSON.stringify(err, undefined, '\t'));
// console.log(module);
resolve({});
});
})));
// await new Promise((resolve, reject) => {
// require(modules, resolve, err => {
// console.log(err);
// reject(err);
// });
// });
// run
return new Promise((resolve, reject) => {
if (!manual) {
mocha.reporter(PlaywrightReporter);
}
mocha.run(failCount => resolve(failCount === 0));
});
}
const modules = new URL(window.location.href).searchParams.getAll('m');
if (Array.isArray(modules) && modules.length > 0) {
console.log('MANUALLY running tests', modules);
loadAndRun(modules, true).then(() => console.log('done'), err => console.log(err));
}
</script>
</body>
</html>

View File

@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const minimatch = require('minimatch');
const fs = require('fs');
const path = require('path');
const iLibInstrument = require('istanbul-lib-instrument');
const iLibCoverage = require('istanbul-lib-coverage');
const iLibSourceMaps = require('istanbul-lib-source-maps');
const iLibReport = require('istanbul-lib-report');
const iReports = require('istanbul-reports');
const REPO_PATH = toUpperDriveLetter(path.join(__dirname, '../../'));
exports.initialize = function (loaderConfig) {
const instrumenter = iLibInstrument.createInstrumenter();
loaderConfig.nodeInstrumenter = function (contents, source) {
if (minimatch(source, '**/test/**')) {
// tests don't get instrumented
return contents;
}
// Try to find a .map file
let map = undefined;
try {
map = JSON.parse(fs.readFileSync(`${source}.map`).toString());
} catch (err) {
// missing source map...
}
return instrumenter.instrumentSync(contents, source, map);
};
};
exports.createReport = function (isSingle) {
const mapStore = iLibSourceMaps.createSourceMapStore();
const coverageMap = iLibCoverage.createCoverageMap(global.__coverage__);
return mapStore.transformCoverage(coverageMap).then((transformed) => {
// Paths come out all broken
let newData = Object.create(null);
Object.keys(transformed.data).forEach((file) => {
const entry = transformed.data[file];
const fixedPath = fixPath(entry.path);
entry.data.path = fixedPath;
newData[fixedPath] = entry;
});
transformed.data = newData;
const context = iLibReport.createContext({
dir: path.join(REPO_PATH, `.build/coverage${isSingle ? '-single' : ''}`),
coverageMap: transformed
});
const tree = context.getTree('flat');
let reports = [];
if (isSingle) {
reports.push(iReports.create('lcovonly'));
} else {
reports.push(iReports.create('json'));
reports.push(iReports.create('lcov'));
reports.push(iReports.create('html'));
}
reports.forEach(report => tree.visit(report, context));
});
};
function toUpperDriveLetter(str) {
if (/^[a-z]:/.test(str)) {
return str.charAt(0).toUpperCase() + str.substr(1);
}
return str;
}
function toLowerDriveLetter(str) {
if (/^[A-Z]:/.test(str)) {
return str.charAt(0).toLowerCase() + str.substr(1);
}
return str;
}
function fixPath(brokenPath) {
const startIndex = brokenPath.lastIndexOf(REPO_PATH);
if (startIndex === -1) {
return toLowerDriveLetter(brokenPath);
}
return toLowerDriveLetter(brokenPath.substr(startIndex));
}

View File

@ -0,0 +1,175 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const { app, BrowserWindow, ipcMain } = require('electron');
const { tmpdir } = require('os');
const { join } = require('path');
const path = require('path');
const mocha = require('mocha');
const events = require('events');
const MochaJUnitReporter = require('mocha-junit-reporter');
const url = require('url');
// Disable render process reuse, we still have
// non-context aware native modules in the renderer.
app.allowRendererProcessReuse = false;
const defaultReporterName = process.platform === 'win32' ? 'list' : 'spec';
const optimist = require('optimist')
.describe('grep', 'only run tests matching <pattern>').alias('grep', 'g').alias('grep', 'f').string('grep')
.describe('run', 'only run tests from <file>').string('run')
.describe('runGlob', 'only run tests matching <file_pattern>').alias('runGlob', 'glob').alias('runGlob', 'runGrep').string('runGlob')
.describe('build', 'run with build output (out-build)').boolean('build')
.describe('coverage', 'generate coverage report').boolean('coverage')
.describe('debug', 'open dev tools, keep window open, reuse app data').string('debug')
.describe('reporter', 'the mocha reporter').string('reporter').default('reporter', defaultReporterName)
.describe('reporter-options', 'the mocha reporter options').string('reporter-options').default('reporter-options', '')
.describe('tfs').string('tfs')
.describe('help', 'show the help').alias('help', 'h');
const argv = optimist.argv;
if (argv.help) {
optimist.showHelp();
process.exit(0);
}
if (!argv.debug) {
app.setPath('userData', join(tmpdir(), `vscode-tests-${Date.now()}`));
}
function deserializeSuite(suite) {
return {
root: suite.root,
suites: suite.suites,
tests: suite.tests,
title: suite.title,
fullTitle: () => suite.fullTitle,
timeout: () => suite.timeout,
retries: () => suite.retries,
enableTimeouts: () => suite.enableTimeouts,
slow: () => suite.slow,
bail: () => suite.bail
};
}
function deserializeRunnable(runnable) {
return {
title: runnable.title,
fullTitle: () => runnable.fullTitle,
async: runnable.async,
slow: () => runnable.slow,
speed: runnable.speed,
duration: runnable.duration,
currentRetry: () => runnable.currentRetry
};
}
function deserializeError(err) {
const inspect = err.inspect;
err.inspect = () => inspect;
return err;
}
class IPCRunner extends events.EventEmitter {
constructor() {
super();
this.didFail = false;
ipcMain.on('start', () => this.emit('start'));
ipcMain.on('end', () => this.emit('end'));
ipcMain.on('suite', (e, suite) => this.emit('suite', deserializeSuite(suite)));
ipcMain.on('suite end', (e, suite) => this.emit('suite end', deserializeSuite(suite)));
ipcMain.on('test', (e, test) => this.emit('test', deserializeRunnable(test)));
ipcMain.on('test end', (e, test) => this.emit('test end', deserializeRunnable(test)));
ipcMain.on('hook', (e, hook) => this.emit('hook', deserializeRunnable(hook)));
ipcMain.on('hook end', (e, hook) => this.emit('hook end', deserializeRunnable(hook)));
ipcMain.on('pass', (e, test) => this.emit('pass', deserializeRunnable(test)));
ipcMain.on('fail', (e, test, err) => {
this.didFail = true;
this.emit('fail', deserializeRunnable(test), deserializeError(err));
});
ipcMain.on('pending', (e, test) => this.emit('pending', deserializeRunnable(test)));
}
}
function parseReporterOption(value) {
let r = /^([^=]+)=(.*)$/.exec(value);
return r ? { [r[1]]: r[2] } : {};
}
app.on('ready', () => {
ipcMain.on('error', (_, err) => {
if (!argv.debug) {
console.error(err);
app.exit(1);
}
});
const win = new BrowserWindow({
height: 600,
width: 800,
show: false,
webPreferences: {
preload: path.join(__dirname, '..', '..', '..', 'src', 'vs', 'base', 'parts', 'sandbox', 'electron-browser', 'preload.js'), // ensure similar environment as VSCode as tests may depend on this
nodeIntegration: true,
enableWebSQL: false,
enableRemoteModule: false,
spellcheck: false,
nativeWindowOpen: true,
webviewTag: true
}
});
win.webContents.on('did-finish-load', () => {
if (argv.debug) {
win.show();
win.webContents.openDevTools();
}
win.webContents.send('run', argv);
});
win.loadURL(url.format({ pathname: path.join(__dirname, 'renderer.html'), protocol: 'file:', slashes: true }));
const runner = new IPCRunner();
if (argv.tfs) {
new mocha.reporters.Spec(runner);
new MochaJUnitReporter(runner, {
reporterOptions: {
testsuitesTitle: `${argv.tfs} ${process.platform}`,
mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined
}
});
} else {
const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', argv.reporter);
let Reporter;
try {
Reporter = require(reporterPath);
} catch (err) {
try {
Reporter = require(argv.reporter);
} catch (err) {
Reporter = process.platform === 'win32' ? mocha.reporters.List : mocha.reporters.Spec;
console.warn(`could not load reporter: ${argv.reporter}, using ${Reporter.name}`);
}
}
let reporterOptions = argv['reporter-options'];
reporterOptions = typeof reporterOptions === 'string' ? [reporterOptions] : reporterOptions;
reporterOptions = reporterOptions.reduce((r, o) => Object.assign(r, parseReporterOption(o)), {});
new Reporter(runner, { reporterOptions });
}
if (!argv.debug) {
ipcMain.on('all done', () => app.exit(runner.didFail ? 1 : 0));
}
});

View File

@ -0,0 +1,44 @@
<html>
<head>
<meta charset="utf-8">
<title>VSCode Tests</title>
<link href="../../../node_modules/mocha/mocha.css" rel="stylesheet" />
</head>
<body>
<div id="mocha"></div>
<script src="../../../node_modules/mocha/mocha.js"></script>
<script>
// !!! DO NOT CHANGE !!!
// Our unit tests may run in environments without
// display (e.g. from builds) and tests may by
// accident bring up native dialogs or even open
// windows. This we cannot allow as it may crash
// the test run.
// !!! DO NOT CHANGE !!!
window.open = function () { throw new Error('window.open() is not supported in tests!'); };
window.alert = function () { throw new Error('window.alert() is not supported in tests!'); }
window.confirm = function () { throw new Error('window.confirm() is not supported in tests!'); }
// Ignore uncaught cancelled promise errors
window.addEventListener('unhandledrejection', e => {
const name = e && e.reason && e.reason.name;
if (name === 'Canceled') {
e.preventDefault();
e.stopPropagation();
}
});
mocha.setup({
ui: 'tdd',
timeout: 5000
});
require('./renderer');
</script>
</body>
</html>

View File

@ -0,0 +1,224 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*eslint-env mocha*/
const { ipcRenderer } = require('electron');
const assert = require('assert');
const path = require('path');
const glob = require('glob');
const util = require('util');
const bootstrap = require('../../../src/bootstrap');
const coverage = require('../coverage');
// Disabled custom inspect. See #38847
if (util.inspect && util.inspect['defaultOptions']) {
util.inspect['defaultOptions'].customInspect = false;
}
let _tests_glob = '**/test/**/*.test.js';
let loader;
let _out;
function initLoader(opts) {
let outdir = opts.build ? 'out-build' : 'out';
_out = path.join(__dirname, `../../../${outdir}`);
// setup loader
loader = require(`${_out}/vs/loader`);
const loaderConfig = {
nodeRequire: require,
nodeMain: __filename,
catchError: true,
baseUrl: bootstrap.fileUriFromPath(path.join(__dirname, '../../../src'), { isWindows: process.platform === 'win32' }),
paths: {
'vs': `../${outdir}/vs`,
'lib': `../${outdir}/lib`,
'bootstrap-fork': `../${outdir}/bootstrap-fork`
}
};
if (opts.coverage) {
// initialize coverage if requested
coverage.initialize(loaderConfig);
}
loader.require.config(loaderConfig);
}
function createCoverageReport(opts) {
if (opts.coverage) {
return coverage.createReport(opts.run || opts.runGlob);
}
return Promise.resolve(undefined);
}
function loadTestModules(opts) {
if (opts.run) {
const files = Array.isArray(opts.run) ? opts.run : [opts.run];
const modules = files.map(file => {
file = file.replace(/^src/, 'out');
file = file.replace(/\.ts$/, '.js');
return path.relative(_out, file).replace(/\.js$/, '');
});
return new Promise((resolve, reject) => {
loader.require(modules, resolve, reject);
});
}
const pattern = opts.runGlob || _tests_glob;
return new Promise((resolve, reject) => {
glob(pattern, { cwd: _out }, (err, files) => {
if (err) {
reject(err);
return;
}
const modules = files.map(file => file.replace(/\.js$/, ''));
resolve(modules);
});
}).then(modules => {
return new Promise((resolve, reject) => {
loader.require(modules, resolve, reject);
});
});
}
function loadTests(opts) {
const _unexpectedErrors = [];
const _loaderErrors = [];
// collect loader errors
loader.require.config({
onError(err) {
_loaderErrors.push(err);
console.error(err);
}
});
// collect unexpected errors
loader.require(['vs/base/common/errors'], function (errors) {
errors.setUnexpectedErrorHandler(function (err) {
let stack = (err ? err.stack : null);
if (!stack) {
stack = new Error().stack;
}
_unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack);
});
});
return loadTestModules(opts).then(() => {
suite('Unexpected Errors & Loader Errors', function () {
test('should not have unexpected errors', function () {
const errors = _unexpectedErrors.concat(_loaderErrors);
if (errors.length) {
errors.forEach(function (stack) {
console.error('');
console.error(stack);
});
assert.ok(false, errors);
}
});
});
});
}
function serializeSuite(suite) {
return {
root: suite.root,
suites: suite.suites.map(serializeSuite),
tests: suite.tests.map(serializeRunnable),
title: suite.title,
fullTitle: suite.fullTitle(),
timeout: suite.timeout(),
retries: suite.retries(),
enableTimeouts: suite.enableTimeouts(),
slow: suite.slow(),
bail: suite.bail()
};
}
function serializeRunnable(runnable) {
return {
title: runnable.title,
fullTitle: runnable.fullTitle(),
async: runnable.async,
slow: runnable.slow(),
speed: runnable.speed,
duration: runnable.duration
};
}
function serializeError(err) {
return {
message: err.message,
stack: err.stack,
actual: err.actual,
expected: err.expected,
uncaught: err.uncaught,
showDiff: err.showDiff,
inspect: typeof err.inspect === 'function' ? err.inspect() : ''
};
}
class IPCReporter {
constructor(runner) {
runner.on('start', () => ipcRenderer.send('start'));
runner.on('end', () => ipcRenderer.send('end'));
runner.on('suite', suite => ipcRenderer.send('suite', serializeSuite(suite)));
runner.on('suite end', suite => ipcRenderer.send('suite end', serializeSuite(suite)));
runner.on('test', test => ipcRenderer.send('test', serializeRunnable(test)));
runner.on('test end', test => ipcRenderer.send('test end', serializeRunnable(test)));
runner.on('hook', hook => ipcRenderer.send('hook', serializeRunnable(hook)));
runner.on('hook end', hook => ipcRenderer.send('hook end', serializeRunnable(hook)));
runner.on('pass', test => ipcRenderer.send('pass', serializeRunnable(test)));
runner.on('fail', (test, err) => ipcRenderer.send('fail', serializeRunnable(test), serializeError(err)));
runner.on('pending', test => ipcRenderer.send('pending', serializeRunnable(test)));
}
}
function runTests(opts) {
return loadTests(opts).then(() => {
if (opts.grep) {
mocha.grep(opts.grep);
}
if (!opts.debug) {
mocha.reporter(IPCReporter);
}
const runner = mocha.run(() => {
createCoverageReport(opts).then(() => {
ipcRenderer.send('all done');
});
});
if (opts.debug) {
runner.on('fail', (test, err) => {
console.error(test.fullTitle());
console.error(err.stack);
});
}
});
}
ipcRenderer.on('run', (e, opts) => {
initLoader(opts);
runTests(opts).catch(err => {
if (typeof err !== 'string') {
err = JSON.stringify(err);
}
console.error(err);
ipcRenderer.send('error', err);
});
});

View File

@ -0,0 +1,172 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*eslint-env mocha*/
/*global define,run*/
const assert = require('assert');
const path = require('path');
const glob = require('glob');
const jsdom = require('jsdom-no-contextify');
const TEST_GLOB = '**/test/**/*.test.js';
const coverage = require('../coverage');
const optimist = require('optimist')
.usage('Run the Code tests. All mocha options apply.')
.describe('build', 'Run from out-build').boolean('build')
.describe('run', 'Run a single file').string('run')
.describe('coverage', 'Generate a coverage report').boolean('coverage')
.describe('browser', 'Run tests in a browser').boolean('browser')
.alias('h', 'help').boolean('h')
.describe('h', 'Show help');
const argv = optimist.argv;
if (argv.help) {
optimist.showHelp();
process.exit(1);
}
const REPO_ROOT = path.join(__dirname, '../../../');
const out = argv.build ? 'out-build' : 'out';
const loader = require(`../../../${out}/vs/loader`);
const src = path.join(REPO_ROOT, out);
function main() {
process.on('uncaughtException', function (e) {
console.error(e.stack || e);
});
const loaderConfig = {
nodeRequire: require,
nodeMain: __filename,
baseUrl: path.join(REPO_ROOT, 'src'),
paths: {
'vs/css': '../test/unit/node/css.mock',
'vs': `../${out}/vs`,
'lib': `../${out}/lib`,
'bootstrap-fork': `../${out}/bootstrap-fork`
},
catchError: true
};
if (argv.coverage) {
coverage.initialize(loaderConfig);
process.on('exit', function (code) {
if (code !== 0) {
return;
}
coverage.createReport(argv.run || argv.runGlob);
});
}
loader.config(loaderConfig);
global.define = loader;
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.self = global.window = global.document.parentWindow;
global.Element = global.window.Element;
global.HTMLElement = global.window.HTMLElement;
global.Node = global.window.Node;
global.navigator = global.window.navigator;
global.XMLHttpRequest = global.window.XMLHttpRequest;
let didErr = false;
const write = process.stderr.write;
process.stderr.write = function (data) {
didErr = didErr || !!data;
write.apply(process.stderr, arguments);
};
let loadFunc = null;
if (argv.runGlob) {
loadFunc = (cb) => {
const doRun = tests => {
const modulesToLoad = tests.map(test => {
if (path.isAbsolute(test)) {
test = path.relative(src, path.resolve(test));
}
return test.replace(/(\.js)|(\.d\.ts)|(\.js\.map)$/, '');
});
define(modulesToLoad, () => cb(null), cb);
};
glob(argv.runGlob, { cwd: src }, function (err, files) { doRun(files); });
};
} else if (argv.run) {
const tests = (typeof argv.run === 'string') ? [argv.run] : argv.run;
const modulesToLoad = tests.map(function (test) {
test = test.replace(/^src/, 'out');
test = test.replace(/\.ts$/, '.js');
return path.relative(src, path.resolve(test)).replace(/(\.js)|(\.js\.map)$/, '').replace(/\\/g, '/');
});
loadFunc = (cb) => {
define(modulesToLoad, () => cb(null), cb);
};
} else {
loadFunc = (cb) => {
glob(TEST_GLOB, { cwd: src }, function (err, files) {
const modulesToLoad = files.map(function (file) {
return file.replace(/\.js$/, '');
});
define(modulesToLoad, function () { cb(null); }, cb);
});
};
}
loadFunc(function (err) {
if (err) {
console.error(err);
return process.exit(1);
}
process.stderr.write = write;
if (!argv.run && !argv.runGlob) {
// set up last test
suite('Loader', function () {
test('should not explode while loading', function () {
assert.ok(!didErr, 'should not explode while loading');
});
});
}
// report failing test for every unexpected error during any of the tests
let unexpectedErrors = [];
suite('Errors', function () {
test('should not have unexpected errors in tests', function () {
if (unexpectedErrors.length) {
unexpectedErrors.forEach(function (stack) {
console.error('');
console.error(stack);
});
assert.ok(false);
}
});
});
// replace the default unexpected error handler to be useful during tests
loader(['vs/base/common/errors'], function (errors) {
errors.setUnexpectedErrorHandler(function (err) {
const stack = (err && err.stack) || (new Error().stack);
unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack);
});
// fire up mocha
run();
});
});
}
if (process.argv.some(function (a) { return /^--browser/.test(a); })) {
require('./browser');
} else {
main();
}

View File

@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* 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 glob = require('glob');
const path = require('path');
const fs = require('fs');
const REPO_ROOT = path.join(__dirname, '../../../');
const PORT = 8887;
function template(str, env) {
return str.replace(/{{\s*([\w_\-]+)\s*}}/g, function (all, part) {
return env[part];
});
}
yaserver.createServer({ rootDir: REPO_ROOT }).then((staticServer) => {
const server = http.createServer((req, res) => {
if (req.url === '' || req.url === '/') {
glob('**/vs/{base,platform,editor}/**/test/{common,browser}/**/*.test.js', {
cwd: path.join(REPO_ROOT, 'out'),
// ignore: ['**/test/{node,electron*}/**/*.js']
}, function (err, files) {
if (err) { console.log(err); process.exit(0); }
var modules = files
.map(function (file) { return file.replace(/\.js$/, ''); });
fs.readFile(path.join(__dirname, 'index.html'), 'utf8', function (err, templateString) {
if (err) { console.log(err); process.exit(0); }
res.end(template(templateString, {
modules: JSON.stringify(modules)
}));
});
});
} else {
return staticServer.handle(req, res);
}
});
server.listen(PORT, () => {
console.log(`http://localhost:${PORT}/`);
});
});

View File

@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
define([], function() {
return {
load: function(name, req, load) {
load({});
}
};
});

View File

@ -0,0 +1,29 @@
<html>
<head>
<meta charset="utf-8">
<title>VSCode Tests</title>
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
</head>
<body>
<div id="mocha"></div>
<script src="/out/vs/loader.js"></script>
<script src="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.js"></script>
<script>
mocha.setup('tdd');
require.config({
baseUrl: '/out',
paths: {
assert: '/test/unit/assert.js',
sinon: '/node_modules/sinon/pkg/sinon-1.17.7.js'
}
});
require({{ modules }}, function () {
mocha.run();
});
</script>
</body>
</html>