diff options
author | 杨宇千 <crupest@outlook.com> | 2019-02-21 16:32:16 +0000 |
---|---|---|
committer | 杨宇千 <crupest@outlook.com> | 2019-02-21 16:32:16 +0000 |
commit | 9ae2a899eb53b8ee31710a5533cdf21b0f14dafd (patch) | |
tree | 8f22c966d15d928a5587385f38b5284f0691b2de | |
parent | 35f8562f1f1db36a42c53ce42c39db3d615deb0f (diff) | |
parent | 504d770e51a07ca7765de725979412b6dacdcf15 (diff) | |
download | timeline-9ae2a899eb53b8ee31710a5533cdf21b0f14dafd.tar.gz timeline-9ae2a899eb53b8ee31710a5533cdf21b0f14dafd.tar.bz2 timeline-9ae2a899eb53b8ee31710a5533cdf21b0f14dafd.zip |
Merged PR 3: Add unit test for front side.
Related work items: #1
-rw-r--r-- | Timeline/ClientApp/.vscode/launch.json | 11 | ||||
-rw-r--r-- | Timeline/ClientApp/angular.json | 5 | ||||
-rw-r--r-- | Timeline/ClientApp/package.json | 1 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/home/home.component.spec.ts | 5 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/home/home.component.ts | 13 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts | 46 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts | 71 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts | 40 | ||||
-rw-r--r-- | Timeline/ClientApp/src/karma.conf.js | 1 | ||||
-rw-r--r-- | Timeline/ClientApp/src/test.ts | 2 | ||||
-rw-r--r-- | Timeline/Configs/TodoListConfig.cs | 12 | ||||
-rw-r--r-- | Timeline/Configs/TodoPageConfig.cs | 21 | ||||
-rw-r--r-- | Timeline/Controllers/TodoListController.cs | 11 | ||||
-rw-r--r-- | Timeline/Startup.cs | 2 | ||||
-rw-r--r-- | Timeline/appsettings.json | 6 | ||||
-rw-r--r-- | azure-pipelines.yml | 20 |
16 files changed, 202 insertions, 65 deletions
diff --git a/Timeline/ClientApp/.vscode/launch.json b/Timeline/ClientApp/.vscode/launch.json index 5b749430..96a3c552 100644 --- a/Timeline/ClientApp/.vscode/launch.json +++ b/Timeline/ClientApp/.vscode/launch.json @@ -7,9 +7,16 @@ { "type": "chrome", "request": "launch", - "name": "Launch Chrome against localhost", + "name": "Launch app", "url": "https://localhost:5001", "webRoot": "${workspaceFolder}" - } + }, + { + "type": "chrome", + "request": "launch", + "name": "Launch test", + "url": "http://localhost:9876", + "webRoot": "${workspaceFolder}" + } ] } diff --git a/Timeline/ClientApp/angular.json b/Timeline/ClientApp/angular.json index bf8a95ed..9a696714 100644 --- a/Timeline/ClientApp/angular.json +++ b/Timeline/ClientApp/angular.json @@ -1,6 +1,9 @@ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, + "cli": { + "packageManager": "yarn" + }, "newProjectRoot": "projects", "projects": { "Timeline": { @@ -73,7 +76,7 @@ "tsConfig": "src/tsconfig.spec.json", "karmaConfig": "src/karma.conf.js", "styles": [ - "styles.css" + "src/styles.css" ], "scripts": [], "assets": [ diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json index 205874e5..9d948004 100644 --- a/Timeline/ClientApp/package.json +++ b/Timeline/ClientApp/package.json @@ -46,6 +46,7 @@ "karma-coverage-istanbul-reporter": "^2.0.4", "karma-jasmine": "^2.0.1", "karma-jasmine-html-reporter": "^1.4.0", + "karma-junit-reporter": "^1.2.0", "tslint": "^5.12.1", "typescript": "~3.2.4" }, diff --git a/Timeline/ClientApp/src/app/home/home.component.spec.ts b/Timeline/ClientApp/src/app/home/home.component.spec.ts index 490e81bd..74bedd08 100644 --- a/Timeline/ClientApp/src/app/home/home.component.spec.ts +++ b/Timeline/ClientApp/src/app/home/home.component.spec.ts @@ -2,15 +2,16 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { HomeComponent } from './home.component'; + describe('HomeComponent', () => { let component: HomeComponent; let fixture: ComponentFixture<HomeComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ HomeComponent ] + declarations: [HomeComponent], }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/Timeline/ClientApp/src/app/home/home.component.ts b/Timeline/ClientApp/src/app/home/home.component.ts index 873aee7f..2b16eef7 100644 --- a/Timeline/ClientApp/src/app/home/home.component.ts +++ b/Timeline/ClientApp/src/app/home/home.component.ts @@ -1,10 +1,4 @@ import { Component, OnInit } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; - -class LoginInfo { - username = ''; - password = ''; -} @Component({ selector: 'app-home', @@ -13,15 +7,8 @@ class LoginInfo { }) export class HomeComponent implements OnInit { - loginInfo = new LoginInfo(); message = ''; - constructor(/* private http: HttpClient */) { } - ngOnInit() { } - - tryLogin() { - alert('Not implemented!!!'); - } } diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts b/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts index a74ce3e6..7fc4164d 100644 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts +++ b/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts @@ -1,16 +1,45 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed, tick } from '@angular/core/testing'; + +import { defer, Observable } from 'rxjs'; import { TodoListPageComponent } from './todo-list-page.component'; +import { TodoListService, WorkItem } from './todo-list.service'; +import { By } from '@angular/platform-browser'; + +@Component({ + selector: 'mat-progress-bar', + template: '' +}) +class MatProgressBarStubComponent { + +} + +function asyncData<T>(data: T): Observable<T> { + return defer(() => Promise.resolve(data)); +} describe('TodoListPageComponent', () => { let component: TodoListPageComponent; let fixture: ComponentFixture<TodoListPageComponent>; beforeEach(async(() => { + const todoListService: jasmine.SpyObj<TodoListService> = jasmine.createSpyObj('TodoListService', ['getWorkItemList']); + + todoListService.getWorkItemList.and.returnValue(asyncData(<WorkItem[]>[{ + id: 0, title: 'Test title 1', closed: true + }, { + id: 1, title: 'Test title 2', closed: false + }])); + TestBed.configureTestingModule({ - declarations: [ TodoListPageComponent ] + declarations: [TodoListPageComponent, MatProgressBarStubComponent], + providers: [ + { provide: TodoListService, useValue: todoListService } + ], + schemas: [NO_ERRORS_SCHEMA] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { @@ -22,4 +51,15 @@ describe('TodoListPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should show progress bar during loading', () => { + expect(fixture.debugElement.query(By.css('mat-progress-bar'))).toBeTruthy(); + }); + + it('should hide progress bar after loading', async(() => { + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('mat-progress-bar'))).toBeFalsy(); + }); + })); }); diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts b/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts index 529ba8cc..7e88ca52 100644 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts +++ b/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts @@ -1,12 +1,79 @@ import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { TodoListService, WorkItem, AzureDevOpsAccessInfo, WiqlResult, WiqlWorkItemResult, WorkItemResult } from './todo-list.service'; -import { TodoListService } from './todo-list.service'; describe('TodoListServiceService', () => { - beforeEach(() => TestBed.configureTestingModule({})); + beforeEach(() => TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + })); it('should be created', () => { + + const service: TodoListService = TestBed.get(TodoListService); + expect(service).toBeTruthy(); + }); + + it('should work well', () => { const service: TodoListService = TestBed.get(TodoListService); expect(service).toBeTruthy(); + + const mockWorkItems: WorkItem[] = [{ + id: 0, + title: 'Test work item 1', + closed: true + }, { + id: 1, + title: 'Test work item 2', + closed: false + }]; + + service.getWorkItemList().subscribe(data => { + expect(data).toEqual(mockWorkItems); + }); + + const httpController: HttpTestingController = TestBed.get(HttpTestingController); + + const mockAccessInfo: AzureDevOpsAccessInfo = { + username: 'testusername', + personalAccessToken: 'testtoken', + organization: 'testorganization', + project: 'testproject' + }; + + httpController.expectOne('/api/TodoPage/AzureDevOpsAccessInfo').flush(mockAccessInfo); + + const mockWiqlWorkItems: WiqlWorkItemResult[] = [{ + id: 0, + url: `https://dev.azure.com/${mockAccessInfo.organization}/${mockAccessInfo.project}/_apis/wit/workItems/0` + }, { + id: 1, + url: `https://dev.azure.com/${mockAccessInfo.organization}/${mockAccessInfo.project}/_apis/wit/workItems/1` + }]; + + const authorizationHeader = 'Basic ' + btoa(mockAccessInfo.username + ':' + mockAccessInfo.personalAccessToken); + + httpController.expectOne(req => + req.url === `https://dev.azure.com/${mockAccessInfo.organization}/${mockAccessInfo.project}/_apis/wit/wiql?api-version=5.0` && + req.headers.get('Authorization') === authorizationHeader + ).flush(<WiqlResult>{ workItems: mockWiqlWorkItems }); + + function mapWorkItemToResult(workItem: WorkItem): WorkItemResult { + return { + id: workItem.id, + fields: { + [TodoListService.titleFieldName]: workItem.title, + [TodoListService.stateFieldName]: (workItem.closed ? 'Closed' : 'Active') + } + }; + } + + for (let i = 0; i < mockWorkItems.length; i++) { + httpController.expectOne(req => + req.url === mockWiqlWorkItems[i].url && + req.headers.get('Authorization') === authorizationHeader + ).flush(mapWorkItemToResult(mockWorkItems[i])); + } }); }); diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts b/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts index 0b54653b..619e9a6b 100644 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts +++ b/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts @@ -3,20 +3,28 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { switchMap, concatMap, map, toArray } from 'rxjs/operators'; -interface WiqlWorkItemResult { +export interface AzureDevOpsAccessInfo { + username: string; + personalAccessToken: string; + organization: string; + project: string; +} + +export interface WiqlWorkItemResult { id: number; url: string; } -interface WiqlResult { +export interface WiqlResult { workItems: WiqlWorkItemResult[]; } -interface WorkItemResult { +export interface WorkItemResult { id: number; fields: { [name: string]: any }; } + export interface WorkItem { id: number; title: string; @@ -28,41 +36,33 @@ export interface WorkItem { }) export class TodoListService { - private username = 'crupest'; - private organization = 'crupest-web'; - private project = 'Timeline'; - private titleFieldName = 'System.Title'; - private stateFieldName = 'System.State'; + public static titleFieldName = 'System.Title'; + public static stateFieldName = 'System.State'; constructor(private client: HttpClient) { } - private getAzureDevOpsPat(): Observable<string> { - return this.client.get('/api/TodoList/AzureDevOpsPat', { - headers: { - 'Accept': 'text/plain' - }, - responseType: 'text' - }); + private getAzureDevOpsPat(): Observable<AzureDevOpsAccessInfo> { + return this.client.get<AzureDevOpsAccessInfo>('/api/TodoPage/AzureDevOpsAccessInfo'); } getWorkItemList(): Observable<WorkItem[]> { return this.getAzureDevOpsPat().pipe( switchMap( - pat => { + accessInfo => { const headers = new HttpHeaders({ 'Accept': 'application/json', - 'Authorization': `Basic ${btoa(this.username + ':' + pat)}` + 'Authorization': `Basic ${btoa(accessInfo.username + ':' + accessInfo.personalAccessToken)}` }); return this.client.post<WiqlResult>( - `https://dev.azure.com/${this.organization}/${this.project}/_apis/wit/wiql?api-version=5.0`, { + `https://dev.azure.com/${accessInfo.organization}/${accessInfo.project}/_apis/wit/wiql?api-version=5.0`, { query: 'SELECT [System.Id] FROM workitems WHERE [System.TeamProject] = @project' }, { headers: headers }).pipe( switchMap(result => result.workItems), concatMap(result => this.client.get<WorkItemResult>(result.url, { headers: headers })), map(result => <WorkItem>{ id: result.id, - title: <string>result.fields[this.titleFieldName], - closed: ((<string>result.fields[this.stateFieldName]).toLowerCase() === 'closed') + title: <string>result.fields[TodoListService.titleFieldName], + closed: ((<string>result.fields[TodoListService.stateFieldName]).toLowerCase() === 'closed') }), toArray() ); diff --git a/Timeline/ClientApp/src/karma.conf.js b/Timeline/ClientApp/src/karma.conf.js index 4a9730b9..775e624c 100644 --- a/Timeline/ClientApp/src/karma.conf.js +++ b/Timeline/ClientApp/src/karma.conf.js @@ -10,6 +10,7 @@ module.exports = function (config) { require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), + require('karma-junit-reporter'), require('@angular-devkit/build-angular/plugins/karma') ], client: { diff --git a/Timeline/ClientApp/src/test.ts b/Timeline/ClientApp/src/test.ts index 16317897..2513deed 100644 --- a/Timeline/ClientApp/src/test.ts +++ b/Timeline/ClientApp/src/test.ts @@ -1,6 +1,8 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files +import 'zone.js/dist/zone-patch-rxjs-fake-async'; import 'zone.js/dist/zone-testing'; + import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, diff --git a/Timeline/Configs/TodoListConfig.cs b/Timeline/Configs/TodoListConfig.cs deleted file mode 100644 index a69e8d03..00000000 --- a/Timeline/Configs/TodoListConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Timeline.Configs -{ - public class TodoListConfig - { - public string AzureDevOpsPat { get; set; } - } -} diff --git a/Timeline/Configs/TodoPageConfig.cs b/Timeline/Configs/TodoPageConfig.cs new file mode 100644 index 00000000..d49e9dc3 --- /dev/null +++ b/Timeline/Configs/TodoPageConfig.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Configs +{ + public class AzureDevOpsAccessInfo + { + public string Username { get; set; } + public string PersonalAccessToken { get; set; } + public string Organization { get; set; } + public string Project { get; set; } + + } + + public class TodoPageConfig + { + public AzureDevOpsAccessInfo AzureDevOpsAccessInfo { get; set; } + } +} diff --git a/Timeline/Controllers/TodoListController.cs b/Timeline/Controllers/TodoListController.cs index b773ed2e..257c1d85 100644 --- a/Timeline/Controllers/TodoListController.cs +++ b/Timeline/Controllers/TodoListController.cs @@ -10,21 +10,20 @@ using Timeline.Configs; namespace Timeline.Controllers { [Route("api/[controller]")] - public class TodoListController : Controller + public class TodoPageController : Controller { - private readonly IOptionsMonitor<TodoListConfig> _config; + private readonly IOptionsMonitor<TodoPageConfig> _config; - public TodoListController(IOptionsMonitor<TodoListConfig> config) + public TodoPageController(IOptionsMonitor<TodoPageConfig> config) { _config = config; } [HttpGet("[action]")] [AllowAnonymous] - [Produces("text/plain")] - public ActionResult<string> AzureDevOpsPat() + public ActionResult<AzureDevOpsAccessInfo> AzureDevOpsAccessInfo() { - return Ok(_config.CurrentValue.AzureDevOpsPat); + return Ok(_config.CurrentValue.AzureDevOpsAccessInfo); } } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index a6bde7fd..e06a98d5 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -41,7 +41,7 @@ namespace Timeline }); - services.Configure<TodoListConfig>(Configuration.GetSection("TodoListConfig")); + services.Configure<TodoPageConfig>(Configuration.GetSection("TodoPageConfig")); services.Configure<JwtConfig>(Configuration.GetSection("JwtConfig")); var jwtConfig = Configuration.GetSection("JwtConfig").Get<JwtConfig>(); diff --git a/Timeline/appsettings.json b/Timeline/appsettings.json index a914603c..8dc28bc8 100644 --- a/Timeline/appsettings.json +++ b/Timeline/appsettings.json @@ -7,5 +7,11 @@ "JwtConfig": { "Issuer": "crupest.xyz", "Audience": "crupest.xyz" + }, + "TodoPageConfig": { + "AzureDevOpsAccessInfo": { + "Organization": "crupest-web", + "Project": "Timeline" + } } } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b67afdc2..4bbf12af 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,9 +3,6 @@ # Add steps that run tests, create a NuGet package, deploy, and more: # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core -trigger: -- master - pool: vmImage: 'Ubuntu-16.04' @@ -15,8 +12,10 @@ variables: steps: - script: dotnet build --configuration $(buildConfiguration) --configfile nuget.config + displayName: Dotnet Build - script: dotnet test Timeline.Tests --configuration $(buildConfiguration) --logger trx + displayName: Dotnet Test - task: PublishTestResults@2 condition: succeededOrFailed() @@ -24,7 +23,22 @@ steps: testRunner: VSTest testResultsFiles: '**/*.trx' +- script: yarn install + workingDirectory: Timeline/ClientApp + displayName: Yarn Install + +- script: yarn run test --no-watch --browsers=ChromeHeadless --reporters junit + workingDirectory: Timeline/ClientApp + displayName: Angular Test + +- task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testRunner: JUnit + testResultsFiles: '**/TESTS-*.xml' + - script: dotnet publish Timeline/Timeline.csproj --configuration $(buildConfiguration) --output ./publish/ + displayName: Dotnet Publish - task: PublishPipelineArtifact@0 inputs: |