Bugzilla – Attachment 183208 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: Cypress tests for the bookings modal
Bug-39916-Cypress-tests-for-the-bookings-modal.patch (text/plain), 47.85 KB, created by
Martin Renvoize (ashimema)
on 2025-06-12 14:51:47 UTC
(
hide
)
Description:
Bug 39916: Cypress tests for the bookings modal
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2025-06-12 14:51:47 UTC
Size:
47.85 KB
patch
obsolete
>From 258f6258fc044862d0757c2cd9dcd08722c1161b 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: Cypress tests for the bookings modal > >This adds comprehensive end-to-end tests for the booking modal functionality: > >- Modal loading and field validation >- Form field enabling sequence >- Item type and item selection dependencies >- Date picker validation and restrictions >- Booking conflict detection >- Lead/trail period handling >- Form submission (create and edit) >- Error handling and form reset > >Tests use optimized shared mock data generation with cy.task() for >realistic test data while minimizing memory usage. Tests cover >both positive and negative scenarios. >--- > .../integration/Biblio/bookingsModal_spec.ts | 1287 +++++++++++++++++ > 1 file changed, 1287 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..0fe872945d1 >--- /dev/null >+++ b/t/cypress/integration/Biblio/bookingsModal_spec.ts >@@ -0,0 +1,1287 @@ >+const dayjs = require("dayjs"); /* Cannot use our calendar JS code, it's in an include file (!) >+ Also note that moment.js is deprecated */ >+const isSameOrBefore = require("dayjs/plugin/isSameOrBefore"); >+dayjs.extend(isSameOrBefore); >+ >+describe("Booking Modal Tests", () => { >+ // Common test data used across all tests >+ const testData = { >+ biblionumber: 134, >+ patronId: 19, >+ pickupLibraryId: "CPL", >+ itemNumber: 287, >+ itemTypeId: "BK", >+ itemTypeDescription: "Book", >+ startDate: dayjs().add(1, "day").startOf("day").toDate(), >+ endDate: dayjs().add(5, "day").endOf("day").toDate(), >+ }; >+ >+ // Shared mock data - will be populated in before() hook >+ const sharedMockData = { >+ patrons: [], >+ items: [], >+ libraries: [], >+ bookings: [], >+ }; >+ >+ // Mock data generators using cy.task() pattern - optimized for minimal data >+ const generateMockPatrons = (count = 3, overridesArray = []) => { >+ let chain = cy.wrap([]); >+ >+ // Define minimal patron data for test scenarios >+ const defaultPatrons = [ >+ { >+ patron_id: testData.patronId, // "19" - main test patron >+ surname: "Doe", >+ firstname: "John", >+ cardnumber: "12345", >+ library_id: testData.pickupLibraryId, >+ category_id: "ADULT", >+ date_of_birth: "1990-01-01", >+ }, >+ { >+ patron_id: 456, // For edit test and booking scenarios >+ surname: "Smith", >+ firstname: "Jane", >+ cardnumber: "23456", >+ library_id: testData.pickupLibraryId, >+ category_id: "ADULT", >+ date_of_birth: "1985-05-15", >+ }, >+ { >+ patron_id: 457, // For booking scenarios >+ surname: "Johnson", >+ firstname: "Bob", >+ cardnumber: "34567", >+ library_id: testData.pickupLibraryId, >+ category_id: "ADULT", >+ date_of_birth: "1988-08-20", >+ }, >+ ]; >+ >+ for (let i = 0; i < count; i++) { >+ const values = { >+ ...defaultPatrons[i % defaultPatrons.length], // Cycle through defaults >+ ...(overridesArray[i] || {}), >+ }; >+ >+ chain = chain.then(patrons => { >+ return cy >+ .task("buildSampleObject", { >+ object: "patron", >+ values, >+ }) >+ .then(newPatron => { >+ return [...patrons, newPatron]; >+ }); >+ }); >+ } >+ >+ return chain; >+ }; >+ >+ const generateMockItems = (count = 3, overridesArray = []) => { >+ let chain = cy.wrap([]); >+ >+ for (let i = 0; i < count; i++) { >+ const values = { >+ effective_item_type_id: testData.itemTypeId, >+ bookable: 1, >+ ...(overridesArray[i] || {}), >+ }; >+ >+ chain = chain.then(items => { >+ return cy >+ .task("buildSampleObject", { >+ object: "item", >+ values, >+ }) >+ .then(newItem => { >+ return [...items, newItem]; >+ }); >+ }); >+ } >+ >+ return chain; >+ }; >+ >+ const generateMockLibraries = (count = 2, overridesArray = []) => { >+ let chain = cy.wrap([]); >+ >+ for (let i = 0; i < count; i++) { >+ const values = { >+ ...(overridesArray[i] || {}), >+ }; >+ >+ chain = chain.then(libraries => { >+ return cy >+ .task("buildSampleObject", { >+ object: "library", >+ values, >+ }) >+ .then(newLibrary => { >+ return [...libraries, newLibrary]; >+ }); >+ }); >+ } >+ >+ return chain; >+ }; >+ >+ const generateMockBookings = (count = 3, overridesArray = []) => { >+ let chain = cy.wrap([]); >+ >+ for (let i = 0; i < count; i++) { >+ const values = { >+ biblio_id: testData.biblionumber, >+ status: "pending", >+ ...(overridesArray[i] || {}), >+ }; >+ >+ chain = chain.then(bookings => { >+ return cy >+ .task("buildSampleObject", { >+ object: "booking", >+ values, >+ }) >+ .then(newBooking => { >+ return [...bookings, newBooking]; >+ }); >+ }); >+ } >+ >+ return chain; >+ }; >+ >+ // Helper functions for API intercepts using shared mock data >+ const setupStaticApiIntercepts = () => { >+ // Use shared mock data to set up intercepts >+ cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1&_per_page=-1", { >+ body: sharedMockData.items, >+ }).as("getBookableItems"); >+ >+ cy.intercept("GET", "/api/v1/biblios/*/checkouts?_per_page=-1", { >+ body: [], // No checkouts for simplicity >+ }).as("getCheckouts"); >+ >+ // Set up patron GET endpoint to return the appropriate patron by ID >+ cy.intercept("GET", "/api/v1/patrons/*", req => { >+ const patronId = req.url.split("/").pop(); >+ const patron = sharedMockData.patrons.find( >+ p => p.patron_id === patronId >+ ); >+ req.reply({ >+ body: patron || sharedMockData.patrons[0], // Fallback to first patron if not found >+ }); >+ }).as("getPatron"); >+ >+ // Mock patron search with all patrons >+ cy.intercept("GET", "/api/v1/patrons*", { >+ body: sharedMockData.patrons, >+ pagination: { more: false }, >+ }).as("searchPatrons"); >+ >+ cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { >+ body: sharedMockData.libraries, >+ }).as("getPickupLocations"); >+ >+ // Mock circulation rules >+ cy.intercept("GET", "/api/v1/circulation_rules*", { >+ body: [ >+ { >+ bookings_lead_period: 2, >+ bookings_trail_period: 1, >+ issuelength: 14, >+ renewalsallowed: 2, >+ renewalperiod: 7, >+ }, >+ ], >+ }).as("getCirculationRules"); >+ }; >+ >+ const setupBookingIntercepts = () => { >+ 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"); >+ }; >+ >+ const processDefaultBookings = (bookings: any[], today: any) => { >+ // Update the dates in the fixture data relative to today >+ bookings[0].start_date = today >+ .add(8, "day") >+ .startOf("day") >+ .toISOString(); // Today + 8 days at 00:00 >+ bookings[0].end_date = today.add(13, "day").endOf("day").toISOString(); // Today + 13 days at 23:59 >+ >+ bookings[1].start_date = today >+ .add(14, "day") >+ .startOf("day") >+ .toISOString(); // Today + 14 days at 00:00 >+ bookings[1].end_date = today.add(18, "day").endOf("day").toISOString(); // Today + 18 days at 23:59 >+ >+ bookings[2].start_date = today >+ .add(28, "day") >+ .startOf("day") >+ .toISOString(); // Today + 28 days at 00:00 >+ bookings[2].end_date = today.add(33, "day").endOf("day").toISOString(); // Today + 33 days at 23:59 >+ >+ return bookings; >+ }; >+ >+ const setupDynamicBookings = (customBookings?: any[]) => { >+ const today = dayjs(); >+ >+ if (customBookings) { >+ // Use provided bookings directly >+ cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { >+ body: customBookings, >+ }).as("getBookings"); >+ } else { >+ // Use shared mock bookings with dynamic dates >+ const processedBookings = processDefaultBookings( >+ sharedMockData.bookings, >+ today >+ ); >+ cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", { >+ body: processedBookings, >+ }).as("getBookings"); >+ } >+ }; >+ >+ const setupModalTriggerButton = () => { >+ 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); >+ }); >+ }; >+ >+ const setupCustomItems = ( >+ items: any[], >+ itemTypeId: string, >+ itemTypeDescription: string >+ ) => { >+ const itemPromises = items.map(item => { >+ return cy >+ .task("buildSampleObject", { >+ object: "item", >+ values: { >+ item_id: item.item_id, >+ external_id: item.barcode, >+ effective_item_type_id: itemTypeId, >+ bookable: 1, >+ location: "Main Library", >+ callnumber: "TEST.CALL.NUMBER", >+ status: "Available", >+ }, >+ }) >+ .then(mockItem => { >+ // Add item type info >+ mockItem.item_type = { >+ item_type_id: itemTypeId, >+ description: itemTypeDescription, >+ }; >+ return mockItem; >+ }); >+ }); >+ >+ // Wait for all items to be generated, then set up intercept >+ return cy.wrap(Promise.all(itemPromises)).then(mockItems => { >+ cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1*", { >+ body: mockItems, >+ }).as("getBookableItems"); >+ }); >+ }; >+ >+ const setupCustomPickupLocations = (items: any[]) => { >+ return generateMockLibraries(2, { >+ library_id: ["MAIN", "BRANCH"], >+ name: ["Main Library", "Branch Library"], >+ needs_override: [false, false], >+ }).then(mockLocations => { >+ // Add pickup items to each location >+ const modifiedLocations = mockLocations.map(location => ({ >+ ...location, >+ pickup_items: [ >+ ...items.map(item => parseInt(item.item_id, 10)), >+ ], >+ })); >+ >+ cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", { >+ body: modifiedLocations, >+ }).as("getPickupLocations"); >+ }); >+ }; >+ >+ // Common setup functions for test workflows >+ const openModalAndWait = ( >+ apiCalls: string[] = [ >+ "@getBookableItems", >+ "@getBookings", >+ "@getCheckouts", >+ ] >+ ) => { >+ cy.get("#placebooking").click(); >+ if (apiCalls.length > 0) { >+ cy.wait(apiCalls); >+ } >+ }; >+ >+ const setupModalForBasicTesting = () => { >+ openModalAndWait(); >+ // Basic setup - just open modal and wait for initial data >+ }; >+ >+ const setupModalForDateTesting = () => { >+ openModalAndWait(); >+ // Select patron, pickup location and item to enable date picker >+ selectPatron(0, "John"); >+ selectLocation(0); >+ selectItem(1); >+ }; >+ >+ const setupModalForFormSubmission = () => { >+ setupModalForDateTesting(); >+ // Set dates with flatpickr to prepare for submission >+ 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); >+ }); >+ }; >+ >+ const selectPatron = ( >+ patronIndex: number = 0, >+ patronSearchTerm: string = "John" >+ ) => { >+ cy.selectFromSelect2ByIndex( >+ "#booking_patron_id", >+ patronIndex, >+ patronSearchTerm >+ ); >+ // Wait for pickup locations API to complete >+ cy.wait("@getPickupLocations"); >+ }; >+ >+ const selectLocation = (locationIndex: number = 0) => { >+ // Ensure the field is enabled before attempting selection >+ cy.get("#pickup_library_id").should("not.be.disabled"); >+ cy.selectFromSelect2ByIndex("#pickup_library_id", locationIndex); >+ }; >+ >+ const selectItem = (itemIndex: number) => { >+ // Ensure the field is enabled before attempting selection >+ cy.get("#booking_item_id").should("not.be.disabled"); >+ cy.selectFromSelect2ByIndex("#booking_item_id", itemIndex); >+ // Wait for circulation rules API and period field to be enabled >+ cy.wait("@getCirculationRules"); >+ cy.get("#period").should("not.be.disabled"); >+ }; >+ >+ const selectItemType = (itemTypeIndex: number) => { >+ // Ensure the field is enabled before attempting selection >+ cy.get("#booking_itemtype").should("not.be.disabled"); >+ cy.selectFromSelect2ByIndex("#booking_itemtype", itemTypeIndex); >+ // Wait for circulation rules API and period field to be enabled >+ cy.wait("@getCirculationRules"); >+ cy.get("#period").should("not.be.disabled"); >+ }; >+ >+ const openModalWithoutWait = () => { >+ cy.get("#placebooking").click(); >+ // Only wait for modal to be visible, not for API calls >+ cy.get("#placeBookingModal").should("be.visible"); >+ }; >+ >+ const resetModalAndReopen = () => { >+ cy.get('#placeBookingModal button[data-bs-dismiss="modal"]') >+ .first() >+ .click(); >+ // Wait for modal to close >+ cy.get("#placeBookingModal").should("not.be.visible"); >+ // Reopen modal without waiting for API calls (they're cached) >+ openModalWithoutWait(); >+ }; >+ >+ // Generate shared mock data once before all tests >+ before(() => { >+ // Generate minimal shared mock data >+ generateMockPatrons(3).then(patrons => { >+ sharedMockData.patrons = patrons; >+ }); >+ >+ generateMockItems(3, [ >+ { >+ item_id: 789, >+ external_id: "BARCODE789", >+ effective_item_type_id: testData.itemTypeId, >+ item_type: { >+ item_type_id: testData.itemTypeId, >+ description: testData.itemTypeDescription, >+ }, >+ }, >+ { >+ item_id: 790, >+ external_id: "BARCODE790", >+ effective_item_type_id: testData.itemTypeId, >+ item_type: { >+ item_type_id: testData.itemTypeId, >+ description: testData.itemTypeDescription, >+ }, >+ }, >+ { >+ item_id: 791, >+ external_id: "BARCODE791", >+ effective_item_type_id: "DVD", >+ item_type: { >+ item_type_id: "DVD", >+ description: "DVD", >+ }, >+ }, >+ ]).then(items => { >+ sharedMockData.items = items; >+ }); >+ >+ generateMockLibraries(2, [ >+ { >+ library_id: "MAIN", >+ name: "Main Library", >+ pickup_items: [789, 790], >+ }, >+ { >+ library_id: "BRANCH", >+ name: "Branch Library", >+ pickup_items: [789, 791], >+ }, >+ ]).then(libraries => { >+ sharedMockData.libraries = libraries; >+ }); >+ >+ generateMockBookings(3, [ >+ { >+ booking_id: 1001, >+ patron_id: 456, >+ item_id: 789, >+ pickup_library_id: "MAIN", >+ }, >+ { >+ booking_id: 1002, >+ patron_id: 457, >+ item_id: 790, >+ pickup_library_id: "MAIN", >+ }, >+ { >+ booking_id: 1003, >+ patron_id: 456, >+ item_id: 791, >+ pickup_library_id: "MAIN", >+ }, >+ ]).then(bookings => { >+ sharedMockData.bookings = bookings; >+ }); >+ }); >+ >+ 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 >+ ); >+ >+ // Setup all API intercepts using helper functions >+ setupStaticApiIntercepts(); >+ setupDynamicBookings(); >+ setupBookingIntercepts(); >+ setupModalTriggerButton(); >+ }); >+ >+ it("should load the booking modal correctly", () => { >+ setupModalForBasicTesting(); >+ >+ // 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", () => { >+ // Open the booking modal and wait for initial data >+ openModalAndWait(); >+ >+ // 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.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", () => { >+ setupModalForBasicTesting(); >+ >+ // Select patron and pickup location first >+ selectPatron(); >+ selectLocation(); >+ >+ // Select an item first >+ selectItem(1); >+ >+ // 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 >+ resetModalAndReopen(); >+ >+ // Now select patron, pickup and item type first >+ selectPatron(); >+ selectLocation(); >+ selectItemType(0); >+ 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 disable dates before today and between today and selected start date", () => { >+ // Test-specific data for date validation >+ const today = dayjs(); >+ const dateTestData = { >+ startDate: today.add(5, "day"), >+ testRanges: { >+ beforeToday: { start: today.subtract(7, "day"), end: today }, >+ afterToday: { >+ start: today.add(1, "day"), >+ end: today.add(7, "day"), >+ }, >+ betweenTodayAndStart: { >+ start: today.add(1, "day"), >+ end: today.add(4, "day"), >+ }, >+ afterStartDate: { >+ start: today.add(6, "day"), >+ end: today.add(10, "day"), >+ }, >+ }, >+ }; >+ >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ cy.get("#period").as("flatpickrInput"); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ >+ cy.get("@flatpickrInput").then($el => { >+ // Phase 1: Test that all dates prior to today are disabled >+ cy.log("Testing dates prior to today are disabled"); >+ >+ // Find the first visible date in the calendar to determine range >+ cy.get(".flatpickr-day:not(.hidden)") >+ .first() >+ .then($firstDay => { >+ const firstDate = dayjs($firstDay.attr("aria-label")); >+ >+ // Check all dates from first visible date up to today are disabled >+ for ( >+ let checkDate = firstDate; >+ checkDate.isSameOrBefore(today, "day"); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("have.class", "flatpickr-disabled"); >+ } >+ }); >+ >+ // Phase 2: Test that dates after today are initially enabled >+ cy.log("Testing dates after today are initially enabled"); >+ >+ // Test a broader range of future dates for better coverage >+ for ( >+ let checkDate = today.add(1, "day"); >+ checkDate.isSameOrBefore(today.add(7, "day"), "day"); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("not.have.class", "flatpickr-disabled"); >+ } >+ }); >+ >+ // Phase 3: Select a start date >+ cy.log( >+ `Selecting start date (${dateTestData.startDate.format("YYYY-MM-DD")})` >+ ); >+ cy.get("@flatpickrInput").selectFlatpickrDate( >+ dateTestData.startDate.toDate() >+ ); >+ >+ // Phase 4: Verify dates between today and start date are now disabled >+ cy.log("Testing dates between today and start date are disabled"); >+ >+ cy.get("@flatpickrInput").then($el => { >+ for ( >+ let checkDate = >+ dateTestData.testRanges.betweenTodayAndStart.start; >+ checkDate.isBefore(dateTestData.startDate, "day"); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("have.class", "flatpickr-disabled"); >+ } >+ >+ // Verify the selected start date itself is properly selected and not disabled >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(dateTestData.startDate.toDate()) >+ .should("not.have.class", "flatpickr-disabled") >+ .and("have.class", "selected"); >+ }); >+ >+ // Phase 5: Verify dates after the start date remain enabled >+ cy.log("Testing dates after start date remain enabled"); >+ >+ cy.get("@flatpickrInput").then($el => { >+ for ( >+ let checkDate = dateTestData.testRanges.afterStartDate.start; >+ checkDate.isSameOrBefore( >+ dateTestData.testRanges.afterStartDate.end, >+ "day" >+ ); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("not.have.class", "flatpickr-disabled"); >+ } >+ }); >+ }); >+ >+ it("should disable dates with existing bookings for same item", () => { >+ // Test-specific data >+ const today = dayjs(); >+ const testItemData = { >+ itemId: 789, >+ itemBarcode: "BARCODE789", >+ bookingPeriods: [ >+ { >+ start: today.add(8, "day"), >+ end: today.add(13, "day"), >+ name: "First booking period", >+ }, >+ { >+ start: today.add(14, "day"), >+ end: today.add(18, "day"), >+ name: "Second booking period", >+ }, >+ { >+ start: today.add(28, "day"), >+ end: today.add(33, "day"), >+ name: "Third booking period", >+ }, >+ { >+ start: today.add(35, "day"), >+ end: today.add(37, "day"), >+ name: "Fourth booking period", >+ }, >+ ], >+ otherItemBooking: { >+ start: today.add(20, "day"), >+ end: today.add(25, "day"), >+ }, >+ }; >+ >+ const TEST_ITEM_ID = testItemData.itemId; >+ const TEST_ITEM_BARCODE = testItemData.itemBarcode; >+ >+ // Create custom bookings for this test using our helper >+ const createCustomBookings = (baseBookings: any[]) => { >+ // Modify existing bookings to create a comprehensive test scenario >+ // All bookings will be for the same item to test date conflicts >+ baseBookings[0].item_id = TEST_ITEM_ID; >+ baseBookings[0].start_date = today >+ .add(8, "day") >+ .startOf("day") >+ .toISOString(); >+ baseBookings[0].end_date = today >+ .add(13, "day") >+ .endOf("day") >+ .toISOString(); >+ >+ baseBookings[1].item_id = TEST_ITEM_ID; >+ baseBookings[1].start_date = today >+ .add(14, "day") >+ .startOf("day") >+ .toISOString(); >+ baseBookings[1].end_date = today >+ .add(18, "day") >+ .endOf("day") >+ .toISOString(); >+ >+ baseBookings[2].item_id = TEST_ITEM_ID; >+ baseBookings[2].start_date = today >+ .add(28, "day") >+ .startOf("day") >+ .toISOString(); >+ baseBookings[2].end_date = today >+ .add(33, "day") >+ .endOf("day") >+ .toISOString(); >+ >+ // Add additional bookings for comprehensive testing >+ const additionalBookings = [ >+ { >+ booking_id: 1004, >+ biblio_id: 123, >+ patron_id: 459, >+ item_id: TEST_ITEM_ID, >+ pickup_library_id: "MAIN", >+ start_date: today >+ .add(35, "day") >+ .startOf("day") >+ .toISOString(), >+ end_date: today.add(37, "day").endOf("day").toISOString(), >+ status: "pending", >+ }, >+ { >+ booking_id: 1005, >+ biblio_id: 123, >+ patron_id: 460, >+ item_id: "different_item", // Different item - should not affect our test >+ pickup_library_id: "MAIN", >+ start_date: today >+ .add(20, "day") >+ .startOf("day") >+ .toISOString(), >+ end_date: today.add(25, "day").endOf("day").toISOString(), >+ status: "pending", >+ }, >+ ]; >+ >+ return [...baseBookings, ...additionalBookings]; >+ }; >+ >+ // Setup custom bookings using our helper function >+ generateMockBookings(3, [ >+ { >+ booking_id: 1001, >+ patron_id: 456, >+ item_id: 789, >+ }, >+ { >+ booking_id: 1002, >+ patron_id: 457, >+ item_id: 790, >+ }, >+ { >+ booking_id: 1003, >+ patron_id: 458, >+ item_id: 791, >+ }, >+ ]).then(baseBookings => { >+ const customBookings = createCustomBookings(baseBookings); >+ setupDynamicBookings(customBookings); >+ }); >+ >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ // Select the specific item that has existing bookings (TEST_ITEM_BARCODE) >+ cy.selectFromSelect2("#booking_item_id", TEST_ITEM_BARCODE); >+ >+ cy.get("#period").as("flatpickrInput"); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ >+ // Define booking periods for the selected item only >+ const bookingPeriodsForSelectedItem = [ >+ { >+ name: "First booking period", >+ start: today.add(8, "day"), >+ end: today.add(13, "day"), >+ }, >+ { >+ name: "Second booking period", >+ start: today.add(14, "day"), >+ end: today.add(18, "day"), >+ }, >+ { >+ name: "Third booking period", >+ start: today.add(28, "day"), >+ end: today.add(33, "day"), >+ }, >+ { >+ name: "Fourth booking period", >+ start: today.add(35, "day"), >+ end: today.add(37, "day"), >+ }, >+ ]; >+ >+ cy.get("@flatpickrInput").then($el => { >+ // Phase 1: Test dates before first booking period are available >+ cy.log("Testing dates before first booking period are available"); >+ >+ for ( >+ let checkDate = today.add(1, "day"); >+ checkDate.isBefore( >+ bookingPeriodsForSelectedItem[0].start, >+ "day" >+ ); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("not.have.class", "flatpickr-disabled"); >+ } >+ >+ // Phase 2: Test each booked period individually >+ bookingPeriodsForSelectedItem.forEach((period, index) => { >+ cy.log(`Testing ${period.name} dates are disabled`); >+ >+ // Test dates within the booked range are disabled >+ for ( >+ let checkDate = period.start; >+ checkDate.isSameOrBefore(period.end, "day"); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("have.class", "flatpickr-disabled"); >+ } >+ }); >+ >+ // Phase 3: Test gaps between booking periods are available >+ cy.log("Testing gaps between booking periods are available"); >+ >+ for (let i = 0; i < bookingPeriodsForSelectedItem.length - 1; i++) { >+ const currentPeriod = bookingPeriodsForSelectedItem[i]; >+ const nextPeriod = bookingPeriodsForSelectedItem[i + 1]; >+ >+ // Test dates in the gap between current and next booking period >+ for ( >+ let checkDate = currentPeriod.end.add(1, "day"); >+ checkDate.isBefore(nextPeriod.start, "day"); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("not.have.class", "flatpickr-disabled"); >+ } >+ } >+ >+ // Phase 4: Test that dates booked for different items are still available >+ cy.log("Testing dates booked for different items remain available"); >+ >+ // The booking for different_item (days 20-25) should not affect our selected item >+ const differentItemBookingStart = today.add(20, "day"); >+ const differentItemBookingEnd = today.add(25, "day"); >+ >+ for ( >+ let checkDate = differentItemBookingStart; >+ checkDate.isSameOrBefore(differentItemBookingEnd, "day"); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("not.have.class", "flatpickr-disabled"); >+ } >+ >+ // Phase 5: Test dates after last booking period are available >+ cy.log("Testing dates after last booking period are available"); >+ >+ const lastPeriod = >+ bookingPeriodsForSelectedItem[ >+ bookingPeriodsForSelectedItem.length - 1 >+ ]; >+ for ( >+ let checkDate = lastPeriod.end.add(1, "day"); >+ checkDate.isSameOrBefore(lastPeriod.end.add(5, "day"), "day"); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(checkDate.toDate()) >+ .should("not.have.class", "flatpickr-disabled"); >+ } >+ }); >+ }); >+ >+ it("should handle lead and trail period hover highlighting", () => { >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ // Open the flatpickr >+ cy.get("#period").as("flatpickrInput"); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ >+ // Get a future date to hover over >+ let hoverDate = dayjs(); >+ hoverDate = hoverDate.add(5, "day"); >+ >+ // Hover over a date and check for lead/trail highlighting >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(hoverDate.toDate()) >+ .trigger("mouseover"); >+ cy.wait(100); >+ >+ // Check for lead range classes (assuming 2-day lead period from circulation rules) >+ cy.get(".leadRange, .leadRangeStart, .leadRangeEnd").should("exist"); >+ >+ // Check for trail range classes (assuming 2-day trail period) >+ cy.get(".trailRange, .trailRangeStart, .trailRangeEnd").should("exist"); >+ }); >+ >+ it("should disable click when lead/trail periods overlap with disabled dates", () => { >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ // Open the flatpickr >+ cy.get("#period").as("flatpickrInput"); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ >+ // Find a date that would have overlapping lead/trail with disabled dates >+ const today = dayjs(); >+ const problematicDate = today.add(7, "day"); // Just before a booked period >+ >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(problematicDate.toDate()) >+ .trigger("mouseover") >+ .should($el => { >+ expect( >+ $el.hasClass("leadDisable") || $el.hasClass("trailDisable"), >+ "element has either leadDisable or trailDisable" >+ ).to.be.true; >+ }); >+ }); >+ >+ it("should show event dots for dates with existing bookings", () => { >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ // Open the flatpickr >+ cy.get("#period").as("flatpickrInput"); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ >+ // Check for event dots on dates with bookings >+ cy.get(".flatpickr-calendar").within(() => { >+ cy.get(".event-dots").should("exist"); >+ cy.get(".event-dots .event").should("exist"); >+ }); >+ }); >+ >+ it("should show only the correct bold dates for issue and renewal periods", () => { >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ // Open the flatpickr >+ cy.get("#period").as("flatpickrInput"); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ >+ const startDate = dayjs().add(3, "day").startOf("day"); >+ cy.get("@flatpickrInput").selectFlatpickrDate(startDate.toDate()); >+ >+ const expectedBoldDates = [ >+ startDate.add(14, "day"), >+ startDate.add(21, "day"), >+ startDate.add(28, "day"), >+ ]; >+ >+ // Confirm each expected bold date is bold >+ expectedBoldDates.forEach(boldDate => { >+ cy.get("@flatpickrInput") >+ .getFlatpickrDate(boldDate.toDate()) >+ .should("have.class", "title"); >+ }); >+ >+ // Confirm that only expected dates are bold >+ cy.get(".flatpickr-day.title").each($el => { >+ const ariaLabel = $el.attr("aria-label"); >+ const date = dayjs(ariaLabel, "MMMM D, YYYY"); >+ const isExpected = expectedBoldDates.some(expected => >+ date.isSame(expected, "day") >+ ); >+ expect(isExpected, `Unexpected bold date: ${ariaLabel}`).to.be.true; >+ }); >+ }); >+ >+ it("should set correct max date based on circulation rules", () => { >+ const today = dayjs(); >+ >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ // Open the flatpickr >+ cy.get("#period").as("flatpickrInput"); >+ cy.get("@flatpickrInput").openFlatpickr(); >+ >+ // Select a start date >+ const startDate = today.add(3, "day"); >+ cy.get("@flatpickrInput").selectFlatpickrDate(startDate.toDate()); >+ >+ // Check that dates beyond the maximum allowed period are disabled >+ cy.get(".flatpickr-calendar").within(() => { >+ // Assuming circulation rules allow 14 days + renewals >+ const maxDate = startDate.add(30, "day"); // Beyond reasonable limit >+ >+ // Navigate to future months if needed and check disabled state >+ cy.get(".flatpickr-next-month").click(); >+ cy.get(".flatpickr-day.flatpickr-disabled").should("exist"); >+ }); >+ }); >+ >+ it("should handle visible and hidden fields on date selection", () => { >+ // Test-specific data for form field validation >+ const today = dayjs(); >+ const fieldTestData = { >+ selectedDates: { >+ startDate: today.add(3, "day"), >+ endDate: today.add(6, "day"), >+ }, >+ expectedFormats: { >+ displayFormat: "YYYY-MM-DD", // Format for visible input >+ isoFormat: "YYYY-MM-DDTHH:mm:ss.sssZ", // Format for hidden fields >+ }, >+ }; >+ >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ cy.get("#period").as("flatpickrInput"); >+ >+ // Select date range using test data >+ cy.get("@flatpickrInput").selectFlatpickrDateRange( >+ fieldTestData.selectedDates.startDate.toDate(), >+ fieldTestData.selectedDates.endDate.toDate() >+ ); >+ >+ // Use should with retry capability instead of a simple assertion >+ const format = date => >+ date.format(fieldTestData.expectedFormats.displayFormat); >+ const expectedDisplayValue = `${format(fieldTestData.selectedDates.startDate)} to ${format(fieldTestData.selectedDates.endDate)}`; >+ >+ cy.get("#period").should("have.value", expectedDisplayValue); >+ >+ // Verify the flatpickr visible input also has value >+ cy.get("#period").should("have.value", expectedDisplayValue); >+ >+ // Now check the hidden fields use ISO format >+ cy.get("#booking_start_date").should( >+ "have.value", >+ fieldTestData.selectedDates.startDate.startOf("day").toISOString() >+ ); >+ cy.get("#booking_end_date").should( >+ "have.value", >+ fieldTestData.selectedDates.endDate.endOf("day").toISOString() >+ ); >+ }); >+ >+ it("should submit a new booking successfully", () => { >+ // Setup modal for form submission >+ setupModalForFormSubmission(); >+ >+ // 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("#placebooking") >+ .invoke("attr", "data-booking", "1001") >+ .invoke("attr", "data-patron", "456") >+ .invoke("attr", "data-itemnumber", "789") >+ .invoke("attr", "data-pickup_library", "1") >+ .invoke("attr", "data-start_date", "2025-05-01T00:00:00.000Z") >+ .invoke("attr", "data-end_date", "2025-05-05T23:59:59.999Z") >+ .click(); >+ >+ // Wait for API calls that populate the modal when editing >+ cy.wait([ >+ "@getBookableItems", >+ "@getBookings", >+ "@getCheckouts", >+ "@getPickupLocations", >+ ]); >+ >+ // 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"); >+ >+ // Verify patron ID is set >+ cy.get("#booking_patron_id").should("have.value", "456"); >+ >+ // Verify pickup_library is set >+ cy.get("#pickup_library_id").should("have.value", "MAIN"); >+ >+ // Verify itemnumber is set >+ cy.get("#booking_item_id").should("have.value", "789"); >+ >+ // Wait for item selection logic to complete and populate item type >+ cy.get("#booking_itemtype") >+ .should("not.have.value", "") >+ .and("not.have.value", null); >+ >+ // Verify item_type is set >+ cy.get("#booking_itemtype").should("have.value", "BK"); >+ >+ // Change pickup location >+ cy.selectFromSelect2ByIndex("#pickup_library_id", 1); >+ >+ // 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"); >+ >+ // Setup modal for form submission >+ setupModalForFormSubmission(); >+ >+ // 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", () => { >+ // Setup modal for date testing >+ setupModalForDateTesting(); >+ >+ // Reset modal to test form clearing >+ resetModalAndReopen(); >+ >+ // Check fields are reset to initial state >+ cy.get("#booking_patron_id") >+ .should("not.be.disabled") >+ .and("have.value", null); >+ 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