@object-ui/plugin-calendar 3.1.5 → 3.3.1
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/CHANGELOG.md +37 -0
- package/README.md +21 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -17
- package/dist/index.umd.cjs +2 -2
- package/dist/packages/plugin-calendar/src/CalendarView.d.ts.map +1 -0
- package/dist/packages/plugin-calendar/src/ObjectCalendar.d.ts.map +1 -0
- package/dist/packages/plugin-calendar/src/calendar-view-renderer.d.ts.map +1 -0
- package/dist/packages/plugin-calendar/src/index.d.ts.map +1 -0
- package/package.json +37 -14
- package/.turbo/turbo-build.log +0 -22
- package/dist/src/CalendarView.d.ts.map +0 -1
- package/dist/src/ObjectCalendar.d.ts.map +0 -1
- package/dist/src/calendar-view-renderer.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/src/CalendarView.test.tsx +0 -118
- package/src/CalendarView.tsx +0 -821
- package/src/ObjectCalendar.msw.test.tsx +0 -100
- package/src/ObjectCalendar.stories.tsx +0 -82
- package/src/ObjectCalendar.tsx +0 -420
- package/src/__tests__/accessibility.test.tsx +0 -290
- package/src/__tests__/calendar-bugfixes.test.tsx +0 -230
- package/src/__tests__/calendar-optimizations.test.tsx +0 -178
- package/src/__tests__/performance-benchmark.test.tsx +0 -227
- package/src/__tests__/view-states.test.tsx +0 -377
- package/src/calendar-view-renderer.tsx +0 -181
- package/src/index.tsx +0 -50
- package/src/registration.test.tsx +0 -41
- package/test/setup.ts +0 -32
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -56
- package/vitest.config.ts +0 -13
- package/vitest.setup.ts +0 -1
- /package/dist/{src → packages/plugin-calendar/src}/CalendarView.d.ts +0 -0
- /package/dist/{src → packages/plugin-calendar/src}/ObjectCalendar.d.ts +0 -0
- /package/dist/{src → packages/plugin-calendar/src}/calendar-view-renderer.d.ts +0 -0
- /package/dist/{src → packages/plugin-calendar/src}/index.d.ts +0 -0
|
@@ -1,290 +0,0 @@
|
|
|
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
|
-
* Screen reader experience tests for CalendarView.
|
|
11
|
-
*
|
|
12
|
-
* Tests ARIA attributes, roles, landmarks, keyboard navigation,
|
|
13
|
-
* and screen reader announcements for the calendar plugin.
|
|
14
|
-
* Part of P2.3 Accessibility & Inclusive Design roadmap.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
18
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
19
|
-
import '@testing-library/jest-dom';
|
|
20
|
-
import React from 'react';
|
|
21
|
-
import { CalendarView, type CalendarEvent } from '../CalendarView';
|
|
22
|
-
|
|
23
|
-
// Mock ResizeObserver
|
|
24
|
-
class ResizeObserver {
|
|
25
|
-
observe() {}
|
|
26
|
-
unobserve() {}
|
|
27
|
-
disconnect() {}
|
|
28
|
-
}
|
|
29
|
-
global.ResizeObserver = ResizeObserver;
|
|
30
|
-
|
|
31
|
-
// Mock PointerEvents for Radix
|
|
32
|
-
if (!global.PointerEvent) {
|
|
33
|
-
class PointerEvent extends Event {
|
|
34
|
-
button: number;
|
|
35
|
-
ctrlKey: boolean;
|
|
36
|
-
metaKey: boolean;
|
|
37
|
-
shiftKey: boolean;
|
|
38
|
-
constructor(type: string, props: any = {}) {
|
|
39
|
-
super(type, props);
|
|
40
|
-
this.button = props.button || 0;
|
|
41
|
-
this.ctrlKey = props.ctrlKey || false;
|
|
42
|
-
this.metaKey = props.metaKey || false;
|
|
43
|
-
this.shiftKey = props.shiftKey || false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// @ts-expect-error Mocking global PointerEvent
|
|
47
|
-
global.PointerEvent = PointerEvent as any;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Mock HTMLElement.offsetParent for Radix Popper
|
|
51
|
-
Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
|
|
52
|
-
get() {
|
|
53
|
-
return this.parentElement;
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const defaultDate = new Date(2024, 0, 15); // Jan 15, 2024
|
|
58
|
-
|
|
59
|
-
const mockEvents: CalendarEvent[] = [
|
|
60
|
-
{
|
|
61
|
-
id: '1',
|
|
62
|
-
title: 'Team Standup',
|
|
63
|
-
start: new Date(2024, 0, 15, 9, 0),
|
|
64
|
-
end: new Date(2024, 0, 15, 9, 30),
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: '2',
|
|
68
|
-
title: 'Sprint Review',
|
|
69
|
-
start: new Date(2024, 0, 15, 14, 0),
|
|
70
|
-
end: new Date(2024, 0, 15, 15, 0),
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
id: '3',
|
|
74
|
-
title: 'All Day Workshop',
|
|
75
|
-
start: new Date(2024, 0, 16, 0, 0),
|
|
76
|
-
end: new Date(2024, 0, 16, 23, 59),
|
|
77
|
-
allDay: true,
|
|
78
|
-
},
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
describe('CalendarView: Screen Reader & Accessibility', () => {
|
|
82
|
-
describe('header navigation controls', () => {
|
|
83
|
-
it('renders navigation buttons with accessible labels', () => {
|
|
84
|
-
render(<CalendarView currentDate={defaultDate} locale="en-US" />);
|
|
85
|
-
|
|
86
|
-
const buttons = screen.getAllByRole('button');
|
|
87
|
-
// Should have: Today, Prev, Next, Date Picker Trigger, and View Switcher
|
|
88
|
-
expect(buttons.length).toBeGreaterThanOrEqual(4);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('Today button is clearly labeled', () => {
|
|
92
|
-
render(<CalendarView currentDate={defaultDate} locale="en-US" />);
|
|
93
|
-
|
|
94
|
-
const todayButton = screen.getByText('Today');
|
|
95
|
-
expect(todayButton).toBeInTheDocument();
|
|
96
|
-
expect(todayButton.closest('button')).toBeInTheDocument();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('date label is inside an interactive element', () => {
|
|
100
|
-
render(<CalendarView currentDate={defaultDate} locale="en-US" />);
|
|
101
|
-
|
|
102
|
-
const dateLabel = screen.getByText('January 2024');
|
|
103
|
-
expect(dateLabel).toBeInTheDocument();
|
|
104
|
-
|
|
105
|
-
const triggerButton = dateLabel.closest('button');
|
|
106
|
-
expect(triggerButton).toBeInTheDocument();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('previous and next navigation buttons exist', () => {
|
|
110
|
-
render(<CalendarView currentDate={defaultDate} locale="en-US" />);
|
|
111
|
-
|
|
112
|
-
const buttons = screen.getAllByRole('button');
|
|
113
|
-
// At least Today, Prev, Next buttons
|
|
114
|
-
expect(buttons.length).toBeGreaterThanOrEqual(3);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('view switcher accessibility', () => {
|
|
119
|
-
it('view switcher displays current view', () => {
|
|
120
|
-
render(<CalendarView currentDate={defaultDate} view="month" locale="en-US" />);
|
|
121
|
-
|
|
122
|
-
const monthText = screen.getByText('Month');
|
|
123
|
-
expect(monthText).toBeInTheDocument();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('view switcher is inside a button trigger', () => {
|
|
127
|
-
render(<CalendarView currentDate={defaultDate} view="month" locale="en-US" />);
|
|
128
|
-
|
|
129
|
-
const monthText = screen.getByText('Month');
|
|
130
|
-
const trigger = monthText.closest('button');
|
|
131
|
-
expect(trigger).toBeInTheDocument();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('week view renders without error when locale is available', () => {
|
|
135
|
-
// Note: WeekView has a known locale scoping issue in CalendarView.tsx.
|
|
136
|
-
// This test verifies the month view switcher text is present before switching.
|
|
137
|
-
render(<CalendarView currentDate={defaultDate} view="month" locale="en-US" />);
|
|
138
|
-
|
|
139
|
-
const monthText = screen.getByText('Month');
|
|
140
|
-
expect(monthText).toBeInTheDocument();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('day view renders accessible buttons', () => {
|
|
144
|
-
// Note: DayView has a known locale scoping issue in CalendarView.tsx.
|
|
145
|
-
// Testing button accessibility with the default month view instead.
|
|
146
|
-
render(<CalendarView currentDate={defaultDate} view="month" locale="en-US" />);
|
|
147
|
-
|
|
148
|
-
const buttons = screen.getAllByRole('button');
|
|
149
|
-
expect(buttons.length).toBeGreaterThanOrEqual(3);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe('calendar grid structure', () => {
|
|
154
|
-
it('month view renders day-of-week headers', () => {
|
|
155
|
-
render(<CalendarView currentDate={defaultDate} view="month" locale="en-US" />);
|
|
156
|
-
|
|
157
|
-
// Day headers (Sun, Mon, Tue, etc.)
|
|
158
|
-
expect(screen.getByText('Sun')).toBeInTheDocument();
|
|
159
|
-
expect(screen.getByText('Mon')).toBeInTheDocument();
|
|
160
|
-
expect(screen.getByText('Tue')).toBeInTheDocument();
|
|
161
|
-
expect(screen.getByText('Wed')).toBeInTheDocument();
|
|
162
|
-
expect(screen.getByText('Thu')).toBeInTheDocument();
|
|
163
|
-
expect(screen.getByText('Fri')).toBeInTheDocument();
|
|
164
|
-
expect(screen.getByText('Sat')).toBeInTheDocument();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('month view renders day numbers', () => {
|
|
168
|
-
render(<CalendarView currentDate={defaultDate} view="month" locale="en-US" />);
|
|
169
|
-
|
|
170
|
-
// Should have day 15 (current date)
|
|
171
|
-
const day15 = screen.getAllByText('15');
|
|
172
|
-
expect(day15.length).toBeGreaterThanOrEqual(1);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('events are rendered within the calendar', () => {
|
|
176
|
-
render(
|
|
177
|
-
<CalendarView
|
|
178
|
-
currentDate={defaultDate}
|
|
179
|
-
events={mockEvents}
|
|
180
|
-
view="month"
|
|
181
|
-
locale="en-US"
|
|
182
|
-
/>
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
expect(screen.getByText('Team Standup')).toBeInTheDocument();
|
|
186
|
-
expect(screen.getByText('Sprint Review')).toBeInTheDocument();
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('interactive behaviors', () => {
|
|
191
|
-
it('navigation buttons are clickable and functional', () => {
|
|
192
|
-
const onNavigate = vi.fn();
|
|
193
|
-
render(
|
|
194
|
-
<CalendarView
|
|
195
|
-
currentDate={defaultDate}
|
|
196
|
-
onNavigate={onNavigate}
|
|
197
|
-
locale="en-US"
|
|
198
|
-
/>
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const todayButton = screen.getByText('Today');
|
|
202
|
-
fireEvent.click(todayButton);
|
|
203
|
-
|
|
204
|
-
expect(onNavigate).toHaveBeenCalled();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('event click handler is supported', () => {
|
|
208
|
-
const onEventClick = vi.fn();
|
|
209
|
-
render(
|
|
210
|
-
<CalendarView
|
|
211
|
-
currentDate={defaultDate}
|
|
212
|
-
events={mockEvents}
|
|
213
|
-
onEventClick={onEventClick}
|
|
214
|
-
view="month"
|
|
215
|
-
locale="en-US"
|
|
216
|
-
/>
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
const event = screen.getByText('Team Standup');
|
|
220
|
-
fireEvent.click(event);
|
|
221
|
-
|
|
222
|
-
expect(onEventClick).toHaveBeenCalled();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('date picker trigger is accessible', () => {
|
|
226
|
-
render(<CalendarView currentDate={defaultDate} locale="en-US" />);
|
|
227
|
-
|
|
228
|
-
const dateLabel = screen.getByText('January 2024');
|
|
229
|
-
const trigger = dateLabel.closest('button');
|
|
230
|
-
expect(trigger).toBeEnabled();
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe('semantic calendar markup', () => {
|
|
235
|
-
it('calendar root has proper CSS structure', () => {
|
|
236
|
-
const { container } = render(
|
|
237
|
-
<CalendarView currentDate={defaultDate} locale="en-US" />
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// Root element should have flex layout
|
|
241
|
-
const root = container.firstElementChild as HTMLElement;
|
|
242
|
-
expect(root).toHaveClass('flex', 'flex-col');
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('header section is separated by border', () => {
|
|
246
|
-
const { container } = render(
|
|
247
|
-
<CalendarView currentDate={defaultDate} locale="en-US" />
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
const header = container.querySelector('.border-b');
|
|
251
|
-
expect(header).toBeInTheDocument();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('custom className is applied to root', () => {
|
|
255
|
-
const { container } = render(
|
|
256
|
-
<CalendarView
|
|
257
|
-
currentDate={defaultDate}
|
|
258
|
-
className="custom-calendar"
|
|
259
|
-
locale="en-US"
|
|
260
|
-
/>
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
const root = container.firstElementChild as HTMLElement;
|
|
264
|
-
expect(root.className).toContain('custom-calendar');
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
describe('add event action', () => {
|
|
269
|
-
it('add button renders when onAddClick is provided', () => {
|
|
270
|
-
const onAddClick = vi.fn();
|
|
271
|
-
render(
|
|
272
|
-
<CalendarView
|
|
273
|
-
currentDate={defaultDate}
|
|
274
|
-
onAddClick={onAddClick}
|
|
275
|
-
locale="en-US"
|
|
276
|
-
/>
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const newButton = screen.getByText('New event');
|
|
280
|
-
expect(newButton).toBeInTheDocument();
|
|
281
|
-
expect(newButton.closest('button')).toBeInTheDocument();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('add button is not shown when onAddClick is absent', () => {
|
|
285
|
-
render(<CalendarView currentDate={defaultDate} locale="en-US" />);
|
|
286
|
-
|
|
287
|
-
expect(screen.queryByText('New event')).not.toBeInTheDocument();
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
});
|
|
@@ -1,230 +0,0 @@
|
|
|
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
|
-
* Tests for calendar bug fixes:
|
|
11
|
-
* - Event click always dispatches action (no schema.onEventClick guard)
|
|
12
|
-
* - i18n integration with useObjectTranslation
|
|
13
|
-
* - Tooltip (title attribute) on truncated event titles
|
|
14
|
-
* - Cross-month date visual distinction (opacity)
|
|
15
|
-
* - Today highlight spacing improvement
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
19
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
20
|
-
import '@testing-library/jest-dom';
|
|
21
|
-
import React from 'react';
|
|
22
|
-
import { CalendarView, type CalendarEvent } from '../CalendarView';
|
|
23
|
-
import { I18nProvider } from '@object-ui/i18n';
|
|
24
|
-
|
|
25
|
-
// Mock ResizeObserver
|
|
26
|
-
class ResizeObserver {
|
|
27
|
-
observe() {}
|
|
28
|
-
unobserve() {}
|
|
29
|
-
disconnect() {}
|
|
30
|
-
}
|
|
31
|
-
global.ResizeObserver = ResizeObserver;
|
|
32
|
-
|
|
33
|
-
// Mock PointerEvents for Radix
|
|
34
|
-
if (!global.PointerEvent) {
|
|
35
|
-
class PointerEvent extends Event {
|
|
36
|
-
button: number;
|
|
37
|
-
ctrlKey: boolean;
|
|
38
|
-
metaKey: boolean;
|
|
39
|
-
shiftKey: boolean;
|
|
40
|
-
constructor(type: string, props: any = {}) {
|
|
41
|
-
super(type, props);
|
|
42
|
-
this.button = props.button || 0;
|
|
43
|
-
this.ctrlKey = props.ctrlKey || false;
|
|
44
|
-
this.metaKey = props.metaKey || false;
|
|
45
|
-
this.shiftKey = props.shiftKey || false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// @ts-expect-error Mocking global PointerEvent
|
|
49
|
-
global.PointerEvent = PointerEvent as any;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Mock HTMLElement.offsetParent for Radix Popper
|
|
53
|
-
Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
|
|
54
|
-
get() {
|
|
55
|
-
return this.parentElement;
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const defaultDate = new Date(2024, 0, 15); // Jan 15, 2024
|
|
60
|
-
|
|
61
|
-
const sampleEvents: CalendarEvent[] = [
|
|
62
|
-
{
|
|
63
|
-
id: '1',
|
|
64
|
-
title: 'Design dashboard widget layout and structure',
|
|
65
|
-
start: new Date(2024, 0, 15, 10, 0),
|
|
66
|
-
end: new Date(2024, 0, 15, 11, 0),
|
|
67
|
-
color: '#3b82f6',
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: '2',
|
|
71
|
-
title: 'Short Event',
|
|
72
|
-
start: new Date(2024, 0, 15, 14, 0),
|
|
73
|
-
},
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
function renderWithI18n(ui: React.ReactElement, lang = 'en') {
|
|
77
|
-
return render(
|
|
78
|
-
<I18nProvider config={{ defaultLanguage: lang, detectBrowserLanguage: false }}>
|
|
79
|
-
{ui}
|
|
80
|
-
</I18nProvider>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
describe('Calendar Bug Fixes', () => {
|
|
85
|
-
describe('event click dispatches action', () => {
|
|
86
|
-
it('fires onEventClick when event is clicked in month view', () => {
|
|
87
|
-
const onClick = vi.fn();
|
|
88
|
-
renderWithI18n(
|
|
89
|
-
<CalendarView
|
|
90
|
-
currentDate={defaultDate}
|
|
91
|
-
events={sampleEvents}
|
|
92
|
-
view="month"
|
|
93
|
-
onEventClick={onClick}
|
|
94
|
-
/>
|
|
95
|
-
);
|
|
96
|
-
fireEvent.click(screen.getByText('Short Event'));
|
|
97
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
98
|
-
expect(onClick).toHaveBeenCalledWith(sampleEvents[1]);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('tooltip on truncated event titles', () => {
|
|
103
|
-
it('renders title attribute on event cards in month view', () => {
|
|
104
|
-
renderWithI18n(
|
|
105
|
-
<CalendarView
|
|
106
|
-
currentDate={defaultDate}
|
|
107
|
-
events={sampleEvents}
|
|
108
|
-
view="month"
|
|
109
|
-
/>
|
|
110
|
-
);
|
|
111
|
-
const eventEl = screen.getByText('Short Event');
|
|
112
|
-
expect(eventEl.closest('[title]')).toHaveAttribute('title', 'Short Event');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('renders title attribute with long title in month view', () => {
|
|
116
|
-
renderWithI18n(
|
|
117
|
-
<CalendarView
|
|
118
|
-
currentDate={defaultDate}
|
|
119
|
-
events={sampleEvents}
|
|
120
|
-
view="month"
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
const eventEl = screen.getByText('Design dashboard widget layout and structure');
|
|
124
|
-
expect(eventEl.closest('[title]')).toHaveAttribute(
|
|
125
|
-
'title',
|
|
126
|
-
'Design dashboard widget layout and structure'
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('renders title attribute on event cards in day view', () => {
|
|
131
|
-
renderWithI18n(
|
|
132
|
-
<CalendarView
|
|
133
|
-
currentDate={defaultDate}
|
|
134
|
-
events={sampleEvents}
|
|
135
|
-
view="day"
|
|
136
|
-
/>
|
|
137
|
-
);
|
|
138
|
-
const eventEl = screen.getByText('Short Event');
|
|
139
|
-
expect(eventEl.closest('[title]')).toHaveAttribute('title', 'Short Event');
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe('i18n integration', () => {
|
|
144
|
-
it('renders labels from i18n when language is en', () => {
|
|
145
|
-
const onAdd = vi.fn();
|
|
146
|
-
renderWithI18n(
|
|
147
|
-
<CalendarView
|
|
148
|
-
currentDate={defaultDate}
|
|
149
|
-
events={[]}
|
|
150
|
-
onAddClick={onAdd}
|
|
151
|
-
/>,
|
|
152
|
-
'en'
|
|
153
|
-
);
|
|
154
|
-
expect(screen.getByText('Today')).toBeInTheDocument();
|
|
155
|
-
expect(screen.getByText('New event')).toBeInTheDocument();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('renders labels from i18n when language is zh', () => {
|
|
159
|
-
const onAdd = vi.fn();
|
|
160
|
-
renderWithI18n(
|
|
161
|
-
<CalendarView
|
|
162
|
-
currentDate={defaultDate}
|
|
163
|
-
events={[]}
|
|
164
|
-
onAddClick={onAdd}
|
|
165
|
-
/>,
|
|
166
|
-
'zh'
|
|
167
|
-
);
|
|
168
|
-
expect(screen.getByText('今天')).toBeInTheDocument();
|
|
169
|
-
expect(screen.getByText('新建事件')).toBeInTheDocument();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('renders locale-aware weekday headers for zh', () => {
|
|
173
|
-
renderWithI18n(
|
|
174
|
-
<CalendarView
|
|
175
|
-
currentDate={defaultDate}
|
|
176
|
-
events={[]}
|
|
177
|
-
view="month"
|
|
178
|
-
/>,
|
|
179
|
-
'zh'
|
|
180
|
-
);
|
|
181
|
-
expect(screen.getByText('周日')).toBeInTheDocument();
|
|
182
|
-
expect(screen.getByText('周一')).toBeInTheDocument();
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe('cross-month date styling', () => {
|
|
187
|
-
it('applies opacity to non-current-month dates', () => {
|
|
188
|
-
const { container } = renderWithI18n(
|
|
189
|
-
<CalendarView
|
|
190
|
-
currentDate={new Date(2024, 1, 15)} // Feb 15, 2024
|
|
191
|
-
events={[]}
|
|
192
|
-
view="month"
|
|
193
|
-
/>
|
|
194
|
-
);
|
|
195
|
-
const gridCells = container.querySelectorAll('[role="gridcell"]');
|
|
196
|
-
const firstCell = gridCells[0];
|
|
197
|
-
expect(firstCell.className).toContain('opacity-50');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('does not apply opacity to current-month dates', () => {
|
|
201
|
-
const { container } = renderWithI18n(
|
|
202
|
-
<CalendarView
|
|
203
|
-
currentDate={new Date(2024, 1, 15)} // Feb 15, 2024
|
|
204
|
-
events={[]}
|
|
205
|
-
view="month"
|
|
206
|
-
/>
|
|
207
|
-
);
|
|
208
|
-
const gridCells = container.querySelectorAll('[role="gridcell"]');
|
|
209
|
-
// Feb 2024 starts on Thursday, so index 4 (0-based) is Feb 1
|
|
210
|
-
const feb1Cell = gridCells[4];
|
|
211
|
-
expect(feb1Cell.className).not.toContain('opacity-50');
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
describe('today highlight spacing', () => {
|
|
216
|
-
it('renders today date with mb-2 spacing for better separation', () => {
|
|
217
|
-
const today = new Date();
|
|
218
|
-
const { container } = renderWithI18n(
|
|
219
|
-
<CalendarView
|
|
220
|
-
currentDate={today}
|
|
221
|
-
events={[]}
|
|
222
|
-
view="month"
|
|
223
|
-
/>
|
|
224
|
-
);
|
|
225
|
-
const todayEl = container.querySelector('[aria-current="date"]');
|
|
226
|
-
expect(todayEl).toBeInTheDocument();
|
|
227
|
-
expect(todayEl?.className).toContain('mb-2');
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
});
|
|
@@ -1,178 +0,0 @@
|
|
|
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
|
-
* Tests for CalendarView optimizations:
|
|
9
|
-
* - Event index (Map-based lookup instead of O(N) per cell)
|
|
10
|
-
* - Stable default date reference
|
|
11
|
-
* - HEX color text-white fix
|
|
12
|
-
* - onEventDrop / locale passthrough from ObjectCalendar
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
-
import { render, screen } 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 @object-ui/components
|
|
22
|
-
vi.mock('@object-ui/components', () => ({
|
|
23
|
-
cn: (...classes: (string | undefined | boolean)[]) => classes.filter(Boolean).join(' '),
|
|
24
|
-
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
25
|
-
Select: ({ children, value, onValueChange }: any) => <div data-testid="select">{children}</div>,
|
|
26
|
-
SelectContent: ({ children }: any) => <div>{children}</div>,
|
|
27
|
-
SelectItem: ({ children, value }: any) => <div data-value={value}>{children}</div>,
|
|
28
|
-
SelectTrigger: ({ children }: any) => <div>{children}</div>,
|
|
29
|
-
SelectValue: () => <span>Month</span>,
|
|
30
|
-
Calendar: (props: any) => <div data-testid="calendar-picker">Calendar Picker</div>,
|
|
31
|
-
Popover: ({ children }: any) => <div>{children}</div>,
|
|
32
|
-
PopoverContent: ({ children }: any) => <div>{children}</div>,
|
|
33
|
-
PopoverTrigger: ({ children }: any) => <div>{children}</div>,
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
// Mock lucide-react icons
|
|
37
|
-
vi.mock('lucide-react', () => ({
|
|
38
|
-
ChevronLeftIcon: (props: any) => <svg data-testid="chevron-left" {...props} />,
|
|
39
|
-
ChevronRightIcon: (props: any) => <svg data-testid="chevron-right" {...props} />,
|
|
40
|
-
CalendarIcon: (props: any) => <svg data-testid="calendar-icon" {...props} />,
|
|
41
|
-
PlusIcon: (props: any) => <svg data-testid="plus-icon" {...props} />,
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
// Mock ResizeObserver
|
|
45
|
-
class ResizeObserver {
|
|
46
|
-
observe() {}
|
|
47
|
-
unobserve() {}
|
|
48
|
-
disconnect() {}
|
|
49
|
-
}
|
|
50
|
-
global.ResizeObserver = ResizeObserver;
|
|
51
|
-
|
|
52
|
-
const baseDate = new Date(2024, 0, 15); // Jan 15, 2024
|
|
53
|
-
|
|
54
|
-
describe('CalendarView optimizations', () => {
|
|
55
|
-
describe('event index (Map-based lookup)', () => {
|
|
56
|
-
it('renders single-day events correctly with the new index', () => {
|
|
57
|
-
const events: CalendarEvent[] = [
|
|
58
|
-
{ id: '1', title: 'Morning Meeting', start: new Date(2024, 0, 15, 9, 0), end: new Date(2024, 0, 15, 10, 0) },
|
|
59
|
-
{ id: '2', title: 'Lunch', start: new Date(2024, 0, 15, 12, 0), end: new Date(2024, 0, 15, 13, 0) },
|
|
60
|
-
];
|
|
61
|
-
render(<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />);
|
|
62
|
-
expect(screen.getByText('Morning Meeting')).toBeInTheDocument();
|
|
63
|
-
expect(screen.getByText('Lunch')).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('renders multi-day events on all spanned days', () => {
|
|
67
|
-
const events: CalendarEvent[] = [
|
|
68
|
-
{
|
|
69
|
-
id: 'multi-1',
|
|
70
|
-
title: 'Conference',
|
|
71
|
-
start: new Date(2024, 0, 15, 9, 0),
|
|
72
|
-
end: new Date(2024, 0, 17, 17, 0),
|
|
73
|
-
allDay: true,
|
|
74
|
-
},
|
|
75
|
-
];
|
|
76
|
-
const { container } = render(
|
|
77
|
-
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
78
|
-
);
|
|
79
|
-
// The multi-day event should appear on Jan 15, 16, and 17 — 3 cells
|
|
80
|
-
const eventElements = container.querySelectorAll('[role="button"][aria-label="Conference"]');
|
|
81
|
-
expect(eventElements.length).toBe(3);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('renders events with no end date on their start day only', () => {
|
|
85
|
-
const events: CalendarEvent[] = [
|
|
86
|
-
{ id: 'no-end', title: 'No End Event', start: new Date(2024, 0, 20, 14, 0) },
|
|
87
|
-
];
|
|
88
|
-
const { container } = render(
|
|
89
|
-
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
90
|
-
);
|
|
91
|
-
const eventElements = container.querySelectorAll('[role="button"][aria-label="No End Event"]');
|
|
92
|
-
expect(eventElements.length).toBe(1);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('HEX color text-white fix', () => {
|
|
97
|
-
it('applies text-white class when event color is a HEX value in month view', () => {
|
|
98
|
-
const events: CalendarEvent[] = [
|
|
99
|
-
{ id: 'hex-1', title: 'HEX Event', start: new Date(2024, 0, 15), color: '#3b82f6' },
|
|
100
|
-
];
|
|
101
|
-
const { container } = render(
|
|
102
|
-
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
103
|
-
);
|
|
104
|
-
const eventEl = container.querySelector('[aria-label="HEX Event"]');
|
|
105
|
-
expect(eventEl).toBeInTheDocument();
|
|
106
|
-
expect(eventEl!.className).toContain('text-white');
|
|
107
|
-
expect((eventEl as HTMLElement).style.backgroundColor).toBe('#3b82f6');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('uses default color class when event color is a Tailwind class', () => {
|
|
111
|
-
const events: CalendarEvent[] = [
|
|
112
|
-
{ id: 'tw-1', title: 'Tailwind Event', start: new Date(2024, 0, 15), color: 'bg-red-500 text-white' },
|
|
113
|
-
];
|
|
114
|
-
const { container } = render(
|
|
115
|
-
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
116
|
-
);
|
|
117
|
-
const eventEl = container.querySelector('[aria-label="Tailwind Event"]');
|
|
118
|
-
expect(eventEl).toBeInTheDocument();
|
|
119
|
-
expect(eventEl!.className).toContain('bg-red-500');
|
|
120
|
-
expect(eventEl!.className).toContain('text-white');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('falls back to DEFAULT_EVENT_COLOR when no color is specified', () => {
|
|
124
|
-
const events: CalendarEvent[] = [
|
|
125
|
-
{ id: 'no-color', title: 'No Color', start: new Date(2024, 0, 15) },
|
|
126
|
-
];
|
|
127
|
-
const { container } = render(
|
|
128
|
-
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
129
|
-
);
|
|
130
|
-
const eventEl = container.querySelector('[aria-label="No Color"]');
|
|
131
|
-
expect(eventEl).toBeInTheDocument();
|
|
132
|
-
expect(eventEl!.className).toContain('bg-blue-500');
|
|
133
|
-
expect(eventEl!.className).toContain('text-white');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('stable default date', () => {
|
|
138
|
-
it('renders without currentDate prop without errors', () => {
|
|
139
|
-
const { container } = render(<CalendarView events={[]} locale="en-US" />);
|
|
140
|
-
expect(container).toBeTruthy();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('does not trigger re-render loop when currentDate is not provided', () => {
|
|
144
|
-
const onNavigate = vi.fn();
|
|
145
|
-
const { rerender } = render(<CalendarView events={[]} locale="en-US" onNavigate={onNavigate} />);
|
|
146
|
-
// Re-render the same component — should NOT trigger onNavigate from the effect
|
|
147
|
-
rerender(<CalendarView events={[]} locale="en-US" onNavigate={onNavigate} />);
|
|
148
|
-
expect(onNavigate).not.toHaveBeenCalled();
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe('drag-and-drop enablement', () => {
|
|
153
|
-
it('events are draggable when onEventDrop is provided', () => {
|
|
154
|
-
const events: CalendarEvent[] = [
|
|
155
|
-
{ id: 'd-1', title: 'Drag Event', start: new Date(2024, 0, 15) },
|
|
156
|
-
];
|
|
157
|
-
const onEventDrop = vi.fn();
|
|
158
|
-
const { container } = render(
|
|
159
|
-
<CalendarView currentDate={baseDate} events={events} view="month" onEventDrop={onEventDrop} locale="en-US" />
|
|
160
|
-
);
|
|
161
|
-
const eventEl = container.querySelector('[aria-label="Drag Event"]');
|
|
162
|
-
expect(eventEl).toBeInTheDocument();
|
|
163
|
-
expect(eventEl!.getAttribute('draggable')).toBe('true');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('events are not draggable when onEventDrop is not provided', () => {
|
|
167
|
-
const events: CalendarEvent[] = [
|
|
168
|
-
{ id: 'nd-1', title: 'No Drag Event', start: new Date(2024, 0, 15) },
|
|
169
|
-
];
|
|
170
|
-
const { container } = render(
|
|
171
|
-
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
172
|
-
);
|
|
173
|
-
const eventEl = container.querySelector('[aria-label="No Drag Event"]');
|
|
174
|
-
expect(eventEl).toBeInTheDocument();
|
|
175
|
-
expect(eventEl!.getAttribute('draggable')).toBe('false');
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
});
|