test(context-menu): add tests for reactive checkbox behavior
This commit is contained in:
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user