Line 0
Link Here
|
0 |
- |
1 |
const dayjs = require("dayjs"); /* Cannot use our calendar JS code, it's in an include file (!) |
|
|
2 |
Also note that moment.js is deprecated */ |
3 |
const isSameOrBefore = require("dayjs/plugin/isSameOrBefore"); |
4 |
dayjs.extend(isSameOrBefore); |
5 |
|
6 |
describe("Booking Modal Tests", () => { |
7 |
// Common test data used across all tests |
8 |
const testData = { |
9 |
biblionumber: 134, |
10 |
patronId: 19, |
11 |
pickupLibraryId: "CPL", |
12 |
itemNumber: 287, |
13 |
itemTypeId: "BK", |
14 |
itemTypeDescription: "Book", |
15 |
startDate: dayjs().add(1, "day").startOf("day").toDate(), |
16 |
endDate: dayjs().add(5, "day").endOf("day").toDate(), |
17 |
}; |
18 |
|
19 |
// Shared mock data - will be populated in before() hook |
20 |
const sharedMockData = { |
21 |
patrons: [], |
22 |
items: [], |
23 |
libraries: [], |
24 |
bookings: [], |
25 |
}; |
26 |
|
27 |
// Mock data generators using cy.task() pattern - optimized for minimal data |
28 |
const generateMockPatrons = (count = 3, overridesArray = []) => { |
29 |
let chain = cy.wrap([]); |
30 |
|
31 |
// Define minimal patron data for test scenarios |
32 |
const defaultPatrons = [ |
33 |
{ |
34 |
patron_id: testData.patronId, // "19" - main test patron |
35 |
surname: "Doe", |
36 |
firstname: "John", |
37 |
cardnumber: "12345", |
38 |
library_id: testData.pickupLibraryId, |
39 |
category_id: "ADULT", |
40 |
date_of_birth: "1990-01-01", |
41 |
}, |
42 |
{ |
43 |
patron_id: 456, // For edit test and booking scenarios |
44 |
surname: "Smith", |
45 |
firstname: "Jane", |
46 |
cardnumber: "23456", |
47 |
library_id: testData.pickupLibraryId, |
48 |
category_id: "ADULT", |
49 |
date_of_birth: "1985-05-15", |
50 |
}, |
51 |
{ |
52 |
patron_id: 457, // For booking scenarios |
53 |
surname: "Johnson", |
54 |
firstname: "Bob", |
55 |
cardnumber: "34567", |
56 |
library_id: testData.pickupLibraryId, |
57 |
category_id: "ADULT", |
58 |
date_of_birth: "1988-08-20", |
59 |
}, |
60 |
]; |
61 |
|
62 |
for (let i = 0; i < count; i++) { |
63 |
const values = { |
64 |
...defaultPatrons[i % defaultPatrons.length], // Cycle through defaults |
65 |
...(overridesArray[i] || {}), |
66 |
}; |
67 |
|
68 |
chain = chain.then(patrons => { |
69 |
return cy |
70 |
.task("buildSampleObject", { |
71 |
object: "patron", |
72 |
values, |
73 |
}) |
74 |
.then(newPatron => { |
75 |
return [...patrons, newPatron]; |
76 |
}); |
77 |
}); |
78 |
} |
79 |
|
80 |
return chain; |
81 |
}; |
82 |
|
83 |
const generateMockItems = (count = 3, overridesArray = []) => { |
84 |
let chain = cy.wrap([]); |
85 |
|
86 |
for (let i = 0; i < count; i++) { |
87 |
const values = { |
88 |
effective_item_type_id: testData.itemTypeId, |
89 |
bookable: 1, |
90 |
...(overridesArray[i] || {}), |
91 |
}; |
92 |
|
93 |
chain = chain.then(items => { |
94 |
return cy |
95 |
.task("buildSampleObject", { |
96 |
object: "item", |
97 |
values, |
98 |
}) |
99 |
.then(newItem => { |
100 |
return [...items, newItem]; |
101 |
}); |
102 |
}); |
103 |
} |
104 |
|
105 |
return chain; |
106 |
}; |
107 |
|
108 |
const generateMockLibraries = (count = 2, overridesArray = []) => { |
109 |
let chain = cy.wrap([]); |
110 |
|
111 |
for (let i = 0; i < count; i++) { |
112 |
const values = { |
113 |
...(overridesArray[i] || {}), |
114 |
}; |
115 |
|
116 |
chain = chain.then(libraries => { |
117 |
return cy |
118 |
.task("buildSampleObject", { |
119 |
object: "library", |
120 |
values, |
121 |
}) |
122 |
.then(newLibrary => { |
123 |
return [...libraries, newLibrary]; |
124 |
}); |
125 |
}); |
126 |
} |
127 |
|
128 |
return chain; |
129 |
}; |
130 |
|
131 |
const generateMockBookings = (count = 3, overridesArray = []) => { |
132 |
let chain = cy.wrap([]); |
133 |
|
134 |
for (let i = 0; i < count; i++) { |
135 |
const values = { |
136 |
biblio_id: testData.biblionumber, |
137 |
status: "pending", |
138 |
...(overridesArray[i] || {}), |
139 |
}; |
140 |
|
141 |
chain = chain.then(bookings => { |
142 |
return cy |
143 |
.task("buildSampleObject", { |
144 |
object: "booking", |
145 |
values, |
146 |
}) |
147 |
.then(newBooking => { |
148 |
return [...bookings, newBooking]; |
149 |
}); |
150 |
}); |
151 |
} |
152 |
|
153 |
return chain; |
154 |
}; |
155 |
|
156 |
// Helper functions for API intercepts using shared mock data |
157 |
const setupStaticApiIntercepts = () => { |
158 |
// Use shared mock data to set up intercepts |
159 |
cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1&_per_page=-1", { |
160 |
body: sharedMockData.items, |
161 |
}).as("getBookableItems"); |
162 |
|
163 |
cy.intercept("GET", "/api/v1/biblios/*/checkouts?_per_page=-1", { |
164 |
body: [], // No checkouts for simplicity |
165 |
}).as("getCheckouts"); |
166 |
|
167 |
// Set up patron GET endpoint to return the appropriate patron by ID |
168 |
cy.intercept("GET", "/api/v1/patrons/*", req => { |
169 |
const patronId = req.url.split("/").pop(); |
170 |
const patron = sharedMockData.patrons.find( |
171 |
p => p.patron_id === patronId |
172 |
); |
173 |
req.reply({ |
174 |
body: patron || sharedMockData.patrons[0], // Fallback to first patron if not found |
175 |
}); |
176 |
}).as("getPatron"); |
177 |
|
178 |
// Mock patron search with all patrons |
179 |
cy.intercept("GET", "/api/v1/patrons*", { |
180 |
body: sharedMockData.patrons, |
181 |
pagination: { more: false }, |
182 |
}).as("searchPatrons"); |
183 |
|
184 |
cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { |
185 |
body: sharedMockData.libraries, |
186 |
}).as("getPickupLocations"); |
187 |
|
188 |
// Mock circulation rules |
189 |
cy.intercept("GET", "/api/v1/circulation_rules*", { |
190 |
body: [ |
191 |
{ |
192 |
bookings_lead_period: 2, |
193 |
bookings_trail_period: 1, |
194 |
issuelength: 14, |
195 |
renewalsallowed: 2, |
196 |
renewalperiod: 7, |
197 |
}, |
198 |
], |
199 |
}).as("getCirculationRules"); |
200 |
}; |
201 |
|
202 |
const setupBookingIntercepts = () => { |
203 |
cy.intercept("POST", "/api/v1/bookings", { |
204 |
statusCode: 201, |
205 |
body: { |
206 |
booking_id: 1001, |
207 |
start_date: testData.startDate.toISOString(), |
208 |
end_date: testData.endDate.toISOString(), |
209 |
pickup_library_id: testData.pickupLibraryId, |
210 |
biblio_id: testData.biblionumber, |
211 |
item_id: testData.itemNumber, |
212 |
patron_id: testData.patronId, |
213 |
}, |
214 |
}).as("createBooking"); |
215 |
|
216 |
cy.intercept("PUT", "/api/v1/bookings/*", { |
217 |
statusCode: 200, |
218 |
body: { |
219 |
booking_id: 1001, |
220 |
start_date: testData.startDate.toISOString(), |
221 |
end_date: testData.endDate.toISOString(), |
222 |
pickup_library_id: testData.pickupLibraryId, |
223 |
biblio_id: testData.biblionumber, |
224 |
item_id: testData.itemNumber, |
225 |
patron_id: testData.patronId, |
226 |
}, |
227 |
}).as("updateBooking"); |
228 |
}; |
229 |
|
230 |
const processDefaultBookings = (bookings: any[], today: any) => { |
231 |
// Update the dates in the fixture data relative to today |
232 |
bookings[0].start_date = today |
233 |
.add(8, "day") |
234 |
.startOf("day") |
235 |
.toISOString(); // Today + 8 days at 00:00 |
236 |
bookings[0].end_date = today.add(13, "day").endOf("day").toISOString(); // Today + 13 days at 23:59 |
237 |
|
238 |
bookings[1].start_date = today |
239 |
.add(14, "day") |
240 |
.startOf("day") |
241 |
.toISOString(); // Today + 14 days at 00:00 |
242 |
bookings[1].end_date = today.add(18, "day").endOf("day").toISOString(); // Today + 18 days at 23:59 |
243 |
|
244 |
bookings[2].start_date = today |
245 |
.add(28, "day") |
246 |
.startOf("day") |
247 |
.toISOString(); // Today + 28 days at 00:00 |
248 |
bookings[2].end_date = today.add(33, "day").endOf("day").toISOString(); // Today + 33 days at 23:59 |
249 |
|
250 |
return bookings; |
251 |
}; |
252 |
|
253 |
const setupDynamicBookings = (customBookings?: any[]) => { |
254 |
const today = dayjs(); |
255 |
|
256 |
if (customBookings) { |
257 |
// Use provided bookings directly |
258 |
cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { |
259 |
body: customBookings, |
260 |
}).as("getBookings"); |
261 |
} else { |
262 |
// Use shared mock bookings with dynamic dates |
263 |
const processedBookings = processDefaultBookings( |
264 |
sharedMockData.bookings, |
265 |
today |
266 |
); |
267 |
cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { |
268 |
body: processedBookings, |
269 |
}).as("getBookings"); |
270 |
} |
271 |
}; |
272 |
|
273 |
const setupModalTriggerButton = () => { |
274 |
cy.document().then(doc => { |
275 |
const button = doc.createElement("button"); |
276 |
button.setAttribute("data-bs-toggle", "modal"); |
277 |
button.setAttribute("data-bs-target", "#placeBookingModal"); |
278 |
button.setAttribute("data-biblionumber", testData.biblionumber); |
279 |
button.setAttribute("id", "placebooking"); |
280 |
doc.body.appendChild(button); |
281 |
}); |
282 |
}; |
283 |
|
284 |
const setupCustomItems = ( |
285 |
items: any[], |
286 |
itemTypeId: string, |
287 |
itemTypeDescription: string |
288 |
) => { |
289 |
const itemPromises = items.map(item => { |
290 |
return cy |
291 |
.task("buildSampleObject", { |
292 |
object: "item", |
293 |
values: { |
294 |
item_id: item.item_id, |
295 |
external_id: item.barcode, |
296 |
effective_item_type_id: itemTypeId, |
297 |
bookable: 1, |
298 |
location: "Main Library", |
299 |
callnumber: "TEST.CALL.NUMBER", |
300 |
status: "Available", |
301 |
}, |
302 |
}) |
303 |
.then(mockItem => { |
304 |
// Add item type info |
305 |
mockItem.item_type = { |
306 |
item_type_id: itemTypeId, |
307 |
description: itemTypeDescription, |
308 |
}; |
309 |
return mockItem; |
310 |
}); |
311 |
}); |
312 |
|
313 |
// Wait for all items to be generated, then set up intercept |
314 |
return cy.wrap(Promise.all(itemPromises)).then(mockItems => { |
315 |
cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1*", { |
316 |
body: mockItems, |
317 |
}).as("getBookableItems"); |
318 |
}); |
319 |
}; |
320 |
|
321 |
const setupCustomPickupLocations = (items: any[]) => { |
322 |
return generateMockLibraries(2, { |
323 |
library_id: ["MAIN", "BRANCH"], |
324 |
name: ["Main Library", "Branch Library"], |
325 |
needs_override: [false, false], |
326 |
}).then(mockLocations => { |
327 |
// Add pickup items to each location |
328 |
const modifiedLocations = mockLocations.map(location => ({ |
329 |
...location, |
330 |
pickup_items: [ |
331 |
...items.map(item => parseInt(item.item_id, 10)), |
332 |
], |
333 |
})); |
334 |
|
335 |
cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { |
336 |
body: modifiedLocations, |
337 |
}).as("getPickupLocations"); |
338 |
}); |
339 |
}; |
340 |
|
341 |
// Common setup functions for test workflows |
342 |
const openModalAndWait = ( |
343 |
apiCalls: string[] = [ |
344 |
"@getBookableItems", |
345 |
"@getBookings", |
346 |
"@getCheckouts", |
347 |
] |
348 |
) => { |
349 |
cy.get("#placebooking").click(); |
350 |
if (apiCalls.length > 0) { |
351 |
cy.wait(apiCalls); |
352 |
} |
353 |
}; |
354 |
|
355 |
const setupModalForBasicTesting = () => { |
356 |
openModalAndWait(); |
357 |
// Basic setup - just open modal and wait for initial data |
358 |
}; |
359 |
|
360 |
const setupModalForDateTesting = () => { |
361 |
openModalAndWait(); |
362 |
// Select patron, pickup location and item to enable date picker |
363 |
selectPatron(0, "John"); |
364 |
selectLocation(0); |
365 |
selectItem(1); |
366 |
}; |
367 |
|
368 |
const setupModalForFormSubmission = () => { |
369 |
setupModalForDateTesting(); |
370 |
// Set dates with flatpickr to prepare for submission |
371 |
cy.window().then(win => { |
372 |
const picker = win.document.getElementById("period")._flatpickr; |
373 |
const startDate = new Date(testData.startDate); |
374 |
const endDate = new Date(testData.endDate); |
375 |
picker.setDate([startDate, endDate], true); |
376 |
}); |
377 |
}; |
378 |
|
379 |
const selectPatron = ( |
380 |
patronIndex: number = 0, |
381 |
patronSearchTerm: string = "John" |
382 |
) => { |
383 |
cy.selectFromSelect2ByIndex( |
384 |
"#booking_patron_id", |
385 |
patronIndex, |
386 |
patronSearchTerm |
387 |
); |
388 |
// Wait for pickup locations API to complete |
389 |
cy.wait("@getPickupLocations"); |
390 |
}; |
391 |
|
392 |
const selectLocation = (locationIndex: number = 0) => { |
393 |
// Ensure the field is enabled before attempting selection |
394 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
395 |
cy.selectFromSelect2ByIndex("#pickup_library_id", locationIndex); |
396 |
}; |
397 |
|
398 |
const selectItem = (itemIndex: number) => { |
399 |
// Ensure the field is enabled before attempting selection |
400 |
cy.get("#booking_item_id").should("not.be.disabled"); |
401 |
cy.selectFromSelect2ByIndex("#booking_item_id", itemIndex); |
402 |
// Wait for circulation rules API and period field to be enabled |
403 |
cy.wait("@getCirculationRules"); |
404 |
cy.get("#period").should("not.be.disabled"); |
405 |
}; |
406 |
|
407 |
const selectItemType = (itemTypeIndex: number) => { |
408 |
// Ensure the field is enabled before attempting selection |
409 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
410 |
cy.selectFromSelect2ByIndex("#booking_itemtype", itemTypeIndex); |
411 |
// Wait for circulation rules API and period field to be enabled |
412 |
cy.wait("@getCirculationRules"); |
413 |
cy.get("#period").should("not.be.disabled"); |
414 |
}; |
415 |
|
416 |
const openModalWithoutWait = () => { |
417 |
cy.get("#placebooking").click(); |
418 |
// Only wait for modal to be visible, not for API calls |
419 |
cy.get("#placeBookingModal").should("be.visible"); |
420 |
}; |
421 |
|
422 |
const resetModalAndReopen = () => { |
423 |
cy.get('#placeBookingModal button[data-bs-dismiss="modal"]') |
424 |
.first() |
425 |
.click(); |
426 |
// Wait for modal to close |
427 |
cy.get("#placeBookingModal").should("not.be.visible"); |
428 |
// Reopen modal without waiting for API calls (they're cached) |
429 |
openModalWithoutWait(); |
430 |
}; |
431 |
|
432 |
// Generate shared mock data once before all tests |
433 |
before(() => { |
434 |
// Generate minimal shared mock data |
435 |
generateMockPatrons(3).then(patrons => { |
436 |
sharedMockData.patrons = patrons; |
437 |
}); |
438 |
|
439 |
generateMockItems(3, [ |
440 |
{ |
441 |
item_id: 789, |
442 |
external_id: "BARCODE789", |
443 |
effective_item_type_id: testData.itemTypeId, |
444 |
item_type: { |
445 |
item_type_id: testData.itemTypeId, |
446 |
description: testData.itemTypeDescription, |
447 |
}, |
448 |
}, |
449 |
{ |
450 |
item_id: 790, |
451 |
external_id: "BARCODE790", |
452 |
effective_item_type_id: testData.itemTypeId, |
453 |
item_type: { |
454 |
item_type_id: testData.itemTypeId, |
455 |
description: testData.itemTypeDescription, |
456 |
}, |
457 |
}, |
458 |
{ |
459 |
item_id: 791, |
460 |
external_id: "BARCODE791", |
461 |
effective_item_type_id: "DVD", |
462 |
item_type: { |
463 |
item_type_id: "DVD", |
464 |
description: "DVD", |
465 |
}, |
466 |
}, |
467 |
]).then(items => { |
468 |
sharedMockData.items = items; |
469 |
}); |
470 |
|
471 |
generateMockLibraries(2, [ |
472 |
{ |
473 |
library_id: "MAIN", |
474 |
name: "Main Library", |
475 |
pickup_items: [789, 790], |
476 |
}, |
477 |
{ |
478 |
library_id: "BRANCH", |
479 |
name: "Branch Library", |
480 |
pickup_items: [789, 791], |
481 |
}, |
482 |
]).then(libraries => { |
483 |
sharedMockData.libraries = libraries; |
484 |
}); |
485 |
|
486 |
generateMockBookings(3, [ |
487 |
{ |
488 |
booking_id: 1001, |
489 |
patron_id: 456, |
490 |
item_id: 789, |
491 |
pickup_library_id: "MAIN", |
492 |
}, |
493 |
{ |
494 |
booking_id: 1002, |
495 |
patron_id: 457, |
496 |
item_id: 790, |
497 |
pickup_library_id: "MAIN", |
498 |
}, |
499 |
{ |
500 |
booking_id: 1003, |
501 |
patron_id: 456, |
502 |
item_id: 791, |
503 |
pickup_library_id: "MAIN", |
504 |
}, |
505 |
]).then(bookings => { |
506 |
sharedMockData.bookings = bookings; |
507 |
}); |
508 |
}); |
509 |
|
510 |
beforeEach(() => { |
511 |
cy.login(); |
512 |
cy.title().should("eq", "Koha staff interface"); |
513 |
|
514 |
// Visit the page with the booking modal |
515 |
cy.visit( |
516 |
"/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + |
517 |
testData.biblionumber |
518 |
); |
519 |
|
520 |
// Setup all API intercepts using helper functions |
521 |
setupStaticApiIntercepts(); |
522 |
setupDynamicBookings(); |
523 |
setupBookingIntercepts(); |
524 |
setupModalTriggerButton(); |
525 |
}); |
526 |
|
527 |
it("should load the booking modal correctly", () => { |
528 |
setupModalForBasicTesting(); |
529 |
|
530 |
// Check modal title |
531 |
cy.get("#placeBookingLabel").should("contain", "Place booking"); |
532 |
|
533 |
// Check form elements are present |
534 |
cy.get("#booking_patron_id").should("exist"); |
535 |
cy.get("#pickup_library_id").should("exist"); |
536 |
cy.get("#booking_itemtype").should("exist"); |
537 |
cy.get("#booking_item_id").should("exist"); |
538 |
cy.get("#period").should("exist"); |
539 |
|
540 |
// Check hidden fields |
541 |
cy.get("#booking_biblio_id").should( |
542 |
"have.value", |
543 |
testData.biblionumber |
544 |
); |
545 |
cy.get("#booking_start_date").should("have.value", ""); |
546 |
cy.get("#booking_end_date").should("have.value", ""); |
547 |
}); |
548 |
|
549 |
it("should enable fields in proper sequence", () => { |
550 |
// Open the booking modal and wait for initial data |
551 |
openModalAndWait(); |
552 |
|
553 |
// Initially only patron field should be enabled |
554 |
cy.get("#booking_patron_id").should("not.be.disabled"); |
555 |
cy.get("#pickup_library_id").should("be.disabled"); |
556 |
cy.get("#booking_itemtype").should("be.disabled"); |
557 |
cy.get("#booking_item_id").should("be.disabled"); |
558 |
cy.get("#period").should("be.disabled"); |
559 |
|
560 |
// Select patron |
561 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
562 |
cy.wait("@getPickupLocations"); |
563 |
|
564 |
// After patron selection, pickup location, item type and item should be enabled |
565 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
566 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
567 |
cy.get("#booking_item_id").should("not.be.disabled"); |
568 |
cy.get("#period").should("be.disabled"); |
569 |
|
570 |
// Select pickup location |
571 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
572 |
|
573 |
// Select item type, trigger circulation rules |
574 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); |
575 |
cy.wait("@getCirculationRules"); |
576 |
|
577 |
// After patron, pickup location and itemtype/item selection, date picker should be enabled |
578 |
cy.get("#period").should("not.be.disabled"); |
579 |
|
580 |
// Clear item type and confirm period is disabled |
581 |
cy.clearSelect2("#booking_itemtype"); |
582 |
cy.get("#period").should("be.disabled"); |
583 |
|
584 |
// Select item, re-enable period |
585 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
586 |
cy.get("#period").should("not.be.disabled"); |
587 |
}); |
588 |
|
589 |
it("should handle item type and item dependencies correctly", () => { |
590 |
setupModalForBasicTesting(); |
591 |
|
592 |
// Select patron and pickup location first |
593 |
selectPatron(); |
594 |
selectLocation(); |
595 |
|
596 |
// Select an item first |
597 |
selectItem(1); |
598 |
|
599 |
// Verify that item type gets selected automatically |
600 |
cy.get("#booking_itemtype").should("have.value", testData.itemTypeId); |
601 |
|
602 |
// Verify that item type gets disabled |
603 |
cy.get("#booking_itemtype").should("be.disabled"); |
604 |
|
605 |
// Reset the modal |
606 |
resetModalAndReopen(); |
607 |
|
608 |
// Now select patron, pickup and item type first |
609 |
selectPatron(); |
610 |
selectLocation(); |
611 |
selectItemType(0); |
612 |
cy.wait(300); |
613 |
|
614 |
// Verify that only 'Any item' option and items of selected type are enabled |
615 |
cy.get("#booking_item_id > option").then($options => { |
616 |
const enabledOptions = $options.filter(":not(:disabled)"); |
617 |
enabledOptions.each(function () { |
618 |
const $option = cy.wrap(this); |
619 |
|
620 |
// Get both the value and the data-itemtype attribute to make decisions |
621 |
$option.invoke("val").then(value => { |
622 |
if (value === "0") { |
623 |
// We need to re-wrap the element since invoke('val') changed the subject |
624 |
cy.wrap(this).should("contain.text", "Any item"); |
625 |
} else { |
626 |
// Re-wrap the element again for this assertion |
627 |
cy.wrap(this).should( |
628 |
"have.attr", |
629 |
"data-itemtype", |
630 |
testData.itemTypeId |
631 |
); |
632 |
} |
633 |
}); |
634 |
}); |
635 |
}); |
636 |
}); |
637 |
|
638 |
it("should disable dates before today and between today and selected start date", () => { |
639 |
// Test-specific data for date validation |
640 |
const today = dayjs(); |
641 |
const dateTestData = { |
642 |
startDate: today.add(5, "day"), |
643 |
testRanges: { |
644 |
beforeToday: { start: today.subtract(7, "day"), end: today }, |
645 |
afterToday: { |
646 |
start: today.add(1, "day"), |
647 |
end: today.add(7, "day"), |
648 |
}, |
649 |
betweenTodayAndStart: { |
650 |
start: today.add(1, "day"), |
651 |
end: today.add(4, "day"), |
652 |
}, |
653 |
afterStartDate: { |
654 |
start: today.add(6, "day"), |
655 |
end: today.add(10, "day"), |
656 |
}, |
657 |
}, |
658 |
}; |
659 |
|
660 |
// Setup modal for date testing |
661 |
setupModalForDateTesting(); |
662 |
|
663 |
cy.get("#period").as("flatpickrInput"); |
664 |
cy.get("@flatpickrInput").openFlatpickr(); |
665 |
|
666 |
cy.get("@flatpickrInput").then($el => { |
667 |
// Phase 1: Test that all dates prior to today are disabled |
668 |
cy.log("Testing dates prior to today are disabled"); |
669 |
|
670 |
// Find the first visible date in the calendar to determine range |
671 |
cy.get(".flatpickr-day:not(.hidden)") |
672 |
.first() |
673 |
.then($firstDay => { |
674 |
const firstDate = dayjs($firstDay.attr("aria-label")); |
675 |
|
676 |
// Check all dates from first visible date up to today are disabled |
677 |
for ( |
678 |
let checkDate = firstDate; |
679 |
checkDate.isSameOrBefore(today, "day"); |
680 |
checkDate = checkDate.add(1, "day") |
681 |
) { |
682 |
cy.get("@flatpickrInput") |
683 |
.getFlatpickrDate(checkDate.toDate()) |
684 |
.should("have.class", "flatpickr-disabled"); |
685 |
} |
686 |
}); |
687 |
|
688 |
// Phase 2: Test that dates after today are initially enabled |
689 |
cy.log("Testing dates after today are initially enabled"); |
690 |
|
691 |
// Test a broader range of future dates for better coverage |
692 |
for ( |
693 |
let checkDate = today.add(1, "day"); |
694 |
checkDate.isSameOrBefore(today.add(7, "day"), "day"); |
695 |
checkDate = checkDate.add(1, "day") |
696 |
) { |
697 |
cy.get("@flatpickrInput") |
698 |
.getFlatpickrDate(checkDate.toDate()) |
699 |
.should("not.have.class", "flatpickr-disabled"); |
700 |
} |
701 |
}); |
702 |
|
703 |
// Phase 3: Select a start date |
704 |
cy.log( |
705 |
`Selecting start date (${dateTestData.startDate.format("YYYY-MM-DD")})` |
706 |
); |
707 |
cy.get("@flatpickrInput").selectFlatpickrDate( |
708 |
dateTestData.startDate.toDate() |
709 |
); |
710 |
|
711 |
// Phase 4: Verify dates between today and start date are now disabled |
712 |
cy.log("Testing dates between today and start date are disabled"); |
713 |
|
714 |
cy.get("@flatpickrInput").then($el => { |
715 |
for ( |
716 |
let checkDate = |
717 |
dateTestData.testRanges.betweenTodayAndStart.start; |
718 |
checkDate.isBefore(dateTestData.startDate, "day"); |
719 |
checkDate = checkDate.add(1, "day") |
720 |
) { |
721 |
cy.get("@flatpickrInput") |
722 |
.getFlatpickrDate(checkDate.toDate()) |
723 |
.should("have.class", "flatpickr-disabled"); |
724 |
} |
725 |
|
726 |
// Verify the selected start date itself is properly selected and not disabled |
727 |
cy.get("@flatpickrInput") |
728 |
.getFlatpickrDate(dateTestData.startDate.toDate()) |
729 |
.should("not.have.class", "flatpickr-disabled") |
730 |
.and("have.class", "selected"); |
731 |
}); |
732 |
|
733 |
// Phase 5: Verify dates after the start date remain enabled |
734 |
cy.log("Testing dates after start date remain enabled"); |
735 |
|
736 |
cy.get("@flatpickrInput").then($el => { |
737 |
for ( |
738 |
let checkDate = dateTestData.testRanges.afterStartDate.start; |
739 |
checkDate.isSameOrBefore( |
740 |
dateTestData.testRanges.afterStartDate.end, |
741 |
"day" |
742 |
); |
743 |
checkDate = checkDate.add(1, "day") |
744 |
) { |
745 |
cy.get("@flatpickrInput") |
746 |
.getFlatpickrDate(checkDate.toDate()) |
747 |
.should("not.have.class", "flatpickr-disabled"); |
748 |
} |
749 |
}); |
750 |
}); |
751 |
|
752 |
it("should disable dates with existing bookings for same item", () => { |
753 |
// Test-specific data |
754 |
const today = dayjs(); |
755 |
const testItemData = { |
756 |
itemId: 789, |
757 |
itemBarcode: "BARCODE789", |
758 |
bookingPeriods: [ |
759 |
{ |
760 |
start: today.add(8, "day"), |
761 |
end: today.add(13, "day"), |
762 |
name: "First booking period", |
763 |
}, |
764 |
{ |
765 |
start: today.add(14, "day"), |
766 |
end: today.add(18, "day"), |
767 |
name: "Second booking period", |
768 |
}, |
769 |
{ |
770 |
start: today.add(28, "day"), |
771 |
end: today.add(33, "day"), |
772 |
name: "Third booking period", |
773 |
}, |
774 |
{ |
775 |
start: today.add(35, "day"), |
776 |
end: today.add(37, "day"), |
777 |
name: "Fourth booking period", |
778 |
}, |
779 |
], |
780 |
otherItemBooking: { |
781 |
start: today.add(20, "day"), |
782 |
end: today.add(25, "day"), |
783 |
}, |
784 |
}; |
785 |
|
786 |
const TEST_ITEM_ID = testItemData.itemId; |
787 |
const TEST_ITEM_BARCODE = testItemData.itemBarcode; |
788 |
|
789 |
// Create custom bookings for this test using our helper |
790 |
const createCustomBookings = (baseBookings: any[]) => { |
791 |
// Modify existing bookings to create a comprehensive test scenario |
792 |
// All bookings will be for the same item to test date conflicts |
793 |
baseBookings[0].item_id = TEST_ITEM_ID; |
794 |
baseBookings[0].start_date = today |
795 |
.add(8, "day") |
796 |
.startOf("day") |
797 |
.toISOString(); |
798 |
baseBookings[0].end_date = today |
799 |
.add(13, "day") |
800 |
.endOf("day") |
801 |
.toISOString(); |
802 |
|
803 |
baseBookings[1].item_id = TEST_ITEM_ID; |
804 |
baseBookings[1].start_date = today |
805 |
.add(14, "day") |
806 |
.startOf("day") |
807 |
.toISOString(); |
808 |
baseBookings[1].end_date = today |
809 |
.add(18, "day") |
810 |
.endOf("day") |
811 |
.toISOString(); |
812 |
|
813 |
baseBookings[2].item_id = TEST_ITEM_ID; |
814 |
baseBookings[2].start_date = today |
815 |
.add(28, "day") |
816 |
.startOf("day") |
817 |
.toISOString(); |
818 |
baseBookings[2].end_date = today |
819 |
.add(33, "day") |
820 |
.endOf("day") |
821 |
.toISOString(); |
822 |
|
823 |
// Add additional bookings for comprehensive testing |
824 |
const additionalBookings = [ |
825 |
{ |
826 |
booking_id: 1004, |
827 |
biblio_id: 123, |
828 |
patron_id: 459, |
829 |
item_id: TEST_ITEM_ID, |
830 |
pickup_library_id: "MAIN", |
831 |
start_date: today |
832 |
.add(35, "day") |
833 |
.startOf("day") |
834 |
.toISOString(), |
835 |
end_date: today.add(37, "day").endOf("day").toISOString(), |
836 |
status: "pending", |
837 |
}, |
838 |
{ |
839 |
booking_id: 1005, |
840 |
biblio_id: 123, |
841 |
patron_id: 460, |
842 |
item_id: "different_item", // Different item - should not affect our test |
843 |
pickup_library_id: "MAIN", |
844 |
start_date: today |
845 |
.add(20, "day") |
846 |
.startOf("day") |
847 |
.toISOString(), |
848 |
end_date: today.add(25, "day").endOf("day").toISOString(), |
849 |
status: "pending", |
850 |
}, |
851 |
]; |
852 |
|
853 |
return [...baseBookings, ...additionalBookings]; |
854 |
}; |
855 |
|
856 |
// Setup custom bookings using our helper function |
857 |
generateMockBookings(3, [ |
858 |
{ |
859 |
booking_id: 1001, |
860 |
patron_id: 456, |
861 |
item_id: 789, |
862 |
}, |
863 |
{ |
864 |
booking_id: 1002, |
865 |
patron_id: 457, |
866 |
item_id: 790, |
867 |
}, |
868 |
{ |
869 |
booking_id: 1003, |
870 |
patron_id: 458, |
871 |
item_id: 791, |
872 |
}, |
873 |
]).then(baseBookings => { |
874 |
const customBookings = createCustomBookings(baseBookings); |
875 |
setupDynamicBookings(customBookings); |
876 |
}); |
877 |
|
878 |
// Setup modal for date testing |
879 |
setupModalForDateTesting(); |
880 |
|
881 |
// Select the specific item that has existing bookings (TEST_ITEM_BARCODE) |
882 |
cy.selectFromSelect2("#booking_item_id", TEST_ITEM_BARCODE); |
883 |
|
884 |
cy.get("#period").as("flatpickrInput"); |
885 |
cy.get("@flatpickrInput").openFlatpickr(); |
886 |
|
887 |
// Define booking periods for the selected item only |
888 |
const bookingPeriodsForSelectedItem = [ |
889 |
{ |
890 |
name: "First booking period", |
891 |
start: today.add(8, "day"), |
892 |
end: today.add(13, "day"), |
893 |
}, |
894 |
{ |
895 |
name: "Second booking period", |
896 |
start: today.add(14, "day"), |
897 |
end: today.add(18, "day"), |
898 |
}, |
899 |
{ |
900 |
name: "Third booking period", |
901 |
start: today.add(28, "day"), |
902 |
end: today.add(33, "day"), |
903 |
}, |
904 |
{ |
905 |
name: "Fourth booking period", |
906 |
start: today.add(35, "day"), |
907 |
end: today.add(37, "day"), |
908 |
}, |
909 |
]; |
910 |
|
911 |
cy.get("@flatpickrInput").then($el => { |
912 |
// Phase 1: Test dates before first booking period are available |
913 |
cy.log("Testing dates before first booking period are available"); |
914 |
|
915 |
for ( |
916 |
let checkDate = today.add(1, "day"); |
917 |
checkDate.isBefore( |
918 |
bookingPeriodsForSelectedItem[0].start, |
919 |
"day" |
920 |
); |
921 |
checkDate = checkDate.add(1, "day") |
922 |
) { |
923 |
cy.get("@flatpickrInput") |
924 |
.getFlatpickrDate(checkDate.toDate()) |
925 |
.should("not.have.class", "flatpickr-disabled"); |
926 |
} |
927 |
|
928 |
// Phase 2: Test each booked period individually |
929 |
bookingPeriodsForSelectedItem.forEach((period, index) => { |
930 |
cy.log(`Testing ${period.name} dates are disabled`); |
931 |
|
932 |
// Test dates within the booked range are disabled |
933 |
for ( |
934 |
let checkDate = period.start; |
935 |
checkDate.isSameOrBefore(period.end, "day"); |
936 |
checkDate = checkDate.add(1, "day") |
937 |
) { |
938 |
cy.get("@flatpickrInput") |
939 |
.getFlatpickrDate(checkDate.toDate()) |
940 |
.should("have.class", "flatpickr-disabled"); |
941 |
} |
942 |
}); |
943 |
|
944 |
// Phase 3: Test gaps between booking periods are available |
945 |
cy.log("Testing gaps between booking periods are available"); |
946 |
|
947 |
for (let i = 0; i < bookingPeriodsForSelectedItem.length - 1; i++) { |
948 |
const currentPeriod = bookingPeriodsForSelectedItem[i]; |
949 |
const nextPeriod = bookingPeriodsForSelectedItem[i + 1]; |
950 |
|
951 |
// Test dates in the gap between current and next booking period |
952 |
for ( |
953 |
let checkDate = currentPeriod.end.add(1, "day"); |
954 |
checkDate.isBefore(nextPeriod.start, "day"); |
955 |
checkDate = checkDate.add(1, "day") |
956 |
) { |
957 |
cy.get("@flatpickrInput") |
958 |
.getFlatpickrDate(checkDate.toDate()) |
959 |
.should("not.have.class", "flatpickr-disabled"); |
960 |
} |
961 |
} |
962 |
|
963 |
// Phase 4: Test that dates booked for different items are still available |
964 |
cy.log("Testing dates booked for different items remain available"); |
965 |
|
966 |
// The booking for different_item (days 20-25) should not affect our selected item |
967 |
const differentItemBookingStart = today.add(20, "day"); |
968 |
const differentItemBookingEnd = today.add(25, "day"); |
969 |
|
970 |
for ( |
971 |
let checkDate = differentItemBookingStart; |
972 |
checkDate.isSameOrBefore(differentItemBookingEnd, "day"); |
973 |
checkDate = checkDate.add(1, "day") |
974 |
) { |
975 |
cy.get("@flatpickrInput") |
976 |
.getFlatpickrDate(checkDate.toDate()) |
977 |
.should("not.have.class", "flatpickr-disabled"); |
978 |
} |
979 |
|
980 |
// Phase 5: Test dates after last booking period are available |
981 |
cy.log("Testing dates after last booking period are available"); |
982 |
|
983 |
const lastPeriod = |
984 |
bookingPeriodsForSelectedItem[ |
985 |
bookingPeriodsForSelectedItem.length - 1 |
986 |
]; |
987 |
for ( |
988 |
let checkDate = lastPeriod.end.add(1, "day"); |
989 |
checkDate.isSameOrBefore(lastPeriod.end.add(5, "day"), "day"); |
990 |
checkDate = checkDate.add(1, "day") |
991 |
) { |
992 |
cy.get("@flatpickrInput") |
993 |
.getFlatpickrDate(checkDate.toDate()) |
994 |
.should("not.have.class", "flatpickr-disabled"); |
995 |
} |
996 |
}); |
997 |
}); |
998 |
|
999 |
it("should handle lead and trail period hover highlighting", () => { |
1000 |
// Setup modal for date testing |
1001 |
setupModalForDateTesting(); |
1002 |
|
1003 |
// Open the flatpickr |
1004 |
cy.get("#period").as("flatpickrInput"); |
1005 |
cy.get("@flatpickrInput").openFlatpickr(); |
1006 |
|
1007 |
// Get a future date to hover over |
1008 |
let hoverDate = dayjs(); |
1009 |
hoverDate = hoverDate.add(5, "day"); |
1010 |
|
1011 |
// Hover over a date and check for lead/trail highlighting |
1012 |
cy.get("@flatpickrInput") |
1013 |
.getFlatpickrDate(hoverDate.toDate()) |
1014 |
.trigger("mouseover"); |
1015 |
cy.wait(100); |
1016 |
|
1017 |
// Check for lead range classes (assuming 2-day lead period from circulation rules) |
1018 |
cy.get(".leadRange, .leadRangeStart, .leadRangeEnd").should("exist"); |
1019 |
|
1020 |
// Check for trail range classes (assuming 2-day trail period) |
1021 |
cy.get(".trailRange, .trailRangeStart, .trailRangeEnd").should("exist"); |
1022 |
}); |
1023 |
|
1024 |
it("should disable click when lead/trail periods overlap with disabled dates", () => { |
1025 |
// Setup modal for date testing |
1026 |
setupModalForDateTesting(); |
1027 |
|
1028 |
// Open the flatpickr |
1029 |
cy.get("#period").as("flatpickrInput"); |
1030 |
cy.get("@flatpickrInput").openFlatpickr(); |
1031 |
|
1032 |
// Find a date that would have overlapping lead/trail with disabled dates |
1033 |
const today = dayjs(); |
1034 |
const problematicDate = today.add(7, "day"); // Just before a booked period |
1035 |
|
1036 |
cy.get("@flatpickrInput") |
1037 |
.getFlatpickrDate(problematicDate.toDate()) |
1038 |
.trigger("mouseover") |
1039 |
.should($el => { |
1040 |
expect( |
1041 |
$el.hasClass("leadDisable") || $el.hasClass("trailDisable"), |
1042 |
"element has either leadDisable or trailDisable" |
1043 |
).to.be.true; |
1044 |
}); |
1045 |
}); |
1046 |
|
1047 |
it("should show event dots for dates with existing bookings", () => { |
1048 |
// Setup modal for date testing |
1049 |
setupModalForDateTesting(); |
1050 |
|
1051 |
// Open the flatpickr |
1052 |
cy.get("#period").as("flatpickrInput"); |
1053 |
cy.get("@flatpickrInput").openFlatpickr(); |
1054 |
|
1055 |
// Check for event dots on dates with bookings |
1056 |
cy.get(".flatpickr-calendar").within(() => { |
1057 |
cy.get(".event-dots").should("exist"); |
1058 |
cy.get(".event-dots .event").should("exist"); |
1059 |
}); |
1060 |
}); |
1061 |
|
1062 |
it("should show only the correct bold dates for issue and renewal periods", () => { |
1063 |
// Setup modal for date testing |
1064 |
setupModalForDateTesting(); |
1065 |
|
1066 |
// Open the flatpickr |
1067 |
cy.get("#period").as("flatpickrInput"); |
1068 |
cy.get("@flatpickrInput").openFlatpickr(); |
1069 |
|
1070 |
const startDate = dayjs().add(3, "day").startOf("day"); |
1071 |
cy.get("@flatpickrInput").selectFlatpickrDate(startDate.toDate()); |
1072 |
|
1073 |
const expectedBoldDates = [ |
1074 |
startDate.add(14, "day"), |
1075 |
startDate.add(21, "day"), |
1076 |
startDate.add(28, "day"), |
1077 |
]; |
1078 |
|
1079 |
// Confirm each expected bold date is bold |
1080 |
expectedBoldDates.forEach(boldDate => { |
1081 |
cy.get("@flatpickrInput") |
1082 |
.getFlatpickrDate(boldDate.toDate()) |
1083 |
.should("have.class", "title"); |
1084 |
}); |
1085 |
|
1086 |
// Confirm that only expected dates are bold |
1087 |
cy.get(".flatpickr-day.title").each($el => { |
1088 |
const ariaLabel = $el.attr("aria-label"); |
1089 |
const date = dayjs(ariaLabel, "MMMM D, YYYY"); |
1090 |
const isExpected = expectedBoldDates.some(expected => |
1091 |
date.isSame(expected, "day") |
1092 |
); |
1093 |
expect(isExpected, `Unexpected bold date: ${ariaLabel}`).to.be.true; |
1094 |
}); |
1095 |
}); |
1096 |
|
1097 |
it("should set correct max date based on circulation rules", () => { |
1098 |
const today = dayjs(); |
1099 |
|
1100 |
// Setup modal for date testing |
1101 |
setupModalForDateTesting(); |
1102 |
|
1103 |
// Open the flatpickr |
1104 |
cy.get("#period").as("flatpickrInput"); |
1105 |
cy.get("@flatpickrInput").openFlatpickr(); |
1106 |
|
1107 |
// Select a start date |
1108 |
const startDate = today.add(3, "day"); |
1109 |
cy.get("@flatpickrInput").selectFlatpickrDate(startDate.toDate()); |
1110 |
|
1111 |
// Check that dates beyond the maximum allowed period are disabled |
1112 |
cy.get(".flatpickr-calendar").within(() => { |
1113 |
// Assuming circulation rules allow 14 days + renewals |
1114 |
const maxDate = startDate.add(30, "day"); // Beyond reasonable limit |
1115 |
|
1116 |
// Navigate to future months if needed and check disabled state |
1117 |
cy.get(".flatpickr-next-month").click(); |
1118 |
cy.get(".flatpickr-day.flatpickr-disabled").should("exist"); |
1119 |
}); |
1120 |
}); |
1121 |
|
1122 |
it("should handle visible and hidden fields on date selection", () => { |
1123 |
// Test-specific data for form field validation |
1124 |
const today = dayjs(); |
1125 |
const fieldTestData = { |
1126 |
selectedDates: { |
1127 |
startDate: today.add(3, "day"), |
1128 |
endDate: today.add(6, "day"), |
1129 |
}, |
1130 |
expectedFormats: { |
1131 |
displayFormat: "YYYY-MM-DD", // Format for visible input |
1132 |
isoFormat: "YYYY-MM-DDTHH:mm:ss.sssZ", // Format for hidden fields |
1133 |
}, |
1134 |
}; |
1135 |
|
1136 |
// Setup modal for date testing |
1137 |
setupModalForDateTesting(); |
1138 |
|
1139 |
cy.get("#period").as("flatpickrInput"); |
1140 |
|
1141 |
// Select date range using test data |
1142 |
cy.get("@flatpickrInput").selectFlatpickrDateRange( |
1143 |
fieldTestData.selectedDates.startDate.toDate(), |
1144 |
fieldTestData.selectedDates.endDate.toDate() |
1145 |
); |
1146 |
|
1147 |
// Use should with retry capability instead of a simple assertion |
1148 |
const format = date => |
1149 |
date.format(fieldTestData.expectedFormats.displayFormat); |
1150 |
const expectedDisplayValue = `${format(fieldTestData.selectedDates.startDate)} to ${format(fieldTestData.selectedDates.endDate)}`; |
1151 |
|
1152 |
cy.get("#period").should("have.value", expectedDisplayValue); |
1153 |
|
1154 |
// Verify the flatpickr visible input also has value |
1155 |
cy.get("#period").should("have.value", expectedDisplayValue); |
1156 |
|
1157 |
// Now check the hidden fields use ISO format |
1158 |
cy.get("#booking_start_date").should( |
1159 |
"have.value", |
1160 |
fieldTestData.selectedDates.startDate.startOf("day").toISOString() |
1161 |
); |
1162 |
cy.get("#booking_end_date").should( |
1163 |
"have.value", |
1164 |
fieldTestData.selectedDates.endDate.endOf("day").toISOString() |
1165 |
); |
1166 |
}); |
1167 |
|
1168 |
it("should submit a new booking successfully", () => { |
1169 |
// Setup modal for form submission |
1170 |
setupModalForFormSubmission(); |
1171 |
|
1172 |
// Submit the form |
1173 |
cy.get("#placeBookingForm").submit(); |
1174 |
cy.wait("@createBooking"); |
1175 |
|
1176 |
// Check success message |
1177 |
cy.get("#transient_result").should( |
1178 |
"contain", |
1179 |
"Booking successfully placed" |
1180 |
); |
1181 |
|
1182 |
// Check modal closes |
1183 |
cy.get("#placeBookingModal").should("not.be.visible"); |
1184 |
}); |
1185 |
|
1186 |
it("should edit an existing booking successfully", () => { |
1187 |
// Open edit booking modal |
1188 |
cy.get("#placebooking") |
1189 |
.invoke("attr", "data-booking", "1001") |
1190 |
.invoke("attr", "data-patron", "456") |
1191 |
.invoke("attr", "data-itemnumber", "789") |
1192 |
.invoke("attr", "data-pickup_library", "1") |
1193 |
.invoke("attr", "data-start_date", "2025-05-01T00:00:00.000Z") |
1194 |
.invoke("attr", "data-end_date", "2025-05-05T23:59:59.999Z") |
1195 |
.click(); |
1196 |
|
1197 |
// Wait for API calls that populate the modal when editing |
1198 |
cy.wait([ |
1199 |
"@getBookableItems", |
1200 |
"@getBookings", |
1201 |
"@getCheckouts", |
1202 |
"@getPickupLocations", |
1203 |
]); |
1204 |
|
1205 |
// Check modal title for edit |
1206 |
cy.get("#placeBookingLabel").should("contain", "Edit booking"); |
1207 |
|
1208 |
// Verify booking ID is set |
1209 |
cy.get("#booking_id").should("have.value", "1001"); |
1210 |
|
1211 |
// Verify patron ID is set |
1212 |
cy.get("#booking_patron_id").should("have.value", "456"); |
1213 |
|
1214 |
// Verify pickup_library is set |
1215 |
cy.get("#pickup_library_id").should("have.value", "MAIN"); |
1216 |
|
1217 |
// Verify itemnumber is set |
1218 |
cy.get("#booking_item_id").should("have.value", "789"); |
1219 |
|
1220 |
// Wait for item selection logic to complete and populate item type |
1221 |
cy.get("#booking_itemtype") |
1222 |
.should("not.have.value", "") |
1223 |
.and("not.have.value", null); |
1224 |
|
1225 |
// Verify item_type is set |
1226 |
cy.get("#booking_itemtype").should("have.value", "BK"); |
1227 |
|
1228 |
// Change pickup location |
1229 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 1); |
1230 |
|
1231 |
// Submit the form |
1232 |
cy.get("#placeBookingForm").submit(); |
1233 |
cy.wait("@updateBooking"); |
1234 |
|
1235 |
// Check success message |
1236 |
cy.get("#transient_result").should( |
1237 |
"contain", |
1238 |
"Booking successfully updated" |
1239 |
); |
1240 |
|
1241 |
// Check modal closes |
1242 |
cy.get("#placeBookingModal").should("not.be.visible"); |
1243 |
}); |
1244 |
|
1245 |
it("should handle booking failure gracefully", () => { |
1246 |
// Override the create booking intercept to return an error |
1247 |
cy.intercept("POST", "/api/v1/bookings", { |
1248 |
statusCode: 400, |
1249 |
body: { |
1250 |
error: "Booking failed", |
1251 |
}, |
1252 |
}).as("failedBooking"); |
1253 |
|
1254 |
// Setup modal for form submission |
1255 |
setupModalForFormSubmission(); |
1256 |
|
1257 |
// Submit the form |
1258 |
cy.get("#placeBookingForm").submit(); |
1259 |
cy.wait("@failedBooking"); |
1260 |
|
1261 |
// Check error message |
1262 |
cy.get("#booking_result").should("contain", "Failure"); |
1263 |
|
1264 |
// Modal should remain open |
1265 |
cy.get("#placeBookingModal").should("be.visible"); |
1266 |
}); |
1267 |
|
1268 |
it("should reset form when modal is closed", () => { |
1269 |
// Setup modal for date testing |
1270 |
setupModalForDateTesting(); |
1271 |
|
1272 |
// Reset modal to test form clearing |
1273 |
resetModalAndReopen(); |
1274 |
|
1275 |
// Check fields are reset to initial state |
1276 |
cy.get("#booking_patron_id") |
1277 |
.should("not.be.disabled") |
1278 |
.and("have.value", null); |
1279 |
cy.get("#pickup_library_id").should("be.disabled"); |
1280 |
cy.get("#booking_itemtype").should("be.disabled"); |
1281 |
cy.get("#booking_item_id").should("be.disabled"); |
1282 |
cy.get("#period").should("be.disabled"); |
1283 |
cy.get("#booking_start_date").should("have.value", ""); |
1284 |
cy.get("#booking_end_date").should("have.value", ""); |
1285 |
cy.get("#booking_id").should("have.value", ""); |
1286 |
}); |
1287 |
}); |