test(context-menu): add tests for reactive checkbox behavior

This commit is contained in:
Jannis Braun
2026-03-24 21:53:46 +01:00
parent 7f4f40a1e5
commit 38ffc214f0
@@ -0,0 +1,144 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useContextMenuStore } from '../../../stores/contextMenuStore';
import { ContextMenuRenderer } from '../ContextMenuRenderer';
// Minimal mock for uiStore (ContextMenuRenderer reads isMobile)
vi.mock('../../../stores/uiStore', () => ({
useUIStore: (selector: (s: { isMobile: boolean }) => unknown) =>
selector({ isMobile: false }),
}));
beforeEach(() => {
// Reset the context menu store to closed state between tests to prevent
// state leaking from one test into the next.
useContextMenuStore.getState().close();
});
describe('ContextMenu checkbox reactivity', () => {
it('updates checkbox indicator when store value changes on click', async () => {
const user = userEvent.setup();
// External state the checkbox subscribes to
let currentValue = false;
const listeners = new Set<() => void>();
const subscribe = (cb: () => void) => {
listeners.add(cb);
return () => listeners.delete(cb);
};
const getChecked = () => currentValue;
const onChange = (checked: boolean) => {
currentValue = checked;
listeners.forEach((cb) => cb());
};
// Open context menu with a reactive checkbox
useContextMenuStore.getState().open({ x: 100, y: 100 }, [
{
key: 'test-checkbox',
type: 'checkbox',
label: 'Test Mute',
subscribe,
getChecked,
onChange,
},
]);
render(<ContextMenuRenderer />);
const button = screen.getByText('Test Mute').closest('button')!;
expect(button).toBeTruthy();
// Checkbox should start unchecked — the indicator's inner SVG checkmark should not be present
const getCheckmark = () => button.querySelector('svg[viewBox="0 0 24 24"]');
expect(getCheckmark()).toBeNull();
// Click to mute
await user.click(button);
// Checkbox should now be checked
expect(currentValue).toBe(true);
expect(getCheckmark()).not.toBeNull();
// Click again to unmute
await user.click(button);
// Checkbox should be unchecked again
expect(currentValue).toBe(false);
expect(getCheckmark()).toBeNull();
});
it('does not close the menu after toggling a checkbox', async () => {
const user = userEvent.setup();
let currentValue = false;
const listeners = new Set<() => void>();
useContextMenuStore.getState().open({ x: 100, y: 100 }, [
{
key: 'test-checkbox',
type: 'checkbox',
label: 'Stay Open',
subscribe: (cb: () => void) => {
listeners.add(cb);
return () => listeners.delete(cb);
},
getChecked: () => currentValue,
onChange: (checked: boolean) => {
currentValue = checked;
listeners.forEach((cb) => cb());
},
},
]);
render(<ContextMenuRenderer />);
await user.click(screen.getByText('Stay Open'));
// Menu should still be open (item still visible)
expect(screen.getByText('Stay Open')).toBeTruthy();
expect(useContextMenuStore.getState().menu).not.toBeNull();
});
it('updates checkbox when external code changes the store (not via click)', async () => {
// External state the checkbox subscribes to
let currentValue = false;
const listeners = new Set<() => void>();
const subscribe = (cb: () => void) => {
listeners.add(cb);
return () => listeners.delete(cb);
};
const getChecked = () => currentValue;
useContextMenuStore.getState().open({ x: 100, y: 100 }, [
{
key: 'test-checkbox',
type: 'checkbox',
label: 'External Update',
subscribe,
getChecked,
onChange: () => {},
},
]);
render(<ContextMenuRenderer />);
const button = screen.getByText('External Update').closest('button')!;
const getCheckmark = () => button.querySelector('svg[viewBox="0 0 24 24"]');
// Starts unchecked
expect(getCheckmark()).toBeNull();
// Simulate external store change (e.g., another component or WebSocket event)
act(() => {
currentValue = true;
listeners.forEach((cb) => cb());
});
// Checkbox should now show as checked without any click
expect(getCheckmark()).not.toBeNull();
});
});