diff --git a/code/addons/test/package.json b/code/addons/test/package.json index c0515a8dcb6f..3f53d0bca205 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -80,6 +80,7 @@ "@storybook/icons": "^1.2.12", "@storybook/instrumenter": "workspace:*", "@storybook/test": "workspace:*", + "@storybook/theming": "workspace:*", "polished": "^4.2.2", "ts-dedent": "^2.2.0" }, diff --git a/code/addons/test/src/components/GlobalErrorModal.stories.tsx b/code/addons/test/src/components/GlobalErrorModal.stories.tsx new file mode 100644 index 000000000000..3cf0e4cf01d1 --- /dev/null +++ b/code/addons/test/src/components/GlobalErrorModal.stories.tsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; + +import { ManagerContext } from 'storybook/internal/manager-api'; + +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, within } from '@storybook/test'; + +import dedent from 'ts-dedent'; + +import { GlobalErrorModal } from './GlobalErrorModal'; + +type Story = StoryObj; + +const managerContext: any = { + state: {}, + api: { + getDocsUrl: fn(({ subpath }) => `https://storybook.js.org/docs/${subpath}`).mockName( + 'api::getDocsUrl' + ), + }, +}; + +const meta = { + component: GlobalErrorModal, + decorators: [ + (storyFn) => ( + +
+ {storyFn()} +
+
+ ), + ], + args: { + onRerun: fn(), + onClose: fn(), + open: false, + }, +} satisfies Meta; + +export default meta; + +export const Default: Story = { + args: { + error: dedent` + ReferenceError: FAIL is not defined + at Constraint.execute (the-best-file.js:525:2) + at Constraint.recalculate (the-best-file.js:424:21) + at Planner.addPropagate (the-best-file.js:701:6) + at Constraint.satisfy (the-best-file.js:184:15) + at Planner.incrementalAdd (the-best-file.js:591:21) + at Constraint.addConstraint (the-best-file.js:162:10) + at Constraint.BinaryConstraint (the-best-file.js:346:7) + at Constraint.EqualityConstraint (the-best-file.js:515:38) + at chainTest (the-best-file.js:807:6) + at deltaBlue (the-best-file.js:879:2)`, + }, + render: (props) => { + const [isOpen, setOpen] = useState(false); + + return ( + <> + + + + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.parentElement!); + const button = canvas.getByText('Open modal'); + await userEvent.click(button); + await expect(canvas.findByText('Storybook Tests error details')).resolves.toBeInTheDocument(); + }, +}; diff --git a/code/addons/test/src/components/GlobalErrorModal.tsx b/code/addons/test/src/components/GlobalErrorModal.tsx new file mode 100644 index 000000000000..6aadd83a6d53 --- /dev/null +++ b/code/addons/test/src/components/GlobalErrorModal.tsx @@ -0,0 +1,91 @@ +import React from 'react'; + +import { Button, IconButton, Modal } from 'storybook/internal/components'; +import { useStorybookApi } from 'storybook/internal/manager-api'; + +import { CloseIcon, SyncIcon } from '@storybook/icons'; +import { styled } from '@storybook/theming'; + +import { TROUBLESHOOTING_LINK } from '../constants'; + +interface GlobalErrorModalProps { + error: string; + open: boolean; + onClose: () => void; + onRerun: () => void; +} + +const ModalBar = styled.div({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '6px 6px 6px 20px', +}); + +const ModalActionBar = styled.div({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', +}); + +const ModalTitle = styled.div(({ theme: { typography } }) => ({ + fontSize: typography.size.s2, + fontWeight: typography.weight.bold, +})); + +const ModalStackTrace = styled.pre(({ theme }) => ({ + whiteSpace: 'pre-wrap', + overflow: 'auto', + maxHeight: '60vh', + margin: 0, + padding: `20px`, + fontFamily: theme.typography.fonts.mono, + fontSize: '12px', + borderTop: `1px solid ${theme.appBorderColor}`, + borderRadius: 0, +})); + +const TroubleshootLink = styled.a(({ theme }) => ({ + color: theme.color.defaultText, +})); + +export function GlobalErrorModal({ onRerun, onClose, error, open }: GlobalErrorModalProps) { + const api = useStorybookApi(); + + const troubleshootURL = api.getDocsUrl({ + subpath: TROUBLESHOOTING_LINK, + versioned: true, + renderer: true, + }); + + return ( + + + Storybook Tests error details + + + + + + + + + + {error} +
+
+ Troubleshoot:{' '} + + {troubleshootURL} + +
+
+ ); +} diff --git a/code/addons/test/src/constants.ts b/code/addons/test/src/constants.ts index 42995d81d183..bc90b7430b34 100644 --- a/code/addons/test/src/constants.ts +++ b/code/addons/test/src/constants.ts @@ -4,3 +4,4 @@ export const PANEL_ID = `${ADDON_ID}/panel`; export const TUTORIAL_VIDEO_LINK = 'https://youtu.be/Waht9qq7AoA'; export const DOCUMENTATION_LINK = 'writing-tests/component-testing'; +export const TROUBLESHOOTING_LINK = `${DOCUMENTATION_LINK}#troubleshooting`; diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts index 1b87bdedf0a5..b5d6ecab179e 100644 --- a/code/addons/test/src/node/test-manager.test.ts +++ b/code/addons/test/src/node/test-manager.test.ts @@ -15,6 +15,7 @@ const vitest = vi.hoisted(() => ({ runFiles: vi.fn(), cancelCurrentRun: vi.fn(), globTestSpecs: vi.fn(), + getModuleProjects: vi.fn(() => []), })); vi.mock('vitest/node', () => ({ @@ -35,28 +36,34 @@ const tests = [ }, ]; +const options: ConstructorParameters[1] = { + onError: (message, error) => { + throw error; + }, + onReady: vi.fn(), +}; + describe('TestManager', () => { it('should create a vitest instance', async () => { - new TestManager(mockChannel); + new TestManager(mockChannel, options); await new Promise((r) => setTimeout(r, 1000)); expect(createVitest).toHaveBeenCalled(); }); it('should call onReady callback', async () => { - const onReady = vi.fn(); - new TestManager(mockChannel, { onReady }); + new TestManager(mockChannel, options); await new Promise((r) => setTimeout(r, 1000)); - expect(onReady).toHaveBeenCalled(); + expect(options.onReady).toHaveBeenCalled(); }); it('TestManager.start should start vitest and resolve when ready', async () => { - const testManager = await TestManager.start(mockChannel); + const testManager = await TestManager.start(mockChannel, options); expect(testManager).toBeInstanceOf(TestManager); expect(createVitest).toHaveBeenCalled(); }); it('should handle watch mode request', async () => { - const testManager = await TestManager.start(mockChannel); + const testManager = await TestManager.start(mockChannel, options); expect(testManager.watchMode).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); @@ -67,7 +74,7 @@ describe('TestManager', () => { it('should handle run request', async () => { vitest.globTestSpecs.mockImplementation(() => tests); - const testManager = await TestManager.start(mockChannel); + const testManager = await TestManager.start(mockChannel, options); expect(createVitest).toHaveBeenCalledTimes(1); await testManager.handleRunRequest({ @@ -91,7 +98,7 @@ describe('TestManager', () => { it('should filter tests', async () => { vitest.globTestSpecs.mockImplementation(() => tests); - const testManager = await TestManager.start(mockChannel); + const testManager = await TestManager.start(mockChannel, options); await testManager.handleRunRequest({ providerId: TEST_PROVIDER_ID, @@ -119,7 +126,7 @@ describe('TestManager', () => { }); it('should handle run all request', async () => { - const testManager = await TestManager.start(mockChannel); + const testManager = await TestManager.start(mockChannel, options); expect(createVitest).toHaveBeenCalledTimes(1); await testManager.handleRunAllRequest({ providerId: TEST_PROVIDER_ID }); diff --git a/code/core/src/components/components/Modal/Modal.stories.tsx b/code/core/src/components/components/Modal/Modal.stories.tsx index 0793d24c8db5..998e033d312e 100644 --- a/code/core/src/components/components/Modal/Modal.stories.tsx +++ b/code/core/src/components/components/Modal/Modal.stories.tsx @@ -10,7 +10,21 @@ type Story = StoryObj; const meta = { component: Modal, - decorators: [(storyFn) =>
{storyFn()}
], + decorators: [ + (storyFn) => ( +
+ {storyFn()} +
+ ), + ], } satisfies Meta; export default meta; diff --git a/code/core/src/components/components/Modal/Modal.styled.tsx b/code/core/src/components/components/Modal/Modal.styled.tsx index ca56fb05627e..37bb8be84404 100644 --- a/code/core/src/components/components/Modal/Modal.styled.tsx +++ b/code/core/src/components/components/Modal/Modal.styled.tsx @@ -30,7 +30,7 @@ const zoomIn = keyframes({ }); export const Overlay = styled.div({ - backgroundColor: 'rgba(27, 28, 29, 0.2)', + backdropFilter: 'blur(24px)', position: 'fixed', inset: 0, width: '100%', @@ -43,7 +43,7 @@ export const Container = styled.div<{ width?: number; height?: number }>( ({ theme, width, height }) => ({ backgroundColor: theme.background.bar, borderRadius: 6, - boxShadow: `rgba(255, 255, 255, 0.05) 0 0 0 1px inset, rgba(14, 18, 22, 0.35) 0px 10px 38px -10px`, + boxShadow: '0px 4px 67px 0px #00000040', position: 'fixed', top: '50%', left: '50%', diff --git a/code/yarn.lock b/code/yarn.lock index 1b737436f043..4cb52a592b91 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6239,6 +6239,7 @@ __metadata: "@storybook/icons": "npm:^1.2.12" "@storybook/instrumenter": "workspace:*" "@storybook/test": "workspace:*" + "@storybook/theming": "workspace:*" "@types/node": "npm:^22.0.0" "@types/semver": "npm:^7" "@vitest/browser": "npm:^2.1.1"