mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-10 10:12:34 -05:00
Merge from vscode 64980ea1f3f532c82bb6c28d27bba9ef2c5b4463 (#7206)
* Merge from vscode 64980ea1f3f532c82bb6c28d27bba9ef2c5b4463 * fix config changes * fix strictnull checks
This commit is contained in:
@@ -1,159 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Workbench } from './areas/workbench/workbench';
|
||||
import { Code, spawn, SpawnOptions } from './vscode/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;
|
||||
|
||||
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();
|
||||
//{{SQL CARBON EDIT}}
|
||||
await this.code.waitForElement('.object-explorer-view');
|
||||
|
||||
//Original
|
||||
/*
|
||||
await this.code.waitForElement('.explorer-folders-view');
|
||||
|
||||
if (expectWalkthroughPart) {
|
||||
await this.code.waitForActiveElement(`.editor-instance[id="workbench.editor.walkThroughPart"] > div > div[tabIndex="0"]`);
|
||||
}
|
||||
*/
|
||||
//{{SQL CARBON EDIT}}
|
||||
}
|
||||
|
||||
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,
|
||||
headless: this.options.headless
|
||||
});
|
||||
|
||||
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.waitForElement('.monaco-workbench .statusbar-item[id="status.host"]');
|
||||
}
|
||||
|
||||
// wait a bit, since focus might be stolen off widgets
|
||||
// as soon as they open (e.g. quick open)
|
||||
await new Promise(c => setTimeout(c, 1000));
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/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}`);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application } from '../../application';
|
||||
import { ProblemSeverity, Problems } from '../problems/problems';
|
||||
import { Application, ProblemSeverity, Problems } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('CSS', () => {
|
||||
@@ -44,4 +43,4 @@ export function setup() {
|
||||
await problems.hideProblemsView();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as http from 'http';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as stripJsonComments from 'strip-json-comments';
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Debug', () => {
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Viewlet } from '../workbench/viewlet';
|
||||
import { Commands } from '../workbench/workbench';
|
||||
import { Code, findElement } from '../../vscode/code';
|
||||
import { Editors } from '../editor/editors';
|
||||
import { Editor } from '../editor/editor';
|
||||
import { IElement } from '../../vscode/driver';
|
||||
|
||||
const VIEWLET = 'div[id="workbench.view.debug"]';
|
||||
const DEBUG_VIEW = `${VIEWLET} .debug-view-content`;
|
||||
const CONFIGURE = `div[id="workbench.parts.sidebar"] .actions-container .configure`;
|
||||
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 = '.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 => `${STACK_FRAME} .file[title*="${filename}"]`;
|
||||
const VARIABLE = `${VIEWLET} .debug-variables .monaco-list-row .expression`;
|
||||
const CONSOLE_OUTPUT = `.repl .output.expression .value`;
|
||||
const CONSOLE_INPUT_OUTPUT = `.repl .input-output-pair .output.expression .value`;
|
||||
|
||||
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.waitForElement(CONSOLE_INPUT_OUTPUT);
|
||||
await this.waitForOutput(output => accept(output[output.length - 1] || ''));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Editor', () => {
|
||||
@@ -67,4 +67,4 @@ export function setup() {
|
||||
await peek.waitForFile('app.js');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { References } from './peek';
|
||||
import { Commands } from '../workbench/workbench';
|
||||
import { Code } from '../../vscode/code';
|
||||
|
||||
const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box';
|
||||
const RENAME_INPUT = `${RENAME_BOX} .rename-input`;
|
||||
const EDITOR = filename => `.monaco-editor[data-uri$="${filename}"]`;
|
||||
const VIEW_LINES = filename => `${EDITOR(filename)} .view-lines`;
|
||||
const LINE_NUMBERS = filename => `${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 Implementation');
|
||||
}
|
||||
|
||||
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, 0, 0);
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/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(tabName: string, untitled: boolean = false): Promise<void> {
|
||||
await this.code.waitAndClick(`.tabs-container div.tab[aria-label="${tabName}, tab"]`);
|
||||
await this.waitForEditorFocus(tabName, untitled);
|
||||
}
|
||||
|
||||
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, untitled: boolean = false): 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"][aria-label="${fileName}, tab"]`);
|
||||
}
|
||||
|
||||
async waitForTab(fileName: string, isDirty: boolean = false): Promise<void> {
|
||||
await this.code.waitForElement(`.tabs-container div.tab${isDirty ? '.dirty' : ''}[aria-label="${fileName}, tab"]`);
|
||||
}
|
||||
|
||||
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', true);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Explorer', () => {
|
||||
@@ -37,4 +37,4 @@ export function setup() {
|
||||
await app.code.dispatchKeybinding('escape');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Viewlet } from '../workbench/viewlet';
|
||||
import { Editors } from '../editor/editors';
|
||||
import { Code } from '../../vscode/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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application, Quality } from '../../application';
|
||||
import { Application, Quality } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Extensions', () => {
|
||||
@@ -28,4 +28,4 @@ export function setup() {
|
||||
await app.workbench.statusbar.waitForStatusbarText('smoke test', 'VS Code Smoke Test Check');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Viewlet } from '../workbench/viewlet';
|
||||
import { Code } from '../../vscode/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, name: string): Promise<void> {
|
||||
await this.searchForExtension(id);
|
||||
const ariaLabel = `${name}. Press enter for extension details.`;
|
||||
await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${ariaLabel}"] .extension li[class='action-item'] .extension-action.install`);
|
||||
await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
|
||||
const DIFF_EDITOR_LINE_INSERT = '.monaco-diff-editor .editor.modified .line-insert';
|
||||
const SYNC_STATUSBAR = 'div[id="workbench.parts.statusbar"] .statusbar-item[title$="Synchronize Changes"]';
|
||||
@@ -74,4 +74,4 @@ export function setup() {
|
||||
cp.execSync('git reset --hard origin/master', { cwd: app.workspacePathOrFolder });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Viewlet } from '../workbench/viewlet';
|
||||
import { IElement } from '../../vscode/driver';
|
||||
import { findElement, findElements, Code } from '../../vscode/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('app.js', '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);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
|
||||
function toUri(path: string): string {
|
||||
if (process.platform === 'win32') {
|
||||
@@ -47,8 +47,7 @@ export function setup() {
|
||||
const app = this.app as Application;
|
||||
await app.workbench.quickopen.openQuickOpen('*.*');
|
||||
|
||||
// TODO roblourens: Go to files finds welcome page: issue 74875
|
||||
await app.workbench.quickopen.waitForQuickOpenElements(names => names.length === 6 || names.length === 7);
|
||||
await app.workbench.quickopen.waitForQuickOpenElements(names => names.length === 6);
|
||||
await app.workbench.quickopen.closeQuickOpen();
|
||||
});
|
||||
|
||||
@@ -57,4 +56,4 @@ export function setup() {
|
||||
await app.code.waitForTitle(title => /smoketest \(Workspace\)/i.test(title));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/code';
|
||||
|
||||
const SEARCH_INPUT = '.keybindings-header .settings-search-input input';
|
||||
|
||||
export class KeybindingsEditor {
|
||||
|
||||
constructor(private code: Code) { }
|
||||
|
||||
async updateKeybinding(command: string, keybinding: string, ariaLabel: 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 .icon.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 div[aria-label="Keybinding is ${ariaLabel}."]`);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application } from '../../application';
|
||||
import { ActivityBarPosition } from '../activitybar/activityBar';
|
||||
import { Application, ActivityBarPosition } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Preferences', () => {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Editor } from '../editor/editor';
|
||||
import { Editors } from '../editor/editors';
|
||||
import { Code } from '../../vscode/code';
|
||||
import { QuickOpen } from '../quickopen/quickopen';
|
||||
|
||||
export const enum ActivityBarPosition {
|
||||
LEFT = 0,
|
||||
RIGHT = 1
|
||||
}
|
||||
|
||||
export class SettingsEditor {
|
||||
|
||||
constructor(private code: Code, private userDataPath: string, private editors: Editors, private editor: Editor, private quickopen: QuickOpen) { }
|
||||
|
||||
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.quickopen.runCommand('Preferences: Open Settings (JSON)');
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/code';
|
||||
|
||||
export const enum ProblemSeverity {
|
||||
WARNING = 0,
|
||||
ERROR = 1
|
||||
}
|
||||
|
||||
export class Problems {
|
||||
|
||||
static PROBLEMS_VIEW_SELECTOR = '.panel.markers-panel';
|
||||
|
||||
constructor(private code: Code) { }
|
||||
|
||||
public async showProblemsView(): Promise<any> {
|
||||
await this.toggleProblemsView();
|
||||
await this.waitForProblemsView();
|
||||
}
|
||||
|
||||
public async hideProblemsView(): Promise<any> {
|
||||
await this.toggleProblemsView();
|
||||
await this.code.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR, el => !el);
|
||||
}
|
||||
|
||||
private async toggleProblemsView(): Promise<void> {
|
||||
if (process.platform === 'darwin') {
|
||||
await this.code.dispatchKeybinding('cmd+shift+m');
|
||||
} else {
|
||||
await this.code.dispatchKeybinding('ctrl+shift+m');
|
||||
}
|
||||
}
|
||||
|
||||
public async waitForProblemsView(): Promise<void> {
|
||||
await this.code.waitForElement(Problems.PROBLEMS_VIEW_SELECTOR);
|
||||
}
|
||||
|
||||
public static getSelectorInProblemsView(problemType: ProblemSeverity): string {
|
||||
let selector = problemType === ProblemSeverity.WARNING ? 'severity-warning' : 'severity-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}`;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/code';
|
||||
|
||||
export class QuickInput {
|
||||
|
||||
static QUICK_INPUT = '.quick-input-widget';
|
||||
static QUICK_INPUT_INPUT = `${QuickInput.QUICK_INPUT} .quick-input-box input`;
|
||||
static QUICK_INPUT_FOCUSED_ELEMENT = `${QuickInput.QUICK_INPUT} .quick-open-tree .monaco-tree-row.focused .monaco-highlighted-label`;
|
||||
|
||||
constructor(private code: Code) { }
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private 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();
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Editors } from '../editor/editors';
|
||||
import { Code } from '../../vscode/code';
|
||||
|
||||
export class QuickOpen {
|
||||
|
||||
static QUICK_OPEN = 'div.monaco-quick-open-widget';
|
||||
static QUICK_OPEN_HIDDEN = 'div.monaco-quick-open-widget[aria-hidden="true"]';
|
||||
static QUICK_OPEN_INPUT = `${QuickOpen.QUICK_OPEN} .quick-open-input input`;
|
||||
static QUICK_OPEN_FOCUSED_ELEMENT = `${QuickOpen.QUICK_OPEN} .quick-open-tree .monaco-tree-row.focused .monaco-highlighted-label`;
|
||||
static QUICK_OPEN_ENTRY_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry';
|
||||
static QUICK_OPEN_ENTRY_LABEL_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry .label-name';
|
||||
|
||||
constructor(private code: Code, private editors: Editors) { }
|
||||
|
||||
async openQuickOpen(value: string): Promise<void> {
|
||||
let retries = 0;
|
||||
|
||||
// other parts of code might steal focus away from quickopen :(
|
||||
while (retries < 5) {
|
||||
if (process.platform === 'darwin') {
|
||||
await this.code.dispatchKeybinding('cmd+p');
|
||||
} else {
|
||||
await this.code.dispatchKeybinding('ctrl+p');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.waitForQuickOpenOpened(10);
|
||||
break;
|
||||
} catch (err) {
|
||||
if (++retries > 5) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
await this.code.dispatchKeybinding('escape');
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
await this.code.waitForSetValue(QuickOpen.QUICK_OPEN_INPUT, value);
|
||||
}
|
||||
}
|
||||
|
||||
async closeQuickOpen(): Promise<void> {
|
||||
await this.code.dispatchKeybinding('escape');
|
||||
await this.waitForQuickOpenClosed();
|
||||
}
|
||||
|
||||
async openFile(fileName: string): Promise<void> {
|
||||
await this.openQuickOpen(fileName);
|
||||
|
||||
await this.waitForQuickOpenElements(names => names[0] === fileName);
|
||||
await this.code.dispatchKeybinding('enter');
|
||||
await this.editors.waitForActiveTab(fileName);
|
||||
await this.editors.waitForEditorFocus(fileName);
|
||||
}
|
||||
|
||||
async waitForQuickOpenOpened(retryCount?: number): Promise<void> {
|
||||
await this.code.waitForActiveElement(QuickOpen.QUICK_OPEN_INPUT, retryCount);
|
||||
}
|
||||
|
||||
private async waitForQuickOpenClosed(): Promise<void> {
|
||||
await this.code.waitForElement(QuickOpen.QUICK_OPEN_HIDDEN);
|
||||
}
|
||||
|
||||
async submit(text: string): Promise<void> {
|
||||
await this.code.waitForSetValue(QuickOpen.QUICK_OPEN_INPUT, text);
|
||||
await this.code.dispatchKeybinding('enter');
|
||||
await this.waitForQuickOpenClosed();
|
||||
}
|
||||
|
||||
async selectQuickOpenElement(index: number): Promise<void> {
|
||||
await this.waitForQuickOpenOpened();
|
||||
for (let from = 0; from < index; from++) {
|
||||
await this.code.dispatchKeybinding('down');
|
||||
}
|
||||
await this.code.dispatchKeybinding('enter');
|
||||
await this.waitForQuickOpenClosed();
|
||||
}
|
||||
|
||||
async waitForQuickOpenElements(accept: (names: string[]) => boolean): Promise<void> {
|
||||
await this.code.waitForElements(QuickOpen.QUICK_OPEN_ENTRY_LABEL_SELECTOR, false, els => accept(els.map(e => e.textContent)));
|
||||
}
|
||||
|
||||
async runCommand(command: string): Promise<void> {
|
||||
await this.openQuickOpen(`> ${command}`);
|
||||
|
||||
// wait for best choice to be focused
|
||||
await this.code.waitForTextContent(QuickOpen.QUICK_OPEN_FOCUSED_ELEMENT, command);
|
||||
|
||||
// wait and click on best choice
|
||||
await this.code.waitAndClick(QuickOpen.QUICK_OPEN_FOCUSED_ELEMENT);
|
||||
}
|
||||
|
||||
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('div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties div.monaco-tree-row .quick-open-entry .monaco-icon-label .label-name .monaco-highlighted-label span');
|
||||
|
||||
if (text !== 'No symbol information for the file') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.closeQuickOpen();
|
||||
await new Promise(c => setTimeout(c, 250));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Search', () => {
|
||||
@@ -56,4 +56,4 @@ export function setup() {
|
||||
await app.workbench.search.waitForNoResultText();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Viewlet } from '../workbench/viewlet';
|
||||
import { Code } from '../../vscode/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 => `${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.icon.action-remove`, el => !!el && el.top > 0 && el.left > 0, 10)
|
||||
);
|
||||
|
||||
// ¯\_(ツ)_/¯
|
||||
await new Promise(c => setTimeout(c, 500));
|
||||
await this.code.waitAndClick(`${fileMatch} .action-label.icon.action-remove`);
|
||||
await this.code.waitForElement(fileMatch, el => !el);
|
||||
}
|
||||
|
||||
async expandReplace(): Promise<void> {
|
||||
await this.code.waitAndClick(`${VIEWLET} .search-widget .monaco-button.toggle-replace-button.collapse`);
|
||||
}
|
||||
|
||||
async collapseReplace(): Promise<void> {
|
||||
await this.code.waitAndClick(`${VIEWLET} .search-widget .monaco-button.toggle-replace-button.expand`);
|
||||
}
|
||||
|
||||
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.icon.action-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.icon.action-replace-all`);
|
||||
}
|
||||
|
||||
async waitForResultText(text: string): Promise<void> {
|
||||
await this.code.waitForTextContent(`${VIEWLET} .messages .message>p`, text);
|
||||
}
|
||||
|
||||
async waitForNoResultText(): Promise<void> {
|
||||
await this.code.waitForElement(`${VIEWLET} .messages[aria-hidden="true"] .message>p`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application, Quality } from '../../application';
|
||||
import { StatusBarElement } from './statusbar';
|
||||
import { Application, Quality, StatusBarElement } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Statusbar', () => {
|
||||
@@ -90,4 +89,4 @@ export function setup() {
|
||||
await app.workbench.statusbar.waitForEOL('CRLF');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/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 = 'div[id="workbench.parts.statusbar"]';
|
||||
private readonly leftSelector = '.statusbar-item.left';
|
||||
private readonly rightSelector = '.statusbar-item.right';
|
||||
|
||||
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 `${this.mainSelector} ${this.leftSelector} .octicon.octicon-git-branch`;
|
||||
case StatusBarElement.SYNC_STATUS:
|
||||
return `${this.mainSelector} ${this.leftSelector} .octicon.octicon-sync`;
|
||||
case StatusBarElement.PROBLEMS_STATUS:
|
||||
return `${this.mainSelector} ${this.leftSelector} .octicon.octicon-error`;
|
||||
case StatusBarElement.SELECTION_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Go to Line"]`;
|
||||
case StatusBarElement.INDENTATION_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Select Indentation"]`;
|
||||
case StatusBarElement.ENCODING_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Select Encoding"]`;
|
||||
case StatusBarElement.EOL_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Select End of Line Sequence"]`;
|
||||
case StatusBarElement.LANGUAGE_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Select Language Mode"]`;
|
||||
case StatusBarElement.FEEDBACK_ICON:
|
||||
return `${this.mainSelector} .statusbar-item.right[id="status.feedback"]`;
|
||||
default:
|
||||
throw new Error(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Terminal', () => {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/code';
|
||||
import { QuickOpen } from '../quickopen/quickopen';
|
||||
|
||||
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 quickopen: QuickOpen) { }
|
||||
|
||||
async showTerminal(): Promise<void> {
|
||||
await this.quickopen.runCommand('View: Toggle Integrated Terminal');
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Dataloss', () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application, ApplicationOptions } from '../../application';
|
||||
import { Application, ApplicationOptions } from '../../../../automation';
|
||||
import { join } from 'path';
|
||||
|
||||
export function setup(stableCodePath: string, testDataPath: string) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { Application, ApplicationOptions } from '../../application';
|
||||
import { Application, ApplicationOptions } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
|
||||
@@ -35,4 +35,4 @@ export function setup() {
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application, Quality } from '../../application';
|
||||
import { Application, Quality } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('Localization', () => {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/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);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Explorer } from '../explorer/explorer';
|
||||
import { ActivityBar } from '../activitybar/activityBar';
|
||||
import { QuickOpen } from '../quickopen/quickopen';
|
||||
import { QuickInput } from '../quickinput/quickinput';
|
||||
import { Extensions } from '../extensions/extensions';
|
||||
import { Search } from '../search/search';
|
||||
import { Editor } from '../editor/editor';
|
||||
import { SCM } from '../git/scm';
|
||||
import { Debug } from '../debug/debugSmoke';
|
||||
import { StatusBar } from '../statusbar/statusbar';
|
||||
import { Problems } from '../problems/problems';
|
||||
import { SettingsEditor } from '../preferences/settings';
|
||||
import { KeybindingsEditor } from '../preferences/keybindings';
|
||||
import { Editors } from '../editor/editors';
|
||||
import { Code } from '../../vscode/code';
|
||||
import { Terminal } from '../terminal/terminal';
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
import { ConnectionDialog } from '../../sql/connectionDialog/connectionDialog';
|
||||
import { Profiler } from '../../sql/profiler/profiler';
|
||||
import { QueryEditors } from '../../sql/queryEditor/queryEditors';
|
||||
// {{END}}
|
||||
|
||||
export interface Commands {
|
||||
runCommand(command: string): Promise<any>;
|
||||
}
|
||||
|
||||
export class Workbench {
|
||||
|
||||
readonly quickopen: QuickOpen;
|
||||
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;
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
readonly connectionDialog: ConnectionDialog;
|
||||
readonly profiler: Profiler;
|
||||
readonly queryEditors: QueryEditors;
|
||||
// {{END}}
|
||||
|
||||
constructor(code: Code, userDataPath: string) {
|
||||
this.editors = new Editors(code);
|
||||
this.quickopen = new QuickOpen(code, this.editors);
|
||||
this.quickinput = new QuickInput(code);
|
||||
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.quickopen);
|
||||
this.scm = new SCM(code);
|
||||
this.debug = new Debug(code, this.quickopen, this.editors, this.editor);
|
||||
this.statusbar = new StatusBar(code);
|
||||
this.problems = new Problems(code);
|
||||
this.settingsEditor = new SettingsEditor(code, userDataPath, this.editors, this.editor, this.quickopen);
|
||||
this.keybindingsEditor = new KeybindingsEditor(code);
|
||||
this.terminal = new Terminal(code, this.quickopen);
|
||||
// {{SQL CARBON EDIT}}
|
||||
this.connectionDialog = new ConnectionDialog(code);
|
||||
this.profiler = new Profiler(code, this.quickopen);
|
||||
this.queryEditors = new QueryEditors(code, this.quickopen);
|
||||
// {{END}}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,15 @@ import * as tmp from 'tmp';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as mkdirp from 'mkdirp';
|
||||
import { ncp } from 'ncp';
|
||||
import { Application, Quality, ApplicationOptions } from './application';
|
||||
import {
|
||||
Application,
|
||||
Quality,
|
||||
ApplicationOptions,
|
||||
MultiLogger,
|
||||
Logger,
|
||||
ConsoleLogger,
|
||||
FileLogger,
|
||||
} from '../../automation';
|
||||
|
||||
//{{SQL CARBON EDIT}}
|
||||
import { setup as runProfilerTests } from './sql/profiler/profiler.test';
|
||||
@@ -33,10 +41,7 @@ import { setup as setupDataExtensionTests } from './areas/extensions/extensions.
|
||||
import { setup as setupTerminalTests } from './areas/terminal/terminal.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';
|
||||
*/
|
||||
//{{END}}
|
||||
import { MultiLogger, Logger, ConsoleLogger, FileLogger } from './logger';
|
||||
import { setup as setupLaunchTests } from './areas/workbench/launch.test';*///{{END}}
|
||||
|
||||
if (!/^v10/.test(process.version)) {
|
||||
console.error('Error: Smoketest must be run using Node 10. Currently running', process.version);
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/code';
|
||||
import { waitForNewDialog, clickDialogButton } from '../sqlutils';
|
||||
import { TestServerProfile, AuthenticationType } from '../testConfig';
|
||||
|
||||
const CONNECTION_DIALOG_TITLE = 'Connection';
|
||||
const CONNECTION_DIALOG_SELECTOR: string = '.modal-dialog .modal-content .modal-body .connection-dialog';
|
||||
const CONNECTION_DETAIL_CONTROL_SELECTOR: string = '.connection-provider-info .connection-table .connection-input';
|
||||
|
||||
const SERVER_INPUT_ARIA_LABEL = 'Server';
|
||||
const USERNAME_INPUT_ARIA_LABEL = 'User name';
|
||||
const PASSWORD_INPUT_ARIA_LABEL = 'Password';
|
||||
const AUTH_TYPE_ARIA_LABEL = 'Authentication type';
|
||||
|
||||
const CONNECT_BUTTON_ARIA_LABEL = 'Connect';
|
||||
|
||||
export class ConnectionDialog {
|
||||
|
||||
constructor(private code: Code) { }
|
||||
|
||||
async waitForConnectionDialog(): Promise<void> {
|
||||
await waitForNewDialog(this.code, CONNECTION_DIALOG_TITLE);
|
||||
}
|
||||
|
||||
async connect(profile: TestServerProfile): Promise<void> {
|
||||
await this.code.waitForSetValue(this.getInputCssSelector(SERVER_INPUT_ARIA_LABEL), profile.serverName);
|
||||
if (profile.authenticationType === AuthenticationType.SqlLogin) {
|
||||
await this.code.waitAndClick(this.getSelectCssSelector(AUTH_TYPE_ARIA_LABEL));
|
||||
await this.selectAuthType(profile.authenticationTypeDisplayName);
|
||||
await this.code.waitForSetValue(this.getInputCssSelector(USERNAME_INPUT_ARIA_LABEL), profile.userName);
|
||||
await this.code.waitForSetValue(this.getInputCssSelector(PASSWORD_INPUT_ARIA_LABEL), profile.password);
|
||||
}
|
||||
await clickDialogButton(this.code, CONNECT_BUTTON_ARIA_LABEL);
|
||||
}
|
||||
|
||||
private getInputCssSelector(ariaLabel: string): string {
|
||||
return `${CONNECTION_DIALOG_SELECTOR} ${CONNECTION_DETAIL_CONTROL_SELECTOR} .monaco-inputbox input[aria-label="${ariaLabel}"]`;
|
||||
}
|
||||
|
||||
private getSelectCssSelector(ariaLabel: string): string {
|
||||
return `${CONNECTION_DIALOG_SELECTOR} ${CONNECTION_DETAIL_CONTROL_SELECTOR} select[aria-label="${ariaLabel}"]`;
|
||||
}
|
||||
|
||||
private async selectAuthType(authType: string) {
|
||||
await this.code.waitAndClick(`.context-view.bottom.left .monaco-select-box-dropdown-container .select-box-dropdown-list-container .monaco-list .monaco-scrollable-element .monaco-list-rows .monaco-list-row div[aria-label="${authType}"][class*="option-text"]`);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application } from '../../application';
|
||||
import { getStandaloneServer } from '../testConfig';
|
||||
import { Application, getStandaloneServer } from '../../../../automation';
|
||||
|
||||
export function setup() {
|
||||
describe('profiler test suite', () => {
|
||||
@@ -16,4 +15,4 @@ export function setup() {
|
||||
await app.workbench.profiler.waitForNewSessionDialogAndStart();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../../vscode/code';
|
||||
import { QuickOpen } from '../../areas/quickopen/quickopen';
|
||||
import { waitForNewDialog, clickDialogButton } from '../sqlutils';
|
||||
|
||||
const NEW_SESSION_DIALOG_TITLE: string = 'Start New Profiler Session';
|
||||
|
||||
export class Profiler {
|
||||
|
||||
constructor(private code: Code, private quickopen: QuickOpen) { }
|
||||
|
||||
async launchProfiler(): Promise<void> {
|
||||
await this.quickopen.runCommand('Profiler: Launch Profiler');
|
||||
}
|
||||
|
||||
async waitForNewSessionDialog() {
|
||||
await waitForNewDialog(this.code, NEW_SESSION_DIALOG_TITLE);
|
||||
}
|
||||
|
||||
async waitForNewSessionDialogAndStart() {
|
||||
await this.waitForNewSessionDialog();
|
||||
await clickDialogButton(this.code, 'Start');
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Application } from '../../application';
|
||||
import { Application } from '../../../../automation';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
@@ -25,4 +25,4 @@ export function setup() {
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Editors } from '../../areas/editor/editors';
|
||||
import { QuickOpen } from '../../areas/quickopen/quickopen';
|
||||
import { Code } from '../../vscode/code';
|
||||
import * as path from 'path';
|
||||
|
||||
export class QueryEditors extends Editors {
|
||||
|
||||
constructor(
|
||||
private vsCode: Code,
|
||||
private quickopen: QuickOpen
|
||||
) {
|
||||
super(vsCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the specified file - this correctly handles SQL files which are opened in a Query Editor window
|
||||
* @param filePath The full path of the file to open.
|
||||
*/
|
||||
async openFile(filePath: string): Promise<void> {
|
||||
await this.quickopen.openQuickOpen(filePath);
|
||||
|
||||
const fileBaseName = path.basename(filePath);
|
||||
await this.quickopen.waitForQuickOpenElements(names => names[0] === fileBaseName);
|
||||
await this.vsCode.dispatchKeybinding('enter');
|
||||
await this.waitForEditorFocus(fileBaseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for an active SQL Query Editor tab for the specified file. This is a modification of the editors.waitForActiveTab that
|
||||
* takes into account the connected status displayed in the title of Query Editors.
|
||||
* @param fileName The name of the file opened in the editor
|
||||
* @param isDirty Whether the file is dirty or not
|
||||
*/
|
||||
async waitForActiveTab(fileName: string, isDirty: boolean = false): Promise<void> {
|
||||
// For now assume all opened tabs are disconnected until we have a need to open connected tabs
|
||||
await this.vsCode.waitForElement(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][aria-label="${fileName} - disconnected, tab"]`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Waits for an active Query Editor for the specified file to have focus. This is a modification of the editors.waitForEditorFocus
|
||||
* that takes into account the connected status displayed in the title of Query Editors.
|
||||
* @param fileName The name of the file opened in the editor
|
||||
*/
|
||||
async waitForEditorFocus(fileName: string): Promise<void> {
|
||||
await this.waitForActiveTab(fileName);
|
||||
await super.waitForActiveEditor(fileName);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Code } from '../vscode/code';
|
||||
|
||||
export async function waitForNewDialog(code: Code, title: string) {
|
||||
await code.waitForElement(`div[aria-label="${title}"][class="modal fade flyout-dialog"]`);
|
||||
}
|
||||
|
||||
export async function clickDialogButton(code: Code, title: string) {
|
||||
await code.waitAndClick(`.modal-dialog .modal-content .modal-footer .right-footer .footer-button a[aria-label="${title}"][aria-disabled="false"]`);
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
TODO: Due to a runtime error, I duplicated this file at these 2 locations:
|
||||
$/extensions/integration-test/src/testConfig.ts
|
||||
$/test/smoke/src/sql/testConfig.ts
|
||||
for now, make sure to keep both files in sync.
|
||||
*/
|
||||
|
||||
interface ITestServerProfile {
|
||||
serverName: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
authenticationType: AuthenticationType;
|
||||
database: string;
|
||||
provider: ConnectionProvider;
|
||||
version: string;
|
||||
engineType: EngineType;
|
||||
}
|
||||
|
||||
interface INameDisplayNamePair {
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export enum AuthenticationType {
|
||||
Windows,
|
||||
SqlLogin
|
||||
}
|
||||
|
||||
export enum ConnectionProvider {
|
||||
SQLServer
|
||||
}
|
||||
|
||||
export enum EngineType {
|
||||
Standalone,
|
||||
Azure,
|
||||
BigDataCluster
|
||||
}
|
||||
|
||||
let connectionProviderMapping = {};
|
||||
let authenticationTypeMapping = {};
|
||||
connectionProviderMapping[ConnectionProvider.SQLServer] = { name: 'MSSQL', displayName: 'Microsoft SQL Server' };
|
||||
|
||||
authenticationTypeMapping[AuthenticationType.SqlLogin] = { name: 'SqlLogin', displayName: 'SQL Login' };
|
||||
authenticationTypeMapping[AuthenticationType.Windows] = { name: 'Integrated', displayName: 'Windows Authentication' };
|
||||
|
||||
export function getConfigValue(name: string): string {
|
||||
let configValue = process.env[name];
|
||||
return configValue ? configValue.toString() : '';
|
||||
}
|
||||
|
||||
export const EnvironmentVariable_BDC_SERVER: string = 'BDC_BACKEND_HOSTNAME';
|
||||
export const EnvironmentVariable_BDC_USERNAME: string = 'BDC_BACKEND_USERNAME';
|
||||
export const EnvironmentVariable_BDC_PASSWORD: string = 'BDC_BACKEND_PWD';
|
||||
export const EnvironmentVariable_STANDALONE_SERVER: string = 'STANDALONE_SQL';
|
||||
export const EnvironmentVariable_STANDALONE_USERNAME: string = 'STANDALONE_SQL_USERNAME';
|
||||
export const EnvironmentVariable_STANDALONE_PASSWORD: string = 'STANDALONE_SQL_PWD';
|
||||
export const EnvironmentVariable_AZURE_SERVER: string = 'AZURE_SQL';
|
||||
export const EnvironmentVariable_AZURE_USERNAME: string = 'AZURE_SQL_USERNAME';
|
||||
export const EnvironmentVariable_AZURE_PASSWORD: string = 'AZURE_SQL_PWD';
|
||||
export const EnvironmentVariable_PYTHON_PATH: string = 'PYTHON_TEST_PATH';
|
||||
|
||||
export class TestServerProfile {
|
||||
constructor(private _profile: ITestServerProfile) { }
|
||||
public get serverName(): string { return this._profile.serverName; }
|
||||
public get userName(): string { return this._profile.userName; }
|
||||
public get password(): string { return this._profile.password; }
|
||||
public get database(): string { return this._profile.database; }
|
||||
public get version(): string { return this._profile.version; }
|
||||
public get provider(): ConnectionProvider { return this._profile.provider; }
|
||||
public get providerName(): string { return getEnumMappingEntry(connectionProviderMapping, this.provider).name; }
|
||||
public get providerDisplayName(): string { return getEnumMappingEntry(connectionProviderMapping, this.provider).displayName; }
|
||||
public get authenticationType(): AuthenticationType { return this._profile.authenticationType; }
|
||||
public get authenticationTypeName(): string { return getEnumMappingEntry(authenticationTypeMapping, this.authenticationType).name; }
|
||||
public get authenticationTypeDisplayName(): string { return getEnumMappingEntry(authenticationTypeMapping, this.authenticationType).displayName; }
|
||||
public get engineType(): EngineType { return this._profile.engineType; }
|
||||
}
|
||||
|
||||
let TestingServers: TestServerProfile[] = [
|
||||
new TestServerProfile(
|
||||
{
|
||||
serverName: getConfigValue(EnvironmentVariable_STANDALONE_SERVER),
|
||||
userName: getConfigValue(EnvironmentVariable_STANDALONE_USERNAME),
|
||||
password: getConfigValue(EnvironmentVariable_STANDALONE_PASSWORD),
|
||||
authenticationType: AuthenticationType.SqlLogin,
|
||||
database: 'master',
|
||||
provider: ConnectionProvider.SQLServer,
|
||||
version: '2017',
|
||||
engineType: EngineType.Standalone
|
||||
}),
|
||||
new TestServerProfile(
|
||||
{
|
||||
serverName: getConfigValue(EnvironmentVariable_AZURE_SERVER),
|
||||
userName: getConfigValue(EnvironmentVariable_AZURE_USERNAME),
|
||||
password: getConfigValue(EnvironmentVariable_AZURE_PASSWORD),
|
||||
authenticationType: AuthenticationType.SqlLogin,
|
||||
database: 'master',
|
||||
provider: ConnectionProvider.SQLServer,
|
||||
version: '2012',
|
||||
engineType: EngineType.Azure
|
||||
}),
|
||||
new TestServerProfile(
|
||||
{
|
||||
serverName: getConfigValue(EnvironmentVariable_BDC_SERVER),
|
||||
userName: getConfigValue(EnvironmentVariable_BDC_USERNAME),
|
||||
password: getConfigValue(EnvironmentVariable_BDC_PASSWORD),
|
||||
authenticationType: AuthenticationType.SqlLogin,
|
||||
database: 'master',
|
||||
provider: ConnectionProvider.SQLServer,
|
||||
version: '2019',
|
||||
engineType: EngineType.BigDataCluster
|
||||
})
|
||||
];
|
||||
|
||||
function getEnumMappingEntry(mapping: any, enumValue: any): INameDisplayNamePair {
|
||||
let entry = mapping[enumValue];
|
||||
if (entry) {
|
||||
return entry;
|
||||
} else {
|
||||
throw new Error(`Unknown enum type: ${enumValue.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAzureServer(): Promise<TestServerProfile> {
|
||||
let servers = await getTestingServers();
|
||||
return servers.filter(s => s.engineType === EngineType.Azure)[0];
|
||||
}
|
||||
|
||||
export async function getStandaloneServer(): Promise<TestServerProfile> {
|
||||
let servers = await getTestingServers();
|
||||
return servers.filter(s => s.version === '2017' && s.engineType === EngineType.Standalone)[0];
|
||||
}
|
||||
|
||||
export async function getBdcServer(): Promise<TestServerProfile> {
|
||||
let servers = await getTestingServers();
|
||||
return servers.filter(s => s.version === '2019' && s.engineType === EngineType.BigDataCluster)[0];
|
||||
}
|
||||
|
||||
export async function getTestingServers(): Promise<TestServerProfile[]> {
|
||||
let promise = new Promise<TestServerProfile[]>(resolve => {
|
||||
resolve(TestingServers);
|
||||
});
|
||||
await promise;
|
||||
return promise;
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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 connectPuppeteerDriver, launch } from './puppeteerDriver';
|
||||
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;
|
||||
/** Run in headless mode (only applies when web is true) */
|
||||
headless?: boolean;
|
||||
}
|
||||
|
||||
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 codePath = options.codePath;
|
||||
const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath();
|
||||
const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath();
|
||||
const handle = await createDriverHandle();
|
||||
|
||||
const args = [
|
||||
options.workspacePath,
|
||||
'--skip-getting-started',
|
||||
'--skip-release-notes',
|
||||
'--sticky-quickopen',
|
||||
'--disable-telemetry',
|
||||
'--disable-updates',
|
||||
'--disable-crash-reporter',
|
||||
`--extensions-dir=${options.extensionsPath}`,
|
||||
`--user-data-dir=${options.userDataDir}`,
|
||||
'--driver', handle
|
||||
];
|
||||
|
||||
const env = process.env;
|
||||
|
||||
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
|
||||
const testResolverExtPath = path.join(options.extensionsPath, 'vscode-test-resolver');
|
||||
if (!fs.existsSync(testResolverExtPath)) {
|
||||
const orig = path.join(repoPath, 'extensions', 'vscode-test-resolver');
|
||||
await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c()));
|
||||
}
|
||||
}
|
||||
args.push('--enable-proposed-api=vscode.vscode-test-resolver');
|
||||
const remoteDataDir = `${options.userDataDir}-server`;
|
||||
mkdirp.sync(remoteDataDir);
|
||||
env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let child: cp.ChildProcess | undefined;
|
||||
let connectDriver: typeof connectElectronDriver;
|
||||
|
||||
if (options.web) {
|
||||
await launch(args);
|
||||
connectDriver = connectPuppeteerDriver.bind(connectPuppeteerDriver, !!options.headless);
|
||||
} else {
|
||||
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 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');
|
||||
}
|
||||
|
||||
if (typeof target[prop] !== 'function') {
|
||||
return target[prop];
|
||||
}
|
||||
|
||||
return function (...args) {
|
||||
logger.log(`${prop}`, ...args.filter(a => typeof a === 'string'));
|
||||
return target[prop].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> {
|
||||
// {{SQL CARBON EDIT}}
|
||||
await poll(() => this.driver.getWindowIds(), fn, `get window ids`, 600, 100);
|
||||
// {{END}}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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));
|
||||
};
|
||||
@@ -1,192 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { mkdir } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const width = 1200;
|
||||
const height = 800;
|
||||
|
||||
const vscodeToPuppeteerKey = {
|
||||
cmd: 'Meta',
|
||||
ctrl: 'Control',
|
||||
shift: 'Shift',
|
||||
enter: 'Enter',
|
||||
escape: 'Escape',
|
||||
right: 'ArrowRight',
|
||||
up: 'ArrowUp',
|
||||
down: 'ArrowDown',
|
||||
left: 'ArrowLeft',
|
||||
home: 'Home'
|
||||
};
|
||||
|
||||
function buildDriver(browser: puppeteer.Browser, page: puppeteer.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 vscodeToPuppeteerKey) {
|
||||
keys[i] = vscodeToPuppeteerKey[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}')`),
|
||||
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));
|
||||
}
|
||||
|
||||
// function runInDriver(call: string, args: (string | boolean)[]): Promise<any> {}
|
||||
|
||||
let args: string[] | undefined;
|
||||
let server: ChildProcess | undefined;
|
||||
let endpoint: string | undefined;
|
||||
|
||||
export async function launch(_args: string[]): Promise<void> {
|
||||
args = _args;
|
||||
const webUserDataDir = args.filter(e => e.includes('--user-data-dir='))[0].replace('--user-data-dir=', '');
|
||||
await promisify(mkdir)(webUserDataDir);
|
||||
server = spawn(join(args[0], `resources/server/web.${process.platform === 'win32' ? 'bat' : 'sh'}`), ['--browser', 'none', '--driver', 'web', '--web-user-data-dir', webUserDataDir]);
|
||||
server.stderr.on('data', e => console.log('Server stderr: ' + e));
|
||||
server.stdout.on('data', e => console.log('Server stdout: ' + e));
|
||||
process.on('exit', teardown);
|
||||
process.on('SIGINT', teardown);
|
||||
process.on('SIGTERM', teardown);
|
||||
endpoint = await waitForEndpoint();
|
||||
}
|
||||
|
||||
function teardown(): void {
|
||||
if (server) {
|
||||
server.kill();
|
||||
server = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function waitForEndpoint(): Promise<string> {
|
||||
return new Promise<string>(r => {
|
||||
server!.stdout.on('data', d => {
|
||||
const matches = d.toString('ascii').match(/Web UI available at (.+)/);
|
||||
if (matches !== null) {
|
||||
r(matches[1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function connect(headless: boolean, outPath: string, handle: string): Promise<{ client: IDisposable, driver: IDriver }> {
|
||||
return new Promise(async (c) => {
|
||||
const browser = await puppeteer.launch({
|
||||
// Run in Edge dev on macOS
|
||||
// executablePath: '/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev',
|
||||
headless,
|
||||
slowMo: 80,
|
||||
args: [`--window-size=${width},${height}`]
|
||||
});
|
||||
const page = (await browser.pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
await page.goto(`${endpoint}&folder=${args![1]}`);
|
||||
const result = {
|
||||
client: { dispose: () => teardown },
|
||||
driver: buildDriver(browser, page)
|
||||
};
|
||||
c(result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export 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>;
|
||||
}
|
||||
|
||||
export interface IElement {
|
||||
tagName: string;
|
||||
className: string;
|
||||
textContent: string;
|
||||
attributes: { [name: string]: string; };
|
||||
children: IElement[];
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
export interface IDriver {
|
||||
_serviceBrand: any;
|
||||
|
||||
getWindowIds(): Promise<number[]>;
|
||||
capturePage(windowId: number): Promise<string>;
|
||||
reloadWindow(windowId: number): Promise<void>;
|
||||
exitApplication(): Promise<void>;
|
||||
dispatchKeybinding(windowId: number, keybinding: string): Promise<void>;
|
||||
click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>;
|
||||
doubleClick(windowId: number, selector: string): Promise<void>;
|
||||
setValue(windowId: number, selector: string, text: string): Promise<void>;
|
||||
getTitle(windowId: number): Promise<string>;
|
||||
isActiveElement(windowId: number, selector: string): Promise<boolean>;
|
||||
getElements(windowId: number, selector: string, recursive?: boolean): Promise<IElement[]>;
|
||||
getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }>;
|
||||
typeInEditor(windowId: number, selector: string, text: string): Promise<void>;
|
||||
getTerminalBuffer(windowId: number, selector: string): Promise<string[]>;
|
||||
writeInTerminal(windowId: number, selector: string, text: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user