mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 718331d6f3ebd1b571530ab499edb266ddd493d5
This commit is contained in:
@@ -133,6 +133,7 @@ export class Application {
|
||||
extraArgs,
|
||||
remote: this.options.remote,
|
||||
web: this.options.web,
|
||||
browser: this.options.browser,
|
||||
headless: this.options.headless
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ 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 { connect as connectPlaywrightDriver, launch } from './playwrightDriver';
|
||||
import { Logger } from './logger';
|
||||
import { ncp } from 'ncp';
|
||||
import { URI } from 'vscode-uri';
|
||||
@@ -101,6 +101,8 @@ export interface SpawnOptions {
|
||||
remote?: boolean;
|
||||
/** Run in the web */
|
||||
web?: boolean;
|
||||
/** A specific browser to use (requires web: true) */
|
||||
browser?: 'chromium' | 'webkit' | 'firefox';
|
||||
/** Run in headless mode (only applies when web is true) */
|
||||
headless?: boolean;
|
||||
}
|
||||
@@ -120,68 +122,69 @@ export async function spawn(options: SpawnOptions): Promise<Code> {
|
||||
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);
|
||||
await launch(options.userDataDir, options.workspacePath, options.codePath);
|
||||
connectDriver = connectPlaywrightDriver.bind(connectPlaywrightDriver, !!options.headless, options.browser);
|
||||
} else {
|
||||
const env = process.env;
|
||||
|
||||
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
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class Editor {
|
||||
const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`;
|
||||
const textarea = `${editor} textarea`;
|
||||
|
||||
await this.code.waitAndClick(line, 0, 0);
|
||||
await this.code.waitAndClick(line, 1, 1);
|
||||
await this.code.waitForActiveElement(textarea);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,27 +17,27 @@ export class Editors {
|
||||
}
|
||||
}
|
||||
|
||||
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 selectTab(fileName: string): Promise<void> {
|
||||
await this.code.waitAndClick(`.tabs-container div.tab[data-resource-name$="${fileName}"]`);
|
||||
await this.waitForEditorFocus(fileName);
|
||||
}
|
||||
|
||||
async waitForActiveEditor(filename: string): Promise<any> {
|
||||
const selector = `.editor-instance .monaco-editor[data-uri$="${filename}"] textarea`;
|
||||
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> {
|
||||
async waitForEditorFocus(fileName: string): Promise<void> {
|
||||
await this.waitForActiveTab(fileName);
|
||||
await this.waitForActiveEditor(fileName);
|
||||
}
|
||||
|
||||
async waitForActiveTab(fileName: string, isDirty: boolean = false): Promise<void> {
|
||||
await this.code.waitForElement(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][aria-label="${fileName}, tab"]`);
|
||||
await this.code.waitForElement(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][data-resource-name$="${fileName}"]`);
|
||||
}
|
||||
|
||||
async waitForTab(fileName: string, isDirty: boolean = false): Promise<void> {
|
||||
await this.code.waitForElement(`.tabs-container div.tab${isDirty ? '.dirty' : ''}[aria-label="${fileName}, tab"]`);
|
||||
await this.code.waitForElement(`.tabs-container div.tab${isDirty ? '.dirty' : ''}[data-resource-name$="${fileName}"]`);
|
||||
}
|
||||
|
||||
async newUntitledFile(): Promise<void> {
|
||||
@@ -47,6 +47,6 @@ export class Editors {
|
||||
await this.code.dispatchKeybinding('ctrl+n');
|
||||
}
|
||||
|
||||
await this.waitForEditorFocus('Untitled-1', true);
|
||||
await this.waitForEditorFocus('Untitled-1');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import * as playwright from 'playwright';
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { mkdir } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { IDriver, IDisposable } from './driver';
|
||||
import { URI } from 'vscode-uri';
|
||||
|
||||
const width = 1200;
|
||||
const height = 800;
|
||||
|
||||
const vscodeToPuppeteerKey: { [key: string]: string } = {
|
||||
const vscodeToPlaywrightKey: { [key: string]: string } = {
|
||||
cmd: 'Meta',
|
||||
ctrl: 'Control',
|
||||
shift: 'Shift',
|
||||
@@ -26,7 +27,7 @@ const vscodeToPuppeteerKey: { [key: string]: string } = {
|
||||
home: 'Home'
|
||||
};
|
||||
|
||||
function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver {
|
||||
function buildDriver(browser: playwright.Browser, page: playwright.Page): IDriver {
|
||||
const driver: IDriver = {
|
||||
_serviceBrand: undefined,
|
||||
getWindowIds: () => {
|
||||
@@ -45,8 +46,8 @@ function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver
|
||||
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]];
|
||||
if (keys[i] in vscodeToPlaywrightKey) {
|
||||
keys[i] = vscodeToPlaywrightKey[keys[i]];
|
||||
}
|
||||
await page.keyboard.down(keys[i]);
|
||||
keysDown.push(keys[i]);
|
||||
@@ -68,7 +69,7 @@ function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver
|
||||
await driver.click(windowId, selector, 0, 0);
|
||||
await timeout(100);
|
||||
},
|
||||
setValue: async (windowId, selector, text) => page.evaluate(`window.driver.setValue('${selector}', '${text}')`),
|
||||
setValue: async (windowId, selector, text) => page.evaluate(`window.driver.setValue('${selector}', '${text}')`).then(undefined),
|
||||
getTitle: (windowId) => page.evaluate(`window.driver.getTitle()`),
|
||||
isActiveElement: (windowId, selector) => page.evaluate(`window.driver.isActiveElement('${selector}')`),
|
||||
getElements: (windowId, selector, recursive) => page.evaluate(`window.driver.getElements('${selector}', ${recursive})`),
|
||||
@@ -86,25 +87,32 @@ function timeout(ms: number): Promise<void> {
|
||||
|
||||
// function runInDriver(call: string, args: (string | boolean)[]): Promise<any> {}
|
||||
|
||||
let args: string[] | undefined;
|
||||
let server: ChildProcess | undefined;
|
||||
let endpoint: string | undefined;
|
||||
let workspacePath: string | undefined;
|
||||
|
||||
export async function launch(_args: string[]): Promise<void> {
|
||||
args = _args;
|
||||
const agentFolder = args.filter(e => e.includes('--user-data-dir='))[0].replace('--user-data-dir=', '');
|
||||
export async function launch(userDataDir: string, _workspacePath: string, codeServerPath = process.env.VSCODE_REMOTE_SERVER_PATH): Promise<void> {
|
||||
workspacePath = _workspacePath;
|
||||
const agentFolder = userDataDir;
|
||||
await promisify(mkdir)(agentFolder);
|
||||
const env = {
|
||||
VSCODE_AGENT_FOLDER: agentFolder,
|
||||
VSCODE_REMOTE_SERVER_PATH: codeServerPath,
|
||||
...process.env
|
||||
};
|
||||
let serverLocation: string | undefined;
|
||||
if (codeServerPath) {
|
||||
serverLocation = join(codeServerPath, `server.${process.platform === 'win32' ? 'cmd' : 'sh'}`);
|
||||
} else {
|
||||
serverLocation = join(__dirname, '..', '..', '..', `resources/server/web.${process.platform === 'win32' ? 'bat' : 'sh'}`);
|
||||
}
|
||||
server = spawn(
|
||||
join(args[0], `resources/server/web.${process.platform === 'win32' ? 'bat' : 'sh'}`),
|
||||
serverLocation,
|
||||
['--browser', 'none', '--driver', 'web'],
|
||||
{ env }
|
||||
);
|
||||
server.stderr.on('data', e => console.log('Server stderr: ' + e));
|
||||
server.stdout.on('data', e => console.log('Server stdout: ' + e));
|
||||
server.stderr?.on('data', error => console.log(`Server stderr: ${error}`));
|
||||
server.stdout?.on('data', data => console.log(`Server stdout: ${data}`));
|
||||
process.on('exit', teardown);
|
||||
process.on('SIGINT', teardown);
|
||||
process.on('SIGTERM', teardown);
|
||||
@@ -120,7 +128,7 @@ function teardown(): void {
|
||||
|
||||
function waitForEndpoint(): Promise<string> {
|
||||
return new Promise<string>(r => {
|
||||
server!.stdout.on('data', (d: Buffer) => {
|
||||
server!.stdout?.on('data', (d: Buffer) => {
|
||||
const matches = d.toString('ascii').match(/Web UI available at (.+)/);
|
||||
if (matches !== null) {
|
||||
r(matches[1]);
|
||||
@@ -129,20 +137,18 @@ function waitForEndpoint(): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
export function connect(headless: boolean, outPath: string, handle: string): Promise<{ client: IDisposable, driver: IDriver }> {
|
||||
export function connect(headless: boolean, engine: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> {
|
||||
return new Promise(async (c) => {
|
||||
const browser = await puppeteer.launch({
|
||||
const browser = await playwright[engine].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}`]
|
||||
headless
|
||||
});
|
||||
const page = (await browser.pages())[0];
|
||||
const page = (await browser.defaultContext().pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
await page.goto(`${endpoint}&folder=${args![1]}`);
|
||||
await page.goto(`${endpoint}&folder=vscode-remote://localhost:9888${URI.file(workspacePath!).path}`);
|
||||
const result = {
|
||||
client: { dispose: () => teardown },
|
||||
client: { dispose: () => browser.close() && teardown() },
|
||||
driver: buildDriver(browser, page)
|
||||
};
|
||||
c(result);
|
||||
@@ -12,7 +12,7 @@ export const enum ProblemSeverity {
|
||||
|
||||
export class Problems {
|
||||
|
||||
static PROBLEMS_VIEW_SELECTOR = '.panel.markers-panel';
|
||||
static PROBLEMS_VIEW_SELECTOR = '.panel .markers-panel';
|
||||
|
||||
constructor(private code: Code) { }
|
||||
|
||||
|
||||
@@ -109,11 +109,12 @@ export class Search extends Viewlet {
|
||||
}
|
||||
|
||||
async waitForResultText(text: string): Promise<void> {
|
||||
await this.code.waitForTextContent(`${VIEWLET} .messages .message>p`, text);
|
||||
// The label can end with " - " depending on whether the search editor is enabled
|
||||
await this.code.waitForTextContent(`${VIEWLET} .messages .message>span`, undefined, result => result.startsWith(text));
|
||||
}
|
||||
|
||||
async waitForNoResultText(): Promise<void> {
|
||||
await this.code.waitForElement(`${VIEWLET} .messages[aria-hidden="true"] .message>p`);
|
||||
await this.code.waitForElement(`${VIEWLET} .messages[aria-hidden="true"] .message>span`);
|
||||
}
|
||||
|
||||
private async waitForInputFocus(selector: string): Promise<void> {
|
||||
|
||||
@@ -20,8 +20,6 @@ export const enum StatusBarElement {
|
||||
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) { }
|
||||
|
||||
@@ -44,23 +42,23 @@ export class StatusBar {
|
||||
private getSelector(element: StatusBarElement): string {
|
||||
switch (element) {
|
||||
case StatusBarElement.BRANCH_STATUS:
|
||||
return `${this.mainSelector} ${this.leftSelector} .codicon.codicon-git-branch`;
|
||||
return `.statusbar-item[id="status.scm"] .codicon.codicon-git-branch`;
|
||||
case StatusBarElement.SYNC_STATUS:
|
||||
return `${this.mainSelector} ${this.leftSelector} .codicon.codicon-sync`;
|
||||
return `.statusbar-item[id="status.scm"] .codicon.codicon-sync`;
|
||||
case StatusBarElement.PROBLEMS_STATUS:
|
||||
return `${this.mainSelector} ${this.leftSelector} .codicon.codicon-error`;
|
||||
return `.statusbar-item[id="status.problems"]`;
|
||||
case StatusBarElement.SELECTION_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Go to Line"]`;
|
||||
return `.statusbar-item[id="status.editor.selection"]`;
|
||||
case StatusBarElement.INDENTATION_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Select Indentation"]`;
|
||||
return `.statusbar-item[id="status.editor.indentation"]`;
|
||||
case StatusBarElement.ENCODING_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Select Encoding"]`;
|
||||
return `.statusbar-item[id="status.editor.encoding"]`;
|
||||
case StatusBarElement.EOL_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Select End of Line Sequence"]`;
|
||||
return `.statusbar-item[id="status.editor.eol"]`;
|
||||
case StatusBarElement.LANGUAGE_STATUS:
|
||||
return `${this.mainSelector} ${this.rightSelector}[title="Select Language Mode"]`;
|
||||
return `.statusbar-item[id="status.editor.mode"]`;
|
||||
case StatusBarElement.FEEDBACK_ICON:
|
||||
return `${this.mainSelector} .statusbar-item.right[id="status.feedback"]`;
|
||||
return `.statusbar-item[id="status.feedback"]`;
|
||||
default:
|
||||
throw new Error(element);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user