diff --git a/README.md b/README.md index e0881664..49ff7648 100644 --- a/README.md +++ b/README.md @@ -20,23 +20,8 @@ This package is a thin wrapper around [TinyMCE](https://github.com/tinymce/tinym |<= 8 |3.x | |< 5 | Not supported | -### Not yet Zoneless ( >=Angular v21 ) -* This wrapper still requires `zone.js` to ensure backward compatibility to older Angular versions. Therefore, if your application uses Angular v21 or higher, it needs to include `provideZoneDetection()` in its providers. - -```jsx -import { NgModule, provideZoneChangeDetection } from '@angular/core'; - -@NgModule({ - declarations: [ - // ... - ], - imports: [ - // ... - ], - providers: [ provideZoneChangeDetection() ], - bootstrap: [ AppComponent ] -}) -``` +### Zoneless Support +This wrapper supports Angular's zoneless change detection. No additional configuration is needed — the component works with both zone-based and zoneless applications. ### Issues diff --git a/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts b/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts index ef30f93d..713af64b 100644 --- a/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts +++ b/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts @@ -1,19 +1,41 @@ import 'core-js/features/reflect'; + +// zone.js is imported here because our test suite needs to cover both zoneless and +// zone.js-based Angular applications. As a component library, our users may run +// either mode, so we must ensure compatibility with both. Since Angular 21, zoneless +// is the default, but zone.js remains supported. Once Angular drops zone.js support +// entirely, this import, ng-zone specific tests and the zone.js devDependency can be removed. +// +// Note: importing zone.js patches native browser APIs (addEventListener, setTimeout, +// setInterval, etc.), but Angular does not use these patches for change detection by +// default. Change detection only relies on zone.js in tests that explicitly configure +// `provideZoneChangeDetection`. import 'zone.js'; import 'zone.js/plugins/fake-async-test'; import { TestBed } from '@angular/core/testing'; import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; -import { NgModule, provideZoneChangeDetection } from '@angular/core'; +import { NgModule, provideZonelessChangeDetection } from '@angular/core'; +// According to Angular docs, TestBed uses zone-based change detection by default +// when zone.js is loaded via polyfills: +// https://angular.dev/guide/zoneless#testing-and-debugging +// +// In practice, this behaviour seems to be driven by Angular's built-in test runners +// (Karma, Vitest) rather than TestBed itself. Since we use Bedrock, we appear to be +// immune to this — zone-based detection does not kick in automatically even with +// zone.js loaded. Nonetheless, we explicitly opt into zoneless change detection here +// to stay aligned with the Angular documentation and to be safe. Zone.js-specific +// tests can override this with `provideZoneChangeDetection` on a per-test basis. @NgModule({ - providers: [ provideZoneChangeDetection() ], + providers: [ provideZonelessChangeDetection() ], }) class AppTestingModule {} TestBed.initTestEnvironment( - [ BrowserTestingModule, AppTestingModule ], platformBrowserTesting(), + [ BrowserTestingModule, AppTestingModule ], + platformBrowserTesting(), { - teardown: { destroyAfterEach: true }, + teardown: { destroyAfterEach: true } } ); diff --git a/tinymce-angular-component/src/test/ts/browser/EventBlacklistingTest.ts b/tinymce-angular-component/src/test/ts/browser/EventBlacklistingTest.ts index ffcc2c68..6eca9612 100644 --- a/tinymce-angular-component/src/test/ts/browser/EventBlacklistingTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/EventBlacklistingTest.ts @@ -1,11 +1,11 @@ import '../alien/InitTestEnvironment'; -import { describe, it } from '@ephox/bedrock-client'; +import { context, describe, it } from '@ephox/bedrock-client'; import { EditorComponent } from '../../../main/ts/public_api'; -import { eachVersionContext, editorHook } from '../alien/TestHooks'; -import { map, merge, timer, first, buffer, Observable, tap, firstValueFrom } from 'rxjs'; -import { NgZone } from '@angular/core'; +import { eachVersionContext, EditorFixture, editorHook } from '../alien/TestHooks'; +import { map, merge, timer, first, buffer, Observable, tap, firstValueFrom, identity } from 'rxjs'; +import { NgZone, provideZoneChangeDetection } from '@angular/core'; import { Assertions } from '@ephox/agar'; import { Fun } from '@ephox/katamari'; import { throwTimeout } from '../alien/TestHelpers'; @@ -16,28 +16,47 @@ describe('EventBlacklistingTest', () => { tap(() => Assertions.assertEq('Subscribers to events should run within NgZone', true, NgZone.isInAngularZone())) ); + const testEventsShouldBeBoundWhenAllowed = async (fixture: EditorFixture, isZoneless: boolean) => { + const pEventsCompleted = firstValueFrom( + merge( + fixture.editorComponent.onKeyUp.pipe(map(Fun.constant('onKeyUp')), isZoneless ? identity : shouldRunInAngularZone), + fixture.editorComponent.onKeyDown.pipe(map(Fun.constant('onKeyDown')), isZoneless ? identity : shouldRunInAngularZone), + fixture.editorComponent.onClick.pipe(map(Fun.constant('onClick')), isZoneless ? identity : shouldRunInAngularZone) + ).pipe(throwTimeout(10000, 'Timed out waiting for some event to fire'), buffer(timer(100)), first()) + ); + fixture.editor.fire('keydown'); + fixture.editor.fire('keyclick'); + fixture.editor.fire('keyup'); + const eventsCompleted = await pEventsCompleted; + Assertions.assertEq('Only one event should have fired', 1, eventsCompleted.length); + Assertions.assertEq('Only keyup should fire', 'onKeyUp', eventsCompleted[0]); + }; + eachVersionContext([ '4', '5', '6', '7', '8' ], () => { - const createFixture = editorHook(EditorComponent); + context('zoneless', () => { + const createFixture = editorHook(EditorComponent); + const isZoneless = true; - it('Events should be bound when allowed', async () => { - const fixture = await createFixture({ - allowedEvents: 'onKeyUp,onClick,onInit', - ignoreEvents: 'onClick', + it('Events should be bound when allowed', async () => { + const fixture = await createFixture({ + allowedEvents: 'onKeyUp,onClick,onInit', + ignoreEvents: 'onClick', + }); + await testEventsShouldBeBoundWhenAllowed(fixture, isZoneless); }); + }); - const pEventsCompleted = firstValueFrom( - merge( - fixture.editorComponent.onKeyUp.pipe(map(Fun.constant('onKeyUp')), shouldRunInAngularZone), - fixture.editorComponent.onKeyDown.pipe(map(Fun.constant('onKeyDown')), shouldRunInAngularZone), - fixture.editorComponent.onClick.pipe(map(Fun.constant('onClick')), shouldRunInAngularZone) - ).pipe(throwTimeout(10000, 'Timed out waiting for some event to fire'), buffer(timer(100)), first()) - ); - fixture.editor.fire('keydown'); - fixture.editor.fire('keyclick'); - fixture.editor.fire('keyup'); - const eventsCompleted = await pEventsCompleted; - Assertions.assertEq('Only one event should have fired', 1, eventsCompleted.length); - Assertions.assertEq('Only keyup should fire', 'onKeyUp', eventsCompleted[0]); + context('with zone.js', () => { + const createFixture = editorHook(EditorComponent, { providers: [ provideZoneChangeDetection() ] }); + const isZoneless = false; + + it('Events should be bound when allowed', async () => { + const fixture = await createFixture({ + allowedEvents: 'onKeyUp,onClick,onInit', + ignoreEvents: 'onClick', + }); + await testEventsShouldBeBoundWhenAllowed(fixture, isZoneless); + }); }); }); }); diff --git a/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts b/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts index fe41e20b..2eec3b48 100644 --- a/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts @@ -1,6 +1,6 @@ import '../alien/InitTestEnvironment'; -import { NgZone } from '@angular/core'; +import { NgZone, provideZoneChangeDetection } from '@angular/core'; import { Assertions } from '@ephox/agar'; import { describe, it } from '@ephox/bedrock-client'; @@ -11,7 +11,7 @@ import { throwTimeout } from '../alien/TestHelpers'; describe('NgZoneTest', () => { eachVersionContext([ '4', '5', '6', '7', '8' ], () => { - const createFixture = fixtureHook(EditorComponent, { imports: [ EditorComponent ] }); + const createFixture = fixtureHook(EditorComponent, { imports: [ EditorComponent ], providers: [ provideZoneChangeDetection() ] }); it('Subscribers to events should run within NgZone', async () => { const fixture = createFixture();