@object-ui/plugin-calendar 2.0.0 → 3.0.0

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.
@@ -0,0 +1,377 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * P3.3 Plugin View Robustness - Calendar View States
11
+ *
12
+ * Tests empty, populated, and edge-case states for CalendarView component.
13
+ */
14
+
15
+ import { describe, it, expect, vi } from 'vitest';
16
+ import { render, screen, fireEvent } from '@testing-library/react';
17
+ import '@testing-library/jest-dom';
18
+ import React from 'react';
19
+ import { CalendarView, type CalendarEvent } from '../CalendarView';
20
+
21
+ // Mock ResizeObserver
22
+ class ResizeObserver {
23
+ observe() {}
24
+ unobserve() {}
25
+ disconnect() {}
26
+ }
27
+ global.ResizeObserver = ResizeObserver;
28
+
29
+ // Mock PointerEvents for Radix
30
+ if (!global.PointerEvent) {
31
+ class PointerEvent extends Event {
32
+ button: number;
33
+ ctrlKey: boolean;
34
+ metaKey: boolean;
35
+ shiftKey: boolean;
36
+ constructor(type: string, props: any = {}) {
37
+ super(type, props);
38
+ this.button = props.button || 0;
39
+ this.ctrlKey = props.ctrlKey || false;
40
+ this.metaKey = props.metaKey || false;
41
+ this.shiftKey = props.shiftKey || false;
42
+ }
43
+ }
44
+ // @ts-expect-error Mocking global PointerEvent
45
+ global.PointerEvent = PointerEvent as any;
46
+ }
47
+
48
+ // Mock HTMLElement.offsetParent for Radix Popper
49
+ Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
50
+ get() {
51
+ return this.parentElement;
52
+ },
53
+ });
54
+
55
+ const defaultDate = new Date(2024, 0, 15); // Jan 15, 2024
56
+
57
+ describe('P3.3 Calendar View States', () => {
58
+ // ---------------------------------------------------------------
59
+ // Empty state (no events)
60
+ // ---------------------------------------------------------------
61
+ describe('empty state', () => {
62
+ it('renders header with no events', () => {
63
+ render(<CalendarView currentDate={defaultDate} events={[]} locale="en-US" />);
64
+ expect(screen.getByText('January 2024')).toBeInTheDocument();
65
+ });
66
+
67
+ it('renders navigation controls with no events', () => {
68
+ render(<CalendarView currentDate={defaultDate} events={[]} locale="en-US" />);
69
+ expect(screen.getByText('Today')).toBeInTheDocument();
70
+ });
71
+
72
+ it('renders day headers in month view with no events', () => {
73
+ render(<CalendarView currentDate={defaultDate} events={[]} locale="en-US" view="month" />);
74
+ expect(screen.getByText('Sun')).toBeInTheDocument();
75
+ expect(screen.getByText('Mon')).toBeInTheDocument();
76
+ expect(screen.getByText('Sat')).toBeInTheDocument();
77
+ });
78
+
79
+ it('renders with undefined events prop', () => {
80
+ render(<CalendarView currentDate={defaultDate} locale="en-US" />);
81
+ expect(screen.getByText('January 2024')).toBeInTheDocument();
82
+ });
83
+ });
84
+
85
+ // ---------------------------------------------------------------
86
+ // Populated state
87
+ // ---------------------------------------------------------------
88
+ describe('populated state', () => {
89
+ const events: CalendarEvent[] = [
90
+ {
91
+ id: '1',
92
+ title: 'Team Meeting',
93
+ start: new Date(2024, 0, 15, 10, 0),
94
+ end: new Date(2024, 0, 15, 11, 0),
95
+ },
96
+ {
97
+ id: '2',
98
+ title: 'Lunch Break',
99
+ start: new Date(2024, 0, 15, 12, 0),
100
+ end: new Date(2024, 0, 15, 13, 0),
101
+ },
102
+ ];
103
+
104
+ it('renders events in month view', () => {
105
+ render(
106
+ <CalendarView
107
+ currentDate={defaultDate}
108
+ events={events}
109
+ view="month"
110
+ locale="en-US"
111
+ />
112
+ );
113
+ expect(screen.getByText('Team Meeting')).toBeInTheDocument();
114
+ expect(screen.getByText('Lunch Break')).toBeInTheDocument();
115
+ });
116
+
117
+ it('fires onEventClick when event is clicked', () => {
118
+ const onClick = vi.fn();
119
+ render(
120
+ <CalendarView
121
+ currentDate={defaultDate}
122
+ events={events}
123
+ view="month"
124
+ locale="en-US"
125
+ onEventClick={onClick}
126
+ />
127
+ );
128
+ fireEvent.click(screen.getByText('Team Meeting'));
129
+ expect(onClick).toHaveBeenCalledTimes(1);
130
+ expect(onClick).toHaveBeenCalledWith(events[0]);
131
+ });
132
+ });
133
+
134
+ // ---------------------------------------------------------------
135
+ // Navigation
136
+ // ---------------------------------------------------------------
137
+ describe('navigation', () => {
138
+ it('navigates to previous month', () => {
139
+ const onNavigate = vi.fn();
140
+ render(
141
+ <CalendarView
142
+ currentDate={defaultDate}
143
+ events={[]}
144
+ locale="en-US"
145
+ onNavigate={onNavigate}
146
+ />
147
+ );
148
+ // Click prev button (first icon button after Today)
149
+ const buttons = screen.getAllByRole('button');
150
+ // Find the prev chevron - it's the button after Today
151
+ const todayIdx = buttons.findIndex(b => b.textContent === 'Today');
152
+ fireEvent.click(buttons[todayIdx + 1]);
153
+ expect(onNavigate).toHaveBeenCalledTimes(1);
154
+ });
155
+
156
+ it('navigates to today', () => {
157
+ const onNavigate = vi.fn();
158
+ render(
159
+ <CalendarView
160
+ currentDate={new Date(2024, 5, 15)}
161
+ events={[]}
162
+ locale="en-US"
163
+ onNavigate={onNavigate}
164
+ />
165
+ );
166
+ fireEvent.click(screen.getByText('Today'));
167
+ expect(onNavigate).toHaveBeenCalledTimes(1);
168
+ });
169
+ });
170
+
171
+ // ---------------------------------------------------------------
172
+ // View modes
173
+ // ---------------------------------------------------------------
174
+ describe('view modes', () => {
175
+ it('renders month view by default', () => {
176
+ render(<CalendarView currentDate={defaultDate} events={[]} locale="en-US" />);
177
+ // Month view has day-of-week headers
178
+ expect(screen.getByText('Sun')).toBeInTheDocument();
179
+ expect(screen.getByText('Mon')).toBeInTheDocument();
180
+ });
181
+
182
+ it('renders week view', () => {
183
+ render(
184
+ <CalendarView
185
+ currentDate={defaultDate}
186
+ events={[]}
187
+ view="week"
188
+ locale="en-US"
189
+ />
190
+ );
191
+ // Week view should show a range header and a 7-column grid
192
+ const grids = screen.getAllByRole('grid');
193
+ expect(grids.length).toBeGreaterThanOrEqual(1);
194
+ });
195
+
196
+ it('renders day view', () => {
197
+ render(
198
+ <CalendarView
199
+ currentDate={defaultDate}
200
+ events={[]}
201
+ view="day"
202
+ locale="en-US"
203
+ />
204
+ );
205
+ // Day view shows time slots
206
+ expect(screen.getByText('12 AM')).toBeInTheDocument();
207
+ expect(screen.getByText('12 PM')).toBeInTheDocument();
208
+ });
209
+ });
210
+
211
+ // ---------------------------------------------------------------
212
+ // Edge cases
213
+ // ---------------------------------------------------------------
214
+ describe('edge cases', () => {
215
+ it('handles event spanning multiple days', () => {
216
+ const multiDayEvent: CalendarEvent[] = [
217
+ {
218
+ id: 'multi',
219
+ title: 'Conference',
220
+ start: new Date(2024, 0, 15),
221
+ end: new Date(2024, 0, 17),
222
+ allDay: true,
223
+ },
224
+ ];
225
+ render(
226
+ <CalendarView
227
+ currentDate={defaultDate}
228
+ events={multiDayEvent}
229
+ view="month"
230
+ locale="en-US"
231
+ />
232
+ );
233
+ // Multi-day events render on each day they span
234
+ const elements = screen.getAllByText('Conference');
235
+ expect(elements.length).toBeGreaterThanOrEqual(1);
236
+ });
237
+
238
+ it('handles event with color', () => {
239
+ const coloredEvents: CalendarEvent[] = [
240
+ {
241
+ id: 'red',
242
+ title: 'Red Event',
243
+ start: new Date(2024, 0, 15, 9, 0),
244
+ color: 'bg-red-500 text-white',
245
+ },
246
+ ];
247
+ render(
248
+ <CalendarView
249
+ currentDate={defaultDate}
250
+ events={coloredEvents}
251
+ view="month"
252
+ locale="en-US"
253
+ />
254
+ );
255
+ expect(screen.getByText('Red Event')).toBeInTheDocument();
256
+ });
257
+
258
+ it('renders onAddClick button when provided', () => {
259
+ const onAdd = vi.fn();
260
+ render(
261
+ <CalendarView
262
+ currentDate={defaultDate}
263
+ events={[]}
264
+ locale="en-US"
265
+ onAddClick={onAdd}
266
+ />
267
+ );
268
+ // The "New" button should be visible
269
+ const newButton = screen.getByText('New');
270
+ expect(newButton).toBeInTheDocument();
271
+ fireEvent.click(newButton);
272
+ expect(onAdd).toHaveBeenCalledTimes(1);
273
+ });
274
+
275
+ it('does not render New button without onAddClick', () => {
276
+ render(
277
+ <CalendarView
278
+ currentDate={defaultDate}
279
+ events={[]}
280
+ locale="en-US"
281
+ />
282
+ );
283
+ expect(screen.queryByText('New')).not.toBeInTheDocument();
284
+ });
285
+
286
+ it('accepts className prop', () => {
287
+ const { container } = render(
288
+ <CalendarView
289
+ currentDate={defaultDate}
290
+ events={[]}
291
+ locale="en-US"
292
+ className="my-calendar"
293
+ />
294
+ );
295
+ expect(container.firstElementChild!.className).toContain('my-calendar');
296
+ });
297
+
298
+ it('handles many events without crashing', () => {
299
+ const manyEvents: CalendarEvent[] = Array.from({ length: 100 }, (_, i) => ({
300
+ id: String(i),
301
+ title: `Event ${i}`,
302
+ start: new Date(2024, 0, (i % 28) + 1, 9, 0),
303
+ }));
304
+ const { container } = render(
305
+ <CalendarView
306
+ currentDate={defaultDate}
307
+ events={manyEvents}
308
+ view="month"
309
+ locale="en-US"
310
+ />
311
+ );
312
+ expect(container).toBeInTheDocument();
313
+ });
314
+
315
+ it('handles date at month boundary (last day of month)', () => {
316
+ const boundaryDate = new Date(2024, 0, 31); // Jan 31, 2024
317
+ render(
318
+ <CalendarView
319
+ currentDate={boundaryDate}
320
+ events={[]}
321
+ view="month"
322
+ locale="en-US"
323
+ />
324
+ );
325
+ expect(screen.getByText('January 2024')).toBeInTheDocument();
326
+ });
327
+
328
+ it('handles date at month boundary (first day of month)', () => {
329
+ const firstDay = new Date(2024, 1, 1); // Feb 1, 2024
330
+ render(
331
+ <CalendarView
332
+ currentDate={firstDay}
333
+ events={[]}
334
+ view="month"
335
+ locale="en-US"
336
+ />
337
+ );
338
+ expect(screen.getByText('February 2024')).toBeInTheDocument();
339
+ });
340
+
341
+ it('handles leap year date', () => {
342
+ const leapDate = new Date(2024, 1, 29); // Feb 29, 2024 (leap year)
343
+ render(
344
+ <CalendarView
345
+ currentDate={leapDate}
346
+ events={[]}
347
+ view="month"
348
+ locale="en-US"
349
+ />
350
+ );
351
+ expect(screen.getByText('February 2024')).toBeInTheDocument();
352
+ });
353
+
354
+ it('handles events spanning across month boundary', () => {
355
+ const crossMonthEvent: CalendarEvent[] = [
356
+ {
357
+ id: 'cross',
358
+ title: 'Cross-Month Event',
359
+ start: new Date(2024, 0, 30),
360
+ end: new Date(2024, 1, 2),
361
+ allDay: true,
362
+ },
363
+ ];
364
+ const { container } = render(
365
+ <CalendarView
366
+ currentDate={new Date(2024, 0, 15)}
367
+ events={crossMonthEvent}
368
+ view="month"
369
+ locale="en-US"
370
+ />
371
+ );
372
+ // Should render without crashing; the event spans into next month padding days
373
+ const elements = container.querySelectorAll('[role="button"]');
374
+ expect(elements.length).toBeGreaterThanOrEqual(1);
375
+ });
376
+ });
377
+ });