Sign up (with export icon)

Testing environment

Contribute to this guideShow the table of contents

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.

Introduction

Copy link

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.

Note

Automated tests support TypeScript, and manual tests require it. Simply use the .ts extension.

Running automated tests

Copy link

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 the coverage script of the selected packages instead of test.
  • --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 to 1.

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 engine
or 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.

Examples

Copy link

Run all tests of the ckeditor5-core package with the code coverage check:

pnpm run test -c -f core
Copy code

Run the engine’s view namespace tests:

pnpm run test -f engine tests/view
Copy code

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
Copy code

Custom Vitest matchers

Copy link

The testing environment registers some custom Vitest matchers. There is no need to import them, as they are registered by default inside all tests.

toEqualMarkup

Copy link

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>` )
Copy code

This assertion will throw an error:

expect(
    '<paragraph>foo bXXX[]r baz</paragraph>'
).toEqualMarkup(
    '<paragraph>foo bYYY[]r baz</paragraph>'
);
Copy code

Running manual tests

Copy link

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.

Creating a manual test

Copy link

A manual test consists of 2 files:

  • A <name>.manual.html file – 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.html suffix is what marks the file as a manual test.
  • A <name>.ts file 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.

Note

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>
Copy code

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 );
    } );
Copy code
Note

Do not forget to add all dependencies of your manual test as devDependencies (in package.json).

Note

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.

Note

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.

Verifying all manual tests

Copy link

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
Copy code

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.

Running memory leak tests

Copy link

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 to BalloonEditor, ClassicEditor, DecoupledEditor, InlineEditor, and MultiRootEditor.
  • --html – The HTML file to load from scripts/memory/assets. Defaults to index.html.
  • --no-build – Skips generating the browser build and reuses the existing editor assets.

Examples

Copy link

Run all memory leak tests:

pnpm run test:memory
Copy code

Test only the classic and inline editors:

pnpm run test:memory --editor ClassicEditor --editor InlineEditor
Copy code

Use a custom HTML file from scripts/memory/assets:

pnpm run test:memory --html my-test.html
Copy code

Reuse existing assets:

pnpm run test:memory --no-build
Copy code

Interpreting the results

Copy link

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.
  • StatusOK when both Growth and Tail Growth stay below the threshold. Exceeds threshold or Error means the run should be treated as a failure.

Test suite and CI

Copy link

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.