aspnetcore/clients/ts/webdriver-tap-runner/lib.ts

202 lines
6.2 KiB
TypeScript

import * as http from "http";
import * as path from "path";
import { promisify } from "util";
import { ChildProcess, spawn, SpawnOptions } from "child_process";
import * as chromedriver from "chromedriver";
import { EOL } from "os";
import { Builder, logging, WebDriver, WebElement } from "selenium-webdriver";
import { Options as ChromeOptions } from "selenium-webdriver/chrome";
import { Readable, Writable } from "stream";
import { delay, flushEntries, getEntryContent, getLogEntry, isComplete, waitForElement } from "./utils";
import * as _debug from "debug";
const debug = _debug("webdriver-tap-runner:bin");
export interface RunnerOptions {
browser: string;
url: string;
chromeBinaryPath?: string,
chromeDriverLogFile?: string;
chromeVerboseLogging?: boolean;
output?: Writable;
webdriverPort: number;
}
function applyBrowserSettings(options: RunnerOptions, builder: Builder) {
if (options.browser === "chrome") {
const chromeOptions = new ChromeOptions();
chromeOptions.headless();
// If we're root, we need to disable the sandbox.
if (process.getuid && process.getuid() === 0) {
chromeOptions.addArguments("--no-sandbox");
}
if (options.chromeBinaryPath) {
debug(`Using Chrome Binary Path: ${options.chromeBinaryPath}`);
chromeOptions.setChromeBinaryPath(options.chromeBinaryPath);
}
builder.setChromeOptions(chromeOptions);
}
}
function writeToDebug(name: string) {
const writer = _debug(name);
let lastLine: string;
return (chunk: Buffer | string) => {
const str = chunk.toString();
const lines = str.split(/\r?\n/g);
const lastLineComplete = str.endsWith("\n");
if (lines.length > 0 && lastLine) {
lines[0] = lastLine + lines[0];
}
const end = lastLineComplete ? lines.length : (lines.length - 1)
for (let i = 0; i < end; i += 1) {
writer(lines[i]);
}
if (lastLineComplete && lines.length > 0) {
lastLine = lines[lines.length - 1];
}
}
}
let driverInstance: ChildProcess;
function startDriver(browser: string, port: number) {
let processName: string;
if (browser === "chrome") {
processName = path.basename(chromedriver.path);
driverInstance = spawn(chromedriver.path, [`--port=${port}`]);
} else {
throw new Error(`Unsupported browser: ${browser}`);
}
// Capture output
driverInstance.stdout.on("data", writeToDebug(`webdriver-tap-runner:${processName}:stdout`));
driverInstance.stderr.on("data", writeToDebug(`webdriver-tap-runner:${processName}:stderr`));
}
function stopDriver(browser: string) {
if (driverInstance && !driverInstance.killed) {
debug("Killing WebDriver...");
driverInstance.kill();
debug("Killed WebDriver");
}
}
function pingUrl(url: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const request = http.request(url);
request.on("response", (resp: http.IncomingMessage) => {
if (resp.statusCode >= 400) {
reject(new Error(`Received ${resp.statusCode} ${resp.statusMessage} from server`));
} else {
resolve();
}
});
request.on("error", (error) => reject(error));
request.end();
});
}
async function pingWithRetry(url: string): Promise<boolean> {
for (let i = 0; i < 5; i += 1) {
try {
debug(`Pinging URL: ${url}`);
await pingUrl(url);
return true;
} catch (e) {
debug(`Error reaching server: '${e}', retrying...`);
await delay(100);
}
}
debug("Retry limit exhausted");
return false;
}
export async function run(runName: string, options: RunnerOptions): Promise<number> {
const output = options.output || (process.stdout as Writable);
debug(`Using WebDriver port: ${options.webdriverPort}`);
startDriver(options.browser, options.webdriverPort);
// Wait for the server to start
const serverUrl = `http://localhost:${options.webdriverPort}`;
if (!await pingWithRetry(`${serverUrl}/status`)) {
console.log("WebDriver did not start in time.");
process.exit(1);
}
try {
// Shut selenium down when we shut down.
process.on("exit", () => {
stopDriver(options.browser);
});
// Build WebDriver
const builder = new Builder()
.usingServer(serverUrl);
// Set the browser
debug(`Using '${options.browser}' browser`);
builder.forBrowser(options.browser);
applyBrowserSettings(options, builder);
// Build driver
const driver = builder.build();
let failureCount = 0;
try {
// Navigate to the URL
debug(`Navigating to ${options.url}`);
await driver.get(options.url);
// Wait for the TAP results list
const listElement = await waitForElement(driver, "__tap_list");
output.write(`TAP version 13${EOL}`);
output.write(`# ${runName}${EOL}`);
// Process messages until the test run is complete
let index = 0;
while (!await isComplete(listElement)) {
const entry = await getLogEntry(index, listElement);
if (entry) {
index += 1;
const content = await getEntryContent(entry);
if (content.startsWith("not ok")) {
failureCount += 1;
}
output.write(content + EOL);
}
}
// Flush any remaining messages
await flushEntries(index, listElement, (entry) => {
if (entry.startsWith("not ok")) {
failureCount += 1;
}
output.write(entry + EOL);
});
} finally {
// Shut down
debug("Shutting WebDriver down...");
await driver.quit();
}
// We're done!
debug("Test run complete");
return failureCount;
} finally {
debug("Shutting Selenium server down...");
stopDriver(options.browser);
}
}