From 819acb9fdf61668f4b9ea9adb4a41717f5c40ebb Mon Sep 17 00:00:00 2001 From: Aleksei Gurianov Date: Thu, 8 Oct 2026 12:08:14 +0000 Subject: [PATCH] feat: support Storybook 11 and CSF Next --- .changeset/storybook-11.md | 7 +++++ .github/workflows/ci.yml | 35 ++++++++++++++++++++++- README.md | 39 +++++++++++++++++++++---- jsrepo.config.ts | 1 + package.json | 6 ++-- pnpm-lock.yaml | 15 +++++----- registry.json | 50 +++++++++++++++++++++++++++++++- src/index.ts | 7 +++++ test/addon.test.ts | 58 ++++++++++++++++++++++++++++++++++++++ vite.config.ts | 5 ++-- 10 files changed, 202 insertions(+), 21 deletions(-) create mode 100644 .changeset/storybook-11.md create mode 100644 test/addon.test.ts diff --git a/.changeset/storybook-11.md b/.changeset/storybook-11.md new file mode 100644 index 0000000..2ea9172 --- /dev/null +++ b/.changeset/storybook-11.md @@ -0,0 +1,7 @@ +--- +'kahraman': minor +--- + +Support Storybook 11 and its prereleases while retaining Storybook 9 and 10 compatibility. Add an opt-in default addon factory for CSF Next (`addons: [kahraman()]`) with typed diagnostics parameters. Existing named exports and the `kahraman/preview` annotation remain available. + +Raise the minimum Node.js version to 22.12 to match Storybook 11. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c95b07..98fe0b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,9 @@ concurrency: jobs: check: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [22, 24] steps: - uses: actions/checkout@v4 @@ -23,7 +26,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 24 + node-version: ${{ matrix.node-version }} cache: pnpm - name: Install @@ -57,3 +60,33 @@ jobs: run: | pnpm run registry:build git diff --exit-code registry.json + + storybook-compatibility: + runs-on: ubuntu-latest + strategy: + matrix: + storybook-version: ['9.1.20', '10.6.1'] + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Use Storybook ${{ matrix.storybook-version }} + run: pnpm add --save-dev storybook@${{ matrix.storybook-version }} + + - name: Typecheck + run: pnpm run typecheck + + - name: Test + run: pnpm run test + + - name: Build + run: pnpm run build diff --git a/README.md b/README.md index 30c1982..c47ddef 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,9 @@ ESM only and is renderer-agnostic: it reads only `canvasElement` and `userEvent` from the story context, so it works with the React, Vue, and Svelte renderers alike. +Supports Storybook 9, 10, and 11, including the Storybook 11 prerelease. Node.js +22.12 or newer is required. + ### Or vendor the source with jsrepo Prefer to own the code? `kahraman` is also published as a @@ -278,7 +281,24 @@ readable: Testing Library's `getByRole` misses dump a listing of _every_ accessible role on the page — hundreds of lines on a full app mount. The `kahraman/preview` annotation filters that to just the queried role's near-misses and caps the DOM -dump. Add it to your Storybook preview: +dump. For CSF Next, register the addon factory in your Storybook preview: + +```ts +// .storybook/preview.ts +import { definePreview } from '@storybook/react-vite' // use your framework +import kahraman from 'kahraman' + +export default definePreview({ + addons: [kahraman()], + // ...your other preview config +}) +``` + +Calling `kahraman()` opts into diagnostics and types `parameters.kahraman`. +The named actor and locator exports work without registering the addon. + +For an object-style preview, the existing `kahraman/preview` annotation still +works: ```ts // .storybook/preview.ts @@ -320,15 +340,22 @@ package: ## Testing the pure helpers -`kahraman`'s own test suite unit-tests only the **Storybook-free** logic: +`kahraman`'s own test suite unit-tests the **Storybook-free** logic: locator label construction, step-label formatting, role-listing filtering, -message capping, and stack-frame retargeting. The full actor behavior requires a -real Storybook browser context and is exercised by consumers' stories — that -boundary is intentional. The DOM-touching modules (`actor.ts`, `preview.ts`) run -only in the browser. +message capping, and stack-frame retargeting. It also checks CSF Next addon +composition and diagnostics configuration against the real Storybook APIs. CI +checks compatibility with Storybook 9, 10, and 11. The full actor behavior +requires a real Storybook browser context and is exercised by consumers' +stories. That boundary is intentional. The DOM-touching code in `actor.ts` and +`preview.ts` runs in the browser; the configuration tests do not render a story +or query the DOM. ## Development +Development uses Storybook 11 alpha, pinned in `pnpm-lock.yaml`. There is no +local Storybook instance in this repository. Use Node.js 22.18+ or 24.11+ to run +the development toolchain, which has a higher minimum than the published package. + This repo uses the [Vite+](https://viteplus.dev/) / Oxc toolchain. The plain `pnpm` commands below remain the contributor path. If you use [mise](https://mise.jdx.dev/), the common workflows are also available as `mise run prepare`, `mise run dev`, diff --git a/jsrepo.config.ts b/jsrepo.config.ts index e138f63..720c435 100644 --- a/jsrepo.config.ts +++ b/jsrepo.config.ts @@ -34,6 +34,7 @@ export default defineConfig({ { path: './src/steps.ts' }, { path: './src/invariant.ts' }, { path: './src/context.ts' }, + { path: './test/addon.test.ts', role: 'test' }, { path: './test/loc.test.ts', role: 'test' }, { path: './test/steps.test.ts', role: 'test' }, { path: './examples/pageActor.ts', role: 'example' }, diff --git a/package.json b/package.json index 68c6a8d..f9fa319 100644 --- a/package.json +++ b/package.json @@ -82,17 +82,17 @@ "fallow": "^3.5.1", "jsrepo": "^3.8.1", "publint": "^0.3.21", - "storybook": "^10.5.0", + "storybook": "^11.0.0-alpha.4", "tsdown": "^0.22.7", "typescript": "^7.0.2", "vite-plus": "^0.2.4", "vitest": "4.1.10" }, "peerDependencies": { - "storybook": ">=9.0.0" + "storybook": "^9.0.0 || ^10.0.0 || ^11.0.0-0" }, "engines": { - "node": ">=20.11.0" + "node": ">=22.12.0" }, "packageManager": "pnpm@11.13.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c231da1..6a24168 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^0.3.21 version: 0.3.24 storybook: - specifier: ^10.5.0 - version: 10.5.10(prettier@2.8.8)(react@19.2.7)(vite-plus@0.2.9(@arethetypeswrong/core@0.18.5)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.24)(typescript@7.0.2)(vite@8.1.4(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0)) + specifier: ^11.0.0-alpha.4 + version: 11.0.0-alpha.4(prettier@2.8.8)(react@19.2.7)(vite-plus@0.2.9(@arethetypeswrong/core@0.18.5)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.24)(typescript@7.0.2)(vite@8.1.4(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0)) tsdown: specifier: ^0.22.7 version: 0.22.14(@arethetypeswrong/core@0.18.5)(oxc-resolver@11.24.2)(publint@0.3.24)(typescript@7.0.2) @@ -2581,13 +2581,13 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - storybook@10.5.10: - resolution: {integrity: sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==} + storybook@11.0.0-alpha.4: + resolution: {integrity: sha512-8xlNpZgfqHs22zb0B37PQVSy0iB0B9InuXLKRlR2zPIb8LeykxE2mY8ias8kcZ9sMMu9N0zvgosM+uDzYOLZHQ==} hasBin: true peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react': ^18.0.0 || ^19.0.0 prettier: ^2 || ^3 - vite-plus: ^0.1.15 || ^0.2.0 + vite-plus: ^0.1.15 || ^0.2.0 || ^0.3.0 peerDependenciesMeta: '@types/react': optional: true @@ -4823,13 +4823,14 @@ snapshots: std-env@4.2.0: {} - storybook@10.5.10(prettier@2.8.8)(react@19.2.7)(vite-plus@0.2.9(@arethetypeswrong/core@0.18.5)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.24)(typescript@7.0.2)(vite@8.1.4(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0)): + storybook@11.0.0-alpha.4(prettier@2.8.8)(react@19.2.7)(vite-plus@0.2.9(@arethetypeswrong/core@0.18.5)(esbuild@0.28.2)(jiti@2.7.0)(publint@0.3.24)(typescript@7.0.2)(vite@8.1.4(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0)): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.1.0(react@19.2.7) '@testing-library/dom': 10.4.1 '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.6(@testing-library/dom@10.4.1) + '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 '@webcontainer/env': 1.1.1 diff --git a/registry.json b/registry.json index 0802bee..8af9757 100644 --- a/registry.json +++ b/registry.json @@ -20,7 +20,9 @@ "description": "createActor() and the role/text/heading/button/link locator DSL, with step-trace diagnostics and call-site stack retargeting.", "type": "lib", "add": "when-added", - "registryDependencies": [], + "registryDependencies": [ + "preview" + ], "dependencies": [], "devDependencies": [], "files": [ @@ -30,6 +32,15 @@ "path": "index.ts", "relativePath": "src/index.ts", "_imports_": [ + { + "import": "./preview", + "item": "preview", + "file": { + "type": "lib", + "path": "preview.ts" + }, + "meta": {} + }, { "import": "./actor", "item": "actor", @@ -168,6 +179,43 @@ "dependencies": [], "devDependencies": [] }, + { + "type": "lib", + "role": "test", + "path": "addon.test.ts", + "relativePath": "test/addon.test.ts", + "_imports_": [ + { + "import": "../src/index", + "item": "actor", + "file": { + "type": "lib", + "path": "index.ts" + }, + "meta": {} + }, + { + "import": "../src/preview", + "item": "preview", + "file": { + "type": "lib", + "path": "preview.ts" + }, + "meta": {} + } + ], + "registryDependencies": [ + "preview" + ], + "dependencies": [], + "devDependencies": [ + { + "ecosystem": "js", + "name": "vitest", + "version": "4.1.10" + } + ] + }, { "type": "lib", "role": "test", diff --git a/src/index.ts b/src/index.ts index 9f96f8b..d524478 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,10 @@ +import { definePreviewAddon } from 'storybook/internal/csf' +import type { KahramanParameters } from './preview' +import addonAnnotations from './preview' + +/** Register the opt-in diagnostics annotation with a CSF Next preview. */ +export default () => definePreviewAddon<{ parameters: KahramanParameters }>(addonAnnotations) + export { createActor } from './actor' export type { Actor, ActorOptions, BaseActor, HopeThat } from './actor' diff --git a/test/addon.test.ts b/test/addon.test.ts new file mode 100644 index 0000000..1a3e0b4 --- /dev/null +++ b/test/addon.test.ts @@ -0,0 +1,58 @@ +import { definePreview } from 'storybook/internal/csf' +import type { InferTypes } from 'storybook/internal/csf' +import { configure, getConfig } from 'storybook/test' +import { afterEach, beforeEach, expect, expectTypeOf, test } from 'vitest' +import kahraman, { button, createActor, heading, invariant, link, role, text } from '../src/index' +import annotations from '../src/preview' +import type { KahramanParameters } from '../src/preview' + +const customElementError = (message: string | null) => new Error(message ?? 'Custom error') + +let originalConfig: ReturnType + +beforeEach(() => { + originalConfig = getConfig() +}) + +afterEach(() => { + configure(originalConfig) +}) + +test('registers the existing diagnostics hook with a CSF Next preview', () => { + const addon = kahraman() + const preview = definePreview({ addons: [addon] }) + + expect(addon.beforeEach).toBe(annotations.beforeEach) + expect(preview.composed.beforeEach).toContain(annotations.beforeEach) + expectTypeOf['parameters']>().toEqualTypeOf() +}) + +test('creating the addon leaves diagnostics opt-in', () => { + const originalHandler = getConfig().getElementError + + kahraman() + + expect(getConfig().getElementError).toBe(originalHandler) +}) + +test('the registered hook uses per-story diagnostics and resets to defaults', () => { + const addon = kahraman() + + expect(addon.beforeEach).toBe(annotations.beforeEach) + annotations.beforeEach({ parameters: { kahraman: { getElementError: customElementError } } }) + expect(getConfig().getElementError).toBe(customElementError) + + annotations.beforeEach({ parameters: {} }) + expect(getConfig().getElementError).not.toBe(customElementError) +}) + +test('the legacy annotation and named actor exports remain available', () => { + expect(annotations.beforeEach).toBeTypeOf('function') + expect(createActor()).toHaveProperty('init') + expect(button('Save').__label).toBe('button "Save"') + expect(heading('Dashboard').__label).toBe('heading "Dashboard"') + expect(link('Home').__label).toBe('link "Home"') + expect(role('textbox', 'Email').__label).toBe('textbox "Email"') + expect(text('Welcome').__label).toBe('text "Welcome"') + expect(() => invariant(true, 'passes')).not.toThrow() +}) diff --git a/vite.config.ts b/vite.config.ts index 2884c7b..8142cf6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,9 +5,8 @@ import { defineConfig } from 'vite-plus' // `lint` block — Vite+ keeps all of this in vite.config.ts rather than in // separate .oxfmtrc.json / .oxlintrc.json files. // -// The unit tests cover the PURE, Storybook-free logic only: locator label -// construction, step-label formatting, role-listing filtering, message capping, -// and stack-frame retargeting. Full actor behaviour needs a Storybook browser +// The unit tests cover pure helpers plus CSF Next addon composition and +// diagnostics configuration. Full actor behaviour needs a Storybook browser // context and is out of scope here — see README (Testing the pure helpers). export default defineConfig({ fmt: {