一键导入
jest-story-api
Use when writing BDD story tests in Jest with executable-stories-jest: top-level given/when/then/and/but imports, story.init(), or doc entries.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when writing BDD story tests in Jest with executable-stories-jest: top-level given/when/then/and/but imports, story.init(), or doc entries.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when the user asks for a rich explanation of a code change, diff, branch, or PR in a repo that uses executable-stories. Produces an explainer that is part of the living documentation — grounded in run artifacts, published next to the stories, and finished with a comprehension quiz — instead of a throwaway HTML file.
Use when teaching a technical or coding topic (onboarding, katas, LeetCode-style practice, a library, a pattern) and you want each lesson to be runnable and self-proving. Authors lessons as executable story tests so the lesson, the test, and the living documentation are one artifact. Companion to the `teach` skill: teach owns the pedagogy (mission, progression, references) and this skill owns the verification layer.
Bootstrap executable-stories into a repo from zero. Installs Vitest and/or Playwright if missing, wires the StoryReporter, drops a sample story test, and adds a test script. Use when the user wants to "set up executable-stories", "add story testing to this repo", "initialize executable-stories", "bootstrap from scratch", or runs `pnpm dlx executable-stories-init`. Mirrors the `executable-stories-init` CLI; prefer the CLI when available, otherwise follow the manual checklist.
Use when running the executable-stories CLI or formatters API: turning a RawRun into a report (Astro, Confluence, HTML, Markdown, JUnit, Cucumber, story-report-json, etc.), gating a release, comparing runs, reviewing AI-authored changes, or driving agent-loop commands (check/triage/goal).
Use when writing BDD story tests in Vitest with executable-stories-vitest: the callback-only story.init(task) API, given/when/then/and/but steps, or doc entries.
Use when incrementally adopting executable-stories in an existing Cypress test suite, converting cy.ts specs to story tests, or adding story.init() without a full rewrite. Progressive enhancement of .story.cy.ts files; requires plugin and support-file wiring.
| name | jest-story-api |
| description | Use when writing BDD story tests in Jest with executable-stories-jest: top-level given/when/then/and/but imports, story.init(), or doc entries. |
| type | core |
| library | executable-stories-jest |
| library_version | 8.4.7 |
| sources | ["jagreehal/executable-stories:packages/executable-stories-jest/src/index.ts","jagreehal/executable-stories:packages/executable-stories-jest/src/story-api.ts"] |
import { describe, expect, it } from "@jest/globals";
import { story, given, when, then } from "executable-stories-jest";
describe("Cart checkout", () => {
it("applies discount code", () => {
story.init({ tags: ["checkout"], ticket: "CART-42", covers: ["src/checkout.ts"] });
given("a cart with items totaling $100");
const cart = createCart([{ name: "Shirt", price: 100 }]);
when("a 20% discount code is applied");
applyDiscount(cart, "SAVE20");
then("the total is $80");
expect(cart.total).toBe(80);
});
});
File naming: *.story.test.ts.
Jest uses top-level step exports. story.init() takes no arguments — the test name is derived from expect.getState().currentTestName.
import { story, given, when, then, and, but } from "executable-stories-jest";
it("blocks suspended user login", () => {
story.init();
given("the user account exists"); // renders "Given"
given("the account is suspended"); // renders "And" (auto-converted)
when("the user submits valid credentials");
then("the user sees an error message");
but("the user is not logged in"); // renders "But" (always)
});
import { story, given, when, then } from "executable-stories-jest";
it("processes payment", () => {
story.init();
given("a valid payment request");
story.json({ label: "Payload", value: { amount: 50, currency: "USD" } });
when("the payment is submitted");
story.code({ label: "Response", content: '{ "status": "ok" }', lang: "json" });
then("the order is confirmed");
story.table({
label: "Order summary",
columns: ["Item", "Qty", "Price"],
rows: [["Widget", "2", "$25"]],
});
story.note("Payment processed in sandbox mode");
});
Embed generated HTML (charts, single-file reports, skill/agent output) in an
always-sandboxed iframe in the report. Exactly one of path / url / content
is required; optional title and height (number → px, string passed through; default 400px).
story.html({ content: chartHtml, title: "Latency chart", height: "60vh" });
story.html({ url: "https://dash.example.com/run/42", height: 600 });
story.html({ path: "./reports/summary.html", title: "Summary" });
Source guidance: generated/ephemeral HTML (a skill writing to a temp dir) → pass content
(captured now, survives temp-dir cleanup). Stable on-disk artifact → pass path (inlined at format time).
The embedded HTML must be self-contained (a single file). Local files are inlined as the
iframe's srcdoc; relative references to sibling CSS/JS/images are not rewritten, so a multi-file
report renders broken. Use a single-file/inline mode or pass markup via content. Directory bundling is planned.
Sandbox-safe contract: renders inside <iframe sandbox="allow-scripts"> (opaque origin, no
allow-same-origin). CDN scripts (Tailwind, Mermaid) and inline DOM scripts work; localStorage/
sessionStorage/cookies throw SecurityError (and an unguarded access aborts the rest of that
script block) — guard with try/catch or avoid. No window.top/parent access; no popups.
const profile = await story.fn("When", "the profile is fetched", async () => {
return fetchProfile(userId);
});
await story.expect("the profile contains the correct name", () => {
expect(profile.name).toBe("Alice");
});
given("valid credentials", {
json: { label: "Credentials", value: { user: "alice", role: "admin" } },
note: "Password masked for security",
});
A bodyless it.todo records specified-but-unimplemented behavior. It appears in generated docs as a Planned scenario (title only, no steps) as long as the file contains at least one story test:
describe("Checkout", () => {
it("applies a discount code", () => {
story.init();
given("a cart with items");
when("a valid discount code is applied");
then("the total reflects the discount");
});
// Renders as "Planned" in the report; canonical status is pending
it.todo("applies gift cards at checkout");
});
Markdown renders the heading with (planned); the HTML report shows a Planned badge. A file containing only todos produces nothing — the story-file gate keeps plain suites out of the docs.
Wrong:
it("my test", ({ task }) => {
story.init(task); // Jest does not provide task
});
Correct:
it("my test", () => {
story.init(); // No arguments needed
});
Jest's story.init() takes no arguments. It reads the test name from expect.getState().currentTestName. Passing an argument is a Vitest pattern that does not apply to Jest.
Source: packages/executable-stories-jest/src/story-api.ts
Wrong assumption:
// Expecting "Calculator" to appear as a ## heading in docs
describe("Calculator", () => {
it("adds numbers", () => {
story.init();
// ...
});
});
// Jest's currentTestName format is "Calculator adds numbers" (space-separated)
// Suite path is usually undefined → docs are flat
This is a known limitation. Jest's expect.getState().currentTestName uses space-separated format ("Describe title test name"), not " > " separated. Suite path extraction only works when " > " is present. In the default Jest setup, docs are flat without ## Suite name headings.
Source: CLAUDE.md — "Doc heading and describe in generated docs"
Wrong:
// jest.config.mjs
export default {
reporters: ["default", "executable-stories-jest/reporter"],
};
Correct:
// jest.config.mjs
export default {
setupFilesAfterEnv: ["executable-stories-jest/setup"],
reporters: ["default", "executable-stories-jest/reporter"],
};
The setup file registers an afterAll hook that flushes story metadata to disk. Without it, the reporter has no data to process.
Source: packages/executable-stories-jest/src/reporter.ts
Wrong:
it("my test", () => {
given("something");
story.init();
});
Correct:
it("my test", () => {
story.init();
given("something");
});
Steps called before init() are silently dropped because no story context exists.
Source: packages/eslint-plugin-executable-stories-jest/src/rules/require-story-context-for-steps.ts
Use Jest's it.each with story.init() to produce one scenario per data row — the framework-native replacement for Cucumber's Scenario Outline + Examples.
import { story, given, when, then } from "executable-stories-jest";
const cases = [
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
{ input: 3, expected: 6 },
];
describe("Doubling", () => {
it.each(cases)("doubles $input to $expected", ({ input, expected }) => {
story.init();
given(`the input is ${input}`);
when("the doubler runs");
then(`the result is ${expected}`);
expect(input * 2).toBe(expected);
});
});
Each iteration produces a separate scenario in the generated report. story.init() is not a scenario-title setter — the scenario title comes from Jest's currentTestName, so use an interpolated it.each title template (as above) so each row gets a distinct, descriptive name.