Add Appveyor builds and webdriver.io tests (tests cover Angular2Spa template only at present)

This commit is contained in:
SteveSandersonMS
2016-12-15 16:38:30 +00:00
parent 6decb30681
commit 67c2cfd84e
9 changed files with 532 additions and 2 deletions

View File

@@ -0,0 +1,102 @@
import * as childProcess from 'child_process';
import * as path from 'path';
import * as readline from 'readline';
const treeKill = require('tree-kill');
const crossSpawn: typeof childProcess.spawn = require('cross-spawn');
export const defaultUrl = 'http://localhost:5000';
export enum AspNetCoreEnviroment {
development,
production
}
export class AspNetProcess {
public static RunInMochaContext(cwd: string, mode: AspNetCoreEnviroment, dllToRun?: string) {
// Set up mocha before/after callbacks so that a 'dotnet run' process exists
// for the same duration as the context this is called inside
let aspNetProcess: AspNetProcess;
before(() => {
aspNetProcess = new AspNetProcess(cwd, mode, dllToRun);
return aspNetProcess.waitUntilListening();
});
after(() => aspNetProcess.dispose());
}
private _process: childProcess.ChildProcess;
private _processHasExited: boolean;
private _stdoutReader: readline.ReadLine;
constructor(cwd: string, mode: AspNetCoreEnviroment, dllToRun?: string) {
try {
// Prepare env for child process. Note that it doesn't inherit parent's env vars automatically,
// hence cloning process.env.
const childProcessEnv = Object.assign({}, process.env);
childProcessEnv.ASPNETCORE_ENVIRONMENT = mode === AspNetCoreEnviroment.development ? 'Development' : 'Production';
const verbOrAssembly = dllToRun || 'run';
console.log(`Running 'dotnet ${ verbOrAssembly }' in ${ cwd }`);
this._process = crossSpawn('dotnet', [verbOrAssembly], { cwd: cwd, stdio: 'pipe', env: childProcessEnv });
this._stdoutReader = readline.createInterface(this._process.stdout, null);
// Echo stdout to the test process's own stdout
this._stdoutReader.on('line', line => {
console.log(`[dotnet] ${ line.toString() }`);
});
// Also echo stderr
this._process.stderr.on('data', chunk => {
console.log(`[dotnet ERROR] ${ chunk.toString() }`);
});
// Ensure the process isn't orphaned even if Node crashes before we're disposed
process.on('exit', () => this._killProcessSync());
// Also track whether it exited on its own already
this._process.on('exit', () => {
this._processHasExited = true;
});
} catch(ex) {
console.log('ERROR: ' + ex.toString());
throw ex;
}
}
public waitUntilListening(): Promise<any> {
return new Promise((resolve, reject) => {
this._stdoutReader.on('line', (line: string) => {
if (line.startsWith('Now listening on:')) {
resolve();
}
});
});
}
public dispose(): Promise<any> {
return new Promise((resolve, reject) => {
this._killProcessSync(err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
private _killProcessSync(callback?: (err: any) => void) {
if (!this._processHasExited) {
// It's important to kill the whole tree, because 'dotnet run' launches a separate 'dotnet exec'
// child process that would otherwise be left running
treeKill(this._process.pid, 'SIGINT', callback);
}
}
}
export function publishProjectSync(sourceDir: string, outputDir: string) {
childProcess.execSync(`dotnet publish -c Release -o ${ outputDir }`, {
cwd: sourceDir,
stdio: 'inherit',
encoding: 'utf8'
});
}

View File

@@ -0,0 +1,12 @@
// Workaround for missing '.value' property on WebdriverIO.Client<RawResult<T>> that should be of type T
// Can't notify TypeScript that the property exists directly, because the interface merging feature doesn't
// appear to support pattern matching in such a way that WebdriverIO.Client<T> is extended only when T
// itself extends RawResult<U> for some U.
export function getValue<T>(client: WebdriverIO.Client<WebdriverIO.RawResult<T>>): T {
return (client as any).value;
}
// The official type declarations for getCssProperty are completely wrong. This function matches runtime behaviour.
export function getCssPropertyValue<T>(client: WebdriverIO.Client<T>, selector: string, cssProperty: string): string {
return (client.getCssProperty(selector, cssProperty) as any).value;
}

View File

@@ -0,0 +1,52 @@
import * as childProcess from 'child_process';
import * as path from 'path';
import * as rimraf from 'rimraf';
import * as mkdirp from 'mkdirp';
const generatorDirRelative = '../templates/package-builder/dist/generator-aspnetcore-spa';
const yoPackageDirAbsolute = path.resolve('./node_modules/yo');
export interface GeneratorOptions {
framework: string;
name: string;
tests?: boolean;
}
export function generateProjectSync(targetDir: string, generatorOptions: GeneratorOptions) {
const generatorDirAbsolute = path.resolve(generatorDirRelative);
console.log(`Running NPM install to prepare Yeoman generator at ${ generatorDirAbsolute }`);
childProcess.execSync(`npm install`, { stdio: 'inherit', cwd: generatorDirAbsolute });
console.log(`Ensuring empty output directory at ${ targetDir }`);
rimraf.sync(targetDir);
mkdirp.sync(targetDir);
const yoExecutableAbsolute = findYeomanCliScript();
console.log(`Will invoke Yeoman at ${ yoExecutableAbsolute } to generate application in ${ targetDir } with options:`);
console.log(JSON.stringify(generatorOptions, null, 2));
const command = `node "${ yoExecutableAbsolute }" "${ path.resolve(generatorDirAbsolute, './app/index.js') }"`;
const args = makeYeomanCommandLineArgs(generatorOptions);
childProcess.execSync(`${ command } ${ args }`, {
stdio: 'inherit',
cwd: targetDir
});
}
function findYeomanCliScript() {
// On Windows, you can't invoke ./node_modules/.bin/yo from the shell for some reason.
// So instead, we'll locate the CLI entrypoint that yeoman would expose if it was installed globally.
const yeomanPackageJsonPath = path.join(yoPackageDirAbsolute, './package.json');
const yeomanPackageJson = require(yeomanPackageJsonPath);
const yeomanCliScriptRelative = yeomanPackageJson.bin.yo;
if (!yeomanCliScriptRelative) {
throw new Error(`Could not find Yeoman CLI script. Looked for a bin/yo entry in ${ yeomanPackageJsonPath }`);
}
return path.join(yoPackageDirAbsolute, yeomanCliScriptRelative);
}
function makeYeomanCommandLineArgs(generatorOptions: GeneratorOptions) {
return Object.getOwnPropertyNames(generatorOptions)
.map(key => `--${ key }="${ generatorOptions[key] }"`)
.join(' ');
}