refactor(core): Add scheduler abstraction and notify when necessary (#53499)

In order to provide a reasonable experience for Angular without Zones,
we need a mechanism to run change detection when we receive a change
notification. There are several existing APIs today that serve as the
change notification: `ChangeDetectorRef.markForCheck`, signal updates,
event listeners (since they mark the view dirty), and attaching a view to
either the `ApplicationRef` or `ChangeDetectorRef`. These operations
are now paired with a notification to the change detection scheduler.

The concrete implementation for this scheduler is still being designed.
However, this gives us a starting point to partner with teams to
experiment with what that might look like.

PR Close #53499
This commit is contained in:
Andrew Scott
2023-12-05 16:25:31 -08:00
committed by Andrew Scott
parent cc74ebfdf6
commit 8d58595478
26 changed files with 420 additions and 29 deletions
@@ -8,7 +8,7 @@
import {Subscription} from 'rxjs';
import {provideZoneChangeDetection} from '../change_detection/scheduling';
import {provideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling';
import {EnvironmentProviders, Provider, StaticProvider} from '../di/interface/provider';
import {EnvironmentInjector} from '../di/r3_injector';
import {ErrorHandler} from '../error_handler';
@@ -8,13 +8,13 @@
import {Subscription} from 'rxjs';
import {ApplicationRef} from '../application/application_ref';
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, Injectable, InjectionToken, makeEnvironmentProviders, StaticProvider} from '../di';
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {PendingTasks} from '../pending_tasks';
import {NgZone} from '../zone';
import {InternalNgZoneOptions} from '../zone/ng_zone';
import {ApplicationRef} from '../../application/application_ref';
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, Injectable, InjectionToken, makeEnvironmentProviders, StaticProvider} from '../../di';
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {PendingTasks} from '../../pending_tasks';
import {NgZone} from '../../zone';
import {InternalNgZoneOptions} from '../../zone/ng_zone';
@Injectable({providedIn: 'root'})
export class NgZoneChangeDetectionScheduler {
@@ -0,0 +1,14 @@
/**
* @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
*/
/**
* Injectable that is notified when an `LView` is made aware of changes to application state.
*/
export abstract class ChangeDetectionScheduler {
abstract notify(): void;
}
+1 -1
View File
@@ -25,7 +25,7 @@ export * from './di';
export {BootstrapOptions, ApplicationRef, NgProbeToken, APP_BOOTSTRAP_LISTENER} from './application/application_ref';
export {PlatformRef} from './platform/platform_ref';
export {createPlatform, createPlatformFactory, assertPlatform, destroyPlatform, getPlatform} from './platform/platform';
export {provideZoneChangeDetection, NgZoneOptions} from './change_detection/scheduling';
export {provideZoneChangeDetection, NgZoneOptions} from './change_detection/scheduling/ng_zone_scheduling';
export {enableProdMode, isDevMode} from './util/is_dev_mode';
export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, PLATFORM_ID, ANIMATION_MODULE_TYPE, CSP_NONCE} from './application/application_tokens';
export {APP_INITIALIZER, ApplicationInitStatus} from './application/application_init';
+1
View File
@@ -12,6 +12,7 @@ export {IMAGE_CONFIG as ɵIMAGE_CONFIG, IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_
export {internalCreateApplication as ɵinternalCreateApplication} from './application/create_application';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {getEnsureDirtyViewsAreAlwaysReachable as ɵgetEnsureDirtyViewsAreAlwaysReachable, setEnsureDirtyViewsAreAlwaysReachable as ɵsetEnsureDirtyViewsAreAlwaysReachable} from './change_detection/flags';
export {ChangeDetectionScheduler as ɵChangeDetectionScheduler} from './change_detection/scheduling/zoneless_scheduling';
export {Console as ɵConsole} from './console';
export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks} from './defer/discovery';
export {renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './defer/instructions';
+1 -1
View File
@@ -8,7 +8,7 @@
import {ApplicationInitStatus} from '../application/application_init';
import {_callAndReportToErrorHandler, ApplicationRef, BootstrapOptions, compileNgModuleFactory, optionsReducer, remove} from '../application/application_ref';
import {getNgZoneOptions, internalProvideZoneChangeDetection, PROVIDED_NG_ZONE} from '../change_detection/scheduling';
import {getNgZoneOptions, internalProvideZoneChangeDetection, PROVIDED_NG_ZONE} from '../change_detection/scheduling/ng_zone_scheduling';
import {Injectable, InjectionToken, Injector} from '../di';
import {ErrorHandler} from '../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../errors';
@@ -7,6 +7,7 @@
*/
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {ChangeDetectionScheduler} from '../change_detection/scheduling/zoneless_scheduling';
import {Injector} from '../di/injector';
import {convertToBitFlags} from '../di/injector_compatibility';
import {InjectFlags, InjectOptions} from '../di/interface/injector';
@@ -201,6 +202,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
const sanitizer = rootViewInjector.get(Sanitizer, null);
const afterRenderEventManager = rootViewInjector.get(AfterRenderEventManager, null);
const changeDetectionScheduler = rootViewInjector.get(ChangeDetectionScheduler, null);
const environment: LViewEnvironment = {
rendererFactory,
@@ -208,6 +210,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
// We don't use inline effects (yet).
inlineEffectRunner: null,
afterRenderEventManager,
changeDetectionScheduler,
};
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);
@@ -7,7 +7,7 @@
*/
import {isRootView} from '../interfaces/type_checks';
import {FLAGS, LView, LViewFlags} from '../interfaces/view';
import {ENVIRONMENT, FLAGS, LView, LViewFlags} from '../interfaces/view';
import {getLViewParent} from '../util/view_traversal_utils';
/**
@@ -22,6 +22,7 @@ import {getLViewParent} from '../util/view_traversal_utils';
* @returns the root LView
*/
export function markViewDirty(lView: LView): LView|null {
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
while (lView) {
lView[FLAGS] |= LViewFlags.Dirty;
const parent = getLViewParent(lView);
@@ -24,6 +24,7 @@ import {Renderer, RendererFactory} from './renderer';
import {RElement} from './renderer_dom';
import {TStylingKey, TStylingRange} from './styling';
import {TDeferBlockDetails} from '../../defer/interfaces';
import type {ChangeDetectionScheduler} from '../../change_detection/scheduling/zoneless_scheduling';
@@ -372,6 +373,9 @@ export interface LViewEnvironment {
/** Container for after render hooks */
afterRenderEventManager: AfterRenderEventManager|null;
/** Scheduler for change detection to notify when application state changes. */
changeDetectionScheduler: ChangeDetectionScheduler|null;
}
/** Flags associated with an LView (saved in LView[FLAGS]) */
+14 -11
View File
@@ -14,7 +14,7 @@ import {LContainer, LContainerFlags, TYPE} from '../interfaces/container';
import {TConstants, TNode} from '../interfaces/node';
import {RNode} from '../interfaces/renderer_dom';
import {isLContainer, isLView} from '../interfaces/type_checks';
import {DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, ON_DESTROY_HOOKS, PARENT, PREORDER_HOOK_FLAGS, PreOrderHookFlags, REACTIVE_TEMPLATE_CONSUMER, TData, TView} from '../interfaces/view';
import {DECLARATION_VIEW, ENVIRONMENT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, ON_DESTROY_HOOKS, PARENT, PREORDER_HOOK_FLAGS, PreOrderHookFlags, REACTIVE_TEMPLATE_CONSUMER, TData, TView} from '../interfaces/view';
@@ -207,17 +207,19 @@ export function requiresRefreshOrTraversal(lView: LView) {
* parents above.
*/
export function updateAncestorTraversalFlagsOnAttach(lView: LView) {
// When we attach a view that's marked `Dirty`, we should ensure that it is reached during the
// next CD traversal so we add the `RefreshView` flag and mark ancestors accordingly.
if (lView[FLAGS] & LViewFlags.Dirty && getEnsureDirtyViewsAreAlwaysReachable()) {
lView[FLAGS] |= LViewFlags.RefreshView;
// TODO(atscott): Simplify if...else cases once getEnsureDirtyViewsAreAlwaysReachable is always
// `true`. When we attach a view that's marked `Dirty`, we should ensure that it is reached during
// the next CD traversal so we add the `RefreshView` flag and mark ancestors accordingly.
if (requiresRefreshOrTraversal(lView)) {
markAncestorsForTraversal(lView);
} else if (lView[FLAGS] & LViewFlags.Dirty) {
if (getEnsureDirtyViewsAreAlwaysReachable()) {
lView[FLAGS] |= LViewFlags.RefreshView;
markAncestorsForTraversal(lView);
} else {
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
}
}
if (!requiresRefreshOrTraversal(lView)) {
return;
}
markAncestorsForTraversal(lView);
}
/**
@@ -228,6 +230,7 @@ export function updateAncestorTraversalFlagsOnAttach(lView: LView) {
* flag is already `true` or the `lView` is detached.
*/
export function markAncestorsForTraversal(lView: LView) {
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
let parent = lView[PARENT];
while (parent !== null) {
// We stop adding markers to the ancestors once we reach one that already has the marker. This
+3 -2
View File
@@ -17,9 +17,9 @@ import {checkNoChangesInternal, detectChangesInternal} from './instructions/chan
import {markViewDirty} from './instructions/mark_view_dirty';
import {CONTAINER_HEADER_OFFSET, VIEW_REFS} from './interfaces/container';
import {isLContainer} from './interfaces/type_checks';
import {CONTEXT, FLAGS, LView, LViewFlags, PARENT, TVIEW} from './interfaces/view';
import {CONTEXT, ENVIRONMENT, FLAGS, LView, LViewFlags, PARENT, TVIEW} from './interfaces/view';
import {destroyLView, detachView, detachViewFromDOM} from './node_manipulation';
import {storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/view_utils';
import {markAncestorsForTraversal, storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/view_utils';
// Needed due to tsickle downleveling where multiple `implements` with classes creates
@@ -326,5 +326,6 @@ export class ViewRef<T> implements EmbeddedViewRef<T>, ChangeDetectorRefInterfac
ngDevMode && 'This view is already attached to a ViewContainer!');
}
this._appRef = appRef;
updateAncestorTraversalFlagsOnAttach(this._lView);
}
}
@@ -116,6 +116,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -1361,6 +1364,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
@@ -137,6 +137,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -1433,6 +1436,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
@@ -68,6 +68,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -1139,6 +1142,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
@@ -71,6 +71,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -1577,6 +1580,9 @@
{
"name": "init_ng_zone"
},
{
"name": "init_ng_zone_scheduling"
},
{
"name": "init_node"
},
@@ -1712,9 +1718,6 @@
{
"name": "init_sanitizer"
},
{
"name": "init_scheduling"
},
{
"name": "init_schema"
},
@@ -1892,6 +1895,9 @@
{
"name": "init_zone"
},
{
"name": "init_zoneless_scheduling"
},
{
"name": "initializeDirectives"
},
@@ -95,6 +95,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -98,6 +98,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -44,6 +44,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ComponentFactory"
},
@@ -890,6 +893,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
@@ -68,6 +68,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -1271,6 +1274,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
@@ -101,6 +101,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -56,6 +56,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -992,6 +995,9 @@
{
"name": "unwrapRNode"
},
{
"name": "updateAncestorTraversalFlagsOnAttach"
},
{
"name": "updateMicroTaskStatus"
},
@@ -68,6 +68,9 @@
{
"name": "ChainedInjector"
},
{
"name": "ChangeDetectionScheduler"
},
{
"name": "ChangeDetectionStrategy"
},
@@ -0,0 +1,303 @@
/**
* @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 {AsyncPipe} from '@angular/common';
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
import {afterNextRender, ApplicationRef, ChangeDetectorRef, Component, ComponentRef, createComponent, DebugElement, ElementRef, EnvironmentInjector, ErrorHandler, getDebugNode, inject, Injectable, Input, NgZone, PLATFORM_ID, signal, TemplateRef, Type, ViewChild, ViewContainerRef, ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵNoopNgZone} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {BehaviorSubject} from 'rxjs';
@Injectable({providedIn: 'root'})
class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
private appRef = inject(ApplicationRef);
private _hasPendingChangeDetection = false;
get hasPendingChangeDetection() {
return this._hasPendingChangeDetection;
}
notify(): void {
if (this._hasPendingChangeDetection) return;
this._hasPendingChangeDetection = true;
setTimeout(() => {
try {
this.appRef.tick();
} finally {
this._hasPendingChangeDetection = false;
}
});
}
}
describe('Angular with NoopNgZone', () => {
function nextRender(): Promise<void> {
const injector = TestBed.inject(EnvironmentInjector);
return new Promise((resolve) => {
afterNextRender(resolve, {injector});
});
}
async function createAndAttachComponent<T>(type: Type<T>): Promise<ComponentRef<T>> {
const environmentInjector = TestBed.inject(EnvironmentInjector);
const component = createComponent(type, {environmentInjector});
environmentInjector.get(ApplicationRef).attachView(component.hostView);
await nextRender();
return component;
}
describe('notifies scheduler', () => {
let scheduler: ChangeDetectionSchedulerImpl;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{provide: NgZone, useClass: ɵNoopNgZone},
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
]
});
scheduler = TestBed.inject(ChangeDetectionSchedulerImpl);
});
it('when signal updates', async () => {
const val = signal('initial');
@Component({template: '{{val()}}', standalone: true})
class TestComponent {
val = val;
}
const component = await createAndAttachComponent(TestComponent);
expect(component.location.nativeElement.innerText).toEqual('initial');
val.set('new');
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(component.location.nativeElement.innerText).toEqual('new');
});
it('when using markForCheck()', async () => {
@Component({template: '{{val}}', standalone: true})
class TestComponent {
cdr = inject(ChangeDetectorRef);
val = 'initial';
setVal(val: string) {
this.val = val;
this.cdr.markForCheck();
}
}
const component = await createAndAttachComponent(TestComponent);
expect(component.location.nativeElement.innerText).toEqual('initial');
component.instance.setVal('new');
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(component.location.nativeElement.innerText).toEqual('new');
});
it('on input binding', async () => {
@Component({template: '{{val}}', standalone: true})
class TestComponent {
@Input() val = 'initial';
}
const component = await createAndAttachComponent(TestComponent);
expect(component.location.nativeElement.innerText).toEqual('initial');
component.setInput('val', 'new');
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(component.location.nativeElement.innerText).toEqual('new');
});
it('on event listener bound in template', async () => {
@Component({template: '<div (click)="updateVal()">{{val}}</div>', standalone: true})
class TestComponent {
val = 'initial';
updateVal() {
this.val = 'new';
}
}
const component = await createAndAttachComponent(TestComponent);
expect(component.location.nativeElement.innerText).toEqual('initial');
getDebugElement(component)
.query(p => p.nativeElement.tagName === 'DIV')
.triggerEventHandler('click');
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(component.location.nativeElement.innerText).toEqual('new');
});
it('on event listener bound in host', async () => {
@Component({host: {'(click)': 'updateVal()'}, template: '{{val}}', standalone: true})
class TestComponent {
val = 'initial';
updateVal() {
this.val = 'new';
}
}
const component = await createAndAttachComponent(TestComponent);
expect(component.location.nativeElement.innerText).toEqual('initial');
getDebugElement(component).triggerEventHandler('click');
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(component.location.nativeElement.innerText).toEqual('new');
});
it('with async pipe', async () => {
@Component({template: '{{val | async}}', standalone: true, imports: [AsyncPipe]})
class TestComponent {
val = new BehaviorSubject('initial');
}
const component = await createAndAttachComponent(TestComponent);
expect(component.location.nativeElement.innerText).toEqual('initial');
component.instance.val.next('new');
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(component.location.nativeElement.innerText).toEqual('new');
});
it('when creating a view', async () => {
@Component({
template: '<ng-template #ref>{{"binding"}}</ng-template>',
standalone: true,
})
class TestComponent {
@ViewChild(TemplateRef) template!: TemplateRef<unknown>;
@ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
createView(): any {
this.viewContainer.createEmbeddedView(this.template);
}
}
const component = await createAndAttachComponent(TestComponent);
expect(scheduler.hasPendingChangeDetection).toBe(false);
component.instance.createView();
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(component.location.nativeElement.innerText).toEqual('binding');
});
it('when inserting a view', async () => {
@Component({
template: '{{"binding"}}',
standalone: true,
})
class DynamicCmp {
elementRef = inject(ElementRef);
}
@Component({
template: '<ng-template #ref></ng-template>',
standalone: true,
})
class TestComponent {
@ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
}
const fixture = await createAndAttachComponent(TestComponent);
expect(scheduler.hasPendingChangeDetection).toBe(false);
const component =
createComponent(DynamicCmp, {environmentInjector: TestBed.inject(EnvironmentInjector)});
fixture.instance.viewContainer.insert(component.hostView);
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(fixture.location.nativeElement.innerText).toEqual('binding');
});
it('when attaching view to ApplicationRef', async () => {
@Component({
template: '{{"binding"}}',
standalone: true,
})
class DynamicCmp {
elementRef = inject(ElementRef);
}
const environmentInjector = TestBed.inject(EnvironmentInjector);
const appRef = TestBed.inject(ApplicationRef);
const component = createComponent(DynamicCmp, {environmentInjector});
expect(scheduler.hasPendingChangeDetection).toBe(false);
expect(component.instance.elementRef.nativeElement.innerText).toEqual('');
appRef.attachView(component.hostView);
expect(scheduler.hasPendingChangeDetection).toBe(true);
await nextRender();
expect(component.instance.elementRef.nativeElement.innerText).toEqual('binding');
// Don't need to run CD on detach because DOM nodes are just removed
// That said, queries need to be updated and currently only update during CD but that's an
// unrelated change that needs to happen.
appRef.detachView(component.hostView);
expect(scheduler.hasPendingChangeDetection).toBe(false);
// reattaching non-dirty view does not notify scheduler
appRef.attachView(component.hostView);
expect(scheduler.hasPendingChangeDetection).toBe(false);
});
});
it('can recover when an error is re-thrown by the ErrorHandler', async () => {
const val = signal('initial');
let throwError = false;
@Component({template: '{{val()}}{{maybeThrow()}}', standalone: true})
class TestComponent {
val = val;
maybeThrow() {
if (throwError) {
throw new Error('e');
} else {
return '';
}
}
}
TestBed.configureTestingModule({
providers: [
{provide: NgZone, useClass: ɵNoopNgZone},
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
{
provide: ErrorHandler, useClass: class extends ErrorHandler {
override handleError(error: any): void {
throw error;
}
}
},
]
});
const component = await createAndAttachComponent(TestComponent);
expect(component.location.nativeElement.innerText).toEqual('initial');
val.set('new');
throwError = true;
// error is thrown in a timeout and can't really be "caught".
// Still need to wrap in expect so it happens in the expect context and doesn't fail the test.
expect(async () => await nextRender()).not.toThrow();
expect(component.location.nativeElement.innerText).toEqual('initial');
throwError = false;
await nextRender();
expect(component.location.nativeElement.innerText).toEqual('new');
});
});
function getDebugElement(component: ComponentRef<unknown>) {
return getDebugNode(component.location.nativeElement) as DebugElement;
}
+1
View File
@@ -147,6 +147,7 @@ describe('di', () => {
sanitizer: null,
inlineEffectRunner: null,
afterRenderEventManager: null,
changeDetectionScheduler: null,
},
{} as any, null, null, null);
enterView(contentView);
@@ -42,8 +42,13 @@ export function enterViewWithOneDiv() {
tView.expandoStartIndex = HEADER_OFFSET + 10;
const tNode = tView.firstChild = createTNode(tView, null!, TNodeType.Element, 0, 'div', null);
const lView = createLView(
null, tView, null, LViewFlags.CheckAlways, null, null,
{rendererFactory, sanitizer: null, inlineEffectRunner: null, afterRenderEventManager: null},
null, tView, null, LViewFlags.CheckAlways, null, null, {
rendererFactory,
sanitizer: null,
inlineEffectRunner: null,
afterRenderEventManager: null,
changeDetectionScheduler: null
},
renderer, null, null, null);
lView[HEADER_OFFSET] = div;
tView.data[HEADER_OFFSET] = tNode;
@@ -76,6 +76,7 @@ export class ViewFixture {
sanitizer: sanitizer || null,
afterRenderEventManager: new AfterRenderEventManager(),
inlineEffectRunner: null,
changeDetectionScheduler: null,
},
hostRenderer, null, null, null);