File size: 8,382 Bytes
cd99321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import { render, screen, fireEvent, act } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { CustomDatePicker, CustomDateTimePicker } from './CustomDateTimePicker';
import { useSettingsStore } from '../../store/settingsStore';

// ─── CustomDatePicker ─────────────────────────────────────────────────────────

describe('CustomDatePicker', () => {
  const onChange = vi.fn();

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('FE-COMP-DATEPICKER-001: renders without crashing', () => {
    render(<CustomDatePicker value="" onChange={onChange} />);
    expect(document.body).toBeTruthy();
  });

  it('FE-COMP-DATEPICKER-002: shows placeholder when no value', () => {
    render(<CustomDatePicker value="" onChange={onChange} placeholder="Start Date" />);
    expect(screen.getByText('Start Date')).toBeTruthy();
  });

  it('FE-COMP-DATEPICKER-003: shows formatted date when value is set', () => {
    render(<CustomDatePicker value="2026-03-15" onChange={onChange} />);
    const btn = screen.getByRole('button');
    // Locale-formatted date should contain "Mar" or "15" or "2026"
    expect(btn.textContent).toMatch(/Mar|15|2026/);
  });

  it('FE-COMP-DATEPICKER-004: clicking button opens calendar portal', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="2026-03-15" onChange={onChange} />);
    await user.click(screen.getByRole('button'));
    const dayBtns = screen.getAllByRole('button').filter(b => /^\d+$/.test(b.textContent?.trim() ?? ''));
    expect(dayBtns.length).toBeGreaterThan(0);
  });

  it('FE-COMP-DATEPICKER-005: clicking a day calls onChange with correct ISO date', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="2026-03-01" onChange={onChange} />);
    await user.click(screen.getByRole('button')); // open March 2026
    const dayBtn = screen.getAllByRole('button').find(b => b.textContent?.trim() === '15');
    await user.click(dayBtn!);
    expect(onChange).toHaveBeenCalledWith('2026-03-15');
  });

  it('FE-COMP-DATEPICKER-006: prev month navigation decrements month', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="2026-03-01" onChange={onChange} />);
    await user.click(screen.getByRole('button')); // open March 2026
    // Nav buttons have no text content (only SVG icons)
    const emptyBtns = screen.getAllByRole('button').filter(b => b.textContent?.trim() === '');
    await user.click(emptyBtns[0]); // left chevron = prev month
    expect(screen.getByText(/february 2026/i)).toBeTruthy();
  });

  it('FE-COMP-DATEPICKER-007: next month navigation increments month', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="2026-03-01" onChange={onChange} />);
    await user.click(screen.getByRole('button')); // open March 2026
    const emptyBtns = screen.getAllByRole('button').filter(b => b.textContent?.trim() === '');
    await user.click(emptyBtns[emptyBtns.length - 1]); // right chevron = next month
    expect(screen.getByText(/april 2026/i)).toBeTruthy();
  });

  it('FE-COMP-DATEPICKER-008: clear button calls onChange with empty string', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="2026-03-15" onChange={onChange} />);
    await user.click(screen.getByRole('button')); // open
    const clearBtn = screen.getByText('βœ•');
    await user.click(clearBtn);
    expect(onChange).toHaveBeenCalledWith('');
  });

  it('FE-COMP-DATEPICKER-009: clear button absent when no value', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="" onChange={onChange} />);
    await user.click(screen.getByRole('button')); // open
    expect(screen.queryByText('βœ•')).toBeNull();
  });

  it('FE-COMP-DATEPICKER-010: clicking outside calendar closes it', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="2026-03-15" onChange={onChange} />);
    await user.click(screen.getByRole('button')); // open
    // Verify calendar is open (day buttons present)
    expect(screen.getAllByRole('button').filter(b => /^\d+$/.test(b.textContent?.trim() ?? '')).length).toBeGreaterThan(0);
    // Fire mousedown outside both the component div and the portal
    const outsideEl = document.createElement('div');
    document.body.appendChild(outsideEl);
    await act(async () => {
      fireEvent.mouseDown(outsideEl);
    });
    document.body.removeChild(outsideEl);
    // Day buttons should be gone
    expect(screen.getAllByRole('button').filter(b => /^\d+$/.test(b.textContent?.trim() ?? '')).length).toBe(0);
  });

  it('FE-COMP-DATEPICKER-011: double-click activates text input mode', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="" onChange={onChange} />);
    await user.dblClick(screen.getByRole('button'));
    expect(screen.getByPlaceholderText('DD.MM.YYYY')).toBeTruthy();
  });

  it('FE-COMP-DATEPICKER-012: text input accepts ISO format YYYY-MM-DD', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="" onChange={onChange} />);
    await user.dblClick(screen.getByRole('button'));
    const input = screen.getByPlaceholderText('DD.MM.YYYY');
    fireEvent.change(input, { target: { value: '2026-07-04' } });
    fireEvent.keyDown(input, { key: 'Enter' });
    expect(onChange).toHaveBeenCalledWith('2026-07-04');
  });

  it('FE-COMP-DATEPICKER-013: text input accepts EU format DD.MM.YYYY', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="" onChange={onChange} />);
    await user.dblClick(screen.getByRole('button'));
    const input = screen.getByPlaceholderText('DD.MM.YYYY');
    fireEvent.change(input, { target: { value: '04.07.2026' } });
    fireEvent.keyDown(input, { key: 'Enter' });
    expect(onChange).toHaveBeenCalledWith('2026-07-04');
  });

  it('FE-COMP-DATEPICKER-014: Escape in text input cancels text mode', async () => {
    const user = userEvent.setup();
    render(<CustomDatePicker value="" onChange={onChange} />);
    await user.dblClick(screen.getByRole('button'));
    const input = screen.getByPlaceholderText('DD.MM.YYYY');
    fireEvent.keyDown(input, { key: 'Escape' });
    expect(screen.queryByPlaceholderText('DD.MM.YYYY')).toBeNull();
    expect(screen.getByRole('button')).toBeTruthy();
  });
});

// ─── CustomDateTimePicker ─────────────────────────────────────────────────────

describe('CustomDateTimePicker', () => {
  const onChange = vi.fn();

  beforeEach(() => {
    vi.clearAllMocks();
    // Use 24h format for predictable time input behavior
    useSettingsStore.setState({
      settings: { ...useSettingsStore.getState().settings, time_format: '24h' },
    });
  });

  it('FE-COMP-DATEPICKER-015: renders date and time pickers side by side', () => {
    render(<CustomDateTimePicker value="" onChange={onChange} />);
    // Date picker renders a trigger button
    expect(screen.getAllByRole('button').length).toBeGreaterThanOrEqual(1);
    // Time picker renders a text input
    expect(screen.getByRole('textbox')).toBeTruthy();
  });

  it('FE-COMP-DATEPICKER-016: setting a date-only value defaults time to 12:00', async () => {
    const user = userEvent.setup();
    render(<CustomDateTimePicker value="" onChange={onChange} />);
    // The date trigger is the first button
    const dateTrigger = screen.getAllByRole('button')[0];
    await user.click(dateTrigger); // open calendar
    // Click day 1
    const day1 = screen.getAllByRole('button').find(b => b.textContent?.trim() === '1');
    await user.click(day1!);
    // onChange should have been called with T12:00 suffix
    expect(onChange).toHaveBeenCalledWith(expect.stringMatching(/T12:00$/));
  });

  it('FE-COMP-DATEPICKER-017: changing time part preserves date part', () => {
    render(<CustomDateTimePicker value="2026-06-01T09:30" onChange={onChange} />);
    const timeInput = screen.getByRole('textbox');
    fireEvent.change(timeInput, { target: { value: '10:00' } });
    expect(onChange).toHaveBeenCalledWith('2026-06-01T10:00');
  });
});