@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.
- package/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +22 -0
- package/dist/index.js +633 -488
- package/dist/index.umd.cjs +2 -2
- package/dist/src/CalendarView.d.ts +2 -1
- package/dist/src/CalendarView.d.ts.map +1 -1
- package/dist/src/ObjectCalendar.d.ts +1 -0
- package/dist/src/ObjectCalendar.d.ts.map +1 -1
- package/dist/src/ObjectCalendar.stories.d.ts +23 -0
- package/dist/src/ObjectCalendar.stories.d.ts.map +1 -0
- package/package.json +11 -10
- package/src/CalendarView.tsx +178 -18
- package/src/ObjectCalendar.stories.tsx +82 -0
- package/src/ObjectCalendar.tsx +54 -4
- package/src/__tests__/accessibility.test.tsx +290 -0
- package/src/__tests__/performance-benchmark.test.tsx +227 -0
- package/src/__tests__/view-states.test.tsx +377 -0
|
@@ -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
|
+
});
|