Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions code/addons/docs/src/blocks/blocks/mdx.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import React from 'react';

import { AnchorMdx, HeaderMdx } from './mdx.tsx';

const { emitMock } = vi.hoisted(() => ({
emitMock: vi.fn(),
}));

vi.mock('./DocsContext', () => ({
DocsContext: React.createContext({
channel: { emit: emitMock },
}),
}));

vi.mock('../components', () => ({
Source: ({ children }: any) => <pre>{children}</pre>,
}));

vi.mock('storybook/internal/components', () => {
const MockAnchor = React.forwardRef<HTMLAnchorElement, any>(({ children, ...props }, ref) => (
<a ref={ref} {...props}>
{children}
</a>
));
MockAnchor.displayName = 'MockAnchor';

return {
Button: ({ children }: any) => <>{children}</>,
Code: ({ children }: any) => <code>{children}</code>,
components: {
a: MockAnchor,
},
nameSpaceClassNames: (props: any) => props,
};
});

vi.mock('@storybook/icons', () => ({
LinkIcon: () => <span data-testid="link-icon" />,
}));

vi.mock('storybook/theming', () => {
const styledFactory = (tag: React.ElementType) => () => {
const Styled = React.forwardRef<any, any>(({ children, ...props }, ref) =>
React.createElement(tag, { ...props, ref }, children)
);
Styled.displayName = `Styled${typeof tag === 'string' ? tag : 'Component'}`;
return Styled;
};

return {
styled: new Proxy(styledFactory, {
get: (_target, tag) => styledFactory(tag as string),
}),
};
});
Comment on lines +13 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update Vitest module mocks to match the repo spy-mocking contract in code/addons/docs/src/blocks/blocks/mdx.test.tsx.

Add the required { spy: true } option to the vi.mock() calls (e.g., ./DocsContext, ../components, storybook/internal/components, @storybook/icons, storybook/theming) and use vi.mocked() for type-safe access to the mocked exports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/addons/docs/src/blocks/blocks/mdx.test.tsx` around lines 13 - 59, Add
the { spy: true } option to each vi.mock(...) call (the mocks for
'./DocsContext', '../components', 'storybook/internal/components',
'`@storybook/icons`', and 'storybook/theming') and update usages to access mocked
exports via vi.mocked(...) for type safety; specifically wrap the mock factory
returns so callers can do const MockedDocsContext = vi.mocked(DocsContext) or
vi.mocked(LinkIcon) as needed, and keep the existing mock implementations for
Source, MockAnchor, styledFactory/styled, Button/Code/components, and LinkIcon
unchanged except for switching to vi.mock(..., { spy: true }) and using
vi.mocked(...) where the tests read from those modules.


describe('AnchorMdx hash link scrolling', () => {
afterEach(() => {
emitMock.mockClear();
cleanup();
document.body.innerHTML = '';
});

it('scrolls hash links inside the preview document without emitting navigation', () => {
const target = document.createElement('div');
target.id = 'some-content';
target.scrollIntoView = vi.fn();
document.body.appendChild(target);

const { getByText } = render(
<AnchorMdx href="#some-content" target="_self">
Go to content
</AnchorMdx>
);

fireEvent.click(getByText('Go to content'));

expect(emitMock).not.toHaveBeenCalled();
expect(target.scrollIntoView).toHaveBeenCalledWith({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
});
});

it('does not emit navigation when a hash target is missing', () => {
const { getByText } = render(
<AnchorMdx href="#missing-content" target="_self">
Missing target
</AnchorMdx>
);

const link = getByText('Missing target');
link.addEventListener('click', (event) => event.preventDefault());
fireEvent.click(link);

expect(emitMock).not.toHaveBeenCalled();
});

it('preserves external link behavior', () => {
const { getByText } = render(
<AnchorMdx href="https://example.com" target="_blank">
External link
</AnchorMdx>
);

const link = getByText('External link') as HTMLAnchorElement;
expect(link.href).toBe('https://example.com/');
expect(link.target).toBe('_blank');
});

it('preserves target blank behavior for hash links', () => {
const target = document.createElement('div');
target.id = 'some-content';
target.scrollIntoView = vi.fn();
document.body.appendChild(target);

const { getByText } = render(
<AnchorMdx href="#some-content" target="_blank">
New tab hash
</AnchorMdx>
);

const link = getByText('New tab hash');
link.addEventListener('click', (event) => event.preventDefault());
fireEvent.click(link);

expect(emitMock).not.toHaveBeenCalled();
expect(target.scrollIntoView).not.toHaveBeenCalled();
expect((link as HTMLAnchorElement).target).toBe('_blank');
});
});

describe('HeaderMdx heading anchor scrolling', () => {
afterEach(() => {
emitMock.mockClear();
cleanup();
document.body.innerHTML = '';
});

it('scrolls heading anchors inside the preview document without emitting navigation', () => {
const scrollIntoView = vi.fn();
const originalScrollIntoView = Element.prototype.scrollIntoView;
Element.prototype.scrollIntoView = scrollIntoView;

try {
const { container } = render(
<HeaderMdx as="h2" id="my-heading">
My Heading
</HeaderMdx>
);

const anchor = container.querySelector('a[href="#my-heading"]');
expect(anchor).toBeTruthy();

fireEvent.click(anchor!);

expect(emitMock).not.toHaveBeenCalled();
expect(scrollIntoView).toHaveBeenCalledWith({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
});
} finally {
Element.prototype.scrollIntoView = originalScrollIntoView;
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
12 changes: 5 additions & 7 deletions code/addons/docs/src/blocks/blocks/mdx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { styled } from 'storybook/theming';
import { Source } from '../components';
import type { DocsContextProps } from './DocsContext';
import { DocsContext } from './DocsContext';
import { scrollToElement } from './utils';

const { document } = globalThis;

Expand Down Expand Up @@ -71,8 +72,6 @@ interface AnchorInPageProps {
}

const AnchorInPage: FC<PropsWithChildren<AnchorInPageProps>> = ({ hash, children }) => {
const context = useContext(DocsContext);

return (
<A
href={hash}
Expand All @@ -81,7 +80,8 @@ const AnchorInPage: FC<PropsWithChildren<AnchorInPageProps>> = ({ hash, children
const id = hash.substring(1);
const element = document.getElementById(id);
if (element) {
navigate(context, hash);
event.preventDefault();
scrollToElement(element);
}
}}
>
Expand Down Expand Up @@ -196,8 +196,6 @@ const HeaderWithOcticonAnchor: FC<PropsWithChildren<HeaderWithOcticonAnchorProps
children,
...rest
}) => {
const context = useContext(DocsContext);

// @ts-expect-error (Converted from ts-ignore)
const OcticonHeader = OcticonHeaders[as];
const hash = `#${id}`;
Expand All @@ -211,7 +209,7 @@ const HeaderWithOcticonAnchor: FC<PropsWithChildren<HeaderWithOcticonAnchorProps
variant="ghost"
size="small"
padding="small"
ariaLabel="Copy heading URL to address bar"
ariaLabel="Scroll to heading"
>
<a
href={hash}
Expand All @@ -220,7 +218,7 @@ const HeaderWithOcticonAnchor: FC<PropsWithChildren<HeaderWithOcticonAnchorProps
event.preventDefault();
const element = document.getElementById(id);
if (element) {
navigate(context, hash);
scrollToElement(element);
}
}}
>
Expand Down
Loading