Skip to content

Commit 854365a

Browse files
test: add tests for ? keypress crash (#2262) and sidebar re-render (UX-972)
- use-developer-view: verify hook doesn't crash on ? keypress, reads localStorage correctly, and handles invalid JSON gracefully - sidebar: verify TracingService defaults to unsupported, Transcripts item appears after endpointCompatibility loads, and store selector triggers re-render Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 97f3bac commit 854365a

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-0
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { createGroupedSidebarItems } from 'utils/route-utils';
3+
import { afterEach, describe, expect, it, vi } from 'vitest';
4+
5+
import type { EndpointCompatibility } from '../../state/rest-interfaces';
6+
import { Feature, useSupportedFeaturesStore } from '../../state/supported-features';
7+
8+
// Mock config to enable embedded + ADP mode (required for Transcripts route visibility)
9+
vi.mock('../../config', async (importOriginal) => {
10+
const actual = await importOriginal<typeof import('../../config')>();
11+
return {
12+
...actual,
13+
isEmbedded: () => true,
14+
isAdpEnabled: () => true,
15+
};
16+
});
17+
18+
describe('SidebarNavigation re-renders on endpointCompatibility change (UX-972)', () => {
19+
afterEach(() => {
20+
// Reset store to initial state between tests
21+
useSupportedFeaturesStore.setState({
22+
endpointCompatibility: null,
23+
tracingService: false,
24+
});
25+
});
26+
27+
it('TracingService defaults to unsupported when endpointCompatibility is null', () => {
28+
const state = useSupportedFeaturesStore.getState();
29+
expect(state.endpointCompatibility).toBeNull();
30+
expect(state.tracingService).toBe(false);
31+
});
32+
33+
it('Transcripts item is hidden when TracingService is not supported', () => {
34+
const groups = createGroupedSidebarItems();
35+
const allItems = groups.flatMap((g) => g.items);
36+
const transcripts = allItems.find((item) => item.to === '/transcripts');
37+
expect(transcripts).toBeUndefined();
38+
});
39+
40+
it('Transcripts item appears after endpointCompatibility loads with TracingService supported', () => {
41+
const compatibility: EndpointCompatibility = {
42+
kafkaVersion: '3.6.0',
43+
endpoints: [
44+
{
45+
endpoint: Feature.TracingService.endpoint,
46+
method: Feature.TracingService.method,
47+
isSupported: true,
48+
},
49+
],
50+
};
51+
52+
act(() => {
53+
useSupportedFeaturesStore.getState().setEndpointCompatibility(compatibility);
54+
});
55+
56+
const groups = createGroupedSidebarItems();
57+
const allItems = groups.flatMap((g) => g.items);
58+
const transcripts = allItems.find((item) => item.to === '/transcripts');
59+
expect(transcripts).toBeDefined();
60+
expect(transcripts?.title).toBe('Transcripts');
61+
});
62+
63+
it('store selector triggers re-render when endpointCompatibility changes', () => {
64+
const selector = (s: { endpointCompatibility: EndpointCompatibility | null }) => s.endpointCompatibility;
65+
const { result } = renderHook(() => useSupportedFeaturesStore(selector));
66+
67+
expect(result.current).toBeNull();
68+
69+
const compatibility: EndpointCompatibility = {
70+
kafkaVersion: '3.6.0',
71+
endpoints: [
72+
{
73+
endpoint: Feature.TracingService.endpoint,
74+
method: Feature.TracingService.method,
75+
isSupported: true,
76+
},
77+
],
78+
};
79+
80+
act(() => {
81+
useSupportedFeaturesStore.getState().setEndpointCompatibility(compatibility);
82+
});
83+
84+
expect(result.current).toBe(compatibility);
85+
});
86+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { renderHook } from '@testing-library/react';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import useDeveloperView from './use-developer-view';
5+
6+
describe('useDeveloperView', () => {
7+
const store: Record<string, string> = {};
8+
const localStorageMock = {
9+
getItem: vi.fn((key: string) => store[key] ?? null),
10+
setItem: vi.fn((key: string, value: string) => {
11+
store[key] = value;
12+
}),
13+
removeItem: vi.fn((key: string) => {
14+
delete store[key];
15+
}),
16+
clear: vi.fn(() => {
17+
for (const key of Object.keys(store)) {
18+
delete store[key];
19+
}
20+
}),
21+
get length() {
22+
return Object.keys(store).length;
23+
},
24+
key: vi.fn((index: number) => Object.keys(store)[index] ?? null),
25+
};
26+
27+
beforeEach(() => {
28+
Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true });
29+
for (const key of Object.keys(store)) {
30+
delete store[key];
31+
}
32+
vi.clearAllMocks();
33+
});
34+
35+
afterEach(() => {
36+
vi.restoreAllMocks();
37+
});
38+
39+
it('returns false by default when localStorage has no stored value', () => {
40+
const { result } = renderHook(() => useDeveloperView());
41+
expect(result.current).toBe(false);
42+
});
43+
44+
it('reads stored developer view preference from localStorage', () => {
45+
store.dv = JSON.stringify(true);
46+
const { result } = renderHook(() => useDeveloperView());
47+
expect(result.current).toBe(true);
48+
});
49+
50+
it('does not crash when pressing ? key', () => {
51+
const { result } = renderHook(() => useDeveloperView());
52+
53+
// Simulate pressing '?' — this previously caused React error #301 in production
54+
// when connected to vanilla Kafka (issue #2262)
55+
expect(() => {
56+
window.dispatchEvent(new KeyboardEvent('keydown', { key: '?' }));
57+
}).not.toThrow();
58+
59+
// Hook should still return a valid boolean
60+
expect(typeof result.current).toBe('boolean');
61+
});
62+
63+
it('returns false when localStorage contains invalid JSON', () => {
64+
store.dv = 'not-json';
65+
const { result } = renderHook(() => useDeveloperView());
66+
expect(typeof result.current).toBe('boolean');
67+
});
68+
});

0 commit comments

Comments
 (0)