Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 67c2cfd

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

File tree

Expand file treeCollapse file tree

9 files changed

+532
-2
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

9 files changed

+532
-2
lines changed
Open diff view settings
Collapse file

‎appveyor.yml‎

Copy file name to clipboard
+15-2Lines changed: 15 additions & 2 deletions
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
Collapse file

‎test/.gitignore‎

Copy file name to clipboard
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/node_modules/
2+
/tmp/
3+
/yarn.lock
Collapse file

‎test/package.json‎

Copy file name to clipboard
+33Lines changed: 33 additions & 0 deletions
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+
}
Collapse file

‎test/templates/angular.spec.ts‎

Copy file name to clipboard
+96Lines changed: 96 additions & 0 deletions
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+
});
Collapse file

‎test/templates/util/aspnet.ts‎

Copy file name to clipboard
+102Lines changed: 102 additions & 0 deletions
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+
}
Collapse file

‎test/templates/util/webdriverio.ts‎

Copy file name to clipboard
+12Lines changed: 12 additions & 0 deletions
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+
}
Collapse file

‎test/templates/util/yeoman.ts‎

Copy file name to clipboard
+52Lines changed: 52 additions & 0 deletions
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+
}
Collapse file

‎test/tsconfig.json‎

Copy file name to clipboard
+15Lines changed: 15 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.