From 91577a4d400297ec34f453ad87fc9e5e05822f74 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 16 Apr 2025 10:22:02 -0400 Subject: [PATCH 01/55] CASH-501: bugfix re setting PBD on preselected date on Schedule --- src/layouts/schedule/schedule.vue | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 2fd06d312..ba4430e03 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -100,6 +100,9 @@ import { PREMIUM_FEE_PART_TYPE, PRICING_BY_DAY_PART_TYPE, } from "@/constants/schedule-constants"; +import { + DAYS_OF_WEEK, +} from "@/digital-components/date-picker/mixins/constants"; import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; import store from "@/store"; @@ -269,24 +272,22 @@ export default { const priceString = getAmountDue(lineItemsToBePriced, false); // pass the IncludeTax param as false const priceStringIntegerRoundedDown = priceString?.split(".")[0]; // same method used as getDisplayPrice() in service-package-radio used on /quote const pricingByDayBasePrice = parseInt(priceStringIntegerRoundedDown); - const payment = store.getters.order.payment; - const isInsurance = payment?.isInsurance; - - // Check to see if includePricingByDayUpcharge should already be set (based on order lineItems) let includePricingByDayUpcharge = false; - if (supportingItemsFromStore) { - const pricingByDayUpchargeFeeIndex = supportingItemsFromStore?.findIndex( - (item) => item.partType == PRICING_BY_DAY_PART_TYPE - ); - if (pricingByDayUpchargeFeeIndex > -1 && !isInsurance) { - includePricingByDayUpcharge = true; - } - } - // Load page with date already selected? + // Check to see if date should be pre-selected let preSelectedDate = await store.getters.order.schedule.date; if (!preSelectedDate || preSelectedDate.startTime === null) { preSelectedDate = null; + } else { + // Check to see if pre-selected date should have pricing by day upcharge + if (showPricingByDay) { + // is this preSelectedDate a premium day? + const dayIndex = convertDateStringToDate(preSelectedDate).getDay(); + const dayObject = DAYS_OF_WEEK[dayIndex]; + if (dayObject.isPricingByDayUpchargeDay) { + includePricingByDayUpcharge = true; + } + } } // Set up promises @@ -767,4 +768,4 @@ export default { margin-bottom: 0.25rem; } } - + \ No newline at end of file From 0c4a6606fd0ead82d4f07c47126bc4c72a9b1b7c Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 16 Apr 2025 10:24:02 -0400 Subject: [PATCH 02/55] CASH-501: prettier changes --- src/layouts/schedule/schedule.vue | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ba4430e03..cd3e09806 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -100,9 +100,7 @@ import { PREMIUM_FEE_PART_TYPE, PRICING_BY_DAY_PART_TYPE, } from "@/constants/schedule-constants"; -import { - DAYS_OF_WEEK, -} from "@/digital-components/date-picker/mixins/constants"; +import { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants"; import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; import store from "@/store"; @@ -768,4 +766,4 @@ export default { margin-bottom: 0.25rem; } } - \ No newline at end of file + From 4344c19a30492c668ce54128baff8e804db8ad57 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:01:53 -0400 Subject: [PATCH 03/55] Payment method page validation changes --- .../business-logic/types/CustomerDetails.ts | 2 + playwright-tests/pages/PaymentMethodPage.ts | 329 ++++++++++++------ playwright-tests/pages/SchedulePage.ts | 51 ++- playwright-tests/pages/ServiceLocationPage.ts | 21 +- .../tests/CashRepairMobileCreditCard.ts | 2 +- .../tests/CashReplaceMultiGlassMobile.ts | 2 +- .../CashReplaceSafeliteCanNotRecalMobile.ts | 2 +- .../tests/CashReplaceVinMobile.ts | 2 +- 8 files changed, 287 insertions(+), 124 deletions(-) diff --git a/playwright-tests/business-logic/types/CustomerDetails.ts b/playwright-tests/business-logic/types/CustomerDetails.ts index bb1f08b96..0ea0e9605 100644 --- a/playwright-tests/business-logic/types/CustomerDetails.ts +++ b/playwright-tests/business-logic/types/CustomerDetails.ts @@ -9,6 +9,8 @@ export interface ICustomerDetails { notes: string, address: IAddress, apptDate?: string, + apptTime?: string, + apptDuration?: string, packagePrice?: string } diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index c859aa916..519cface5 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -1,7 +1,7 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IPaymentDetails } from '@business-logic/types/CustomerDetails'; -import { PaymentMethod, PaymentType, ServicePackage, VehicleDamage } from '@business-logic/types/Enums'; +import { AppointmentType, PaymentMethod, PaymentType, ServicePackage, VehicleDamage } from '@business-logic/types/Enums'; import { PaymentPage } from './PaymentPage'; import { AfterpayPage } from './AfterpayPage'; import { PaypalPage } from './PaypalPage'; @@ -17,6 +17,7 @@ export class PaymentMethodPage extends BasePage { readonly payInFourButton: Locator; readonly amountDueTextField: Locator; readonly amountDueDropDown: Locator; + readonly appointmentDetailsSection: Locator; readonly appointmentDetailsDropdown: Locator; readonly subtotalAmountTextField: Locator; readonly submitButton: Locator; @@ -48,6 +49,7 @@ export class PaymentMethodPage extends BasePage { this.payAtServiceButton = this.page.locator('[buttonlabel="Pay at my appointment"]'); //this.page.getByText('Pay at time of service'); this.amountDueTextField = this.page.getByLabel('expand cart').locator('.amount-due'); this.amountDueDropDown = this.page.getByLabel('expand cart'); + this.appointmentDetailsSection = this.page.locator('div .appt-details-snapshot'); this.appointmentDetailsDropdown = this.page.getByLabel('expand appointment details'); this.payWithInsuranceButton = this.page.locator('div').filter({ hasText: /^Pay with insurance$/ }).nth(1); this.subtotalAmountTextField = this.page.locator('.sub-total span').nth(1); @@ -88,110 +90,27 @@ export class PaymentMethodPage extends BasePage { // Wait for review table to be visible to ensure page is loaded await this.appointmentDetailsDropdown.waitFor({state: "visible"}); - await this.appointmentDetailsDropdown.click(); // Expand cart to see all details + await this.appointmentDetailsDropdown.click(); + + let expectedAppointmentDetails = new Map(); + expectedAppointmentDetails = await this.getExpectedVehicleDetails(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.getExpectedVehicleDamage(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.expectedServicePackageDetails(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.getExpectedServiceLocation(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.getExpectedAppointmentDate(testData, expectedAppointmentDetails); + expectedAppointmentDetails = await this.getExpectedCustomerDetails(testData, expectedAppointmentDetails); + + let actualAppointmentDetails = await this.getActualAppointmentDetails(); + + for (const key in expectedAppointmentDetails) { + expect.soft(actualAppointmentDetails[key]?.map(item => item.toLowerCase())) + .toEqual(expectedAppointmentDetails[key].map(item => item.toLowerCase())); + } + + // Expand cart to see all details await this.amountDueDropDown.click(); await this.reviewTable.waitFor({ state: "visible" }); - // Helper method to get content lines from a section - const getSectionContent = async (section: Locator) => { - // First make sure section exists - if (await section.count() === 0) return []; - - // Find all content lines within this section - const contentLines = await section.locator('div.small.review-block-content').allInnerTexts(); - return contentLines; - }; - - // Validate vehicle information - const vehicleContent = await getSectionContent(this.vehicleSection); - if (vehicleContent.length > 0) { - const vehicleText = vehicleContent[0]; - expect.soft(vehicleText).toContain(`${vehicleDetails?.year} ${vehicleDetails?.make} ${vehicleDetails?.model}`); - } - - // Validate damage type - - // TODO: Work out logic on how to verify vehicle Damage in payment details with vehicleDamage - - // const damageContent = await getSectionContent(this.damageSection); - // if (vehicleDamage && damageContent.length > 0) { - // const damageText = damageContent[0]; - - // // For each damage type in the array, check if its display text is in the damage content - // for (const damage of vehicleDamage) { - // const expectedDamageText = this.getDamageDisplayText(damage); - - // // If this is a single damage item, it should match exactly - // if (vehicleDamage.length === 1) { - // expect.soft(damageText).toContain(expectedDamageText); - // } else { - // // For multiple damages, check if any of the damage content lines contain this damage type - // const damageFound = damageContent.some(content => - // content.includes(expectedDamageText) - // ); - // expect.soft(damageFound).toBeTruthy(); - // } - // } - // } - - // Validate service details based on package - const serviceContent = await getSectionContent(this.serviceDetailsSection); - if (servicePackage && serviceContent.length > 0) { - //TODO: Add service package validation - // move over logic from Kishan's code - - // Additional validations for Standard and Premium packages - if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) { - await expect.soft(this.wiperBladesText).toBeVisible(); - } - - if (servicePackage === ServicePackage.Premium) { - await expect.soft(this.rainDefenseText).toBeVisible(); - } - } - - // Validate service location - const locationContent = await getSectionContent(this.serviceLocationSection); - if (appointmentDetails?.serviceLocation && locationContent.length > 0) { - // Find the title element of the service location section - const serviceLocationTitle = this.serviceLocationSection.locator('span').first(); - const serviceLocationValue = await serviceLocationTitle.innerText(); - - // Check for mobile/inshop service wording - if (appointmentDetails.serviceLocation.toString().includes('Mobile')) { - expect.soft(serviceLocationValue).toContain("We're coming to you"); - } else if (appointmentDetails.serviceLocation.toString().includes('InShop')) { - expect.soft(serviceLocationValue).toContain("Bring to shop"); - } - - // Validate address if available - if (appointmentDetails.serviceAddress && locationContent.length > 0) { - const addressText = locationContent[0].toLowerCase(); - expect.soft(addressText).toContain(appointmentDetails.serviceAddress.street.toLowerCase()); - } - } - - // Validate appointment date/time - const appointmentContent = await getSectionContent(this.appointmentDateSection); - if (customerDetails?.apptDate && appointmentContent.length > 0) { - const appointmentText = appointmentContent[0]; - expect.soft(appointmentText).toContain(customerDetails.apptDate); - } - - // Validate contact details - const contactContent = await getSectionContent(this.contactDetailsSection); - if (customerDetails && contactContent.length > 0) { - const fullName = `${customerDetails.firstName} ${customerDetails.lastName}`; - const email = customerDetails.email; - const phone = customerDetails.phoneNumber; - - // Check if contact details are present - const contactTextJoined = contactContent.join(' '); - expect.soft(contactTextJoined).toContain(fullName); - expect.soft(contactTextJoined).toContain(email); - expect.soft(contactTextJoined).toContain(phone); - } - // Cart Validation // Get pricing information from cart panel if (await this.subtotalText.isVisible()) { @@ -339,4 +258,210 @@ l } } + async getActualAppointmentDetails(): Promise { + let actualAppointmentDetails = new Map(); + let appointmentDetailsSubSections = await this.appointmentDetailsSection.locator('div .py-3').all(); + + for (let element of appointmentDetailsSubSections) { + let label = await element.locator('div .text-block').innerText(); + //added to trim the text to remove any leading or trailing spaces (example: "expert installation " to "expert installation") + let value = (await element.locator('.review-block-content').allInnerTexts() as string[]).map(item => item.trim()); + actualAppointmentDetails[label] = value; + } + return actualAppointmentDetails; + } + + async getExpectedVehicleDetails(testdata: Partial, expectedServicePackageDetails: Map): Promise { + const { vehicleDetails } = testdata; + expectedServicePackageDetails["Vehicle"] = [vehicleDetails?.year + " " + vehicleDetails?.make + " " + vehicleDetails?.model]; + return expectedServicePackageDetails; + } + + async getExpectedVehicleDamage(testdata: Partial, expectedServicePackageDetails: Map): Promise { + + const {vehicleDamage} = testdata; + + let vehicleDamageText: string[] = []; + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldOneChip)) { + vehicleDamageText.push("Windshield repair - 1 chip"); + } + + else if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldTwoChips)) { + vehicleDamageText.push("Windshield repair - 2 chips"); + } + + else if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldThreeChips)) { + vehicleDamageText.push("Windshield repair - 3 chips"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldCrack)) { + vehicleDamageText.push("Windshield crack"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverVentGlass)) { + vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nVent glass" : vehicleDamageText.push("Side door - Driver side", "Vent glass"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverFrontDoor)) { + vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nFront door" : vehicleDamageText.push("Side door - Driver side", "Front door"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverRearDoor)) { + vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nBack door" : vehicleDamageText.push("Side door - Driver side", "Back door"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverQuarterPanel)) { + vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nQuarter panel" : vehicleDamageText.push("Side door - Driver side", "Quarter panel"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerVentGlass)) { + vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nVent glass" : vehicleDamageText.push("Side door - Passenger side", "Vent glass"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerFrontDoor)) { + vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nFront door" : vehicleDamageText.push("Side door - Passenger side", "Front door"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerRearDoor)) { + vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nBack door" : vehicleDamageText.push("Side door - Passenger side", "Back door"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerQuarterPanel)) { + vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nQuarter panel" : vehicleDamageText.push("Side door - Passenger side", "Quarter panel"); + } + + if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.RearWindow || item == VehicleDamage.RearSliding)) { + vehicleDamageText.push("Rear window"); + } + + expectedServicePackageDetails["Damage"] = vehicleDamageText; + return expectedServicePackageDetails; + } + + async expectedServicePackageDetails(testData: Partial, expectedServicePackageDetails: Map): Promise { + const {servicePackage} = testData; + + let localStorage= JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); + let isRepair = localStorage.order.damage.isRepair as boolean; + let isInsurance = localStorage.order.payment.isInsurance as boolean; + let hasNonWindshieldGlass: boolean = false; + if (!isRepair) { + hasNonWindshieldGlass = localStorage.order.lineItems.glassParts.find((item: any) => item.partType !== "WINDSHIELD") ? true : false; + } + let isCaliforniaState = localStorage.order.serviceLocation.state as string == 'CA' ? true : false; + let recalRequired = !isRepair && (isInsurance || isCaliforniaState) && + localStorage.order.lineItems.glassParts.find((item: any) => item["requiresRecalibration"].value === true) ? true : false; + let stringForRepair: string[] = ["Expert windshield repair", "Exclusive resin sealant", "Nationwide lifetime guarantee"]; + let stringIfRecal = isRepair + ? "" + : recalRequired + ? " and recalibration" + : ""; + let stringForReplace: string[] = [hasNonWindshieldGlass ? "New replacement glass" : "New replacement windshield", "Expert installation" + `${stringIfRecal}`, "Nationwide lifetime warranty"]; + switch (servicePackage) + { + case ServicePackage.GlassOnly: + expectedServicePackageDetails["Glass service only"] = isRepair + ? stringForRepair + : stringForReplace; + break; + case ServicePackage.Standard: + stringForRepair.push("New wiper blades"); + stringForReplace.push("New wiper blades"); + expectedServicePackageDetails["Standard service"] = isRepair + ? stringForRepair + : stringForReplace; + break; + case ServicePackage.Premium: + stringForRepair.push("New wiper blades", "Rain Defense™"); + stringForReplace.push("New wiper blades", "Rain Defense™"); + expectedServicePackageDetails["Premium service"] = isRepair + ? stringForRepair + : stringForReplace; + break; + } + return expectedServicePackageDetails; + } + + async getExpectedServiceLocation(testData: Partial, expectedServicePackageDetails: Map): Promise { + const { appointmentDetails } = testData; + let serviceLocationTitle = appointmentDetails?.serviceLocation == AppointmentType.Mobile + ? "We're coming to you" + : "You're going to a Safelite shop"; + let serviceLocation: string[] = []; + + serviceLocation.push( + appointmentDetails?.serviceLocation == AppointmentType.Mobile + ? appointmentDetails?.serviceAddress + ? appointmentDetails.serviceAddress.street + ", " + appointmentDetails.serviceAddress.city + ", " + appointmentDetails.serviceAddress.state + " " + appointmentDetails.serviceAddress.postalCode + : "" + : appointmentDetails?.shopAddress + ? appointmentDetails.shopAddress + : "" + ); + + expectedServicePackageDetails[serviceLocationTitle] = serviceLocation; + return expectedServicePackageDetails; + } + + async getExpectedAppointmentDate(testData: Partial, expectedServicePackageDetails: Map): Promise { + const { customerDetails, appointmentDetails } = testData; + let appointmentDateText: string[] = []; + const isMobileAppointment = appointmentDetails?.serviceLocation == AppointmentType.Mobile + if (isMobileAppointment) { + let localStorage= JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); + let jobMinMinutes = localStorage.order.schedule.jobMinMinutes as number; + let jobMaxMinutes = localStorage.order.schedule.jobMaxMinutes as number; + + if (jobMinMinutes < 60) { + customerDetails!.apptDuration = `${jobMinMinutes} - ${jobMaxMinutes} minutes`; + } else { + const minHours = Math.floor(jobMinMinutes / 60); + const maxHours = Math.floor(jobMaxMinutes / 60); + customerDetails!.apptDuration = `${minHours} - ${maxHours} hours`; + } + } + + appointmentDateText.push( + customerDetails?.apptDate ? await this.getFormattedAppointmentDate(customerDetails.apptDate) + " " + customerDetails?.apptTime?.replace("-", "—") : "", + customerDetails?.apptDuration ? "Estimated appointment length: " + customerDetails.apptDuration : "" + ); + + expectedServicePackageDetails["Appointment date + time"] = appointmentDateText; + return expectedServicePackageDetails; + } + + async getExpectedCustomerDetails(testData: Partial, expectedServicePackageDetails: Map): Promise { + const { customerDetails } = testData; + let customerDetailsText: string[] = []; + + customerDetailsText.push( + customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(), + customerDetails?.email ? customerDetails?.email.toUpperCase() : "", + customerDetails?.phoneNumber ? customerDetails?.phoneNumber : "", + "Opted out of text message updates" + ); + + expectedServicePackageDetails["Contact details"] = customerDetailsText; + return expectedServicePackageDetails; + } + + async getFormattedAppointmentDate(appointmentDate: string) { + const currentYear = new Date().getFullYear(); + + // Parse the original date + let parsedAppointmentDate = new Date(`${appointmentDate}, ${currentYear}`); + + let currentDate = new Date(); + currentDate.setDate(currentDate.getDate() - 2); + + // If the original date is not in the future, adjust the year + if (parsedAppointmentDate < currentDate) { + parsedAppointmentDate.setFullYear(parsedAppointmentDate.getFullYear() + 1); + } + + // Format the new date as a string + let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + return updatedAppointmentDate; + } } \ No newline at end of file diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index bea9e82bd..0bf2b1105 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -1,8 +1,9 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; -import { IAppointmentDetails } from '@business-logic/types/CustomerDetails'; +import { IAppointmentDetails, ICustomerDetails } from '@business-logic/types/CustomerDetails'; import { formatDate, formatTime } from '@impl/utils/DateUtils'; import { AppointmentType, ServiceLocation } from '@business-logic/types/Enums'; +import { time } from 'console'; export class SchedulePage extends BasePage { readonly page: Page; @@ -13,6 +14,8 @@ export class SchedulePage extends BasePage { readonly dropOffButton: Locator; readonly dateText: Locator; readonly viewMoreDatesLink: Locator; + readonly appointmentDuration: Locator; + readonly timeSlots: Locator; constructor(page: Page) { super(page); @@ -23,6 +26,8 @@ export class SchedulePage extends BasePage { this.dropOffButton = this.page.getByText('Drop off your vehicle', { exact: true }); this.dateText = this.page.locator('label.modal-title'); this.viewMoreDatesLink = this.page.getByText(/View more dates/).first(); + this.appointmentDuration = this.page.locator('.duration-text-block'); + this.timeSlots = this.page.locator('fieldset:has(legend[id *= \'chooseTimeSlot\']) label'); } async scheduleAppointment(appointmentDetails: IAppointmentDetails) { @@ -40,20 +45,40 @@ export class SchedulePage extends BasePage { await this.modalContinueButton.click(); } - async scheduleFirstAppointment(serviceLocation: AppointmentType) { - if (await this.firstAvailableDate.isVisible()) { - await this.firstAvailableDate.click(); - } else { + async scheduleFirstAppointment(customerDetails: ICustomerDetails) { + while (!(await this.firstAvailableDate.isVisible())) { await this.viewMoreDatesLink.click(); - while(await this.firstAvailableDate.isHidden()){ - await this.viewMoreDatesLink.click(); - } - await this.firstAvailableDate.click(); } + await this.firstAvailableDate.click(); - serviceLocation === AppointmentType.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click(); - const apptDate = `${await this.dateText.allInnerTexts()}` + // const timeSlots = this.timeSlots; + const timeSlotCount = await this.timeSlots.count(); + const randomIndex = Math.floor(Math.random() * timeSlotCount); + const timeSlot = this.timeSlots.nth(randomIndex); + await timeSlot.click().then(async () => { + customerDetails.apptTime = await this.getFormattedTimeSlot(timeSlot); + }); + // appointmentmentDetails.serviceLocation === AppointmentType.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click(); + customerDetails.apptDate = `${await this.dateText.allInnerTexts()}` + customerDetails.apptDuration = (await this.appointmentDuration.innerText()).replace("Duration: ", ""); await this.modalContinueButton.click(); - return (apptDate); } -} \ No newline at end of file + + async getFormattedTimeSlot(timeSlot: Locator) { + const selectedTimeSlot = await timeSlot.innerText(); + let formattedTimeSlot: string = ""; + if (selectedTimeSlot.toLowerCase().includes("drop off")) + { + formattedTimeSlot = selectedTimeSlot.includes("overnight") ? "drop off by 5:30 pm on the night of your scheduled appointment. Pick-up time dependent on shop schedule" : "drop off before 9:30 AM"; + } + else if (selectedTimeSlot.includes("-") || selectedTimeSlot.toLowerCase().includes("Earlybird")) + { + formattedTimeSlot = selectedTimeSlot.includes("Earlybird") ? "arriving between 8:00 AM - 12:00 PM" : `arriving between ${selectedTimeSlot}`; + } + else + { + formattedTimeSlot = `at ${selectedTimeSlot}`; + } + return formattedTimeSlot; + } +} \ No newline at end of file diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index b8c8649b6..14830acf4 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -55,7 +55,7 @@ export class ServiceLocationPage extends BasePage { // For in-shop and drop off this.selectAShopOptions = this.page.locator('[class="shop-question"]'); - this.firstAppointmentButton = this.page.locator('div').filter({ hasText: /Appts/}).first(); + this.firstAppointmentButton = this.page.locator('fieldset:has(legend#chooseShop)').locator('label').first(); this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))'); this.updateZipTextBox = this.page.locator('#serviceZipCode'); this.saveZipButton = this.page.getByText('Save ZIP code', { exact: true }); @@ -103,9 +103,14 @@ export class ServiceLocationPage extends BasePage { await this.saveZipButton.click(); } await this.inShopButton.click(); - await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); + await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click()); } else { - await this.firstAppointmentButton.click(); + await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { + await this.firstAppointmentButton.click(); + if (appointmentDetails) { + appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); + } + }); } } @@ -115,7 +120,7 @@ export class ServiceLocationPage extends BasePage { await this.enterServiceAddressButton.click(); await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! }); if (await this.repeatedClicksModalCloseButton.isVisible()) { - await this.repeatedClicksModalCloseButton.click(); + await this.repeatedClicksModalCloseButton.click(); } if (faker.datatype.boolean()) { await this.vehicleProtectedYesButton.check(); @@ -134,7 +139,13 @@ export class ServiceLocationPage extends BasePage { await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check(); } else { // await this.firstAppointmentButton.click(); - await this.clickWithRetry(this.firstAppointmentButton, this.page); + // await this.clickWithRetry(this.firstAppointmentButton, this.page); + await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => { + await this.firstAppointmentButton.click(); + if (appointmentDetails) { + appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText(); + } + }); } } diff --git a/playwright-tests/tests/CashRepairMobileCreditCard.ts b/playwright-tests/tests/CashRepairMobileCreditCard.ts index 3674159aa..892c8d71e 100644 --- a/playwright-tests/tests/CashRepairMobileCreditCard.ts +++ b/playwright-tests/tests/CashRepairMobileCreditCard.ts @@ -28,7 +28,7 @@ const cashRepairMobileCCData : Partial = { serviceAddress: { street: '13735 San Antonio Ave', city: 'Chino', - state: 'California', + state: 'CA', postalCode: '91710', country: 'United States' } diff --git a/playwright-tests/tests/CashReplaceMultiGlassMobile.ts b/playwright-tests/tests/CashReplaceMultiGlassMobile.ts index 54da96bb6..eac5e2263 100644 --- a/playwright-tests/tests/CashReplaceMultiGlassMobile.ts +++ b/playwright-tests/tests/CashReplaceMultiGlassMobile.ts @@ -35,7 +35,7 @@ const cashReplaceMultiGlassMobileData: Partial = { // Use street address from current faker seed street: getDefaultTestData().customerDetails!.address.street, city: 'Rosedale', - state: 'Maryland', + state: 'MD', postalCode: '21237', country: 'United States' }, diff --git a/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts b/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts index 9ad33f23d..f7d301da1 100644 --- a/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts +++ b/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts @@ -48,7 +48,7 @@ const cashReplaceSafeliteCanNotRecalMobileData: Partial = { // Use street address from current faker seed street: getDefaultTestData().customerDetails!.address.street, city: 'Rosedale', - state: 'Maryland', + state: 'MD', postalCode: '21237', country: 'United States' }, diff --git a/playwright-tests/tests/CashReplaceVinMobile.ts b/playwright-tests/tests/CashReplaceVinMobile.ts index 59930b1ac..37470200e 100644 --- a/playwright-tests/tests/CashReplaceVinMobile.ts +++ b/playwright-tests/tests/CashReplaceVinMobile.ts @@ -45,7 +45,7 @@ const cashReplaceVinMobileData: Partial = { // Use street address from current faker seed street: getDefaultTestData().customerDetails!.address.street, city: 'Rosedale', - state: 'Maryland', + state: 'MD', postalCode: '21237', country: 'United States' } From c74a6a7d10fa571b89cf855a4694738c110a0182 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:13:15 -0400 Subject: [PATCH 04/55] adding more changes --- playwright-tests/pages/ServiceLocationPage.ts | 2 +- playwright-tests/pages/ServicePackagesPage.ts | 2 +- playwright-tests/tests/0000__M.test.ts | 2 +- playwright-tests/tests/CashReplaceDynamicRecalMobile.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index 14830acf4..f405175e6 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -71,7 +71,7 @@ export class ServiceLocationPage extends BasePage { this.vehicleProtectedYesButton = this.page.locator('label').filter({ hasText: 'Yes' }).locator('div'); this.vehicleProtectedNoButton = this.page.locator('label').filter({ hasText: 'No' }).locator('div'); this.saveAddressButton = this.page.getByRole('button', { name: 'Continue' }) - this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); + this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); } async selectLocation(appointmentDetails: IAppointmentDetails){ diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 9a3b87ae3..360b285c3 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -41,7 +41,7 @@ export class ServicePackagesPage extends BasePage { this.closeButton = this.page. getByRole('dialog').locator('button').filter({ hasText: 'Close' }); this.promoCodeTextbox = this.page.getByLabel('Enter a promo code'); this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' }); - this.repeatedClicksModalCloseButton = this.page.getByRole('img').nth(1); + this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); // this.validateURL(this.url); } diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 6bef5a2f3..6f84c9431 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -482,7 +482,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { // Schedule appointment await test.step('SchedulePage >> Select day and time', async () => { let schedulePage = testCase.pages.schedulePage; - customerDetails!.apptDate! = await schedulePage.scheduleFirstAppointment(appointmentDetails!.serviceLocation); + await schedulePage.scheduleFirstAppointment(customerDetails!); }); // Enter contact details diff --git a/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts b/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts index 50c1d72b5..25bb3bf50 100644 --- a/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts +++ b/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts @@ -39,7 +39,7 @@ const cashReplaceDynamicRecalMobileData: Partial = { // Use street address from current faker seed street: getDefaultTestData().customerDetails!.address.street, city: 'Rosedale', - state: 'Maryland', + state: 'MD', postalCode: '21237', country: 'United States' }, From 43b53c9be898eef958199edd09a2da5a1ef2a97d Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Mon, 21 Apr 2025 22:59:36 -0400 Subject: [PATCH 05/55] CASH-464: working without modal and validation works too --- .../date-picker/date-picker.vue | 168 +++++++++++++++++- src/layouts/schedule/schedule.vue | 143 +++++++++++---- 2 files changed, 276 insertions(+), 35 deletions(-) diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index 0d0ef066d..235d32b26 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -4,6 +4,12 @@ :class="`${calendarViewDirection} ${isPricingByDayClass} ${showPricingByDayClass}`">
Select a day and time +
+
componentId {{ componentId }}
+
meta {{ meta }}
+
errors {{ errors }}
+
errorMessage {{ errorMessage }}
+
View more dates + +
+
selectedDate: {{ selectedDate }}
+
selectedTimeSlotInfo: {{ selectedTimeSlotInfo }}
+ +
+ +
@@ -1115,7 +1271,7 @@ export default { &:focus + label, &:checked:focus + label { box-shadow: none; - background-color: $white; + background-color: $blue-100; color: $black; } &:checked + label { @@ -1209,4 +1365,12 @@ export default { max-width: 414px; text-align: left; } + +.new-times { + min-height: 1rem; + padding: .2rem; + margin: 1rem 0; + background: lightgreen; + font-size: .6rem; +} diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index c3081d0fe..f061e39a0 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1,6 +1,14 @@