Testing environment
The CKEditor 5 testing environment lets you run the project’s automated (unit) and manual tests. This article explains how to run them.
Before reading this article we recommend getting familiar with the CKEditor 5 development environment.
The CKEditor 5 testing environment uses Vitest running automated tests in real browsers (the browser mode), while manual tests are served by a Vite-based server. We created some npm scripts which glue all these pieces and special requirements for CKEditor together.
Each CKEditor 5 package has its own tests suite (see for example the engine’s tests) together with its own Vitest configuration and test script. Automated tests are executed directly with pnpm, without any custom test runner. The custom Vitest matchers are implemented in the @ckeditor/ckeditor5-dev-tests package, and the Vite plugins powering the manual test server are implemented in the @ckeditor/ckeditor5-dev-manual-server package. Both can be reused outside of ckeditor5.
Automated tests support TypeScript, and manual tests require it. Simply use the .ts extension.
Automated tests are executed with the packages’ own test scripts, run directly via pnpm. There is no dedicated test runner binary.
The root pnpm run test script is a thin wrapper that translates a shorthand package selection into pnpm filters. It accepts the following options:
--filter(alias-f) – Comma-separated short package names selecting the packages to test. Globs are allowed, for example-f editor-*.--coverage(alias-c) – Runs thecoveragescript of the selected packages instead oftest.--attempts– The number of attempts for each package’s test run. Failed runs are retried per package. It is meant for continuous integration environments and defaults to1.
All remaining arguments are passed to Vitest. Positional arguments are treated by Vitest as test file filters (they match a part of the test file path). You can also pass any other Vitest CLI option.
You can run the automated tests for the whole repository, a single package, a directory, or a single file:
| Scope | Command |
|---|---|
| The whole repository | pnpm run test (it sequentially runs the test script of every package) |
| A single package | pnpm run test -f engineor natively: pnpm --filter ckeditor5-engine run test |
| Multiple packages | pnpm run test -f editor-*,core |
| A directory inside a package | pnpm run test -f engine tests/view |
| A single file | pnpm run test -f basic-styles tests/bold.js |
Apart from the test and coverage scripts, each package provides the following scripts:
test:browser– Runs the tests in a visible (non-headless) browser.test:debug– Runs the tests in a visible browser in the watch mode. Useful for debugging with the browser developer tools.
Run all tests of the ckeditor5-core package with the code coverage check:
pnpm run test -c -f core
Run the engine’s view namespace tests:
pnpm run test -f engine tests/view
Run and debug the bold.js tests in the ckeditor5-basic-styles package in a visible browser:
pnpm --filter ckeditor5-basic-styles run test:debug tests/bold.js
The testing environment registers some custom Vitest matchers. There is no need to import them, as they are registered by default inside all tests.
Tests whether two given strings containing markup language are equal. Unlike expect().toEqual(), this matcher formats the markup before showing a diff. It can be used to test HTML strings and strings containing a serialized model.
This assertion will pass:
expect( `<b>foo</b>` ).toEqualMarkup( `<b>foo</b>` )
This assertion will throw an error:
expect(
'<paragraph>foo bXXX[]r baz</paragraph>'
).toEqualMarkup(
'<paragraph>foo bYYY[]r baz</paragraph>'
);
To start the manual tests server, use the pnpm run manual task. It starts a Vite development server available at http://localhost:8125.
The task accepts the standard Vite CLI options, for example --port.
Debug flags are controlled with the CK_DEBUG environment variable. The base set of debug logs (// @if CK_DEBUG //) is always enabled. To uncomment additional debug code, pass a comma-separated list of flags, for example CK_DEBUG=engine pnpm run manual to enable the // @if CK_DEBUG_ENGINE // lines in the code.
A manual test consists of 2 files:
- A
<name>.manual.htmlfile – a complete HTML document (with the DOCTYPE,<head>, and<body>) that you fully own. You can freely add a Content Security Policy<meta>tag, external scripts,<style>, or<link>tags in the<head>. The.manual.htmlsuffix is what marks the file as a manual test. - A
<name>.tsfile with the TypeScript part of the test (for example, the code initializing an editor). Reference it from the document with a<script type="module">tag.
Test instructions live inside the document in a <ck-manual-header> element – its children are rendered as a collapsible instructions panel. In the tests list, each test is identified by its file path relative to the tests/manual/ directory.
Only files with the .manual.html suffix are treated as manual tests. A plain .html file placed in a tests/manual/ directory is treated as a static fixture (for example, content loaded into an <iframe>) and is never registered as a test.
An example <name>.manual.html file, which also serves as a template for new tests:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Create a new link</title>
<script type="module" src="./link.ts"></script>
</head>
<body>
<ck-manual-header>
<h2>Create a new link</h2>
<ol>
<li>Select a fragment of the regular text.</li>
<li>Click the toolbar "Link" button.</li>
<li>Check if the balloon panel attached to the selection appeared.</li>
<li>Fill in the "Link URL" input in the panel.</li>
<li>Click the "Save" button.</li>
<li>Check if the selected text is converted into a link.</li>
</ol>
</ck-manual-header>
<div id="editor">...</div>
</body>
</html>
An example script file (link.ts):
import { ClassicEditor, Essentials, Paragraph } from 'ckeditor5';
ClassicEditor
.create( {
attachTo: document.querySelector( '#editor' ),
licenseKey: '<YOUR_LICENSE_KEY>', // Or 'GPL'.
plugins: [ Essentials, Paragraph ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
Do not forget to add all dependencies of your manual test as devDependencies (in package.json).
We recommend using the official CKEditor 5 inspector for development and debugging. It will give you tons of useful information about the state of the editor such as internal data structures, selection, commands, and many more.
The manual/ test directories should always be located in the root of the tests/ directories.
packages/ckeditor5-engine/tests/manual/view/focus.ts– correct path.packages/ckeditor5-engine/tests/view/manual/focus.ts– incorrect path.
To verify that all manual tests can be opened without any errors (the crawler does not execute the manual test steps, it just visits the page), you do not need to do that manually, page by page. Instead, there is a web crawler that automatically traverses the documentation and visits all pages that have been found. The crawler opens a headless Chromium browser and logs to the console any error that has been found.
To check manual tests, run:
pnpm run manual:verify
It builds the manual tests, starts a local preview server, and runs the crawler against it – no separate server is needed.
Read more about the crawler in the Verifying documentation guide.
To run the memory leak tests, use the pnpm run test:memory command. It builds a browser bundle, starts a local server, and runs the tests in headless Chromium.
The command accepts the following arguments:
--editor– A list of editor names to test. You can pass the option multiple times. Defaults toBalloonEditor,ClassicEditor,DecoupledEditor,InlineEditor, andMultiRootEditor.--html– The HTML file to load fromscripts/memory/assets. Defaults toindex.html.--no-build– Skips generating the browser build and reuses the existing editor assets.
Run all memory leak tests:
pnpm run test:memory
Test only the classic and inline editors:
pnpm run test:memory --editor ClassicEditor --editor InlineEditor
Use a custom HTML file from scripts/memory/assets:
pnpm run test:memory --html my-test.html
Reuse existing assets:
pnpm run test:memory --no-build
Each editor run consists of multiple create/destroy cycles. The warmup phase creates and destroys the editor a few times to populate caches and JIT state. The actual test then repeats the same create/destroy cycle more times while sampling memory between cycles. This makes the results less sensitive to first‑run effects and helps highlight steady growth trends.
After the run completes, the summary table reports:
- Baseline – The memory level after warmup. It is the reference point for the rest of the run and should already include initial cache effects.
- Growth – The difference between the final measurement and the baseline across repeated cycles. Use this to spot steady memory increases over time rather than one‑off spikes.
- Tail Growth – The spread within the last few measurements. It helps verify that memory stabilized near the end; large values suggest a still-growing footprint or high noise even after multiple cycles.
- Status –
OKwhen both Growth and Tail Growth stay below the threshold.Exceeds thresholdorErrormeans the run should be treated as a failure.
To ensure the highest quality, we maintain a complete test suite with a stable 100% code coverage for each of the packages. As of 2026, this means over 29,000 tests and the number is growing. Since every package is tested separately, we implement lower-level tests for libraries and higher-level tests for end-user features.
Such an extensive test suite requires a proper continuous integration service. We use CircleCI as a build platform. This service ensures a seamless and fast developer experience and allows us to focus on the job.
Besides automated tests, we also maintain a smaller set of manual tests. They help us verify whether something unexpected happens that might have been missed by the automated tests.
When proposing a pull request, make sure to add test(s) that verify it. Every code change should be accompanied by a test which proves that it is needed. Such a strict approach to testing ensures that we have not only 100% of code coverage (which is quite easy to achieve and gives only illusory safety) but also a high level of coverage for cases that we failed to notice initially (and might do that again in the future).
The conventions those tests should follow – how to organize them, name them, and write assertions, spies, and fake timers – are described in the Tests section of the code style guide.