diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index 276cf91fce..a668074a44 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -47,6 +47,7 @@ describe('AppComponent', () => { }); describe('isHamburgerVisible', () => { + console.log('PENDING: AppComponent isHamburgerVisible'); }); describe('onResize', () => { diff --git a/aio/src/app/embedded/api/api-list.component.spec.ts b/aio/src/app/embedded/api/api-list.component.spec.ts new file mode 100644 index 0000000000..a38d69e438 --- /dev/null +++ b/aio/src/app/embedded/api/api-list.component.spec.ts @@ -0,0 +1,291 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +import { ApiListComponent } from './api-list.component'; +import { ApiItem, ApiSection, ApiService } from './api.service'; +import { LocationService } from 'app/shared/location.service'; + +describe('ApiListComponent', () => { + let component: ApiListComponent; + let fixture: ComponentFixture; + let sections: ApiSection[]; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ApiListComponent ], + providers: [ + { provide: ApiService, useClass: TestApiService }, + { provide: LocationService, useClass: TestLocationService } + ] + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ApiListComponent); + component = fixture.componentInstance; + sections = getApiSections(); + }); + + it('should be creatable', () => { + expect(component).toBeDefined(); + }); + + /** + * Expectation Utility: Assert that filteredSections has the expected result for this test + * @param itemTest - return true if the item passes the match test + * + * Subscibes to `filteredSections` and performs expectation within subscription callback. + */ + function expectFilteredResult(label: string, itemTest: (item: ApiItem) => boolean) { + component.filteredSections.subscribe(filtered => { + let badItem: ApiItem; + expect(filtered.length).toBeGreaterThan(0, 'expected something'); + expect(filtered.every(section => section.items.every( + item => { + const ok = item.show === itemTest(item); + if (!ok) { badItem = item; } + return ok; + } + ))).toBe(true, `${label} fail: ${JSON.stringify(badItem, null, 2)}`); + }); + } + + describe('#filteredSections', () => { + + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should return all complete sections when no criteria', () => { + let filtered: ApiSection[]; + component.filteredSections.subscribe(f => filtered = f); + expect(filtered).toEqual(sections); + }); + + it('item.show should be true for all queried items', () => { + component.setQuery('class'); + expectFilteredResult('query: class', item => /class/.test(item.name)); + }); + + it('item.show should be true for every item in section when query matches section name', () => { + component.setQuery('core'); + component.filteredSections.subscribe(filtered => { + expect(filtered.length).toBe(1, 'only one section'); + expect(filtered[0].name).toBe('core'); + expect(filtered[0].items.every(item => item.show)).toBe(true, 'all core items shown'); + }); + }); + + it('item.show should be true for items with selected status', () => { + component.setStatus({name: 'stable', title: 'Stable'}); + expectFilteredResult('status: stable', item => item.stability === 'stable'); + }); + + it('item.show should be true for items with "security-risk" status when selected', () => { + component.setStatus({name: 'security-risk', title: 'Security Risk'}); + expectFilteredResult('status: security-risk', item => item.securityRisk); + }); + + it('item.show should be true for items of selected type', () => { + component.setType({name: 'class', title: 'Class'}); + expectFilteredResult('type: class', item => item.docType === 'class'); + }); + + it('should have no sections and no items when no match', () => { + component.setQuery('fizbuzz'); + component.filteredSections.subscribe(filtered => { + expect(filtered.length).toBe(0, 'expected no sections'); + }); + }); + }); + + describe('initial critera from location', () => { + let locationService: TestLocationService; + + beforeEach(() => { + locationService = fixture.componentRef.injector.get(LocationService); + }); + + function expectOneItem(name: string, section: string, type: string, stability: string) { + fixture.detectChanges(); + + component.filteredSections.subscribe(filtered => { + expect(filtered.length).toBe(1, 'sections'); + expect(filtered[0].name).toBe(section, 'section name'); + const items = filtered[0].items.filter(item => item.show); + expect(items.length).toBe(1, 'items'); + + const item = items[0]; + const badItem = 'Wrong item: ' + JSON.stringify(item, null, 2); + + expect(item.docType).toBe(type, badItem); + expect(item.stability).toBe(stability, badItem); + expect(item.name).toBe(name, badItem); + }); + } + + it('should filter as expected for ?query', () => { + locationService.query = {query: '_3'}; + expectOneItem('class_3', 'core', 'class', 'experimental'); + }); + + it('should filter as expected for ?status', () => { + locationService.query = {status: 'deprecated'}; + expectOneItem('function_1', 'core', 'function', 'deprecated'); + }); + + it('should filter as expected when status is security-risk', () => { + locationService.query = {status: 'security-risk'}; + fixture.detectChanges(); + expectFilteredResult('security-risk', item => item.securityRisk); + }); + + it('should filter as expected for ?type', () => { + locationService.query = {type: 'pipe'}; + expectOneItem('pipe_1', 'common', 'pipe', 'stable'); + }); + + it('should filter as expected for ?query&status&type', () => { + locationService.query = { + query: 's_1', + status: 'experimental', + type: 'class' + }; + fixture.detectChanges(); + expectOneItem('class_1', 'common', 'class', 'experimental'); + }); + + it('should ignore case for ?query&status&type', () => { + locationService.query = { + query: 'S_1', + status: 'ExperiMental', + type: 'CLASS' + }; + fixture.detectChanges(); + expectOneItem('class_1', 'common', 'class', 'experimental'); + }); + }); + + describe('location path after criteria change', () => { + let locationService: TestLocationService; + + beforeEach(() => { + locationService = fixture.componentRef.injector.get(LocationService); + }); + + it('should have query', () => { + component.setQuery('foo'); + + // `setSearch` 2nd param is a query/search params object + const search = locationService.setSearch.calls.mostRecent().args[1]; + expect(search.query).toBe('foo'); + }); + + it('should keep last of multiple query settings (in lowercase)', () => { + component.setQuery('foo'); + component.setQuery('fooBar'); + + const search = locationService.setSearch.calls.mostRecent().args[1]; + expect(search.query).toBe('foobar'); + }); + + it('should have query, status, and type', () => { + component.setQuery('foo'); + component.setStatus({name: 'stable', title: 'Stable'}); + component.setType({name: 'class', title: 'Class'}); + + const search = locationService.setSearch.calls.mostRecent().args[1]; + expect(search.query).toBe('foo'); + expect(search.status).toBe('stable'); + expect(search.type).toBe('class'); + }); + }); +}); + +////// Helpers //////// + +class TestLocationService { + query: {[index: string]: string } = {}; + setSearch = jasmine.createSpy('setSearch'); + search() { return this.query; } +} + +class TestApiService { + sectionsSubject = new BehaviorSubject(getApiSections()); + sections = this.sectionsSubject.asObservable(); +} + +// tslint:disable:quotemark +const apiSections: ApiSection[] = [ + { + "name": "common", + "title": "common", + "items": [ + { + "name": "class_1", + "title": "Class 1", + "path": "api/common/class_1", + "docType": "class", + "stability": "experimental", + "securityRisk": false + }, + { + "name": "class_2", + "title": "Class 2", + "path": "api/common/class_2", + "docType": "class", + "stability": "stable", + "securityRisk": false + }, + { + "name": "directive_1", + "title": "Directive 1", + "path": "api/common/directive_1", + "docType": "directive", + "stability": "stable", + "securityRisk": true + }, + { + "name": "pipe_1", + "title": "Pipe 1", + "path": "api/common/pipe_1", + "docType": "pipe", + "stability": "stable", + "securityRisk": true + }, + ] + }, + { + "name": "core", + "title": "core", + "items": [ + { + "name": "class_3", + "title": "Class 3", + "path": "api/core/class_3", + "docType": "class", + "stability": "experimental", + "securityRisk": false + }, + { + "name": "function_1", + "title": "Function 1", + "path": "api/core/function 1", + "docType": "function", + "stability": "deprecated", + "securityRisk": true + }, + { + "name": "const_1", + "title": "Const 1", + "path": "api/core/const_1", + "docType": "const", + "stability": "stable", + "securityRisk": false + } + ] + } +]; + +function getApiSections() { return apiSections; } diff --git a/aio/src/app/embedded/api/api.service.spec.ts b/aio/src/app/embedded/api/api.service.spec.ts new file mode 100644 index 0000000000..bcc27b2f46 --- /dev/null +++ b/aio/src/app/embedded/api/api.service.spec.ts @@ -0,0 +1,125 @@ +import { ReflectiveInjector } from '@angular/core'; +import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http'; +import { MockBackend, MockConnection } from '@angular/http/testing'; + +import { Logger } from 'app/shared/logger.service'; + +import { ApiService } from './api.service'; + +describe('ApiService', () => { + + let injector: ReflectiveInjector; + let service: ApiService; + let backend: MockBackend; + + function createResponse(body: any) { + return new Response(new ResponseOptions({ body: JSON.stringify(body) })); + } + + beforeEach(() => { + injector = ReflectiveInjector.resolveAndCreate([ + ApiService, + { provide: ConnectionBackend, useClass: MockBackend }, + { provide: RequestOptions, useClass: BaseRequestOptions }, + Http, + { provide: Logger, useClass: TestLogger } + ]); + }); + + beforeEach(() => { + backend = injector.get(ConnectionBackend); + service = injector.get(ApiService); + }); + + it('should be creatable', () => { + expect(service).toBeTruthy(); + }); + + it('should not immediately connect to the server', () => { + expect(backend.connectionsArray.length).toEqual(0); + }); + + it('subscribers should be completed/unsubscribed when service destroyed', () => { + let completed = false; + + service.sections.subscribe( + null, + null, + () => completed = true + ); + + service.ngOnDestroy(); + expect(completed).toBe(true); + }); + + describe('#sections', () => { + it('first subscriber should fetch sections', () => { + const data = [{name: 'a'}, {name: 'b'}]; + + service.sections.subscribe(sections => { + expect(sections).toEqual(data); + }); + + backend.connectionsArray[0].mockRespond(createResponse(data)); + }); + + it('second subscriber should get previous sections and NOT trigger refetch', () => { + const data = [{name: 'a'}, {name: 'b'}]; + let subscriptions = 0; + + service.sections.subscribe(sections => { + subscriptions++; + expect(sections).toEqual(data); + }); + + service.sections.subscribe(sections => { + subscriptions++; + expect(sections).toEqual(data); + }); + + backend.connectionsArray[0].mockRespond(createResponse(data)); + + expect(backend.connectionsArray.length).toBe(1, 'server connections'); + expect(subscriptions).toBe(2, 'subscriptions'); + }); + + }); + + describe('#fetchSections', () => { + + it('should connect to the server w/ expected URL', () => { + service.fetchSections(); + expect(backend.connectionsArray.length).toEqual(1); + expect(backend.connectionsArray[0].request.url).toEqual('content/docs/api/api-list.json'); + }); + + it('should refresh the #sections observable w/ new content on second call', () => { + + let call = 0; + let connection: MockConnection; + backend.connections.subscribe(c => connection = c); + + let data = [{name: 'a'}, {name: 'b'}]; + + service.sections.subscribe(sections => { + // called twice during this test + // (1) during subscribe + // (2) after refresh + expect(sections).toEqual(data, 'call ' + call++); + }); + connection.mockRespond(createResponse(data)); + + // refresh/refetch + data = [{name: 'c'}]; + service.fetchSections(); + connection.mockRespond(createResponse(data)); + + expect(call).toBe(2, 'should be called twice'); + }); + }); +}); + +class TestLogger { + log = jasmine.createSpy('log'); + error = jasmine.createSpy('error'); +}