Skip to content

Commit 7c4ccc8

Browse files
hman38705claude
andcommitted
fix: repair frontend dev-mode client/server boundaries and axe-core crash
- Add missing "use client" to OfflineBanner and MarketDetailView, which call client-only hooks directly; without it, dev mode throws "Attempted to call X() from the server but X is on the client." (next build didn't catch this since prerendering takes a different path) - Give @axe-core/react a mutable shallow copy of the React module instead of the frozen ES module namespace object Turbopack hands client bundles, so its createElement monkey-patch no longer throws "Cannot set property createElement ... which has only a getter" - Update/add reportAccessibility tests to cover the copy behavior and the new graceful-failure path Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwH69MWtWXdEwaBWyxEiNb
1 parent 2ecdd6e commit 7c4ccc8

4 files changed

Lines changed: 34 additions & 2 deletions

File tree

frontend/src/components/OfflineBanner.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import React from 'react';
24
import { useOnlineStatus } from '../lib/hooks/useOnlineStatus';
35
import './OfflineBanner.css';

frontend/src/components/markets/MarketDetailView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import React from 'react';
24
import { useAsync } from '../../lib/hooks/useAsync';
35
import { api, ApiError, isMarketNotResolvedError } from '../../lib/api/public-client';

frontend/src/lib/__tests__/reportAccessibility.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,30 @@ describe('reportAccessibility', () => {
3333

3434
expect(axeDefault).toHaveBeenCalledTimes(1);
3535
const [reactArg, domArg, timeout, cfg] = axeDefault.mock.calls[0];
36-
expect(reactArg).toBe(fakeReact);
36+
// A shallow copy, not the same reference — axe patches `createElement` in
37+
// place, and Turbopack's client bundle hands us a frozen module namespace
38+
// object for `react`, so the harness must pass a mutable copy instead.
39+
expect(reactArg).not.toBe(fakeReact);
40+
expect(reactArg).toEqual(fakeReact);
3741
expect(domArg.__isMockDOM).toBe(true);
3842
expect(timeout).toBe(1000);
3943
expect(cfg).toEqual(config);
4044
});
4145

46+
it('does not throw when @axe-core/react fails to patch React', async () => {
47+
const { reportAccessibility, axeDefault } = load();
48+
jest.replaceProperty(process.env, 'NODE_ENV', 'development');
49+
axeDefault.mockImplementation(() => {
50+
throw new TypeError('Cannot set property createElement of [object Module] which has only a getter');
51+
});
52+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
53+
54+
await expect(reportAccessibility(fakeReact)).resolves.toBeUndefined();
55+
56+
expect(warnSpy).toHaveBeenCalled();
57+
warnSpy.mockRestore();
58+
});
59+
4260
it('only initializes once even when called multiple times', async () => {
4361
const { reportAccessibility, axeDefault } = load();
4462
jest.replaceProperty(process.env, 'NODE_ENV', 'development');

frontend/src/lib/reportAccessibility.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,17 @@ export async function reportAccessibility(
3737
const axe = await import('@axe-core/react');
3838
const ReactDOM = await import('react-dom');
3939

40+
// @axe-core/react monkey-patches `React.createElement` in place. Turbopack's
41+
// client bundle hands us a frozen ES module namespace object for `react`, so
42+
// assigning to it directly throws ("... has only a getter"). Give axe a
43+
// mutable shallow copy instead — it only ever writes to `createElement`.
44+
const patchableReact = { ...ReactModule };
45+
4046
// (ReactModule, ReactDOM, timeoutMs, config) — timeout is how long axe waits
4147
// after a render before scanning, to avoid throttling during bursty updates.
42-
axe.default(ReactModule, ReactDOM, 1000, config);
48+
try {
49+
axe.default(patchableReact, ReactDOM, 1000, config);
50+
} catch (err) {
51+
console.warn('[a11y] failed to initialize @axe-core/react', err);
52+
}
4353
}

0 commit comments

Comments
 (0)