diff options
Diffstat (limited to 'test/automation/src/code.ts')
-rw-r--r-- | test/automation/src/code.ts | 229 |
1 files changed, 91 insertions, 138 deletions
diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 33eba97ba52..5e6b52c7d73 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -6,31 +6,31 @@ import { join } from 'path'; import * as os from 'os'; import * as cp from 'child_process'; -import { IDriver, IDisposable, IElement, Thenable, ILocalizedStrings, ILocaleInfo } from './driver'; -import { launch as launchElectron } from './electronDriver'; -import { launch as launchPlaywrightBrowser } from './playwrightBrowserDriver'; -import { launch as launchPlaywrightElectron } from './playwrightElectronDriver'; +import { IElement, ILocalizedStrings, ILocaleInfo } from './driver'; +import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; +import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { Logger, measureAndLog } from './logger'; import { copyExtension } from './extensions'; import * as treekill from 'tree-kill'; +import { teardown } from './processes'; +import { PlaywrightDriver } from './playwrightDriver'; const rootPath = join(__dirname, '../../..'); export interface LaunchOptions { codePath?: string; - workspacePath: string; + readonly workspacePath: string; userDataDir: string; - extensionsPath: string; - logger: Logger; + readonly extensionsPath: string; + readonly logger: Logger; logsPath: string; - verbose?: boolean; - extraArgs?: string[]; - remote?: boolean; - web?: boolean; - legacy?: boolean; - tracing?: boolean; - headless?: boolean; - browser?: 'chromium' | 'webkit' | 'firefox'; + readonly verbose?: boolean; + readonly extraArgs?: string[]; + readonly remote?: boolean; + readonly web?: boolean; + readonly tracing?: boolean; + readonly headless?: boolean; + readonly browser?: 'chromium' | 'webkit' | 'firefox'; } interface ICodeInstance { @@ -39,8 +39,8 @@ interface ICodeInstance { const instances = new Set<ICodeInstance>(); -function registerInstance(process: cp.ChildProcess, logger: Logger, type: string, kill: () => Promise<void>) { - const instance = { kill }; +function registerInstance(process: cp.ChildProcess, logger: Logger, type: string) { + const instance = { kill: () => teardown(process, logger) }; instances.add(instance); process.stdout?.on('data', data => logger.log(`[${type}] stdout: ${data}`)); @@ -53,7 +53,7 @@ function registerInstance(process: cp.ChildProcess, logger: Logger, type: string }); } -async function teardown(signal?: number) { +async function teardownAll(signal?: number) { stopped = true; for (const instance of instances) { @@ -66,9 +66,9 @@ async function teardown(signal?: number) { } let stopped = false; -process.on('exit', () => teardown()); -process.on('SIGINT', () => teardown(128 + 2)); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events -process.on('SIGTERM', () => teardown(128 + 15)); // same as above +process.on('exit', () => teardownAll()); +process.on('SIGINT', () => teardownAll(128 + 2)); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events +process.on('SIGTERM', () => teardownAll(128 + 15)); // same as above export async function launch(options: LaunchOptions): Promise<Code> { if (stopped) { @@ -79,73 +79,29 @@ export async function launch(options: LaunchOptions): Promise<Code> { // Browser smoke tests if (options.web) { - const { serverProcess, client, driver, kill } = await measureAndLog(launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); - registerInstance(serverProcess, options.logger, 'server', kill); + const { serverProcess, driver } = await measureAndLog(launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); + registerInstance(serverProcess, options.logger, 'server'); - return new Code(client, driver, options.logger); + return new Code(driver, options.logger, serverProcess); } // Electron smoke tests (playwright) - else if (!options.legacy) { - const { client, driver } = await measureAndLog(launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); - - return new Code(client, driver, options.logger); - } - - // Electron smoke tests (legacy driver) else { - const { electronProcess, client, driver, kill } = await measureAndLog(launchElectron(options), 'launch electron', options.logger); - registerInstance(electronProcess, options.logger, 'electron', kill); - - return new Code(client, driver, options.logger); - } -} - -async function poll<T>( - fn: () => Thenable<T>, - acceptFn: (result: T) => boolean, - logger: Logger, - timeoutMessage: string, - retryCount = 200, - retryInterval = 100 // millis -): Promise<T> { - let trial = 1; - let lastError: string = ''; - - while (true) { - if (trial > retryCount) { - logger.log('Timeout!'); - logger.log(lastError); - logger.log(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`); - throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`); - } - - let result; - try { - result = await fn(); - if (acceptFn(result)) { - return result; - } else { - lastError = 'Did not pass accept function'; - } - } catch (e: any) { - lastError = Array.isArray(e.stack) ? e.stack.join(os.EOL) : e.stack; - } + const { electronProcess, driver } = await measureAndLog(launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); + registerInstance(electronProcess, options.logger, 'electron'); - await new Promise(resolve => setTimeout(resolve, retryInterval)); - trial++; + return new Code(driver, options.logger, electronProcess); } } export class Code { - private _activeWindowId: number | undefined = undefined; - driver: IDriver; + readonly driver: PlaywrightDriver; constructor( - private client: IDisposable, - driver: IDriver, - readonly logger: Logger + driver: PlaywrightDriver, + readonly logger: Logger, + private readonly mainProcess: cp.ChildProcess ) { this.driver = new Proxy(driver, { get(target, prop) { @@ -166,42 +122,28 @@ export class Code { }); } - async capturePage(): Promise<string> { - const windowId = await this.getActiveWindowId(); - return await this.driver.capturePage(windowId); - } - async startTracing(name: string): Promise<void> { - const windowId = await this.getActiveWindowId(); - if (typeof this.driver.startTracing === 'function') { // added only in 1.64 - return await this.driver.startTracing(windowId, name); - } + return await this.driver.startTracing(name); } async stopTracing(name: string, persist: boolean): Promise<void> { - const windowId = await this.getActiveWindowId(); - if (typeof this.driver.stopTracing === 'function') { // added only in 1.64 - return await this.driver.stopTracing(windowId, name, persist); - } - } - - async waitForWindowIds(accept: (windowIds: number[]) => boolean): Promise<void> { - await poll(() => this.driver.getWindowIds(), accept, this.logger, `get window ids`); + return await this.driver.stopTracing(name, persist); } async dispatchKeybinding(keybinding: string): Promise<void> { - const windowId = await this.getActiveWindowId(); - await this.driver.dispatchKeybinding(windowId, keybinding); + await this.driver.dispatchKeybinding(keybinding); } async exit(): Promise<void> { - - // Start the exit flow via driver - const pid = await measureAndLog(this.driver.exitApplication(), 'driver.exitApplication()', this.logger); - return measureAndLog(new Promise<void>((resolve, reject) => { + const pid = this.mainProcess.pid!; + let done = false; + // Start the exit flow via driver + this.driver.exitApplication(); + + // Await the exit of the application (async () => { let retries = 0; while (!done) { @@ -212,7 +154,12 @@ export class Code { // no need to await since we're polling for the process to die anyways treekill(pid, err => { - this.logger.log('Failed to kill Electron process tree:', err?.message); + try { + process.kill(pid, 0); // throws an exception if the process doesn't exist anymore + this.logger.log('Failed to kill Electron process tree:', err?.message); + } catch (error) { + // Expected when process is gone + } }); } @@ -230,92 +177,98 @@ export class Code { } } })(); - }).finally(() => { - this.dispose(); }), 'Code#exit()', this.logger); } async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean, retryCount?: number): 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'))), + return await this.poll( + () => this.driver.getElements(selector).then(els => els.length > 0 ? Promise.resolve(els[0].textContent) : Promise.reject(new Error('Element not found for textContent'))), s => accept!(typeof s === 'string' ? s : ''), - this.logger, `get text content '${selector}'`, retryCount ); } async waitAndClick(selector: string, xoffset?: number, yoffset?: number, retryCount: number = 200): Promise<void> { - const windowId = await this.getActiveWindowId(); - await poll(() => this.driver.click(windowId, selector, xoffset, yoffset), () => true, this.logger, `click '${selector}'`, retryCount); + await this.poll(() => this.driver.click(selector, xoffset, yoffset), () => true, `click '${selector}'`, retryCount); } async waitForSetValue(selector: string, value: string): Promise<void> { - const windowId = await this.getActiveWindowId(); - await poll(() => this.driver.setValue(windowId, selector, value), () => true, this.logger, `set value '${selector}'`); + await this.poll(() => this.driver.setValue(selector, value), () => true, `set value '${selector}'`); } async waitForElements(selector: string, recursive: boolean, accept: (result: IElement[]) => boolean = result => result.length > 0): Promise<IElement[]> { - const windowId = await this.getActiveWindowId(); - return await poll(() => this.driver.getElements(windowId, selector, recursive), accept, this.logger, `get elements '${selector}'`); + return await this.poll(() => this.driver.getElements(selector, recursive), accept, `get elements '${selector}'`); } async waitForElement(selector: string, accept: (result: IElement | undefined) => boolean = result => !!result, retryCount: number = 200): Promise<IElement> { - const windowId = await this.getActiveWindowId(); - return await poll<IElement>(() => this.driver.getElements(windowId, selector).then(els => els[0]), accept, this.logger, `get element '${selector}'`, retryCount); + return await this.poll<IElement>(() => this.driver.getElements(selector).then(els => els[0]), accept, `get element '${selector}'`, retryCount); } async waitForActiveElement(selector: string, retryCount: number = 200): Promise<void> { - const windowId = await this.getActiveWindowId(); - await poll(() => this.driver.isActiveElement(windowId, selector), r => r, this.logger, `is active element '${selector}'`, retryCount); + await this.poll(() => this.driver.isActiveElement(selector), r => r, `is active element '${selector}'`, retryCount); } async waitForTitle(accept: (title: string) => boolean): Promise<void> { - const windowId = await this.getActiveWindowId(); - await poll(() => this.driver.getTitle(windowId), accept, this.logger, `get title`); + await this.poll(() => this.driver.getTitle(), accept, `get title`); } async waitForTypeInEditor(selector: string, text: string): Promise<void> { - const windowId = await this.getActiveWindowId(); - await poll(() => this.driver.typeInEditor(windowId, selector, text), () => true, this.logger, `type in editor '${selector}'`); + await this.poll(() => this.driver.typeInEditor(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, this.logger, `get terminal buffer '${selector}'`); + await this.poll(() => this.driver.getTerminalBuffer(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, this.logger, `writeInTerminal '${selector}'`); + await this.poll(() => this.driver.writeInTerminal(selector, value), () => true, `writeInTerminal '${selector}'`); } async getLocaleInfo(): Promise<ILocaleInfo> { - const windowId = await this.getActiveWindowId(); - return this.driver.getLocaleInfo(windowId); + return this.driver.getLocaleInfo(); } async getLocalizedStrings(): Promise<ILocalizedStrings> { - const windowId = await this.getActiveWindowId(); - return this.driver.getLocalizedStrings(windowId); + return this.driver.getLocalizedStrings(); } - private async getActiveWindowId(): Promise<number> { - if (typeof this._activeWindowId !== 'number') { - this.logger.log('getActiveWindowId(): begin'); - const windows = await this.driver.getWindowIds(); - this._activeWindowId = windows[0]; - this.logger.log(`getActiveWindowId(): end (windowId=${this._activeWindowId})`); - } + private async poll<T>( + fn: () => Promise<T>, + acceptFn: (result: T) => boolean, + timeoutMessage: string, + retryCount = 200, + retryInterval = 100 // millis + ): Promise<T> { + let trial = 1; + let lastError: string = ''; + + while (true) { + if (trial > retryCount) { + this.logger.log('Timeout!'); + this.logger.log(lastError); + this.logger.log(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`); + + throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`); + } - return this._activeWindowId; - } + let result; + try { + result = await fn(); + if (acceptFn(result)) { + return result; + } else { + lastError = 'Did not pass accept function'; + } + } catch (e: any) { + lastError = Array.isArray(e.stack) ? e.stack.join(os.EOL) : e.stack; + } - dispose(): void { - this.client.dispose(); + await new Promise(resolve => setTimeout(resolve, retryInterval)); + trial++; + } } } |