change to use Karma for Functional Tests (#2450)

This commit is contained in:
Andrew Stanton-Nurse 2018-06-12 13:42:21 -07:00 committed by GitHub
parent cb8264321d
commit c7af64332b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 4313 additions and 2030 deletions

View File

@ -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>

View File

@ -1 +1,4 @@
wwwroot/lib/
wwwroot/lib/
# Sauce Connect proxy logs.
sc-*.log

View File

@ -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)" />

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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"

View File

@ -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;

View File

@ -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,
});

View File

@ -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"]
}
},
});

View File

@ -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);
}
})();

View File

@ -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));

View File

@ -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[] {

View File

@ -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,

View File

@ -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();

View File

@ -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());

View File

@ -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;
}

View File

@ -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"));

View File

@ -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";

View File

@ -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;

View File

@ -5,7 +5,6 @@
"target": "es5",
"sourceMap": true,
"moduleResolution": "node",
"importHelpers": true,
"inlineSources": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,

View File

@ -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));

View File

@ -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

View File

@ -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"
}
}

View File

@ -1,7 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"lib": [ "es2016" ]
}
}

View File

@ -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);
}
}

41
docs/JSFunctionalTests.md Normal file
View File

@ -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`

View File

@ -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.