diff --git a/CHANGELOG.md b/CHANGELOG.md index a2b083c7f424..1f5c83852102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 9.1.16 + +- CLI: Fix Nextjs project creation in empty directories - [#32828](https://github.com/storybookjs/storybook/pull/32828), thanks @yannbf! +- Core: Add `experimental_devServer` preset - [#32862](https://github.com/storybookjs/storybook/pull/32862), thanks @yannbf! +- Telemetry: Fix preview-first-load event - [#32859](https://github.com/storybookjs/storybook/pull/32859), thanks @shilman! + ## 9.1.15 - Core: Add `preview-first-load` telemetry - [#32770](https://github.com/storybookjs/storybook/pull/32770), thanks @shilman! diff --git a/code/core/src/core-server/dev-server.ts b/code/core/src/core-server/dev-server.ts index 7039977cee05..128e250a1b4c 100644 --- a/code/core/src/core-server/dev-server.ts +++ b/code/core/src/core-server/dev-server.ts @@ -50,6 +50,9 @@ export async function storybookDevServer(options: Options) { getMiddleware(options.configDir)(app); + // Apply experimental_devServer preset to allow addons/frameworks to extend the dev server with middlewares, etc. + await options.presets.apply('experimental_devServer', app); + const { port, host, initialPath } = options; invariant(port, 'expected options to have a port'); const proto = options.https ? 'https' : 'http'; diff --git a/code/core/src/core-server/server-channel/preview-initialized-channel.test.ts b/code/core/src/core-server/server-channel/preview-initialized-channel.test.ts new file mode 100644 index 000000000000..0055849979c0 --- /dev/null +++ b/code/core/src/core-server/server-channel/preview-initialized-channel.test.ts @@ -0,0 +1,86 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { makePayload } from './preview-initialized-channel'; + +describe('makePayload', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2024-01-01T00:00:00Z')); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + it('new user init session', () => { + const userAgent = 'Mozilla/5.0'; + const sessionId = 'session-123'; + const lastInit = { + timestamp: Date.now() - 3000, + body: { + sessionId, + payload: { newUser: true }, + }, + }; + + expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(` + { + "isNewUser": true, + "timeSinceInit": 3000, + "userAgent": "Mozilla/5.0", + } + `); + }); + + it('existing user init session', () => { + const userAgent = 'Mozilla/5.0'; + const sessionId = 'session-123'; + const lastInit = { + timestamp: Date.now() - 3000, + body: { + sessionId, + payload: {}, + }, + }; + + expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(` + { + "isNewUser": false, + "timeSinceInit": 3000, + "userAgent": "Mozilla/5.0", + } + `); + }); + + it('no init session', () => { + const userAgent = 'Mozilla/5.0'; + const sessionId = 'session-123'; + const lastInit = undefined; + + expect(makePayload(userAgent, lastInit, sessionId)).toMatchInlineSnapshot(` + { + "isNewUser": false, + "timeSinceInit": undefined, + "userAgent": "Mozilla/5.0", + } + `); + }); + + it('init session with different sessionId', () => { + const userAgent = 'Mozilla/5.0'; + const sessionId = 'session-123'; + const lastInit = { + timestamp: Date.now() - 3000, + body: { + sessionId: 'session-456', + }, + }; + + expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(` + { + "isNewUser": false, + "timeSinceInit": undefined, + "userAgent": "Mozilla/5.0", + } + `); + }); +}); diff --git a/code/core/src/core-server/server-channel/preview-initialized-channel.ts b/code/core/src/core-server/server-channel/preview-initialized-channel.ts index 1a0ad5e595da..0f689435aa1d 100644 --- a/code/core/src/core-server/server-channel/preview-initialized-channel.ts +++ b/code/core/src/core-server/server-channel/preview-initialized-channel.ts @@ -1,11 +1,30 @@ import type { Channel } from 'storybook/internal/channels'; import { PREVIEW_INITIALIZED } from 'storybook/internal/core-events'; -import { telemetry } from 'storybook/internal/telemetry'; +import { type InitPayload, telemetry } from 'storybook/internal/telemetry'; import type { CoreConfig, Options } from 'storybook/internal/types'; -import { getLastEvents } from '../../telemetry/event-cache'; +import { type CacheEntry, getLastEvents } from '../../telemetry/event-cache'; import { getSessionId } from '../../telemetry/session-id'; +export const makePayload = ( + userAgent: string, + lastInit: CacheEntry | undefined, + sessionId: string +) => { + let timeSinceInit: number | undefined; + const payload = { + userAgent, + isNewUser: false, + timeSinceInit, + }; + + if (sessionId && lastInit?.body?.sessionId === sessionId) { + payload.timeSinceInit = Date.now() - lastInit.timestamp; + payload.isNewUser = !!(lastInit.body.payload as InitPayload).newUser; + } + return payload; +}; + export function initPreviewInitializedChannel( channel: Channel, options: Options, @@ -19,9 +38,8 @@ export function initPreviewInitializedChannel( const lastInit = lastEvents.init; const lastPreviewFirstLoad = lastEvents['preview-first-load']; if (!lastPreviewFirstLoad) { - const isInitSession = lastInit?.body.sessionId === sessionId; - const timeSinceInit = lastInit ? Date.now() - lastInit.body.timestamp : undefined; - telemetry('preview-first-load', { timeSinceInit, isInitSession, userAgent }); + const payload = makePayload(userAgent, lastInit, sessionId); + telemetry('preview-first-load', payload); } } catch (e) { // do nothing diff --git a/code/core/src/telemetry/event-cache.ts b/code/core/src/telemetry/event-cache.ts index 0f25b1c14579..97c4e60da254 100644 --- a/code/core/src/telemetry/event-cache.ts +++ b/code/core/src/telemetry/event-cache.ts @@ -1,6 +1,6 @@ import { cache } from 'storybook/internal/common'; -import type { EventType } from './types'; +import type { EventType, TelemetryEvent } from './types'; interface UpgradeSummary { timestamp: number; @@ -9,9 +9,14 @@ interface UpgradeSummary { sessionId?: string; } +export interface CacheEntry { + timestamp: number; + body: TelemetryEvent; +} + let operation: Promise = Promise.resolve(); -const setHelper = async (eventType: EventType, body: any) => { +const setHelper = async (eventType: EventType, body: TelemetryEvent) => { const lastEvents = (await cache.get('lastEvents')) || {}; lastEvents[eventType] = { body, timestamp: Date.now() }; await cache.set('lastEvents', lastEvents); @@ -23,16 +28,16 @@ export const set = async (eventType: EventType, body: any) => { return operation; }; -export const get = async (eventType: EventType) => { +export const get = async (eventType: EventType): Promise => { const lastEvents = await getLastEvents(); return lastEvents[eventType]; }; -export const getLastEvents = async () => { +export const getLastEvents = async (): Promise> => { return (await cache.get('lastEvents')) || {}; }; -const upgradeFields = (event: any): UpgradeSummary => { +const upgradeFields = (event: CacheEntry): UpgradeSummary => { const { body, timestamp } = event; return { timestamp, diff --git a/code/core/src/telemetry/index.ts b/code/core/src/telemetry/index.ts index cf2f4ac084a0..617b2db2b7e6 100644 --- a/code/core/src/telemetry/index.ts +++ b/code/core/src/telemetry/index.ts @@ -43,21 +43,21 @@ export const telemetry = async ( telemetryData.metadata = await getStorybookMetadata(options?.configDir); } } catch (error: any) { - telemetryData.payload.metadataErrorMessage = sanitizeError(error).message; + payload.metadataErrorMessage = sanitizeError(error).message; if (options?.enableCrashReports) { - telemetryData.payload.metadataError = sanitizeError(error); + payload.metadataError = sanitizeError(error); } } finally { - const { error } = telemetryData.payload; + const { error } = payload; // make sure to anonymise possible paths from error messages // make sure to anonymise possible paths from error messages if (error) { - telemetryData.payload.error = sanitizeError(error); + payload.error = sanitizeError(error); } - if (!telemetryData.payload.error || options?.enableCrashReports) { + if (!payload.error || options?.enableCrashReports) { if (process.env?.STORYBOOK_TELEMETRY_DEBUG) { logger.info('\n[telemetry]'); logger.info(JSON.stringify(telemetryData, null, 2)); diff --git a/code/core/src/telemetry/types.ts b/code/core/src/telemetry/types.ts index 89b23b58a111..6191aef743ea 100644 --- a/code/core/src/telemetry/types.ts +++ b/code/core/src/telemetry/types.ts @@ -33,7 +33,6 @@ export type EventType = | 'onboarding-survey' | 'mocking' | 'preview-first-load'; - export interface Dependency { version: string | undefined; versionSpecifier?: string; @@ -89,6 +88,10 @@ export interface Payload { [key: string]: any; } +export interface Context { + [key: string]: any; +} + export interface Options { retryDelay: number; immediate: boolean; @@ -103,3 +106,17 @@ export interface TelemetryData { payload: Payload; metadata?: StorybookMetadata; } + +export interface TelemetryEvent extends TelemetryData { + eventId: string; + sessionId: string; + context: Context; +} + +export interface InitPayload { + projectType: string; + features: { dev: boolean; docs: boolean; test: boolean; onboarding: boolean }; + newUser: boolean; + versionSpecifier: string | undefined; + cliIntegration: string | undefined; +} diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index caff102504df..d2e1d2f4459c 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -223,7 +223,7 @@ export type Middleware = ( next: (err?: string | Error) => Promise | void ) => Promise | void; -interface ServerApp { +export interface ServerApp { server: NetServer; use(pattern: RegExp | string, ...handlers: Middleware[]): this; @@ -476,6 +476,8 @@ export interface StorybookConfigRaw { experimental_indexers?: Indexer[]; + experimental_devServer?: ServerApp; + docs?: DocsOptions; previewHead?: string; diff --git a/code/package.json b/code/package.json index 587048a53a53..0fd1d6a07846 100644 --- a/code/package.json +++ b/code/package.json @@ -281,5 +281,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "9.1.16" } diff --git a/docs/_snippets/addon-vitest-set-project-annotations-simple.md b/docs/_snippets/addon-vitest-set-project-annotations-simple.md index a38e9ae39c6d..8691dc558d0e 100644 --- a/docs/_snippets/addon-vitest-set-project-annotations-simple.md +++ b/docs/_snippets/addon-vitest-set-project-annotations-simple.md @@ -1,4 +1,4 @@ -```tsx filename=".storybook/vitest.setup.ts" renderer="react" language="ts" +```ts filename=".storybook/vitest.setup.ts" renderer="react" language="ts" // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. import { setProjectAnnotations } from '@storybook/your-framework'; import * as previewAnnotations from './preview'; @@ -6,7 +6,7 @@ import * as previewAnnotations from './preview'; const annotations = setProjectAnnotations([previewAnnotations]); ``` -```tsx filename=".storybook/vitest.setup.ts" renderer="svelte" language="ts" +```ts filename=".storybook/vitest.setup.ts" renderer="svelte" language="ts" // Replace your-framework with the framework you are using, e.g. sveltekit or svelte-vite import { setProjectAnnotations } from '@storybook/your-framework'; import * as previewAnnotations from './preview'; @@ -14,9 +14,16 @@ import * as previewAnnotations from './preview'; const annotations = setProjectAnnotations([previewAnnotations]); ``` -```tsx filename=".storybook/vitest.setup.ts" renderer="vue" language="ts" +```ts filename=".storybook/vitest.setup.ts" renderer="vue" language="ts" import { setProjectAnnotations } from '@storybook/vue3-vite'; import * as previewAnnotations from './preview'; const annotations = setProjectAnnotations([previewAnnotations]); ``` + +```ts filename=".storybook/vitest.setup.ts" renderer="web-components" language="ts" +import { setProjectAnnotations } from '@storybook/web-components-vite'; +import * as previewAnnotations from './preview'; + +const annotations = setProjectAnnotations([previewAnnotations]); +``` diff --git a/docs/_snippets/vitest-plugin-vitest-config.md b/docs/_snippets/vitest-plugin-vitest-config.md index bc53c8fea6ce..c35ee741fe9b 100644 --- a/docs/_snippets/vitest-plugin-vitest-config.md +++ b/docs/_snippets/vitest-plugin-vitest-config.md @@ -1,6 +1,9 @@ -```ts filename="vitest.config.ts" renderer="react" +```ts filename="vitest.config.ts" renderer="react" tabTitle="Vitest 4" import { defineConfig, defineProject, mergeConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -22,8 +25,56 @@ export default mergeConfig( // The location of your Storybook config, main.js|ts configDir: path.join(dirname, '.storybook'), // This should match your package.json script to run Storybook - // The --ci flag will skip prompts and not open a browser - storybookScript: 'yarn storybook --ci', + // The --no-open flag will skip the automatic opening of a browser + storybookScript: 'yarn storybook --no-open', + }), + ], + test: { + name: 'storybook', + // Enable browser mode + browser: { + enabled: true, + // Make sure to install Playwright + provider: playwright({}), + headless: true, + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }), + ], + }, + }), +); +``` + +```ts filename="vitest.config.ts" renderer="react" tabTitle="Vitest 3" +import { defineConfig, defineProject, mergeConfig } from 'vitest/config'; + +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +import viteConfig from './vite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + // Use `workspace` field in Vitest < 3.2 + projects: [ + defineProject({ + extends: true, + plugins: [ + storybookTest({ + // The location of your Storybook config, main.js|ts + configDir: path.join(dirname, '.storybook'), + // This should match your package.json script to run Storybook + // The --no-open flag will skip the automatic opening of a browser + storybookScript: 'yarn storybook --no-open', }), ], test: { @@ -45,9 +96,60 @@ export default mergeConfig( ); ``` -```ts filename="vitest.config.ts" renderer="vue" -import { defineConfig, mergeConfig, defineProject } from 'vitest/config'; +```ts filename="vitest.config.ts" renderer="vue" tabTitle="Vitest 4" +import { defineConfig, defineProject, mergeConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; + +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +import viteConfig from './vite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + // Use `workspace` field in Vitest < 3.2 + projects: [ + defineProject({ + extends: true, + plugins: [ + storybookTest({ + // The location of your Storybook config, main.js|ts + configDir: path.join(dirname, '.storybook'), + // This should match your package.json script to run Storybook + // The --no-open flag will skip the automatic opening of a browser + storybookScript: 'yarn storybook --no-open', + }), + ], + test: { + name: 'storybook', + // Enable browser mode + browser: { + enabled: true, + // Make sure to install Playwright + provider: playwright({}), + headless: true, + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }), + ], + }, + }), +); +``` + +```ts filename="vitest.config.ts" renderer="vue" tabTitle="Vitest 3" +import { defineConfig, defineProject, mergeConfig } from 'vitest/config'; + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -69,8 +171,8 @@ export default mergeConfig( // The location of your Storybook config, main.js|ts configDir: path.join(dirname, '.storybook'), // This should match your package.json script to run Storybook - // The --ci flag will skip prompts and not open a browser - storybookScript: 'yarn storybook --ci', + // The --no-open flag will skip the automatic opening of a browser + storybookScript: 'yarn storybook --no-open', }), ], test: { @@ -92,9 +194,12 @@ export default mergeConfig( ); ``` -```ts filename="vitest.config.ts" renderer="svelte" -import { defineConfig, mergeConfig, defineProject } from 'vitest/config'; +```ts filename="vitest.config.ts" renderer="svelte" tabTitle="Vitest 4" +import { defineConfig, defineProject, mergeConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -116,8 +221,153 @@ export default mergeConfig( // The location of your Storybook config, main.js|ts configDir: path.join(dirname, '.storybook'), // This should match your package.json script to run Storybook - // The --ci flag will skip prompts and not open a browser - storybookScript: 'yarn storybook --ci', + // The --no-open flag will skip the automatic opening of a browser + storybookScript: 'yarn storybook --no-open', + }), + ], + test: { + name: 'storybook', + // Enable browser mode + browser: { + enabled: true, + // Make sure to install Playwright + provider: playwright({}), + headless: true, + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }), + ], + }, + }), +); +``` + +```ts filename="vitest.config.ts" renderer="svelte" tabTitle="Vitest 3" +import { defineConfig, defineProject, mergeConfig } from 'vitest/config'; + +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +import viteConfig from './vite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + // Use `workspace` field in Vitest < 3.2 + projects: [ + defineProject({ + extends: true, + plugins: [ + storybookTest({ + // The location of your Storybook config, main.js|ts + configDir: path.join(dirname, '.storybook'), + // This should match your package.json script to run Storybook + // The --no-open flag will skip the automatic opening of a browser + storybookScript: 'yarn storybook --no-open', + }), + ], + test: { + name: 'storybook', + // Enable browser mode + browser: { + enabled: true, + // Make sure to install Playwright + provider: 'playwright', + headless: true, + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }), + ], + }, + }), +); +``` + +```ts filename="vitest.config.ts" renderer="web-components" tabTitle="Vitest 4" +import { defineConfig, defineProject, mergeConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; + +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +import viteConfig from './vite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + // Use `workspace` field in Vitest < 3.2 + projects: [ + defineProject({ + extends: true, + plugins: [ + storybookTest({ + // The location of your Storybook config, main.js|ts + configDir: path.join(dirname, '.storybook'), + // This should match your package.json script to run Storybook + // The --no-open flag will skip the automatic opening of a browser + storybookScript: 'yarn storybook --no-open', + }), + ], + test: { + name: 'storybook', + // Enable browser mode + browser: { + enabled: true, + // Make sure to install Playwright + provider: playwright({}), + headless: true, + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }), + ], + }, + }), +); +``` + +```ts filename="vitest.config.ts" renderer="web-components" tabTitle="Vitest 3" +import { defineConfig, defineProject, mergeConfig } from 'vitest/config'; + +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +import viteConfig from './vite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + // Use `workspace` field in Vitest < 3.2 + projects: [ + defineProject({ + extends: true, + plugins: [ + storybookTest({ + // The location of your Storybook config, main.js|ts + configDir: path.join(dirname, '.storybook'), + // This should match your package.json script to run Storybook + // The --no-open flag will skip the automatic opening of a browser + storybookScript: 'yarn storybook --no-open', }), ], test: { diff --git a/docs/_snippets/vitest-plugin-vitest-debug-option-ci.md b/docs/_snippets/vitest-plugin-vitest-debug-option-ci.md new file mode 100644 index 000000000000..9d32ae78a092 --- /dev/null +++ b/docs/_snippets/vitest-plugin-vitest-debug-option-ci.md @@ -0,0 +1,37 @@ +```ts filename="vitest.config.ts" renderer="common" tabTitle="Vitest 4" +export default defineConfig({ + // ... + test: { + // ... + projects: [ + { + plugins: [ + storybookTest({ + // ... + // 👇 Use the environment variable you passed + storybookUrl: process.env.SB_URL, + }), + ], + }, + ], + }, +}); +``` + +```ts filename="vitest.workspace.ts" renderer="common" tabTitle="Vitest 3" +export default defineWorkspace([ + // ... + { + // ... + { + plugins: [ + storybookTest({ + // ... + // 👇 Use the environment variable you passed + storybookUrl: process.env.SB_URL + }), + ], + }, + }, +]); +``` diff --git a/docs/_snippets/vitest-plugin-vitest-tags-configuration.md b/docs/_snippets/vitest-plugin-vitest-tags-configuration.md new file mode 100644 index 000000000000..01a5e9cc0aa5 --- /dev/null +++ b/docs/_snippets/vitest-plugin-vitest-tags-configuration.md @@ -0,0 +1,42 @@ +```ts filename="vitest.config.ts" renderer="common" tabTitle="Vitest 4" +export default defineConfig({ + // ... + test: { + // ... + projects: [ + { + plugins: [ + storybookTest({ + // ... + tags: { + include: ['test'], + exclude: ['experimental'], + }, + }), + ], + // ... + }, + ], + }, +}); +``` + +```ts filename="vitest.workspace.ts" renderer="common" tabTitle="Vitest 3" +export default defineWorkspace([ + // ... + { + // ... + { + plugins: [ + storybookTest({ + // ... + tags: { + include: ['test'], + exclude: ['experimental'], + }, + }), + ], + }, + }, +]); +``` diff --git a/docs/_snippets/vitest-plugin-vitest-workspace.md b/docs/_snippets/vitest-plugin-vitest-workspace.md index 2304a06d3fc5..1e6dd1c7fd69 100644 --- a/docs/_snippets/vitest-plugin-vitest-workspace.md +++ b/docs/_snippets/vitest-plugin-vitest-workspace.md @@ -121,3 +121,44 @@ export default defineWorkspace([ }, ]); ``` + +```ts filename="vitest.config.ts" renderer="web-components" +import { defineConfig, mergeConfig } from 'vitest/config'; +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import viteConfig from './vite.config'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default defineWorkspace([ + // This is the path to your existing Vitest config file + './vitest.config.ts', + { + // This is the path to your existing Vite config file + extends: './vite.config.ts', + plugins: [ + storybookTest({ + // The location of your Storybook config, main.js|ts + configDir: path.join(dirname, '.storybook'), + // This should match your package.json script to run Storybook + // The --ci flag will skip prompts and not open a browser + storybookScript: 'yarn storybook --ci', + }), + ], + test: { + name: 'storybook', + // Enable browser mode + browser: { + enabled: true, + // Make sure to install Playwright + provider: 'playwright', + headless: true, + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }, +]); +``` diff --git a/docs/addons/integration-catalog.mdx b/docs/addons/integration-catalog.mdx index 16fef2dd7853..e92266353666 100644 --- a/docs/addons/integration-catalog.mdx +++ b/docs/addons/integration-catalog.mdx @@ -24,14 +24,14 @@ Add your addon to the catalog by publishing a npm package that follows these req ### Addon metadata -We rely on metadata to organize your addon in the catalog. You must add the storybook-addons as the first keyword, followed by your addon's category. Additional keywords will be used in search and as tags. +We rely on metadata to organize your addon in the catalog. You must add the storybook-addon as the first keyword, followed by your addon's category. Additional keywords will be used in search and as tags. | Property | Description | Example | | ------------- | -------------------------------------- | ------------------------------------------------------------------------- | | `name` | Addon package name | storybook-addon-example | | `description` | Addon description | Outline all elements with CSS to help with layout placement and alignment | | `author` | Name of the author | winkerVSbecks | -| `keywords` | List of keywords to describe the addon | `["storybook-addons","style","debug"]` | +| `keywords` | List of keywords to describe the addon | `["storybook-addon","style","debug"]` | | `repository` | Addon repository | `{"type": "git","url": "https://github.com/someone/my-addon" }` | Customize your addon's appearance by adding the `storybook` property with the following fields. @@ -69,7 +69,7 @@ Use the list below as a reference when filling in the values for both the `suppo "url": "https://github.com/chromaui/storybook-addon-example" }, "author": "winkerVSbecks", - "keywords": ["storybook-addons", "style", "debug", "layout", "css"], + "keywords": ["storybook-addon", "style", "debug", "layout", "css"], "storybook": { "displayName": "Outline", "unsupportedFrameworks": ["vue"], diff --git a/docs/addons/writing-addons.mdx b/docs/addons/writing-addons.mdx index 619c79a51530..ce809705fb5d 100644 --- a/docs/addons/writing-addons.mdx +++ b/docs/addons/writing-addons.mdx @@ -215,7 +215,7 @@ The second metadata category is related to the [integration catalog](https://sto "unsupportedFrameworks": ["react-native"], "icon": "https://yoursite.com/link-to-your-icon.png" }, - "keywords": ["storybook-addons", "appearance", "style", "css", "layout", "debug"] + "keywords": ["storybook-addon", "appearance", "style", "css", "layout", "debug"] } ``` @@ -223,7 +223,7 @@ The second metadata category is related to the [integration catalog](https://sto The `storybook` configuration element includes additional properties that help customize the addon's searchability and indexing. For more information, see the [Integration catalog documentation](./integration-catalog.mdx). -One essential item to note is the `keywords` property as it maps to the catalog's tag system. Adding the `storybook-addons` ensures that the addon is discoverable in the catalog when searching for addons. The remaining keywords help with the searchability and categorization of the addon. +One essential item to note is the `keywords` property as it maps to the catalog's tag system. Adding the `storybook-addon` keyword ensures that the addon is discoverable in the catalog when searching for addons. The remaining keywords help with the searchability and categorization of the addon. ### Publishing to NPM diff --git a/docs/versions/latest.json b/docs/versions/latest.json index a97d3b11b5a2..d98cb055c29f 100644 --- a/docs/versions/latest.json +++ b/docs/versions/latest.json @@ -1 +1 @@ -{"version":"9.1.15","info":{"plain":"- Core: Add `preview-first-load` telemetry - [#32770](https://github.com/storybookjs/storybook/pull/32770), thanks @shilman!\n- Dependencies: Update `vite-plugin-storybook-nextjs` - [#32821](https://github.com/storybookjs/storybook/pull/32821), thanks @ndelangen!"}} +{"version":"9.1.16","info":{"plain":"- CLI: Fix Nextjs project creation in empty directories - [#32828](https://github.com/storybookjs/storybook/pull/32828), thanks @yannbf!\n- Core: Add `experimental_devServer` preset - [#32862](https://github.com/storybookjs/storybook/pull/32862), thanks @yannbf!\n- Telemetry: Fix preview-first-load event - [#32859](https://github.com/storybookjs/storybook/pull/32859), thanks @shilman!"}} diff --git a/docs/versions/next.json b/docs/versions/next.json index 4cb7c3383fae..6c9f63a10996 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.0.0-rc.0","info":{"plain":"- A11Y: Bugfix missing `manager.js` entry-file - [#32780](https://github.com/storybookjs/storybook/pull/32780), thanks @ndelangen!\n- Addon Vitest: Support modifying mergeConfig on addon setup - [#32753](https://github.com/storybookjs/storybook/pull/32753), thanks @yannbf!\n- CLI: Change message in downgrade-blocker - [#32745](https://github.com/storybookjs/storybook/pull/32745), thanks @ndelangen!"}} \ No newline at end of file +{"version":"10.0.0-rc.3","info":{"plain":"- A11y: Persist tab/highlight across docs navigation - [#32762](https://github.com/storybookjs/storybook/pull/32762), thanks @404Dealer!\n- Addon Vitest: Fix incorrect file modifications during setup - [#32844](https://github.com/storybookjs/storybook/pull/32844), thanks @yannbf!\n- Core: Enhance warning for Testing Library's `screen` usage in docs mode - [#32851](https://github.com/storybookjs/storybook/pull/32851), thanks @yannbf!\n- Core: Mark pnp support as deprecated - [#32645](https://github.com/storybookjs/storybook/pull/32645), thanks @ndelangen!"}} \ No newline at end of file diff --git a/docs/writing-tests/in-ci.mdx b/docs/writing-tests/in-ci.mdx index 9da12d087dde..620b309a5be3 100644 --- a/docs/writing-tests/in-ci.mdx +++ b/docs/writing-tests/in-ci.mdx @@ -331,23 +331,11 @@ jobs: Finally, we update the plugin configuration to use that environment variable in the [`storybookUrl` plugin option](./integrations/vitest-addon.mdx#storybookurl). -```diff title="vitest.workspace.ts" -export default defineWorkspace([ - // ... - { - // ... - { - plugins: [ - storybookTest({ - // ... -+ // 👇 Use the environment variable you passed -+ storybookUrl: process.env.SB_URL - }), - ], - }, - }, -]) -``` +{/* prettier-ignore-start */} + + + +{/* prettier-ignore-end */} Now, when a test fails in CI, the printed story URL will point to the published Storybook, making debugging that failure a breeze! diff --git a/docs/writing-tests/integrations/vitest-addon.mdx b/docs/writing-tests/integrations/vitest-addon.mdx index ae7fe4241a7f..80e131d82fd2 100644 --- a/docs/writing-tests/integrations/vitest-addon.mdx +++ b/docs/writing-tests/integrations/vitest-addon.mdx @@ -63,7 +63,7 @@ For some project setups, the `add` command may be unable to automate the addon a 1. Install the addon, `@storybook/addon-vitest`, in your project and [register it in your Storybook configuration](../../addons/install-addons.mdx#manual-installation). 1. Create a test setup file, `.storybook/vitest.setup.ts`. You can use the [example setup file](#example-vitest-setup) as a guide. 1. Adjust your Vitest configuration to include the plugin and reference the setup file. You can use the [example configuration files](#example-configuration-files) as a guide. - - For projects with existing Vitest tests, we recommend using a [workspace file](https://vitest.dev/guide/workspace/) to define separate projects for your Storybook tests and other tests. This allows you to run them either in isolation or together, depending on your needs. + - For projects with existing Vitest tests, we recommend using a separate [test project](https://vitest.dev/guide/projects) if you're using Vitest ≥ 4.0 and a [workspace file](https://v3.vitest.dev/config/#workspace) if you're using Vitest 3.x to define separate configurations for your Storybook tests and other tests. You can run them in isolation or together, depending on your needs. ### Example configuration files @@ -99,7 +99,7 @@ When the addon is set up automatically, it will create or adjust your Vitest con
Example Vitest workspace file (Vitest < 3.2) - If you're using a [Vitest workspace](https://vitest.dev/guide/workspace), you can define a new workspace project: + If you're using a [Vitest workspace](https://v3.vitest.dev/config/#workspace), you can define a new workspace project: {/* prettier-ignore-start */} @@ -224,25 +224,11 @@ In this example, we'll apply the `stable` tag to all of the Button component's s To connect those tags to our test behavior, we can adjust the plugin configuration to exclude the `experimental` tag: -```js title="vitest.workspace.ts" -export default defineWorkspace([ - // ... - { - // ... - { - plugins: [ - storybookTest({ - // ... - tags: { - include: ['test'], - exclude: ['experimental'], - }, - }), - ], - }, - }, -]) -``` +{/* prettier-ignore-start */} + + + +{/* prettier-ignore-end */} If the same tag is in both the `include` and `exclude` arrays, the `exclude` behavior takes precedence. @@ -309,7 +295,8 @@ Some projects might contain a `test` property in their Vite configuration. Becau To isolate your Storybook tests from other tests, you need to move the `test` property from your Vite configuration to the Vitest configuration. The Vitest config used by the plugin can then safely extend your Vite config without merging the `test` property. -Additionally, we recommend using a [workspace file](#example-configuration-files) to define separate projects for your Storybook tests and other tests. This ensures each can be run either in isolation or together, depending on your needs. +Additionally, we recommend using a [test project](#example-configuration-files) if you're using Vitest ≥ 4.0, or a workspace for previous versions to define separate configurations for your Storybook tests and other tests. This ensures each can be run either in isolation or together, depending on your needs. + ### Why do we recommend browser mode? diff --git a/test-storybooks/portable-stories-kitchen-sink/svelte/stories/__snapshots__/Button.test.ts.snap b/test-storybooks/portable-stories-kitchen-sink/svelte/stories/__snapshots__/Button.test.ts.snap index c55663382310..ec5530701943 100644 --- a/test-storybooks/portable-stories-kitchen-sink/svelte/stories/__snapshots__/Button.test.ts.snap +++ b/test-storybooks/portable-stories-kitchen-sink/svelte/stories/__snapshots__/Button.test.ts.snap @@ -16,6 +16,7 @@ exports[`Renders CSF2Secondary story 1`] = ` + `; @@ -47,6 +48,7 @@ exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = ` + `; @@ -67,6 +69,7 @@ exports[`Renders CSF3Button story 1`] = ` + `; @@ -96,6 +99,7 @@ exports[`Renders CSF3ButtonWithRender story 1`] = ` + `; @@ -111,6 +115,7 @@ exports[`Renders CSF3InputFieldFilled story 1`] = ` + `; @@ -131,6 +136,7 @@ exports[`Renders CSF3Primary story 1`] = ` + `; @@ -156,6 +162,7 @@ exports[`Renders LoaderStory story 1`] = ` + `; @@ -187,6 +194,7 @@ exports[`Renders NewStory story 1`] = ` + `;