Storybook Integration-Test Runtime
@coremedia/ckeditor5-storybook-itest is the Storybook runtime that the
Playwright integration tests in tests/playwright execute
against. It replaces the former example application (app) as the test target:
every Playwright test opens a dedicated, fully prepared story
(data, mock contents, read-only state, blocked words and drag sources all baked
in) and interacts with and asserts against the editor purely through
Playwright locators — no page.evaluate. Any value a test needs to read
back from the running editor is exposed by the story as an
observable DOM output that Playwright reads with a
locator.
The same Storybook is also published to GitHub Pages, so the test scenarios can be browsed interactively.
Contents
- Why Storybook?
- Getting Started
- Package Layout
- Stories
- Story ↔ Test Mapping
- Observable Outputs Harness
- Shadow DOM Scenarios
- How Playwright Targets Storybook
- CKEditor License (
.env) - GitHub Pages Deployment
Why Storybook?
Previously the Playwright tests ran against the app package and set up their
scenarios through JS-handle wrappers (JSWrapper and friends), which required
type workarounds for non-exported Playwright internals. The Storybook approach:
- gives each test a dedicated, isolated story (no shared application state);
- moves scenario setup into reusable Storybook-side utilities
(
src/setup,src/runtime) instead of Playwright wrapper classes; - bakes every scenario into the story declaratively via
ScenarioArgs(data, mock contents, read-only state, blocked words, drag sources, and even a prepared clipboard payload), so tests no longer arrange state at runtime; - exposes live editor/service values a test must read back through an
Observable Outputs Harness — stable
[data-test="…"]DOM elements read with locators — so tests run with locators only, nopage.evaluate; - shares the literals baked into each story (and asserted by its test) through
the
@coremedia/ckeditor5-itest-constantspackage, so story and test never drift.
Getting Started
All commands run from the repository root (or use --filter):
# Start the Storybook dev server on http://localhost:6006
pnpm --filter "@coremedia/ckeditor5-storybook-itest" run storybook
# Build the static Storybook into ./build/storybook
pnpm --filter "@coremedia/ckeditor5-storybook-itest" run build-storybook
# Type-check / lint
pnpm --filter "@coremedia/ckeditor5-storybook-itest" run typecheck
pnpm --filter "@coremedia/ckeditor5-storybook-itest" run lint
You normally do not start Storybook by hand for tests: the Playwright config starts it automatically (see below).
Package Layout
tests/storybook/
├─ .storybook/ Storybook configuration
│ ├─ main.ts Shared config + stories location, .env, license key
│ ├─ preview.ts Shared preview config; imports content-styles.css
│ ├─ content-styles.css Editing-view CSS (float/align/list/min-height, …)
│ ├─ core-icons.css Stand-in SVG icons for the CoreMedia Studio icon font
│ └─ framework/preset.js Local framework: HTML renderer + webpack builder
├─ src/
│ ├─ editors/ Editor factories (richtext, bbCode) + license helper
│ ├─ runtime/ Story runtime contract:
│ │ ├─ mountStory.ts Mounts a scenario into the story canvas
│ │ ├─ scenario.ts Scenario args (`ScenarioArgs`) + ready signalling
│ │ ├─ outputs.ts Observable Outputs Harness (`installOutputsHarness`)
│ │ ├─ shadowDom.ts Shadow-root style adoption + overlay container
│ │ └─ environment.ts Storybook URL / story-id helpers
│ └─ setup/ Reusable scenario setup utilities (migrated from the
│ former JS-handle wrappers)
└─ stories/
├─ scenarios/ Reusable editor scenario stories
├─ tests/ One dedicated story per Playwright test
├─ RuntimeContract.* Documents/validates the runtime contract
└─ Smoke.stories.ts Minimal smoke story
The Storybook configuration is built on the shared
@coremedia-internal/studio-client.storybook-config (addons, SWC compiler,
*.svg imports as raw source, fonts and docs styling). main.ts only adds what
is specific to this package: the stories/ location, the .env lookup and the
CKEDITOR_LICENSE_KEY define, applied on top of the shared webpackFinal.
The shared configuration requires a webpack-based Storybook framework, but
Storybook ships its HTML renderer only for Vite (@storybook/html-vite). The
local framework in .storybook/framework/preset.js therefore pairs the official
@storybook/html renderer with @storybook/builder-webpack5, so stories stay
plain HTML and keep importing their types from @storybook/html.
Stories
Test stories live under stories/tests/ and follow a single convention:
- Title:
Tests/<Name>(e.g.Tests/HelloEditor). - Exports: one named export per Playwright test case. Each Playwright
test opens the prepared story for exactly that case, so a story file has as
many exports as its test has scenarios (e.g.
Welcome,Cleared,ExternalLink,InternalLinkforHelloEditor). Story files that back a single test case keep a singleDefaultexport. - Story id: Storybook derives a stable, kebab-cased id from title + export.
Tests/HelloEditor→Welcomebecomestests-helloeditor--welcome;Tests/Images→OpenInTabEnabledbecomestests-images--open-in-tab-enabled.
Each export is fully prepared: its ScenarioArgs bake in everything the
test needs (data, mock contents, read-only state, blocked words, drag sources,
…). Tests never arrange editor state at runtime — they only open the story and
read/assert through locators.
Because CSF requires statically analysable named exports, parametrised story
families keep their per-case fixtures in a shared table in
@coremedia/ckeditor5-itest-constants and expose each case through a small
factory plus an explicit one-line export, e.g.:
const meta: Meta<ScenarioArgs> = {
title: "Tests/PasteButton",
render: (args) => mountScenario(createEditorScenario, args),
};
export default meta;
export const OneLink: Story = pasteButtonStory(pasteButtonScenario.oneLink);
export const SlowLinks: Story = pasteButtonStory(pasteButtonScenario.slowLinks);
The Playwright preview iframe URL for a story id is:
http://localhost:6006/iframe.html?id=<storyId>&viewMode=story
Story ↔ Test Mapping
Each *.test.ts in tests/playwright/test targets the Tests/<Name> story
group with the matching name; its individual test cases open the matching
prepared export via openStory(page, storyId). The story id of an export is
tests-<kebab(Name)>--<kebab(Export)>.
| Playwright test | Story group (Tests/…) |
Prepared exports (story id …--<kebab>) |
|---|---|---|
HelloEditor.test.ts |
HelloEditor |
Welcome, Cleared, ExternalLink, InternalLink |
Application.test.ts |
Application |
Default |
BBCode.test.ts |
BBCode |
BoldWord |
ContentLink.test.ts |
ContentLink |
RenderWithName, KeyboardButtons, EmptyUrlForm, EmptyUrlKeyboard, AddWithKeyboard, SelectFromSuggestions |
LinkBalloon.test.ts |
LinkBalloon |
Default |
LinkUserInteraction.test.ts |
LinkUserInteraction |
ExternalLink, ExternalLinkReadOnly, ContentLink, ContentLinkReadOnly |
Blocklist.test.ts |
Blocklist |
Default |
BlocklistCollapsed.test.ts |
BlocklistCollapsed |
Default |
BlocklistExpandedToolbar.test.ts |
BlocklistExpandedToolbar |
Default |
BlocklistExpandedKeyboard.test.ts |
BlocklistExpandedKeyboard |
Default |
Differencing.test.ts |
Differencing |
Addition, Removal, Change, Conflict, AllAttributes, FalsePositiveNewline, AddedNewline, ImageChangetype |
DocumentLists.test.ts |
DocumentLists |
one export per ol-*/ul-* case (e.g. OlContainsAttributes → tests-documentlists--ol-contains-attributes) |
DragDrop.test.ts |
DragDrop |
OneLink, SlowLinks, MultipleLinks, ExternalLink, AlreadyImportedExternalLink, …, OneImage, MultipleImages, SlowImages |
FontMapper.test.ts |
FontMapper |
WordTemplate, WordTemplateTable, WordTemplateTableInheritFont |
Images.test.ts |
Images |
NormalFast, SlowLoading, Unreadable, NoData, InvalidHref, Alignment, OpenInTabEnabled, OpenInTabDisabled, LinksNoLink, LinksWithLink |
PasteButton.test.ts |
PasteButton |
OneLink, SlowLinks, MultipleLinks, PasteViaKeyboardLink, OneImage, MultipleImages, SlowImages |
ShadowDom.test.ts |
ShadowDom |
Typing, HeadingDropdown, ContentLink |
Adding a test case: add a named export to
stories/tests/<Name>.stories.ts(sharing its fixture via@coremedia/ckeditor5-itest-constantswhen parametrised), then referencetests-<name>--<kebab(export)>from the corresponding test case.
Observable Outputs Harness
Tests run with Playwright locators only — no page.evaluate. When a test
must read a live value back from the running editor or a mock service, the story
publishes it through the Observable Outputs Harness: stable, hidden DOM
elements addressed by data-test ids, which Playwright reads with a locator.
installOutputsHarness(editor, element, args)(src/runtime/outputs.ts) wires the requested outputs into<pre data-test="…">elements inside the outputs container (OUTPUTS_CONTAINER_CLASS).- The available outputs (
OUTPUT_TEST_IDSin@coremedia/ckeditor5-itest-constants) are:editor-data— the currenteditor.getData().data-view— the renderedrichtext:toViewdata view (re-set from the story’s original loadeddata, sincegetData()strips editing-only augmentations such as<xdiff:span>).last-opened-entities— the awaited result of the work-area service’sgetLastOpenedEntities().is-droppable-state/is-droppable-in-link-balloon— drag-and-drop droppability signals.
- On the Playwright side,
tests/playwright/test/locators/outputs.tsexposes matching locator readers (editorData,dataView,lastOpenedEntities,isDroppableState,isDroppableInLinkBalloon).
Stories can also bake in a prepared clipboard payload: when a scenario’s
clipboard arg is set, the runtime writes that text/html (or other) item to
the browser clipboard while mounting (src/setup/clipboard.ts). This lets the
clipboard-driven FontMapper test paste a fully prepared Word document and
assert through the editor-data output — so it, too, runs with locators
only, no page.evaluate (FontMapper.test.ts grants clipboard-read and
clipboard-write to its browser context via test.use({ permissions })).
Shadow DOM Scenarios
A scenario with shadowDom: true mounts the editor inside an open shadow root,
following CKEditor’s
shadow DOM guide
(src/runtime/shadowDom.ts):
- the scenario container (readiness attribute, outputs harness) stays in the
light DOM; the editor host lives in the shadow root of a
SHADOW_HOST_CLASSelement inside it; - page style sheets do not cross the shadow boundary, so all document style
sheets are mirrored into every shadow root as constructed style sheets and
kept in sync by a
MutationObserverondocument.head. These includeckeditor5.css(imported by the editor factories),.storybook/content-styles.css(imported by the preview) and the plugin styles, which the plugins load as side-effect imports (e.g.theme/linkformviewextension.css); - the floating UI renders into a separate shadow root on an
OVERLAY_HOST_CLASSelement at the end ofdocument.body, configured asconfig.ui.overlayContainer.
Playwright CSS and role locators pierce open shadow roots, so existing locators
keep working; shadowHost(page) and overlayHost(page)
(tests/playwright/test/locators/storybook.ts) scope assertions to either tree.
How Playwright Targets Storybook
Playwright no longer talks to the app. Instead:
tests/playwright/playwright.config.tsbuilds on the shared@coremedia-internal/studio-client.playwright-config(createPlaywrightConfig) and declares a singlewebServerthat runspnpm run webserver:storybook(which starts this package’s Storybook dev server) and waits forhttp://localhost:6006.tests/playwright/test/storybook/environment.tsbuilds the story preview URL (storyUrl(storyId)).tests/playwright/test/storybook/mountStory.ts#openStorynavigates to that iframe URL and waits for the scenario-ready signal (data-editor-ready="true").
Run the tests as usual:
# Full suite (starts Storybook automatically)
pnpm --filter "@coremedia/ckeditor5-playwright-itest" run ui-test
# A single test file (one Playwright project per *.test.ts)
pnpm --filter "@coremedia/ckeditor5-playwright-itest" run ui-test --project=HelloEditor.test.ts
Useful environment variables: STORYBOOK_PORT/STORYBOOK_HOST, and — read by
the shared Playwright config — PLAYWRIGHT_RETRIES, PLAYWRIGHT_TIMEOUT_FACTOR
and CDP_ENDPOINT (drive an already running browser over the Chrome DevTools
Protocol). Tests run headless unless you pass --headed (or use --ui).
CKEditor License (.env)
The editor factories read CKEDITOR_LICENSE_KEY (use GPL for the GNU GPL).
.storybook/main.ts resolves it from the first available .env:
tests/storybook/.env(local to this package),app/.env,- the workspace-root
.env.
In CI the value comes from the CKEDITOR_LICENSE secret, written to the
workspace-root .env by the build action (.github/actions/build).
Never publish a Storybook built with that key: the key is inlined into the
bundle. Builds meant for publishing use GPL.
GitHub Pages Deployment
The deploy-storybook action (.github/actions/deploy-storybook) builds this
Storybook and publishes it to storybook/<branch>/ on the gh-pages branch:
for every pull request from this repository (pull-request.yml) and for main
after each integration (qa-gate.yml). on-delete-branch.yml removes the
Storybooks of deleted branches.
- Live URL: https://coremedia.github.io/ckeditor-plugins/storybook/ lists
all deployed branches;
…/storybook/main/is the currentmain. - License: the deployed build uses
CKEDITOR_LICENSE_KEY=GPL; the action fails if the commercial key ends up in the bundle. - Base path: none required — the static build uses relative asset paths, so it works unchanged under any subpath.