|
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 |
//cy.getSelect2("#booking_patron_id") |
| 138 |
// .select2({ search: "John" }) |
| 139 |
// .select2({ selectIndex: 0 }); |
| 140 |
cy.wait("@getPickupLocations"); |
| 141 |
|
| 142 |
// After patron selection, pickup location should be enabled |
| 143 |
cy.get("#pickup_library_id").should("not.be.disabled"); |
| 144 |
|
| 145 |
// Select pickup location |
| 146 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
| 147 |
cy.wait("@getCirculationRules"); |
| 148 |
|
| 149 |
// After pickup location selection, item type should be enabled |
| 150 |
cy.get("#booking_itemtype").should("not.be.disabled"); |
| 151 |
|
| 152 |
// Select item type |
| 153 |
cy.get("#booking_itemtype").select(testData.itemTypeId); |
| 154 |
|
| 155 |
// After item type selection, item should be enabled |
| 156 |
cy.get("#booking_item_id").should("not.be.disabled"); |
| 157 |
|
| 158 |
// Select item |
| 159 |
cy.get("#booking_item_id").select(testData.itemNumber); |
| 160 |
|
| 161 |
// After all selections, date picker should be enabled |
| 162 |
cy.get("#period").should("not.be.disabled"); |
| 163 |
}); |
| 164 |
|
| 165 |
it("should handle item type and item dependencies correctly", () => { |
| 166 |
// Select patron and pickup location first |
| 167 |
cy.select2Select("booking_patron_id", "John", { index: 0 }); |
| 168 |
cy.wait("@getPickupLocations"); |
| 169 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
| 170 |
cy.wait("@getCirculationRules"); |
| 171 |
|
| 172 |
// Select an item first |
| 173 |
cy.get("#booking_item_id").select(testData.itemNumber); |
| 174 |
|
| 175 |
// Verify that item type gets selected automatically |
| 176 |
cy.get("#booking_itemtype").should("have.value", testData.itemTypeId); |
| 177 |
|
| 178 |
// Verify that item type gets disabled |
| 179 |
cy.get("#booking_itemtype").should("be.disabled"); |
| 180 |
|
| 181 |
// Reset the modal |
| 182 |
cy.get("#placeBookingModal button.close").click(); |
| 183 |
cy.get( |
| 184 |
'button[data-biblionumber="' + testData.biblionumber + '"]' |
| 185 |
).click(); |
| 186 |
|
| 187 |
// Now select patron, pickup and item type first |
| 188 |
cy.select2Select("booking_patron_id", "John", { index: 0 }); |
| 189 |
cy.wait("@getPickupLocations"); |
| 190 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
| 191 |
cy.wait("@getCirculationRules"); |
| 192 |
cy.get("#booking_itemtype").select(testData.itemTypeId); |
| 193 |
|
| 194 |
// Verify that only items of selected type are enabled |
| 195 |
cy.get("#booking_item_id option").then($options => { |
| 196 |
const enabledOptions = $options.filter(":not(:disabled)"); |
| 197 |
enabledOptions.each(function () { |
| 198 |
const $option = cy.wrap(this); |
| 199 |
$option.should( |
| 200 |
"have.attr", |
| 201 |
"data-itemtype", |
| 202 |
testData.itemTypeId |
| 203 |
); |
| 204 |
}); |
| 205 |
}); |
| 206 |
}); |
| 207 |
|
| 208 |
it("should handle date selection and availability", () => { |
| 209 |
// Select patron, pickup location, item type, and item |
| 210 |
cy.select2Select("booking_patron_id", "John", { index: 0 }); |
| 211 |
cy.wait("@getPickupLocations"); |
| 212 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
| 213 |
cy.wait("@getCirculationRules"); |
| 214 |
cy.get("#booking_itemtype").select(testData.itemTypeId); |
| 215 |
cy.get("#booking_item_id").select(testData.itemNumber); |
| 216 |
|
| 217 |
// Get the flatpickr instance and select dates |
| 218 |
cy.window().then(win => { |
| 219 |
const picker = win.document.getElementById("period")._flatpickr; |
| 220 |
const startDate = new Date(testData.startDate); |
| 221 |
const endDate = new Date(testData.endDate); |
| 222 |
|
| 223 |
picker.setDate([startDate, endDate], true); |
| 224 |
|
| 225 |
// Verify the hidden fields are set correctly |
| 226 |
cy.get("#booking_start_date").should("not.have.value", ""); |
| 227 |
cy.get("#booking_end_date").should("not.have.value", ""); |
| 228 |
|
| 229 |
// Check for disabled dates due to other bookings |
| 230 |
cy.get(".flatpickr-day.flatpickr-disabled").should("exist"); |
| 231 |
|
| 232 |
// Check for event dots indicating existing bookings |
| 233 |
cy.get(".flatpickr-day .event-dots").should("exist"); |
| 234 |
}); |
| 235 |
}); |
| 236 |
|
| 237 |
it("should submit a new booking successfully", () => { |
| 238 |
// Fill out the booking form |
| 239 |
cy.select2Select("booking_patron_id", "John", { index: 0 }); |
| 240 |
cy.wait("@getPickupLocations"); |
| 241 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
| 242 |
cy.wait("@getCirculationRules"); |
| 243 |
cy.get("#booking_itemtype").select(testData.itemTypeId); |
| 244 |
cy.get("#booking_item_id").select(testData.itemNumber); |
| 245 |
|
| 246 |
// Set dates with flatpickr |
| 247 |
cy.window().then(win => { |
| 248 |
const picker = win.document.getElementById("period")._flatpickr; |
| 249 |
const startDate = new Date(testData.startDate); |
| 250 |
const endDate = new Date(testData.endDate); |
| 251 |
picker.setDate([startDate, endDate], true); |
| 252 |
}); |
| 253 |
|
| 254 |
// Submit the form |
| 255 |
cy.get("#placeBookingForm").submit(); |
| 256 |
cy.wait("@createBooking"); |
| 257 |
|
| 258 |
// Check success message |
| 259 |
cy.get("#transient_result").should( |
| 260 |
"contain", |
| 261 |
"Booking successfully placed" |
| 262 |
); |
| 263 |
|
| 264 |
// Check modal closes |
| 265 |
cy.get("#placeBookingModal").should("not.be.visible"); |
| 266 |
}); |
| 267 |
|
| 268 |
it("should edit an existing booking successfully", () => { |
| 269 |
// Open edit booking modal |
| 270 |
cy.get("#test-modal-btn") |
| 271 |
.invoke("attr", "data-booking", "1001") |
| 272 |
.click(); |
| 273 |
|
| 274 |
// Check modal title for edit |
| 275 |
cy.get("#placeBookingLabel").should("contain", "Edit booking"); |
| 276 |
|
| 277 |
// Verify booking ID is set |
| 278 |
cy.get("#booking_id").should("have.value", "1001"); |
| 279 |
|
| 280 |
// Change pickup location |
| 281 |
cy.get("#pickup_library_id").select("2"); |
| 282 |
|
| 283 |
// Submit the form |
| 284 |
cy.get("#placeBookingForm").submit(); |
| 285 |
cy.wait("@updateBooking"); |
| 286 |
|
| 287 |
// Check success message |
| 288 |
cy.get("#transient_result").should( |
| 289 |
"contain", |
| 290 |
"Booking successfully updated" |
| 291 |
); |
| 292 |
|
| 293 |
// Check modal closes |
| 294 |
cy.get("#placeBookingModal").should("not.be.visible"); |
| 295 |
}); |
| 296 |
|
| 297 |
it("should handle booking failure gracefully", () => { |
| 298 |
// Override the create booking intercept to return an error |
| 299 |
cy.intercept("POST", "/api/v1/bookings", { |
| 300 |
statusCode: 400, |
| 301 |
body: { |
| 302 |
error: "Booking failed", |
| 303 |
}, |
| 304 |
}).as("failedBooking"); |
| 305 |
|
| 306 |
// Fill out the booking form |
| 307 |
cy.select2Select("booking_patron_id", "John", { index: 0 }); |
| 308 |
cy.wait("@getPickupLocations"); |
| 309 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
| 310 |
cy.wait("@getCirculationRules"); |
| 311 |
cy.get("#booking_itemtype").select(testData.itemTypeId); |
| 312 |
cy.get("#booking_item_id").select(testData.itemNumber); |
| 313 |
|
| 314 |
// Set dates with flatpickr |
| 315 |
cy.window().then(win => { |
| 316 |
const picker = win.document.getElementById("period")._flatpickr; |
| 317 |
const startDate = new Date(testData.startDate); |
| 318 |
const endDate = new Date(testData.endDate); |
| 319 |
picker.setDate([startDate, endDate], true); |
| 320 |
}); |
| 321 |
|
| 322 |
// Submit the form |
| 323 |
cy.get("#placeBookingForm").submit(); |
| 324 |
cy.wait("@failedBooking"); |
| 325 |
|
| 326 |
// Check error message |
| 327 |
cy.get("#booking_result").should("contain", "Failure"); |
| 328 |
|
| 329 |
// Modal should remain open |
| 330 |
cy.get("#placeBookingModal").should("be.visible"); |
| 331 |
}); |
| 332 |
|
| 333 |
it("should reset form when modal is closed", () => { |
| 334 |
// Fill out some form fields |
| 335 |
cy.getSelect2("#booking_patron_id") |
| 336 |
.select2({ search: "John" }) |
| 337 |
.select2({ selectIndex: 0 }); |
| 338 |
cy.wait("@getPickupLocations"); |
| 339 |
cy.get("#pickup_library_id").select(testData.pickupLibraryId); |
| 340 |
|
| 341 |
// Close the modal |
| 342 |
cy.get("#placeBookingModal .modal-header .close").click(); |
| 343 |
|
| 344 |
// Re-open the modal |
| 345 |
cy.get( |
| 346 |
'button[data-biblionumber="' + testData.biblionumber + '"]' |
| 347 |
).click(); |
| 348 |
|
| 349 |
// Check fields are reset |
| 350 |
cy.get("#booking_patron_id").should("have.value", ""); |
| 351 |
cy.get("#pickup_library_id").should("be.disabled"); |
| 352 |
cy.get("#booking_itemtype").should("be.disabled"); |
| 353 |
cy.get("#booking_item_id").should("be.disabled"); |
| 354 |
cy.get("#period").should("be.disabled"); |
| 355 |
cy.get("#booking_start_date").should("have.value", ""); |
| 356 |
cy.get("#booking_end_date").should("have.value", ""); |
| 357 |
cy.get("#booking_id").should("have.value", ""); |
| 358 |
}); |
| 359 |
}); |