Line 0
Link Here
|
0 |
- |
1 |
describe("Booking Modal Tests", () => { |
|
|
2 |
// Test data setup |
3 |
const testData = { |
4 |
biblionumber: "134", |
5 |
patronId: "19", |
6 |
pickupLibraryId: "CPL", |
7 |
itemNumber: "287", |
8 |
itemTypeId: "BK", |
9 |
startDate: new Date(new Date().setDate(new Date().getDate() + 1)), // 1 day from now |
10 |
endDate: new Date(new Date().setDate(new Date().getDate() + 5)), // 5 days from now |
11 |
}; |
12 |
|
13 |
beforeEach(() => { |
14 |
cy.login(); |
15 |
cy.title().should("eq", "Koha staff interface"); |
16 |
|
17 |
// Visit the page with the booking modal |
18 |
cy.visit( |
19 |
"/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + |
20 |
testData.biblionumber |
21 |
); |
22 |
|
23 |
// Intercept API calls and provide mock responses |
24 |
cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1&_per_page=-1", { |
25 |
fixture: "bookings/bookable_items.json", |
26 |
}).as("getBookableItems"); |
27 |
|
28 |
cy.fixture("bookings/bookings.json").then(bookings => { |
29 |
// Get today's date |
30 |
const today = new Date(); |
31 |
|
32 |
// Update the dates in the fixture data relative to today |
33 |
bookings[0].start_date = new Date( |
34 |
today.getTime() + 8 * 24 * 60 * 60 * 1000 |
35 |
).toISOString(); // Today + 8 day |
36 |
bookings[0].end_date = new Date( |
37 |
today.getTime() + 13 * 24 * 60 * 60 * 1000 |
38 |
).toISOString(); // Today + 13 days |
39 |
|
40 |
bookings[1].start_date = new Date( |
41 |
today.getTime() + 14 * 24 * 60 * 60 * 1000 |
42 |
).toISOString(); // Today + 14 days |
43 |
bookings[1].end_date = new Date( |
44 |
today.getTime() + 18 * 24 * 60 * 60 * 1000 |
45 |
).toISOString(); // Today + 18 days |
46 |
|
47 |
bookings[2].start_date = new Date( |
48 |
today.getTime() + 28 * 24 * 60 * 60 * 1000 |
49 |
).toISOString(); // Today + 28 days |
50 |
bookings[2].end_date = new Date( |
51 |
today.getTime() + 33 * 24 * 60 * 60 * 1000 |
52 |
).toISOString(); // Today + 33 days |
53 |
|
54 |
// Use the modified fixture data in your intercept |
55 |
cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { |
56 |
body: bookings, |
57 |
}).as("getBookings"); |
58 |
}); |
59 |
|
60 |
cy.intercept("GET", "/api/v1/biblios/*/checkouts?_per_page=-1", { |
61 |
fixture: "bookings/checkouts.json", |
62 |
}).as("getCheckouts"); |
63 |
|
64 |
cy.intercept("GET", "/api/v1/patrons/*", { |
65 |
fixture: "bookings/patron.json", |
66 |
}).as("getPatron"); |
67 |
|
68 |
cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { |
69 |
fixture: "bookings/pickup_locations.json", |
70 |
}).as("getPickupLocations"); |
71 |
|
72 |
cy.intercept("GET", "/api/v1/circulation_rules*", { |
73 |
fixture: "bookings/circulation_rules.json", |
74 |
}).as("getCirculationRules"); |
75 |
|
76 |
cy.intercept("POST", "/api/v1/bookings", { |
77 |
statusCode: 201, |
78 |
body: { |
79 |
booking_id: "1001", |
80 |
start_date: testData.startDate.toISOString(), |
81 |
end_date: testData.endDate.toISOString(), |
82 |
pickup_library_id: testData.pickupLibraryId, |
83 |
biblio_id: testData.biblionumber, |
84 |
item_id: testData.itemNumber, |
85 |
patron_id: testData.patronId, |
86 |
}, |
87 |
}).as("createBooking"); |
88 |
|
89 |
cy.intercept("PUT", "/api/v1/bookings/*", { |
90 |
statusCode: 200, |
91 |
body: { |
92 |
booking_id: "1001", |
93 |
start_date: testData.startDate.toISOString(), |
94 |
end_date: testData.endDate.toISOString(), |
95 |
pickup_library_id: testData.pickupLibraryId, |
96 |
biblio_id: testData.biblionumber, |
97 |
item_id: testData.itemNumber, |
98 |
patron_id: testData.patronId, |
99 |
}, |
100 |
}).as("updateBooking"); |
101 |
|
102 |
// Populate the select2 search results for patron search |
103 |
cy.intercept("GET", "/api/v1/patrons*", { |
104 |
body: [ |
105 |
{ |
106 |
patron_id: testData.patronId, |
107 |
surname: "Doe", |
108 |
firstname: "John", |
109 |
cardnumber: "12345", |
110 |
library_id: "1", |
111 |
library: { |
112 |
name: "Main Library", |
113 |
}, |
114 |
category_id: "1", |
115 |
date_of_birth: "1990-01-01", |
116 |
}, |
117 |
], |
118 |
pagination: { more: false }, |
119 |
}).as("searchPatrons"); |
120 |
|
121 |
// Add clickable button |
122 |
cy.document().then(doc => { |
123 |
const button = doc.createElement("button"); |
124 |
button.setAttribute("data-bs-toggle", "modal"); |
125 |
button.setAttribute("data-bs-target", "#placeBookingModal"); |
126 |
button.setAttribute("data-biblionumber", testData.biblionumber); |
127 |
button.setAttribute("id", "placebooking"); |
128 |
doc.body.appendChild(button); |
129 |
}); |
130 |
|
131 |
// Open the booking modal |
132 |
cy.get("#placebooking").click(); |
133 |
}); |
134 |
|
135 |
it("should load the booking modal correctly", () => { |
136 |
// Check modal title |
137 |
cy.get("#placeBookingLabel").should("contain", "Place booking"); |
138 |
|
139 |
// Check form elements are present |
140 |
cy.get("#booking_patron_id").should("exist"); |
141 |
cy.get("#pickup_library_id").should("exist"); |
142 |
cy.get("#booking_itemtype").should("exist"); |
143 |
cy.get("#booking_item_id").should("exist"); |
144 |
cy.get("#period").should("exist"); |
145 |
|
146 |
// Check hidden fields |
147 |
cy.get("#booking_biblio_id").should( |
148 |
"have.value", |
149 |
testData.biblionumber |
150 |
); |
151 |
cy.get("#booking_start_date").should("have.value", ""); |
152 |
cy.get("#booking_end_date").should("have.value", ""); |
153 |
}); |
154 |
|
155 |
it("should enable fields in proper sequence", () => { |
156 |
// Initially only patron field should be enabled |
157 |
cy.get("#booking_patron_id").should("not.be.disabled"); |
158 |
cy.get("#pickup_library_id").should("be.disabled"); |
159 |
cy.get("#booking_itemtype").should("be.disabled"); |
160 |
cy.get("#booking_item_id").should("be.disabled"); |
161 |
cy.get("#period").should("be.disabled"); |
162 |
|
163 |
// Select patron |
164 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
165 |
//cy.getSelect2("#booking_patron_id") |
166 |
// .select2({ search: "John" }) |
167 |
// .select2({ selectIndex: 0 }); |
168 |
cy.wait("@getPickupLocations"); |
169 |
|
170 |
// After patron selection, pickup location, item type and item should be enabled |
171 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
172 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
173 |
cy.get("#booking_item_id").should("not.be.disabled"); |
174 |
cy.get("#period").should("be.disabled"); |
175 |
|
176 |
// Select pickup location |
177 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
178 |
|
179 |
// Select item type, trigger circulation rules |
180 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); |
181 |
cy.wait("@getCirculationRules"); |
182 |
|
183 |
// After patron, pickup location and itemtype/item selection, date picker should be enabled |
184 |
cy.get("#period").should("not.be.disabled"); |
185 |
|
186 |
// Clear item type and confirm period is disabled |
187 |
cy.clearSelect2("#booking_itemtype"); |
188 |
cy.get("#period").should("be.disabled"); |
189 |
|
190 |
// Select item, re-enable period |
191 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
192 |
cy.get("#period").should("not.be.disabled"); |
193 |
}); |
194 |
|
195 |
it("should handle item type and item dependencies correctly", () => { |
196 |
// Select patron and pickup location first |
197 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
198 |
cy.wait("@getPickupLocations"); |
199 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
200 |
|
201 |
// Select an item first |
202 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
203 |
cy.wait("@getCirculationRules"); |
204 |
|
205 |
// Verify that item type gets selected automatically |
206 |
cy.get("#booking_itemtype").should("have.value", testData.itemTypeId); |
207 |
|
208 |
// Verify that item type gets disabled |
209 |
cy.get("#booking_itemtype").should("be.disabled"); |
210 |
|
211 |
// Reset the modal |
212 |
cy.get('#placeBookingModal button[data-bs-dismiss="modal"]') |
213 |
.first() |
214 |
.click(); |
215 |
cy.get("#placebooking").click(); |
216 |
|
217 |
// Now select patron, pickup and item type first |
218 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
219 |
cy.wait("@getPickupLocations"); |
220 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
221 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); |
222 |
cy.wait("@getCirculationRules"); |
223 |
cy.wait(300); |
224 |
|
225 |
// Verify that only 'Any item' option and items of selected type are enabled |
226 |
cy.get("#booking_item_id > option").then($options => { |
227 |
const enabledOptions = $options.filter(":not(:disabled)"); |
228 |
enabledOptions.each(function () { |
229 |
const $option = cy.wrap(this); |
230 |
|
231 |
// Get both the value and the data-itemtype attribute to make decisions |
232 |
$option.invoke("val").then(value => { |
233 |
if (value === "0") { |
234 |
// We need to re-wrap the element since invoke('val') changed the subject |
235 |
cy.wrap(this).should("contain.text", "Any item"); |
236 |
} else { |
237 |
// Re-wrap the element again for this assertion |
238 |
cy.wrap(this).should( |
239 |
"have.attr", |
240 |
"data-itemtype", |
241 |
testData.itemTypeId |
242 |
); |
243 |
} |
244 |
}); |
245 |
}); |
246 |
}); |
247 |
}); |
248 |
|
249 |
it("should handle date selection and availability", () => { |
250 |
// Select patron, pickup location and item |
251 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
252 |
cy.wait("@getPickupLocations"); |
253 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
254 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
255 |
cy.wait("@getCirculationRules"); |
256 |
|
257 |
// Open flatpickr |
258 |
cy.openFlatpickr("#period"); |
259 |
|
260 |
// Get today's date |
261 |
const today = new Date(); |
262 |
const todayDay = today.getDate(); |
263 |
|
264 |
// Get target date (5 days from now) |
265 |
const targetDate = new Date(today); |
266 |
targetDate.setDate(today.getDate() + 5); |
267 |
const targetDay = targetDate.getDate(); |
268 |
|
269 |
// Check if the dates are in the same month |
270 |
if ( |
271 |
today.getMonth() === targetDate.getMonth() && |
272 |
today.getFullYear() === targetDate.getFullYear() |
273 |
) { |
274 |
// Same month - select start date |
275 |
cy.selectDay(todayDay); |
276 |
cy.wait(300); |
277 |
|
278 |
// Select end date |
279 |
cy.selectDay(targetDay, "#period"); |
280 |
} else { |
281 |
// Different months - select start date |
282 |
cy.selectDay(todayDay); |
283 |
cy.wait(300); |
284 |
|
285 |
// Navigate to the target month |
286 |
cy.navigateToMonth(targetDate); |
287 |
cy.wait(300); |
288 |
|
289 |
// Select end date |
290 |
cy.selectDay(targetDay, "#period"); |
291 |
cy.wait(300); |
292 |
} |
293 |
|
294 |
// QUESTION: Why does my flatpickr appear to be re-triggered here |
295 |
// so it's open and all the below assertions fail? |
296 |
|
297 |
/* |
298 |
// Use should with retry capability instead of a simple assertion |
299 |
cy.get("#period").should("have.value", /\w+/); // First check that there's any value |
300 |
|
301 |
// Verify the flatpickr visible input also has value |
302 |
cy.getFlatpickr("#period").should("have.value", /\w+/); |
303 |
|
304 |
// Now check the hidden fields |
305 |
cy.get("#booking_start_date").should("have.value", /\w+/); |
306 |
cy.get("#booking_end_date").should("have.value", /\w+/); |
307 |
|
308 |
// Verify the range was selected properly |
309 |
cy.get("#period").should("not.have.value", ""); |
310 |
|
311 |
// Now check the hidden fields |
312 |
cy.get("#booking_start_date").should("not.have.value", ""); |
313 |
cy.get("#booking_end_date").should("not.have.value", ""); |
314 |
|
315 |
// Check for disabled dates due to other bookings |
316 |
cy.get(".flatpickr-day.flatpickr-disabled").should("exist"); |
317 |
|
318 |
// Check for event dots indicating existing bookings |
319 |
cy.get(".flatpickr-day .event-dots").should("exist"); |
320 |
*/ |
321 |
}); |
322 |
|
323 |
it("should handle date selection and availability - programatically", () => { |
324 |
// Select patron, pickup location and item |
325 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
326 |
cy.wait("@getPickupLocations"); |
327 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
328 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
329 |
cy.wait("@getCirculationRules"); |
330 |
|
331 |
const picker = win.document.getElementById("period")._flatpickr; |
332 |
|
333 |
// Available dates |
334 |
let startDate = new Date(testData.startDate); |
335 |
let endDate = new Date(testData.endDate); |
336 |
picker.setDate([startDate, endDate], true); |
337 |
|
338 |
// Verify the range was selected properly |
339 |
cy.get("#period").should("not.have.value", ""); |
340 |
|
341 |
// Now check the hidden fields |
342 |
cy.get("#booking_start_date").should("not.have.value", ""); |
343 |
cy.get("#booking_end_date").should("not.have.value", ""); |
344 |
|
345 |
// Unavailable dates - Should throw error |
346 |
startDate = new Date(testData.startDate); |
347 |
endDate = new Date(testData.endDate); |
348 |
picker.setDate([startDate, endDate], true); |
349 |
}); |
350 |
|
351 |
it("should submit a new booking successfully", () => { |
352 |
// Select patron, pickup location and item |
353 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
354 |
cy.wait("@getPickupLocations"); |
355 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
356 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
357 |
cy.wait("@getCirculationRules"); |
358 |
|
359 |
// Set dates with flatpickr |
360 |
cy.window().then(win => { |
361 |
const picker = win.document.getElementById("period")._flatpickr; |
362 |
const startDate = new Date(testData.startDate); |
363 |
const endDate = new Date(testData.endDate); |
364 |
picker.setDate([startDate, endDate], true); |
365 |
}); |
366 |
|
367 |
// Submit the form |
368 |
cy.get("#placeBookingForm").submit(); |
369 |
cy.wait("@createBooking"); |
370 |
|
371 |
// Check success message |
372 |
cy.get("#transient_result").should( |
373 |
"contain", |
374 |
"Booking successfully placed" |
375 |
); |
376 |
|
377 |
// Check modal closes |
378 |
cy.get("#placeBookingModal").should("not.be.visible"); |
379 |
}); |
380 |
|
381 |
it("should edit an existing booking successfully", () => { |
382 |
// Open edit booking modal |
383 |
cy.get("#placebooking").invoke("attr", "data-booking", "1001").click(); |
384 |
|
385 |
// Check modal title for edit |
386 |
cy.get("#placeBookingLabel").should("contain", "Edit booking"); |
387 |
|
388 |
// Verify booking ID is set |
389 |
cy.get("#booking_id").should("have.value", "1001"); |
390 |
|
391 |
// Change pickup location |
392 |
cy.get("#pickup_library_id").select("2"); |
393 |
|
394 |
// Submit the form |
395 |
cy.get("#placeBookingForm").submit(); |
396 |
cy.wait("@updateBooking"); |
397 |
|
398 |
// Check success message |
399 |
cy.get("#transient_result").should( |
400 |
"contain", |
401 |
"Booking successfully updated" |
402 |
); |
403 |
|
404 |
// Check modal closes |
405 |
cy.get("#placeBookingModal").should("not.be.visible"); |
406 |
}); |
407 |
|
408 |
it("should handle booking failure gracefully", () => { |
409 |
// Override the create booking intercept to return an error |
410 |
cy.intercept("POST", "/api/v1/bookings", { |
411 |
statusCode: 400, |
412 |
body: { |
413 |
error: "Booking failed", |
414 |
}, |
415 |
}).as("failedBooking"); |
416 |
|
417 |
// Fill out the booking form |
418 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
419 |
cy.wait("@getPickupLocations"); |
420 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
421 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
422 |
cy.wait("@getCirculationRules"); |
423 |
|
424 |
// Set dates with flatpickr |
425 |
cy.window().then(win => { |
426 |
const picker = win.document.getElementById("period")._flatpickr; |
427 |
const startDate = new Date(testData.startDate); |
428 |
const endDate = new Date(testData.endDate); |
429 |
picker.setDate([startDate, endDate], true); |
430 |
}); |
431 |
|
432 |
// Submit the form |
433 |
cy.get("#placeBookingForm").submit(); |
434 |
cy.wait("@failedBooking"); |
435 |
|
436 |
// Check error message |
437 |
cy.get("#booking_result").should("contain", "Failure"); |
438 |
|
439 |
// Modal should remain open |
440 |
cy.get("#placeBookingModal").should("be.visible"); |
441 |
}); |
442 |
|
443 |
it("should reset form when modal is closed", () => { |
444 |
// Fill out some form fields |
445 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
446 |
cy.wait("@getPickupLocations"); |
447 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
448 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
449 |
cy.wait("@getCirculationRules"); |
450 |
|
451 |
// Close the modal |
452 |
cy.get('#placeBookingModal button[data-bs-dismiss="modal"]') |
453 |
.first() |
454 |
.click(); |
455 |
|
456 |
// Re-open the modal |
457 |
cy.get("#placebooking").click(); |
458 |
|
459 |
// Check fields are reset |
460 |
cy.get("#booking_patron_id").should("have.value", null); |
461 |
cy.get("#pickup_library_id").should("be.disabled"); |
462 |
cy.get("#booking_itemtype").should("be.disabled"); |
463 |
cy.get("#booking_item_id").should("be.disabled"); |
464 |
cy.get("#period").should("be.disabled"); |
465 |
cy.get("#booking_start_date").should("have.value", ""); |
466 |
cy.get("#booking_end_date").should("have.value", ""); |
467 |
cy.get("#booking_id").should("have.value", ""); |
468 |
}); |
469 |
}); |