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..cce62a00 100644 --- a/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts +++ b/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts @@ -1,19 +1,25 @@ 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'; - -@NgModule({ - providers: [ provideZoneChangeDetection() ], -}) -class AppTestingModule {} TestBed.initTestEnvironment( - [ BrowserTestingModule, AppTestingModule ], platformBrowserTesting(), + [ BrowserTestingModule ], + 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..4ed553fe 100644 --- a/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts @@ -1,22 +1,29 @@ 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'; +import { beforeEach, describe, it } from '@ephox/bedrock-client'; import { EditorComponent } from '../../../main/ts/editor/editor.component'; -import { eachVersionContext, fixtureHook } from '../alien/TestHooks'; +import { eachVersionContext } from '../alien/TestHooks'; import { first } from 'rxjs'; import { throwTimeout } from '../alien/TestHelpers'; +import { TestBed } from '@angular/core/testing'; describe('NgZoneTest', () => { eachVersionContext([ '4', '5', '6', '7', '8' ], () => { - const createFixture = fixtureHook(EditorComponent, { imports: [ EditorComponent ] }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ EditorComponent ], + providers: [ provideZoneChangeDetection() ] + }).compileComponents(); + }); it('Subscribers to events should run within NgZone', async () => { - const fixture = createFixture(); + const fixture = TestBed.createComponent(EditorComponent); const editor = fixture.componentInstance; - fixture.detectChanges(); + fixture.detectChanges(); // TODO: consider using fixture.whenStable() + await new Promise((resolve) => { editor.onInit.pipe(first(), throwTimeout(10000, 'Timed out waiting for init event')).subscribe(() => { Assertions.assertEq('Subscribers to onInit should run within NgZone', true, NgZone.isInAngularZone()); @@ -27,9 +34,10 @@ describe('NgZoneTest', () => { // Lets just test one EventEmitter, if one works all should work it('Subscribers to onKeyUp should run within NgZone', async () => { - const fixture = createFixture(); + const fixture = TestBed.createComponent(EditorComponent); const editor = fixture.componentInstance; - fixture.detectChanges(); + fixture.detectChanges(); // TODO: consider using fixture.whenStable() + await new Promise((resolve) => { editor.onKeyUp.pipe(first(), throwTimeout(10000, 'Timed out waiting for key up event')).subscribe(() => { Assertions.assertEq('Subscribers to onKeyUp should run within NgZone', true, NgZone.isInAngularZone());