Challenge Overview
IMPORTANT NOTE:
We found out that members from certain countries are restricted from working on this challenge, due to the client’s company policies. Challenge submissions cannot be accepted from members registered in a country with a score of 37 or less according to Transparency International's Corruption Perception Index 2015.
If your country is highlighted in red in the following sheet your submission WILL NOT be accepted, unfortunately. This is NOT a new Topcoder policy or something that will become a normal part of future challenges. This is an isolated policy and requirement for this specific client and their challenges. Thank you for your understanding.
https://drive.google.com/file/d/0B9lpcmurgZ35WlkyWUV2OW9IRjQ/view
Welcome to the Document Management UI Unit Testing Challenge!
The goal of this challenge is to improve the unit testing coverage for a desktop rich HTML5 Prototype application using the AngularJS 2 development framework.
As part of this challenge you will need to document all unit testing code. We need clear explanation of the code to help us figure all the code functions and make it easier for future developers and the client to understand what you have built.
IMPORTANT!
- The existing tests are failing, these can be ignored. You are responsible to implement the unit tests for the models layer as outlined below.
- This application will eventually use a customised version of Bootstrap 3. Exact visual interpretation is less important that a functionally correct and elegant Angular component(s) and other code.
- Asking questions early and getting Copilot or PM's feedback is very important for the success of this challenge.
Source Code
The source code is provided in challenge forums.
Challenge Overview
The main task of this competition is to develop the unit test(s) for the AngularJS 2 functions / components provided. Your unit tests must run as part of all existing npm / gulp commands.
UI Prototype Page Requirements
Below are the requirements for this challenge.
Challenge Description
Component / Code Description
The ‘models layer’ for the current Document Management user interface is currently only unit tested indirectly in the application. We would like to be able to add unit testing to provide full coverage of all such calls.
The application has a ‘model layer’ to represent common models and objects used in the application. Most are simple (just constructors) but two in particular have more complex logic. It is these two which we request unit tests to be created for.
Angular Setup provided
- Custom cut-down AngularJS 2 application with gulp workflow, Karma / Jasmine / Istanbul unit testing and coverage.
- Model layer files, and all other dependent components.
Development Criteria
- Where applicable, meet criteria specified in General Coding Requirements and Coding Standards at the end of this document.
- A corresponding spec file for each of the document-result.model.ts and elastic-query.model.ts files provided:
…app/models/
document-result.model.ts
elastic-query.model.ts
document-result.model.spec.ts
elastic-query.model.spec.ts
- Unit testing file for the model for document results returning from ElasticSearch.
- Unit testing file for the model for representing an ElasticSearch query (and population of it).
- Any required external libraries or assets placed in the assets folder, amending SystemJS, index.html or other relevant files.
- Istanbul to report 100% coverage of these two files.
- Unit tests to execute using the following commands:
- npm start (where the spec file is included in /testsuite file)
- npm run testsuite –s (where the spec file is included in /testsuite file)
- npm test
- npm build
General Coding Requirements
All components (whether generic or route) will be in their own directory, containing html, ts, styl and spec files (see RULE 3 in Coding Standards).
.html file - template
- Provide comments on the page elements to give clear explanation of the code usage. The goal is to help future developers understand the code.
- Please use clean INDENTATION for all HTML code so future developers can follow the code.
- All HTML code naming should not have any conflicts or errors.
- Element and Attribute names should be in lowercase and use a "-" to separate multiple-word classes (e.g. "main-content")
- Use semantically correct tags- use H tags for headers, etc. Use strong and em tags instead of bold and italic tags.
- No inline CSS styles- all styles must be placed in the external css (Stylus) stylesheet.
- Validate your code - reviewers may accept minor validation errors, but please comment your reason for any validation errors. Use the validators listed in the scorecard.
.styl file - styling
- We use Stylus, which is transpiled to CSS within our gulp workflow.
- We also use a modified version of Bootstrap 3 for styling. Do not be too concerned about pixel perfect representation – this will be done by a separate User Experience team.
- Use CSS3 Media Queries to load different styles for each page and don't build different page for different device/layout.
- Provide comments on the CSS code. We need CSS comments to give a clear explanation of the code usage. The goal is to help future developers understand the code.
- Please use clean INDENTATION for all CSS so developers can follow the code.
- All CSS naming should not have any conflicts.
- As possible use CSS3 style when create all styling.
- You MUST use Stylus to make the CSS code clean, provide source files on your submission.
- Use CSS to space out objects, not clear/transparent images (GIFs or PNGs) and use proper structural CSS to lay out your page. Only use table tags for tables of data/information and not for page layout.
.ts file - Typescript
- We use TypeScript for our components / coding in AngularJS 2.
- A full list of coding standards is shown at the end of this document.
.spec file – Unit testing
- The spec file must provide full (100%) coverage as reported by Istanbul.
External Files – including Javascript libraries
- All external libraries must be loaded according to the instructions set out in DEVNOTES.md.
- All JavaScript, images or other must not have a copyright by a third party. You are encouraged to use your own scripts, or scripts that are free, publicly available and do not have copyright statements or author recognition requirements anywhere in the code.
Desktop Browsers Requirements
Windows & Mac OS
- Mandatory:
- IE11
- Chrome (version 50+)
- Optional:
- IE10
- Safari (version 7)
- Firefox (version 32)
Development Framework Requirements
- Bootstrap (version 3)
- AngularJS 2.0Final Submission Guidelines
Below is an overview of the deliverables:
- - All updated source code and tests that address the requirements.
- A complete and detailed deployment document explaining how to deploy the application including configuration information.
Final Submission
For each member, the final submission should be uploaded via the challenge detail page on topcoder.com.
Coding Standards:
RULE 1 - Listen to Paps
Where possible, adhere to https://angular.io/docs/ts/latest/guide/style-guide.html
If in any doubt of standards to follow, this style guide should provide guidance (unless a rule defined herein overrides it).
The following rules within this guide are mandatory:
- STYLE 03-01: Use upper camel case when naming classes;
- STYLE 03-03: Use upper camel case when naming interfaces;
- STYLE 03-04: Use lower camel case to name properties and methods;
- STYLE 05-12: Do use @Input and @Output instead of inputs and outputs properties.
RULE 2 - Organise imports alphabetically
import {addProviders, async, ComponentFixture, inject, TestComponentBuilder} from '@angular/core/testing';
import {HTTP_PROVIDERS, Response, ResponseOptions, XHRBackend} from '@angular/http';
import {MockBackend, MockConnection} from '@angular/http/testing';
import {By} from '@angular/platform-browser';
import {ActivatedRoute} from '@angular/router';
import {persons} from '../../../data/persons';
import {MockActivatedRoute} from '../../../mocks/mock-activated-route';
import {Route1DetailComponent} from './route1-detail.component';
import {PersonService} from '../../../services/person-service';
Order imports alphabetically by the string '@angular/core/testing' and ignore ../ when ordering.
Also order within the curlies {addProviders, async, ComponentFixture, inject, TestComponentBuilder}
RULE 3 - Follow our 5 tiered component structure
Given the following routing ..
export const routes:RouterConfig = [
{
path: 'route1',
component: Route1Component,
children: [
{path: ':id', component: Route1DetailComponent},
{path: '', component: Route1ListComponent}
]
},
{path: 'route2', component: Route2Component}
];
We require the following component folder structure.
- routes
- route1
- route1.component.ts|html|styl|spec (parent)
- detail
- route1-detail.component.ts|html|styl|spec (child)
- partial1
- route1-detail-partial1.component.ts|html|styl|spec (partial)
- partial2
- route1-detail-partial2.component.ts|html|styl|spec (partial)
- list
- route1-list.component.ts|html|styl|spec (child)
- route2
- route2.component.ts|html|styl|spec (parent)
We also employ the following component folder structure in respect of reusable components.
- apponents
- apponent1
- apponent1.component.ts|html|styl|spec (apponent)
- partial1
- apponent1-partial1.component.ts|html|styl|spec (partial)
- partial2
- apponent1-partial2.component.ts|html|styl|spec (partial)
- apponent2
- apponent2.component.ts|html|styl|spec (apponent)
- components
- my-component1
- my-component1.component.ts|html|styl|spec (reusable)
- partial1
- my-component1-partial1.component.ts|html|styl|spec (partial)
- partial2
- my-component1-partial2.component.ts|html|styl|spec (partial)
- my-component2
- my-component2.component.ts|html|styl|spec (reusable)
This structure reveals a 5 tiered component approach.
- parent - a non-reusable parent routing component.
- child - a non-reusable child routing component. These components are always nested within their parent.
- apponent - a component that offers application level reuse.
- reusable - a component that offers total reuse. Total reuse is defined as the level of reuse which would permit a component to form part of a public library.
- partial - a component that comprises part of another component. Used for organisational purposes. These components are always nested within their owning component.
RULE 4 - Data requests must be made in child components
Child components must make ALL data requests (eg HTTP requests) with 3 limited exceptions.
- A parent component may make a data request if it has no children.
- A partial component owned by a child component may make a data request.
- An apponent or reusable component should normally receive data as Angular2 @Input - this approach makes the component generic thereby extending reuse cases. There could however be a use case for making data requests here. For instance, consider a weather component. This may make a call to a public weather API and render the results accordingly. The API call here closely maps to the fundamental purpose of the component. A proposal for making a data request from an apponent or reusable component should be referred to project lead. One must consider whether public APIs are accessible in the production environment.
RULE 5 - Use RxJS Observables NOT promises
These support streaming and binding. For instance, you can use RxJS Observables to bind to a URL param.
When the URL param changes, the relevant parts of the view are updated without the need to completely recompile. This affords great speed rewards.
RxJS Observables are harder to get your head around but are worth the pain. An RxJS Observable can be thought of as a stream of promises. The .subscribe() method (equivalent to a promises .then() method) is potentially called many times, every time an event trickles down the stream.
export class Route1DetailComponent implements OnInit {
private DEFAULT_ID:number = 1;
private person:Person;
constructor(public route:ActivatedRoute, private personService:PersonService) {
}
ngOnInit() {
let paramsStream = this.route.params
.map((params) => parseInt(params['id'], 10))
.map((id) => !isNaN(id) ? id : this.DEFAULT_ID);
// We use flatMap instead of map to prevent this stream being a metastream - i.e. stream of streams
let responseStream = paramsStream.flatMap((id) => {
let personStream = this.personService.getPerson(id);
return personStream;
}).publish().refCount();
responseStream.subscribe((person:Person) => {
console.log(person);
this.person = person;
},
(err) => console.error('oops', err)
);
}
}
RxJS is reactive programming. Reactive programming means reacting to events emitted from streams.
To understand RxJS you must understand streams. A stream is a sequence just like an array. The one difference is that arrays are stored in memory. Streams are time based.
Please read this RxJS tutorial
FROM the heading Reactive programming is programming with asynchronous data streams
TO the text that says Since this feels so familiar already
In particular all developers must be conversant with ASCII stream diagrams from reading this tutorial.
Now discussing the above code ..
An RxJS Observable is a stream and has a .subscribe() method (just like promises have a .then() method)
Our first stream is paramsStream which is a stream of URL params and looks like ..
` paramsStream ------- {id:10} -------- {id:7} ------ {id:'x'} -------------- {id:20} ------->
We can .map() a stream exactly as we .map() an array. Our two .map() calls give ..
` .map() ------- 10 ------------- 7 ----------- NaN ------------------- 20 ------------>
` .map() ------- 10 ------------- 7 ----------- 1 --------------------- 20 ------------>
We now want to use the ID numbers being emitted from this stream to make API calls using personService.getPerson(id)
The problem is that personService.getPerson(id) returns a stream. Do we want responseStream to be a metastream viz a stream of streams? NO
If responseStream was a metastream it would emit streams but we want it to emit API responses.
To overcome this issue we simply use .flatMap() instead of .map()
Assuming that there is no person with ID 7 being served up by our API, we end up with ..
responseStream ------- person10 ------- undefined --- person1 --------------- person20 ------>
We can .subscribe() to any of our streams with ..
stream.subscribe(successFunc, errorFunc);
Compare that with promises.
promise.then(successFunc, errorFunc);
No wonder we liken RxJS Observables to streaming promises!
As a team we will refer to RxJS Observables as streams.
Please use RxJS wherever possible and try to think with a reactive mindset.
RULE 6 - Skinny controller, phat model
Controller code should be minimal and high level. A developer should be able to glance at controller code and know what it is doing immediately.
Data requests and heavy data processing should take place in an Angular2 service.
RULE 7 - Model transform
A models folder provides classes for converting, for example, API responses to TypeScript objects.
An overarching class should be used to store JSON responses. That class should then provide methods for returning refined / massaged results.
The preferred TypeScript approach here is use of the Interface Segregation Principle and Type Casting
Don't forget John Papa and his Rule of One!
RULE 8 - Use trackBy for iterators
When using *ngFor if you do not use trackBy then any changes to the list will cause all list DOM items to be reloaded. trackBy introduces intelligence, allowing Angular2 to only update those DOM elements bound to updated list items.
<div *ngFor="let person of persons; trackBy:myTrackingFunction"> // essential practice
<div *ngFor="let person of persons"> // extremely inefficient
For more see https://angular.io/docs/ts/latest/guide/template-syntax.html#!#ngfortrackby
RULE 9 - Naming conventions
class names use .. PascalCase AKA UpperCamelCase
function names use .. camelCase
filenames use .. spinal-case AKA kebab-case in conjunction with Papa John's feature.type.ext
- my-component1-partial1.component.ts|html|styl|spec
- my-http.service.ts
These naming conventions can AND WILL BE enforced by the TypeScript compiler!
RULE 10 - Template and Styles
Components can have an inline or external template and / or styles.
External is preferred but inline may be used where there are 3 or less lines of HTML or CSS.
Inline CSS must be pure CSS, inline Stylus is not supported.
@Component({
template: `<section class="greeting">hello</section>`,
styles: [`section.greeting { color:red }`]
})
External is de facto. External stylesheets may be written in Stylus. The styleUrls property must reference the .css extension.
@Component({
templateUrl: 'route1-detail.component.html',
styleUrls: ['route1-detail.component.css']
})
The /templates folder provides two template components, one using an external template, the other uses inline.
Copy and paste these as appropriate when creating a new component.
RULE 11 - Unit testing
All components must have a .spec.ts file containing unit tests.
RULE 13 - NEVER import 'rxjs/Rx'
Importing 'rxjs/Rx' imports the entire RxJS library, which is massive, and kills performance.
A developer must never do this. Rather import only the RxJS operators you need with ..?
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/publish';
RULE 14 - Understand providers and make them available at the right hierarchical level
In Angular1 each service provider provides a singleton instance.
In Angular2 each service provider provides a singleton instance BUT there can be numerous service providers at different hierarchical levels for the same service.
The following diagrams depict this.
Here the parent component provides a singleton instance to itself and all descendant components.
All components share the same singleton instance.
Here the parent component, as before, provides a singleton instance.
However, child 2 component provides a different singleton instance to itself and its descendants.
Different components have access to a different singleton instance.
Because of this behavior within Angular2 we require that
- Providers for stateless services be defined at any hierarchical level.
- Providers for stateful services be defined in the top level parent component only.
- Stateful services be avoided wherever possible. Please consult a team lead before developing a stateful service.
RULE 15 - Minimal constructor logic use ngOnInit
Angular2 docs advise to put minimal logic in the constructor.
export class MyClass implements OnInit {
constructor(private myApiService:MyApiService) {
}
ngOnInit() {
// Do initialisation here
// For example, API calls needed to render the page via MyApiService
}
}
This, as well as complying with Angular2 advice, gives us more control when unit testing since we can call ngOnInit() when we want.
RULE 16 - Component to component comms
Where two components exist on the same page they should communicate via a service. The service should expose an Rx.Subject() to facilitate comms.