change to use Karma for Functional Tests (#2450)
This commit is contained in:
parent
cb8264321d
commit
c7af64332b
|
|
@ -19,7 +19,6 @@
|
|||
<Exec Command="npm install --no-optional" WorkingDirectory="$(RepositoryRoot)clients/ts/FunctionalTests" />
|
||||
<Exec Command="npm install --no-optional" WorkingDirectory="$(RepositoryRoot)clients/ts/signalr" />
|
||||
<Exec Command="npm install --no-optional" WorkingDirectory="$(RepositoryRoot)clients/ts/signalr-protocol-msgpack" />
|
||||
<Exec Command="npm install --no-optional" WorkingDirectory="$(RepositoryRoot)clients/ts/webdriver-tap-runner" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
@ -27,13 +26,13 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<Target Name="RunTSClientNodeTests">
|
||||
<Message Text="Running TypeScript client Node tests" Importance="high" />
|
||||
<Message Text="Running JavaScript client Node tests" Importance="high" />
|
||||
<Exec Command="npm test" WorkingDirectory="$(RepositoryRoot)clients/ts" IgnoreStandardErrorWarningFormat="true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="RunBrowserTests">
|
||||
<Message Text="Running TypeScript client Browser tests" Importance="high" />
|
||||
<Exec Command="npm run ci-test -- --configuration $(Configuration)" WorkingDirectory="$(RepositoryRoot)clients/ts/FunctionalTests" IgnoreStandardErrorWarningFormat="true" />
|
||||
<Message Text="Running JavaScript client Browser tests" Importance="high" />
|
||||
<Exec Command="npm run test:inner -- --no-color --configuration $(Configuration)" WorkingDirectory="$(RepositoryRoot)clients/ts/FunctionalTests" IgnoreStandardErrorWarningFormat="true" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
wwwroot/lib/
|
||||
wwwroot/lib/
|
||||
|
||||
# Sauce Connect proxy logs.
|
||||
sc-*.log
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<TypeScriptCompileBlocked>True</TypeScriptCompileBlocked>
|
||||
<TypeScriptToolsVersion>2.8</TypeScriptToolsVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="$(MicrosoftAspNetCoreCorsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -11,7 +12,19 @@ namespace FunctionalTests
|
|||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var host = new WebHostBuilder()
|
||||
string url = null;
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--url":
|
||||
i += 1;
|
||||
url = args[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var hostBuilder = new WebHostBuilder()
|
||||
.ConfigureLogging(factory =>
|
||||
{
|
||||
factory.AddConsole(options => options.IncludeScopes = true);
|
||||
|
|
@ -21,10 +34,15 @@ namespace FunctionalTests
|
|||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
.UseStartup<Startup>();
|
||||
|
||||
host.Run();
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
Console.WriteLine($"Forcing URL to: {url}");
|
||||
hostBuilder.UseUrls(url);
|
||||
}
|
||||
|
||||
hostBuilder.Build().Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ namespace FunctionalTests
|
|||
})
|
||||
.AddMessagePackProtocol();
|
||||
|
||||
services.AddCors();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
|
||||
|
|
@ -87,6 +89,15 @@ namespace FunctionalTests
|
|||
}
|
||||
|
||||
app.UseFileServer();
|
||||
|
||||
app.UseCors(policyBuilder =>
|
||||
{
|
||||
policyBuilder.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
});
|
||||
|
||||
app.UseConnections(routes =>
|
||||
{
|
||||
routes.MapConnectionHandler<EchoConnectionHandler>("/echo");
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,17 +6,29 @@
|
|||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@aspnet/signalr": "file:../signalr",
|
||||
"@aspnet/signalr-protocol-msgpack": "file:../signalr-protocol-msgpack"
|
||||
"@aspnet/signalr-protocol-msgpack": "file:../signalr-protocol-msgpack",
|
||||
"msgpack5": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "0.0.30",
|
||||
"@types/jasmine": "^2.8.6",
|
||||
"@types/karma": "^1.7.3",
|
||||
"@types/node": "^9.4.6",
|
||||
"debug": "^3.1.0",
|
||||
"es6-promise": "^4.2.2",
|
||||
"jasmine": "^3.1.0",
|
||||
"tap-parser": "^7.0.0",
|
||||
"tee": "^0.2.0",
|
||||
"jasmine-core": "^3.1.0",
|
||||
"karma": "^2.0.2",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-edge-launcher": "^0.4.2",
|
||||
"karma-firefox-launcher": "^1.1.0",
|
||||
"karma-ie-launcher": "^1.0.0",
|
||||
"karma-jasmine": "^1.1.2",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"karma-safari-launcher": "^1.0.0",
|
||||
"karma-sauce-launcher": "^1.2.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-summary-reporter": "^1.5.0",
|
||||
"ts-node": "^4.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -25,10 +37,14 @@
|
|||
"build:lint": "node ../node_modules/tslint/bin/tslint -c ../tslint.json -p ./tsconfig.json",
|
||||
"build:tsc": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json",
|
||||
"build:rollup": "node ../node_modules/rollup/bin/rollup -c",
|
||||
"pretest": "npm run build",
|
||||
"test": "dotnet build && npm run test-only",
|
||||
"test-only": "ts-node --project ./selenium/tsconfig-selenium.json ./selenium/run-tests.ts",
|
||||
"ci-test": "ts-node --project ./selenium/tsconfig-selenium.json ./selenium/run-ci-tests.ts"
|
||||
"build:parent": "cd .. && npm run build",
|
||||
"pretest": "npm run build:parent && npm run build && dotnet build",
|
||||
"test": "npm run test:local --",
|
||||
"test:inner": "npm run build && dotnet build && npm run test:local --",
|
||||
"test:local": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts",
|
||||
"test:all": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts --all-browsers",
|
||||
"test:sauce": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts --sauce",
|
||||
"sauce": "npm run pretest && npm run test:sauce"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
const path = require("path");
|
||||
|
||||
let defaultReporters = ["progress", "summary"];
|
||||
|
||||
/** Creates the Karma config function based on the provided options
|
||||
*
|
||||
* @param {object} config Configuration options to override on/add to the base config.
|
||||
*/
|
||||
function createKarmaConfig(config) {
|
||||
return function (karmaConfig) {
|
||||
karmaConfig.set({
|
||||
basePath: path.resolve(__dirname, ".."),
|
||||
frameworks: ["jasmine"],
|
||||
files: [
|
||||
"wwwroot/lib/msgpack5/msgpack5.js",
|
||||
"node_modules/@aspnet/signalr/dist/browser/signalr.js",
|
||||
"node_modules/@aspnet/signalr-protocol-msgpack/dist/browser/signalr-protocol-msgpack.js",
|
||||
"wwwroot/dist/signalr-functional-tests.js"
|
||||
],
|
||||
preprocessors: {
|
||||
"**/*.js": ["sourcemap"]
|
||||
},
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: false,
|
||||
singleRun: false,
|
||||
concurrency: Infinity,
|
||||
|
||||
// Log browser messages to a file, not the terminal.
|
||||
browserConsoleLogOptions: {
|
||||
level: "debug",
|
||||
terminal: false
|
||||
},
|
||||
|
||||
// Increase some timeouts that are a little aggressive when multiple browsers (or SauceLabs) are in play.
|
||||
browserDisconnectTimeout: 10000, // default 2000
|
||||
browserDisconnectTolerance: 1, // default 0
|
||||
browserNoActivityTimeout: 4 * 60 * 1000, //default 10000
|
||||
captureTimeout: 4 * 60 * 1000, //default 60000
|
||||
|
||||
// Override/add values using the passed-in config.
|
||||
...config,
|
||||
|
||||
// Apply the default reporters along with whatever was passed in
|
||||
reporters: [...defaultReporters, ...(config.reporters || [])],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createKarmaConfig;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Karma configuration for a local run (the default)
|
||||
const createKarmaConfig = require("./karma.base.conf");
|
||||
const fs = require("fs");
|
||||
const which = require("which");
|
||||
|
||||
// Bring in the launchers directly to detect browsers
|
||||
const ChromeHeadlessBrowser = require("karma-chrome-launcher")["launcher:ChromeHeadless"][1];
|
||||
const ChromiumHeadlessBrowser = require("karma-chrome-launcher")["launcher:ChromiumHeadless"][1];
|
||||
const FirefoxHeadlessBrowser = require("karma-firefox-launcher")["launcher:FirefoxHeadless"][1];
|
||||
const EdgeBrowser = require("karma-edge-launcher")["launcher:Edge"][1];
|
||||
const SafariBrowser = require("karma-safari-launcher")["launcher:Safari"][1];
|
||||
const IEBrowser = require("karma-ie-launcher")["launcher:IE"][1];
|
||||
|
||||
let browsers = [];
|
||||
|
||||
function browserExists(path) {
|
||||
// On linux, the browsers just return the command, not a path, so we need to check if it exists.
|
||||
if (process.platform === "linux") {
|
||||
return !!which.sync(path, { nothrow: true });
|
||||
} else {
|
||||
return fs.existsSync(path);
|
||||
}
|
||||
}
|
||||
|
||||
function tryAddBrowser(name, b) {
|
||||
var path = b.DEFAULT_CMD[process.platform];
|
||||
if (b.ENV_CMD && process.env[b.ENV_CMD]) {
|
||||
path = process.env[b.ENV_CMD];
|
||||
}
|
||||
console.log(`Checking for ${name} at ${path}...`);
|
||||
|
||||
if (path && browserExists(path)) {
|
||||
console.log(`Located ${name} at ${path}.`);
|
||||
browsers.push(name);
|
||||
}
|
||||
else {
|
||||
console.log(`Unable to locate ${name}. Skipping.`);
|
||||
}
|
||||
}
|
||||
|
||||
// We use the launchers themselves to figure out if the browser exists. It's a bit sneaky, but it works.
|
||||
tryAddBrowser("ChromeHeadless", new ChromeHeadlessBrowser(() => { }, {}));
|
||||
tryAddBrowser("ChromiumHeadless", new ChromiumHeadlessBrowser(() => { }, {}));
|
||||
tryAddBrowser("FirefoxHeadless", new FirefoxHeadlessBrowser(0, () => { }, {}));
|
||||
|
||||
// We need to receive an argument from the caller, but globals don't seem to work, so we use an environment variable.
|
||||
if (process.env.ASPNETCORE_SIGNALR_TEST_ALL_BROWSERS === "true") {
|
||||
tryAddBrowser("Edge", new EdgeBrowser(() => { }, { create() { } }));
|
||||
tryAddBrowser("IE", new IEBrowser(() => { }, { create() { } }, {}));
|
||||
tryAddBrowser("Safari", new SafariBrowser(() => { }, {}));
|
||||
}
|
||||
|
||||
module.exports = createKarmaConfig({
|
||||
browsers,
|
||||
});
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// Karma configuration for a SauceLabs-based CI run.
|
||||
const createKarmaConfig = require("./karma.base.conf");
|
||||
|
||||
// "Evergreen" Desktop Browsers
|
||||
var evergreenBrowsers = {
|
||||
// Microsoft Edge Latest, Windows 10
|
||||
sl_edge_win10: {
|
||||
base: "SauceLabs",
|
||||
browserName: "microsoftedge",
|
||||
version: "latest",
|
||||
},
|
||||
|
||||
// Apple Safari Latest, macOS 10.13 (High Sierra)
|
||||
sl_safari_macOS1013: {
|
||||
base: "SauceLabs",
|
||||
browserName: "safari",
|
||||
version: "latest",
|
||||
platform: "OS X 10.13",
|
||||
},
|
||||
|
||||
// Google Chrome Latest, any OS.
|
||||
sl_chrome: {
|
||||
base: "SauceLabs",
|
||||
browserName: "chrome",
|
||||
version: "latest",
|
||||
},
|
||||
|
||||
// Mozilla Firefox Latest, any OS
|
||||
sl_firefox: {
|
||||
base: "SauceLabs",
|
||||
browserName: "firefox",
|
||||
version: "latest",
|
||||
},
|
||||
}
|
||||
|
||||
// Legacy Browsers
|
||||
var legacyBrowsers = {
|
||||
// Microsoft Internet Explorer 11, Windows 7
|
||||
sl_ie11_win7: {
|
||||
base: "SauceLabs",
|
||||
browserName: "internet explorer",
|
||||
version: "11",
|
||||
platform: "Windows 7",
|
||||
},
|
||||
};
|
||||
|
||||
// Mobile Browsers
|
||||
// TODO: Fill this in.
|
||||
var mobileBrowsers = {};
|
||||
|
||||
var customLaunchers = {
|
||||
...evergreenBrowsers,
|
||||
...legacyBrowsers,
|
||||
...mobileBrowsers,
|
||||
};
|
||||
|
||||
module.exports = createKarmaConfig({
|
||||
customLaunchers,
|
||||
browsers: Object.keys(customLaunchers),
|
||||
reporters: ["saucelabs"],
|
||||
sauceLabs: {
|
||||
testName: "SignalR Functional Tests",
|
||||
connectOptions: {
|
||||
// Required to enable WebSockets through the Sauce Connect proxy.
|
||||
noSslBumpDomains: ["all"]
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,14 +1,25 @@
|
|||
import { ChildProcess, spawn } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { EOL } from "os";
|
||||
import * as path from "path";
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { Readable } from "stream";
|
||||
|
||||
import { run } from "../../webdriver-tap-runner/lib";
|
||||
import * as _fs from "fs";
|
||||
import * as path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
import * as karma from "karma";
|
||||
|
||||
import * as _debug from "debug";
|
||||
const debug = _debug("signalr-functional-tests:run");
|
||||
|
||||
const ARTIFACTS_DIR = path.resolve(__dirname, "..", "..", "..", "..", "artifacts");
|
||||
const LOGS_DIR = path.resolve(ARTIFACTS_DIR, "logs");
|
||||
|
||||
// Promisify things from fs we want to use.
|
||||
const fs = {
|
||||
exists: promisify(_fs.exists),
|
||||
mkdir: promisify(_fs.mkdir),
|
||||
};
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error(`Unhandled promise rejection: ${reason}`);
|
||||
process.exit(1);
|
||||
|
|
@ -22,7 +33,7 @@ setTimeout(() => {
|
|||
|
||||
function waitForMatch(command: string, process: ChildProcess, regex: RegExp): Promise<RegExpMatchArray> {
|
||||
return new Promise<RegExpMatchArray>((resolve, reject) => {
|
||||
const commandDebug = _debug(`signalr-functional-tests:${command}`);
|
||||
const commandDebug = _debug(`${command}`);
|
||||
try {
|
||||
let lastLine = "";
|
||||
|
||||
|
|
@ -70,8 +81,10 @@ function waitForMatch(command: string, process: ChildProcess, regex: RegExp): Pr
|
|||
}
|
||||
|
||||
let configuration = "Debug";
|
||||
let chromePath: string;
|
||||
let spec: string;
|
||||
let sauce = false;
|
||||
let allBrowsers = false;
|
||||
let noColor = false;
|
||||
|
||||
for (let i = 2; i < process.argv.length; i += 1) {
|
||||
switch (process.argv[i]) {
|
||||
|
|
@ -83,30 +96,90 @@ for (let i = 2; i < process.argv.length; i += 1) {
|
|||
case "--verbose":
|
||||
_debug.enable("signalr-functional-tests:*");
|
||||
break;
|
||||
case "--chrome":
|
||||
i += 1;
|
||||
chromePath = process.argv[i];
|
||||
case "-vv":
|
||||
case "--very-verbose":
|
||||
_debug.enable("*");
|
||||
break;
|
||||
case "--spec":
|
||||
i += 1;
|
||||
spec = process.argv[i];
|
||||
break;
|
||||
case "--sauce":
|
||||
sauce = true;
|
||||
console.log("Running on SauceLabs.");
|
||||
break;
|
||||
case "-a":
|
||||
case "--all":
|
||||
allBrowsers = true;
|
||||
break;
|
||||
case "--no-color":
|
||||
noColor = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (chromePath) {
|
||||
debug(`Using Google Chrome at: '${chromePath}'`);
|
||||
const configFile = sauce ?
|
||||
path.resolve(__dirname, "karma.sauce.conf.js") :
|
||||
path.resolve(__dirname, "karma.local.conf.js");
|
||||
debug(`Loading Karma config file: ${configFile}`);
|
||||
|
||||
// Gross but it works
|
||||
process.env.ASPNETCORE_SIGNALR_TEST_ALL_BROWSERS = allBrowsers ? "true" : null;
|
||||
const config = (karma as any).config.parseConfig(configFile);
|
||||
|
||||
if (sauce) {
|
||||
let failed = false;
|
||||
|
||||
if (!process.env.SAUCE_USERNAME) {
|
||||
failed = true;
|
||||
console.error("Required environment variable 'SAUCE_USERNAME' is missing!");
|
||||
}
|
||||
|
||||
if (!process.env.SAUCE_ACCESS_KEY) {
|
||||
failed = true;
|
||||
console.error("Required environment variable 'SAUCE_ACCESS_KEY' is missing!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function runKarma(karmaConfig) {
|
||||
return new Promise<karma.TestResults>((resolve, reject) => {
|
||||
const server = new karma.Server(karmaConfig);
|
||||
server.on("run_complete", (browsers, results) => {
|
||||
return resolve(results);
|
||||
});
|
||||
server.start();
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Check if we got any browsers
|
||||
if (config.browsers.length === 0) {
|
||||
console.log("Unable to locate any suitable browsers. Skipping browser functional tests.");
|
||||
process.exit(0);
|
||||
return; // For good measure
|
||||
}
|
||||
|
||||
const serverPath = path.resolve(__dirname, "..", "bin", configuration, "netcoreapp2.2", "FunctionalTests.dll");
|
||||
|
||||
debug(`Launching Functional Test Server: ${serverPath}`);
|
||||
let desiredServerUrl = "http://127.0.0.1:0";
|
||||
|
||||
if (sauce) {
|
||||
// SauceLabs can only proxy certain ports for Edge and Safari.
|
||||
// https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy+FAQS
|
||||
desiredServerUrl = "http://127.0.0.1:9000";
|
||||
}
|
||||
|
||||
const dotnet = spawn("dotnet", [serverPath], {
|
||||
env: {
|
||||
...process.env,
|
||||
["ASPNETCORE_URLS"]: "http://127.0.0.1:0"
|
||||
["ASPNETCORE_URLS"]: desiredServerUrl,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -121,26 +194,38 @@ if (chromePath) {
|
|||
process.on("exit", cleanup);
|
||||
|
||||
debug("Waiting for Functional Test Server to start");
|
||||
const results = await waitForMatch("dotnet", dotnet, /Now listening on: (http:\/\/[^\/]+:[\d]+)/);
|
||||
debug(`Functional Test Server has started at ${results[1]}`);
|
||||
const matches = await waitForMatch("dotnet", dotnet, /Now listening on: (http:\/\/[^\/]+:[\d]+)/);
|
||||
const url = matches[1];
|
||||
debug(`Functional Test Server has started at ${url}`);
|
||||
|
||||
let url = results[1] + "?cacheBust=true";
|
||||
if (spec) {
|
||||
url += `&spec=${encodeURI(spec)}`;
|
||||
debug(`Using SignalR Server: ${url}`);
|
||||
|
||||
// Start karma server
|
||||
const conf = {
|
||||
...config,
|
||||
singleRun: true,
|
||||
};
|
||||
|
||||
// Set output directory for console log
|
||||
if (!await fs.exists(ARTIFACTS_DIR)) {
|
||||
await fs.mkdir(ARTIFACTS_DIR);
|
||||
}
|
||||
if (!await fs.exists(LOGS_DIR)) {
|
||||
await fs.mkdir(LOGS_DIR);
|
||||
}
|
||||
conf.browserConsoleLogOptions.path = path.resolve(LOGS_DIR, `browserlogs.console.${new Date().toISOString().replace(/:|\./g, "-")}`);
|
||||
|
||||
if (noColor) {
|
||||
conf.colors = false;
|
||||
}
|
||||
|
||||
debug(`Using server url: ${url}`);
|
||||
// Pass server URL to tests
|
||||
conf.client.args = ["--server", url];
|
||||
|
||||
const failureCount = await run("SignalR Browser Functional Tests", {
|
||||
browser: "chrome",
|
||||
chromeBinaryPath: chromePath,
|
||||
output: process.stdout,
|
||||
url,
|
||||
webdriverPort: 9515,
|
||||
});
|
||||
process.exit(failureCount);
|
||||
const results = await runKarma(conf);
|
||||
process.exit(results.exitCode);
|
||||
} catch (e) {
|
||||
console.error("Error: " + e.toString());
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import { ChildProcess, spawn, spawnSync } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const teamcity = !!process.env.TEAMCITY_VERSION;
|
||||
|
||||
let force = process.env.ASPNETCORE_SIGNALR_FORCE_BROWSER_TESTS === "true";
|
||||
let chromePath = process.env.ASPNETCORE_CHROME_PATH;
|
||||
|
||||
let configuration;
|
||||
let verbose;
|
||||
|
||||
for (let i = 2; i < process.argv.length; i += 1) {
|
||||
switch (process.argv[i]) {
|
||||
case "-f":
|
||||
case "--force":
|
||||
force = true;
|
||||
break;
|
||||
|
||||
case "--chrome":
|
||||
i += 1;
|
||||
chromePath = process.argv[i];
|
||||
break;
|
||||
|
||||
case "--configuration":
|
||||
i += 1;
|
||||
configuration = process.argv[i];
|
||||
break;
|
||||
|
||||
case "-v":
|
||||
case "--verbose":
|
||||
verbose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function failPrereq(error: string) {
|
||||
if (force) {
|
||||
console.error(`Browser functional tests cannot be run: ${error}`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`Skipping browser functional Tests: ${error}`);
|
||||
|
||||
// Zero exit code because we don't want to fail the build.
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
function getChromeBinaryPath(): string {
|
||||
if (chromePath) {
|
||||
return chromePath;
|
||||
} else {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
let candidatePath = path.resolve(process.env["ProgramFiles(x86)"], "Google", "Chrome", "Application", "chrome.exe");
|
||||
if (!existsSync(candidatePath)) {
|
||||
candidatePath = path.resolve(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe");
|
||||
}
|
||||
return candidatePath;
|
||||
case "darwin":
|
||||
return path.resolve("/", "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome");
|
||||
case "linux":
|
||||
return path.resolve("/", "usr", "bin", "google-chrome");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check prerequisites
|
||||
const chromeBinary = getChromeBinaryPath();
|
||||
if (!existsSync(chromeBinary)) {
|
||||
failPrereq(`Unable to locate Google Chrome at '${chromeBinary}'. Use the '--chrome' argument or 'ASPNETCORE_CHROME_PATH' environment variable to specify an alternate path`);
|
||||
} else {
|
||||
console.log(`Using Google Chrome Browser from '${chromeBinary}`);
|
||||
}
|
||||
|
||||
// Launch the tests (we know the CI already built, so run the 'test-only' script)
|
||||
const args = ["run", "test-only", "--", "--raw", "--chrome", chromeBinary];
|
||||
if (configuration) {
|
||||
args.push("--configuration");
|
||||
args.push(configuration);
|
||||
}
|
||||
if (verbose) {
|
||||
args.push("--verbose");
|
||||
}
|
||||
|
||||
let command = "npm";
|
||||
|
||||
if (process.platform === "win32") {
|
||||
// NPM is a cmd file, and it's tricky to "spawn". Instead, we'll find the NPM js file and use process.execPath to locate node.exe and run it directly
|
||||
const npmPath = path.resolve(process.execPath, "..", "node_modules", "npm", "bin", "npm-cli.js");
|
||||
if (!existsSync(npmPath)) {
|
||||
failPrereq(`Unable to locate npm command line at '${npmPath}'`);
|
||||
}
|
||||
|
||||
args.unshift(npmPath);
|
||||
command = process.execPath;
|
||||
}
|
||||
|
||||
console.log(`running: ${command} ${args.join(" ")}`);
|
||||
|
||||
const testProcess = spawn(command, args, { cwd: path.resolve(__dirname, "..") });
|
||||
testProcess.stderr.pipe(process.stderr);
|
||||
testProcess.stdout.pipe(process.stdout);
|
||||
testProcess.on("close", (code) => process.exit(code));
|
||||
|
|
@ -4,7 +4,28 @@
|
|||
import { HttpTransportType, IHubProtocol, JsonHubProtocol } from "@aspnet/signalr";
|
||||
import { MessagePackHubProtocol } from "@aspnet/signalr-protocol-msgpack";
|
||||
|
||||
export const ENDPOINT_BASE_URL = document.location.protocol + "//" + document.location.host;
|
||||
export let ENDPOINT_BASE_URL: string;
|
||||
|
||||
if ((window as any).__karma__) {
|
||||
const args = (window as any).__karma__.config.args as string[];
|
||||
let server = "";
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
switch (args[i]) {
|
||||
case "--server":
|
||||
i += 1;
|
||||
server = args[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Running in Karma? Need to use an absolute URL
|
||||
ENDPOINT_BASE_URL = server;
|
||||
console.log(`Using SignalR Server: ${ENDPOINT_BASE_URL}`);
|
||||
} else {
|
||||
ENDPOINT_BASE_URL = "";
|
||||
}
|
||||
|
||||
export const ECHOENDPOINT_URL = ENDPOINT_BASE_URL + "/echo";
|
||||
|
||||
export function getHttpTransportTypes(): HttpTransportType[] {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ describe("connection", () => {
|
|||
const message = "Hello World!";
|
||||
// the url should be resolved relative to the document.location.host
|
||||
// and the leading '/' should be automatically added to the url
|
||||
const connection = new HttpConnection("echo", {
|
||||
const connection = new HttpConnection(ECHOENDPOINT_URL, {
|
||||
...commonOptions,
|
||||
transport: transportType,
|
||||
});
|
||||
|
|
@ -73,10 +73,11 @@ describe("connection", () => {
|
|||
});
|
||||
|
||||
it("does not log content of messages sent or received by default", (done) => {
|
||||
TestLogger.saveLogsAndReset();
|
||||
const message = "Hello World!";
|
||||
|
||||
// DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is not set.
|
||||
const connection = new HttpConnection("echo", {
|
||||
const connection = new HttpConnection(ECHOENDPOINT_URL, {
|
||||
logger: TestLogger.instance,
|
||||
transport: transportType,
|
||||
});
|
||||
|
|
@ -105,10 +106,11 @@ describe("connection", () => {
|
|||
});
|
||||
|
||||
it("does log content of messages sent or received when enabled", (done) => {
|
||||
TestLogger.saveLogsAndReset();
|
||||
const message = "Hello World!";
|
||||
|
||||
// DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is set to true (even if commonOptions changes).
|
||||
const connection = new HttpConnection("echo", {
|
||||
const connection = new HttpConnection(ECHOENDPOINT_URL, {
|
||||
logMessageContent: true,
|
||||
logger: TestLogger.instance,
|
||||
transport: transportType,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { MessagePackHubProtocol } from "@aspnet/signalr-protocol-msgpack";
|
|||
import { eachTransport, eachTransportAndProtocol, ENDPOINT_BASE_URL } from "./Common";
|
||||
import { TestLogger } from "./TestLogger";
|
||||
|
||||
const TESTHUBENDPOINT_URL = "/testhub";
|
||||
const TESTHUB_NOWEBSOCKETS_ENDPOINT_URL = "/testhub-nowebsockets";
|
||||
const TESTHUBENDPOINT_URL = ENDPOINT_BASE_URL + "/testhub";
|
||||
const TESTHUB_NOWEBSOCKETS_ENDPOINT_URL = ENDPOINT_BASE_URL + "/testhub-nowebsockets";
|
||||
|
||||
// On slower CI machines, these tests sometimes take longer than 5s
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 1000;
|
||||
|
|
@ -498,7 +498,7 @@ describe("hubConnection", () => {
|
|||
try {
|
||||
const jwtToken = await getJwtToken(ENDPOINT_BASE_URL + "/generateJwtToken");
|
||||
|
||||
const hubConnection = getConnectionBuilder(transportType, "/authorizedhub", {
|
||||
const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/authorizedhub", {
|
||||
accessTokenFactory: () => jwtToken,
|
||||
}).build();
|
||||
|
||||
|
|
@ -524,7 +524,7 @@ describe("hubConnection", () => {
|
|||
const message = "你好,世界!";
|
||||
|
||||
try {
|
||||
const hubConnection = getConnectionBuilder(transportType, "/authorizedhub", {
|
||||
const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/authorizedhub", {
|
||||
accessTokenFactory: () => getJwtToken(ENDPOINT_BASE_URL + "/generateJwtToken"),
|
||||
}).build();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
export class LogBannerReporter implements jasmine.CustomReporter {
|
||||
public jasmineStarted(suiteInfo: jasmine.SuiteInfo): void {
|
||||
console.log("*** JASMINE SUITE STARTED ***");
|
||||
}
|
||||
|
||||
public jasmineDone(runDetails: jasmine.RunDetails): void {
|
||||
console.log("*** JASMINE SUITE FINISHED ***");
|
||||
}
|
||||
|
||||
public specStarted(result: jasmine.CustomReporterResult): void {
|
||||
console.log(`*** SPEC STARTED: ${result.fullName} ***`);
|
||||
}
|
||||
|
||||
public specDone(result: jasmine.CustomReporterResult): void {
|
||||
console.log(`*** SPEC DONE: ${result.fullName} ***`);
|
||||
}
|
||||
}
|
||||
|
||||
jasmine.getEnv().addReporter(new LogBannerReporter());
|
||||
|
|
@ -44,18 +44,8 @@ export class TestLogger implements ILogger {
|
|||
TestLogger.consoleLogger.log(logLevel, message);
|
||||
}
|
||||
|
||||
public static saveLogsAndReset(testName: string): TestLog {
|
||||
public static saveLogsAndReset(): TestLog {
|
||||
const currentLog = TestLogger.instance.currentLog;
|
||||
|
||||
// Stash the messages in a global to help people review them
|
||||
if (window) {
|
||||
const win = window as any;
|
||||
if (!win.TestLogMessages) {
|
||||
win.TestLogMessages = {};
|
||||
}
|
||||
win.TestLogMessages[testName] = currentLog;
|
||||
}
|
||||
|
||||
TestLogger.instance.currentLog = new TestLog();
|
||||
return currentLog;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
import { LogLevel } from "@aspnet/signalr";
|
||||
import { TestLogger } from "./TestLogger";
|
||||
import { getParameterByName } from "./Utils";
|
||||
|
||||
function formatValue(v: any): string {
|
||||
if (v === undefined) {
|
||||
return "<undefined>";
|
||||
} else if (v === null) {
|
||||
return "<null>";
|
||||
} else if (v.toString) {
|
||||
return v.toString();
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
class WebDriverReporter implements jasmine.CustomReporter {
|
||||
private element: HTMLDivElement;
|
||||
private specCounter: number = 1; // TAP number start at 1
|
||||
private recordCounter: number = 0;
|
||||
private concurrentSpecCount: number = 0;
|
||||
|
||||
constructor(private document: Document, show: boolean = false) {
|
||||
// We write to the DOM because it's the most compatible way for WebDriver to read.
|
||||
// For example, Chrome supports scraping console.log from WebDriver which would be ideal, but Firefox does not :(
|
||||
|
||||
// Create an element for the output
|
||||
this.element = document.createElement("div");
|
||||
this.element.setAttribute("id", "__tap_list");
|
||||
|
||||
if (!show) {
|
||||
this.element.setAttribute("style", "display: none");
|
||||
}
|
||||
|
||||
document.body.appendChild(this.element);
|
||||
}
|
||||
|
||||
public jasmineStarted(suiteInfo: jasmine.SuiteInfo): void {
|
||||
this.taplog(`1..${suiteInfo.totalSpecsDefined}`);
|
||||
}
|
||||
|
||||
public specStarted(result: jasmine.CustomReporterResult): void {
|
||||
this.concurrentSpecCount += 1;
|
||||
if (this.concurrentSpecCount > 1) {
|
||||
throw new Error("Unexpected concurrent tests!");
|
||||
}
|
||||
}
|
||||
|
||||
public specDone(result: jasmine.CustomReporterResult): void {
|
||||
this.concurrentSpecCount -= 1;
|
||||
const testLog = TestLogger.saveLogsAndReset(result.fullName);
|
||||
if (result.status === "disabled") {
|
||||
return;
|
||||
} else if (result.status === "failed") {
|
||||
this.taplog(`not ok ${this.specCounter} ${result.fullName}`);
|
||||
|
||||
// Just report the first failure
|
||||
this.taplog(" ---");
|
||||
if (result.failedExpectations && result.failedExpectations.length > 0) {
|
||||
this.taplog(" - messages:");
|
||||
for (const expectation of result.failedExpectations) {
|
||||
// Include YAML block with failed expectations
|
||||
this.taplog(` - message: ${expectation.message}`);
|
||||
if (expectation.matcherName) {
|
||||
this.taplog(` operator: ${expectation.matcherName}`);
|
||||
}
|
||||
if (expectation.expected) {
|
||||
this.taplog(` expected: ${formatValue(expectation.expected)}`);
|
||||
}
|
||||
if (expectation.actual) {
|
||||
this.taplog(` actual: ${formatValue(expectation.actual)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Report log messages
|
||||
if (testLog.messages.length > 0) {
|
||||
this.taplog(" - logs: ");
|
||||
for (const [timestamp, level, message] of testLog.messages) {
|
||||
this.taplog(` - level: ${LogLevel[level]}`);
|
||||
this.taplog(` timestamp: ${timestamp.toISOString()}`);
|
||||
this.taplog(` message: ${message}`);
|
||||
}
|
||||
}
|
||||
this.taplog(" ...");
|
||||
} else {
|
||||
this.taplog(`ok ${this.specCounter} ${result.fullName}`);
|
||||
}
|
||||
|
||||
this.specCounter += 1;
|
||||
}
|
||||
|
||||
public jasmineDone(runDetails: jasmine.RunDetails): void {
|
||||
this.element.setAttribute("data-done", "1");
|
||||
}
|
||||
|
||||
private taplog(msg: string) {
|
||||
for (const line of msg.split(/\r|\n|\r\n/)) {
|
||||
const input = this.document.createElement("input");
|
||||
input.setAttribute("id", `__tap_item_${this.recordCounter}`);
|
||||
this.recordCounter += 1;
|
||||
|
||||
input.value = line;
|
||||
this.element.appendChild(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jasmine.getEnv().addReporter(new WebDriverReporter(window.document, getParameterByName("displayTap") === "true"));
|
||||
|
|
@ -3,8 +3,11 @@
|
|||
|
||||
console.log("SignalR Functional Tests Loaded");
|
||||
|
||||
// Prereqs
|
||||
import "es6-promise/dist/es6-promise.auto.js";
|
||||
import "./LogBannerReporter";
|
||||
|
||||
// Tests
|
||||
import "./ConnectionTests";
|
||||
import "./HubConnectionTests";
|
||||
import "./WebDriverReporter";
|
||||
import "./WebSocketTests";
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@
|
|||
<script type="text/javascript" src="lib/jasmine/jasmine.js"></script>
|
||||
<script type="text/javascript" src="lib/jasmine/jasmine-html.js"></script>
|
||||
<script type="text/javascript" src="lib/jasmine/boot.js"></script>
|
||||
<script type="text/javascript" src="lib/jasmine-jsreporter.js"></script>
|
||||
<script type="text/javascript" src="lib/msgpack5/msgpack5.js"></script>
|
||||
<script type="text/javascript">
|
||||
jasmine.getEnv().addReporter(new jasmine.JSReporter2());
|
||||
function getParameterByName(name, url) {
|
||||
if (!url) {
|
||||
url = window.location.href;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
"target": "es5",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"inlineSources": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import * as path from "path";
|
||||
|
||||
import * as yargs from "yargs";
|
||||
|
||||
import * as _debug from "debug";
|
||||
import { run } from "./lib";
|
||||
const debug = _debug("webdriver-tap-runner:bin");
|
||||
|
||||
const argv = yargs
|
||||
.option("url", { demand: true, description: "The URL of the server to test against" })
|
||||
.option("name", { demand: true, description: "The name of the test run" })
|
||||
.option("browser", { alias: "b", default: "chrome", description: "The browser to use (only 'chrome' is supported right now)" })
|
||||
.option("webdriver-port", { default: 9515, description: "The port on which to launch the WebDriver server", number: true })
|
||||
.option("chrome-driver-log", { })
|
||||
.option("chrome-driver-log-verbose", { })
|
||||
.argv;
|
||||
|
||||
run(argv.name, {
|
||||
browser: argv.browser,
|
||||
chromeDriverLogFile: argv["chrome-driver-log"],
|
||||
chromeVerboseLogging: !!argv["chrome-driver-log-verbose"],
|
||||
url: argv.url,
|
||||
webdriverPort: argv["webdriver-port"],
|
||||
}).then((failures) => process.exit(failures));
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"name": "selenium-tap-runner",
|
||||
"version": "1.0.0",
|
||||
"description": "Run Browser tests in a Selenium browser and proxy TAP results to the console",
|
||||
"main": "dist/lib.js",
|
||||
"scripts": {
|
||||
"update-selenium": "selenium-standalone install",
|
||||
"exec": "ts-node ./bin.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chromedriver": "^2.35.0",
|
||||
"debug": "^3.1.0",
|
||||
"selenium-webdriver": "^4.0.0-alpha.1",
|
||||
"yargs": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "0.0.30",
|
||||
"@types/selenium-webdriver": "^3.0.8",
|
||||
"@types/yargs": "^11.0.0",
|
||||
"ts-node": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"lib": [ "es2016" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import * as os from "os";
|
||||
|
||||
import { By, logging, WebDriver, WebElement } from "selenium-webdriver";
|
||||
|
||||
import * as _debug from "debug";
|
||||
const debug = _debug("webdriver-tap-runner:utils");
|
||||
|
||||
export function delay(ms: number): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForElement(driver: WebDriver, id: string): Promise<WebElement> {
|
||||
debug(`Waiting for '${id}' element`);
|
||||
for (let attempts = 0; attempts < 2; attempts += 1) {
|
||||
const elements = await driver.findElements(By.id(id));
|
||||
if (elements && elements.length > 0) {
|
||||
debug(`Found '${id}' element`);
|
||||
return elements[0];
|
||||
}
|
||||
|
||||
debug(`Waiting 5 sec for '${id}' element to appear...`);
|
||||
await delay(5 * 1000);
|
||||
}
|
||||
|
||||
// We failed to find the item
|
||||
// Collect page source
|
||||
const source = await driver.getPageSource();
|
||||
const logs = await driver.manage().logs().get(logging.Type.BROWSER);
|
||||
const messages = logs.map((l) => `[${l.level}] ${l.message}`)
|
||||
.join(os.EOL);
|
||||
throw new Error(
|
||||
`Failed to find element '${id}'. Page Source:${os.EOL}${source}${os.EOL}` +
|
||||
`Browser Logs (${logs.length} messages):${os.EOL}${messages}${os.EOL}`);
|
||||
}
|
||||
|
||||
export async function isComplete(element: WebElement): Promise<boolean> {
|
||||
return (await element.getAttribute("data-done")) === "1";
|
||||
}
|
||||
|
||||
export async function getLogEntry(index: number, element: WebElement): Promise<WebElement> {
|
||||
const elements = await element.findElements(By.id(`__tap_item_${index}`));
|
||||
if (elements && elements.length > 0) {
|
||||
return elements[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getEntryContent(element: WebElement): Promise<string> {
|
||||
return await element.getAttribute("value");
|
||||
}
|
||||
|
||||
export async function flushEntries(index: number, element: WebElement, cb: (entry: string) => void): Promise<void> {
|
||||
let entry = await getLogEntry(index, element);
|
||||
while (entry) {
|
||||
index += 1;
|
||||
cb(await getEntryContent(entry));
|
||||
entry = await getLogEntry(index, element);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# JavaScript Functional Tests
|
||||
|
||||
Our JavaScript client functional tests are written using [Jasmine](https://jasmine.github.io/) and run in [Karma](https://karma-runner.github.io/2.0/index.html).
|
||||
|
||||
## Running tests from the command line
|
||||
|
||||
### Easy Mode
|
||||
|
||||
1. Start in the root of the repository
|
||||
2. `./build /t:Restore` (Windows) / `./build.sh /t:Restore` (macOS/Linux)
|
||||
3. `cd clients/ts/FunctionalTests`
|
||||
4. `npm test`
|
||||
|
||||
### Iterating
|
||||
|
||||
The `npm test` command will take a while, because it will build the `clients\ts\signalr`, `clients\ts\signalr-protocol-msgpack` and `clients\ts\FunctionalTests` folders as well as `dotnet build` the `clients\ts\FunctionalTests` folder (to build the server-side components). If you are making changes, it's nice to be able to build only the things you need to build. To skip all the optional build steps, you can run `npm run test:inner` in `clients\ts\FunctionalTests`. This will skip building `clients\ts\signalr` and `clients\signalr-protocol-msgpack` (it will still build the `clients\ts\FunctionalTests` folder). If you make changes to those libraries, you have to manually build those directories.
|
||||
|
||||
## Running tests from the browser
|
||||
|
||||
1. Start in the root of the repository
|
||||
2. `./build /t:Restore` (Windows) / `./build.sh /t:Restore` (macOS/Linux)
|
||||
3. `cd clients/ts`
|
||||
4. `npm run build` (Builds the `signalr` and `signalr-protocol-msgpack` libraries)
|
||||
5. `cd FunctionalTests`
|
||||
6. `npm run build` (Builds the `FunctionalTests` **and** copies in the necessary JavaScript)
|
||||
7. `dotnet run`
|
||||
|
||||
Copy-paste the URL that appears into the browser and the tests will run from the browser. They are easier to debug in the browser and you can use any browser you'd like.
|
||||
|
||||
## Running tests on SauceLabs
|
||||
|
||||
Prerequisite: You need a SauceLabs account. Running tests this way **will consume test minutes from your account** so be careful!
|
||||
|
||||
You must set the `SAUCE_USERNAME` environment variable to your SauceLabs username and the `SAUCE_ACCESS_KEY` environment variable to your SauceLabs access key.
|
||||
|
||||
**NOTE:** Running this will open a secure virtual network tunnel from your local machine to SauceLabs using the [Sauce Connect Proxy](https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy).
|
||||
|
||||
1. Start in the root of the repository
|
||||
2. `./build /t:Restore` (Windows) / `./build.sh /t:Restore` (macOS/Linux)
|
||||
3. `cd clients/ts/FunctionalTests`
|
||||
4. `npm run sauce`
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Debugging/Running Jest Tests
|
||||
# Debugging/Running JavaScript Unit Tests
|
||||
|
||||
We use [Jest](https://facebook.github.io/jest/) as our JavaScript testing framework. We also use [ts-jest](https://github.com/kulshekhar/ts-jest) which builds TypeScript automatically.
|
||||
|
||||
Loading…
Reference in New Issue