@violice/rmu 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -36
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.mts +13 -28
- package/dist/index.mjs +2 -204
- package/dist/index.mjs.map +1 -0
- package/package.json +27 -19
- package/src/constants.ts +8 -0
- package/src/emitter.test.ts +85 -0
- package/src/emitter.ts +24 -16
- package/src/events.test.ts +59 -0
- package/src/events.ts +14 -13
- package/src/integration.test.tsx +266 -0
- package/src/rmu-outlet.test.tsx +213 -0
- package/src/rmu-outlet.tsx +3 -2
- package/src/rmu-provider.test.tsx +15 -0
- package/src/types.ts +20 -16
- package/src/use-rmu-events.test.ts +117 -0
- package/src/use-rmu-events.ts +5 -5
- package/src/use-rmu-state.test.ts +129 -0
- package/dist/index.d.ts +0 -49
- package/dist/index.js +0 -231
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, waitFor, act, cleanup } from '@testing-library/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { RMUProvider } from './rmu-provider';
|
|
5
|
+
import { RMUOutlet } from './rmu-outlet';
|
|
6
|
+
import { openModal, closeModal } from './events';
|
|
7
|
+
import { emitter } from './emitter';
|
|
8
|
+
|
|
9
|
+
describe('Integration: Full RMU Workflow', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Clear all emitter subscriptions before each test
|
|
12
|
+
emitter._clear();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
// Clean up the DOM after each test
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should handle complete workflow with single outlet', async () => {
|
|
21
|
+
render(
|
|
22
|
+
<RMUProvider>
|
|
23
|
+
<div data-testid="app">App Content</div>
|
|
24
|
+
<RMUOutlet />
|
|
25
|
+
</RMUProvider>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Wait for outlet to be registered
|
|
29
|
+
await waitFor(() => {
|
|
30
|
+
expect(document.body).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Initial state - app content visible, no modals
|
|
34
|
+
expect(screen.getByTestId('app')).toBeDefined();
|
|
35
|
+
expect(screen.queryByTestId('modal-1')).toBeNull();
|
|
36
|
+
|
|
37
|
+
// Open first modal
|
|
38
|
+
let modal1Info: { modalId: string; outletId: string };
|
|
39
|
+
act(() => {
|
|
40
|
+
modal1Info = openModal(<div data-testid="modal-1">First Modal</div>);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.getByTestId('modal-1')).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Open second modal
|
|
48
|
+
let modal2Info: { modalId: string; outletId: string };
|
|
49
|
+
act(() => {
|
|
50
|
+
modal2Info = openModal(<div data-testid="modal-2">Second Modal</div>);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await waitFor(() => {
|
|
54
|
+
expect(screen.getByTestId('modal-1')).toBeDefined();
|
|
55
|
+
expect(screen.getByTestId('modal-2')).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Close first modal
|
|
59
|
+
act(() => {
|
|
60
|
+
closeModal(modal1Info);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await waitFor(() => {
|
|
64
|
+
expect(screen.queryByTestId('modal-1')).toBeNull();
|
|
65
|
+
expect(screen.getByTestId('modal-2')).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Close second modal
|
|
69
|
+
act(() => {
|
|
70
|
+
closeModal(modal2Info);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
expect(screen.queryByTestId('modal-1')).toBeNull();
|
|
75
|
+
expect(screen.queryByTestId('modal-2')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// App content still visible
|
|
79
|
+
expect(screen.getByTestId('app')).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle workflow with multiple outlets', async () => {
|
|
83
|
+
render(
|
|
84
|
+
<RMUProvider>
|
|
85
|
+
<div data-testid="sidebar">
|
|
86
|
+
<RMUOutlet outletId="sidebar-outlet" />
|
|
87
|
+
</div>
|
|
88
|
+
<div data-testid="main">
|
|
89
|
+
<RMUOutlet outletId="main-outlet" />
|
|
90
|
+
</div>
|
|
91
|
+
</RMUProvider>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Wait for outlets to be registered
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(document.body).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Open modals in different outlets
|
|
100
|
+
let sidebarModalInfo: { modalId: string; outletId: string };
|
|
101
|
+
let mainModalInfo: { modalId: string; outletId: string };
|
|
102
|
+
|
|
103
|
+
act(() => {
|
|
104
|
+
sidebarModalInfo = openModal(
|
|
105
|
+
<div data-testid="sidebar-modal">Sidebar Modal</div>,
|
|
106
|
+
{ outletId: 'sidebar-outlet' }
|
|
107
|
+
);
|
|
108
|
+
mainModalInfo = openModal(
|
|
109
|
+
<div data-testid="main-modal">Main Modal</div>,
|
|
110
|
+
{ outletId: 'main-outlet' }
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(screen.getByTestId('sidebar-modal')).toBeDefined();
|
|
116
|
+
expect(screen.getByTestId('main-modal')).toBeDefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Verify modals are in correct outlets
|
|
120
|
+
expect(screen.getByTestId('sidebar').contains(screen.getByTestId('sidebar-modal'))).toBe(true);
|
|
121
|
+
expect(screen.getByTestId('main').contains(screen.getByTestId('main-modal'))).toBe(true);
|
|
122
|
+
|
|
123
|
+
// Close sidebar modal only
|
|
124
|
+
act(() => {
|
|
125
|
+
closeModal(sidebarModalInfo);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(screen.queryByTestId('sidebar-modal')).toBeNull();
|
|
130
|
+
expect(screen.getByTestId('main-modal')).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Close main modal
|
|
134
|
+
act(() => {
|
|
135
|
+
closeModal(mainModalInfo);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(screen.queryByTestId('sidebar-modal')).toBeNull();
|
|
140
|
+
expect(screen.queryByTestId('main-modal')).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle rapid open/close operations', async () => {
|
|
145
|
+
render(
|
|
146
|
+
<RMUProvider>
|
|
147
|
+
<RMUOutlet />
|
|
148
|
+
</RMUProvider>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Wait for outlet to be registered
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(document.body).toBeDefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const modals: Array<{ modalId: string; outletId: string }> = [];
|
|
157
|
+
|
|
158
|
+
// Open 5 modals sequentially (with small delays to avoid ID collision)
|
|
159
|
+
for (let i = 0; i < 5; i++) {
|
|
160
|
+
act(() => {
|
|
161
|
+
modals.push(
|
|
162
|
+
openModal(<div data-testid={`rapid-modal-${i}`}>Modal {i}</div>)
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
// Small delay to ensure unique IDs
|
|
166
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
for (let i = 0; i < 5; i++) {
|
|
171
|
+
expect(screen.getByTestId(`rapid-modal-${i}`)).toBeDefined();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Close all modals
|
|
176
|
+
act(() => {
|
|
177
|
+
modals.forEach(modalInfo => closeModal(modalInfo));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
for (let i = 0; i < 5; i++) {
|
|
182
|
+
expect(screen.queryByTestId(`rapid-modal-${i}`)).toBeNull();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle modal with complex React components', async () => {
|
|
188
|
+
const ComplexComponent = ({ title, onAction }: { title: string; onAction?: () => void }) => (
|
|
189
|
+
<div data-testid="complex-modal">
|
|
190
|
+
<h1>{title}</h1>
|
|
191
|
+
<button data-testid="action-btn" onClick={onAction}>
|
|
192
|
+
Action
|
|
193
|
+
</button>
|
|
194
|
+
<ul>
|
|
195
|
+
<li>Item 1</li>
|
|
196
|
+
<li>Item 2</li>
|
|
197
|
+
</ul>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
render(
|
|
202
|
+
<RMUProvider>
|
|
203
|
+
<RMUOutlet />
|
|
204
|
+
</RMUProvider>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Wait for outlet to be registered
|
|
208
|
+
await waitFor(() => {
|
|
209
|
+
expect(document.body).toBeDefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const actionHandler = vi.fn();
|
|
213
|
+
|
|
214
|
+
act(() => {
|
|
215
|
+
openModal(<ComplexComponent title="Test Modal" onAction={actionHandler} />);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await waitFor(() => {
|
|
219
|
+
expect(screen.getByTestId('complex-modal')).toBeDefined();
|
|
220
|
+
expect(screen.getByText('Test Modal')).toBeDefined();
|
|
221
|
+
expect(screen.getByTestId('action-btn')).toBeDefined();
|
|
222
|
+
expect(screen.getByText('Item 1')).toBeDefined();
|
|
223
|
+
expect(screen.getByText('Item 2')).toBeDefined();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle outlet removal and re-addition', async () => {
|
|
228
|
+
const { unmount } = render(
|
|
229
|
+
<RMUProvider>
|
|
230
|
+
<RMUOutlet outletId="dynamic-outlet" />
|
|
231
|
+
</RMUProvider>
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Wait for outlet to be registered
|
|
235
|
+
await waitFor(() => {
|
|
236
|
+
expect(document.body).toBeDefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Open a modal
|
|
240
|
+
let modalInfo: { modalId: string; outletId: string };
|
|
241
|
+
act(() => {
|
|
242
|
+
modalInfo = openModal(
|
|
243
|
+
<div data-testid="dynamic-modal">Dynamic Modal</div>,
|
|
244
|
+
{ outletId: 'dynamic-outlet' }
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(screen.getByTestId('dynamic-modal')).toBeDefined();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Unmount the outlet
|
|
253
|
+
unmount();
|
|
254
|
+
|
|
255
|
+
// Re-render with a new outlet (need to use render again after unmount)
|
|
256
|
+
render(
|
|
257
|
+
<RMUProvider>
|
|
258
|
+
<RMUOutlet outletId="dynamic-outlet" />
|
|
259
|
+
</RMUProvider>
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Modal should not be visible after outlet removal and re-addition
|
|
263
|
+
// because the outlet was removed from state
|
|
264
|
+
expect(screen.queryByTestId('dynamic-modal')).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, waitFor, act, cleanup } from '@testing-library/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { RMUProvider } from './rmu-provider';
|
|
5
|
+
import { RMUOutlet } from './rmu-outlet';
|
|
6
|
+
import { openModal, closeModal } from './events';
|
|
7
|
+
import { emitter } from './emitter';
|
|
8
|
+
|
|
9
|
+
describe('RMUOutlet', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Clear all emitter subscriptions before each test
|
|
12
|
+
emitter._clear();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
// Clean up the DOM after each test
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should throw when used outside RMUProvider', () => {
|
|
21
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
22
|
+
|
|
23
|
+
expect(() => {
|
|
24
|
+
render(<RMUOutlet />);
|
|
25
|
+
}).toThrow('RMUProvider not found in component tree');
|
|
26
|
+
|
|
27
|
+
consoleSpy.mockRestore();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should render with default outlet', () => {
|
|
31
|
+
render(
|
|
32
|
+
<RMUProvider>
|
|
33
|
+
<div data-testid="content">content</div>
|
|
34
|
+
<RMUOutlet />
|
|
35
|
+
</RMUProvider>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(screen.getByTestId('content')).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should render with custom outlet id', () => {
|
|
42
|
+
render(
|
|
43
|
+
<RMUProvider>
|
|
44
|
+
<RMUOutlet outletId="custom-outlet" />
|
|
45
|
+
</RMUProvider>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// The outlet should be rendered without errors
|
|
49
|
+
expect(document.body).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should render multiple outlets', () => {
|
|
53
|
+
render(
|
|
54
|
+
<RMUProvider>
|
|
55
|
+
<RMUOutlet outletId="outlet-1" />
|
|
56
|
+
<RMUOutlet outletId="outlet-2" />
|
|
57
|
+
</RMUProvider>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Both outlets should be rendered without errors
|
|
61
|
+
expect(document.body).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should render modal when opened via events', async () => {
|
|
65
|
+
render(
|
|
66
|
+
<RMUProvider>
|
|
67
|
+
<RMUOutlet />
|
|
68
|
+
</RMUProvider>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Wait for outlet to be registered via useEffect
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(document.body).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const modalContent = <div data-testid="modal-content">Modal Content</div>;
|
|
77
|
+
|
|
78
|
+
act(() => {
|
|
79
|
+
openModal(modalContent);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await waitFor(() => {
|
|
83
|
+
expect(screen.getByTestId('modal-content')).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should render modal in custom outlet', async () => {
|
|
88
|
+
render(
|
|
89
|
+
<RMUProvider>
|
|
90
|
+
<RMUOutlet outletId="custom-outlet" />
|
|
91
|
+
</RMUProvider>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Wait for outlet to be registered via useEffect
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(document.body).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const modalContent = <div data-testid="custom-modal">Custom Modal</div>;
|
|
100
|
+
|
|
101
|
+
act(() => {
|
|
102
|
+
openModal(modalContent, { outletId: 'custom-outlet' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(screen.getByTestId('custom-modal')).toBeDefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should not render modal in wrong outlet', async () => {
|
|
111
|
+
render(
|
|
112
|
+
<RMUProvider>
|
|
113
|
+
<RMUOutlet outletId="outlet-1" />
|
|
114
|
+
</RMUProvider>
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Wait for outlet to be registered via useEffect
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(document.body).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const modalContent = <div data-testid="wrong-modal">Wrong Modal</div>;
|
|
123
|
+
|
|
124
|
+
// Opening modal in non-existent outlet should throw
|
|
125
|
+
expect(() => {
|
|
126
|
+
act(() => {
|
|
127
|
+
openModal(modalContent, { outletId: 'outlet-2' });
|
|
128
|
+
});
|
|
129
|
+
}).toThrow('Outlet with id outlet-2 not found');
|
|
130
|
+
|
|
131
|
+
// Modal should not be rendered in outlet-1
|
|
132
|
+
expect(screen.queryByTestId('wrong-modal')).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should remove modal when closed', async () => {
|
|
136
|
+
render(
|
|
137
|
+
<RMUProvider>
|
|
138
|
+
<RMUOutlet />
|
|
139
|
+
</RMUProvider>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Wait for outlet to be registered via useEffect
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(document.body).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const modalContent = <div data-testid="closable-modal">Closable Modal</div>;
|
|
148
|
+
|
|
149
|
+
let modalInfo: { modalId: string; outletId: string };
|
|
150
|
+
|
|
151
|
+
act(() => {
|
|
152
|
+
modalInfo = openModal(modalContent);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(screen.getByTestId('closable-modal')).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
act(() => {
|
|
160
|
+
closeModal(modalInfo);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
expect(screen.queryByTestId('closable-modal')).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should render multiple modals in same outlet', async () => {
|
|
169
|
+
render(
|
|
170
|
+
<RMUProvider>
|
|
171
|
+
<RMUOutlet />
|
|
172
|
+
</RMUProvider>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Wait for outlet to be registered via useEffect
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(document.body).toBeDefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Open first modal
|
|
181
|
+
act(() => {
|
|
182
|
+
openModal(<div data-testid="modal-1">Modal 1</div>);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await waitFor(() => {
|
|
186
|
+
expect(screen.getByTestId('modal-1')).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Open second modal after first one is rendered
|
|
190
|
+
act(() => {
|
|
191
|
+
openModal(<div data-testid="modal-2">Modal 2</div>);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await waitFor(() => {
|
|
195
|
+
expect(screen.getByTestId('modal-1')).toBeDefined();
|
|
196
|
+
expect(screen.getByTestId('modal-2')).toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should cleanup outlet on unmount', () => {
|
|
201
|
+
const { unmount } = render(
|
|
202
|
+
<RMUProvider>
|
|
203
|
+
<RMUOutlet outletId="cleanup-outlet" />
|
|
204
|
+
</RMUProvider>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Unmount the outlet
|
|
208
|
+
unmount();
|
|
209
|
+
|
|
210
|
+
// The component should unmount without errors
|
|
211
|
+
expect(document.body).toBeDefined();
|
|
212
|
+
});
|
|
213
|
+
});
|
package/src/rmu-outlet.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { Fragment, useContext, useEffect } from 'react';
|
|
2
2
|
import { RMUContext } from './rmu-context';
|
|
3
|
+
import { RMU_DEFAULT_OUTLET_ID } from './constants';
|
|
3
4
|
|
|
4
|
-
export const RMUOutlet = ({ outletId =
|
|
5
|
+
export const RMUOutlet = ({ outletId = RMU_DEFAULT_OUTLET_ID }) => {
|
|
5
6
|
const ctx = useContext(RMUContext);
|
|
6
7
|
|
|
7
8
|
if (!ctx) {
|
|
@@ -15,7 +16,7 @@ export const RMUOutlet = ({ outletId = 'rmu-default-outlet' }) => {
|
|
|
15
16
|
return () => {
|
|
16
17
|
removeOutlet(outletId);
|
|
17
18
|
};
|
|
18
|
-
}, []);
|
|
19
|
+
}, []);
|
|
19
20
|
|
|
20
21
|
const modals = outlets[outletId] ?? {};
|
|
21
22
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { RMUProvider } from './rmu-provider';
|
|
4
|
+
|
|
5
|
+
describe('RMUProvider', () => {
|
|
6
|
+
it('should render children', () => {
|
|
7
|
+
render(
|
|
8
|
+
<RMUProvider>
|
|
9
|
+
<div data-testid="child">Child Content</div>
|
|
10
|
+
</RMUProvider>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(screen.getByTestId('child')).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
export enum RMUEventType {
|
|
4
|
+
OpenModal = 'RMU:OPEN_MODAL',
|
|
5
|
+
CloseModal = 'RMU:CLOSE_MODAL',
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export type RMUContextState = {
|
|
4
9
|
outlets: Record<string, Record<string, ReactNode>>;
|
|
5
|
-
openModal: (
|
|
6
|
-
|
|
7
|
-
modalComponent,
|
|
8
|
-
outletId,
|
|
9
|
-
}: {
|
|
10
|
-
modalId: string;
|
|
11
|
-
modalComponent: ReactNode;
|
|
12
|
-
outletId: string;
|
|
13
|
-
}) => void;
|
|
14
|
-
closeModal: ({
|
|
15
|
-
modalId,
|
|
16
|
-
outletId,
|
|
17
|
-
}: {
|
|
18
|
-
modalId: string;
|
|
19
|
-
outletId: string;
|
|
20
|
-
}) => void;
|
|
10
|
+
openModal: (payload: OpenModalPayload) => void;
|
|
11
|
+
closeModal: (payload: CloseModalPayload) => void;
|
|
21
12
|
addOutlet: (outletId: string) => void;
|
|
22
13
|
removeOutlet: (outletId: string) => void;
|
|
23
14
|
};
|
|
15
|
+
|
|
16
|
+
export type OpenModalPayload = {
|
|
17
|
+
modalId: string;
|
|
18
|
+
modalComponent: ReactNode;
|
|
19
|
+
outletId: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CloseModalPayload = {
|
|
23
|
+
modalId: string;
|
|
24
|
+
outletId: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type RMUEventPayload = OpenModalPayload | CloseModalPayload;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { useRMUEvents } from './use-rmu-events';
|
|
4
|
+
import { emitter } from './emitter';
|
|
5
|
+
import { RMU_EVENTS } from './constants';
|
|
6
|
+
import { RMUContextState } from './types';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
describe('useRMUEvents', () => {
|
|
10
|
+
const mockOpenModal = vi.fn();
|
|
11
|
+
const mockCloseModal = vi.fn();
|
|
12
|
+
const mockAddOutlet = vi.fn();
|
|
13
|
+
const mockRemoveOutlet = vi.fn();
|
|
14
|
+
|
|
15
|
+
const mockCtx: RMUContextState = {
|
|
16
|
+
outlets: {},
|
|
17
|
+
openModal: mockOpenModal,
|
|
18
|
+
closeModal: mockCloseModal,
|
|
19
|
+
addOutlet: mockAddOutlet,
|
|
20
|
+
removeOutlet: mockRemoveOutlet,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
// Clear all emitter subscriptions before each test
|
|
26
|
+
emitter._clear();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should subscribe to open and close events on mount', () => {
|
|
30
|
+
renderHook(() => useRMUEvents(mockCtx));
|
|
31
|
+
|
|
32
|
+
// Emit open event and verify it calls openModal
|
|
33
|
+
const mockComponent = React.createElement('div', null, 'Test');
|
|
34
|
+
emitter.emit(RMU_EVENTS.open, {
|
|
35
|
+
modalId: 'test-modal',
|
|
36
|
+
modalComponent: mockComponent,
|
|
37
|
+
outletId: 'test-outlet',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(mockOpenModal).toHaveBeenCalledWith({
|
|
41
|
+
modalId: 'test-modal',
|
|
42
|
+
modalComponent: mockComponent,
|
|
43
|
+
outletId: 'test-outlet',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Emit close event and verify it calls closeModal
|
|
47
|
+
emitter.emit(RMU_EVENTS.close, {
|
|
48
|
+
modalId: 'test-modal',
|
|
49
|
+
outletId: 'test-outlet',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(mockCloseModal).toHaveBeenCalledWith({
|
|
53
|
+
modalId: 'test-modal',
|
|
54
|
+
outletId: 'test-outlet',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should unsubscribe from events on unmount', () => {
|
|
59
|
+
const { unmount } = renderHook(() => useRMUEvents(mockCtx));
|
|
60
|
+
|
|
61
|
+
// Clear mocks after subscription
|
|
62
|
+
vi.clearAllMocks();
|
|
63
|
+
|
|
64
|
+
// Unmount the hook
|
|
65
|
+
unmount();
|
|
66
|
+
|
|
67
|
+
// Emit events after unmount - handlers should not be called
|
|
68
|
+
const mockComponent = React.createElement('div', null, 'Test');
|
|
69
|
+
emitter.emit(RMU_EVENTS.open, {
|
|
70
|
+
modalId: 'test-modal',
|
|
71
|
+
modalComponent: mockComponent,
|
|
72
|
+
outletId: 'test-outlet',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
emitter.emit(RMU_EVENTS.close, {
|
|
76
|
+
modalId: 'test-modal',
|
|
77
|
+
outletId: 'test-outlet',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(mockOpenModal).not.toHaveBeenCalled();
|
|
81
|
+
expect(mockCloseModal).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle multiple open and close events', () => {
|
|
85
|
+
renderHook(() => useRMUEvents(mockCtx));
|
|
86
|
+
|
|
87
|
+
const mockComponent = React.createElement('div', null, 'Test');
|
|
88
|
+
|
|
89
|
+
// Emit multiple open events
|
|
90
|
+
emitter.emit(RMU_EVENTS.open, {
|
|
91
|
+
modalId: 'modal-1',
|
|
92
|
+
modalComponent: mockComponent,
|
|
93
|
+
outletId: 'outlet-1',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
emitter.emit(RMU_EVENTS.open, {
|
|
97
|
+
modalId: 'modal-2',
|
|
98
|
+
modalComponent: mockComponent,
|
|
99
|
+
outletId: 'outlet-1',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(mockOpenModal).toHaveBeenCalledTimes(2);
|
|
103
|
+
|
|
104
|
+
// Emit multiple close events
|
|
105
|
+
emitter.emit(RMU_EVENTS.close, {
|
|
106
|
+
modalId: 'modal-1',
|
|
107
|
+
outletId: 'outlet-1',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
emitter.emit(RMU_EVENTS.close, {
|
|
111
|
+
modalId: 'modal-2',
|
|
112
|
+
outletId: 'outlet-1',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(mockCloseModal).toHaveBeenCalledTimes(2);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/use-rmu-events.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
-
import { RMUContextState } from './types';
|
|
3
|
-
import { RMU_EVENTS } from './
|
|
2
|
+
import { RMUContextState, OpenModalPayload, CloseModalPayload } from './types';
|
|
3
|
+
import { RMU_EVENTS } from './constants';
|
|
4
4
|
import { emitter } from './emitter';
|
|
5
5
|
|
|
6
6
|
export const useRMUEvents = (ctx: RMUContextState) => {
|
|
7
7
|
const events = {
|
|
8
|
-
open: (payload:
|
|
9
|
-
close: (payload:
|
|
8
|
+
open: (payload: OpenModalPayload) => ctx.openModal(payload),
|
|
9
|
+
close: (payload: CloseModalPayload) => ctx.closeModal(payload),
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
useEffect(() => {
|
|
@@ -19,5 +19,5 @@ export const useRMUEvents = (ctx: RMUContextState) => {
|
|
|
19
19
|
emitter.unsubscribe(RMU_EVENTS[event], events[event]);
|
|
20
20
|
});
|
|
21
21
|
};
|
|
22
|
-
}, []);
|
|
22
|
+
}, []);
|
|
23
23
|
};
|