Skip to content

Instantly share code, notes, and snippets.

@tanepiper
Forked from LayZeeDK/breadcrumb.integration.spec.ts
Last active February 11, 2021 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tanepiper/80b65d498833e8ddf6d3c486d3c35553 to your computer and use it in GitHub Desktop.
Save tanepiper/80b65d498833e8ddf6d3c486d3c35553 to your computer and use it in GitHub Desktop.
Tane Piper: Auxiliary route test
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', { outlet: { testing: ['sub-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', { outlet: { testing: ['sub-page-1']}}]);
fixture.detectChanges();
await fixture.whenStable();
const leaf = fixture.debugElement.query(By.css('.leaf > span'));
expect(leaf.nativeElement.textContent).toBe('Test Sub-Page Breadcrumb');
})
);
it(
'should render the page title for a leaf',
waitForAsync(async () => {
await router.navigate(['page-1', { outlet: { testing: ['sub-page-1']}}]);
fixture.detectChanges();
await fixture.whenStable();
const rootLink = fixture.debugElement.query(By.css('.page-title'));
expect(rootLink.nativeElement.textContent.trim()).toBe('Test Sub-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