🎬 codeceptjs-storyboard
A CodeceptJS plugin that turns one Gherkin scenario into one human-readable storyboard, straight from the real screenshots the test captures. Markdown is the default; select SVG explicitly when you want a self-contained picture.
ONE Gherkin sentence = ONE card = ONE pixel baseline, asserted inside its step at tolerance: 0: a visual regression fails on the exact sentence whose image drifted.
When the scenario ends, Markdown shows screenshots side by side with readable captions, notes and copyable commands. It links the original screenshots instead of embedding their bytes, so reviewers can open the file directly in GitLab. SVG embeds the screenshots and retains selectable text around them (Chromium/Safari).
Installation 📦
npm install --save-dev codeceptjs playwright @digital-commons-official/codeceptjs-storyboard @digital-commons-official/codeceptjs-visual-helper
npx playwright install chromium
Requires Node.js >= 22.19.0, CodeceptJS ^4.2.0 and
@digital-commons-official/codeceptjs-visual-helper
>= 1.3.0. The plugin captures and asserts through I.takeScreenshot,
I.captureScreenshot and I.assertVisualMatch.
VisualHelper is required: enable it in CodeceptJS as shown below. Keeping it as a peer dependency lets the plugin use your project's helper and CodeceptJS installation.
Tested versions
The browser suite installs the actual packed package into six fresh projects:
| Node.js | CodeceptJS | VisualHelper |
|---|---|---|
| 22.23.4, 24.22.1, 26.12.1 | 4.2.0 | 1.3.0 |
| 22.23.4, 24.22.1, 26.12.1 | 4.2.0 | 2.0.3 |
Each project imports its own four packages, passes npm audit, records one
English journey and three French Rule cases, then reruns with strict pixel
comparison. All fourteen baseline hashes must stay unchanged and match across
the six combinations. Each project also runs the default Markdown format against
those same hashes and checks all four Markdown gallery links.
Node 18 is rejected with --engine-strict; CodeceptJS 4.0 is rejected with
--strict-peer-deps. Older CodeceptJS 4.0 consumers also fail the security audit.
The declared ranges allow later compatible versions; the table states the exact
versions exercised in CI. Use the same Playwright browser and fonts when
comparing pictures across machines.
These installation tests use npm pack and real npm commands. They do not
authenticate a publication to the registry; only a successful release job
provides that separate proof. Releases use GitLab OIDC authentication without
a stored npm token and run only on the protected default branch. npm provenance
is explicitly disabled because npm rejects attestations from self-hosted
runners, so these releases do not include an npm provenance attestation.
Public API and compatibility
The package root exports the CodeceptJS plugin factory and these functions. Their signatures are defined in src/index.d.ts.
| Functions | Contract |
|---|---|
storyboardStep | Register a literal sentence and its optional note/copy annotation. |
capturePageFrame, captureElementFrame, frameOutputPath | Capture a real viewport or element and return its frame path. |
addStoryboardFrame, assertOrUpdateBaseline | Assert pixels strictly; only TASK_E2E_UPDATE_BASELINES=1 outside CI records baselines. |
begin, baseDir, panel, frame, annotate, panels | Manage a scenario's cards; frame attaches an image without asserting it. |
parseScenarioLines, render | Read the original Gherkin sentences and render Markdown or SVG. |
tryTo | Re-export the CodeceptJS effect for an optional action. |
The supported plugin options are format, theme, markdown and replayCommand; CodeceptJS owns
require and enabled. From 1.0.0, compatible releases preserve this public
API and the documented baseline recording behaviour. Removing a public
function, changing its required arguments or raising a supported minimum
requires a major release. Internal modules under src/ are implementation
details; use the package root.
The parser follows Cucumber's Gherkin dialects. Feature and Rule backgrounds, Scenario Outlines and Examples, inherited tags, repeated titles, data tables and doc strings are supported. Real browser journeys cover English, French and German; parser tests cover other dialects, including joined keywords. Tables and doc strings are step arguments, not extra cards. A filtered run with no matching scenario fails. Future dependency versions still need the same compatibility checks; the matrix above identifies the versions verified.
Configuration ⚙️
Register the plugin next to the visual helper in codecept.conf.js, and point Gherkin at your step files:
exports.config = {
output: './_output',
noGlobals: true,
helpers: {
Playwright: {
browser: 'chromium',
url: 'http://localhost:8080',
show: false,
windowSize: '1280x720'
},
VisualHelper: {
require: require.resolve('@digital-commons-official/codeceptjs-visual-helper'),
baselineDir: './screenshots/base/',
diffDir: './screenshots/diff/',
actualDir: './_output/',
tolerance: 0,
captureBaseline: false
}
},
gherkin: {
features: './features/**/*.feature',
steps: ['./steps/first-run.js']
},
plugins: {
storyboard: {
require: '@digital-commons-official/codeceptjs-storyboard',
enabled: true
}
}
}
Choose one format
Omit format or set format: 'markdown' for Markdown. Set format: 'svg' for SVG:
storyboard: {
require: '@digital-commons-official/codeceptjs-storyboard',
enabled: true,
format: 'svg'
}
Exactly one format is generated per run. Arrays, both, md, null and other values are rejected. Markdown is a breaking default change: add format: 'svg' to retain the earlier SVG files and HTML gallery. Existing standalone render('board.svg', options) calls still render SVG.
Paths are derived from the feature file location. Frames land in _output/storyboard-frames/ and baselines in screenshots/base/<feature-dir>/<scenario-tag>/.
| Output | Markdown (default) | SVG |
|---|---|---|
| Current scenario | _output/<feature-dir>/<scenario-tag>.md | _output/<feature-dir>/<scenario-tag>.svg |
| Reviewed scenario | storyboards/<feature-dir>/<scenario-tag>.md | storyboards/<feature-dir>/<scenario-tag>.svg |
| Current-run gallery | _output/index.md | _output/index.html |
| Reviewed gallery | storyboards/index.md | storyboards/index.html |
A scenario without captured frames produces no storyboard, so plain Gherkin tests are left untouched.
When the run ends, open the selected gallery to browse its storyboards. It groups scenarios by feature directory, puts failed groups and scenarios first, and shows the failed sentence with its replay command. Links open the selected format.
A filtered run lists only the scenarios it ran. In baseline-update mode, the reviewed gallery lists the passing scenarios from that update; failed scenarios never replace reviewed storyboards. Run the full suite in update mode to rebuild the complete reviewed gallery. Files from earlier runs or another format are not deleted or added to the current gallery.
Read Markdown on a branch or merge request
Commit the generated files under storyboards/ together with their reviewed screenshots under screenshots/base/.
Open the Markdown file on the branch being reviewed. Relative image links use that same revision; after
merging, opening the file on the target branch uses the merged images. No branch name, GitLab hostname or
token is stored in the document. A URL to a deleted source branch is not redirected automatically.
For example, storyboards/login/sign-in.md can link to ../../screenshots/base/login/sign-in/form.png. The paths also work in a local Markdown viewer with the repository layout intact. This applies to repository files; pasting their contents into an MR description uses a different link context.
Runtime Markdown references actual captures and, for a failure, the expected/diff/actual images. Keep _output/ and screenshots/ in their original relative layout when downloading reports. A Markdown file alone does not include its images. SVG is the self-contained option. Repository rendering does not imply that GitLab's CI artifact viewer previews Markdown.
Markdown keeps chapters, sentence order, annotations, multilingual keywords and failure information. The page title is the scenario, with the feature on the line below, named with the keyword of its feature file. The report then gives its recorded outcome and frame/step counts, followed by links to chapters, the result and replay information. A standalone render without an explicit outcome says "Outcome not recorded"; it does not infer success from the absence of a visual failure.
Each chapter starts with a plain Markdown heading. Screenshots sit in two equal-width columns by default. Each card keeps its title, image, note and copyable command together. Each pair has its own table so GitLab does not put an entire chapter inside one scrolling region. A one-line command shows under its note as inline code, which wraps, so both columns keep their width; a command on several lines shows as an open code block. Replay and source details fold at the end. A failed comparison keeps Expected / Diff / Actual and unavailable-image labels visible, regardless of the details setting.
Markdown layout
Configure presentation directly on the plugin; no CSS file is needed:
storyboard: {
require: '@digital-commons-official/codeceptjs-storyboard',
enabled: true,
markdown: {
columns: 2,
tableOfContents: true,
technicalDetails: 'collapsed'
}
}
The example shows the defaults. Set columns: 1 for full-width cards, especially when reading on
narrow screens. GitLab tables do not automatically stack like a responsive HTML grid.
Set technicalDetails: 'expanded' to open the replay and source details, or
tableOfContents: false to omit navigation. Neither option removes evidence or notes.
Unsupported options and invalid layout values raise a clear error before the plugin registers listeners or the standalone renderer writes artifacts.
GitLab controls the page background, fonts and native alert colours through the reader's preferences. Essential information
remains text even when images cannot load. The renderer never recolours screenshots, loads external
assets, or applies the SVG-only theme option to Markdown. Explicit format: 'svg' keeps its existing
theme behaviour and ignores Markdown presentation settings.
Standalone callers can use render('report.md', { markdown: { columns: 1 }, outcome: 'passed' }).
Only provide outcome when it is known; failure evidence always takes precedence over a passed value.
Replay commands
The gallery and storyboard default to npx codeceptjs run --steps --grep "{tag}".
{tag} becomes the scenario's last tag. This command works in a standalone
CodeceptJS project with a local installation and its default configuration file.
Toolbox projects can set their Task command on the plugin:
storyboard: {
require: '@digital-commons-official/codeceptjs-storyboard',
enabled: true,
replayCommand: 'task test -- --grep "{tag}"'
}
For another configuration file, include its --config argument in the template.
Use a unique scenario tag so the command selects the intended journey.
Colours and fonts
In SVG mode, set theme on the storyboard plugin to an object, a CSS file path, or an HTTP(S) URL:
storyboard: {
require: '@digital-commons-official/codeceptjs-storyboard',
enabled: true,
format: 'svg',
theme: './storyboard.css'
// Or: theme: 'https://example.org/storyboard.css'
// Or: theme: { bg: '#f6f8fb', text: '#192b3d', sans: 'Georgia, serif' }
}
Relative paths start at the directory containing codecept.conf.js. The plugin loads the theme once per run, before the first scenario's steps, and embeds its values in every SVG. Remote requests time out after five seconds. A missing, unreachable or invalid theme emits a warning and uses the default dark theme.
:root {
--sb-bg: #f6f8fb;
--sb-card: #ffffff;
--sb-slot: #edf2f8;
--sb-text: #192b3d;
--sb-dim: #42576b;
--sb-accent: #175fa4;
--sb-code-bg: #e7eef7;
--sb-given: #356b16;
--sb-when: #175fa4;
--sb-then: #7b3990;
--sb-font-sans: Georgia, serif;
}
Supported object keys are bg, card, slot, text, dim, accent, badge, codeBg, given, when, then, fail, sans and mono.
CSS uses --sb-<key>, except codeBg becomes --sb-code-bg, and the fonts use --sb-font-sans and --sb-font-mono.
Omitted values keep their defaults. Colours accept hex, named colours, rgb() and hsl() values; fonts accept installed font family names and fallback lists.
Only these properties are read. Imports, external fonts and arbitrary CSS rules are not embedded. The SVG works offline; install the same fonts on machines where matching typography matters. Themes change the labels and cards around captured frames; the screenshot pixels stay unchanged.
Usage 🚀
The .feature is the single human-authored source
Attach structured comments to the sentence just below them. The feature file carries the story, the step file only drives and captures:
# Chapter: Titleopens a section before that card (a full-width band in SVG).# Note: textis the card's explanation: what to look at, why it matters. Repeat the line to stack paragraphs.# Copy: commandis the exact copyable command shown with the card.
The scenario's LAST tag names the baseline directory and the replay command printed on the board.
@e2e @first-run
Feature: A developer opens their new project
Scenario: The project home shows the framework
# Chapter: The empty project
# Note: The project as GitLab shows it, before anything is installed.
# Copy: http://gitlab/<user>/<project>
Given a fresh project with only a README on its main branch
The step file drives and captures
/* global inject Given */
const { I } = inject()
const storyboard = require('@digital-commons-official/codeceptjs-storyboard')
storyboard.storyboardStep(Given, 'a fresh project with only a README on its main branch', async () => {
await I.amOnPage('/my/project')
await storyboard.addStoryboardFrame(I, await storyboard.capturePageFrame(I, 'project-home'))
})
storyboardStep(register, sentence, fn)registers the step and escapes cucumber-expression metacharacters, soCI/CDor(y/N)in a sentence never breaks the match. Cards open automatically for every sentence.capturePageFrame(I, name)captures the full viewport;captureElementFrame(I, name, selector)crops the frame to one element, so a short verdict yields a short card.addStoryboardFrame(I, png)refuses a blank frame, attaches the frame to the open card and asserts it against its own baseline at once. On drift the error names the frame, the pixel diff, the failure storyboard and the command that regenerates the baseline.annotate({ note, copy })overrides the feature comments for a value only known at runtime.
PNG or WebP frames
Frames keep the format the VisualHelper writes: PNG up to its version 2, lossless WebP by default from its version 3 (imageFormat). Baselines, the Markdown links and the SVG images follow the frame. A WebP frame whose reviewed baseline is still a PNG is refused, because the helper would record it as a new baseline and compare nothing. Convert the reviewed baselines once, with the same pixels:
npx -y -p @digital-commons-official/codeceptjs-storyboard codeceptjs-storyboard-webp tests/e2e/screenshots/base
Gherkin languages and examples
Set # language: fr (or another Cucumber dialect) on the first line of a feature file. Without this header, Gherkin uses English. Storyboards keep the source keywords, accents and scenario title, including keywords joined directly to the sentence. Register the translated sentences in your CodeceptJS step definitions.
Background steps and outline examples appear in execution order. Each example row gets its own storyboard and baseline directory, with an -example-<line> suffix. Repeated sentences keep their own annotations.
# Chapter, # Note and # Copy remain the annotation keys in every language; outline values are substituted in them too. Text inside doc strings and data tables does not become an extra card or annotation.
Invalid syntax or an unknown dialect produces an error naming the feature file.
The enabled plugin completes CodeceptJS's Gherkin loader for Rule scenarios,
including localized keywords such as Règle. The feature background runs before
each case; a rule background runs only for cases in that rule. Outline examples
remain separate tests and rule tags can be selected with --grep. Repeated
scenario titles in different rules keep their own source annotations.
An empty selection fails clearly instead of succeeding with zero tests.
The multilingual storyboard runs English, French and German examples and shows a real visual regression on a French sentence.
The rule storyboard executes two French outline examples and a second rule, checks both backgrounds and the tag filter, and proves the exact sentence of a deliberate visual regression.
What you get
- On every run the selected storyboard is written under
_output/<feature-dir>/<scenario-tag>.mdor.svg. - In SVG, cards sit two per row, 708 px each. A frame still 1000 px wide or more once fitted to the 1100 px height limit, such as a web page 1440 px wide, takes a full 1440 px row and shows at its real size. A narrower strip stays in its half of a row, shrunk to fit. A card alone on its row is centred in it.
- On a failed run the storyboard shows the expected image, the changed pixels and the actual image with their paths, then lists the sentences the run never reached. SVG presents the comparison as a red band; Markdown uses a labelled table.
- In baseline-update mode the plugin asserts first and rewrites a baseline only when its assert fails, then rebuilds the reviewed storyboard under
storyboards/from the reviewed baselines:
TASK_E2E_UPDATE_BASELINES=1 npx codeceptjs run
Only the exact value 1 enables recording. An empty value, 0 or false keeps
strict comparison. Every baseline written in recording mode must be inspected
by a human, which is why that mode throws when CI is set.
API 📚
| Function | Purpose |
|---|---|
storyboardStep(register, sentence, fn) | Register a sentence literally through Given, When or Then |
capturePageFrame(I, name) | Full-viewport frame, returns the PNG or WebP path |
captureElementFrame(I, name, selector) | Element-cropped frame, returns the PNG or WebP path |
addStoryboardFrame(I, png) | Attach the frame to the open card and assert it at tolerance: 0 |
annotate({ note, copy }) | Runtime override of the open card's note and copyable command |
assertOrUpdateBaseline(I, name) | The assert-or-rewrite primitive addStoryboardFrame relies on |
render(outFile, options) | Write Markdown for .md or SVG for .svg; theme applies only to SVG |
convertBaselinesToWebp(dir) | Convert reviewed PNG baselines to lossless WebP with the same pixels |
parseScenarioLines(featureFile, title) | Every sentence of a scenario with its # Chapter / # Note / # Copy |
begin, panel, frame, panels | The board primitives the plugin drives (useful for standalone rendering) |
Type definitions ship in src/index.d.ts. Standalone rendering uses the output extension independently of plugin configuration and rejects other extensions.
Development 🛠️
task build # install dependencies + build the e2e runner image
task test # unit tests, then the e2e storyboard suite
task project:test:storyboard # unit tests only (vitest)
task project:test:storyboard:e2e # e2e storyboard suite only, inside the pinned Playwright runner
task project:test:storyboard:markdown # render the four selected review journeys as Markdown
task project:test:storyboard:svg # render the remaining review journeys as SVG
task project:test:storyboard:e2e -- --grep "@storyboard-happy-path"
TASK_E2E_UPDATE_BASELINES=1 task project:test:storyboard:svg
TASK_E2E_UPDATE_BASELINES=1 task project:test:storyboard:markdown
task code # linters (MegaLinter)
The e2e suite is the plugin's own dogfood: every scenario is a storyboard rendered by the plugin under test, committed under tests/e2e/storyboards/.
Open the Markdown gallery on the merge request
branch to review installation, Gherkin rules, compatibility and the Markdown consumer with relative screenshot links. Regenerate
these reviewed Markdown files with TASK_E2E_UPDATE_BASELINES=1 task project:test:storyboard:markdown.
The main suite explicitly selects SVG to retain its existing SVG regression coverage.
Use both recording commands above to preserve the reviewed format selection, then inspect the generated
boards. The SVG recording task excludes exactly those four Markdown journeys.
The installation storyboard packs the current source, installs the tarball into a fresh project, and runs the README configuration with explicit SVG selection. The Markdown consumer journey exercises the omitted-format default separately.
It checks CodeceptJS 4.2.0, Playwright 1.63.0 and VisualHelper 1.3.5 in Chromium.
It opens the generated index.html and SVG, repeats the run with strict baselines, then proves that a changed page fails on its exact sentence.
The consumer manifest pins those tools and overrides vulnerable transitive
dependencies. Its lockfile makes the test repeatable. The separate version
matrix verifies the six combinations listed above.
Complete installation and run logs are kept in tests/e2e/_output/consumer-install.log and
tests/e2e/_output/matrix/.
The clean-consumer test also copies the replay command from the generated gallery and executes it unchanged, then checks that every strict baseline hash is intact.
Licence 📜
EUPL-1.2