Files
angular__angular/packages/router/test/view_transitions.spec.ts
Andrew Scott 1da28f4825 feat(router): Add callback to execute when a view transition is created (#52002)
This commit adds the ability to provide a function that will get called
immediately after the view transition is created. This will allow
developers to do things like add/remove classes from the DOM when the
transition animation is finished, skip the transition based on
application conditions, etc. Having access to the transition unlocks
just about every example outlined in https://developer.chrome.com/docs/web-platform/view-transitions.

Note that the timing of the `updateCallback` execution is in the spec as
being called asyncronously (https://drafts.csswg.org/css-view-transitions-1/#callbackdef-updatecallback).
This means the `onViewTransitionCreated` callback is guaranteed to
execute before the update callback which in turn means it is guaranteed
to execute before the view transition `ready`/animation. As a result, it
is safe/effictive to add classes to the document in the
`onViewTransitionCreated` function in order to control animations of
that transition and then remove those classes in the transition's `finished`
`Promise`. The animation also doesn't start until the `Promise` returned
by `updateCallback` resolves, so this would also guarantee that the
animation starts asynchronously.

resolves #51827

PR Close #52002
2023-10-10 11:16:22 -07:00

110 lines
3.1 KiB
TypeScript

/**
* @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.io/license
*/
import {DOCUMENT} from '@angular/common';
import {Component, destroyPlatform, inject} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {withBody} from '@angular/private/testing';
import {Event, NavigationEnd, provideRouter, Router, withDisabledInitialNavigation, withViewTransitions} from '@angular/router';
describe('view transitions', () => {
if (isNode) {
it('are not available in node environment', () => {});
return;
}
beforeEach(destroyPlatform);
afterEach(destroyPlatform);
@Component({
selector: 'test-app',
standalone: true,
template: ``,
})
class App {
}
beforeEach(withBody('<test-app></test-app>', () => {}));
it('should skip initial transition', async () => {
const appRef = await bootstrapApplication(App, {
providers: [provideRouter(
[{path: '**', component: App}],
withDisabledInitialNavigation(),
withViewTransitions({skipInitialTransition: true}),
)]
});
const doc = appRef.injector.get(DOCUMENT);
if (!doc.startViewTransition) {
return;
}
const viewTransitionSpy = spyOn(doc, 'startViewTransition').and.callThrough();
await appRef.injector.get(Router).navigateByUrl('/a');
expect(viewTransitionSpy).not.toHaveBeenCalled();
await appRef.injector.get(Router).navigateByUrl('/b');
expect(viewTransitionSpy).toHaveBeenCalled();
});
it('should have the correct event order when using view transitions', async () => {
@Component({
selector: 'component-b',
template: `b`,
standalone: true,
})
class ComponentB {
}
const res = await bootstrapApplication(
App,
{providers: [provideRouter([{path: 'b', component: ComponentB}], withViewTransitions())]});
const router = res.injector.get(Router);
const eventLog = [] as Event[];
router.events.subscribe(e => {
eventLog.push(e);
});
await router.navigateByUrl('/b');
expect(eventLog[eventLog.length - 1]).toBeInstanceOf(NavigationEnd);
});
describe('onViewTransitionCreated option', () => {
it('should not create a view transition if only the fragment changes', async () => {
@Component({
selector: 'test-app',
standalone: true,
template: `{{checks}}`,
})
class App {
checks = 0;
ngDoCheck() {
this.checks++;
}
}
const transitionSpy = jasmine.createSpy();
const appRef = await bootstrapApplication(App, {
providers: [provideRouter(
[{path: '**', component: App}],
withDisabledInitialNavigation(),
withViewTransitions({onViewTransitionCreated: transitionSpy}),
)]
});
const doc = appRef.injector.get(DOCUMENT);
if (!doc.startViewTransition) {
return;
}
await appRef.injector.get(Router).navigateByUrl('/a');
expect(transitionSpy).toHaveBeenCalled();
});
});
});