Skip to content

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‎

Lines 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

‎test/.gitignore‎

Lines 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

‎test/package.json‎

Lines 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+
}

‎test/templates/angular.spec.ts‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import*asfsfrom'fs';
2+
import*aspathfrom'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+
constappDir=path.resolve(__dirname,'../generated/angular');
11+
constpublishedAppDir=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+
functiontestBasicNavigation(){
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+
functiontestHotModuleReplacement(){
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+
constfilePath=path.resolve(appDir,'./ClientApp/app/components/home/home.component.html');
54+
constorigFileContents=fs.readFileSync(filePath,'utf8');
55+
56+
try{
57+
constnewFileContents=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+
constfilePath=path.resolve(appDir,'./ClientApp/app/components/navmenu/navmenu.component.css');
71+
constorigFileContents=fs.readFileSync(filePath,'utf8');
72+
73+
try{
74+
constnewFileContents=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‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import*aschildProcessfrom'child_process';
2+
import*aspathfrom'path';
3+
import*asreadlinefrom'readline';
4+
consttreeKill=require('tree-kill');
5+
constcrossSpawn: typeofchildProcess.spawn=require('cross-spawn');
6+
7+
exportconstdefaultUrl='http://localhost:5000';
8+
9+
exportenumAspNetCoreEnviroment{
10+
development,
11+
production
12+
}
13+
14+
exportclassAspNetProcess{
15+
publicstaticRunInMochaContext(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+
letaspNetProcess: AspNetProcess;
19+
before(()=>{
20+
aspNetProcess=newAspNetProcess(cwd,mode,dllToRun);
21+
returnaspNetProcess.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+
constchildProcessEnv=Object.assign({},process.env);
35+
childProcessEnv.ASPNETCORE_ENVIRONMENT=mode===AspNetCoreEnviroment.development ? 'Development' : 'Production';
36+
37+
constverbOrAssembly=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+
throwex;
62+
}
63+
}
64+
65+
publicwaitUntilListening(): Promise<any>{
66+
returnnewPromise((resolve,reject)=>{
67+
this._stdoutReader.on('line',(line: string)=>{
68+
if(line.startsWith('Now listening on:')){
69+
resolve();
70+
}
71+
});
72+
});
73+
}
74+
75+
publicdispose(): Promise<any>{
76+
returnnewPromise((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+
exportfunctionpublishProjectSync(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‎

Lines 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+
exportfunctiongetValue<T>(client: WebdriverIO.Client<WebdriverIO.RawResult<T>>): T{
6+
return(clientasany).value;
7+
}
8+
9+
// The official type declarations for getCssProperty are completely wrong. This function matches runtime behaviour.
10+
exportfunctiongetCssPropertyValue<T>(client: WebdriverIO.Client<T>,selector: string,cssProperty: string): string{
11+
return(client.getCssProperty(selector,cssProperty)asany).value;
12+
}

‎test/templates/util/yeoman.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import*aschildProcessfrom'child_process';
2+
import*aspathfrom'path';
3+
import*asrimraffrom'rimraf';
4+
import*asmkdirpfrom'mkdirp';
5+
6+
constgeneratorDirRelative='../templates/package-builder/dist/generator-aspnetcore-spa';
7+
constyoPackageDirAbsolute=path.resolve('./node_modules/yo');
8+
9+
exportinterfaceGeneratorOptions{
10+
framework: string;
11+
name: string;
12+
tests?: boolean;
13+
}
14+
15+
exportfunctiongenerateProjectSync(targetDir: string,generatorOptions: GeneratorOptions){
16+
constgeneratorDirAbsolute=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+
constyoExecutableAbsolute=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+
constcommand=`node "${yoExecutableAbsolute}" "${path.resolve(generatorDirAbsolute,'./app/index.js')}"`;
28+
constargs=makeYeomanCommandLineArgs(generatorOptions);
29+
childProcess.execSync(`${command}${args}`,{
30+
stdio: 'inherit',
31+
cwd: targetDir
32+
});
33+
}
34+
35+
functionfindYeomanCliScript(){
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+
constyeomanPackageJsonPath=path.join(yoPackageDirAbsolute,'./package.json');
39+
constyeomanPackageJson=require(yeomanPackageJsonPath);
40+
constyeomanCliScriptRelative=yeomanPackageJson.bin.yo;
41+
if(!yeomanCliScriptRelative){
42+
thrownewError(`Could not find Yeoman CLI script. Looked for a bin/yo entry in ${yeomanPackageJsonPath}`);
43+
}
44+
45+
returnpath.join(yoPackageDirAbsolute,yeomanCliScriptRelative);
46+
}
47+
48+
functionmakeYeomanCommandLineArgs(generatorOptions: GeneratorOptions){
49+
returnObject.getOwnPropertyNames(generatorOptions)
50+
.map(key=>`--${key}="${generatorOptions[key]}"`)
51+
.join(' ');
52+
}

‎test/tsconfig.json‎

Lines 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)