From 1843702038195c7f2328dcec563f08e7ff97ab7c Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Mon, 21 Sep 2026 13:45:36 +0930 Subject: [PATCH 01/15] Update readme, contributing and metadata --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++------ contributing.md | 7 ++--- package.json | 13 ++++++++-- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 30408b462..04b0bac63 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,68 @@ -# Official TinyMCE React component +# Official TinyMCE React Component ## About -This package is a thin wrapper around [TinyMCE](https://github.com/tinymce/tinymce) to make it easier to use in a React application. + Official React component for TinyMCE, the rich text editor. It makes integrating TinyMCE into React applications easy and seamless. -* If you need detailed documentation on TinyMCE, see: [TinyMCE Documentation](https://www.tiny.cloud/docs/tinymce/8/). -* For the TinyMCE React Quick Start, see: [TinyMCE Documentation - React Integration](https://www.tiny.cloud/docs/tinymce/8/react-cloud/). -* For the TinyMCE React Technical Reference, see: [TinyMCE Documentation - TinyMCE React Technical Reference](https://www.tiny.cloud/docs/tinymce/8/react-ref/). -* For our quick demos, check out the TinyMCE React [Storybook](https://tinymce.github.io/tinymce-react/). +## Quickstart +### Cloud CDN -### Issues +In your React project: -Have you found an issue with tinymce-react or do you have a feature request? Open up an [issue](https://github.com/tinymce/tinymce-react/issues) and let us know or submit a [pull request](https://github.com/tinymce/tinymce-react/pulls). *Note: For issues concerning TinyMCE please visit the [TinyMCE repository](https://github.com/tinymce/tinymce).* \ No newline at end of file +1. [Sign up for a Tiny Cloud account](https://www.tiny.cloud/pricing/) to receive a Tiny Cloud API key. +2. `npm install @tinymce/tinymce-react` +3. Include the following code: + +```js +import React from 'react'; +import { Editor } from '@tinymce/tinymce-react'; + +export default function App() { + return ( + <> +

TinyMCE React demo

+ + + ); +} +``` + +4. Update the `apiKey` prop on the `Editor` component to include your Tiny Cloud API key. + +For more information: [Using TinyMCE with React - Cloud CDN](https://www.tiny.cloud/docs/tinymce/latest/react-cloud/) + +### Self hosted via NPM package + +Using TinyMCE from NPM in a React project requires a couple of extra steps. See the documentation for more information: [Using TinyMCE with React - Self hosted via NPM](https://www.tiny.cloud/docs/tinymce/latest/react-pm/) + + +## Demos + +For our quick demos, check out the TinyMCE React [Storybook](https://tinymce.github.io/tinymce-react/). + +## Detailed documentation + +* [TinyMCE React Technical Reference](https://www.tiny.cloud/docs/tinymce/latest/react-ref/). +* [TinyMCE Documentation](https://www.tiny.cloud/docs/tinymce/latest/). + + +## Issues + +Have you found an issue with `tinymce-react` or do you have a feature request? +Open up an [issue](https://github.com/tinymce/tinymce-react/issues) and let us know +or submit a [pull request](https://github.com/tinymce/tinymce-react/pulls). + +_Note: for issues concerning TinyMCE please visit the [TinyMCE repository](https://github.com/tinymce/tinymce)._ + + +## License + +`tinymce-react` is licensed under the MIT License. See the LICENSE.txt file for details. + +Depending on use case, the TinyMCE core editor can be used under either GPL-2.0-or-later or a commercial license. See the [tinymce package](https://www.npmjs.com/package/tinymce) for details. diff --git a/contributing.md b/contributing.md index b561e52d5..c970dad8e 100644 --- a/contributing.md +++ b/contributing.md @@ -1,8 +1,5 @@ # Contributing -This project follows the [beehive-flow](https://github.com/tinymce/beehive-flow/) branching process ("Basic process - release from main branch"). -Please read the [beehive-flow readme](https://github.com/tinymce/beehive-flow/blob/main/README.md) for more information. -This mainly affects branching and merging for Tiny staff. +External contributors are free to submit PRs against the `main` branch. -External contributors are free to submit PRs against the `main` branch. -Note that contributions will require signing of our Contributor License Agreement. +In order for Tiny to accept your contribution, we will need legal permission from you to use your code. Please email `legal@tiny.cloud` with the subject `CLA request for [your Github username]`. Our legal team will send you a Contributors License Agreement (CLA) which you will need to sign and return. diff --git a/package.json b/package.json index b627bd6e9..6f493a3a9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "description": "Official TinyMCE React Component", + "description": "Official React component for TinyMCE, a rich text editor.", "repository": { "url": "https://github.com/tinymce/tinymce-react" }, @@ -22,7 +22,16 @@ "build-storybook": "storybook build", "deploy-storybook": "storybook build && gh-pages -d ./storybook-static -u 'tiny-bot ' --nojekyll" }, - "keywords": [], + "keywords": [ + "tinymce", + "react", + "component", + "editor", + "wysiwyg", + "rich-text-editor", + "html", + "contenteditable" + ], "author": "Ephox Corporation DBA Tiny Technologies, Inc.", "license": "MIT", "dependencies": { From b36ff774c50577e87689b98d0b1351316dd93977 Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Thu, 24 Sep 2026 12:03:41 +0930 Subject: [PATCH 02/15] More changes to package.json --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 04b0bac63..49dead5f5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## About - Official React component for TinyMCE, the rich text editor. It makes integrating TinyMCE into React applications easy and seamless. + Official React component for TinyMCE rich text edito makes integrating TinyMCE into React applications easy and seamless. It automatically pulls `tinymce` from the Tiny Cloud CDN unless configured to use a different setup, such as self-hosting the [tinymce NPM package](https://www.npmjs.com/package/tinymce) ## Quickstart diff --git a/package.json b/package.json index 6f493a3a9..d063adf32 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "description": "Official React component for TinyMCE, a rich text editor.", + "description": "Official React component for TinyMCE rich text editor.", "repository": { "url": "https://github.com/tinymce/tinymce-react" }, From f1b5df793027eb91db37ff055e3853477849516f Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Thu, 24 Sep 2026 14:02:03 +0930 Subject: [PATCH 03/15] Minor changes --- CHANGELOG.md | 2 +- README.md | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60031a4e6..69e52e8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Change log +# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), diff --git a/README.md b/README.md index 49dead5f5..69ed21dcb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Official TinyMCE React Component +# Official React Component for TinyMCE ## About - Official React component for TinyMCE rich text edito makes integrating TinyMCE into React applications easy and seamless. It automatically pulls `tinymce` from the Tiny Cloud CDN unless configured to use a different setup, such as self-hosting the [tinymce NPM package](https://www.npmjs.com/package/tinymce) +Official React component for TinyMCE, the rich text editor. It wraps TinyMCE as a React `` component, supporting controlled and uncontrolled usage. By default, it pulls TinyMCE from the Tiny Cloud CDN unless configured to use a different setup, such as self-hosting the [tinymce NPM package](https://www.npmjs.com/package/tinymce). ## Quickstart diff --git a/package.json b/package.json index d063adf32..0a6c19c50 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "description": "Official React component for TinyMCE rich text editor.", + "description": "Official React component for TinyMCE, the rich text editor.", "repository": { "url": "https://github.com/tinymce/tinymce-react" }, From 23aad019c84fc841c0d76c9fbfb7cd5981c335ec Mon Sep 17 00:00:00 2001 From: tiny-ben-tran Date: Thu, 24 Sep 2026 14:55:38 +0930 Subject: [PATCH 04/15] Update package.json Co-authored-by: Millie --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a6c19c50..352d6cf58 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "description": "Official React component for TinyMCE, the rich text editor.", + "description": "Official React component for the TinyMCE rich text editor.", "repository": { "url": "https://github.com/tinymce/tinymce-react" }, From 03a307623a7c76a2d56ebf75617bee0536bb58b5 Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Thu, 24 Sep 2026 16:48:04 +0930 Subject: [PATCH 05/15] Fix quick start --- README.md | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 69ed21dcb..5f57546ae 100644 --- a/README.md +++ b/README.md @@ -8,32 +8,30 @@ Official React component for TinyMCE, the rich text editor. It wraps TinyMCE as ### Cloud CDN -In your React project: - 1. [Sign up for a Tiny Cloud account](https://www.tiny.cloud/pricing/) to receive a Tiny Cloud API key. -2. `npm install @tinymce/tinymce-react` -3. Include the following code: - -```js -import React from 'react'; -import { Editor } from '@tinymce/tinymce-react'; - -export default function App() { - return ( - <> -

TinyMCE React demo

- - - ); -} -``` - -4. Update the `apiKey` prop on the `Editor` component to include your Tiny Cloud API key. +1. Then in your React project: + 1. Run `npm install @tinymce/tinymce-react` + 1. Include the following code: + + ```js + import React from 'react'; + import { Editor } from '@tinymce/tinymce-react'; + + export default function App() { + return ( + <> +

TinyMCE React demo

+ + + ); + } + ``` + 1. Update the `apiKey` prop on the `Editor` component to include your Tiny Cloud API key. For more information: [Using TinyMCE with React - Cloud CDN](https://www.tiny.cloud/docs/tinymce/latest/react-cloud/) From c3cebd04a6e93683ea6b3613751aa6cb1fe4b473 Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Mon, 28 Sep 2026 14:35:45 +0930 Subject: [PATCH 06/15] Update test platforms config to use lambdatest --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 101b5e83a..05f689ee4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,9 +5,9 @@ mixedBeehiveFlow( container: [ resourceRequestMemory: '3Gi', resourceLimitMemory: '3Gi' ], testPrefix: 'Tiny-React', platforms: [ - [ browser: 'chrome', headless: true ], + [ browser: 'chrome', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ], [ browser: 'firefox', provider: 'aws', buckets: 1 ], - [ browser: 'safari', provider: 'lambdatest', buckets: 1 ] + [ browser: 'safari', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ] ], customSteps: { stage("update storybook") { From 186ed73b930b332658eb4fd009bbee7797f3fbbb Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Wed, 30 Sep 2026 09:15:17 +0930 Subject: [PATCH 07/15] Attempt to fix failing tests --- src/test/ts/alien/Loader.tsx | 10 +++++++++- src/test/ts/browser/EditorBehaviorTest.ts | 5 +++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/test/ts/alien/Loader.tsx b/src/test/ts/alien/Loader.tsx index 67fd19b6f..c58775af5 100644 --- a/src/test/ts/alien/Loader.tsx +++ b/src/test/ts/alien/Loader.tsx @@ -1,4 +1,4 @@ -import { before, context } from '@ephox/bedrock-client'; +import { after, before, context } from '@ephox/bedrock-client'; import { Fun, Optional } from '@ephox/katamari'; import { Remove, SugarElement, SugarNode } from '@ephox/sugar'; import { VersionLoader } from '@tinymce/miniature'; @@ -109,5 +109,13 @@ export const withVersion = (version: Version, fn: (render: RenderWithVersion) => }); fn(render as RenderWithVersion); + + after(() => { + // remove script tag for version + const scriptTag = document.querySelector(`script[src*="tinymce-${version}"]`); + if (scriptTag) { + scriptTag.remove(); + } + }); }); }; diff --git a/src/test/ts/browser/EditorBehaviorTest.ts b/src/test/ts/browser/EditorBehaviorTest.ts index 9f4f25fc6..479bd6a75 100644 --- a/src/test/ts/browser/EditorBehaviorTest.ts +++ b/src/test/ts/browser/EditorBehaviorTest.ts @@ -89,7 +89,7 @@ describe('EditorBehaviourTest', () => { await ctx.reRender({ onSetContent: eventStore.createHandler('onSetContent') }); TinyAssertions.assertContent(ctx.editor, '

Initial Content

'); - await Waiter.pWait(0); // Wait for React's state updates to complete before setting new content + await Waiter.pWait(100); // Wait for React's state updates to complete before setting new content ctx.editor.setContent('

New Content

'); eventStore.each('onSetContent', (events) => { @@ -115,7 +115,7 @@ describe('EditorBehaviourTest', () => { eventStore.clearState(); ctx.editor.setContent('

Initial Content

'); await ctx.reRender({ onSetContent: eventStore.createHandler('NewHandler') }); - await Waiter.pWait(0); // Wait for React's state updates to complete before setting new content + await Waiter.pWait(100); // Wait for React's state updates to complete before setting new content ctx.editor.setContent('

New Content

'); eventStore.each('InitialHandler', (events) => { @@ -135,6 +135,7 @@ describe('EditorBehaviourTest', () => { eventStore.clearState(); }); + it('INT-3226: onEditorChange is triggered only once after calling insertContent', async () => { using ctx = await render({ onEditorChange: eventStore.createHandler('onEditorChange') }); const { editor } = ctx; From f889b69d5f72e448cefd549675d47cf16a42531c Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Wed, 30 Sep 2026 12:06:50 +0930 Subject: [PATCH 08/15] Fix other tests --- package.json | 5 +++-- src/test/ts/browser/EditorBehaviorTest.ts | 19 +++++++++++-------- src/test/ts/browser/LoadTinyTest.ts | 18 ++++++++++++++---- yarn.lock | 11 ++++++++--- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 352d6cf58..719a7f0fa 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "author": "Ephox Corporation DBA Tiny Technologies, Inc.", "license": "MIT", "dependencies": { - "prop-types": "^15.8.1" + "prop-types": "^15.8.1", + "tinymce-8": "npm:tinymce@^8.0.0" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0", @@ -83,7 +84,7 @@ "tinymce-6": "npm:tinymce@^6", "tinymce-7": "npm:tinymce@^7", "tinymce-7.5": "npm:tinymce@7.5", - "tinymce-8": "npm:tinymce@^8.0.0", + "tinymce-premium": "^8.9.2", "typescript": "~5.8.2", "vite": "^7.1.5" }, diff --git a/src/test/ts/browser/EditorBehaviorTest.ts b/src/test/ts/browser/EditorBehaviorTest.ts index 479bd6a75..cb1445cb7 100644 --- a/src/test/ts/browser/EditorBehaviorTest.ts +++ b/src/test/ts/browser/EditorBehaviorTest.ts @@ -1,6 +1,6 @@ import { Assertions, Waiter } from '@ephox/agar'; -import { describe, it } from '@ephox/bedrock-client'; +import { afterEach, describe, it } from '@ephox/bedrock-client'; import { TinyAssertions, TinySelections } from '@ephox/mcagar'; import { PlatformDetection } from '@ephox/sand'; import { EditorEvent, Events, Editor as TinyMCEEditor } from 'tinymce'; @@ -29,6 +29,10 @@ describe('EditorBehaviourTest', () => { const eventStore = EventStore(); + afterEach(() => { + eventStore.clearState(); + }); + VERSIONS.forEach((version) => Loader.withVersion(version, (render) => { it('Assert structure of tinymce and tinymce-react events', async () => { @@ -67,7 +71,6 @@ describe('EditorBehaviourTest', () => { Assertions.assertEq('First arg should be new content', '

Initial Content

', events[0].editorEvent); Assertions.assertEq('Second arg should be editor', true, isEditor(events[0].editor)); }); - eventStore.clearState(); }); it('onEditorChange should only fire when the editors content changes', async () => { @@ -81,7 +84,6 @@ describe('EditorBehaviourTest', () => { eventStore.each('onEditorChange', (events) => { Assertions.assertEq('onEditorChange should have been fired once', 1, events.length); }); - eventStore.clearState(); }); it('Should be able to register an event handler after initial render', async () => { @@ -99,7 +101,6 @@ describe('EditorBehaviourTest', () => { events[0].editorEvent.content ); }); - eventStore.clearState(); }); it('Providing a new event handler and re-rendering should unbind old handler and bind new handler', async () => { @@ -132,19 +133,21 @@ describe('EditorBehaviourTest', () => { events[0].editorEvent.content ); }); - - eventStore.clearState(); }); it('INT-3226: onEditorChange is triggered only once after calling insertContent', async () => { using ctx = await render({ onEditorChange: eventStore.createHandler('onEditorChange') }); const { editor } = ctx; editor.setContent('

abc

'); - await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => ctx.editor.getContent() === '

abc

'); + await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => { + return ctx.editor.getContent() === '

abc

'; + }); eventStore.clearState(); TinySelections.setSelection(editor, [ 0, 0 ], 1, [ 0, 0 ], 2); editor.insertContent('e'); - await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => ctx.editor.getContent() === '

aec

'); + await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => { + return ctx.editor.getContent() === '

aec

'; + }); eventStore.each('onEditorChange', (events) => { Assertions.assertEq( 'onEditorChange should have been triggered once', diff --git a/src/test/ts/browser/LoadTinyTest.ts b/src/test/ts/browser/LoadTinyTest.ts index f6865b85b..79767b160 100644 --- a/src/test/ts/browser/LoadTinyTest.ts +++ b/src/test/ts/browser/LoadTinyTest.ts @@ -40,9 +40,9 @@ describe('LoadTinyTest', () => { }); }); - CLOUD_VERSIONS.forEach((version) => { + ['7'].forEach((version) => { it(`Should be able to load TinyMCE from Cloud (${version})`, async () => { - const apiKey = 'a-fake-api-key'; + const apiKey = VALID_API_KEY; using _ = await render({ apiKey, cloudChannel: version }); assertTinymceVersion(version); Assertions.assertEq( @@ -56,9 +56,19 @@ describe('LoadTinyTest', () => { using _ = await render({ tinymceScriptSrc: [ `/project/node_modules/tinymce-${version}/tinymce.min.js`, - `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?tinydrive=${version}` + `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?tinydrive=${version}`, ], - plugins: [ 'tinydrive' ] + apiKey: VALID_API_KEY, + licenseKey: 'gpl', + plugins: [ 'tinydrive' ], + // TinyMCE 8 requires the Commercial License Key Manager to validate an apiKey. It's not + // served from Tiny Cloud, so for a self-hosted core it needs to load from the local + // `tinymce-premium` package instead. See https://www.tiny.cloud/docs/tinymce/latest/license-key/ + init: version === '8' ? { + external_plugins: { + licensekeymanager: '/project/node_modules/tinymce-premium/plugins/licensekeymanager/plugin.min.js' + } + } : {}, }); assertTinymceVersion(version); Assertions.assertEq( diff --git a/yarn.lock b/yarn.lock index bbfa5ff94..0657aec3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8756,9 +8756,14 @@ tinyglobby@^0.2.15: integrity sha512-zaOHwmiP1EqTeLRXAvVriDb00JYnfEjWGPdKEuac7MiZJ5aiDMZ4Unc98Gmajn+PBljOmO1GKV6G0KwWn3+k8A== "tinymce-8@npm:tinymce@^8.0.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-8.2.0.tgz#3fde03d91a1a8fd2d33de6a378843724e1341e2e" - integrity sha512-HZasRaAYQmePi+jjP4YFqHMtBJvbUaxDmjlXNf96uuyqsH6zZkGtJiFpsNi80Ec+L+snyF0OTDjrWVYDbkUrgQ== + version "8.9.2" + resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-8.9.2.tgz#0e6cb1d0230a27afe701651c1dbd99e2dac7ffdb" + integrity sha512-Qh6ADSC0x1F/KsrupfResNJFAaINJQBtfVjx5zxxWJ0ZrGtaKldtAXDMI6uEA57s2n+0rEtXmtbitiU4zV7+0Q== + +tinymce-premium@^8.9.2: + version "8.9.2" + resolved "https://registry.yarnpkg.com/tinymce-premium/-/tinymce-premium-8.9.2.tgz#e3beb35676d19709ba7ee33c5d54af46941084fa" + integrity sha512-9fx33G7Omixa7BlPqakx6xEs4jwQzA1EyPcS/5KenTWi+/yVEA2ILSkLeE4pfWW5qyY8ogdMYmGZDd476UJuEQ== tinymce@^8.0.2: version "8.2.0" From 7c53071517522abd213c57658939c6b3635442e4 Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Wed, 30 Sep 2026 12:17:56 +0930 Subject: [PATCH 09/15] Undo a mistake --- src/test/ts/browser/LoadTinyTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/ts/browser/LoadTinyTest.ts b/src/test/ts/browser/LoadTinyTest.ts index 79767b160..52c24b20c 100644 --- a/src/test/ts/browser/LoadTinyTest.ts +++ b/src/test/ts/browser/LoadTinyTest.ts @@ -40,7 +40,7 @@ describe('LoadTinyTest', () => { }); }); - ['7'].forEach((version) => { + CLOUD_VERSIONS.forEach((version) => { it(`Should be able to load TinyMCE from Cloud (${version})`, async () => { const apiKey = VALID_API_KEY; using _ = await render({ apiKey, cloudChannel: version }); From e1e59bbc0738b4a9e680c14f75f7de124e23e610 Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Wed, 30 Sep 2026 13:31:42 +0930 Subject: [PATCH 10/15] Add a workaround to handle fetching licensemanager --- src/test/ts/alien/Loader.tsx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/test/ts/alien/Loader.tsx b/src/test/ts/alien/Loader.tsx index c58775af5..f39a70d11 100644 --- a/src/test/ts/alien/Loader.tsx +++ b/src/test/ts/alien/Loader.tsx @@ -51,13 +51,25 @@ export const render = async (props: Partial = {}, container: HTMLElem .filter(SugarNode.isHTMLElement) .map((val) => val.dom) .fold(() => reject('Could not find DOMNode'), (DOMNode) => { - resolve({ - ref: ref as React.RefObject, - editor, - DOMNode, - }); - } - ); + // This is a workaround to avoid a race condition occurring in tinymce 8 where licenseKeyManager is still validating the license key + // after global tinymce is removed in a clean up. Specifically, it happens when unloading/loading different versions of TinyMCE + if (editor.licenseKeyManager) { + editor.licenseKeyManager.validate({}).then(() => { + resolve({ + ref: ref as React.RefObject, + editor, + DOMNode, + }); + // eslint-disable-next-line no-console + }).catch((reason) => console.warn(reason)); + } else { + resolve({ + ref: ref as React.RefObject, + editor, + DOMNode, + }); + } + }); }, 0); }); } From f0050ae3b269861dd24b6d016f78e309b37e6dd9 Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Wed, 30 Sep 2026 14:27:07 +0930 Subject: [PATCH 11/15] Replace tinydrive with casechange plugin hoping that it fixes tests on firefox --- src/test/ts/browser/LoadTinyTest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/ts/browser/LoadTinyTest.ts b/src/test/ts/browser/LoadTinyTest.ts index 52c24b20c..20c9e41c8 100644 --- a/src/test/ts/browser/LoadTinyTest.ts +++ b/src/test/ts/browser/LoadTinyTest.ts @@ -56,11 +56,11 @@ describe('LoadTinyTest', () => { using _ = await render({ tinymceScriptSrc: [ `/project/node_modules/tinymce-${version}/tinymce.min.js`, - `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?tinydrive=${version}`, + `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?casechange=${version}`, ], apiKey: VALID_API_KEY, licenseKey: 'gpl', - plugins: [ 'tinydrive' ], + plugins: [ 'casechange' ], // TinyMCE 8 requires the Commercial License Key Manager to validate an apiKey. It's not // served from Tiny Cloud, so for a self-hosted core it needs to load from the local // `tinymce-premium` package instead. See https://www.tiny.cloud/docs/tinymce/latest/license-key/ @@ -77,9 +77,9 @@ describe('LoadTinyTest', () => { Global.tinymce.baseURI.path ); Assertions.assertEq( - 'The tinydrive plugin should have defaults for the cloud', - `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/tinydrive/${version}/plugin.min.js`, - (Global.tinymce.defaultOptions || Global.tinymce.defaultSettings)?.custom_plugin_urls?.tinydrive + 'The casechange plugin should have defaults for the cloud', + `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/casechange/${version}/plugin.min.js`, + (Global.tinymce.defaultOptions || Global.tinymce.defaultSettings)?.custom_plugin_urls?.casechange ); }); }); From cc58d091d57d9a60b31811bda69224cbf30e6b8c Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Wed, 30 Sep 2026 15:16:32 +0930 Subject: [PATCH 12/15] Use checklist as it exists across all versions --- src/test/ts/browser/LoadTinyTest.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/test/ts/browser/LoadTinyTest.ts b/src/test/ts/browser/LoadTinyTest.ts index 20c9e41c8..376ff4e61 100644 --- a/src/test/ts/browser/LoadTinyTest.ts +++ b/src/test/ts/browser/LoadTinyTest.ts @@ -56,17 +56,17 @@ describe('LoadTinyTest', () => { using _ = await render({ tinymceScriptSrc: [ `/project/node_modules/tinymce-${version}/tinymce.min.js`, - `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?casechange=${version}`, + `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?checklist=${version}` ], apiKey: VALID_API_KEY, licenseKey: 'gpl', - plugins: [ 'casechange' ], + plugins: [ 'checklist' ], // TinyMCE 8 requires the Commercial License Key Manager to validate an apiKey. It's not // served from Tiny Cloud, so for a self-hosted core it needs to load from the local // `tinymce-premium` package instead. See https://www.tiny.cloud/docs/tinymce/latest/license-key/ init: version === '8' ? { external_plugins: { - licensekeymanager: '/project/node_modules/tinymce-premium/plugins/licensekeymanager/plugin.min.js' + licensekeymanager: '/project/node_modules/tinymce-premium/plugins/licensekeymanager/plugin.min.js', } } : {}, }); @@ -76,10 +76,11 @@ describe('LoadTinyTest', () => { `/project/node_modules/tinymce-${version}`, Global.tinymce.baseURI.path ); + Assertions.assertEq( - 'The casechange plugin should have defaults for the cloud', - `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/casechange/${version}/plugin.min.js`, - (Global.tinymce.defaultOptions || Global.tinymce.defaultSettings)?.custom_plugin_urls?.casechange + 'The checklist plugin should have been loaded from Cloud', + `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/checklist/${version}`, + Global.tinymce.PluginManager.urls.checklist ); }); }); From 15810490f6d799663f2378bc603aab2fb9db88cd Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Wed, 30 Sep 2026 15:45:13 +0930 Subject: [PATCH 13/15] Try formatpainter --- src/test/ts/browser/LoadTinyTest.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/ts/browser/LoadTinyTest.ts b/src/test/ts/browser/LoadTinyTest.ts index 376ff4e61..561cde7a6 100644 --- a/src/test/ts/browser/LoadTinyTest.ts +++ b/src/test/ts/browser/LoadTinyTest.ts @@ -56,11 +56,11 @@ describe('LoadTinyTest', () => { using _ = await render({ tinymceScriptSrc: [ `/project/node_modules/tinymce-${version}/tinymce.min.js`, - `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?checklist=${version}` + `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?formatpainter=${version}` ], apiKey: VALID_API_KEY, licenseKey: 'gpl', - plugins: [ 'checklist' ], + plugins: [ 'formatpainter' ], // TinyMCE 8 requires the Commercial License Key Manager to validate an apiKey. It's not // served from Tiny Cloud, so for a self-hosted core it needs to load from the local // `tinymce-premium` package instead. See https://www.tiny.cloud/docs/tinymce/latest/license-key/ @@ -76,11 +76,11 @@ describe('LoadTinyTest', () => { `/project/node_modules/tinymce-${version}`, Global.tinymce.baseURI.path ); - + console.log('PluginManager.urls.formatpainter: ', Global.tinymce.PluginManager.urls.formatpainter); Assertions.assertEq( - 'The checklist plugin should have been loaded from Cloud', - `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/checklist/${version}`, - Global.tinymce.PluginManager.urls.checklist + 'The formatpainter plugin should have been loaded from Cloud', + `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/formatpainter/${version}`, + Global.tinymce.PluginManager.urls.formatpainter ); }); }); From b5ea7ac475627f0cf8a96de35436f6447a87c44e Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Wed, 30 Sep 2026 15:50:45 +0930 Subject: [PATCH 14/15] fix lint --- src/test/ts/browser/LoadTinyTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/ts/browser/LoadTinyTest.ts b/src/test/ts/browser/LoadTinyTest.ts index 561cde7a6..902de8915 100644 --- a/src/test/ts/browser/LoadTinyTest.ts +++ b/src/test/ts/browser/LoadTinyTest.ts @@ -76,7 +76,6 @@ describe('LoadTinyTest', () => { `/project/node_modules/tinymce-${version}`, Global.tinymce.baseURI.path ); - console.log('PluginManager.urls.formatpainter: ', Global.tinymce.PluginManager.urls.formatpainter); Assertions.assertEq( 'The formatpainter plugin should have been loaded from Cloud', `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/formatpainter/${version}`, From fc3873f402fa1125f3bc778c3dd33ecd4231608f Mon Sep 17 00:00:00 2001 From: Ben Tran Date: Thu, 1 Oct 2026 07:56:36 +0930 Subject: [PATCH 15/15] Load formatpainter through external_plugins --- src/test/ts/browser/LoadTinyTest.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/test/ts/browser/LoadTinyTest.ts b/src/test/ts/browser/LoadTinyTest.ts index 902de8915..de531e9e0 100644 --- a/src/test/ts/browser/LoadTinyTest.ts +++ b/src/test/ts/browser/LoadTinyTest.ts @@ -55,8 +55,7 @@ describe('LoadTinyTest', () => { it(`Should be able to load TinyMCE (${version}) in hybrid`, async () => { using _ = await render({ tinymceScriptSrc: [ - `/project/node_modules/tinymce-${version}/tinymce.min.js`, - `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?formatpainter=${version}` + `/project/node_modules/tinymce-${version}/tinymce.min.js` ], apiKey: VALID_API_KEY, licenseKey: 'gpl', @@ -67,8 +66,13 @@ describe('LoadTinyTest', () => { init: version === '8' ? { external_plugins: { licensekeymanager: '/project/node_modules/tinymce-premium/plugins/licensekeymanager/plugin.min.js', + formatpainter: `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/formatpainter/${version}/plugin.min.js` } - } : {}, + } : { + external_plugins: { + formatpainter: `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/formatpainter/${version}/plugin.min.js`, + } + }, }); assertTinymceVersion(version); Assertions.assertEq(