@balby/booking-search 1.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,424 @@
1
+ import { test, expect, describe, beforeEach } from "bun:test"
2
+ import React from "react"
3
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react"
4
+ import { BookingSearch } from "../index"
5
+ import type { SearchLocation, AvailabilityDay, BookingSearchPayload } from "../../../types/booking"
6
+ import { format, addDays } from "date-fns"
7
+
8
+ describe("BookingSearch", () => {
9
+ const today = new Date()
10
+ const tomorrow = addDays(today, 1)
11
+ const nextWeek = addDays(today, 7)
12
+
13
+ const mockLocations: SearchLocation[] = [
14
+ { id: "1", name: "Rome", type: "City" },
15
+ { id: "2", name: "Milan", type: "City" },
16
+ { id: "3", name: "Venice", type: "City" },
17
+ ]
18
+
19
+ const mockAvailability: AvailabilityDay[] = Array.from({ length: 30 }, (_, i) => {
20
+ const date = addDays(today, i)
21
+ return {
22
+ date: format(date, "yyyy-MM-dd"),
23
+ isAvailable: true,
24
+ price: 100 + i * 5,
25
+ }
26
+ })
27
+
28
+ const mockOnSearch = (params: BookingSearchPayload) => {}
29
+
30
+ beforeEach(() => {
31
+ // Mock window.innerWidth for desktop view
32
+ Object.defineProperty(window, "innerWidth", {
33
+ writable: true,
34
+ configurable: true,
35
+ value: 1024,
36
+ })
37
+ })
38
+
39
+ test("renders all search components on desktop", () => {
40
+ render(
41
+ <BookingSearch
42
+ availability={mockAvailability}
43
+ locations={mockLocations}
44
+ onSearch={mockOnSearch}
45
+ />
46
+ )
47
+
48
+ expect(screen.getByText("Destination")).toBeDefined()
49
+ expect(screen.getByText("Check-in - Check-out")).toBeDefined()
50
+ expect(screen.getByText("Guests")).toBeDefined()
51
+ expect(screen.getByRole("button", { name: /search/i })).toBeDefined()
52
+ })
53
+
54
+ test("displays default location when provided", () => {
55
+ const firstLocation = mockLocations[0]
56
+ if (!firstLocation) return
57
+
58
+ render(
59
+ <BookingSearch
60
+ availability={mockAvailability}
61
+ locations={mockLocations}
62
+ onSearch={mockOnSearch}
63
+ defaultValues={{
64
+ location: firstLocation,
65
+ checkIn: null,
66
+ checkOut: null,
67
+ adults: 2,
68
+ children: 0,
69
+ }}
70
+ />
71
+ )
72
+
73
+ expect(screen.getByText("Rome")).toBeDefined()
74
+ })
75
+
76
+ test("displays default dates when provided", () => {
77
+ render(
78
+ <BookingSearch
79
+ availability={mockAvailability}
80
+ locations={mockLocations}
81
+ onSearch={mockOnSearch}
82
+ defaultValues={{
83
+ location: null,
84
+ checkIn: today,
85
+ checkOut: nextWeek,
86
+ adults: 2,
87
+ children: 0,
88
+ }}
89
+ />
90
+ )
91
+
92
+ const dateText = screen.getByText(/\d{2} \w{3} - \d{2} \w{3}/)
93
+ expect(dateText).toBeDefined()
94
+ })
95
+
96
+ test("displays default guest count when provided", () => {
97
+ render(
98
+ <BookingSearch
99
+ availability={mockAvailability}
100
+ locations={mockLocations}
101
+ onSearch={mockOnSearch}
102
+ defaultValues={{
103
+ location: null,
104
+ checkIn: null,
105
+ checkOut: null,
106
+ adults: 3,
107
+ children: 2,
108
+ }}
109
+ />
110
+ )
111
+
112
+ expect(screen.getByText("3 adulti, 2 bambini")).toBeDefined()
113
+ })
114
+
115
+ test("search button is disabled when location is not selected", () => {
116
+ render(
117
+ <BookingSearch
118
+ availability={mockAvailability}
119
+ locations={mockLocations}
120
+ onSearch={mockOnSearch}
121
+ />
122
+ )
123
+
124
+ const searchButton = screen.getByRole("button", { name: /search/i })
125
+ expect(searchButton.hasAttribute("disabled")).toBe(true)
126
+ })
127
+
128
+ test("search button is disabled when dates are not selected", () => {
129
+ const firstLocation = mockLocations[0]
130
+ if (!firstLocation) return
131
+
132
+ render(
133
+ <BookingSearch
134
+ availability={mockAvailability}
135
+ locations={mockLocations}
136
+ onSearch={mockOnSearch}
137
+ defaultValues={{
138
+ location: firstLocation,
139
+ checkIn: null,
140
+ checkOut: null,
141
+ adults: 2,
142
+ children: 0,
143
+ }}
144
+ />
145
+ )
146
+
147
+ const searchButton = screen.getByRole("button", { name: /search/i })
148
+ expect(searchButton.hasAttribute("disabled")).toBe(true)
149
+ })
150
+
151
+ test("search button is enabled when location and dates are selected", () => {
152
+ const firstLocation = mockLocations[0]
153
+ if (!firstLocation) return
154
+
155
+ render(
156
+ <BookingSearch
157
+ availability={mockAvailability}
158
+ locations={mockLocations}
159
+ onSearch={mockOnSearch}
160
+ defaultValues={{
161
+ location: firstLocation,
162
+ checkIn: today,
163
+ checkOut: nextWeek,
164
+ adults: 2,
165
+ children: 0,
166
+ }}
167
+ />
168
+ )
169
+
170
+ const searchButton = screen.getByRole("button", { name: /search/i })
171
+ expect(searchButton.hasAttribute("disabled")).toBe(false)
172
+ })
173
+
174
+ test("calls onSearch with correct parameters when search button is clicked", () => {
175
+ const firstLocation = mockLocations[0]
176
+ if (!firstLocation) return
177
+
178
+ let searchParams: BookingSearchPayload | null = null
179
+ const handleSearch = (params: BookingSearchPayload) => {
180
+ searchParams = params
181
+ }
182
+
183
+ render(
184
+ <BookingSearch
185
+ availability={mockAvailability}
186
+ locations={mockLocations}
187
+ onSearch={handleSearch}
188
+ defaultValues={{
189
+ location: firstLocation,
190
+ checkIn: today,
191
+ checkOut: nextWeek,
192
+ adults: 2,
193
+ children: 1,
194
+ }}
195
+ />
196
+ )
197
+
198
+ const searchButton = screen.getByRole("button", { name: /search/i })
199
+ expect(searchButton).toBeDefined()
200
+ fireEvent.click(searchButton)
201
+
202
+ expect(searchParams).not.toBeNull()
203
+ expect(searchParams!.location?.id).toBe(firstLocation.id)
204
+ expect(searchParams!.adults).toBe(2)
205
+ expect(searchParams!.children).toBe(1)
206
+ })
207
+
208
+ test("uses custom search button text when provided", () => {
209
+ render(
210
+ <BookingSearch
211
+ availability={mockAvailability}
212
+ locations={mockLocations}
213
+ onSearch={mockOnSearch}
214
+ searchButtonText="Ricerca"
215
+ />
216
+ )
217
+
218
+ expect(screen.getByText("Ricerca")).toBeDefined()
219
+ })
220
+
221
+ test("uses custom location placeholder when provided", () => {
222
+ render(
223
+ <BookingSearch
224
+ availability={mockAvailability}
225
+ locations={mockLocations}
226
+ onSearch={mockOnSearch}
227
+ locationPlaceholder="Custom placeholder"
228
+ />
229
+ )
230
+
231
+ expect(screen.getByText("Custom placeholder")).toBeDefined()
232
+ })
233
+
234
+ test("applies custom className to container", () => {
235
+ const { container } = render(
236
+ <BookingSearch
237
+ availability={mockAvailability}
238
+ locations={mockLocations}
239
+ onSearch={mockOnSearch}
240
+ className="custom-search-class"
241
+ />
242
+ )
243
+
244
+ const searchContainer = container.querySelector('[role="search"]')
245
+ expect(searchContainer?.classList.contains("custom-search-class")).toBe(true)
246
+ })
247
+
248
+ test("renders mobile view when window width is below 768px", () => {
249
+ // Mock mobile viewport
250
+ Object.defineProperty(window, "innerWidth", {
251
+ writable: true,
252
+ configurable: true,
253
+ value: 375,
254
+ })
255
+
256
+ // Trigger resize event
257
+ window.dispatchEvent(new Event("resize"))
258
+
259
+ render(
260
+ <BookingSearch
261
+ availability={mockAvailability}
262
+ locations={mockLocations}
263
+ onSearch={mockOnSearch}
264
+ />
265
+ )
266
+
267
+ // In mobile view, a dialog trigger button should be rendered
268
+ expect(screen.getByText("Dove vuoi andare?")).toBeDefined()
269
+ })
270
+
271
+ test("integrates all sub-components correctly", async () => {
272
+ render(
273
+ <BookingSearch
274
+ availability={mockAvailability}
275
+ locations={mockLocations}
276
+ onSearch={mockOnSearch}
277
+ />
278
+ )
279
+
280
+ // Test location selection
281
+ const locationButton = screen.getByRole("combobox")
282
+ expect(locationButton).toBeDefined()
283
+
284
+ // Test date picker
285
+ const dateButton = screen.getByRole("button", { name: "Select Date Range" })
286
+ expect(dateButton).toBeDefined()
287
+
288
+ // Test guest selector
289
+ const guestButton = screen.getByRole("button", { name: "Select guests" })
290
+ expect(guestButton).toBeDefined()
291
+
292
+ // Test search button
293
+ const searchButton = screen.getByRole("button", { name: /search/i })
294
+ expect(searchButton).toBeDefined()
295
+ })
296
+
297
+ test("passes minNights prop to DateRangePicker", () => {
298
+ render(
299
+ <BookingSearch
300
+ availability={mockAvailability}
301
+ locations={mockLocations}
302
+ onSearch={mockOnSearch}
303
+ minNights={3}
304
+ />
305
+ )
306
+
307
+ // The component should render without errors
308
+ expect(screen.getByText("Check-in - Check-out")).toBeDefined()
309
+ })
310
+
311
+ test("passes maxAdults prop to GuestSelector", () => {
312
+ render(
313
+ <BookingSearch
314
+ availability={mockAvailability}
315
+ locations={mockLocations}
316
+ onSearch={mockOnSearch}
317
+ maxAdults={10}
318
+ />
319
+ )
320
+
321
+ // The component should render without errors
322
+ expect(screen.getByText("Guests")).toBeDefined()
323
+ })
324
+
325
+ test("passes maxChildren prop to GuestSelector", () => {
326
+ render(
327
+ <BookingSearch
328
+ availability={mockAvailability}
329
+ locations={mockLocations}
330
+ onSearch={mockOnSearch}
331
+ maxChildren={5}
332
+ />
333
+ )
334
+
335
+ // The component should render without errors
336
+ expect(screen.getByText("Guests")).toBeDefined()
337
+ })
338
+
339
+ test("updates state when location is changed", async () => {
340
+ let searchParams: BookingSearchPayload | null = null
341
+ const handleSearch = (params: BookingSearchPayload) => {
342
+ searchParams = params
343
+ }
344
+
345
+ render(
346
+ <BookingSearch
347
+ availability={mockAvailability}
348
+ locations={mockLocations}
349
+ onSearch={handleSearch}
350
+ defaultValues={{
351
+ location: null,
352
+ checkIn: today,
353
+ checkOut: nextWeek,
354
+ adults: 2,
355
+ children: 0,
356
+ }}
357
+ />
358
+ )
359
+
360
+ // Initially search button should be disabled
361
+ const searchButton = screen.getByRole("button", { name: /search/i })
362
+ expect(searchButton.hasAttribute("disabled")).toBe(true)
363
+
364
+ // Open location combobox
365
+ const locationButton = screen.getByRole("combobox")
366
+ fireEvent.click(locationButton)
367
+
368
+ const locationItems = await waitFor(() => {
369
+ const items = screen.getAllByText("Rome")
370
+ expect(items.length).toBeGreaterThan(0)
371
+ return items
372
+ })
373
+
374
+ // Click on the location in the popover
375
+ const lastItem = locationItems[locationItems.length - 1]
376
+ if (lastItem) {
377
+ fireEvent.click(lastItem)
378
+ }
379
+
380
+ // Wait for state to update and get updated button reference
381
+ const updatedSearchButton = await waitFor(() => {
382
+ const btn = screen.getByRole("button", { name: /search/i })
383
+ expect(btn).toBeDefined()
384
+ expect(btn.hasAttribute("disabled")).toBe(false)
385
+ return btn
386
+ })
387
+
388
+ // Click search button
389
+ if (updatedSearchButton) {
390
+ fireEvent.click(updatedSearchButton)
391
+ }
392
+
393
+ // Verify search params include the selected location
394
+ const firstLocation = mockLocations[0]
395
+ await waitFor(() => {
396
+ expect(searchParams).not.toBeNull()
397
+ if (searchParams && firstLocation) {
398
+ expect(searchParams.location?.id).toBe(firstLocation.id)
399
+ }
400
+ })
401
+ })
402
+
403
+ test("has proper ARIA labels for accessibility", () => {
404
+ render(
405
+ <BookingSearch
406
+ availability={mockAvailability}
407
+ locations={mockLocations}
408
+ onSearch={mockOnSearch}
409
+ />
410
+ )
411
+
412
+ const searchContainer = screen.getByRole("search", { name: "Booking search" })
413
+ expect(searchContainer).toBeDefined()
414
+
415
+ const locationButton = screen.getByLabelText("Select location")
416
+ expect(locationButton).toBeDefined()
417
+
418
+ const dateButton = screen.getByLabelText("Select Date Range")
419
+ expect(dateButton).toBeDefined()
420
+
421
+ const guestButton = screen.getByLabelText("Select guests")
422
+ expect(guestButton).toBeDefined()
423
+ })
424
+ })
@@ -0,0 +1,263 @@
1
+ import { test, expect, describe, beforeEach } from "bun:test"
2
+ import React from "react"
3
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react"
4
+ import { LocationCombobox } from "../location-combobox"
5
+ import type { SearchLocation } from "../../../types/booking"
6
+
7
+ describe("LocationCombobox", () => {
8
+ const mockLocations: SearchLocation[] = [
9
+ { id: "1", name: "Rome", type: "City" },
10
+ { id: "2", name: "Milan", type: "City" },
11
+ { id: "3", name: "Venice", type: "City" },
12
+ ]
13
+
14
+ const mockOnChange = (location: SearchLocation | null) => {}
15
+
16
+ beforeEach(() => {
17
+ // Reset any state between tests if needed
18
+ })
19
+
20
+ test("renders with placeholder when no value is selected", () => {
21
+ render(
22
+ <LocationCombobox
23
+ locations={mockLocations}
24
+ value={null}
25
+ onChange={mockOnChange}
26
+ placeholder="Where do you want to go?"
27
+ />
28
+ )
29
+
30
+ expect(screen.getByText("Where do you want to go?")).toBeDefined()
31
+ expect(screen.getByText("Destination")).toBeDefined()
32
+ })
33
+
34
+ test("displays selected location", () => {
35
+ const firstLocation = mockLocations[0]
36
+ if (!firstLocation) return
37
+
38
+ render(
39
+ <LocationCombobox
40
+ locations={mockLocations}
41
+ value={firstLocation}
42
+ onChange={mockOnChange}
43
+ />
44
+ )
45
+
46
+ expect(screen.getByText("Rome")).toBeDefined()
47
+ })
48
+
49
+ test("opens popover when button is clicked", async () => {
50
+ render(
51
+ <LocationCombobox
52
+ locations={mockLocations}
53
+ value={null}
54
+ onChange={mockOnChange}
55
+ />
56
+ )
57
+
58
+ const button = screen.getByRole("combobox")
59
+ fireEvent.click(button)
60
+
61
+ await waitFor(() => {
62
+ expect(screen.getByPlaceholderText("Search a destination...")).toBeDefined()
63
+ })
64
+ })
65
+
66
+ test("displays all locations in the popover", async () => {
67
+ render(
68
+ <LocationCombobox
69
+ locations={mockLocations}
70
+ value={null}
71
+ onChange={mockOnChange}
72
+ />
73
+ )
74
+
75
+ const button = screen.getByRole("combobox")
76
+ fireEvent.click(button)
77
+
78
+ await waitFor(() => {
79
+ for (const location of mockLocations) {
80
+ expect(screen.getAllByText(location.name).length).toBeGreaterThan(0)
81
+ }
82
+ })
83
+ })
84
+
85
+ test("calls onChange when a location is selected", async () => {
86
+ let selectedLocation: SearchLocation | null = null
87
+ const handleChange = (location: SearchLocation | null) => {
88
+ selectedLocation = location
89
+ }
90
+
91
+ render(
92
+ <LocationCombobox
93
+ locations={mockLocations}
94
+ value={null}
95
+ onChange={handleChange}
96
+ />
97
+ )
98
+
99
+ const button = screen.getByRole("combobox")
100
+ fireEvent.click(button)
101
+
102
+ const locationItems = await waitFor(() => {
103
+ const items = screen.getAllByText("Rome")
104
+ expect(items.length).toBeGreaterThan(0)
105
+ return items
106
+ })
107
+
108
+ // Click on the location in the popover (not the button)
109
+ const lastItem = locationItems[locationItems.length - 1]
110
+ if (lastItem) {
111
+ fireEvent.click(lastItem)
112
+ }
113
+
114
+ const firstLocation = mockLocations[0]
115
+ await waitFor(() => {
116
+ expect(selectedLocation).not.toBeNull()
117
+ if (selectedLocation && firstLocation) {
118
+ expect(selectedLocation.id).toBe(firstLocation.id)
119
+ }
120
+ })
121
+ })
122
+
123
+ test("toggles selection when clicking on already selected location", async () => {
124
+ const firstLocation = mockLocations[0]
125
+ if (!firstLocation) return
126
+
127
+ let selectedLocation: SearchLocation | null = firstLocation
128
+ const handleChange = (location: SearchLocation | null) => {
129
+ selectedLocation = location
130
+ }
131
+
132
+ render(
133
+ <LocationCombobox
134
+ locations={mockLocations}
135
+ value={firstLocation}
136
+ onChange={handleChange}
137
+ />
138
+ )
139
+
140
+ const button = screen.getByRole("combobox")
141
+ fireEvent.click(button)
142
+
143
+ const locationItems = await waitFor(() => {
144
+ const items = screen.getAllByText("Rome")
145
+ expect(items.length).toBeGreaterThan(0)
146
+ return items
147
+ })
148
+
149
+ const lastItem = locationItems[locationItems.length - 1]
150
+ if (lastItem) {
151
+ fireEvent.click(lastItem)
152
+ }
153
+
154
+ await waitFor(() => {
155
+ expect(selectedLocation).toBe(null)
156
+ })
157
+ })
158
+
159
+ test("disables the button when disabled prop is true", () => {
160
+ render(
161
+ <LocationCombobox
162
+ locations={mockLocations}
163
+ value={null}
164
+ onChange={mockOnChange}
165
+ disabled={true}
166
+ />
167
+ )
168
+
169
+ const button = screen.getByRole("combobox")
170
+ expect(button.hasAttribute("disabled")).toBe(true)
171
+ })
172
+
173
+ test("shows location type when available", async () => {
174
+ render(
175
+ <LocationCombobox
176
+ locations={mockLocations}
177
+ value={null}
178
+ onChange={mockOnChange}
179
+ />
180
+ )
181
+
182
+ const button = screen.getByRole("combobox")
183
+ fireEvent.click(button)
184
+
185
+ await waitFor(() => {
186
+ mockLocations.forEach((location) => {
187
+ if (location.type) {
188
+ expect(screen.getAllByText(location.type).length).toBeGreaterThan(0)
189
+ }
190
+ })
191
+ })
192
+ })
193
+
194
+ test("shows empty message when no locations match search", async () => {
195
+ render(
196
+ <LocationCombobox
197
+ locations={mockLocations}
198
+ value={null}
199
+ onChange={mockOnChange}
200
+ />
201
+ )
202
+
203
+ const button = screen.getByRole("combobox")
204
+ fireEvent.click(button)
205
+
206
+ await waitFor(() => {
207
+ const searchInput = screen.getByPlaceholderText("Search a destination...")
208
+ fireEvent.change(searchInput, { target: { value: "Nonexistent City" } })
209
+ })
210
+
211
+ await waitFor(() => {
212
+ expect(screen.getByText("No location found.")).toBeDefined()
213
+ })
214
+ })
215
+
216
+ test("applies custom className", () => {
217
+ const { container } = render(
218
+ <LocationCombobox
219
+ locations={mockLocations}
220
+ value={null}
221
+ onChange={mockOnChange}
222
+ className="custom-class"
223
+ />
224
+ )
225
+
226
+ const button = container.querySelector("button")
227
+ expect(button).toBeDefined()
228
+ expect(button?.classList.contains("custom-class")).toBe(true)
229
+ })
230
+
231
+ test("closes popover after selecting a location", async () => {
232
+ render(
233
+ <LocationCombobox
234
+ locations={mockLocations}
235
+ value={null}
236
+ onChange={mockOnChange}
237
+ />
238
+ )
239
+
240
+ const button = screen.getByRole("combobox")
241
+ expect(button).toBeDefined()
242
+ fireEvent.click(button)
243
+
244
+ await waitFor(() => {
245
+ expect(screen.getByPlaceholderText("Search a destination...")).toBeDefined()
246
+ })
247
+
248
+ const locationItems = await waitFor(() => {
249
+ const items = screen.getAllByText("Milan")
250
+ expect(items.length).toBeGreaterThan(0)
251
+ return items
252
+ })
253
+
254
+ const lastItem = locationItems[locationItems.length - 1]
255
+ if (lastItem) {
256
+ fireEvent.click(lastItem)
257
+ }
258
+
259
+ await waitFor(() => {
260
+ expect(screen.queryByPlaceholderText("Search a destination...")).toBeNull()
261
+ })
262
+ })
263
+ })