Bugzilla – Attachment 182546 Details for
Bug 39916
The 'Place booking' modal should have cypress tests
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 39916: WIP Cypress tests for the bookings modal
Bug-39916-WIP-Cypress-tests-for-the-bookings-modal.patch (text/plain), 22.58 KB, created by
Martin Renvoize (ashimema)
on 2025-05-16 16:08:57 UTC
(
hide
)
Description:
Bug 39916: WIP Cypress tests for the bookings modal
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2025-05-16 16:08:57 UTC
Size:
22.58 KB
patch
obsolete
>From 272db7ac20067b702aa78e43fd35ecb683488f6f Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Fri, 16 May 2025 11:36:47 +0100 >Subject: [PATCH] Bug 39916: WIP Cypress tests for the bookings modal > >Lots of passing tests :) >--- > .../prog/js/modals/place_booking.js | 84 ++-- > .../integration/Biblio/bookingsModal_spec.ts | 374 ++++++++++++++++++ > 2 files changed, 416 insertions(+), 42 deletions(-) > create mode 100644 t/cypress/integration/Biblio/bookingsModal_spec.ts > >diff --git a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >index 634cce0c36e..8c579b7a4fe 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >@@ -93,29 +93,29 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > let $patron = $("<span></span>") > .append( > "" + >- (patron.surname >- ? escape_str(patron.surname) + ", " >- : "") + >- (patron.firstname >- ? escape_str(patron.firstname) + " " >- : "") + >- (patron.cardnumber >- ? " (" + escape_str(patron.cardnumber) + ")" >- : "") + >- "<small>" + >- (patron.date_of_birth >- ? ' <span class="age_years">' + >- $get_age(patron.date_of_birth) + >- " " + >- __("years") + >- "</span>" >- : "") + >- (patron.library >- ? ' <span class="ac-library">' + >- escape_str(patron.library.name) + >- "</span>" >- : "") + >- "</small>" >+ (patron.surname >+ ? escape_str(patron.surname) + ", " >+ : "") + >+ (patron.firstname >+ ? escape_str(patron.firstname) + " " >+ : "") + >+ (patron.cardnumber >+ ? " (" + escape_str(patron.cardnumber) + ")" >+ : "") + >+ "<small>" + >+ (patron.date_of_birth >+ ? ' <span class="age_years">' + >+ $get_age(patron.date_of_birth) + >+ " " + >+ __("years") + >+ "</span>" >+ : "") + >+ (patron.library >+ ? ' <span class="ac-library">' + >+ escape_str(patron.library.name) + >+ "</span>" >+ : "") + >+ "</small>" > ) > .addClass(loggedInClass); > return $patron; >@@ -211,10 +211,10 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > ) { > let option = $( > '<option value="' + >- pickup_location.library_id + >- '">' + >- pickup_location.name + >- "</option>" >+ pickup_location.library_id + >+ '">' + >+ pickup_location.name + >+ "</option>" > ); > > option.attr( >@@ -413,8 +413,8 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > if ( > !$("#booking_itemtype").find( > "option[value='" + >- item.item_type.item_type_id + >- "']" >+ item.item_type.item_type_id + >+ "']" > ).length > ) { > // Create a DOM Option and de-select by default >@@ -752,7 +752,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > for (let i = 0; i < renewalsAllowed; i++) { > nextDate.setDate( > nextDate.getDate() + >- parseInt(renewalLength) >+ parseInt(renewalLength) > ); > boldDates.push(new Date(nextDate)); > } >@@ -932,8 +932,8 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > ); > const startDate = periodPicker.selectedDates[0] > ? dayjs(periodPicker.selectedDates[0]).startOf( >- "day" >- ) >+ "day" >+ ) > : null; > > const leadStart = startDate >@@ -959,7 +959,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > dayElem.classList.toggle( > "leadRange", > elemDate.isSameOrAfter(leadStart) && >- elemDate.isBefore(leadEnd) >+ elemDate.isBefore(leadEnd) > ); > dayElem.classList.toggle( > "leadRangeEnd", >@@ -972,7 +972,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > dayElem.classList.toggle( > "trailRange", > elemDate.isAfter(trailStart) && >- elemDate.isSameOrBefore(trailEnd) >+ elemDate.isSameOrBefore(trailEnd) > ); > dayElem.classList.toggle( > "trailRangeEnd", >@@ -1195,8 +1195,8 @@ $("#placeBookingForm").on("submit", function (e) { > // Set feedback > $("#transient_result").replaceWith( > '<div id="transient_result" class="alert alert-info">' + >- __("Booking successfully placed") + >- "</div>" >+ __("Booking successfully placed") + >+ "</div>" > ); > > // Close modal >@@ -1206,8 +1206,8 @@ $("#placeBookingForm").on("submit", function (e) { > posting.fail(function (data) { > $("#booking_result").replaceWith( > '<div id="booking_result" class="alert alert-danger">' + >- __("Failure") + >- "</div>" >+ __("Failure") + >+ "</div>" > ); > }); > } else { >@@ -1264,8 +1264,8 @@ $("#placeBookingForm").on("submit", function (e) { > // Set feedback > $("#transient_result").replaceWith( > '<div id="transient_result" class="alert alert-info">' + >- __("Booking successfully updated") + >- "</div>" >+ __("Booking successfully updated") + >+ "</div>" > ); > > // Close modal >@@ -1275,8 +1275,8 @@ $("#placeBookingForm").on("submit", function (e) { > putting.fail(function (data) { > $("#booking_result").replaceWith( > '<div id="booking_result" class="alert alert-danger">' + >- __("Failure") + >- "</div>" >+ __("Failure") + >+ "</div>" > ); > }); > } >diff --git a/t/cypress/integration/Biblio/bookingsModal_spec.ts b/t/cypress/integration/Biblio/bookingsModal_spec.ts >new file mode 100644 >index 00000000000..9f9eaa67992 >--- /dev/null >+++ b/t/cypress/integration/Biblio/bookingsModal_spec.ts >@@ -0,0 +1,374 @@ >+describe("Booking Modal Tests", () => { >+ // Test data setup >+ const testData = { >+ biblionumber: "134", >+ patronId: "19", >+ pickupLibraryId: "CPL", >+ itemNumber: "287", >+ itemTypeId: "BK", >+ startDate: new Date(), >+ endDate: new Date(new Date().setDate(new Date().getDate() + 7)), // 7 days from now >+ }; >+ >+ beforeEach(() => { >+ cy.login(); >+ cy.title().should("eq", "Koha staff interface"); >+ >+ // Visit the page with the booking modal >+ cy.visit( >+ "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + >+ testData.biblionumber >+ ); >+ >+ // Intercept API calls and provide mock responses >+ cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1&_per_page=-1", { >+ fixture: "bookings/bookable_items.json", >+ }).as("getBookableItems"); >+ >+ cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { >+ fixture: "bookings/bookings.json", >+ }).as("getBookings"); >+ >+ cy.intercept("GET", "/api/v1/biblios/*/checkouts?_per_page=-1", { >+ fixture: "bookings/checkouts.json", >+ }).as("getCheckouts"); >+ >+ cy.intercept("GET", "/api/v1/patrons/*", { >+ fixture: "bookings/patron.json", >+ }).as("getPatron"); >+ >+ cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { >+ fixture: "bookings/pickup_locations.json", >+ }).as("getPickupLocations"); >+ >+ cy.intercept("GET", "/api/v1/circulation_rules*", { >+ fixture: "bookings/circulation_rules.json", >+ }).as("getCirculationRules"); >+ >+ cy.intercept("POST", "/api/v1/bookings", { >+ statusCode: 201, >+ body: { >+ booking_id: "1001", >+ start_date: testData.startDate.toISOString(), >+ end_date: testData.endDate.toISOString(), >+ pickup_library_id: testData.pickupLibraryId, >+ biblio_id: testData.biblionumber, >+ item_id: testData.itemNumber, >+ patron_id: testData.patronId, >+ }, >+ }).as("createBooking"); >+ >+ cy.intercept("PUT", "/api/v1/bookings/*", { >+ statusCode: 200, >+ body: { >+ booking_id: "1001", >+ start_date: testData.startDate.toISOString(), >+ end_date: testData.endDate.toISOString(), >+ pickup_library_id: testData.pickupLibraryId, >+ biblio_id: testData.biblionumber, >+ item_id: testData.itemNumber, >+ patron_id: testData.patronId, >+ }, >+ }).as("updateBooking"); >+ >+ // Populate the select2 search results for patron search >+ cy.intercept("GET", "/api/v1/patrons*", { >+ body: [ >+ { >+ patron_id: testData.patronId, >+ surname: "Doe", >+ firstname: "John", >+ cardnumber: "12345", >+ library_id: "1", >+ library: { >+ name: "Main Library", >+ }, >+ category_id: "1", >+ date_of_birth: "1990-01-01", >+ }, >+ ], >+ pagination: { more: false }, >+ }).as("searchPatrons"); >+ >+ // Add clickable button >+ cy.document().then(doc => { >+ const button = doc.createElement("button"); >+ button.setAttribute("data-bs-toggle", "modal"); >+ button.setAttribute("data-bs-target", "#placeBookingModal"); >+ button.setAttribute("data-biblionumber", testData.biblionumber); >+ button.setAttribute("id", "placebooking"); >+ doc.body.appendChild(button); >+ }); >+ >+ // Open the booking modal >+ cy.get("#placebooking").click(); >+ }); >+ >+ it("should load the booking modal correctly", () => { >+ // Check modal title >+ cy.get("#placeBookingLabel").should("contain", "Place booking"); >+ >+ // Check form elements are present >+ cy.get("#booking_patron_id").should("exist"); >+ cy.get("#pickup_library_id").should("exist"); >+ cy.get("#booking_itemtype").should("exist"); >+ cy.get("#booking_item_id").should("exist"); >+ cy.get("#period").should("exist"); >+ >+ // Check hidden fields >+ cy.get("#booking_biblio_id").should( >+ "have.value", >+ testData.biblionumber >+ ); >+ cy.get("#booking_start_date").should("have.value", ""); >+ cy.get("#booking_end_date").should("have.value", ""); >+ }); >+ >+ it("should enable fields in proper sequence", () => { >+ // Initially only patron field should be enabled >+ cy.get("#booking_patron_id").should("not.be.disabled"); >+ cy.get("#pickup_library_id").should("be.disabled"); >+ cy.get("#booking_itemtype").should("be.disabled"); >+ cy.get("#booking_item_id").should("be.disabled"); >+ cy.get("#period").should("be.disabled"); >+ >+ // Select patron >+ cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); >+ /* >+ cy.getSelect2("#booking_patron_id") >+ .select2({ search: "John" }) >+ .select2({ selectIndex: 0 }); >+ */ >+ cy.wait("@getPickupLocations"); >+ >+ // After patron selection, pickup location, item type and item should be enabled >+ cy.get("#pickup_library_id").should("not.be.disabled"); >+ cy.get("#booking_itemtype").should("not.be.disabled"); >+ cy.get("#booking_item_id").should("not.be.disabled"); >+ cy.get("#period").should("be.disabled"); >+ >+ // Select pickup location >+ cy.selectFromSelect2ByIndex("#pickup_library_id", 0); >+ >+ // Select item type, trigger circulation rules >+ cy.selectFromSelect2ByIndex("#booking_itemtype", 0); >+ cy.wait("@getCirculationRules"); >+ >+ // After patron, pickup location and itemtype/item selection, date picker should be enabled >+ cy.get("#period").should("not.be.disabled"); >+ >+ // Clear item type and confirm period is disabled >+ cy.clearSelect2("#booking_itemtype"); >+ cy.get("#period").should("be.disabled"); >+ >+ // Select item, re-enable period >+ cy.selectFromSelect2ByIndex("#booking_item_id", 1); >+ cy.get("#period").should("not.be.disabled"); >+ }); >+ >+ it("should handle item type and item dependencies correctly", () => { >+ // Select patron and pickup location first >+ cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); >+ cy.wait("@getPickupLocations"); >+ cy.selectFromSelect2ByIndex("#pickup_library_id", 0); >+ >+ // Select an item first >+ cy.selectFromSelect2ByIndex("#booking_item_id", 1); >+ cy.wait("@getCirculationRules"); >+ >+ // Verify that item type gets selected automatically >+ cy.get("#booking_itemtype").should("have.value", testData.itemTypeId); >+ >+ // Verify that item type gets disabled >+ cy.get("#booking_itemtype").should("be.disabled"); >+ >+ // Reset the modal >+ cy.get('#placeBookingModal button[data-bs-dismiss="modal"]') >+ .first() >+ .click(); >+ cy.get("#placebooking").click(); >+ >+ // Now select patron, pickup and item type first >+ cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John"); >+ cy.wait("@getPickupLocations"); >+ cy.selectFromSelect2ByIndex("#pickup_library_id", 0); >+ cy.selectFromSelect2ByIndex("#booking_itemtype", 0); >+ cy.wait("@getCirculationRules"); >+ cy.wait(300); >+ >+ // Verify that only 'Any item' option and items of selected type are enabled >+ cy.get("#booking_item_id > option").then($options => { >+ const enabledOptions = $options.filter(":not(:disabled)"); >+ enabledOptions.each(function () { >+ const $option = cy.wrap(this); >+ >+ // Get both the value and the data-itemtype attribute to make decisions >+ $option.invoke("val").then(value => { >+ if (value === "0") { >+ // We need to re-wrap the element since invoke('val') changed the subject >+ cy.wrap(this).should("contain.text", "Any item"); >+ } else { >+ // Re-wrap the element again for this assertion >+ cy.wrap(this).should( >+ "have.attr", >+ "data-itemtype", >+ testData.itemTypeId >+ ); >+ } >+ }); >+ }); >+ }); >+ }); >+ >+ it("should handle date selection and availability", () => { >+ // Select patron, pickup location, item type, and item >+ cy.select2Select("booking_patron_id", "John", { index: 0 }); >+ cy.wait("@getPickupLocations"); >+ cy.get("#pickup_library_id").select(testData.pickupLibraryId); >+ cy.wait("@getCirculationRules"); >+ cy.get("#booking_itemtype").select(testData.itemTypeId); >+ cy.get("#booking_item_id").select(testData.itemNumber); >+ >+ // Get the flatpickr instance and select dates >+ cy.window().then(win => { >+ const picker = win.document.getElementById("period")._flatpickr; >+ const startDate = new Date(testData.startDate); >+ const endDate = new Date(testData.endDate); >+ >+ picker.setDate([startDate, endDate], true); >+ >+ // Verify the hidden fields are set correctly >+ cy.get("#booking_start_date").should("not.have.value", ""); >+ cy.get("#booking_end_date").should("not.have.value", ""); >+ >+ // Check for disabled dates due to other bookings >+ cy.get(".flatpickr-day.flatpickr-disabled").should("exist"); >+ >+ // Check for event dots indicating existing bookings >+ cy.get(".flatpickr-day .event-dots").should("exist"); >+ }); >+ }); >+ >+ it("should submit a new booking successfully", () => { >+ // Fill out the booking form >+ cy.select2Select("booking_patron_id", "John", { index: 0 }); >+ cy.wait("@getPickupLocations"); >+ cy.get("#pickup_library_id").select(testData.pickupLibraryId); >+ cy.wait("@getCirculationRules"); >+ cy.get("#booking_itemtype").select(testData.itemTypeId); >+ cy.get("#booking_item_id").select(testData.itemNumber); >+ >+ // Set dates with flatpickr >+ cy.window().then(win => { >+ const picker = win.document.getElementById("period")._flatpickr; >+ const startDate = new Date(testData.startDate); >+ const endDate = new Date(testData.endDate); >+ picker.setDate([startDate, endDate], true); >+ }); >+ >+ // Submit the form >+ cy.get("#placeBookingForm").submit(); >+ cy.wait("@createBooking"); >+ >+ // Check success message >+ cy.get("#transient_result").should( >+ "contain", >+ "Booking successfully placed" >+ ); >+ >+ // Check modal closes >+ cy.get("#placeBookingModal").should("not.be.visible"); >+ }); >+ >+ it("should edit an existing booking successfully", () => { >+ // Open edit booking modal >+ cy.get("#test-modal-btn") >+ .invoke("attr", "data-booking", "1001") >+ .click(); >+ >+ // Check modal title for edit >+ cy.get("#placeBookingLabel").should("contain", "Edit booking"); >+ >+ // Verify booking ID is set >+ cy.get("#booking_id").should("have.value", "1001"); >+ >+ // Change pickup location >+ cy.get("#pickup_library_id").select("2"); >+ >+ // Submit the form >+ cy.get("#placeBookingForm").submit(); >+ cy.wait("@updateBooking"); >+ >+ // Check success message >+ cy.get("#transient_result").should( >+ "contain", >+ "Booking successfully updated" >+ ); >+ >+ // Check modal closes >+ cy.get("#placeBookingModal").should("not.be.visible"); >+ }); >+ >+ it("should handle booking failure gracefully", () => { >+ // Override the create booking intercept to return an error >+ cy.intercept("POST", "/api/v1/bookings", { >+ statusCode: 400, >+ body: { >+ error: "Booking failed", >+ }, >+ }).as("failedBooking"); >+ >+ // Fill out the booking form >+ cy.select2Select("booking_patron_id", "John", { index: 0 }); >+ cy.wait("@getPickupLocations"); >+ cy.get("#pickup_library_id").select(testData.pickupLibraryId); >+ cy.wait("@getCirculationRules"); >+ cy.get("#booking_itemtype").select(testData.itemTypeId); >+ cy.get("#booking_item_id").select(testData.itemNumber); >+ >+ // Set dates with flatpickr >+ cy.window().then(win => { >+ const picker = win.document.getElementById("period")._flatpickr; >+ const startDate = new Date(testData.startDate); >+ const endDate = new Date(testData.endDate); >+ picker.setDate([startDate, endDate], true); >+ }); >+ >+ // Submit the form >+ cy.get("#placeBookingForm").submit(); >+ cy.wait("@failedBooking"); >+ >+ // Check error message >+ cy.get("#booking_result").should("contain", "Failure"); >+ >+ // Modal should remain open >+ cy.get("#placeBookingModal").should("be.visible"); >+ }); >+ >+ it("should reset form when modal is closed", () => { >+ // Fill out some form fields >+ cy.getSelect2("#booking_patron_id") >+ .select2({ search: "John" }) >+ .select2({ selectIndex: 0 }); >+ cy.wait("@getPickupLocations"); >+ cy.get("#pickup_library_id").select(testData.pickupLibraryId); >+ >+ // Close the modal >+ cy.get("#placeBookingModal .modal-header .close").click(); >+ >+ // Re-open the modal >+ cy.get( >+ 'button[data-biblionumber="' + testData.biblionumber + '"]' >+ ).click(); >+ >+ // Check fields are reset >+ cy.get("#booking_patron_id").should("have.value", ""); >+ cy.get("#pickup_library_id").should("be.disabled"); >+ cy.get("#booking_itemtype").should("be.disabled"); >+ cy.get("#booking_item_id").should("be.disabled"); >+ cy.get("#period").should("be.disabled"); >+ cy.get("#booking_start_date").should("have.value", ""); >+ cy.get("#booking_end_date").should("have.value", ""); >+ cy.get("#booking_id").should("have.value", ""); >+ }); >+}); >-- >2.49.0
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 39916
:
182515
|
182516
|
182545
|
182546
|
182635
|
182636
|
182637
|
182771
|
182772
|
182773
|
183206
|
183207
|
183208
|
183209
|
183210