Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Commit 67c2cfd

Browse files
Add Appveyor builds and webdriver.io tests (tests cover Angular2Spa template only at present)
1 parent 6decb30 commit 67c2cfd

File tree

9 files changed

+532
-2
lines changed

9 files changed

+532
-2
lines changed

appveyor.yml

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
init:
22
- git config --global core.autocrlf true
33
build_script:
4-
- build.cmd verify
4+
- npm install -g npm@^3.0.0
5+
- npm --prefix templates/package-builder install
6+
- npm --prefix templates/package-builder run build
7+
# - build.cmd verify
58
clone_depth: 1
6-
test: off
9+
test_script:
10+
- dotnet restore ./src
11+
- npm install -g selenium-standalone
12+
- selenium-standalone install
13+
# The nosys flag is needed for selenium to work on Appveyor
14+
- ps: Start-Process selenium-standalone 'start','--','-Djna.nosys=true'
15+
- npm --prefix test install
16+
- npm --prefix test test
17+
on_finish :
18+
# After running tests, upload results to Appveyor
19+
- ps: (new-object net.webclient).UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\test\tmp\junit\*.xml))
720
deploy: off

test/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/node_modules/
2+
/tmp/
3+
/yarn.lock

test/package.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "test",
3+
"version": "1.0.0",
4+
"description": "Integration tests for the templates in JavaScriptServices. This is not really an NPM package and will not be published.",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "tsc && wdio"
8+
},
9+
"author": "Microsoft",
10+
"license": "Apache-2.0",
11+
"dependencies": {
12+
"@types/chai": "^3.4.34",
13+
"@types/mkdirp": "^0.3.29",
14+
"@types/mocha": "^2.2.33",
15+
"@types/node": "^6.0.52",
16+
"@types/rimraf": "^0.0.28",
17+
"@types/webdriverio": "^4.0.32",
18+
"chai": "^3.5.0",
19+
"cross-spawn": "^5.0.1",
20+
"mkdirp": "^0.5.1",
21+
"rimraf": "^2.5.4",
22+
"selenium-standalone": "^5.9.0",
23+
"tree-kill": "^1.1.0",
24+
"typescript": "^2.1.4",
25+
"webdriverio": "^4.5.0",
26+
"yo": "^1.8.5"
27+
},
28+
"devDependencies": {
29+
"wdio-junit-reporter": "^0.2.0",
30+
"wdio-mocha-framework": "^0.5.7",
31+
"wdio-selenium-standalone-service": "0.0.7"
32+
}
33+
}

test/templates/angular.spec.ts

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { expect } from 'chai';
4+
import { generateProjectSync } from './util/yeoman';
5+
import { AspNetProcess, AspNetCoreEnviroment, defaultUrl, publishProjectSync } from './util/aspnet';
6+
import { getValue, getCssPropertyValue } from './util/webdriverio';
7+
8+
// First, generate a new project using the locally-built generator-aspnetcore-spa
9+
// Do this outside the Mocha fixture, otherwise Mocha will time out
10+
const appDir = path.resolve(__dirname, '../generated/angular');
11+
const publishedAppDir = path.resolve(appDir, './bin/Release/published');
12+
if (!process.env.SKIP_PROJECT_GENERATION) {
13+
generateProjectSync(appDir, { framework: 'angular-2', name: 'Test App', tests: false });
14+
publishProjectSync(appDir, publishedAppDir);
15+
}
16+
17+
function testBasicNavigation() {
18+
describe('Basic navigation', () => {
19+
beforeEach(() => browser.url(defaultUrl));
20+
21+
it('should initially display the home page', () => {
22+
expect(browser.getText('h1')).to.eq('Hello, world!');
23+
expect(browser.getText('li a[href="https://angular.io/"]')).to.eq('Angular 2');
24+
});
25+
26+
it('should be able to show the counter page', () => {
27+
browser.click('a[href="/counter"]');
28+
expect(browser.getText('h1')).to.eq('Counter');
29+
30+
// Test clicking the 'increment' button
31+
expect(browser.getText('counter strong')).to.eq('0');
32+
browser.click('counter button');
33+
expect(browser.getText('counter strong')).to.eq('1');
34+
});
35+
36+
it('should be able to show the fetchdata page', () => {
37+
browser.click('a[href="/fetch-data"]');
38+
expect(browser.getText('h1')).to.eq('Weather forecast');
39+
40+
browser.waitForExist('fetchdata table');
41+
expect(getValue(browser.elements('fetchdata table tbody tr')).length).to.eq(5);
42+
});
43+
});
44+
}
45+
46+
function testHotModuleReplacement() {
47+
describe('Hot module replacement', () => {
48+
beforeEach(() => browser.url(defaultUrl));
49+
50+
it('should update when HTML is changed', () => {
51+
expect(browser.getText('h1')).to.eq('Hello, world!');
52+
53+
const filePath = path.resolve(appDir, './ClientApp/app/components/home/home.component.html');
54+
const origFileContents = fs.readFileSync(filePath, 'utf8');
55+
56+
try {
57+
const newFileContents = origFileContents.replace('<h1>Hello, world!</h1>', '<h1>HMR is working</h1>');
58+
fs.writeFileSync(filePath, newFileContents, { encoding: 'utf8' });
59+
60+
browser.waitUntil(() => browser.getText('h1').toString() === 'HMR is working');
61+
} finally {
62+
// Restore old contents so that other tests don't have to account for this
63+
fs.writeFileSync(filePath, origFileContents, { encoding: 'utf8' });
64+
}
65+
});
66+
67+
it('should update when CSS is changed', () => {
68+
expect(getCssPropertyValue(browser, 'li.link-active a', 'color')).to.eq('rgba(255,255,255,1)');
69+
70+
const filePath = path.resolve(appDir, './ClientApp/app/components/navmenu/navmenu.component.css');
71+
const origFileContents = fs.readFileSync(filePath, 'utf8');
72+
73+
try {
74+
const newFileContents = origFileContents.replace('color: white;', 'color: purple;');
75+
fs.writeFileSync(filePath, newFileContents, { encoding: 'utf8' });
76+
77+
browser.waitUntil(() => getCssPropertyValue(browser, 'li.link-active a', 'color') === 'rgba(128,0,128,1)');
78+
} finally {
79+
// Restore old contents so that other tests don't have to account for this
80+
fs.writeFileSync(filePath, origFileContents, { encoding: 'utf8' });
81+
}
82+
});
83+
});
84+
}
85+
86+
// Now launch dotnet and use selenium to perform tests
87+
describe('Angular template: dev mode', () => {
88+
AspNetProcess.RunInMochaContext(appDir, AspNetCoreEnviroment.development);
89+
testBasicNavigation();
90+
testHotModuleReplacement();
91+
});
92+
93+
describe('Angular template: production mode', () => {
94+
AspNetProcess.RunInMochaContext(publishedAppDir, AspNetCoreEnviroment.production, 'angular.dll');
95+
testBasicNavigation();
96+
});

test/templates/util/aspnet.ts

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as childProcess from 'child_process';
2+
import * as path from 'path';
3+
import * as readline from 'readline';
4+
const treeKill = require('tree-kill');
5+
const crossSpawn: typeof childProcess.spawn = require('cross-spawn');
6+
7+
export const defaultUrl = 'http://localhost:5000';
8+
9+
export enum AspNetCoreEnviroment {
10+
development,
11+
production
12+
}
13+
14+
export class AspNetProcess {
15+
public static RunInMochaContext(cwd: string, mode: AspNetCoreEnviroment, dllToRun?: string) {
16+
// Set up mocha before/after callbacks so that a 'dotnet run' process exists
17+
// for the same duration as the context this is called inside
18+
let aspNetProcess: AspNetProcess;
19+
before(() => {
20+
aspNetProcess = new AspNetProcess(cwd, mode, dllToRun);
21+
return aspNetProcess.waitUntilListening();
22+
});
23+
after(() => aspNetProcess.dispose());
24+
}
25+
26+
private _process: childProcess.ChildProcess;
27+
private _processHasExited: boolean;
28+
private _stdoutReader: readline.ReadLine;
29+
30+
constructor(cwd: string, mode: AspNetCoreEnviroment, dllToRun?: string) {
31+
try {
32+
// Prepare env for child process. Note that it doesn't inherit parent's env vars automatically,
33+
// hence cloning process.env.
34+
const childProcessEnv = Object.assign({}, process.env);
35+
childProcessEnv.ASPNETCORE_ENVIRONMENT = mode === AspNetCoreEnviroment.development ? 'Development' : 'Production';
36+
37+
const verbOrAssembly = dllToRun || 'run';
38+
console.log(`Running 'dotnet ${ verbOrAssembly }' in ${ cwd }`);
39+
this._process = crossSpawn('dotnet', [verbOrAssembly], { cwd: cwd, stdio: 'pipe', env: childProcessEnv });
40+
this._stdoutReader = readline.createInterface(this._process.stdout, null);
41+
42+
// Echo stdout to the test process's own stdout
43+
this._stdoutReader.on('line', line => {
44+
console.log(`[dotnet] ${ line.toString() }`);
45+
});
46+
47+
// Also echo stderr
48+
this._process.stderr.on('data', chunk => {
49+
console.log(`[dotnet ERROR] ${ chunk.toString() }`);
50+
});
51+
52+
// Ensure the process isn't orphaned even if Node crashes before we're disposed
53+
process.on('exit', () => this._killProcessSync());
54+
55+
// Also track whether it exited on its own already
56+
this._process.on('exit', () => {
57+
this._processHasExited = true;
58+
});
59+
} catch(ex) {
60+
console.log('ERROR: ' + ex.toString());
61+
throw ex;
62+
}
63+
}
64+
65+
public waitUntilListening(): Promise<any> {
66+
return new Promise((resolve, reject) => {
67+
this._stdoutReader.on('line', (line: string) => {
68+
if (line.startsWith('Now listening on:')) {
69+
resolve();
70+
}
71+
});
72+
});
73+
}
74+
75+
public dispose(): Promise<any> {
76+
return new Promise((resolve, reject) => {
77+
this._killProcessSync(err => {
78+
if (err) {
79+
reject(err);
80+
} else {
81+
resolve();
82+
}
83+
});
84+
});
85+
}
86+
87+
private _killProcessSync(callback?: (err: any) => void) {
88+
if (!this._processHasExited) {
89+
// It's important to kill the whole tree, because 'dotnet run' launches a separate 'dotnet exec'
90+
// child process that would otherwise be left running
91+
treeKill(this._process.pid, 'SIGINT', callback);
92+
}
93+
}
94+
}
95+
96+
export function publishProjectSync(sourceDir: string, outputDir: string) {
97+
childProcess.execSync(`dotnet publish -c Release -o ${ outputDir }`, {
98+
cwd: sourceDir,
99+
stdio: 'inherit',
100+
encoding: 'utf8'
101+
});
102+
}

test/templates/util/webdriverio.ts

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

test/templates/util/yeoman.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as childProcess from 'child_process';
2+
import * as path from 'path';
3+
import * as rimraf from 'rimraf';
4+
import * as mkdirp from 'mkdirp';
5+
6+
const generatorDirRelative = '../templates/package-builder/dist/generator-aspnetcore-spa';
7+
const yoPackageDirAbsolute = path.resolve('./node_modules/yo');
8+
9+
export interface GeneratorOptions {
10+
framework: string;
11+
name: string;
12+
tests?: boolean;
13+
}
14+
15+
export function generateProjectSync(targetDir: string, generatorOptions: GeneratorOptions) {
16+
const generatorDirAbsolute = path.resolve(generatorDirRelative);
17+
console.log(`Running NPM install to prepare Yeoman generator at ${ generatorDirAbsolute }`);
18+
childProcess.execSync(`npm install`, { stdio: 'inherit', cwd: generatorDirAbsolute });
19+
20+
console.log(`Ensuring empty output directory at ${ targetDir }`);
21+
rimraf.sync(targetDir);
22+
mkdirp.sync(targetDir);
23+
24+
const yoExecutableAbsolute = findYeomanCliScript();
25+
console.log(`Will invoke Yeoman at ${ yoExecutableAbsolute } to generate application in ${ targetDir } with options:`);
26+
console.log(JSON.stringify(generatorOptions, null, 2));
27+
const command = `node "${ yoExecutableAbsolute }" "${ path.resolve(generatorDirAbsolute, './app/index.js') }"`;
28+
const args = makeYeomanCommandLineArgs(generatorOptions);
29+
childProcess.execSync(`${ command } ${ args }`, {
30+
stdio: 'inherit',
31+
cwd: targetDir
32+
});
33+
}
34+
35+
function findYeomanCliScript() {
36+
// On Windows, you can't invoke ./node_modules/.bin/yo from the shell for some reason.
37+
// So instead, we'll locate the CLI entrypoint that yeoman would expose if it was installed globally.
38+
const yeomanPackageJsonPath = path.join(yoPackageDirAbsolute, './package.json');
39+
const yeomanPackageJson = require(yeomanPackageJsonPath);
40+
const yeomanCliScriptRelative = yeomanPackageJson.bin.yo;
41+
if (!yeomanCliScriptRelative) {
42+
throw new Error(`Could not find Yeoman CLI script. Looked for a bin/yo entry in ${ yeomanPackageJsonPath }`);
43+
}
44+
45+
return path.join(yoPackageDirAbsolute, yeomanCliScriptRelative);
46+
}
47+
48+
function makeYeomanCommandLineArgs(generatorOptions: GeneratorOptions) {
49+
return Object.getOwnPropertyNames(generatorOptions)
50+
.map(key => `--${ key }="${ generatorOptions[key] }"`)
51+
.join(' ');
52+
}

test/tsconfig.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"moduleResolution": "node",
4+
"target": "es5",
5+
"rootDir": ".",
6+
"outDir": "tmp",
7+
"sourceMap": false,
8+
"lib": ["es6", "dom"]
9+
},
10+
"exclude": [
11+
"node_modules",
12+
"**/node_modules",
13+
"tmp"
14+
]
15+
}

0 commit comments

Comments
 (0)