Skip to content
Merged
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
7 changes: 7 additions & 0 deletions jest.polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ const { TextEncoder, TextDecoder } = require('node:util');
Reflect.set(globalThis, 'TextEncoder', TextEncoder);
Reflect.set(globalThis, 'TextDecoder', TextDecoder);

// Polyfill setImmediate and clearImmediate for undici
// undici uses these Node.js-specific timers internally, but they don't exist in jsdom
const { setImmediate: nodeSetImmediate, clearImmediate: nodeClearImmediate } = require('node:timers');

Reflect.set(globalThis, 'setImmediate', nodeSetImmediate);
Reflect.set(globalThis, 'clearImmediate', nodeClearImmediate);

const { Blob } = require('node:buffer');
const { fetch, Request, Response, Headers, FormData } = require('undici');

Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const LeftSidebar = ({ className, inHeader = false }: LeftSidebarProps) => {
type="multiple"
value={openProducts}
className={cn(
'bg-neutral-000 dark:bg-neutral-1300 overflow-y-auto',
'bg-neutral-000 dark:bg-neutral-1300 overflow-x-hidden overflow-y-auto',
inHeader
? 'w-full h-[calc(100dvh-64px-128px)]'
: [
Expand Down
58 changes: 54 additions & 4 deletions src/components/Layout/MDXWrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { ReactNode } from 'react';
import { ReactNode } from 'react';
import { WindowLocation } from '@reach/router';
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { Helmet } from 'react-helmet';
import If from './mdx/If';
Expand Down Expand Up @@ -66,7 +66,36 @@ jest.mock('@ably/ui/core/Code', () => {
};
});

// Mock Radix UI Tooltip to avoid act() warnings from async state updates
jest.mock('@radix-ui/react-tooltip', () => ({
Provider: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Root: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Trigger: ({ children, asChild }: { children: ReactNode; asChild?: boolean }) =>
asChild ? children : <div>{children}</div>,
Portal: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Content: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));

describe('MDX component integration', () => {
beforeEach(() => {
jest.clearAllMocks();
// Suppress console warnings for MSW unhandled .md requests
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => {
const message = String(args[0]);
if (message.includes('[MSW]') && message.includes('.md')) {
return;
}
originalWarn.apply(console, args);
};
});

afterEach(() => {
// Restore console.warn and clear timers
jest.restoreAllMocks();
jest.clearAllTimers();
});

it('renders basic content correctly', () => {
render(
<div>
Expand Down Expand Up @@ -276,13 +305,24 @@ describe('MDXWrapper structured data', () => {
template: 'mdx' as const,
},
});

// Mock fetch to prevent async issues with jsdom teardown
global.fetch = jest.fn(() => Promise.reject(new Error('Markdown not available'))) as jest.Mock;

// Suppress console.error for expected fetch failures
jest.spyOn(console, 'error').mockImplementation((message) => {
if (typeof message === 'string' && message.includes('Failed to fetch markdown')) {
return;
}
});
});

afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

it('does not generate structured data when only one language is present', () => {
it('does not generate structured data when only one language is present', async () => {
render(
<UserContext.Provider value={{ sessionState: { signedIn: false }, apps: [] }}>
<MDXWrapper pageContext={defaultPageContext} location={defaultLocation}>
Expand All @@ -291,13 +331,18 @@ describe('MDXWrapper structured data', () => {
</UserContext.Provider>,
);

// Wait for any async operations to settle
await waitFor(() => {
expect(screen.getByText('Test content')).toBeInTheDocument();
});

const helmet = Helmet.peek();
const jsonLdScript = helmet.scriptTags?.find((tag: { type?: string }) => tag.type === 'application/ld+json');

expect(jsonLdScript).toBeUndefined();
});

it('generates TechArticle structured data with multiple languages', () => {
it('generates TechArticle structured data with multiple languages', async () => {
mockUseLayoutContext.mockReturnValue({
activePage: {
product: 'pubsub',
Expand All @@ -320,6 +365,11 @@ describe('MDXWrapper structured data', () => {
</UserContext.Provider>,
);

// Wait for any async operations to settle
await waitFor(() => {
expect(screen.getByText('Test content')).toBeInTheDocument();
});

const helmet = Helmet.peek();
const jsonLdScript = helmet.scriptTags?.find((tag: { type?: string }) => tag.type === 'application/ld+json');

Expand Down
151 changes: 151 additions & 0 deletions src/components/Layout/mdx/PageHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, { ReactNode } from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { PageHeader } from './PageHeader';

const mockUseLayoutContext = jest.fn(() => ({
activePage: {
language: 'javascript',
languages: ['javascript'],
product: 'pubsub',
page: {
name: 'Test Page',
link: '/docs/test-page',
},
tree: [],
template: 'mdx' as const,
},
}));

// Mock the layout context
jest.mock('src/contexts/layout-context', () => ({
useLayoutContext: () => mockUseLayoutContext(),
}));

// Mock useLocation from @reach/router
jest.mock('@reach/router', () => ({
useLocation: () => ({ pathname: '/docs/test-page' }),
}));

// Mock Radix UI Tooltip to avoid act() warnings from async state updates
jest.mock('@radix-ui/react-tooltip', () => ({
Provider: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Root: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Trigger: ({ children, asChild }: { children: ReactNode; asChild?: boolean }) =>
asChild ? children : <div>{children}</div>,
Portal: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Content: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));

// Mock Icon component
jest.mock('@ably/ui/core/Icon', () => {
return {
__esModule: true,
default: ({ name, size }: { name: string; size: string }) => (
<span data-testid={`icon-${name}`} style={{ width: size, height: size }}>
{name}
</span>
),
};
});

// Mock track function
jest.mock('@ably/ui/core/insights', () => ({
track: jest.fn(),
}));

// Mock LanguageSelector to avoid its complexity
jest.mock('../LanguageSelector', () => ({
LanguageSelector: () => <div data-testid="language-selector">Language Selector</div>,
}));

describe('PageHeader Markdown buttons', () => {
beforeEach(() => {
jest.clearAllMocks();
});

afterEach(() => {
jest.restoreAllMocks();
jest.clearAllTimers();
});

it('shows Markdown copy and view buttons when markdown content is available', async () => {
const mockMarkdownContent = '# Mock markdown content\n\nThis is a test.';

// Mock successful fetch response
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
headers: {
get: (name: string) => (name === 'Content-Type' ? 'text/markdown' : null),
},
text: () => Promise.resolve(mockMarkdownContent),
} as Response),
);

// Mock clipboard API
const mockWriteText = jest.fn();
Object.assign(navigator, {
clipboard: {
writeText: mockWriteText,
},
});

// Use fake timers to control the setTimeout in resetCopyTooltip
jest.useFakeTimers();

render(<PageHeader title="Test Page" intro="Test intro" />);

// Wait for the fetch to complete and state to update
await screen.findByText('Markdown');

// Check that the Copy button is present
const copyButton = screen.getByLabelText('Copy Markdown');
expect(copyButton).toBeInTheDocument();

// Check that the View link is present
const viewLink = screen.getByRole('link', { name: /icon-gui-eye-outline/i });
expect(viewLink).toBeInTheDocument();
expect(viewLink).toHaveAttribute('href', '/docs/test-page.md');

// Click the copy button and verify clipboard interaction
act(() => {
fireEvent.click(copyButton);
});

expect(mockWriteText).toHaveBeenCalledWith(mockMarkdownContent);
expect(mockWriteText).toHaveBeenCalledTimes(1);

// Clean up timers
act(() => {
jest.runOnlyPendingTimers();
});
jest.useRealTimers();
});

it('does not show Markdown buttons when markdown content is null', async () => {
// Suppress console.error for this test since we're testing a failure case
const originalError = console.error;
console.error = jest.fn();

// Mock failed fetch response
global.fetch = jest.fn(() =>
Promise.resolve({
ok: false,
status: 404,
} as Response),
);

render(<PageHeader title="Test Page" intro="Test intro" />);

// Wait a bit for any async updates
await new Promise<void>((resolve) => setTimeout(resolve, 100));

// Check that the Markdown section is not present
expect(screen.queryByText('Markdown')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Copy Markdown')).not.toBeInTheDocument();

// Restore console.error
console.error = originalError;
});
});
Loading