mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(core): feature for potential zoneless-compatibility debug check (#55663)
This commit adds a feature that is useful for determining if an application is zoneless-ready. The way this works is generally only useful right now when zoneless is enabled. Some version of this may be useful in the future as a general configuration option to change detection to make `checkNoChanges` pass always exhaustive as an opt-in to address #45612. Because this is an experimental, debug-only feature, it is okay to merge during the RC period. PR Close #55663
This commit is contained in:
parent
abbaf8f639
commit
ca6cdcd269
12 changed files with 487 additions and 39 deletions
|
|
@ -28,28 +28,6 @@ platformBrowser().bootstrapModule(AppModule, {ngZone: 'noop'});
|
|||
export class AppModule {}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The zoneless provider function can also be used with `TestBed` to help
|
||||
ensure the components under test are compatible with a Zoneless
|
||||
Angular application.
|
||||
|
||||
```typescript
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideExperimentalZonelessChangeDetection()]
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
await fixture.whenStable();
|
||||
```
|
||||
|
||||
To ensure tests have the most similar behavior to production code,
|
||||
avoid using `fixture.detectChanges()` when possibe. This forces
|
||||
change detection to run when Angular might otherwise have not
|
||||
scheduled change detection. Tests should ensure these notifications
|
||||
are happening and allow Angular to handle when to synchronize
|
||||
state rather than manually forcing it to happen in the test.
|
||||
|
||||
## Requirements for Zoneless compatibility
|
||||
|
||||
Angular relies on notifications from core APIs in order to determine when to run change detection and on which views.
|
||||
|
|
@ -106,3 +84,35 @@ taskCleanup();
|
|||
The framework uses this service internally as well to prevent serialization until asynchronous tasks are complete. These include, but are not limited to,
|
||||
an ongoing Router navigation and an incomplete `HttpClient` request.
|
||||
|
||||
## Testing and Debugging
|
||||
|
||||
### Using Zoneless in `TestBed`
|
||||
|
||||
The zoneless provider function can also be used with `TestBed` to help
|
||||
ensure the components under test are compatible with a Zoneless
|
||||
Angular application.
|
||||
|
||||
```typescript
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideExperimentalZonelessChangeDetection()]
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyComponent);
|
||||
await fixture.whenStable();
|
||||
```
|
||||
|
||||
To ensure tests have the most similar behavior to production code,
|
||||
avoid using `fixture.detectChanges()` when possibe. This forces
|
||||
change detection to run when Angular might otherwise have not
|
||||
scheduled change detection. Tests should ensure these notifications
|
||||
are happening and allow Angular to handle when to synchronize
|
||||
state rather than manually forcing it to happen in the test.
|
||||
|
||||
### Debug-mode check to ensure updates are detected
|
||||
|
||||
Angular also provides an additional tool to help verify that an application is making
|
||||
updates to state in a zoneless-compatible way. `provideExperimentalCheckNoChangesForDebug`
|
||||
can be used to periodically check to ensure that no bindings have been updated
|
||||
without a notification. Angular will throw `ExpressionChangedAfterItHasBeenCheckedError`
|
||||
if there is an updated binding that would not have refreshed by the zoneless change
|
||||
detection.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
```ts
|
||||
|
||||
import { EnvironmentProviders as EnvironmentProviders_2 } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SIGNAL } from '@angular/core/primitives/signals';
|
||||
import { SignalNode } from '@angular/core/primitives/signals';
|
||||
|
|
@ -1365,6 +1366,13 @@ export class PlatformRef {
|
|||
// @public
|
||||
export type Predicate<T> = (value: T) => boolean;
|
||||
|
||||
// @public
|
||||
export function provideExperimentalCheckNoChangesForDebug(options: {
|
||||
interval?: number;
|
||||
useNgZoneOnStable?: boolean;
|
||||
exhaustive?: boolean;
|
||||
}): EnvironmentProviders_2;
|
||||
|
||||
// @public
|
||||
export function provideExperimentalZonelessChangeDetection(): EnvironmentProviders;
|
||||
|
||||
|
|
|
|||
|
|
@ -312,7 +312,8 @@ export class ApplicationRef {
|
|||
private beforeRender = new Subject<boolean>();
|
||||
/** @internal */
|
||||
afterTick = new Subject<void>();
|
||||
private get allViews() {
|
||||
/** @internal */
|
||||
get allViews() {
|
||||
return [...this.externalTestViews.keys(), ...this._views];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* @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 {ApplicationRef} from '../../application/application_ref';
|
||||
import {ChangeDetectionSchedulerImpl} from './zoneless_scheduling_impl';
|
||||
import {inject} from '../../di/injector_compatibility';
|
||||
import {makeEnvironmentProviders} from '../../di/provider_collection';
|
||||
import {NgZone} from '../../zone/ng_zone';
|
||||
|
||||
import {EnvironmentInjector} from '../../di/r3_injector';
|
||||
import {ENVIRONMENT_INITIALIZER} from '../../di/initializer_token';
|
||||
import {CheckNoChangesMode} from '../../render3/state';
|
||||
import {ErrorHandler} from '../../error_handler';
|
||||
import {checkNoChangesInternal} from '../../render3/instructions/change_detection';
|
||||
import {ZONELESS_ENABLED} from './zoneless_scheduling';
|
||||
|
||||
/**
|
||||
* Used to periodically verify no expressions have changed after they were checked.
|
||||
*
|
||||
* @param options Used to configure when the check will execute.
|
||||
* - `interval` will periodically run exhaustive `checkNoChanges` on application views
|
||||
* - `useNgZoneOnStable` will us ZoneJS to determine when change detection might have run
|
||||
* in an application using ZoneJS to drive change detection. When the `NgZone.onStable` would
|
||||
* have emit, all views attached to the `ApplicationRef` are checked for changes.
|
||||
* - 'exhaustive' means that all views attached to `ApplicationRef` and all the descendants of those views will be
|
||||
* checked for changes (excluding those subtrees which are detached via `ChangeDetectorRef.detach()`).
|
||||
* This is useful because the check that runs after regular change detection does not work for components using `ChangeDetectionStrategy.OnPush`.
|
||||
* This check is will surface any existing errors hidden by `OnPush` components. By default, this check is exhaustive
|
||||
* and will always check all views, regardless of their "dirty" state and `ChangeDetectionStrategy`.
|
||||
*
|
||||
* When the `useNgZoneOnStable` option is `true`, this function will provide its own `NgZone` implementation and needs
|
||||
* to come after any other `NgZone` provider, including `provideZoneChangeDetection()` and `provideExperimentalZonelessChangeDetection()`.
|
||||
*
|
||||
* @experimental
|
||||
* @publicApi
|
||||
*/
|
||||
export function provideExperimentalCheckNoChangesForDebug(options: {
|
||||
interval?: number;
|
||||
useNgZoneOnStable?: boolean;
|
||||
exhaustive?: boolean;
|
||||
}) {
|
||||
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
||||
if (options.interval === undefined && !options.useNgZoneOnStable) {
|
||||
throw new Error('Must provide one of `useNgZoneOnStable` or `interval`');
|
||||
}
|
||||
const checkNoChangesMode =
|
||||
options?.exhaustive === false
|
||||
? CheckNoChangesMode.OnlyDirtyViews
|
||||
: CheckNoChangesMode.Exhaustive;
|
||||
return makeEnvironmentProviders([
|
||||
options?.useNgZoneOnStable
|
||||
? {provide: NgZone, useFactory: () => new DebugNgZoneForCheckNoChanges(checkNoChangesMode)}
|
||||
: [],
|
||||
options?.interval !== undefined
|
||||
? exhaustiveCheckNoChangesInterval(options.interval, checkNoChangesMode)
|
||||
: [],
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
if (
|
||||
options?.useNgZoneOnStable &&
|
||||
!(inject(NgZone) instanceof DebugNgZoneForCheckNoChanges)
|
||||
) {
|
||||
throw new Error(
|
||||
'`provideCheckNoChangesForDebug` with `useNgZoneOnStable` must be after any other provider for `NgZone`.',
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
return makeEnvironmentProviders([]);
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugNgZoneForCheckNoChanges extends NgZone {
|
||||
private applicationRef?: ApplicationRef;
|
||||
private scheduler?: ChangeDetectionSchedulerImpl;
|
||||
private errorHandler?: ErrorHandler;
|
||||
private readonly injector = inject(EnvironmentInjector);
|
||||
|
||||
constructor(private readonly checkNoChangesMode: CheckNoChangesMode) {
|
||||
const zonelessEnabled = inject(ZONELESS_ENABLED);
|
||||
// Use coalsecing to ensure we aren't ever running this check synchronously
|
||||
super({
|
||||
shouldCoalesceEventChangeDetection: true,
|
||||
shouldCoalesceRunChangeDetection: zonelessEnabled,
|
||||
});
|
||||
|
||||
if (zonelessEnabled) {
|
||||
// prevent emits to ensure code doesn't rely on these
|
||||
this.onMicrotaskEmpty.emit = () => {};
|
||||
this.onStable.emit = () => {
|
||||
this.scheduler ||= this.injector.get(ChangeDetectionSchedulerImpl);
|
||||
if (this.scheduler.pendingRenderTaskId || this.scheduler.runningTick) {
|
||||
return;
|
||||
}
|
||||
this.checkApplicationViews();
|
||||
};
|
||||
this.onUnstable.emit = () => {};
|
||||
} else {
|
||||
this.runOutsideAngular(() => {
|
||||
this.onStable.subscribe(() => {
|
||||
this.checkApplicationViews();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private checkApplicationViews() {
|
||||
this.applicationRef ||= this.injector.get(ApplicationRef);
|
||||
for (const view of this.applicationRef.allViews) {
|
||||
try {
|
||||
checkNoChangesInternal(view._lView, this.checkNoChangesMode, view.notifyErrorHandler);
|
||||
} catch (e) {
|
||||
this.errorHandler ||= this.injector.get(ErrorHandler);
|
||||
this.errorHandler.handleError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exhaustiveCheckNoChangesInterval(
|
||||
interval: number,
|
||||
checkNoChangesMode: CheckNoChangesMode,
|
||||
) {
|
||||
return {
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useFactory: () => {
|
||||
const applicationRef = inject(ApplicationRef);
|
||||
const errorHandler = inject(ErrorHandler);
|
||||
const scheduler = inject(ChangeDetectionSchedulerImpl);
|
||||
const ngZone = inject(NgZone);
|
||||
|
||||
return () => {
|
||||
function scheduleCheckNoChanges() {
|
||||
ngZone.runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
if (applicationRef.destroyed) {
|
||||
return;
|
||||
}
|
||||
if (scheduler.pendingRenderTaskId || scheduler.runningTick) {
|
||||
scheduleCheckNoChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const view of applicationRef.allViews) {
|
||||
try {
|
||||
checkNoChangesInternal(view._lView, checkNoChangesMode, view.notifyErrorHandler);
|
||||
} catch (e) {
|
||||
errorHandler.handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleCheckNoChanges();
|
||||
}, interval);
|
||||
});
|
||||
}
|
||||
scheduleCheckNoChanges();
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ export class NgZoneChangeDetectionScheduler {
|
|||
private readonly zone = inject(NgZone);
|
||||
private readonly changeDetectionScheduler = inject(ChangeDetectionScheduler, {optional: true});
|
||||
private readonly applicationRef = inject(ApplicationRef);
|
||||
private readonly zonelessEnabled = inject(ZONELESS_ENABLED);
|
||||
|
||||
private _onMicrotaskEmptySubscription?: Subscription;
|
||||
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
|||
|
||||
private cancelScheduledCallback: null | (() => void) = null;
|
||||
private shouldRefreshViews = false;
|
||||
private pendingRenderTaskId: number | null = null;
|
||||
private useMicrotaskScheduler = false;
|
||||
runningTick = false;
|
||||
pendingRenderTaskId: number | null = null;
|
||||
|
||||
constructor() {
|
||||
this.subscriptions.add(
|
||||
|
|
@ -175,7 +175,7 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
|||
}
|
||||
// If we're inside the zone don't bother with scheduler. Zone will stabilize
|
||||
// eventually and run change detection.
|
||||
if (this.zoneIsDefined && NgZone.isInAngularZone()) {
|
||||
if (!this.zonelessEnabled && this.zoneIsDefined && NgZone.isInAngularZone()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export {
|
|||
} from './change_detection/scheduling/ng_zone_scheduling';
|
||||
export {provideExperimentalZonelessChangeDetection} from './change_detection/scheduling/zoneless_scheduling_impl';
|
||||
export {ExperimentalPendingTasks} from './pending_tasks';
|
||||
export {provideExperimentalCheckNoChangesForDebug} from './change_detection/scheduling/exhaustive_check_no_changes';
|
||||
export {enableProdMode, isDevMode} from './util/is_dev_mode';
|
||||
export {
|
||||
APP_ID,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ import {
|
|||
ReactiveLViewConsumer,
|
||||
} from '../reactive_lview_consumer';
|
||||
import {
|
||||
CheckNoChangesMode,
|
||||
enterView,
|
||||
isExhaustiveCheckNoChanges,
|
||||
isInCheckNoChangesMode,
|
||||
isRefreshingViews,
|
||||
leaveView,
|
||||
|
|
@ -143,12 +145,16 @@ function detectChangesInViewWhileDirty(lView: LView, mode: ChangeDetectionMode)
|
|||
}
|
||||
}
|
||||
|
||||
export function checkNoChangesInternal(lView: LView, notifyErrorHandler = true) {
|
||||
setIsInCheckNoChangesMode(true);
|
||||
export function checkNoChangesInternal(
|
||||
lView: LView,
|
||||
mode: CheckNoChangesMode,
|
||||
notifyErrorHandler = true,
|
||||
) {
|
||||
setIsInCheckNoChangesMode(mode);
|
||||
try {
|
||||
detectChangesInternal(lView, notifyErrorHandler);
|
||||
} finally {
|
||||
setIsInCheckNoChangesMode(false);
|
||||
setIsInCheckNoChangesMode(CheckNoChangesMode.Off);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,12 +335,13 @@ export function refreshView<T>(
|
|||
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
|
||||
}
|
||||
} catch (e) {
|
||||
// If refreshing a view causes an error, we need to remark the ancestors as needing traversal
|
||||
// because the error might have caused a situation where views below the current location are
|
||||
// dirty but will be unreachable because the "has dirty children" flag in the ancestors has been
|
||||
// cleared during change detection and we failed to run to completion.
|
||||
|
||||
markAncestorsForTraversal(lView);
|
||||
if (!isInCheckNoChangesPass) {
|
||||
// If refreshing a view causes an error, we need to remark the ancestors as needing traversal
|
||||
// because the error might have caused a situation where views below the current location are
|
||||
// dirty but will be unreachable because the "has dirty children" flag in the ancestors has been
|
||||
// cleared during change detection and we failed to run to completion.
|
||||
markAncestorsForTraversal(lView);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
if (currentConsumer !== null) {
|
||||
|
|
@ -469,6 +476,8 @@ function detectChangesInView(lView: LView, mode: ChangeDetectionMode) {
|
|||
// Refresh views when they have a dirty reactive consumer, regardless of mode.
|
||||
shouldRefreshView ||= !!(consumer?.dirty && consumerPollProducersForChange(consumer));
|
||||
|
||||
shouldRefreshView ||= !!(ngDevMode && isExhaustiveCheckNoChanges());
|
||||
|
||||
// Mark the Flags and `ReactiveNode` as not dirty before refreshing the component, so that they
|
||||
// can be re-dirtied during the refresh process.
|
||||
if (consumer) {
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@ const instructionState: InstructionState = {
|
|||
skipHydrationRootTNode: null,
|
||||
};
|
||||
|
||||
export enum CheckNoChangesMode {
|
||||
Off,
|
||||
Exhaustive,
|
||||
OnlyDirtyViews,
|
||||
}
|
||||
|
||||
/**
|
||||
* In this mode, any changes in bindings will throw an ExpressionChangedAfterChecked error.
|
||||
*
|
||||
|
|
@ -216,7 +222,7 @@ const instructionState: InstructionState = {
|
|||
* The `checkNoChanges` function is invoked only in ngDevMode=true and verifies that no unintended
|
||||
* changes exist in the change detector or its children.
|
||||
*/
|
||||
let _isInCheckNoChangesMode = false;
|
||||
let _checkNoChangesMode: CheckNoChangesMode = 0; /* CheckNoChangesMode.Off */
|
||||
|
||||
/**
|
||||
* Flag used to indicate that we are in the middle running change detection on a view
|
||||
|
|
@ -411,12 +417,17 @@ export function getContextLView(): LView {
|
|||
|
||||
export function isInCheckNoChangesMode(): boolean {
|
||||
!ngDevMode && throwError('Must never be called in production mode');
|
||||
return _isInCheckNoChangesMode;
|
||||
return _checkNoChangesMode !== CheckNoChangesMode.Off;
|
||||
}
|
||||
|
||||
export function setIsInCheckNoChangesMode(mode: boolean): void {
|
||||
export function isExhaustiveCheckNoChanges(): boolean {
|
||||
!ngDevMode && throwError('Must never be called in production mode');
|
||||
_isInCheckNoChangesMode = mode;
|
||||
return _checkNoChangesMode === CheckNoChangesMode.Exhaustive;
|
||||
}
|
||||
|
||||
export function setIsInCheckNoChangesMode(mode: CheckNoChangesMode): void {
|
||||
!ngDevMode && throwError('Must never be called in production mode');
|
||||
_checkNoChangesMode = mode;
|
||||
}
|
||||
|
||||
export function isRefreshingViews(): boolean {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
detachViewFromDOM,
|
||||
trackMovedView,
|
||||
} from './node_manipulation';
|
||||
import {CheckNoChangesMode} from './state';
|
||||
import {storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/view_utils';
|
||||
|
||||
// Needed due to tsickle downleveling where multiple `implements` with classes creates
|
||||
|
|
@ -320,7 +321,11 @@ export class ViewRef<T> implements EmbeddedViewRef<T>, ChangeDetectorRefInterfac
|
|||
*/
|
||||
checkNoChanges(): void {
|
||||
if (ngDevMode) {
|
||||
checkNoChangesInternal(this._lView, this.notifyErrorHandler);
|
||||
checkNoChangesInternal(
|
||||
this._lView,
|
||||
CheckNoChangesMode.OnlyDirtyViews,
|
||||
this.notifyErrorHandler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
|
||||
import {
|
||||
ApplicationRef,
|
||||
NgZone,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
|
|
@ -29,7 +31,15 @@ import {
|
|||
ViewChild,
|
||||
ViewChildren,
|
||||
ViewContainerRef,
|
||||
provideExperimentalCheckNoChangesForDebug,
|
||||
provideExperimentalZonelessChangeDetection,
|
||||
ɵRuntimeError as RuntimeError,
|
||||
ɵRuntimeErrorCode as RuntimeErrorCode,
|
||||
afterRender,
|
||||
PLATFORM_ID,
|
||||
provideZoneChangeDetection,
|
||||
} from '@angular/core';
|
||||
import {} from '@angular/core/src/errors';
|
||||
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
|
@ -1290,6 +1300,225 @@ describe('change detection', () => {
|
|||
expect(comp.contentCheckCount).toEqual(1);
|
||||
expect(comp.viewCheckCount).toEqual(1);
|
||||
});
|
||||
|
||||
describe('provideCheckNoChangesForDebug', () => {
|
||||
// Needed because tests in this repo patch rAF to be setTimeout
|
||||
// and coalescing tries to get the native one but fails so
|
||||
// coalescing will run a timeout in the zone and cause an infinite loop.
|
||||
const previousRaf = global.requestAnimationFrame;
|
||||
beforeEach(() => {
|
||||
(global as any).requestAnimationFrame = undefined;
|
||||
});
|
||||
afterEach(() => {
|
||||
(global as any).requestAnimationFrame = previousRaf;
|
||||
});
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
template: '{{state}}{{resolveReadPromise()}}',
|
||||
})
|
||||
class MyApp {
|
||||
state = 'initial';
|
||||
promise?: Promise<void>;
|
||||
private resolve?: Function;
|
||||
changeDetectorRef = inject(ChangeDetectorRef);
|
||||
createReadPromise() {
|
||||
this.promise = new Promise<void>((resolve) => {
|
||||
this.resolve = resolve;
|
||||
});
|
||||
}
|
||||
resolveReadPromise() {
|
||||
this.resolve?.();
|
||||
}
|
||||
}
|
||||
|
||||
it('throws error if used after zoneless provider', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
|
||||
provideExperimentalCheckNoChangesForDebug({useNgZoneOnStable: true}),
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
],
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
TestBed.createComponent(MyApp);
|
||||
}).toThrowError(/must be after any other provider for `NgZone`/);
|
||||
});
|
||||
|
||||
it('throws error if used after zone provider', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
|
||||
provideExperimentalCheckNoChangesForDebug({useNgZoneOnStable: true}),
|
||||
provideZoneChangeDetection(),
|
||||
],
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
TestBed.createComponent(MyApp);
|
||||
}).toThrowError(/must be after any other provider for `NgZone`/);
|
||||
});
|
||||
|
||||
it('throws expression changed with useNgZoneOnStable', async () => {
|
||||
let error: RuntimeError | undefined = undefined;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideExperimentalCheckNoChangesForDebug({useNgZoneOnStable: true}),
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useValue: {
|
||||
handleError(e: unknown) {
|
||||
error = e as RuntimeError;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let renderHookCalls = 0;
|
||||
TestBed.runInInjectionContext(() => {
|
||||
afterRender(() => {
|
||||
renderHookCalls++;
|
||||
});
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
await fixture.whenStable();
|
||||
expect(renderHookCalls).toBe(1);
|
||||
|
||||
fixture.componentInstance.createReadPromise();
|
||||
TestBed.inject(NgZone).run(() => {
|
||||
fixture.componentInstance.state = 'new';
|
||||
});
|
||||
await fixture.componentInstance.promise;
|
||||
// should not have run appplicationRef.tick again
|
||||
expect(renderHookCalls).toBe(1);
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.code).toEqual(RuntimeErrorCode.EXPRESSION_CHANGED_AFTER_CHECKED);
|
||||
});
|
||||
|
||||
it('does not throw expression changed with useNgZoneOnStable if there is a change detection scheduled', async () => {
|
||||
let error: RuntimeError | undefined = undefined;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideExperimentalCheckNoChangesForDebug({useNgZoneOnStable: true}),
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useValue: {
|
||||
handleError(e: unknown) {
|
||||
error = e as RuntimeError;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
await fixture.whenStable();
|
||||
|
||||
fixture.componentInstance.createReadPromise();
|
||||
TestBed.inject(NgZone).run(() => {
|
||||
setTimeout(() => {
|
||||
fixture.componentInstance.state = 'new';
|
||||
fixture.componentInstance.changeDetectorRef.markForCheck();
|
||||
}, 20);
|
||||
});
|
||||
await fixture.componentInstance.promise;
|
||||
// checkNoChanges runs from zone.run call
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
// checkNoChanges runs from the timeout
|
||||
fixture.componentInstance.createReadPromise();
|
||||
await fixture.componentInstance.promise;
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws expression changed with interval', async () => {
|
||||
let error: RuntimeError | undefined = undefined;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideExperimentalCheckNoChangesForDebug({interval: 5}),
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useValue: {
|
||||
handleError(e: unknown) {
|
||||
error = e as RuntimeError;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.state = 'new';
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(error!.code).toEqual(RuntimeErrorCode.EXPRESSION_CHANGED_AFTER_CHECKED);
|
||||
});
|
||||
|
||||
it('does not throw expression changed with interval if change detection is scheduled', async () => {
|
||||
let error: RuntimeError | undefined = undefined;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideExperimentalCheckNoChangesForDebug({interval: 0}),
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useValue: {
|
||||
handleError(e: unknown) {
|
||||
error = e as RuntimeError;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.state = 'new';
|
||||
// markForCheck schedules change detection
|
||||
fixture.componentInstance.changeDetectorRef.markForCheck();
|
||||
// wait beyond the exhaustive check interval
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1));
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not throw expression changed with interval if OnPush component an no exhaustive', async () => {
|
||||
let error: RuntimeError | undefined = undefined;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideExperimentalCheckNoChangesForDebug({interval: 0, exhaustive: false}),
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useValue: {
|
||||
handleError(e: unknown) {
|
||||
error = e as RuntimeError;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.state = 'new';
|
||||
// wait beyond the exhaustive check interval
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1));
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1430,6 +1430,9 @@
|
|||
{
|
||||
"name": "init_eventcontract"
|
||||
},
|
||||
{
|
||||
"name": "init_exhaustive_check_no_changes"
|
||||
},
|
||||
{
|
||||
"name": "init_fields"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue