/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/router/test/regression_integration.spec.ts
575 строк
17 KB
SkyZeroZx
test(router): move timeout and autoTick helpers to shared testing utilities
10 фев 2026, 18:45
10 фев 2026, 18:45
0f47fda
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {CommonModule, HashLocationStrategy, Location, LocationStrategy} from '@angular/common'; import {provideLocationMocks, SpyLocation} from '@angular/common/testing'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Injectable, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef, inject, signal, } from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import { ChildrenOutletContexts, DefaultUrlSerializer, NavigationCancel, NavigationError, Router, RouterModule, RouterOutlet, UrlSerializer, UrlTree, } from '../index'; import {of} from 'rxjs'; import {switchMap, filter, mapTo, take} from 'rxjs/operators'; import { provideRouter, withDisabledInitialNavigation, withRouterConfig, withViewTransitions, } from '../src/provide_router'; import {afterNextNavigation} from '../src/utils/navigations'; import {isBrowser, withBody, timeout} from '@angular/private/testing'; import {bootstrapApplication} from '@angular/platform-browser'; describe('Integration', () => { describe('routerLinkActive', () => { it('should update when the associated routerLinks change - #18469', async () => { @Component({ template: ` <a id="first-link" [routerLink]="[firstLink]" routerLinkActive="active">{{ firstLink }}</a> <div id="second-link" routerLinkActive="active"> <a [routerLink]="[secondLink]">{{ secondLink }}</a> </div> `, standalone: false, }) class LinkComponent { firstLink = 'link-a'; secondLink = 'link-b'; cdr = inject(ChangeDetectorRef); changeLinks(): void { const temp = this.secondLink; this.secondLink = this.firstLink; this.firstLink = temp; this.cdr.markForCheck(); } } @Component({ template: 'simple', standalone: false, }) class SimpleCmp {} TestBed.configureTestingModule({ imports: [ RouterModule.forRoot([ {path: 'link-a', component: SimpleCmp}, {path: 'link-b', component: SimpleCmp}, ]), ], declarations: [LinkComponent, SimpleCmp], }); const router: Router = TestBed.inject(Router); const fixture = await createRoot(router, LinkComponent); const firstLink = fixture.debugElement.query((p) => p.nativeElement.id === 'first-link'); const secondLink = fixture.debugElement.query((p) => p.nativeElement.id === 'second-link'); router.navigateByUrl('/link-a'); await advance(fixture); expect(firstLink.nativeElement.classList).toContain('active'); expect(secondLink.nativeElement.classList).not.toContain('active'); fixture.componentInstance.changeLinks(); fixture.detectChanges(); await advance(fixture); expect(firstLink.nativeElement.classList).not.toContain('active'); expect(secondLink.nativeElement.classList).toContain('active'); }); it('should not cause infinite loops in the change detection - #15825', async () => { @Component({ selector: 'simple', template: 'simple', standalone: false, }) class SimpleCmp {} @Component({ selector: 'some-root', template: ` <div *ngIf="show"> <ng-container *ngTemplateOutlet="tpl"></ng-container> </div> <router-outlet></router-outlet> <ng-template #tpl> <a routerLink="/simple" routerLinkActive="active"></a> </ng-template>`, standalone: false, }) class MyCmp { show: boolean = false; } @NgModule({ imports: [CommonModule, RouterModule.forRoot([])], declarations: [MyCmp, SimpleCmp], }) class MyModule {} TestBed.configureTestingModule({imports: [MyModule]}); const router: Router = TestBed.inject(Router); const fixture = await createRoot(router, MyCmp); router.resetConfig([{path: 'simple', component: SimpleCmp}]); router.navigateByUrl('/simple'); await advance(fixture); const instance = fixture.componentInstance; instance.show = true; expect(() => advance(fixture)).not.toThrow(); }); it('should set isActive right after looking at its children -- #18983', async () => { @Component({ template: ` <div #rla="routerLinkActive" routerLinkActive> isActive: {{ rla.isActive }} <ng-template let-data> <a [routerLink]="data">link</a> </ng-template> <ng-container #container></ng-container> </div> `, standalone: false, }) class ComponentWithRouterLink { @ViewChild(TemplateRef, {static: true}) templateRef?: TemplateRef<unknown>; @ViewChild('container', {read: ViewContainerRef, static: true}) container?: ViewContainerRef; addLink() { if (this.templateRef) { this.container?.createEmbeddedView(this.templateRef, {$implicit: '/simple'}); } } removeLink() { this.container?.clear(); } } @Component({ template: 'simple', standalone: false, }) class SimpleCmp {} TestBed.configureTestingModule({ imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])], declarations: [ComponentWithRouterLink, SimpleCmp], }); const router: Router = TestBed.inject(Router); const fixture = await createRoot(router, ComponentWithRouterLink); router.navigateByUrl('/simple'); await advance(fixture); fixture.componentInstance.addLink(); fixture.detectChanges(); fixture.componentInstance.removeLink(); await advance(fixture); await advance(fixture); expect(fixture.nativeElement.innerHTML).toContain('isActive: false'); }); it('should set isActive with OnPush change detection - #19934', async () => { @Component({ template: ` <div routerLink="/simple" #rla="routerLinkActive" routerLinkActive> isActive: {{ rla.isActive }} </div> `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, }) class OnPushComponent {} @Component({ template: 'simple', standalone: false, }) class SimpleCmp {} TestBed.configureTestingModule({ imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])], declarations: [OnPushComponent, SimpleCmp], }); const router = TestBed.inject(Router); const fixture = await createRoot(router, OnPushComponent); router.navigateByUrl('/simple'); await advance(fixture); expect(fixture.nativeElement.innerHTML).toContain('isActive: true'); }); }); it('should not reactivate a deactivated outlet when destroyed and recreated - #41379', async () => { @Component({ template: 'simple', standalone: false, }) class SimpleComponent {} @Component({ template: ` <router-outlet *ngIf="outletVisible" name="aux"></router-outlet> `, standalone: false, }) class AppComponent { outletVisible = true; } TestBed.configureTestingModule({ imports: [RouterModule.forRoot([{path: ':id', component: SimpleComponent, outlet: 'aux'}])], declarations: [SimpleComponent, AppComponent], }); const router = TestBed.inject(Router); const fixture = await createRoot(router, AppComponent); const componentCdr = fixture.componentRef.injector.get<ChangeDetectorRef>(ChangeDetectorRef); router.navigate([{outlets: {aux: ['1234']}}]); await advance(fixture); expect(fixture.nativeElement.innerHTML).toContain('simple'); router.navigate([{outlets: {aux: null}}]); await advance(fixture); expect(fixture.nativeElement.innerHTML).not.toContain('simple'); fixture.componentInstance.outletVisible = false; componentCdr.detectChanges(); expect(fixture.nativeElement.innerHTML).not.toContain('simple'); expect(fixture.nativeElement.innerHTML).not.toContain('router-outlet'); fixture.componentInstance.outletVisible = true; componentCdr.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain('router-outlet'); expect(fixture.nativeElement.innerHTML).not.toContain('simple'); }); describe('useHash', () => { it('should restore hash to match current route - #28561', async () => { @Component({ selector: 'root-cmp', template: `<router-outlet></router-outlet>`, standalone: false, }) class RootCmp {} @Component({ template: 'simple', standalone: false, }) class SimpleCmp {} @Component({ template: 'one', standalone: false, }) class OneCmp {} TestBed.configureTestingModule({ imports: [ RouterModule.forRoot([ {path: '', component: SimpleCmp}, {path: 'one', component: OneCmp, canActivate: [() => inject(Router).parseUrl('/')]}, ]), ], declarations: [SimpleCmp, RootCmp, OneCmp], providers: [provideLocationMocks()], }); const router = TestBed.inject(Router); const location = TestBed.inject(Location) as SpyLocation; router.navigateByUrl('/'); // Will setup location change listeners const fixture = await createRoot(router, RootCmp); location.simulateHashChange('/one'); await advance(fixture); expect(location.path()).toEqual('/'); expect(fixture.nativeElement.innerHTML).toContain('one'); }); }); describe('duplicate navigation handling (#43447, #43446)', () => { let location: Location; let router: Router; let fixture: ComponentFixture<{}>; beforeEach(async () => { @Injectable() class DelayedResolve { resolve() { return of('').pipe( switchMap((v) => new Promise((r) => setTimeout(r, 10)).then(() => v)), mapTo(true), ); } } @Component({ selector: 'root-cmp', template: `<router-outlet></router-outlet>`, standalone: false, }) class RootCmp {} @Component({ template: 'simple', standalone: false, }) class SimpleCmp {} @Component({ template: 'one', standalone: false, }) class OneCmp {} TestBed.configureTestingModule({ declarations: [SimpleCmp, RootCmp, OneCmp], imports: [RouterOutlet], providers: [ DelayedResolve, provideLocationMocks(), provideRouter([ {path: '', component: SimpleCmp}, {path: 'one', component: OneCmp, resolve: {x: DelayedResolve}}, ]), {provide: LocationStrategy, useClass: HashLocationStrategy}, ], }); router = TestBed.inject(Router); location = TestBed.inject(Location); router.navigateByUrl('/'); // Will setup location change listeners fixture = await createRoot(router, RootCmp); }); it('duplicate navigation to same url', async () => { location.go('/one'); await timeout(1); location.go('/one'); await timeout(10); await advance(fixture); expect(location.path()).toEqual('/one'); expect(fixture.nativeElement.innerHTML).toContain('one'); }); it('works with a duplicate popstate/hashchange navigation (as seen in firefox)', async () => { (location as any)._subject.next({'url': 'one', 'pop': true, 'type': 'popstate'}); await timeout(1); (location as any)._subject.next({'url': 'one', 'pop': true, 'type': 'hashchange'}); await timeout(10); await advance(fixture); expect(router.routerState.toString()).toContain(`url:'one'`); expect(fixture.nativeElement.innerHTML).toContain('one'); }); }); it('should not unregister outlet if a different one already exists #36711, 32453', async () => { @Component({ template: ` <router-outlet *ngIf="outlet1()"></router-outlet> <router-outlet *ngIf="outlet2()"></router-outlet> `, standalone: false, }) class TestCmp { outlet1 = signal(true); outlet2 = signal(false); } @Component({ template: '', standalone: false, }) class EmptyCmp {} TestBed.configureTestingModule({ imports: [CommonModule, RouterModule.forRoot([{path: '**', component: EmptyCmp}])], declarations: [TestCmp, EmptyCmp], }); const fixture = TestBed.createComponent(TestCmp); const contexts = TestBed.inject(ChildrenOutletContexts); await TestBed.inject(Router).navigateByUrl('/'); fixture.detectChanges(); expect(contexts.getContext('primary')).toBeDefined(); expect(contexts.getContext('primary')?.outlet).not.toBeNull(); // Show the second outlet. Applications shouldn't really have more than one outlet but there can // be timing issues between destroying and recreating a second one in some cases: // https://github.com/angular/angular/issues/36711, // https://github.com/angular/angular/issues/32453 fixture.componentInstance.outlet2.set(true); fixture.detectChanges(); expect(contexts.getContext('primary')?.outlet).not.toBeNull(); fixture.componentInstance.outlet1.set(false); fixture.detectChanges(); // Destroying the first one show not clear the outlet context because the second one takes over // as the registered outlet. expect(contexts.getContext('primary')?.outlet).not.toBeNull(); }); it('should respect custom serializer all the way to the final url on state', async () => { const QUERY_VALUE = {user: 'atscott'}; const SPECIAL_SERIALIZATION = 'special'; class CustomSerializer extends DefaultUrlSerializer { override serialize(tree: UrlTree): string { const mutableCopy = new UrlTree(tree.root, {...tree.queryParams}, tree.fragment); mutableCopy.queryParams['q'] &&= SPECIAL_SERIALIZATION; return new DefaultUrlSerializer().serialize(mutableCopy); } } TestBed.configureTestingModule({ providers: [provideRouter([]), {provide: UrlSerializer, useValue: new CustomSerializer()}], }); const router = TestBed.inject(Router); const tree = router.createUrlTree([]); tree.queryParams = {q: QUERY_VALUE}; await router.navigateByUrl(tree); expect(router.url).toEqual(`/?q=${SPECIAL_SERIALIZATION}`); }); it('navigation works when a redirecting NavigationCancel event causes another synchronous navigation', async () => { TestBed.configureTestingModule({ providers: [ provideRouter( [ {path: 'a', children: []}, {path: 'b', children: []}, {path: 'c', children: []}, ], withRouterConfig({resolveNavigationPromiseOnError: true}), ), ], }); let errors: NavigationError[] = []; let cancellations: NavigationCancel[] = []; const router = TestBed.inject(Router); router.events .pipe(filter((e): e is NavigationError => e instanceof NavigationError)) .subscribe((e) => errors.push(e)); router.events .pipe(filter((e): e is NavigationCancel => e instanceof NavigationCancel)) .subscribe((e) => cancellations.push(e)); router.events .pipe( filter((e) => e instanceof NavigationCancel), take(1), ) .subscribe(() => { router.navigateByUrl('/c'); }); router.navigateByUrl('/a'); router.navigateByUrl('/b'); await new Promise<void>((resolve) => afterNextNavigation(router, resolve)); expect(router.url).toEqual('/c'); expect(errors).toEqual([]); // navigations to a and b were both cancelled. expect(cancellations.length).toEqual(2); }); if (isBrowser) { it( 'should allow navigating during template execution', withBody('<test-app></test-app>', async () => { // Note this might not be a behavior that we want to keep, but it is currently relied on by some targets on G3 @Component({ template: 'simple', }) class SimpleCmp {} @Component({ selector: 'test-app', template: `<router-outlet /> {{ sideEffectWithRedirection() }} `, imports: [RouterOutlet], }) class App { navigate = true; constructor(private router: Router) {} sideEffectWithRedirection() { if (this.navigate) { this.router.navigateByUrl('/simple'); this.navigate = false; } } } const appRef = await bootstrapApplication(App, { providers: [ provideRouter( [{path: 'simple', component: SimpleCmp}], withDisabledInitialNavigation(), withViewTransitions({skipInitialTransition: true}), ), ], }); await appRef.injector.get(Router).navigateByUrl('/simple'); await expectAsync(appRef.whenStable()).toBeResolved(); }), ); } }); async function advance<T>(fixture: ComponentFixture<T>): Promise<void> { await timeout(); fixture.detectChanges(); } function createRoot<T>(router: Router, type: Type<T>): ComponentFixture<T> { const f = TestBed.createComponent(type); advance(f); router.initialNavigation(); advance(f); return f; }