+
+ );
+
+ const links = container.querySelectorAll('a');
+ expect(links).toHaveLength(3);
+
+ links.forEach((link) => {
+ const styles = window.getComputedStyle(link);
+ expect(styles.textDecoration).toContain('underline');
+ });
+ });
+ });
+});
diff --git a/code/core/src/components/components/typography/DocumentWrapper.tsx b/code/core/src/components/components/typography/DocumentWrapper.tsx
index b5b4cca5eccb..25a91f33b3b7 100644
--- a/code/core/src/components/components/typography/DocumentWrapper.tsx
+++ b/code/core/src/components/components/typography/DocumentWrapper.tsx
@@ -64,7 +64,10 @@ export const DocumentWrapper = styled.div(({ theme }) => ({
},
a: {
color: theme.color.secondary,
- textDecoration: 'none',
+ // Ensure WCAG Level A compliance (SC 1.4.1), see https://www.w3.org/WAI/WCAG22/Techniques/failures/F73
+ textDecoration: 'underline',
+ textDecorationThickness: '0.03125rem',
+ textUnderlineOffset: '0.11em',
},
'a.absent': {
color: '#cc0000',
@@ -78,6 +81,10 @@ export const DocumentWrapper = styled.div(({ theme }) => ({
top: 0,
left: 0,
bottom: 0,
+ textDecoration: 'none',
+ },
+ '&.anchor:hover, &.anchor:focus': {
+ textDecoration: 'underline',
},
'h1, h2, h3, h4, h5, h6': {
margin: '20px 0 10px',
diff --git a/code/core/src/components/components/typography/elements/A.test.tsx b/code/core/src/components/components/typography/elements/A.test.tsx
new file mode 100644
index 000000000000..cbb638ca8d5c
--- /dev/null
+++ b/code/core/src/components/components/typography/elements/A.test.tsx
@@ -0,0 +1,75 @@
+// @vitest-environment happy-dom
+import { cleanup, render } from '@testing-library/react';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import React from 'react';
+
+import { ThemeProvider, convert, themes } from 'storybook/theming';
+
+import { A } from './A';
+
+function ThemedA({ children, ...props }: React.ComponentProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+describe('A', () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ describe('accessibility', () => {
+ it('should render with underline text decoration for accessibility', () => {
+ const { container } = render(Test Link);
+
+ const link = container.querySelector('a');
+ expect(link).toBeTruthy();
+
+ const styles = window.getComputedStyle(link!);
+ expect(styles.textDecoration).toContain('underline');
+ });
+
+ it('should render with underline in dark theme', () => {
+ const { container } = render(
+
+ Test Link
+
+ );
+
+ const link = container.querySelector('a');
+ expect(link).toBeTruthy();
+
+ const styles = window.getComputedStyle(link!);
+ expect(styles.textDecoration).toContain('underline');
+ });
+
+ it('should not underline anchor position markers (a.anchor)', () => {
+ const { container } = render(
+
+ Anchor Link
+
+ );
+
+ const link = container.querySelector('a.anchor');
+ expect(link).toBeTruthy();
+
+ const styles = window.getComputedStyle(link!);
+ expect(styles.textDecoration).not.toContain('underline');
+ });
+
+ it('should render with correct color and styling', () => {
+ const { container } = render(Link Text);
+
+ const link = container.querySelector('a');
+ expect(link).toBeTruthy();
+ expect(link?.textContent).toBe('Link Text');
+
+ const styles = window.getComputedStyle(link!);
+ expect(styles.textDecoration).toContain('underline');
+ expect(styles.fontSize).toBe('inherit');
+ });
+ });
+});
diff --git a/code/core/src/components/components/typography/elements/A.tsx b/code/core/src/components/components/typography/elements/A.tsx
index e91833da6cfc..9ab4e9bf03b2 100644
--- a/code/core/src/components/components/typography/elements/A.tsx
+++ b/code/core/src/components/components/typography/elements/A.tsx
@@ -8,7 +8,10 @@ export const A = styled(Link)(withReset, ({ theme }) => ({
lineHeight: '24px',
color: theme.color.secondary,
- textDecoration: 'none',
+ // Ensure WCAG Level A compliance (SC 1.4.1), see https://www.w3.org/WAI/WCAG22/Techniques/failures/F73
+ textDecoration: 'underline',
+ textDecorationThickness: '0.03125rem',
+ textUnderlineOffset: '0.11em',
'&.absent': {
color: '#cc0000',
},
@@ -21,5 +24,9 @@ export const A = styled(Link)(withReset, ({ theme }) => ({
top: 0,
left: 0,
bottom: 0,
+ textDecoration: 'none',
+ },
+ '&.anchor:hover, &.anchor:focus': {
+ textDecoration: 'underline',
},
}));
diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts
index 22387945616a..f1004214dc8b 100644
--- a/code/core/src/core-server/presets/common-preset.ts
+++ b/code/core/src/core-server/presets/common-preset.ts
@@ -93,7 +93,7 @@ export const favicon = async (
.reduce((l1, l2) => l1.concat(l2), []);
if (faviconPaths.length > 1) {
- logger.warn(dedent`
+ logger.debug(dedent`
Looks like multiple favicons were detected. Using the first one.
${faviconPaths.join(', ')}
diff --git a/code/core/src/core-server/presets/favicon.test.ts b/code/core/src/core-server/presets/favicon.test.ts
index e6be6ebb054f..b35a99d6f602 100644
--- a/code/core/src/core-server/presets/favicon.test.ts
+++ b/code/core/src/core-server/presets/favicon.test.ts
@@ -49,7 +49,7 @@ const createOptions = (locations: string[]): Parameters[1] =>
vi.mock('storybook/internal/node-logger', () => {
return {
logger: {
- warn: vi.fn(() => {}),
+ debug: vi.fn(() => {}),
},
};
});
@@ -125,7 +125,7 @@ it('with staticDirs containing a single favicon.svg should return the found favi
);
});
-it('with staticDirs containing a multiple favicons should return the first favicon and warn', async () => {
+it('with staticDirs containing a multiple favicons should return the first favicon and show a debug message', async () => {
const location = 'static';
existsSyncMock.mockImplementation((p) => {
if (normalize(String(p)) === normalize(createPath(location))) {
@@ -145,10 +145,10 @@ it('with staticDirs containing a multiple favicons should return the first favic
normalize(createPath(location, 'favicon.svg'))
);
- expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('multiple favicons'));
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('multiple favicons'));
});
-it('with multiple staticDirs containing a multiple favicons should return the first favicon and warn', async () => {
+it('with multiple staticDirs containing a multiple favicons should return the first favicon and show a debug message', async () => {
const locationA = 'static-a';
const locationB = 'static-b';
existsSyncMock.mockImplementation((p) => {
@@ -172,5 +172,5 @@ it('with multiple staticDirs containing a multiple favicons should return the fi
normalize(createPath(locationA, 'favicon.ico'))
);
- expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('multiple favicons'));
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('multiple favicons'));
});
diff --git a/code/core/src/manager/components/error-boundary/ManagerErrorBoundary.stories.tsx b/code/core/src/manager/components/error-boundary/ManagerErrorBoundary.stories.tsx
index 3d1cea253671..f12578d8c6f2 100644
--- a/code/core/src/manager/components/error-boundary/ManagerErrorBoundary.stories.tsx
+++ b/code/core/src/manager/components/error-boundary/ManagerErrorBoundary.stories.tsx
@@ -1,10 +1,33 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
-import { spyOn } from 'storybook/test';
+import { expect, fn, spyOn } from 'storybook/test';
import preview from '../../../../../.storybook/preview';
import { ManagerErrorBoundary } from './ManagerErrorBoundary';
+// Mocks for play assertions: set by decorator, asserted in play functions
+let consoleErrorSpy: ReturnType;
+const sendTelemetryErrorMock = fn();
+
+// Stable wrapper: only cleanup in useEffect so teardown runs correctly with React Strict Mode.
+// Setup must run synchronously in the decorator so the spy is in place before Story renders.
+const RestoreGlobals = ({
+ children,
+ originalSendTelemetryError,
+}: {
+ children: React.ReactNode;
+ originalSendTelemetryError: typeof globalThis.sendTelemetryError;
+}) => {
+ useEffect(
+ () => () => {
+ consoleErrorSpy.mockRestore();
+ globalThis.sendTelemetryError = originalSendTelemetryError;
+ },
+ [originalSendTelemetryError]
+ );
+ return <>{children}>;
+};
+
// Component that throws an error immediately when rendered
const ThrowingComponent = ({ shouldThrow = true }: { shouldThrow?: boolean }) => {
if (shouldThrow) {
@@ -59,9 +82,15 @@ const meta = preview.meta({
},
decorators: [
(Story) => {
- // Suppress console.error in stories to prevent noisy test output
- spyOn(console, 'error').mockImplementation(() => {});
- return ;
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
+ sendTelemetryErrorMock.mockClear();
+ const originalSendTelemetryError = globalThis.sendTelemetryError;
+ globalThis.sendTelemetryError = sendTelemetryErrorMock;
+ return (
+
+
+
+ );
},
],
});
@@ -74,6 +103,26 @@ export const WithError = meta.story({
),
+ play: async ({ canvas }) => {
+ await expect(canvas.getByTestId('manager-error-boundary')).toBeInTheDocument();
+ await expect(canvas.getByText('Something went wrong')).toBeInTheDocument();
+ await expect(
+ canvas.getByText('This is a test error thrown by ThrowingComponent')
+ ).toBeInTheDocument();
+
+ await expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Storybook Manager UI Error:',
+ expect.any(Error)
+ );
+ await expect(consoleErrorSpy).toHaveBeenCalledWith('Component Stack:', expect.any(String));
+
+ await expect(sendTelemetryErrorMock).toHaveBeenCalledTimes(1);
+ await expect(sendTelemetryErrorMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'This is a test error thrown by ThrowingComponent',
+ })
+ );
+ },
});
export const WithDeepStackTrace = meta.story({
@@ -93,6 +142,13 @@ export const WithoutError = meta.story({
),
+ play: async ({ canvas }) => {
+ await expect(canvas.getByText('Everything is fine!')).toBeInTheDocument();
+ await expect(
+ canvas.getByText('This content should render normally when there is no error.')
+ ).toBeInTheDocument();
+ await expect(canvas.queryByTestId('manager-error-boundary')).not.toBeInTheDocument();
+ },
});
export const InteractiveError = meta.story({
@@ -121,4 +177,19 @@ export const CustomErrorMessage = meta.story({
);
},
+ play: async ({ canvas }) => {
+ await expect(canvas.getByTestId('manager-error-boundary')).toBeInTheDocument();
+ await expect(
+ canvas.getByText(
+ 'Custom error: Unable to load addons configuration. Please check your manager.ts file.'
+ )
+ ).toBeInTheDocument();
+
+ await expect(sendTelemetryErrorMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message:
+ 'Custom error: Unable to load addons configuration. Please check your manager.ts file.',
+ })
+ );
+ },
});
diff --git a/code/core/src/manager/components/error-boundary/ManagerErrorBoundary.tsx b/code/core/src/manager/components/error-boundary/ManagerErrorBoundary.tsx
index 6aa356f2ca8c..cf4dd5605baa 100644
--- a/code/core/src/manager/components/error-boundary/ManagerErrorBoundary.tsx
+++ b/code/core/src/manager/components/error-boundary/ManagerErrorBoundary.tsx
@@ -196,6 +196,10 @@ export class ManagerErrorBoundary extends Component<
console.error('Storybook Manager UI Error:', error);
console.error('Component Stack:', errorInfo.componentStack);
this.setState({ errorInfo });
+
+ if (typeof globalThis.sendTelemetryError === 'function') {
+ globalThis.sendTelemetryError(error);
+ }
}
render() {
diff --git a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts
index 62ff798d118d..e27e897bf23a 100644
--- a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts
+++ b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts
@@ -1,6 +1,5 @@
import { logger } from 'storybook/internal/node-logger';
import { AngularLegacyBuildOptionsError } from 'storybook/internal/server-errors';
-import { WebpackDefinePlugin, WebpackIgnorePlugin } from '@storybook/builder-webpack5';
import type { BuilderContext } from '@angular-devkit/architect';
import { targetFromTargetString } from '@angular-devkit/architect';
@@ -9,7 +8,6 @@ import { logging } from '@angular-devkit/core';
import * as find from 'empathic/find';
import type webpack from 'webpack';
-import { getWebpackConfig as getCustomWebpackConfig } from './angular-cli-webpack';
import type { PresetOptions } from './preset-options';
import { getProjectRoot, resolvePackageDir } from 'storybook/internal/common';
import { relative } from 'pathe';
@@ -20,6 +18,9 @@ export async function webpackFinal(baseConfig: webpack.Configuration, options: P
return baseConfig;
}
+ const { WebpackDefinePlugin, WebpackIgnorePlugin } = await import('@storybook/builder-webpack5');
+ const { getWebpackConfig: getCustomWebpackConfig } = await import('./angular-cli-webpack');
+
checkForLegacyBuildOptions(options);
const builderContext = getBuilderContext(options);
diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json
index 88e7b0c5cbf1..b308569942e4 100644
--- a/code/frameworks/nextjs-vite/package.json
+++ b/code/frameworks/nextjs-vite/package.json
@@ -102,7 +102,7 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "workspace:^",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
diff --git a/code/frameworks/nextjs-vite/template/cli/js/Configure.mdx b/code/frameworks/nextjs-vite/template/cli/js/Configure.mdx
index 70fcc2a9777c..923c1a47dc93 100644
--- a/code/frameworks/nextjs-vite/template/cli/js/Configure.mdx
+++ b/code/frameworks/nextjs-vite/template/cli/js/Configure.mdx
@@ -52,6 +52,7 @@ export const RightArrow = () =>
Learn more
@@ -128,6 +132,7 @@ export const RightArrow = () => Publish to Chromatic
Publish your Storybook to review and collaborate with your entire team.
Learn more
@@ -144,6 +149,7 @@ export const RightArrow = () => Embed your stories into Figma to cross-reference the design and live
implementation in one place.
Learn more
@@ -160,6 +166,7 @@ export const RightArrow = () => Use stories to test a component in all its variations, no matter how
complex.
Learn more
@@ -175,6 +182,7 @@ export const RightArrow = () => Accessibility
Automatically test your components for a11y issues as you develop.
Integrate your tools with Storybook to connect workflows.
Discover all addons
@@ -229,6 +239,7 @@ export const RightArrow = () => Star on GitHub
@@ -246,6 +257,7 @@ export const RightArrow = () => Join Discord server
@@ -264,6 +276,7 @@ export const RightArrow = () => Watch on YouTube
@@ -281,6 +294,7 @@ export const RightArrow = () => Follow guided walkthroughs on for key workflows.
Discover tutorials
@@ -328,6 +342,16 @@ export const RightArrow = () => Add styling and CSS
Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.
Learn more
@@ -67,6 +68,7 @@ export const RightArrow = () => Provide context and mocking
Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.
Learn more
@@ -85,6 +87,7 @@ export const RightArrow = () => Learn more
@@ -113,6 +116,7 @@ export const RightArrow = () => Auto-generate living,
interactive reference documentation from your components and stories.
Learn more
@@ -128,6 +132,7 @@ export const RightArrow = () => Publish to Chromatic
Publish your Storybook to review and collaborate with your entire team.
Learn more
@@ -144,6 +149,7 @@ export const RightArrow = () => Embed your stories into Figma to cross-reference the design and live
implementation in one place.
Learn more
@@ -160,6 +166,7 @@ export const RightArrow = () => Use stories to test a component in all its variations, no matter how
complex.
Learn more
@@ -175,6 +182,7 @@ export const RightArrow = () => Accessibility
Automatically test your components for a11y issues as you develop.
Integrate your tools with Storybook to connect workflows.
Discover all addons
@@ -229,6 +239,7 @@ export const RightArrow = () => Star on GitHub
@@ -246,6 +257,7 @@ export const RightArrow = () => Join Discord server
@@ -264,6 +276,7 @@ export const RightArrow = () => Watch on YouTube
@@ -281,6 +294,7 @@ export const RightArrow = () => Follow guided walkthroughs on for key workflows.
Discover tutorials
@@ -328,6 +342,16 @@ export const RightArrow = () => Add styling and CSS
Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.
Learn more
@@ -67,6 +68,7 @@ export const RightArrow = () => Provide context and mocking
Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.
Learn more
@@ -85,6 +87,7 @@ export const RightArrow = () => Learn more
@@ -113,6 +116,7 @@ export const RightArrow = () => Auto-generate living,
interactive reference documentation from your components and stories.
Learn more
@@ -128,6 +132,7 @@ export const RightArrow = () => Publish to Chromatic
Publish your Storybook to review and collaborate with your entire team.
Learn more
@@ -144,6 +149,7 @@ export const RightArrow = () => Embed your stories into Figma to cross-reference the design and live
implementation in one place.
Learn more
@@ -160,6 +166,7 @@ export const RightArrow = () => Use stories to test a component in all its variations, no matter how
complex.
Learn more
@@ -175,6 +182,7 @@ export const RightArrow = () => Accessibility
Automatically test your components for a11y issues as you develop.
Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.
Learn more
@@ -60,6 +61,7 @@ export const RightArrow = () => Provide context and mocking
Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.
Learn more
@@ -72,6 +74,7 @@ export const RightArrow = () => Learn more
@@ -94,6 +97,7 @@ export const RightArrow = () => Auto-generate living,
interactive reference documentation from your components and stories.
Learn more
@@ -103,6 +107,7 @@ export const RightArrow = () => Publish to Chromatic
Publish your Storybook to review and collaborate with your entire team.
Learn more
@@ -113,6 +118,7 @@ export const RightArrow = () => Embed your stories into Figma to cross-reference the design and live
implementation in one place.
Learn more
@@ -123,6 +129,7 @@ export const RightArrow = () => Use stories to test a component in all its variations, no matter how
complex.
Learn more
@@ -132,6 +139,7 @@ export const RightArrow = () => Accessibility
Automatically test your components for a11y issues as you develop.