ckeditor/ckeditor5-react

Support running tests in Jest

Reinmar opened this issue · 7 comments

Problem

Jest is the default test runner used by many React apps. Unfortunately, Jest does not use a real browser – instead, it runs tests in Node.js with the use of JSDOM. JSDOM is not a complete DOM implementation and while it's apparently sufficient for standard apps, it's not able to polyfill all the DOM APIs that CKEditor 5 requires. Thus, even if someone solves compilation issues, then running CKEditor 5 tests for real in Jest will not work.

Example error:

console.log src/App.js:84
    CKEditor (1): Error. TypeError: Cannot read property 'substr' of undefined
        at startsWithFiller (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/view/filler.js:92:45)
        at getDataWithoutFiller (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/view/filler.js:119:7)
        at DomConverter._processDataFromDomText (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/view/domconverter.js:1072:10)
        at DomConverter.domToView (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/view/domconverter.js:413:27)
        at DomConverter.child [as domChildrenToView] (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/view/domconverter.js:472:27)
        at domChildrenToView.next (<anonymous>)
        at DomConverter.domToView (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/view/domconverter.js:451:17)
        at DomConverter.child [as domChildrenToView] (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/view/domconverter.js:472:27)
        at domChildrenToView.next (<anonymous>)
        at DomConverter.domToView (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/view/domconverter.js:451:17)
        at HtmlDataProcessor.toView (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js:79:29)
        at DataController.parse (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/controller/datacontroller.js:392:47)
        at Object.callback (/Users/pomek/Projects/pomek/react-ckeditor5-eject/node_modules/@ckeditor/ckeditor5-engine/src/controller/datacontroller.js:314:25)

Solution(s)

I don't think there's a solution for running all kind of CKEditor 5 tests in Jest. However, there are a couple things that could be done:

  • CKEditor 5 consists of pure-JS model and view implementations. Many of our tests rely on these two levels only (they skip the DOM) and the same could be true for developers integrating CKEditor 5. So, we could expose and document how to use a virtual editor in the tests.
  • We could investigate the most common JSDOM incompatibilities that break CKEditor 5 and try to figure out how to resolve them. One option would be to contribute to JSDOM, the other options is to add some hacks in our code for these specific issues.
  • We could document how to use Karma instead of Jest at least for CKEditor 5 tests. That's what worked best so far.

What else?


If you'd like to have this issue resolved please react with 👍 to this ticket.

pomek commented

I was able to run a single test case using VirtualTestEditor which does not use DOM.

I decided to substitute the ClassicEditor class (which I use in my app) with the VirtualTestEditor class in tests. It can be done using the #moduleNameMapper option in the Jest configuration.

My configuration in package.json:

{
    "jest":{
        "roots":[
            "<rootDir>/src"
        ],
        "collectCoverageFrom":[
            "src/**/*.{js,jsx,ts,tsx}",
            "!src/**/*.d.ts"
        ],
        "setupFiles":[
            "react-app-polyfill/jsdom"
        ],
        "setupFilesAfterEnv":[
            "<rootDir>/src/setupTests.js"
        ],
        "testMatch":[
            "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
            "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
        ],
        "testEnvironment":"jest-environment-jsdom-fourteen",
        "transform":{
            "^.+\\.(js|jsx|ts|tsx)$":"<rootDir>/node_modules/babel-jest",
            "^.+\\.(svg)$":"<rootDir>/node_modules/jest-raw-loader",
            "^.+\\.css$":"<rootDir>/config/jest/cssTransform.js",
            "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)":"<rootDir>/config/jest/fileTransform.js"
        },
        "transformIgnorePatterns":[
            "\\\\/node_modules\\\\/(?!@ckeditor).+\\.(js|jsx|ts|tsx|svg)$",
            "^.+\\.module\\.(css|sass|scss)$"
        ],
        "modulePaths":[],
        "moduleNameMapper":{
            "^react-native$":"react-native-web",
            "^.+\\.module\\.(css|sass|scss)$":"identity-obj-proxy",
            "classiceditor":"<rootDir>/node_modules/@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor"
        },
        "moduleFileExtensions":[
            "web.js",
            "js",
            "web.ts",
            "ts",
            "web.tsx",
            "tsx",
            "json",
            "web.jsx",
            "jsx",
            "node"
        ],
        "watchPlugins":[
            "jest-watch-typeahead/filename",
            "jest-watch-typeahead/testname"
        ]
    }
}

The <App> component:

import React, { Component } from 'react';

import { CKEditor } from '@ckeditor/ckeditor5-react';

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

const editorConfiguration = {
	plugins: [
		Essentials,
		Bold,
		Italic,
		Paragraph
	],
	toolbar: [ 'bold', 'italic' ]
};


class App extends Component {
	render() {
		return (
			<div className="App">
				<h2>Using CKEditor 5 from source in React</h2>

				<div className="ckeditor-component" data-ckeditor-class-name={ ClassicEditor.name }>
					<CKEditor
						editor={ ClassicEditor }
						config={ editorConfiguration }
						data="<p>Hello from CKEditor 5!</p>"
					/>
				</div>
			</div>
		);
	}
}

export default App;

And a test that is saved into two files: App.test.js, and App2.test.js

import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test( 'renders the <CKEditor> component', () => {
	const { container } = render( <App/> );
	const editorWrapper = container.querySelector( '.ckeditor-component' );

	expect( editorWrapper ).toBeInTheDocument();
	expect( editorWrapper.getAttribute( 'data-ckeditor-class-name' ) ).toEqual( 'VirtualTestEditor' );
} );

I am using two of the same files in order to check whether the ckeditor-duplicated-modules error occurs.

My results:

image

We should start thinking about publishing our dev-utils.

Edit: My project followed the Integrating CKEditor 5 built from source section in the React component guide.

@pomek is there any chance you could share a minimal complete example of the test setup you have, perhaps in a separate repo? As far as I can tell my code is doing more or less the same things you're doing here but I'm still getting the 'duplicate modules' error (#219) so I'm curious what the difference is between my setup and yours.

pomek commented

@aliceriot, I published my example application here – https://github.com/pomek/react-ckeditor5-eject.

In the README you will find everything that would help to execute the test.

I installed this @ckeditor/ckeditor5-core package, but there is no tests folder there to map to virtual editor, maybe it was removed in some new version?

tony commented

If anyone has a recent example of the latest Jest + latest CKEditor + and TypeScript, it'd be appreciated.

My ck-editor version is 35.x I'm trying update to last version to get last improviments, but my app do not run tests anymore using jest. I have been recived the following css erros:

console.error Error: Could not parse CSS stylesheet at exports.createStylesheet (***\node_modules\jsdom\lib\jsdom\living\helpers\stylesheets.js:34:21) at HTMLStyleElementImpl._updateAStyleBlock (***\node_modules\jsdom\lib\jsdom\living\nodes\HTMLStyleElement-impl.js:68:5) at HTMLStyleElementImpl._childTextContentChangeSteps (***\node_modules\jsdom\lib\jsdom\living\nodes\HTMLStyleElement-impl.js:36:12) at HTMLStyleElementImpl._insert (***\node_modules\jsdom\lib\jsdom\living\nodes\Node-impl.js:822:14) at HTMLStyleElementImpl._preInsert (***\node_modules\jsdom\lib\jsdom\living\nodes\Node-impl.js:756:10) at HTMLStyleElementImpl.insertBefore (***\node_modules\jsdom\lib\jsdom\living\nodes\Node-impl.js:593:17) at HTMLStyleElement.insertBefore (***\node_modules\jsdom\lib\jsdom\living\generated\Node.js:384:60) at insertBefore (***\ckeditor-custom\build\ckeditor.js:6:432142) at o (***\ckeditor-custom\build\ckeditor.js:6:432835) at f (***\ckeditor-custom\build\ckeditor.js:6:431391) { detail: '.ck-hidden{display:none!important}.ck-reset_all :not(.ck-reset_all-excluded *),.ck.ck-reset,.ck.ck-reset_all{box-sizing:border-box;height:auto;position:static;width:auto}:root{--ck-z-default:1;--ck-z-panel:calc(var(--ck-z-default) + 999);--ck-z-dialog:9999}.ck-transitions-disabled,.ck-transitions-disabled *{transition:none!important}:root{--ck-powered-by-line-height:10px;--ck-powered-by-padding-vertical:2px;--ck-powered-by-padding-horizontal:4px;--ck-powered-by-text-color:#4f4f4f;--ck-powered-by-border-radius:var(--ck-border-radius);--ck-powered-by-background:#fff;--ck-powered-by-border-color:var(--ck-color-focus-border)}.ck.ck-balloon-panel.ck-powered-by-balloon{--ck-border-radius:var(--ck-powered-by-border-radius);background:var(--ck-powered-by-background);box-shadow:none;min-height:unset;z-index:calc(var(--ck-z-panel) - 1)}.ck.ck-balloon-panel.ck-powered-by-balloon .ck.ck-powered-by{line-height:var(--ck-powered-by-line-height)}.ck.ck-balloon-panel.ck-powered-by-balloon .ck.ck-powered-by a{align-items:center;cursor:pointer;display:flex;filter:grayscale(80%);line-height:var(--ck-powered-by-line-height);opacity:.66;padding:var(--ck-powered-by-padding-vertical) var(--ck-powered-by-padding-horizontal)}.ck.ck-balloon-panel.ck-powered-by-balloon .ck.ck-powered-by .ck-powered-by__label{color:var(--ck-powered-by-text-color);cursor:pointer;font-size:7.5px;font-weight:700;letter-spacing:-.2px;line-height:normal;margin-right:4px;padding-left:2px;text-transform:uppercase}.ck.ck-balloon-panel.ck-powered-by-balloon .ck.ck-powered-by .ck-icon{cursor:pointer;display:block}.ck.ck-balloon-panel.ck-powered-by-balloon .ck.ck-powered-by:hover a{filter:grayscale(0);opacity:1}.ck.ck-balloon-panel.ck-powered-by-balloon[class*=position_inside]{border-color:transparent}.ck.ck-balloon-panel.ck-powered-by-balloon[class*=position_border]{border:var(--ck-focus-ring);border-color:var(--ck-powered-by-border-color)}:root{--ck-color-base-foreground:#fafafa;--ck-color-base-background:#fff;--ck-color-base-border:#ccced1;--ck-color-base-action:#53a336;--ck-color-base-focus:#6cb5f9;--ck-color-base-text:#333;--ck-color-base-active:#2977ff;--ck-color-base-active-focus:#0d65ff;--ck-color-base-error:#db3700;--ck-color-focus-border-coordinates:218,81.8%,56.9%;--ck-color-focus-border:hsl(var(--ck-color-focus-border-coordinates));--ck-color-focus-outer-shadow:#cae1fc;--ck-color-focus-disabled-shadow:rgba(119,186,248,.3);--ck-color-focus-error-shadow:rgba(255,64,31,.3);--ck-color-text:var(--ck-color-base-text);--ck-color-shadow-drop:rgba(0,0,0,.15);--ck-color-shadow-drop-active:rgba(0,0,0,.2);--ck-color-shadow-inner:rgba(0,0,0,.1);--ck-color-button-default-background:transparent;--ck-color-button-default-hover-background:#f0f0f0;--ck-color-button-default-active-background:#f0f0f0;--ck-color-button-default-disabled-background:transparent;--ck-color-button-on-background:#f0f7ff;--ck-color-button-on-hover-background:#dbecff;--ck-color-button-on-active-background:#dbecff;--ck-color-button-on-disabled-background:#f0f2f4;--ck-color-button-on-color:#2977ff;--ck-color-button-action-background:var(--ck-color-base-action);--ck-color-button-action-hover-background:#4d9d30;--ck-color-button-action-active-background:#4d9d30;--ck-color-button-action-disabled-background:#7ec365;--ck-color-button-action-text:var(--ck-color-base-background);--ck-color-button-save:#008a00;--ck-color-button-cancel:#db3700;--ck-color-switch-button-off-background:#939393;--ck-color-switch-button-off-ho

This is not my oficial app, is is a boilerplate created to try jest works, but a have the same issue in;

this happens from version 40.0.0

Yes please, we are trying to integrate CKEditor5 into a project that uses Jest heavily. We could use some guidance. Is it even supported? @pomek 's example from 2021 is a 404 now :)