Commit d6b2cbd2 authored by Matt Teeter's avatar Matt Teeter

Merge branch 'feature/browser-rx' into 'develop'

Feature/browser rx

See merge request !1
parents c5e07045 5a9ef924
Pipeline #74048 passed with stages
in 3 minutes and 50 seconds
# UtilsLib # Utils Library
This project was generated using [Nx](https://nx.dev). Monorepo for building-block type utilities that don't have dependencies on any other `@psu` libraries.
<p align="center"><img src="https://raw.githubusercontent.com/nrwl/nx/master/nx-logo.png" width="450"></p> - [Browser](https://git.psu.edu/ais-swe/ux/utils/tree/develop/libs/utils/browser)
- Cache Interceptor
- Autofocus Directive
- Error Service
- Loading Events Service
- Local Storage Service
- Request Tracing Interceptor
- UrlBuilder
- Window Service
- [Rx](https://git.psu.edu/ais-swe/ux/utils/tree/develop/libs/utils/rx)
- Http Retry Backoff Operator
- [Security](https://git.psu.edu/ais-swe/ux/utils/tree/develop/libs/utils/security)
- `REQUIRE_AUTH_HEADER` string constant
🔎 **Nx is a set of Angular CLI power-ups for modern development.** # Contributing
## Quick Start & Documentation Write small, portable utils, and try to keep the docs up to date as you add things.
[Nx Documentation](https://nx.dev) See the README here for how to create a new entry point https://git.psu.edu/ais-swe/ux/components/blob/develop/README.md
[30-minute video showing all Nx features](https://nx.dev/getting-started/what-is-nx)
[Interactive Tutorial](https://nx.dev/tutorial/01-create-application)
## Adding capabilities to your workspace
Nx supports many plugins which add capabilities for developing different types of applications and different tools.
These capabilities include generating applications, libraries, .etc as well as the devtools to test, and build projects as well.
Below are some plugins which you can add to your workspace:
- [Angular](https://angular.io)
- `ng add @nrwl/angular`
- [React](https://reactjs.org)
- `ng add @nrwl/react`
- Web (no framework frontends)
- `ng add @nrwl/web`
- [Nest](https://nestjs.com)
- `ng add @nrwl/nest`
- [Express](https://expressjs.com)
- `ng add @nrwl/express`
- [Node](https://nodejs.org)
- `ng add @nrwl/node`
## Generate an application
Run `ng g @nrwl/angular:app my-app` to generate an application.
> You can use any of the plugins above to generate applications as well.
When using Nx, you can create multiple applications and libraries in the same workspace.
## Generate a library
Run `ng g @nrwl/angular:lib my-lib` to generate a library.
> You can also use any of the plugins above to generate libraries as well.
Libraries are sharable across libraries and applications. They can be imported from `@utils-lib/mylib`.
## Development server
Run `ng serve my-app` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng g component my-component --project=my-app` to generate a new component.
## Build
Run `ng build my-app` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test my-app` to execute the unit tests via [Jest](https://jestjs.io).
Run `npm run affected:test` to execute the unit tests affected by a change.
## Running end-to-end tests
Run `ng e2e my-app` to execute the end-to-end tests via [Cypress](https://www.cypress.io).
Run `npm run affected:e2e` to execute the end-to-end tests affected by a change.
## Understand your workspace
Run `npm run dep-graph` to see a diagram of the dependencies of your projects.
## Further help
Visit the [Nx Documentation](https://nx.dev) to learn more.
...@@ -73,10 +73,7 @@ ...@@ -73,10 +73,7 @@
"lint": { "lint": {
"builder": "@angular-devkit/build-angular:tslint", "builder": "@angular-devkit/build-angular:tslint",
"options": { "options": {
"tsConfig": [ "tsConfig": ["apps/demo/tsconfig.app.json", "apps/demo/tsconfig.spec.json"],
"apps/demo/tsconfig.app.json",
"apps/demo/tsconfig.spec.json"
],
"exclude": ["**/node_modules/**"] "exclude": ["**/node_modules/**"]
} }
}, },
...@@ -132,10 +129,7 @@ ...@@ -132,10 +129,7 @@
"lint": { "lint": {
"builder": "@angular-devkit/build-angular:tslint", "builder": "@angular-devkit/build-angular:tslint",
"options": { "options": {
"tsConfig": [ "tsConfig": ["libs/utils/tsconfig.lib.json", "libs/utils/tsconfig.spec.json"],
"libs/utils/tsconfig.lib.json",
"libs/utils/tsconfig.spec.json"
],
"exclude": ["**/node_modules/**"] "exclude": ["**/node_modules/**"]
} }
}, },
...@@ -153,6 +147,90 @@ ...@@ -153,6 +147,90 @@
"styleext": "scss" "styleext": "scss"
} }
} }
},
"utils-security": {
"projectType": "library",
"root": "libs/utils/security",
"sourceRoot": "libs/utils/security/src",
"prefix": "psu",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/security/tsconfig.lib.json", "libs/utils/security/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/security/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/security/jest.config.js",
"tsConfig": "libs/utils/security/tsconfig.spec.json",
"setupFile": "libs/utils/security/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
},
"utils-rx": {
"projectType": "library",
"root": "libs/utils/rx",
"sourceRoot": "libs/utils/rx/src",
"prefix": "psu",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/rx/tsconfig.lib.json", "libs/utils/rx/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/rx/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/rx/jest.config.js",
"tsConfig": "libs/utils/rx/tsconfig.spec.json",
"setupFile": "libs/utils/rx/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
},
"utils-browser": {
"projectType": "library",
"root": "libs/utils/browser",
"sourceRoot": "libs/utils/browser/src",
"prefix": "psu",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/browser/tsconfig.lib.json", "libs/utils/browser/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/browser/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/browser/jest.config.js",
"tsConfig": "libs/utils/browser/tsconfig.spec.json",
"setupFile": "libs/utils/browser/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
} }
}, },
"cli": { "cli": {
...@@ -169,7 +247,10 @@ ...@@ -169,7 +247,10 @@
"e2eTestRunner": "cypress" "e2eTestRunner": "cypress"
}, },
"@nrwl/angular:library": { "@nrwl/angular:library": {
"unitTestRunner": "jest" "unitTestRunner": "jest",
"directory": "utils",
"simpleModuleName": true,
"style": "scss"
}, },
"@psu/schematics:ng-new": { "@psu/schematics:ng-new": {
"workspaceType": "library" "workspaceType": "library"
......
# utils
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `ng test utils` to execute the unit tests.
# Browser Utils
## Cache Interceptor
Adds an always different `_d` param to any request which has the `NO_CACHE_HEADER` present. This header is only used by the client as a marker, and is deleted within this interceptor
Must be registered at the same level as `HttpClientModule`
### Exports
- `CacheInterceptor`
- `const NO_CACHE_HEADER = 'X-No-Cache'`
## Focus Directive
Autofocuses the host element
```html
<input type="text" utAutoFocus />
```
## Error Service
Provides a facade for handling http errors
```typescript
import { HttpErrorService } from '@psu/utils/browser';
http.get('some-url').pipe(
map(() => {}),
catchError(e => this.httpErrorService.handleError(e))
);
```
## Loading Events Service
Provides method to trigger a DOM event outside of angular that can be picked up by a loading screen defined in index.html.
```typescript
import { LoadingEvents } from '@psu/utils/browser';
```
## Local Storage Service
Provides an abstraction layer over various local storage areas
```typescript
import { LocalStorageService } from '@psu/utils/browser';
```
## Request Tracing Interceptor
Provides a client-only header, `TRACE_HEADER` that will enable server side request tracing on any request with said header attached.
Must be registered at the same level as `HttpClientModule`
## UrlBuilder
A class which normalizes alot of the pain around consistently building URLs
```typescript
import { UrlBuilder } from '@psu/utils/browser';
```
## Window Service
An abstraction over the browser's native window object.
```typescript
import { WindowService } from '@psu/utils/browser';
```
module.exports = {
name: 'utils-browser',
preset: '../../../jest.config.js',
coverageDirectory: '../../../coverage/libs/utils/browser',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};
{
"lib": {
"entryFile": "src/index.ts"
}
}
export { CacheInterceptor } from './lib/cache/cache.interceptor';
export { FocusDirective } from './lib/focus/focus.directive';
export { FocusModule } from './lib/focus/focus.module';
export { HttpErrorService } from './lib/http-error/http-error.service';
export { LoadingEvents } from './lib/loading-screen/loading-events';
export { LocalStorageService } from './lib/local-storage/local-storage.service';
export { RequestTracingConfig } from './lib/tracing/request-tracing.config';
export { RequestTracingInterceptor } from './lib/tracing/request-tracing.interceptor';
export { UrlBuilder } from './lib/url-builder/url-builder';
export { WindowService } from './lib/window/window.service';
import { CacheInterceptor, NO_CACHE_HEADER } from './cache.interceptor';
import * as MockDate from 'mockdate';
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient, HttpRequest, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable()
class TestService {
private headers: HttpHeaders;
constructor(private httpClient: HttpClient) {
this.headers = new HttpHeaders().append(NO_CACHE_HEADER, 'true');
}
public getNoCache(): Observable<boolean> {
return this.httpClient.get<boolean>('busted', {
headers: this.headers.append('Another', 'abc')
});
}
public getNoCache2(): Observable<boolean> {
return this.httpClient.get<boolean>('busted2', {
headers: this.headers.append('Content-Type', 'junk'),
params: {
a: 'a',
b: 'b'
}
});
}
public get(): Observable<boolean> {
return this.httpClient.get<boolean>('normal');
}
}
describe('CacheInterceptor', () => {
let backend: HttpTestingController;
let service: TestService;
let now: Date;
beforeEach(() => {
now = new Date();
MockDate.set(now);
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [{ provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true }, TestService]
});
service = TestBed.get(TestService);
backend = TestBed.get(HttpTestingController);
});
describe('intercept', () => {
describe('when request has cache bust header', () => {
beforeEach(() => {
service.getNoCache().subscribe(r => {
expect(r).toBeTruthy();
});
});
it('should add cache busting query param', () => {
const req = backend.expectOne((r: HttpRequest<any>) => {
return r.url === 'busted';
});
expect(req.request.params.get('_d')).toEqual(`${now.getTime()}`);
expect(req.request.headers.has('X-No-Cache')).toBe(false);
expect(req.request.headers.get('Another')).toBe('abc');
});
});
describe('when request has cache bust header and other query params', () => {
beforeEach(() => {
service.getNoCache2().subscribe(r => {
expect(r).toBeTruthy();
});
});
it('should add cache busting query param', () => {
const req = backend.expectOne((r: HttpRequest<any>) => {
return r.url === 'busted2';
});
expect(req.request.params.get('_d')).toEqual(`${now.getTime()}`);
expect(req.request.params.get('a')).toEqual('a');
expect(req.request.params.get('b')).toEqual('b');
expect(req.request.headers.has('X-No-Cache')).toBe(false);
expect(req.request.headers.get('Content-Type')).toBe('junk');
});
});
describe('when request does not have cache bust header', () => {
beforeEach(() => {
service.get().subscribe(r => {
expect(r).toBeTruthy();
});
});
it('should not add cache busting query param', () => {
const req = backend.expectOne('normal');
expect(req.request.params.has('_d')).toBe(false);
expect(req.request.headers.has('X-No-Cache')).toBe(false);
});
});
});
});
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export const NO_CACHE_HEADER = 'X-No-Cache';
@Injectable({ providedIn: 'root' })
export class CacheInterceptor implements HttpInterceptor {
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.headers.has(NO_CACHE_HEADER)) {
const busted = req.clone({
setParams: {
_d: `${new Date().getTime()}`
},
headers: req.headers.delete(NO_CACHE_HEADER)
});
return next.handle(busted);
}
return next.handle(req);
}
}
import { CommonModule } from '@angular/common';
import { Component, PLATFORM_ID } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FocusModule } from './focus.module';
@Component({
selector: 'ut-test-comp',
template: `
<input id="inputId" utAutoFocus />
`
})
class TestComponent {}
describe('FocusDirective', () => {
let fixture: ComponentFixture<TestComponent>;
describe('when platform is browser', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CommonModule, FocusModule],
declarations: [TestComponent],
providers: [{ provide: PLATFORM_ID, useValue: 'browser' }]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should set focus', () => {
expect(document.activeElement.id).toEqual('inputId');
});
});
describe('when platform is server', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CommonModule, FocusModule],
declarations: [TestComponent],
providers: [{ provide: PLATFORM_ID, useValue: 'server' }]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should not set focus', () => {
expect(document.activeElement.id).not.toBe('inputId');
});
});
});
import { isPlatformBrowser } from '@angular/common';
import { AfterViewInit, Directive, ElementRef, Inject, PLATFORM_ID } from '@angular/core';
@Directive({
selector: '[utAutoFocus]'
})
export class FocusDirective implements AfterViewInit {
constructor(private element: ElementRef, @Inject(PLATFORM_ID) private platform: Object) {}
public ngAfterViewInit(): void {
if (isPlatformBrowser(this.platform)) {
this.element.nativeElement.focus();
}
}
}
import { NgModule } from '@angular/core';
import { FocusDirective } from './focus.directive';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [CommonModule],
declarations: [FocusDirective],
exports: [FocusDirective]
})
export class FocusModule {}
import { HttpErrorResponse } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { HttpErrorService } from './http-error.service';
describe('HttpErrorService', () => {
let service: HttpErrorService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [HttpErrorService]
});
service = TestBed.get(HttpErrorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('handleError', () => {
let result: string;
let emitted: boolean;