Skip to content

Commit 36e0986

Browse files
jamiehensonclaude
andcommitted
test: add tests for structured data and hidden language links
Add comprehensive tests for: - Head component JSON-LD structured data rendering - HiddenLanguageLinks component crawlable links generation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent d18b729 commit 36e0986

File tree

3 files changed

+166
-16
lines changed

3 files changed

+166
-16
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import '@testing-library/jest-dom/extend-expect';
4+
import { useLocation } from '@reach/router';
5+
import HiddenLanguageLinks from './HiddenLanguageLinks';
6+
import { useLayoutContext } from 'src/contexts/layout-context';
7+
8+
jest.mock('src/contexts/layout-context', () => ({
9+
useLayoutContext: jest.fn(),
10+
}));
11+
12+
jest.mock('@reach/router', () => ({
13+
useLocation: jest.fn(),
14+
}));
15+
16+
const mockUseLayoutContext = useLayoutContext as jest.Mock;
17+
const mockUseLocation = useLocation as jest.Mock;
18+
19+
describe('HiddenLanguageLinks', () => {
20+
beforeEach(() => {
21+
mockUseLocation.mockReturnValue({
22+
pathname: '/docs/channels',
23+
});
24+
});
25+
26+
afterEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
30+
it('renders nothing when there is only one language', () => {
31+
mockUseLayoutContext.mockReturnValue({
32+
activePage: {
33+
product: 'pubsub',
34+
languages: ['javascript'],
35+
},
36+
});
37+
38+
const { container } = render(<HiddenLanguageLinks />);
39+
expect(container.firstChild).toBeNull();
40+
});
41+
42+
it('renders nothing when there are no languages', () => {
43+
mockUseLayoutContext.mockReturnValue({
44+
activePage: {
45+
product: 'pubsub',
46+
languages: [],
47+
},
48+
});
49+
50+
const { container } = render(<HiddenLanguageLinks />);
51+
expect(container.firstChild).toBeNull();
52+
});
53+
54+
it('renders links with correct hrefs for each language when > 1 language is available', () => {
55+
mockUseLayoutContext.mockReturnValue({
56+
activePage: {
57+
product: 'pubsub',
58+
languages: ['javascript', 'python', 'flutter'],
59+
},
60+
});
61+
62+
const { container } = render(<HiddenLanguageLinks />);
63+
64+
const links = Array.from(container.querySelectorAll('a'));
65+
expect(links).toHaveLength(3);
66+
67+
const hrefs = links.map((link) => link.getAttribute('href'));
68+
expect(hrefs).toContain('/docs/channels?lang=javascript');
69+
expect(hrefs).toContain('/docs/channels?lang=python');
70+
expect(hrefs).toContain('/docs/channels?lang=flutter');
71+
});
72+
});

src/components/Layout/MDXWrapper.test.tsx

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
import React, { ReactNode } from 'react';
2+
import { WindowLocation } from '@reach/router';
23
import { render, screen } from '@testing-library/react';
34
import '@testing-library/jest-dom/extend-expect';
5+
import { Helmet } from 'react-helmet';
46
import If from './mdx/If';
57
import CodeSnippet from '@ably/ui/core/CodeSnippet';
68
import UserContext from 'src/contexts/user-context';
9+
import MDXWrapper from './MDXWrapper';
710

8-
// Mock the dependencies we need for testing
9-
jest.mock('./MDXWrapper', () => {
10-
return {
11-
__esModule: true,
12-
default: ({ children, pageContext }: { children: ReactNode; pageContext: any }) => (
13-
<div data-testid="mdx-wrapper">
14-
{pageContext?.frontmatter?.title && <h1>{pageContext.frontmatter.title}</h1>}
15-
<div data-testid="mdx-content">{children}</div>
16-
</div>
17-
),
18-
};
19-
});
11+
const mockUseLayoutContext = jest.fn(() => ({
12+
activePage: { language: 'javascript', languages: ['javascript'], product: 'pubsub' },
13+
}));
2014

2115
// Mock the layout context
2216
jest.mock('src/contexts/layout-context', () => ({
23-
useLayoutContext: () => ({
24-
activePage: { language: 'javascript' },
25-
}),
17+
useLayoutContext: () => mockUseLayoutContext(),
2618
LayoutProvider: ({ children }: { children: ReactNode }) => <div data-testid="layout-provider">{children}</div>,
2719
}));
2820

21+
jest.mock('@reach/router', () => ({
22+
useLocation: () => ({ pathname: '/docs/test-page' }),
23+
}));
24+
25+
jest.mock('src/hooks/use-site-metadata', () => ({
26+
useSiteMetadata: () => ({
27+
canonicalUrl: (path: string) => `https://example.com${path}`,
28+
}),
29+
}));
30+
2931
// We need to mock minimal implementation of other dependencies that CodeSnippet might use
3032
jest.mock('@ably/ui/core/Icon', () => {
3133
return {
@@ -235,3 +237,80 @@ channel.subscribe('event', (message: Ably.Types.Message) => {
235237
expect(typescriptElement).toBeInTheDocument();
236238
});
237239
});
240+
241+
describe('MDXWrapper structured data', () => {
242+
const defaultPageContext = {
243+
frontmatter: {
244+
title: 'Test Page',
245+
meta_description: 'Test description',
246+
},
247+
languages: [],
248+
layout: { mdx: true, leftSidebar: true, rightSidebar: true, searchBar: true, template: 'docs' },
249+
};
250+
251+
const defaultLocation = {
252+
pathname: '/docs/test-page',
253+
} as WindowLocation;
254+
255+
beforeEach(() => {
256+
mockUseLayoutContext.mockReturnValue({
257+
activePage: {
258+
product: 'pubsub',
259+
language: 'javascript',
260+
languages: [],
261+
},
262+
});
263+
});
264+
265+
afterEach(() => {
266+
jest.clearAllMocks();
267+
});
268+
269+
it('does not generate structured data when only one language is present', () => {
270+
render(
271+
<UserContext.Provider value={{ sessionState: { signedIn: false }, apps: [] }}>
272+
<MDXWrapper pageContext={defaultPageContext} location={defaultLocation}>
273+
<div>Test content</div>
274+
</MDXWrapper>
275+
</UserContext.Provider>,
276+
);
277+
278+
const helmet = Helmet.peek();
279+
const jsonLdScript = helmet.scriptTags?.find((tag: { type?: string }) => tag.type === 'application/ld+json');
280+
281+
expect(jsonLdScript).toBeUndefined();
282+
});
283+
284+
it('generates TechArticle structured data with multiple languages', () => {
285+
mockUseLayoutContext.mockReturnValue({
286+
activePage: {
287+
product: 'pubsub',
288+
language: 'javascript',
289+
languages: ['javascript', 'python'],
290+
},
291+
});
292+
293+
render(
294+
<UserContext.Provider value={{ sessionState: { signedIn: false }, apps: [] }}>
295+
<MDXWrapper pageContext={defaultPageContext} location={defaultLocation}>
296+
<div>Test content</div>
297+
</MDXWrapper>
298+
</UserContext.Provider>,
299+
);
300+
301+
const helmet = Helmet.peek();
302+
const jsonLdScript = helmet.scriptTags?.find((tag: { type?: string }) => tag.type === 'application/ld+json');
303+
304+
expect(jsonLdScript).toBeDefined();
305+
expect(jsonLdScript?.type).toBe('application/ld+json');
306+
307+
const structuredData = JSON.parse(jsonLdScript?.innerHTML || '{}');
308+
expect(structuredData['@type']).toBe('TechArticle');
309+
expect(structuredData.hasPart).toHaveLength(2);
310+
expect(structuredData.hasPart[0]['@type']).toBe('SoftwareSourceCode');
311+
expect(structuredData.hasPart[0].programmingLanguage).toBe('JavaScript');
312+
expect(structuredData.hasPart[1].programmingLanguage).toBe('Python');
313+
expect(structuredData.hasPart[0].url).toBe('https://example.com/docs/test-page?lang=javascript');
314+
expect(structuredData.hasPart[1].url).toBe('https://example.com/docs/test-page?lang=python');
315+
});
316+
});

src/components/Layout/MDXWrapper.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import { useSiteMetadata } from 'src/hooks/use-site-metadata';
3333
import { ProductName } from 'src/templates/template-data';
3434
import { getMetaTitle } from '../common/meta-title';
3535
import UserContext from 'src/contexts/user-context';
36-
import { LanguageKey } from 'src/data/languages/types';
3736

3837
type MDXWrapperProps = PageProps<unknown, PageContextType>;
3938

0 commit comments

Comments
 (0)