From a40c8e2aa355f841dde1397d126cf67aac367b53 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Fri, 16 May 2025 11:36:47 +0100 Subject: [PATCH] Bug 39916: WIP Cypress tests for the bookings modal --- .../integration/Biblio/bookingsModal_spec.ts | 359 ++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 t/cypress/integration/Biblio/bookingsModal_spec.ts diff --git a/t/cypress/integration/Biblio/bookingsModal_spec.ts b/t/cypress/integration/Biblio/bookingsModal_spec.ts new file mode 100644 index 00000000000..57e0ecadb99 --- /dev/null +++ b/t/cypress/integration/Biblio/bookingsModal_spec.ts @@ -0,0 +1,359 @@ +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 should be enabled + cy.get("#pickup_library_id").should("not.be.disabled"); + + // Select pickup location + cy.get("#pickup_library_id").select(testData.pickupLibraryId); + cy.wait("@getCirculationRules"); + + // After pickup location selection, item type should be enabled + cy.get("#booking_itemtype").should("not.be.disabled"); + + // Select item type + cy.get("#booking_itemtype").select(testData.itemTypeId); + + // After item type selection, item should be enabled + cy.get("#booking_item_id").should("not.be.disabled"); + + // Select item + cy.get("#booking_item_id").select(testData.itemNumber); + + // After all selections, date picker should be enabled + cy.get("#period").should("not.be.disabled"); + }); + + it("should handle item type and item dependencies correctly", () => { + // Select patron and pickup location first + cy.select2Select("booking_patron_id", "John", { index: 0 }); + cy.wait("@getPickupLocations"); + cy.get("#pickup_library_id").select(testData.pickupLibraryId); + cy.wait("@getCirculationRules"); + + // Select an item first + cy.get("#booking_item_id").select(testData.itemNumber); + + // 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.close").click(); + cy.get( + 'button[data-biblionumber="' + testData.biblionumber + '"]' + ).click(); + + // Now select patron, pickup and item type first + 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); + + // Verify that only 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); + $option.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