Logs.Datepickers.test.tsx320 lines · main
1import { screen, within } from '@testing-library/react'
2import userEvent from '@testing-library/user-event'
3import dayjs from 'dayjs'
4import timezone from 'dayjs/plugin/timezone'
5import utc from 'dayjs/plugin/utc'
6import { describe, expect, test, vi } from 'vitest'
7
8import { render } from '../../helpers'
9import { PREVIEWER_DATEPICKER_HELPERS } from '@/components/interfaces/Settings/Logs/Logs.constants'
10import {
11 generateDynamicHelper,
12 generateDynamicHelpers,
13 generateHelpersFromInput,
14 LogsDatePicker,
15 parseCustomInput,
16} from '@/components/interfaces/Settings/Logs/Logs.DatePickers'
17import { DatetimeHelper } from '@/components/interfaces/Settings/Logs/Logs.types'
18
19dayjs.extend(timezone)
20dayjs.extend(utc)
21
22describe('parseCustomInput', () => {
23 test('returns invalid for empty input', () => {
24 expect(parseCustomInput('')).toEqual({ type: 'invalid' })
25 expect(parseCustomInput(' ')).toEqual({ type: 'invalid' })
26 })
27
28 test('parses number only input', () => {
29 expect(parseCustomInput('25')).toEqual({ type: 'number', value: 25 })
30 expect(parseCustomInput(' 10 ')).toEqual({ type: 'number', value: 10 })
31 })
32
33 test('parses number with unit letter', () => {
34 expect(parseCustomInput('2h')).toEqual({ type: 'unit', value: 2, unit: 'hour' })
35 expect(parseCustomInput('30m')).toEqual({ type: 'unit', value: 30, unit: 'minute' })
36 expect(parseCustomInput('7d')).toEqual({ type: 'unit', value: 7, unit: 'day' })
37 })
38
39 test('parses number with space and unit letter', () => {
40 expect(parseCustomInput('2 h')).toEqual({ type: 'unit', value: 2, unit: 'hour' })
41 expect(parseCustomInput('30 m')).toEqual({ type: 'unit', value: 30, unit: 'minute' })
42 expect(parseCustomInput('7 d')).toEqual({ type: 'unit', value: 7, unit: 'day' })
43 })
44
45 test('parses number with full unit name prefix', () => {
46 expect(parseCustomInput('2hour')).toEqual({ type: 'unit', value: 2, unit: 'hour' })
47 expect(parseCustomInput('2hours')).toEqual({ type: 'invalid' })
48 expect(parseCustomInput('4day')).toEqual({ type: 'unit', value: 4, unit: 'day' })
49 expect(parseCustomInput('4days')).toEqual({ type: 'invalid' })
50 expect(parseCustomInput('30min')).toEqual({ type: 'unit', value: 30, unit: 'minute' })
51 expect(parseCustomInput('30minute')).toEqual({ type: 'unit', value: 30, unit: 'minute' })
52 })
53
54 test('is case insensitive', () => {
55 expect(parseCustomInput('2H')).toEqual({ type: 'unit', value: 2, unit: 'hour' })
56 expect(parseCustomInput('30M')).toEqual({ type: 'unit', value: 30, unit: 'minute' })
57 expect(parseCustomInput('7D')).toEqual({ type: 'unit', value: 7, unit: 'day' })
58 })
59
60 test('returns invalid for non-matching unit', () => {
61 expect(parseCustomInput('2x')).toEqual({ type: 'invalid' })
62 expect(parseCustomInput('2yoie')).toEqual({ type: 'invalid' })
63 expect(parseCustomInput('abc')).toEqual({ type: 'invalid' })
64 })
65
66 test('returns invalid for zero or negative', () => {
67 expect(parseCustomInput('0')).toEqual({ type: 'invalid' })
68 expect(parseCustomInput('-5')).toEqual({ type: 'invalid' })
69 })
70})
71
72describe('generateDynamicHelper', () => {
73 test('generates helper with correct text', () => {
74 const helper = generateDynamicHelper(5, 'hour')
75 expect(helper.text).toBe('Last 5 hours')
76 })
77
78 test('uses singular form when value is 1', () => {
79 expect(generateDynamicHelper(1, 'minute').text).toBe('Last 1 minute')
80 expect(generateDynamicHelper(1, 'hour').text).toBe('Last 1 hour')
81 expect(generateDynamicHelper(1, 'day').text).toBe('Last 1 day')
82 })
83
84 test('uses plural form when value > 1', () => {
85 expect(generateDynamicHelper(2, 'minute').text).toBe('Last 2 minutes')
86 expect(generateDynamicHelper(2, 'hour').text).toBe('Last 2 hours')
87 expect(generateDynamicHelper(2, 'day').text).toBe('Last 2 days')
88 })
89
90 test('calcFrom returns correct ISO string', () => {
91 const helper = generateDynamicHelper(1, 'hour')
92 const from = dayjs(helper.calcFrom())
93 const expectedFrom = dayjs().subtract(1, 'hour')
94 expect(from.diff(expectedFrom, 'second')).toBeLessThan(2)
95 })
96})
97
98describe('generateDynamicHelpers', () => {
99 test('generates 3 helpers for minutes, hours, days', () => {
100 const helpers = generateDynamicHelpers(5)
101 expect(helpers).toHaveLength(3)
102 expect(helpers[0].text).toBe('Last 5 minutes')
103 expect(helpers[1].text).toBe('Last 5 hours')
104 expect(helpers[2].text).toBe('Last 5 days')
105 })
106})
107
108describe('generateHelpersFromInput', () => {
109 test('returns null for invalid input', () => {
110 expect(generateHelpersFromInput('')).toBeNull()
111 expect(generateHelpersFromInput('abc')).toBeNull()
112 expect(generateHelpersFromInput('2yoie')).toBeNull()
113 })
114
115 test('returns 3 helpers for number only input', () => {
116 const helpers = generateHelpersFromInput('25')
117 expect(helpers).toHaveLength(3)
118 expect(helpers![0].text).toBe('Last 25 minutes')
119 expect(helpers![1].text).toBe('Last 25 hours')
120 expect(helpers![2].text).toBe('Last 25 days')
121 })
122
123 test('returns single helper for unit input', () => {
124 const helpers = generateHelpersFromInput('2h')
125 expect(helpers).toHaveLength(1)
126 expect(helpers![0].text).toBe('Last 2 hours')
127 })
128})
129
130const mockFn = vi.fn()
131
132test('renders warning', async () => {
133 const from = dayjs().subtract(10, 'days')
134 const to = dayjs()
135
136 render(
137 <LogsDatePicker
138 helpers={[]}
139 onSubmit={mockFn}
140 value={{
141 from: from.toISOString(),
142 to: to.toISOString(),
143 }}
144 />
145 )
146 await userEvent.click(await screen.findByText(RegExp(from.format('DD MMM'))))
147 await screen.findByText(/memory errors/)
148 await screen.findByText(RegExp(from.format('DD MMM')))
149})
150
151test('renders dates in local time', async () => {
152 const from = dayjs().subtract(1, 'days')
153 const to = dayjs()
154 render(
155 <LogsDatePicker
156 helpers={PREVIEWER_DATEPICKER_HELPERS}
157 onSubmit={mockFn}
158 value={{
159 from: from.toISOString(),
160 to: to.toISOString(),
161 }}
162 />
163 )
164 // renders time locally
165 await userEvent.click(await screen.findByText(RegExp(from.format('DD MMM'))))
166 await screen.findByText(RegExp(from.format('DD MMM')))
167})
168
169test('renders datepicker selected dates in local time', async () => {
170 const from = dayjs().date(25)
171 const to = dayjs().date(27)
172 render(
173 <LogsDatePicker
174 helpers={PREVIEWER_DATEPICKER_HELPERS}
175 value={{
176 from: from.toISOString(),
177 to: to.toISOString(),
178 }}
179 onSubmit={mockFn}
180 />
181 )
182 // renders time locally
183 await userEvent.click(await screen.findByText(RegExp(from.format('DD MMM'))))
184 // inputs with local time
185 await screen.findByText(
186 `${from.format('DD MMM')}, ${from.format('HH:mm')} - ${to.format('DD MMM')}, ${to.format('HH:mm')}`
187 )
188 // selected date should be in local time
189 await screen.findByText('25', { selector: "*[aria-label*='selected'" })
190 await screen.findByText('27', { selector: "*[aria-label*='selected'" })
191})
192
193test('datepicker onSubmit will return ISO string of selected dates', async () => {
194 const mockFn = vi.fn()
195 const todayAt1300 = dayjs().hour(13).minute(0).second(0).millisecond(0).toISOString()
196 const todayAt2359 = dayjs().hour(23).minute(59).second(59).millisecond(0).toISOString()
197
198 render(
199 <LogsDatePicker
200 helpers={PREVIEWER_DATEPICKER_HELPERS}
201 value={{
202 from: todayAt1300,
203 to: todayAt2359,
204 }}
205 onSubmit={mockFn}
206 />
207 )
208
209 // open the datepicker
210 userEvent.click(screen.getByText(/13:00/i))
211
212 const day15 = dayjs().date(15)
213 const day16 = day15.add(1, 'day')
214
215 // Find and click on first date
216 const day15Element = await screen.findByText(day15.format('D'))
217 userEvent.dblClick(day15Element)
218
219 // Find and click on second date
220 const day16Element = await screen.findByText(day16.format('D'))
221 userEvent.click(day16Element)
222
223 await userEvent.click(await screen.findByText('Apply'))
224 expect(mockFn).toBeCalled()
225
226 const call = mockFn.mock.calls[0][0]
227
228 expect(call).toMatchObject({
229 from: dayjs().date(day15.date()).hour(13).minute(0).second(0).millisecond(0).toISOString(),
230 to: dayjs().date(day16.date()).hour(23).minute(59).second(59).millisecond(0).toISOString(),
231 })
232})
233
234test('disabled helpers are disabled', async () => {
235 const helpers: DatetimeHelper[] = [
236 {
237 text: 'Last 7 days',
238 calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(),
239 calcTo: () => '',
240 },
241 {
242 text: 'Last 30 days',
243 calcFrom: () => dayjs().subtract(30, 'day').startOf('day').toISOString(),
244 calcTo: () => '',
245 disabled: true,
246 },
247 ]
248
249 render(
250 <LogsDatePicker
251 helpers={helpers}
252 onSubmit={mockFn}
253 value={{
254 from: dayjs().subtract(7, 'day').startOf('day').toISOString(),
255 to: '',
256 isHelper: true,
257 text: 'Last 7 days',
258 }}
259 />
260 )
261
262 // click the datepicker
263 userEvent.click(screen.getByText('Last 7 days'))
264
265 const disabledHelperContainer = await screen.findByText(/last 30 days/i)
266
267 const disabledButton = within(disabledHelperContainer).getByRole('radio', {
268 hidden: true,
269 })
270
271 expect(disabledButton.getAttribute('aria-disabled')).toBe('true')
272})
273
274test('passing a value prop shows the correct dates in the label', async () => {
275 const from = dayjs().subtract(10, 'days')
276 const to = dayjs()
277
278 render(
279 <LogsDatePicker
280 helpers={[]}
281 value={{ from: from.toISOString(), to: to.toISOString() }}
282 onSubmit={mockFn}
283 />
284 )
285
286 await screen.findByText(
287 `${from.format('DD MMM')}, ${from.format('HH:mm')} - ${to.format('DD MMM')}, ${to.format('HH:mm')}`
288 )
289
290 // change the date
291 userEvent.click(await screen.findByText(RegExp(from.format('DD MMM'))))
292 userEvent.click(await screen.findByText(RegExp(to.format('DD MMM'))))
293
294 await screen.findByText(
295 `${from.format('DD MMM')}, ${from.format('HH:mm')} - ${to.format('DD MMM')}, ${to.format('HH:mm')}`
296 )
297})
298
299test('passing a helper as a value prop shows the helper text in the label', async () => {
300 const helper = {
301 text: 'Last 7 days',
302 calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(),
303 calcTo: () => '',
304 }
305
306 render(
307 <LogsDatePicker
308 helpers={[helper]}
309 value={{
310 from: helper.calcFrom(),
311 to: helper.calcTo(),
312 isHelper: true,
313 text: helper.text,
314 }}
315 onSubmit={mockFn}
316 />
317 )
318
319 await screen.findByText(helper.text)
320})