Skip to content

Commit 7d17984

Browse files
committed
fix: add tests and fix types
1 parent 8e30a83 commit 7d17984

File tree

2 files changed

+175
-46
lines changed

2 files changed

+175
-46
lines changed

packages/compass-context-menu/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ export type ContextMenuContext = {
1717

1818
export type MenuItem = {
1919
label: string;
20-
onAction: (event: Event) => void;
20+
onAction: (event: React.KeyboardEvent | React.MouseEvent) => void;
2121
};

packages/compass-context-menu/src/use-context-menu.spec.tsx

Lines changed: 174 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,109 @@ import { useContextMenu } from './use-context-menu';
66
import { ContextMenuProvider } from './context-menu-provider';
77
import type { MenuItem } from './types';
88

9-
type TestMenuItem = MenuItem & { id: number };
10-
119
describe('useContextMenu', function () {
12-
const TestMenu: React.FC<{ items: TestMenuItem[] }> = ({ items }) => (
10+
const TestMenu: React.FC<{ items: MenuItem[] }> = ({ items }) => (
1311
<div data-testid="test-menu">
1412
{items.map((item, idx) => (
15-
<div key={idx} data-testid={`menu-item-${item.id}`}>
13+
<div
14+
key={idx}
15+
data-testid={`menu-item-${item.label}`}
16+
role="menuitem"
17+
tabIndex={0}
18+
onClick={(event) => item.onAction?.(event)}
19+
onKeyDown={(event) => {
20+
if (event.key === 'Enter') {
21+
item.onAction?.(event);
22+
}
23+
}}
24+
>
1625
{item.label}
1726
</div>
1827
))}
1928
</div>
2029
);
2130

22-
const TestComponent = () => {
31+
const TestComponent = ({
32+
onRegister,
33+
onAction,
34+
}: {
35+
onRegister?: (ref: any) => void;
36+
onAction?: (id) => void;
37+
}) => {
2338
const contextMenu = useContextMenu({ Menu: TestMenu });
24-
const items: TestMenuItem[] = [
25-
{
26-
id: 1,
27-
label: 'Test A',
28-
onAction: () => {
29-
/* noop */
30-
},
31-
},
39+
const items: MenuItem[] = [
3240
{
33-
id: 2,
34-
label: 'Test B',
35-
onAction: () => {
36-
/* noop */
37-
},
41+
label: 'Test Item',
42+
onAction: () => onAction?.(1),
3843
},
3944
];
4045
const ref = contextMenu.registerItems(items);
4146

47+
React.useEffect(() => {
48+
onRegister?.(ref);
49+
}, [ref, onRegister]);
50+
4251
return (
4352
<div data-testid="test-trigger" ref={ref}>
4453
Test Component
4554
</div>
4655
);
4756
};
4857

58+
// Add new test components for nested context menu scenario
59+
const ParentComponent = ({
60+
onAction,
61+
children,
62+
}: {
63+
onAction?: (id: number) => void;
64+
children?: React.ReactNode;
65+
}) => {
66+
const contextMenu = useContextMenu({ Menu: TestMenu });
67+
const parentItems: MenuItem[] = [
68+
{
69+
label: 'Parent Item 1',
70+
onAction: () => onAction?.(1),
71+
},
72+
{
73+
label: 'Parent Item 2',
74+
onAction: () => onAction?.(2),
75+
},
76+
];
77+
const ref = contextMenu.registerItems(parentItems);
78+
79+
return (
80+
<div data-testid="parent-trigger" ref={ref}>
81+
<div>Parent Component</div>
82+
{children}
83+
</div>
84+
);
85+
};
86+
87+
const ChildComponent = ({
88+
onAction,
89+
}: {
90+
onAction?: (id: number) => void;
91+
}) => {
92+
const contextMenu = useContextMenu({ Menu: TestMenu });
93+
const childItems: MenuItem[] = [
94+
{
95+
label: 'Child Item 1',
96+
onAction: () => onAction?.(1),
97+
},
98+
{
99+
label: 'Child Item 2',
100+
onAction: () => onAction?.(2),
101+
},
102+
];
103+
const ref = contextMenu.registerItems(childItems);
104+
105+
return (
106+
<div data-testid="child-trigger" ref={ref}>
107+
Child Component
108+
</div>
109+
);
110+
};
111+
49112
describe('when used outside provider', function () {
50113
it('throws an error', function () {
51114
expect(() => {
@@ -54,7 +117,7 @@ describe('useContextMenu', function () {
54117
});
55118
});
56119

57-
describe('with valid provider', function () {
120+
describe('with a valid provider', function () {
58121
beforeEach(() => {
59122
// Create the container for the context menu portal
60123
const container = document.createElement('div');
@@ -80,51 +143,117 @@ describe('useContextMenu', function () {
80143
expect(screen.getByTestId('test-trigger')).to.exist;
81144
});
82145

146+
it('registers context menu event listener', function () {
147+
const onRegister = sinon.spy();
148+
149+
render(
150+
<ContextMenuProvider>
151+
<TestComponent onRegister={onRegister} />
152+
</ContextMenuProvider>
153+
);
154+
155+
expect(onRegister).to.have.been.calledOnce;
156+
expect(onRegister.firstCall.args[0]).to.be.a('function');
157+
});
158+
83159
it('shows context menu on right click', function () {
84160
render(
85161
<ContextMenuProvider>
86162
<TestComponent />
87163
</ContextMenuProvider>
88164
);
89165

90-
expect(screen.queryByTestId('menu-item-1')).not.to.exist;
91-
expect(screen.queryByTestId('menu-item-2')).not.to.exist;
92-
93166
const trigger = screen.getByTestId('test-trigger');
94167
userEvent.click(trigger, { button: 2 });
95168

96169
// The menu should be rendered in the portal
97-
expect(screen.getByTestId('menu-item-1')).to.exist;
98-
expect(screen.getByTestId('menu-item-2')).to.exist;
170+
expect(screen.getByTestId('menu-item-Test Item')).to.exist;
99171
});
100172

101-
it('cleans up previous event listener when ref changes', function () {
102-
const removeEventListenerSpy = sinon.spy();
103-
const addEventListenerSpy = sinon.spy();
173+
describe('with nested context menus', function () {
174+
it('shows only parent items when right clicking parent area', function () {
175+
render(
176+
<ContextMenuProvider>
177+
<ParentComponent />
178+
</ContextMenuProvider>
179+
);
104180

105-
const { rerender } = render(
106-
<ContextMenuProvider>
107-
<TestComponent />
108-
</ContextMenuProvider>
109-
);
181+
const parentTrigger = screen.getByTestId('parent-trigger');
182+
userEvent.click(parentTrigger, { button: 2 });
183+
184+
// Should show parent items
185+
expect(screen.getByTestId('menu-item-Parent Item 1')).to.exist;
186+
expect(screen.getByTestId('menu-item-Parent Item 2')).to.exist;
110187

111-
// Simulate ref change
112-
const ref = screen.getByTestId('test-trigger');
113-
Object.defineProperty(ref, 'addEventListener', {
114-
value: addEventListenerSpy,
188+
// Should not show child items
189+
expect(() => screen.getByTestId('menu-item-Child Item 1')).to.throw;
190+
expect(() => screen.getByTestId('menu-item-Child Item 2')).to.throw;
115191
});
116-
Object.defineProperty(ref, 'removeEventListener', {
117-
value: removeEventListenerSpy,
192+
193+
it('shows both parent and child items when right clicking child area', function () {
194+
render(
195+
<ContextMenuProvider>
196+
<ParentComponent>
197+
<ChildComponent />
198+
</ParentComponent>
199+
</ContextMenuProvider>
200+
);
201+
202+
const childTrigger = screen.getByTestId('child-trigger');
203+
userEvent.click(childTrigger, { button: 2 });
204+
205+
// Should show both parent and child items
206+
expect(screen.getByTestId('menu-item-Parent Item 1')).to.exist;
207+
expect(screen.getByTestId('menu-item-Parent Item 2')).to.exist;
208+
expect(screen.getByTestId('menu-item-Child Item 1')).to.exist;
209+
expect(screen.getByTestId('menu-item-Child Item 2')).to.exist;
118210
});
119211

120-
rerender(
121-
<ContextMenuProvider>
122-
<TestComponent />
123-
</ContextMenuProvider>
124-
);
212+
it('triggers only the child action when clicking child menu item', function () {
213+
const parentOnAction = sinon.spy();
214+
const childOnAction = sinon.spy();
215+
216+
render(
217+
<ContextMenuProvider>
218+
<ParentComponent onAction={parentOnAction}>
219+
<ChildComponent onAction={childOnAction} />
220+
</ParentComponent>
221+
</ContextMenuProvider>
222+
);
223+
224+
const childTrigger = screen.getByTestId('child-trigger');
225+
userEvent.click(childTrigger, { button: 2 });
226+
227+
const childItem1 = screen.getByTestId('menu-item-Child Item 1');
228+
userEvent.click(childItem1);
125229

126-
expect(removeEventListenerSpy).to.have.been.calledWith('contextmenu');
127-
expect(addEventListenerSpy).to.have.been.calledWith('contextmenu');
230+
expect(childOnAction).to.have.been.calledOnceWithExactly(1);
231+
expect(parentOnAction).to.not.have.been.called;
232+
expect(() => screen.getByTestId('test-menu')).to.throw;
233+
});
234+
235+
it('triggers only the parent action when clicking a parent menu item from child context', function () {
236+
const parentOnAction = sinon.spy();
237+
const childOnAction = sinon.spy();
238+
239+
render(
240+
<ContextMenuProvider>
241+
<ParentComponent onAction={parentOnAction}>
242+
<ChildComponent onAction={childOnAction} />
243+
</ParentComponent>
244+
</ContextMenuProvider>
245+
);
246+
247+
const childTrigger = screen.getByTestId('child-trigger');
248+
userEvent.click(childTrigger, { button: 2 });
249+
250+
const parentItem1 = screen.getByTestId('menu-item-Parent Item 1');
251+
userEvent.click(parentItem1);
252+
253+
expect(parentOnAction).to.have.been.calledOnceWithExactly(1);
254+
expect(childOnAction).to.not.have.been.called;
255+
expect(() => screen.getByTestId('test-menu')).to.throw;
256+
});
128257
});
129258
});
130259
});

0 commit comments

Comments
 (0)