refactor(core): Update error for both zone and zoneless to be only for apps (#55813)

Developers may want to enable zoneless for all tests by default by
adding the zoneless provider to `initTestEnvironment` and then
temporarily disabling it for individual tests with the zone provider
until they can be made zoneless compatible.

PR Close #55813
This commit is contained in:
Andrew Scott 2024-05-15 09:05:46 -07:00
parent 88fb94693a
commit e76bebd409
7 changed files with 54 additions and 51 deletions

View file

@ -8,7 +8,10 @@
import {Subscription} from 'rxjs';
import {internalProvideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling';
import {
internalProvideZoneChangeDetection,
PROVIDED_NG_ZONE,
} 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';
@ -26,6 +29,7 @@ import {NgZone} from '../zone/ng_zone';
import {ApplicationInitStatus} from './application_init';
import {_callAndReportToErrorHandler, ApplicationRef} from './application_ref';
import {PROVIDED_ZONELESS} from '../change_detection/scheduling/zoneless_scheduling';
/**
* Internal create application API that implements the core application creation logic and optional
@ -70,11 +74,20 @@ export function internalCreateApplication(config: {
return ngZone.run(() => {
envInjector.resolveInjectorInitializers();
const exceptionHandler: ErrorHandler | null = envInjector.get(ErrorHandler, null);
if ((typeof ngDevMode === 'undefined' || ngDevMode) && !exceptionHandler) {
throw new RuntimeError(
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
'No `ErrorHandler` found in the Dependency Injection tree.',
);
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!exceptionHandler) {
throw new RuntimeError(
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
'No `ErrorHandler` found in the Dependency Injection tree.',
);
}
if (envInjector.get(PROVIDED_ZONELESS) && envInjector.get(PROVIDED_NG_ZONE)) {
throw new RuntimeError(
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
'Invalid change detection configuration: ' +
'provideZoneChangeDetection and provideExperimentalZonelessChangeDetection cannot be used together.',
);
}
}
let onErrorSubscription: Subscription;

View file

@ -38,7 +38,6 @@ 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;
@ -73,6 +72,7 @@ export class NgZoneChangeDetectionScheduler {
*/
export const PROVIDED_NG_ZONE = new InjectionToken<boolean>(
typeof ngDevMode === 'undefined' || ngDevMode ? 'provideZoneChangeDetection token' : '',
{factory: () => false},
);
export function internalProvideZoneChangeDetection({
@ -167,8 +167,9 @@ export function provideZoneChangeDetection(options?: NgZoneOptions): Environment
});
return makeEnvironmentProviders([
typeof ngDevMode === 'undefined' || ngDevMode
? [{provide: PROVIDED_NG_ZONE, useValue: true}, bothZoneAndZonelessErrorCheckProvider]
? [{provide: PROVIDED_NG_ZONE, useValue: true}]
: [],
{provide: ZONELESS_ENABLED, useValue: false},
zoneProviders,
]);
}
@ -301,19 +302,3 @@ export class ZoneStablePendingTask {
this.subscription.unsubscribe();
}
}
const bothZoneAndZonelessErrorCheckProvider = {
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory: () => {
const providedZoneless = inject(ZONELESS_ENABLED, {optional: true});
if (providedZoneless) {
throw new RuntimeError(
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
'Invalid change detection configuration: ' +
'provideZoneChangeDetection and provideExperimentalZonelessChangeDetection cannot be used together.',
);
}
return () => {};
},
};

View file

@ -59,6 +59,12 @@ export const ZONELESS_ENABLED = new InjectionToken<boolean>(
{providedIn: 'root', factory: () => false},
);
/** Token used to indicate `provideExperimentalZonelessChangeDetection` was used. */
export const PROVIDED_ZONELESS = new InjectionToken<boolean>(
typeof ngDevMode === 'undefined' || ngDevMode ? 'Zoneless provided' : '',
{providedIn: 'root', factory: () => false},
);
export const ZONELESS_SCHEDULER_DISABLED = new InjectionToken<boolean>(
typeof ngDevMode === 'undefined' || ngDevMode ? 'scheduler disabled' : '',
);

View file

@ -26,6 +26,7 @@ import {
ChangeDetectionScheduler,
NotificationSource,
ZONELESS_ENABLED,
PROVIDED_ZONELESS,
ZONELESS_SCHEDULER_DISABLED,
} from './zoneless_scheduling';
@ -297,5 +298,8 @@ export function provideExperimentalZonelessChangeDetection(): EnvironmentProvide
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
{provide: NgZone, useClass: NoopNgZone},
{provide: ZONELESS_ENABLED, useValue: true},
typeof ngDevMode === 'undefined' || ngDevMode
? [{provide: PROVIDED_ZONELESS, useValue: true}]
: [],
]);
}

View file

@ -95,25 +95,20 @@ export class PlatformRef {
internalProvideZoneChangeDetection({ngZoneFactory: () => ngZone, ignoreChangesOutsideZone}),
);
if (
(typeof ngDevMode === 'undefined' || ngDevMode) &&
moduleRef.injector.get(PROVIDED_NG_ZONE, null) !== null
) {
throw new RuntimeError(
RuntimeErrorCode.PROVIDER_IN_WRONG_CONTEXT,
'`bootstrapModule` does not support `provideZoneChangeDetection`. Use `BootstrapOptions` instead.',
);
}
if (
(typeof ngDevMode === 'undefined' || ngDevMode) &&
moduleRef.injector.get(ZONELESS_ENABLED, null) &&
options?.ngZone !== 'noop'
) {
throw new RuntimeError(
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
'Invalid change detection configuration: ' +
"`ngZone: 'noop'` must be set in `BootstrapOptions` with provideExperimentalZonelessChangeDetection.",
);
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (moduleRef.injector.get(PROVIDED_NG_ZONE)) {
throw new RuntimeError(
RuntimeErrorCode.PROVIDER_IN_WRONG_CONTEXT,
'`bootstrapModule` does not support `provideZoneChangeDetection`. Use `BootstrapOptions` instead.',
);
}
if (moduleRef.injector.get(ZONELESS_ENABLED) && options?.ngZone !== 'noop') {
throw new RuntimeError(
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
'Invalid change detection configuration: ' +
"`ngZone: 'noop'` must be set in `BootstrapOptions` with provideExperimentalZonelessChangeDetection.",
);
}
}
const exceptionHandler = moduleRef.injector.get(ErrorHandler, null);

View file

@ -45,6 +45,14 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
import {BehaviorSubject} from 'rxjs';
describe('change detection', () => {
it('can provide zone and zoneless (last one wins like any other provider) in TestBed', () => {
expect(() => {
TestBed.configureTestingModule({
providers: [provideExperimentalZonelessChangeDetection(), provideZoneChangeDetection()],
});
TestBed.inject(ApplicationRef);
}).not.toThrow();
});
describe('embedded views', () => {
@Directive({selector: '[viewManipulation]', exportAs: 'vm'})
class ViewManipulation {

View file

@ -64,14 +64,6 @@ describe('Angular with zoneless enabled', () => {
});
});
it('throws an error if used with zone provider', () => {
TestBed.configureTestingModule({providers: [provideZoneChangeDetection()]});
expect(() => TestBed.inject(NgZone)).toThrowError(
/NG0408: Invalid change detection configuration/,
);
});
describe('notifies scheduler', () => {
it('contributes to application stableness', async () => {
const val = signal('initial');