Instantly share code, notes, and snippets.
Created
February 11, 2021 21:06
-
Star
0
(0)
You must be signed in to star a gist -
Fork
1
(1)
You must be signed in to fork a gist
-
Save LayZeeDK/7a020dcce4a228178afc68606dd50a02 to your computer and use it in GitHub Desktop.
Tane Piper: Auxiliary route test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HttpClientTestingModule } from '@angular/common/http/testing'; | |
import { Component, Injectable, Input, OnDestroy } from '@angular/core'; | |
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; | |
import { By } from '@angular/platform-browser'; | |
import { ActivatedRouteSnapshot, NavigationEnd, Route, Router } from '@angular/router'; | |
import { RouterTestingModule } from '@angular/router/testing'; | |
import { Subject } from 'rxjs'; | |
import { filter, map, takeUntil, tap } from 'rxjs/operators'; | |
interface Breadcrumb { | |
label: string; | |
pageTitle: string; | |
isLeaf: boolean; | |
url: string; | |
route: Route | null; | |
} | |
@Injectable({ | |
providedIn: 'root', | |
}) | |
export class BreadcrumbService { | |
/** | |
* Internal breadcrumb state | |
* @private | |
*/ | |
private currentBreadcrumb: Breadcrumb[] = []; | |
/** | |
* Check if the passed route is the the leaf of the breadcrumb | |
* @param route | |
*/ | |
static isLeaf(route: ActivatedRouteSnapshot): boolean { | |
return ( | |
route.firstChild === null || | |
route.firstChild.routeConfig === null || | |
!route.firstChild.routeConfig.path | |
); | |
} | |
/** | |
* Create a url string from a url segements | |
* @param route | |
*/ | |
static createUrl(route: ActivatedRouteSnapshot): string { | |
return route.url.map(s => s.toString()).join('/'); | |
} | |
/** | |
* Create a breadcrumb object | |
* @param route | |
* @param url | |
*/ | |
static createBreadcrumb( | |
route: ActivatedRouteSnapshot, | |
url: string | |
): Breadcrumb { | |
const { breadcrumbTitle, pageTitle } = route.data; | |
return { | |
label: breadcrumbTitle, | |
pageTitle: pageTitle || breadcrumbTitle, | |
isLeaf: BreadcrumbService.isLeaf(route), | |
url: url, | |
route: route.routeConfig, | |
}; | |
} | |
/** | |
* Take a router e | |
* @param router | |
* @private | |
*/ | |
public onRouterEvent(router: Router) { | |
let snapshot = router.routerState.root.snapshot; | |
let url = ''; | |
let breadCrumbIndex = 0; | |
const newCrumbs = []; | |
while (snapshot.firstChild != null) { | |
snapshot = snapshot.firstChild; | |
if ( | |
snapshot.routeConfig === null || | |
(snapshot.routeConfig && !snapshot.routeConfig.path) | |
) { | |
continue; | |
} | |
// Append URL | |
url += `/${BreadcrumbService.createUrl(snapshot)}`; | |
if (!snapshot.data.breadcrumbTitle) { | |
continue; | |
} | |
const newCrumb = BreadcrumbService.createBreadcrumb(snapshot, url); | |
if (breadCrumbIndex < this.currentBreadcrumb.length) { | |
const existing = this.currentBreadcrumb[breadCrumbIndex++]; | |
if (existing && existing.route == snapshot.routeConfig) { | |
newCrumb.label = existing.label; | |
} | |
} | |
newCrumbs.push(newCrumb); | |
} | |
this.currentBreadcrumb = newCrumbs; | |
return this.currentBreadcrumb; | |
} | |
} | |
@Component({ | |
// eslint-disable-next-line @angular-eslint/component-selector | |
selector: 'breadcrumb', | |
template: ` | |
<div class="container"> | |
<div class="nav-wrapper"> | |
<span class="breadcrumb root"> | |
<a [routerLink]="['/']" | |
><span>{{ rootLabel }}</span></a | |
> | |
</span> | |
<ng-container *ngFor="let route of breadcrumbs"> | |
<span class="breadcrumb" *ngIf="!route.isLeaf"> | |
{{ separator }} | |
<a [routerLink]="[route.url]" | |
><span>{{ route.label }}</span></a | |
> | |
</span> | |
<span class="breadcrumb leaf" *ngIf="route.isLeaf"> | |
{{ separator }} | |
<span>{{ route.label }}</span> | |
</span> | |
<div | |
class="page-title spot-typography__heading--level-2" | |
*ngIf="route.isLeaf && showTitle" | |
> | |
{{ route.pageTitle }} | |
</div> | |
</ng-container> | |
</div> | |
</div> | |
`, | |
styleUrls: [ | |
// './breadcrumb.component.scss' | |
], | |
}) | |
export class BreadcrumbComponent implements OnDestroy { | |
private destroy$ = new Subject(); | |
/** | |
* Label for the root | |
*/ | |
@Input() rootLabel = 'Home'; | |
@Input() separator = '/'; | |
@Input() showTitle = true; | |
breadcrumbs: Breadcrumb[] = []; | |
constructor( | |
private readonly router: Router, | |
private breadcrumbService: BreadcrumbService | |
) { | |
/** | |
* Due to the way router events work this *must* be in the constructor as ngOnInit is already | |
* too late in the life cycle | |
*/ | |
this.router.events | |
.pipe( | |
filter(event => event instanceof NavigationEnd), | |
map<NavigationEnd, Breadcrumb[]>(() => | |
this.breadcrumbService.onRouterEvent(this.router) | |
), | |
tap( | |
breadcrumbs => | |
Array.isArray(breadcrumbs) && (this.breadcrumbs = breadcrumbs) | |
), | |
takeUntil(this.destroy$) | |
) | |
.subscribe(); | |
} | |
ngOnDestroy() { | |
this.destroy$.next(); | |
this.destroy$.complete(); | |
} | |
} | |
describe('BreadcrumbComponent', () => { | |
@Component({ | |
template: ``, | |
}) | |
class MockPageComponent {} | |
@Component({ | |
template: ` <router-outlet name="testing"></router-outlet>`, | |
}) | |
class MockViewComponent {} | |
@Component({ | |
template: ` | |
<breadcrumb [rootLabel]="rootLabel" [showTitle]="showTitle"></breadcrumb> | |
<router-outlet></router-outlet> | |
`, | |
}) | |
class HostComponent { | |
rootLabel = 'Test Root Page'; | |
showTitle = true; | |
} | |
let component: HostComponent; | |
let fixture: ComponentFixture<HostComponent>; | |
let router: Router; | |
beforeEach(async () => { | |
await TestBed.configureTestingModule({ | |
imports: [ | |
HttpClientTestingModule, | |
RouterTestingModule.withRoutes([ | |
{ | |
path: 'page-1', | |
component: MockViewComponent, | |
data: { | |
breadcrumbTitle: 'Test Page Breadcrumb', | |
pageTitle: 'Test Page Title', | |
}, | |
children: [ | |
{ | |
path: 'sub-page-1', | |
component: MockPageComponent, | |
outlet: 'testing', | |
data: [ | |
{ | |
breadcrumbTitle: 'Test Sub-Page Breadcrumb', | |
pageTitle: 'Test Sub-Page Title', | |
}, | |
], | |
}, | |
{ | |
path: '', | |
pathMatch: 'full', | |
component: MockPageComponent, | |
outlet: 'testing', | |
}, | |
], | |
}, | |
{ | |
path: '', | |
pathMatch: 'full', | |
component: MockViewComponent, | |
}, | |
]), | |
], | |
declarations: [ | |
BreadcrumbComponent, | |
MockViewComponent, | |
MockPageComponent, | |
HostComponent, | |
], | |
}).compileComponents(); | |
}); | |
beforeEach(() => { | |
router = TestBed.inject(Router); | |
fixture = TestBed.createComponent(HostComponent); | |
component = fixture.componentInstance; | |
fixture.detectChanges(); | |
}); | |
afterEach(async () => { | |
await router.navigate(['']); | |
}); | |
it('should create the host component', () => { | |
expect(component).toBeTruthy(); | |
}); | |
it( | |
'should render the passed root label', | |
waitForAsync(async () => { | |
await router.navigate(['page-1']); | |
fixture.detectChanges(); | |
await fixture.whenStable(); | |
const rootLink = fixture.debugElement.query(By.css('.root a > span')); | |
expect(rootLink.nativeElement.textContent).toBe('Test Root Page'); | |
}) | |
); | |
/** | |
* We only go to page 1 in the tests and we cannot test with named outlets | |
*/ | |
it( | |
'should render the page breadcrumb as a leaf', | |
waitForAsync(async () => { | |
await router.navigate(['page-1']); | |
fixture.detectChanges(); | |
await fixture.whenStable(); | |
const leaf = fixture.debugElement.query(By.css('.leaf > span')); | |
expect(leaf.nativeElement.textContent).toBe('Test Page Breadcrumb'); | |
}) | |
); | |
it( | |
'should render the page title for a leaf', | |
waitForAsync(async () => { | |
await router.navigate(['page-1']); | |
fixture.detectChanges(); | |
await fixture.whenStable(); | |
const rootLink = fixture.debugElement.query(By.css('.page-title')); | |
expect(rootLink.nativeElement.textContent.trim()).toBe('Test Page Title'); | |
}) | |
); | |
it( | |
'should not render the page title for a leaf if showTitle is false', | |
waitForAsync(async () => { | |
component.showTitle = false; | |
await router.navigate(['page-1']); | |
fixture.detectChanges(); | |
await fixture.whenStable(); | |
const rootLink = fixture.debugElement.query(By.css('.page-title')); | |
expect(rootLink).toBeNull(); | |
}) | |
); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment