Skip to content

Commit

Permalink
Merge pull request #29309 from storybookjs/valentin/add-global-error-…
Browse files Browse the repository at this point in the history
…modal

Addon-Test: Implement Global Error Modal
  • Loading branch information
valentinpalkovic authored Oct 10, 2024
2 parents b24b3a2 + 06836a0 commit 1354966
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 12 deletions.
1 change: 1 addition & 0 deletions code/addons/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
82 changes: 82 additions & 0 deletions code/addons/test/src/components/GlobalErrorModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof meta>;

const managerContext: any = {
state: {},
api: {
getDocsUrl: fn(({ subpath }) => `https://storybook.js.org/docs/${subpath}`).mockName(
'api::getDocsUrl'
),
},
};

const meta = {
component: GlobalErrorModal,
decorators: [
(storyFn) => (
<ManagerContext.Provider value={managerContext}>
<div
style={{
width: '100%',
minWidth: '1200px',
height: '800px',
background:
'repeating-linear-gradient(45deg, #000000, #ffffff 50px, #ffffff 50px, #ffffff 80px)',
}}
>
{storyFn()}
</div>
</ManagerContext.Provider>
),
],
args: {
onRerun: fn(),
onClose: fn(),
open: false,
},
} satisfies Meta<typeof GlobalErrorModal>;

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 (
<>
<GlobalErrorModal {...props} open={isOpen} />
<button onClick={() => setOpen(true)}>Open modal</button>
</>
);
},
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();
},
};
91 changes: 91 additions & 0 deletions code/addons/test/src/components/GlobalErrorModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Modal onEscapeKeyDown={onClose} onInteractOutside={onClose} open={open}>
<ModalBar>
<ModalTitle>Storybook Tests error details</ModalTitle>
<ModalActionBar>
<Button onClick={onRerun} variant="ghost">
<SyncIcon />
Rerun
</Button>
<Button variant="ghost" asChild>
<a target="_blank" href={troubleshootURL} rel="noreferrer">
Troubleshoot
</a>
</Button>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</ModalActionBar>
</ModalBar>
<ModalStackTrace>
{error}
<br />
<br />
Troubleshoot:{' '}
<TroubleshootLink target="_blank" href={troubleshootURL}>
{troubleshootURL}
</TroubleshootLink>
</ModalStackTrace>
</Modal>
);
}
1 change: 1 addition & 0 deletions code/addons/test/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
25 changes: 16 additions & 9 deletions code/addons/test/src/node/test-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const vitest = vi.hoisted(() => ({
runFiles: vi.fn(),
cancelCurrentRun: vi.fn(),
globTestSpecs: vi.fn(),
getModuleProjects: vi.fn(() => []),
}));

vi.mock('vitest/node', () => ({
Expand All @@ -35,28 +36,34 @@ const tests = [
},
];

const options: ConstructorParameters<typeof TestManager>[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);

Expand All @@ -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({
Expand All @@ -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,
Expand Down Expand Up @@ -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 });
Expand Down
16 changes: 15 additions & 1 deletion code/core/src/components/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@ type Story = StoryObj<typeof meta>;

const meta = {
component: Modal,
decorators: [(storyFn) => <div style={{ width: '1200px', height: '800px' }}>{storyFn()}</div>],
decorators: [
(storyFn) => (
<div
style={{
width: '100%',
minWidth: '1200px',
height: '800px',
background:
'repeating-linear-gradient(45deg, #000000, #ffffff 50px, #ffffff 50px, #ffffff 80px)',
}}
>
{storyFn()}
</div>
),
],
} satisfies Meta<typeof Modal>;

export default meta;
Expand Down
4 changes: 2 additions & 2 deletions code/core/src/components/components/Modal/Modal.styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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%',
Expand All @@ -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%',
Expand Down
1 change: 1 addition & 0 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 1354966

Please sign in to comment.