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(), |
10 |
endDate: new Date(new Date().setDate(new Date().getDate() + 7)), // 7 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.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { |
29 |
fixture: "bookings/bookings.json", |
30 |
}).as("getBookings"); |
31 |
|
32 |
cy.intercept("GET", "/api/v1/biblios/*/checkouts?_per_page=-1", { |
33 |
fixture: "bookings/checkouts.json", |
34 |
}).as("getCheckouts"); |
35 |
|
36 |
cy.intercept("GET", "/api/v1/patrons/*", { |
37 |
fixture: "bookings/patron.json", |
38 |
}).as("getPatron"); |
39 |
|
40 |
cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { |
41 |
fixture: "bookings/pickup_locations.json", |
42 |
}).as("getPickupLocations"); |
43 |
|
44 |
cy.intercept("GET", "/api/v1/circulation_rules*", { |
45 |
fixture: "bookings/circulation_rules.json", |
46 |
}).as("getCirculationRules"); |
47 |
|
48 |
cy.intercept("POST", "/api/v1/bookings", { |
49 |
statusCode: 201, |
50 |
body: { |
51 |
booking_id: "1001", |
52 |
start_date: testData.startDate.toISOString(), |
53 |
end_date: testData.endDate.toISOString(), |
54 |
pickup_library_id: testData.pickupLibraryId, |
55 |
biblio_id: testData.biblionumber, |
56 |
item_id: testData.itemNumber, |
57 |
patron_id: testData.patronId, |
58 |
}, |
59 |
}).as("createBooking"); |
60 |
|
61 |
cy.intercept("PUT", "/api/v1/bookings/*", { |
62 |
statusCode: 200, |
63 |
body: { |
64 |
booking_id: "1001", |
65 |
start_date: testData.startDate.toISOString(), |
66 |
end_date: testData.endDate.toISOString(), |
67 |
pickup_library_id: testData.pickupLibraryId, |
68 |
biblio_id: testData.biblionumber, |
69 |
item_id: testData.itemNumber, |
70 |
patron_id: testData.patronId, |
71 |
}, |
72 |
}).as("updateBooking"); |
73 |
|
74 |
// Populate the select2 search results for patron search |
75 |
cy.intercept("GET", "/api/v1/patrons*", { |
76 |
body: [ |
77 |
{ |
78 |
patron_id: testData.patronId, |
79 |
surname: "Doe", |
80 |
firstname: "John", |
81 |
cardnumber: "12345", |
82 |
library_id: "1", |
83 |
library: { |
84 |
name: "Main Library", |
85 |
}, |
86 |
category_id: "1", |
87 |
date_of_birth: "1990-01-01", |
88 |
}, |
89 |
], |
90 |
pagination: { more: false }, |
91 |
}).as("searchPatrons"); |
92 |
|
93 |
// Add clickable button |
94 |
cy.document().then(doc => { |
95 |
const button = doc.createElement("button"); |
96 |
button.setAttribute("data-bs-toggle", "modal"); |
97 |
button.setAttribute("data-bs-target", "#placeBookingModal"); |
98 |
button.setAttribute("data-biblionumber", testData.biblionumber); |
99 |
button.setAttribute("id", "placebooking"); |
100 |
doc.body.appendChild(button); |
101 |
}); |
102 |
|
103 |
// Open the booking modal |
104 |
cy.get("#placebooking").click(); |
105 |
}); |
106 |
|
107 |
it("should load the booking modal correctly", () => { |
108 |
// Check modal title |
109 |
cy.get("#placeBookingLabel").should("contain", "Place booking"); |
110 |
|
111 |
// Check form elements are present |
112 |
cy.get("#booking_patron_id").should("exist"); |
113 |
cy.get("#pickup_library_id").should("exist"); |
114 |
cy.get("#booking_itemtype").should("exist"); |
115 |
cy.get("#booking_item_id").should("exist"); |
116 |
cy.get("#period").should("exist"); |
117 |
|
118 |
// Check hidden fields |
119 |
cy.get("#booking_biblio_id").should( |
120 |
"have.value", |
121 |
testData.biblionumber |
122 |
); |
123 |
cy.get("#booking_start_date").should("have.value", ""); |
124 |
cy.get("#booking_end_date").should("have.value", ""); |
125 |
}); |
126 |
|
127 |
it("should enable fields in proper sequence", () => { |
128 |
// Initially only patron field should be enabled |
129 |
cy.get("#booking_patron_id").should("not.be.disabled"); |
130 |
cy.get("#pickup_library_id").should("be.disabled"); |
131 |
cy.get("#booking_itemtype").should("be.disabled"); |
132 |
cy.get("#booking_item_id").should("be.disabled"); |
133 |
cy.get("#period").should("be.disabled"); |
134 |
|
135 |
// Select patron |
136 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
137 |
/* |
138 |
cy.getSelect2("#booking_patron_id") |
139 |
.select2({ search: "John" }) |
140 |
.select2({ selectIndex: 0 }); |
141 |
*/ |
142 |
cy.wait("@getPickupLocations"); |
143 |
|
144 |
// After patron selection, pickup location, item type and item should be enabled |
145 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
146 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
147 |
cy.get("#booking_item_id").should("not.be.disabled"); |
148 |
cy.get("#period").should("be.disabled"); |
149 |
|
150 |
// Select pickup location |
151 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
152 |
|
153 |
// Select item type, trigger circulation rules |
154 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); |
155 |
cy.wait("@getCirculationRules"); |
156 |
|
157 |
// After patron, pickup location and itemtype/item selection, date picker should be enabled |
158 |
cy.get("#period").should("not.be.disabled"); |
159 |
|
160 |
// Clear item type and confirm period is disabled |
161 |
cy.clearSelect2("#booking_itemtype"); |
162 |
cy.get("#period").should("be.disabled"); |
163 |
|
164 |
// Select item, re-enable period |
165 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
166 |
cy.get("#period").should("not.be.disabled"); |
167 |
}); |
168 |
|
169 |
it("should handle item type and item dependencies correctly", () => { |
170 |
// Select patron and pickup location first |
171 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
172 |
cy.wait("@getPickupLocations"); |
173 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
174 |
|
175 |
// Select an item first |
176 |
cy.selectFromSelect2ByIndex("#booking_item_id", 1); |
177 |
cy.wait("@getCirculationRules"); |
178 |
|
179 |
// Verify that item type gets selected automatically |
180 |
cy.get("#booking_itemtype").should("have.value", testData.itemTypeId); |
181 |
|
182 |
// Verify that item type gets disabled |
183 |
cy.get("#booking_itemtype").should("be.disabled"); |
184 |
|
185 |
// Reset the modal |
186 |
cy.get('#placeBookingModal button[data-bs-dismiss="modal"]') |
187 |
.first() |
188 |
.click(); |
189 |
cy.get("#placebooking").click(); |
190 |
|
191 |
// Now select patron, pickup and item type first |
192 |
cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); |
193 |
cy.wait("@getPickupLocations"); |
194 |
cy.selectFromSelect2ByIndex("#pickup_library_id", 0); |
195 |
cy.selectFromSelect2ByIndex("#booking_itemtype", 0); |
196 |
cy.wait("@getCirculationRules"); |
197 |
cy.wait(300); |
198 |
|
199 |
// Verify that only 'Any item' option and items of selected type are enabled |
200 |
cy.get("#booking_item_id > option").then($options => { |
201 |
const enabledOptions = $options.filter(":not(:disabled)"); |
202 |
enabledOptions.each(function () { |
203 |
const $option = cy.wrap(this); |
204 |
|
205 |
// Get both the value and the data-itemtype attribute to make decisions |
206 |
$option.invoke("val").then(value => { |
207 |
if (value === "0") { |
208 |
// We need to re-wrap the element since invoke('val') changed the subject |
209 |
cy.wrap(this).should("contain.text", "Any item"); |
210 |
} else { |
211 |
// Re-wrap the element again for this assertion |
212 |
cy.wrap(this).should( |
213 |
"have.attr", |
214 |
"data-itemtype", |
215 |
testData.itemTypeId |
216 |
); |
217 |
} |
218 |
}); |
219 |
}); |
220 |
}); |
221 |
}); |
222 |
|
223 |
it("should handle date selection and availability", () => { |
224 |
// Select patron, pickup location, item type, and item |
225 |
cy.select2Select("booking_patron_id", "John", { index: 0 }); |
226 |
cy.wait("@getPickupLocations"); |
227 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
228 |
cy.wait("@getCirculationRules"); |
229 |
cy.get("#booking_itemtype").select(testData.itemTypeId); |
230 |
cy.get("#booking_item_id").select(testData.itemNumber); |
231 |
|
232 |
// Get the flatpickr instance and select dates |
233 |
cy.window().then(win => { |
234 |
const picker = win.document.getElementById("period")._flatpickr; |
235 |
const startDate = new Date(testData.startDate); |
236 |
const endDate = new Date(testData.endDate); |
237 |
|
238 |
picker.setDate([startDate, endDate], true); |
239 |
|
240 |
// Verify the hidden fields are set correctly |
241 |
cy.get("#booking_start_date").should("not.have.value", ""); |
242 |
cy.get("#booking_end_date").should("not.have.value", ""); |
243 |
|
244 |
// Check for disabled dates due to other bookings |
245 |
cy.get(".flatpickr-day.flatpickr-disabled").should("exist"); |
246 |
|
247 |
// Check for event dots indicating existing bookings |
248 |
cy.get(".flatpickr-day .event-dots").should("exist"); |
249 |
}); |
250 |
}); |
251 |
|
252 |
it("should submit a new booking successfully", () => { |
253 |
// Fill out the booking form |
254 |
cy.select2Select("booking_patron_id", "John", { index: 0 }); |
255 |
cy.wait("@getPickupLocations"); |
256 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
257 |
cy.wait("@getCirculationRules"); |
258 |
cy.get("#booking_itemtype").select(testData.itemTypeId); |
259 |
cy.get("#booking_item_id").select(testData.itemNumber); |
260 |
|
261 |
// Set dates with flatpickr |
262 |
cy.window().then(win => { |
263 |
const picker = win.document.getElementById("period")._flatpickr; |
264 |
const startDate = new Date(testData.startDate); |
265 |
const endDate = new Date(testData.endDate); |
266 |
picker.setDate([startDate, endDate], true); |
267 |
}); |
268 |
|
269 |
// Submit the form |
270 |
cy.get("#placeBookingForm").submit(); |
271 |
cy.wait("@createBooking"); |
272 |
|
273 |
// Check success message |
274 |
cy.get("#transient_result").should( |
275 |
"contain", |
276 |
"Booking successfully placed" |
277 |
); |
278 |
|
279 |
// Check modal closes |
280 |
cy.get("#placeBookingModal").should("not.be.visible"); |
281 |
}); |
282 |
|
283 |
it("should edit an existing booking successfully", () => { |
284 |
// Open edit booking modal |
285 |
cy.get("#test-modal-btn") |
286 |
.invoke("attr", "data-booking", "1001") |
287 |
.click(); |
288 |
|
289 |
// Check modal title for edit |
290 |
cy.get("#placeBookingLabel").should("contain", "Edit booking"); |
291 |
|
292 |
// Verify booking ID is set |
293 |
cy.get("#booking_id").should("have.value", "1001"); |
294 |
|
295 |
// Change pickup location |
296 |
cy.get("#pickup_library_id").select("2"); |
297 |
|
298 |
// Submit the form |
299 |
cy.get("#placeBookingForm").submit(); |
300 |
cy.wait("@updateBooking"); |
301 |
|
302 |
// Check success message |
303 |
cy.get("#transient_result").should( |
304 |
"contain", |
305 |
"Booking successfully updated" |
306 |
); |
307 |
|
308 |
// Check modal closes |
309 |
cy.get("#placeBookingModal").should("not.be.visible"); |
310 |
}); |
311 |
|
312 |
it("should handle booking failure gracefully", () => { |
313 |
// Override the create booking intercept to return an error |
314 |
cy.intercept("POST", "/api/v1/bookings", { |
315 |
statusCode: 400, |
316 |
body: { |
317 |
error: "Booking failed", |
318 |
}, |
319 |
}).as("failedBooking"); |
320 |
|
321 |
// Fill out the booking form |
322 |
cy.select2Select("booking_patron_id", "John", { index: 0 }); |
323 |
cy.wait("@getPickupLocations"); |
324 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
325 |
cy.wait("@getCirculationRules"); |
326 |
cy.get("#booking_itemtype").select(testData.itemTypeId); |
327 |
cy.get("#booking_item_id").select(testData.itemNumber); |
328 |
|
329 |
// Set dates with flatpickr |
330 |
cy.window().then(win => { |
331 |
const picker = win.document.getElementById("period")._flatpickr; |
332 |
const startDate = new Date(testData.startDate); |
333 |
const endDate = new Date(testData.endDate); |
334 |
picker.setDate([startDate, endDate], true); |
335 |
}); |
336 |
|
337 |
// Submit the form |
338 |
cy.get("#placeBookingForm").submit(); |
339 |
cy.wait("@failedBooking"); |
340 |
|
341 |
// Check error message |
342 |
cy.get("#booking_result").should("contain", "Failure"); |
343 |
|
344 |
// Modal should remain open |
345 |
cy.get("#placeBookingModal").should("be.visible"); |
346 |
}); |
347 |
|
348 |
it("should reset form when modal is closed", () => { |
349 |
// Fill out some form fields |
350 |
cy.getSelect2("#booking_patron_id") |
351 |
.select2({ search: "John" }) |
352 |
.select2({ selectIndex: 0 }); |
353 |
cy.wait("@getPickupLocations"); |
354 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
355 |
|
356 |
// Close the modal |
357 |
cy.get("#placeBookingModal .modal-header .close").click(); |
358 |
|
359 |
// Re-open the modal |
360 |
cy.get( |
361 |
'button[data-biblionumber="' + testData.biblionumber + '"]' |
362 |
).click(); |
363 |
|
364 |
// Check fields are reset |
365 |
cy.get("#booking_patron_id").should("have.value", ""); |
366 |
cy.get("#pickup_library_id").should("be.disabled"); |
367 |
cy.get("#booking_itemtype").should("be.disabled"); |
368 |
cy.get("#booking_item_id").should("be.disabled"); |
369 |
cy.get("#period").should("be.disabled"); |
370 |
cy.get("#booking_start_date").should("have.value", ""); |
371 |
cy.get("#booking_end_date").should("have.value", ""); |
372 |
cy.get("#booking_id").should("have.value", ""); |
373 |
}); |
374 |
}); |