From d151256ca9ac444a89f10a2d563ef2768f07c6e7 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Tue, 13 May 2025 15:51:45 -0400 Subject: [PATCH 1/9] adding changes for earlybird coverage and progress bar validation --- .../business-logic/types/CustomerDetails.ts | 5 +- .../business-logic/types/Enums.ts | 6 +++ playwright-tests/pages/BasePage.ts | 42 +++++++++++++++- .../pages/CapabilityQuestionsPage.ts | 1 + playwright-tests/pages/ContactDetailsPage.ts | 2 + playwright-tests/pages/EstimatePage.ts | 1 + .../pages/InsuranceCompanyPage.ts | 1 + .../pages/MoldingQuestionsPage.ts | 2 + .../pages/OrderConfirmationPage.ts | 2 + playwright-tests/pages/PartQuestionPage.ts | 2 + playwright-tests/pages/PaymentMethodPage.ts | 11 ++++- playwright-tests/pages/SchedulePage.ts | 49 +++++++++++++------ playwright-tests/pages/ServiceLocationPage.ts | 26 ++++++---- playwright-tests/pages/ServicePackagesPage.ts | 1 + playwright-tests/pages/ServiceZipPage.ts | 1 + playwright-tests/pages/VehicleDamagePage.ts | 1 + .../pages/VehicleLookupAddressPage.ts | 2 + .../pages/VehicleLookupLicensePage.ts | 2 + playwright-tests/pages/VehiclePartsPage.ts | 2 + .../pages/VehicleSelectionPage.ts | 1 + playwright-tests/pages/VinLookupPage.ts | 2 + .../tests/CashReplaceVinMobile.ts | 5 +- 22 files changed, 134 insertions(+), 33 deletions(-) diff --git a/playwright-tests/business-logic/types/CustomerDetails.ts b/playwright-tests/business-logic/types/CustomerDetails.ts index 0ea0e9605..3ac2747a0 100644 --- a/playwright-tests/business-logic/types/CustomerDetails.ts +++ b/playwright-tests/business-logic/types/CustomerDetails.ts @@ -1,4 +1,4 @@ -import { DamageType as DamageCause, WindshieldDamage, ServiceLocation, EndorsementType, VehicleLookupType, PartQuestionType, PaymentType, AppointmentType } from "./Enums"; +import { DamageType as DamageCause, WindshieldDamage, ServiceLocation, EndorsementType, VehicleLookupType, PartQuestionType, PaymentType, AppointmentType, AppointmentTimeslot } from "./Enums"; import { IAddress } from "./IAddress"; export interface ICustomerDetails { @@ -30,7 +30,8 @@ export interface IAppointmentDetails { appointmentDate?: Date, shopAddress?: string, // Used for in-shop serviceAddress?: IAddress, // Used for mobile - isVehicleProtected?: boolean // Used for mobile + isVehicleProtected?: boolean, + appointmentTimeSlot?: AppointmentTimeslot // Used for mobile } export interface IEndorsementDetails { diff --git a/playwright-tests/business-logic/types/Enums.ts b/playwright-tests/business-logic/types/Enums.ts index 782d21d45..91b0248ce 100644 --- a/playwright-tests/business-logic/types/Enums.ts +++ b/playwright-tests/business-logic/types/Enums.ts @@ -124,5 +124,11 @@ export enum AppointmentType{ DropOff = "Drop-off" } +export enum AppointmentTimeslot{ + EarlyBird = "EarlyBird", + DropOff = "DropOff", + overnight = "Overnight" +} + diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index 3c6722bd8..348ee9476 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -1,3 +1,5 @@ +import { IAppointmentDetails, ICustomerDetails } from '@business-logic/types/CustomerDetails'; +import Soft from '@business-logic/validations/Soft'; import test, { expect, type Locator, type Page } from '@playwright/test'; import { error } from 'console'; @@ -13,6 +15,7 @@ export class BasePage { readonly pageSpinner: Locator; readonly buttonLoadSpin: Locator; readonly hamburgerMenu: Locator; + readonly progressBar: Locator; constructor(page: Page){ this.page = page; @@ -21,6 +24,7 @@ export class BasePage { this.pageSpinner = page.getByRole('status'); this.buttonLoadSpin = page.getByRole('alert'); this.hamburgerMenu = this.page.getByRole('button', { name: 'Hamburger Menu (modal window)' }); + this.progressBar = this.page.locator('#progress-bar-container progress'); } async nextPage() { @@ -94,6 +98,42 @@ export class BasePage { console.log(`Referral Number:${referralNumber}`); console.log(`Referral Sequence Number:${referralSequenceNumber}`); }); - } + + async mockScheduleResponseForEarlyBird(customerDetails: ICustomerDetails) { + { + const apiUrl = `https://digitalapi.${process.env['NODE_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/schedule/api/v1/schedule/mobile-time-slots`; + await this.page.route(apiUrl, async (route) => { + const response = await route.fetch(); + const responseBody = await response.json(); + + + responseBody.days.forEach((day: any) => { + day.timeSlots.forEach((slot: any) => { + if (slot.id.includes("AM")) { + slot.offerPremium = true; + } + }); + }); + + customerDetails.apptDate = responseBody.days.find((day: any) => + day.timeSlots.some((slot: any) => slot.offerPremium === true) + ).date || undefined; + + // Mock the response + await route.fulfill({ + response, + body: JSON.stringify(responseBody), + }); + }); + } + } + + async validateProgressBar(progressPercentage: string) { + await this.page.locator('button .loader').waitFor({ state: 'hidden', timeout: 60000 }); + const actualProgressPercentage = await this.progressBar.getAttribute("value") || "Not Found"; + Soft.expect(actualProgressPercentage).toBe(progressPercentage); + console.log(`Progress Bar Percentage: Actual - ${actualProgressPercentage} vs Expected - ${progressPercentage}`); + } + } \ No newline at end of file diff --git a/playwright-tests/pages/CapabilityQuestionsPage.ts b/playwright-tests/pages/CapabilityQuestionsPage.ts index 092a424f3..9416d534a 100644 --- a/playwright-tests/pages/CapabilityQuestionsPage.ts +++ b/playwright-tests/pages/CapabilityQuestionsPage.ts @@ -14,6 +14,7 @@ export default class CapabilityQuestionsPage extends PartQuestionsPage { async handleCapabilityQuestionsPage(testCase: Partial) { const { capabilityQuestions } = testCase; + await this.validateProgressBar("40"); // Validate the capability questions are on the page await this.validatePartQuestions(capabilityQuestions!); diff --git a/playwright-tests/pages/ContactDetailsPage.ts b/playwright-tests/pages/ContactDetailsPage.ts index 2641f6436..8dbd29f50 100644 --- a/playwright-tests/pages/ContactDetailsPage.ts +++ b/playwright-tests/pages/ContactDetailsPage.ts @@ -45,6 +45,8 @@ export class ContactDetailsPage extends BasePage { @step("ContactDetailsPage >> Enter contact details: ") async handleContactDetailsPage(testData: Partial) { const { customerDetails } = testData; + + await this.validateProgressBar("84"); await this.enterContactDetails(customerDetails!); await this.nextPage(); } diff --git a/playwright-tests/pages/EstimatePage.ts b/playwright-tests/pages/EstimatePage.ts index 2beb507b3..6a605293b 100644 --- a/playwright-tests/pages/EstimatePage.ts +++ b/playwright-tests/pages/EstimatePage.ts @@ -74,6 +74,7 @@ export class EstimatePage extends BasePage { @step("EstimatePage >> Select Lookup Type") async handleEstimatePage(testData: Partial) { const { vehicleDetails } = testData; + await this.validateProgressBar("28"); await this.vehicleLookup(vehicleDetails!); } } diff --git a/playwright-tests/pages/InsuranceCompanyPage.ts b/playwright-tests/pages/InsuranceCompanyPage.ts index e2d63ec2d..594075292 100644 --- a/playwright-tests/pages/InsuranceCompanyPage.ts +++ b/playwright-tests/pages/InsuranceCompanyPage.ts @@ -52,6 +52,7 @@ export class InsuranceCompanyPage extends BasePage { async handleInsuranceCompanyPage(testData: Partial) { const { claimDetails } = testData; + await this.validateProgressBar("52"); await this.enterInsuranceCompany(claimDetails!.client!); await this.nextPage(); } diff --git a/playwright-tests/pages/MoldingQuestionsPage.ts b/playwright-tests/pages/MoldingQuestionsPage.ts index 5671900ff..ab12cf9be 100644 --- a/playwright-tests/pages/MoldingQuestionsPage.ts +++ b/playwright-tests/pages/MoldingQuestionsPage.ts @@ -13,6 +13,8 @@ export default class MoldingQuestionsPage extends PartQuestionsPage { @step("MoldingQuestionsPage >> Select Molding Questions: ") async handleMoldingQuestionsPage(testCase: Partial) { const { moldingQuestions } = testCase; + + await this.validateProgressBar("40"); await this.validatePartQuestions(moldingQuestions!); await this.selectPartQuestionResponses(moldingQuestions!); await this.nextPage(); diff --git a/playwright-tests/pages/OrderConfirmationPage.ts b/playwright-tests/pages/OrderConfirmationPage.ts index 1c6158c68..6e513d7d2 100644 --- a/playwright-tests/pages/OrderConfirmationPage.ts +++ b/playwright-tests/pages/OrderConfirmationPage.ts @@ -178,6 +178,8 @@ export class OrderConfirmationPage extends BasePage { @step("OrderConfirmationPage >> Validate order") async verifyOrderConfirmationPage(testData: Partial) { + + await this.validateProgressBar("100"); await this.validateOrderConfirmationPage(testData); const workOrderNumber = await this.logOrderNumber(); await test.step(`Session Storage Work Order Number: ${workOrderNumber}`, async () => { diff --git a/playwright-tests/pages/PartQuestionPage.ts b/playwright-tests/pages/PartQuestionPage.ts index 831a27dc1..8ecced581 100644 --- a/playwright-tests/pages/PartQuestionPage.ts +++ b/playwright-tests/pages/PartQuestionPage.ts @@ -62,6 +62,8 @@ export class PartQuestionsPage extends BasePage { @step("PartQuestionsPage >> Select Vehicle Part Question Responses") async handlePartQuestionsPage(testData: Partial) { const { partQuestions } = testData; + + await this.validateProgressBar("40"); await this.validatePartQuestions(partQuestions!); await this.selectPartQuestionResponses(partQuestions!); await this.nextPage(); diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 92d0da651..b75157204 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 { AppointmentType, PaymentMethod, PaymentType, ServicePackage, VehicleDamage } from '@business-logic/types/Enums'; +import { AppointmentTimeslot, AppointmentType, PaymentMethod, PaymentType, ServicePackage, VehicleDamage } from '@business-logic/types/Enums'; import { PaymentPage } from './PaymentPage'; import { AfterpayPage } from './AfterpayPage'; import { PaypalPage } from './PaypalPage'; @@ -155,6 +155,12 @@ export class PaymentMethodPage extends BasePage { if (promoCode) { expect.soft(servicePackageValue).toContain(`Promo code ${promoCode} applied`); }; + + // Early Bird line item validation + if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) { + + expect.soft(servicePackageValue).toContain('Early Bird'); + } } } @@ -261,7 +267,7 @@ l async getActualAppointmentDetails(): Promise { let actualAppointmentDetails = new Map(); - let appointmentDetailsSubSections = await this.appointmentDetailsSection.locator('div .py-3').all(); + let appointmentDetailsSubSections = await this.appointmentDetailsSection.locator('.review-table .py-3').all(); for (let element of appointmentDetailsSubSections) { let label = await element.locator('div .text-block').innerText(); @@ -464,6 +470,7 @@ l async handlePaymentMethodPage(testData: Partial) { const { servicePackage, isRecalVehicle, paymentDetails } = testData; + await this.validateProgressBar("92"); await this.validatePaymentDetailsPage(testData); // Verify VAPS wipers on backend for standard and premium packages diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 5daa3060e..16f70b333 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -2,7 +2,7 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IAppointmentDetails, ICustomerDetails } from '@business-logic/types/CustomerDetails'; import { formatDate, formatTime } from '@impl/utils/DateUtils'; -import { AppointmentType, ServiceLocation } from '@business-logic/types/Enums'; +import { AppointmentTimeslot, AppointmentType, ServiceLocation } from '@business-logic/types/Enums'; import { time } from 'console'; import { step } from '@business-logic/types/Step'; import { ITestData } from '@business-logic/types/ITestData'; @@ -47,23 +47,39 @@ export class SchedulePage extends BasePage { await this.modalContinueButton.click(); } - async scheduleFirstAppointment(customerDetails: ICustomerDetails) { + async scheduleFirstAppointment(testData: Partial) { + const { appointmentDetails, customerDetails } = testData; while (!(await this.firstAvailableDate.isVisible())) { + + if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) { + await this.mockScheduleResponseForEarlyBird(customerDetails!); + } await this.viewMoreDatesLink.click(); } - await this.firstAvailableDate.click().then(async () => { - customerDetails.apptDate = `${await this.firstAvailableDate.getAttribute("id")}` - }); - // 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); - }); + if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) { + await this.page.locator(`.selectable-days, [id='${customerDetails?.apptDate}']`).click(); + const earlyBirdTimeSlot = this.timeSlots.filter({ hasText: "Earlybird" }).first(); + await earlyBirdTimeSlot.click().then(async () => { + customerDetails!.apptTime = await this.getFormattedTimeSlot(earlyBirdTimeSlot); + }); + } + else { + await this.firstAvailableDate.click().then(async () => { + customerDetails!.apptDate = `${await this.firstAvailableDate.getAttribute("id")}` + }); + + // 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.apptDuration = (await this.appointmentDuration.innerText()).replace("Duration: ", ""); + customerDetails!.apptDuration = (await this.appointmentDuration.innerText()).replace("Duration: ", ""); await this.nextPage(); } @@ -74,7 +90,7 @@ export class SchedulePage extends BasePage { { 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")) + else if (selectedTimeSlot.includes("-") || selectedTimeSlot.includes("Earlybird")) { formattedTimeSlot = selectedTimeSlot.includes("Earlybird") ? "arriving between 8:00 AM - 12:00 PM" : `arriving between ${selectedTimeSlot}`; } @@ -87,7 +103,8 @@ export class SchedulePage extends BasePage { @step("SchedulePage >> Schedule appointment: ") async handleSchedulePage(testData: Partial) { - const { customerDetails } = testData; - await this.scheduleFirstAppointment(customerDetails!); + + await this.validateProgressBar("72"); + await this.scheduleFirstAppointment(testData); } } \ No newline at end of file diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index a31f38eb5..49577e963 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -1,7 +1,7 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IAppointmentDetails } from '@business-logic/types/CustomerDetails'; -import { AppointmentType } from '@business-logic/types/Enums'; +import { AppointmentTimeslot, AppointmentType } from '@business-logic/types/Enums'; import { AddressForm } from './forms/AddressForm'; import { faker } from '@faker-js/faker'; import { step } from '@business-logic/types/Step'; @@ -76,11 +76,12 @@ export class ServiceLocationPage extends BasePage { this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); } - async selectLocation(appointmentDetails: IAppointmentDetails){ + async selectLocation(testData: Partial) { + const { appointmentDetails, customerDetails } = testData; - switch(appointmentDetails.serviceLocation) { - case AppointmentType.Mobile: - await this.scheduleMobile(appointmentDetails); + switch(appointmentDetails?.serviceLocation) { + case AppointmentType.Mobile: + await this.scheduleMobile(testData); break; case AppointmentType.InShop: await this.scheduleInShop(appointmentDetails); @@ -91,7 +92,6 @@ export class ServiceLocationPage extends BasePage { } } - async scheduleInShop(appointmentDetails?: IAppointmentDetails) { await this.inShopButton.click(); if (appointmentDetails && appointmentDetails.shopAddress) { @@ -116,8 +116,9 @@ export class ServiceLocationPage extends BasePage { } } - async scheduleMobile(appointmentDetails: IAppointmentDetails){ - if (appointmentDetails.serviceAddress) { + async scheduleMobile(testData: Partial) { + const { appointmentDetails, customerDetails } = testData; + if (appointmentDetails?.serviceAddress) { await this.mobileButton.click(); await this.enterServiceAddressButton.click(); await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! }); @@ -129,6 +130,10 @@ export class ServiceLocationPage extends BasePage { } else { await this.vehicleProtectedNoButton.check(); } + if (appointmentDetails.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) + { + await this.mockScheduleResponseForEarlyBird(customerDetails!); + } await this.saveAddressButton.click(); } else { console.error('ServiceLocationPage >> Please supply an address') @@ -159,8 +164,9 @@ export class ServiceLocationPage extends BasePage { @step("ServiceLocationPage >> Select service location: ") async handleServiceLocationPage(testData: Partial) { - const { appointmentDetails } = testData; - await this.selectLocation(appointmentDetails!); + + await this.validateProgressBar("60"); + await this.selectLocation(testData); await this.nextPage(); } } \ No newline at end of file diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 35872828b..6a464d77e 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -185,6 +185,7 @@ export class ServicePackagesPage extends BasePage { async handleServicePackagePage(testData: Partial) { const { customerDetails, paymentMethod, servicePackage, promoCode, canNotRecal, dynamicRecal, hasOemEndorsement, vehicleDamage } = testData; + await this.validateProgressBar("48"); // Define repair damage types (vs. replacement types) const repairTypes: VehicleDamage[] = [ VehicleDamage.WindshieldOneChip, diff --git a/playwright-tests/pages/ServiceZipPage.ts b/playwright-tests/pages/ServiceZipPage.ts index d11ffa139..cf2574b22 100644 --- a/playwright-tests/pages/ServiceZipPage.ts +++ b/playwright-tests/pages/ServiceZipPage.ts @@ -14,6 +14,7 @@ export class ServiceZipPage extends LookupPage { @step("ZipLookupPage >> Lookup by service ZIP: ") async handleServiceZipPage(testData: Partial) { const { customerDetails, vehicleDetails, alertFlags } = testData; + await this.validateProgressBar("32"); await this.enterZip(customerDetails!.address.postalCode!); await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); } diff --git a/playwright-tests/pages/VehicleDamagePage.ts b/playwright-tests/pages/VehicleDamagePage.ts index 1b48a1d4d..6e56553e5 100644 --- a/playwright-tests/pages/VehicleDamagePage.ts +++ b/playwright-tests/pages/VehicleDamagePage.ts @@ -169,6 +169,7 @@ export class VehicleDamagePage extends BasePage { const {vehicleDamage} = testData; const {isRepairReplace, isRepairOnly} = testData.alertFlags || {}; + await this.validateProgressBar("16"); await this.selectDamage(vehicleDamage!); // Handle alert conditions for vehicle damage if (isRepairReplace) { diff --git a/playwright-tests/pages/VehicleLookupAddressPage.ts b/playwright-tests/pages/VehicleLookupAddressPage.ts index ab0ad86f8..9fe02ea6f 100644 --- a/playwright-tests/pages/VehicleLookupAddressPage.ts +++ b/playwright-tests/pages/VehicleLookupAddressPage.ts @@ -31,6 +31,8 @@ export class VehicleLookupAddressPage extends LookupPage { @step("VehicleLookupAddressPage >> Lookup by address: ") async handleVehicleLookupAddressPage(testData: Partial) { const { customerDetails, vehicleDetails, alertFlags } = testData; + + await this.validateProgressBar("32"); await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!); await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); } diff --git a/playwright-tests/pages/VehicleLookupLicensePage.ts b/playwright-tests/pages/VehicleLookupLicensePage.ts index ef7f51f85..6045fc4cc 100644 --- a/playwright-tests/pages/VehicleLookupLicensePage.ts +++ b/playwright-tests/pages/VehicleLookupLicensePage.ts @@ -30,6 +30,8 @@ export class VehicleLookupLicensePage extends LookupPage { @step("VehicleLookupLicensePage >> Lookup by license plate: ") async handleVehicleLookupLicensePage(testData: Partial) { const { customerDetails, vehicleDetails, alertFlags } = testData; + + await this.validateProgressBar("32"); await this.enterPlateDetails(vehicleDetails!); await this.enterZip(customerDetails!.address.postalCode!); await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); diff --git a/playwright-tests/pages/VehiclePartsPage.ts b/playwright-tests/pages/VehiclePartsPage.ts index aabaa1f4e..ca07ffd53 100644 --- a/playwright-tests/pages/VehiclePartsPage.ts +++ b/playwright-tests/pages/VehiclePartsPage.ts @@ -13,6 +13,8 @@ export default class VehiclePartQuestionsPage extends PartQuestionsPage{ @step("VehiclePartsPage >> Select Vehicle Part Questions: ") async handleVehiclePartsPage(testCase: Partial) { const { vehiclePartQuestions } = testCase; + + await this.validateProgressBar("40"); await this.validatePartQuestions(vehiclePartQuestions!); await this.selectPartQuestionResponses(vehiclePartQuestions!); await this.nextPage(); diff --git a/playwright-tests/pages/VehicleSelectionPage.ts b/playwright-tests/pages/VehicleSelectionPage.ts index 049f9fb8d..f554639a5 100644 --- a/playwright-tests/pages/VehicleSelectionPage.ts +++ b/playwright-tests/pages/VehicleSelectionPage.ts @@ -47,6 +47,7 @@ export class VehicleSelectionPage extends BasePage { @step("VehicleSelectionPage >> Select Vehicle: ") async handleVehicleSelectionPage(testData: Partial) { + await this.validateProgressBar("4"); const { vehicleDetails } = testData; const { isHeavyTruckVehicle, isSplitWindshield } = testData.alertFlags || {}; diff --git a/playwright-tests/pages/VinLookupPage.ts b/playwright-tests/pages/VinLookupPage.ts index 1e96c14c5..acbeade7f 100644 --- a/playwright-tests/pages/VinLookupPage.ts +++ b/playwright-tests/pages/VinLookupPage.ts @@ -26,6 +26,8 @@ export class VinLookupPage extends LookupPage { @step("VinLookupPage >> Lookup by VIN: ") async handleVehicleLookupVinPage(testData: Partial) { const { customerDetails, vehicleDetails, alertFlags } = testData; + + await this.validateProgressBar("32"); await this.enterVin(vehicleDetails!.vin!); await this.enterZip(customerDetails!.address.postalCode!); await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); diff --git a/playwright-tests/tests/CashReplaceVinMobile.ts b/playwright-tests/tests/CashReplaceVinMobile.ts index 37470200e..b9acf3542 100644 --- a/playwright-tests/tests/CashReplaceVinMobile.ts +++ b/playwright-tests/tests/CashReplaceVinMobile.ts @@ -1,6 +1,6 @@ //Imports here import { ITestData } from "@business-logic/types/ITestData" -import { AppointmentType, PaymentType } from "@business-logic/types/Enums"; +import { AppointmentTimeslot, AppointmentType, PaymentType } from "@business-logic/types/Enums"; import TestCase from "@business-logic/types/TestCase"; import { VehicleLookupType } from "@business-logic/types/Enums"; import { getDefaultTestData, setFakerSeedFromTestName } from "@business-logic/constants/DefaultTestData"; @@ -48,7 +48,8 @@ const cashReplaceVinMobileData: Partial = { state: 'MD', postalCode: '21237', country: 'United States' - } + }, + appointmentTimeSlot: AppointmentTimeslot.EarlyBird }, // Payment at service From a3234c92130b7f265068ab2d48ca7d25367bf66e Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Wed, 14 May 2025 16:27:36 -0400 Subject: [PATCH 2/9] made final amount due text selector more specific --- playwright-tests/pages/PaymentMethodPage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index b75157204..2c267c69e 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -76,7 +76,7 @@ export class PaymentMethodPage extends BasePage { // Cart panel elements this.cartPanelDetails = this.page.locator('.cart-panel'); this.subtotalText = this.page.locator('.sub-total'); - this.finalAmountDueText = this.page.locator('div.amount-due'); + this.finalAmountDueText = this.cartPanelDetails.locator('div.amount-due'); this.glassServiceText = this.page.locator('div', { hasText: /^Glass service only$/ }); this.wiperBladesText = this.page.locator('div', { hasText: /^New wiper blades$/ }); this.rainDefenseText = this.page.locator('div', { hasText: /^Rain Defense™$/ }); From b3c3411a3736357cf0ef5c9a568dcbb519cec258 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Thu, 15 May 2025 10:47:17 -0400 Subject: [PATCH 3/9] Fixes service location selection Corrects an issue where the service location wasn't being selected properly in certain scenarios. Specifically, it ensures the 'Enter Service Address' button is clicked only when the address text box is not visible. It also removes the unnecessary 'saveAddressButton' click. --- playwright-tests/pages/ServiceLocationPage.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts index 49577e963..375566c04 100644 --- a/playwright-tests/pages/ServiceLocationPage.ts +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -36,7 +36,6 @@ export class ServiceLocationPage extends BasePage { readonly zipCodeTextBox: Locator; readonly vehicleProtectedYesButton: Locator; readonly vehicleProtectedNoButton: Locator; - readonly saveAddressButton: Locator; readonly repeatedClicksModalCloseButton: Locator; url = process.env['BASE_URL']! + '/fmg/?fmgPage=service-location'; @@ -49,7 +48,7 @@ export class ServiceLocationPage extends BasePage { // Initial selection this.inShopButton = this.page.getByText(/In-shop/); - this.mobileButton = this.page.getByText(/Mobile/); + this.mobileButton = this.page.getByText(/Mobile/).nth(0); this.dropOffButton = this.page.getByText(/Drop-off/); this.RecalWarningMessage1 = this.page.getByText(/We're not able to provide mobile service/); this.RecalWarningMessage2 = this.page.getByText(/advanced safety system recalibration needs to be done in our shop./); @@ -72,7 +71,6 @@ export class ServiceLocationPage extends BasePage { this.zipCodeTextBox = this.page.getByRole('textbox', { name: 'Zip code' }); 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.locator('.QSISlider').locator('img[src*=\'close\']'); } @@ -120,7 +118,9 @@ export class ServiceLocationPage extends BasePage { const { appointmentDetails, customerDetails } = testData; if (appointmentDetails?.serviceAddress) { await this.mobileButton.click(); - await this.enterServiceAddressButton.click(); + if (!(await this.serviceAddressTextBox.isVisible())) { + await this.enterServiceAddressButton.click(); + } await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! }); if (await this.repeatedClicksModalCloseButton.isVisible()) { await this.repeatedClicksModalCloseButton.click(); @@ -134,7 +134,6 @@ export class ServiceLocationPage extends BasePage { { await this.mockScheduleResponseForEarlyBird(customerDetails!); } - await this.saveAddressButton.click(); } else { console.error('ServiceLocationPage >> Please supply an address') } From 1ebd4c69a0a2217f2e1bc39a24cdee594f184dc5 Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Fri, 16 May 2025 10:21:20 -0400 Subject: [PATCH 4/9] CASH-580: Add switch to Afterpay button --- src/layouts/payment/payment.vue | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 745d5ebe1..91e3c04c5 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -62,6 +62,23 @@ :isNoComp="isNoComp" :isExpandedOnLoad="true" /> +
+ +
+
{ + return { + buttonLabel: answer.Text, + altText: answer.Text, + groupName: "payment-method", + value: answer.Name, + buttonImage: answer.AnswerImageUrl, + }; + }); + + return tempItems; + }, }, methods: { arePagePrerequisitesValid() { @@ -594,6 +652,15 @@ export default { return preReqResult; }, + getAnswersNullSafe(widgetName) { + const rawData = this.getCmsContent(widgetName, "Answers"); + + if (!rawData) { + return []; + } else { + return rawData; + } + }, getWOrkOrderNumber() { if (store.getters.order.workOrderNumber) { const items = store.getters.order.workOrderNumber.split("-"); @@ -797,6 +864,18 @@ export default { ); } }, + switchToAfterpay() { + this.dispatchStoreAction( + storeActions.SAVE_PAYMENT_METHOD_CHOICE, + paymentMethods.AFTERPAY, + false + ); + const iframe = this.$refs.paymentFrame; + if (iframe) { + iframe.contentWindow.postMessage("afterpay", "*"); + window.scrollTo({ top: 0, left: 0, behavior: "smooth" }); + } + }, setIFrameListener() { window.addEventListener("message", (event) => this.handleIFrameContentWindowMessage(event) @@ -820,6 +899,7 @@ export default { loadingModal, cart, alert, + buttonQuestion, }, }; From c6124a658e96cee380cb119189cb39e0d015eb1d Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Fri, 16 May 2025 10:51:42 -0400 Subject: [PATCH 5/9] Change rain defense to rain repel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrects the premium service package description to align with the updated service offering. The text "Rain Defense™" is replaced with "Rain repel treatment" for accuracy and consistency. --- playwright-tests/pages/PaymentMethodPage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 2c267c69e..0705955f9 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -148,7 +148,7 @@ export class PaymentMethodPage extends BasePage { expect.soft(servicePackageValue).toContain('New wiper blades'); } if (servicePackage === ServicePackage.Premium) { - expect.soft(servicePackageValue).toContain('Rain Defense™'); + expect.soft(servicePackageValue).toContain('Rain repel treatment'); } // Promo Code Validation @@ -383,8 +383,8 @@ l : stringForReplace; break; case ServicePackage.Premium: - stringForRepair.push("New wiper blades", "Rain Defense™"); - stringForReplace.push("New wiper blades", "Rain Defense™"); + stringForRepair.push("New wiper blades", "Rain repel treatment"); + stringForReplace.push("New wiper blades", "Rain repel treatment"); expectedServicePackageDetails["Premium service"] = isRepair ? stringForRepair : stringForReplace; From 339b9113dbf0f041f1ae216e7a894df6a1c2557d Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Fri, 16 May 2025 14:40:52 -0400 Subject: [PATCH 6/9] CASH-636: Keep "As little as" on insurance quote for afterpay breakout display --- .../quote/service-package-question/service-package-question.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue index 546bb573a..2dc3e4028 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -292,7 +292,7 @@ export default { }) ).toFixed(2); - if (this.hasPricingByDay()) { + if (this.hasPricingByDay() && !this.isAfterpayBreakoutDisplay()) { return "$" + formattedPriceFloat; } return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat; From 2203c56638b63b05c77abcab3d402918763b0757 Mon Sep 17 00:00:00 2001 From: Matt Caimi Date: Mon, 19 May 2025 11:45:25 -0400 Subject: [PATCH 7/9] CASH-700 include decimal in total afterpay price --- .../service-package-radio-for-afterpay.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/experiment-components/service-package-radio-for-afterpay.vue b/src/experiment-components/service-package-radio-for-afterpay.vue index f38621462..7e4e4551a 100644 --- a/src/experiment-components/service-package-radio-for-afterpay.vue +++ b/src/experiment-components/service-package-radio-for-afterpay.vue @@ -117,6 +117,9 @@ export default { ); return displayPrice != this.additionalButtonData.strikeThroughPrice; }, + totalAfterpayPrice() { + return parseFloat(this.buttonAuxillaryCopy.replace("$", "")); + }, }, methods: { getRouterLinkDisplayTextFromCopy, @@ -134,11 +137,12 @@ export default { .filter((lineItem) => lineItem); }, afterpayPrice() { - const decimalPrice = parseFloat(this.buttonAuxillaryCopy.replace("$", "")); + const decimalPrice = this.totalAfterpayPrice; return "$" + (decimalPrice / 4).toFixed(2); }, truncatedSinglePayment() { - return "$" + Math.trunc(this.buttonAuxillaryCopy.replace("$", "")); + const decimalPrice = this.totalAfterpayPrice; + return "$" + decimalPrice.toFixed(2); }, hasPackageDiscount() { return ( From 5a5be03cc14de3618cce32b85e0f3fa936855ce8 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Mon, 19 May 2025 15:23:55 -0400 Subject: [PATCH 8/9] Adds local FMG base URL option Adds a commented-out base URL for a local FMG server instance to the development environment configuration. This allows developers to easily test against a local version of FMG by uncommenting the appropriate line. --- playwright-tests/.env.dev | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playwright-tests/.env.dev b/playwright-tests/.env.dev index c89292674..76f1b93ae 100644 --- a/playwright-tests/.env.dev +++ b/playwright-tests/.env.dev @@ -9,6 +9,8 @@ SKIP_CONTENT_SITE=false # Base URLs by environment (uncomment the one you need) # qa BASE_URL="https://www-qa2.safelite.com/" +# local version of FMG (after running the local server) +# BASE_URL="http://localhost:8080/fmg/" # qa with skipToInsurance Turned Off # BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true,NextGen_IGQSkipToInsurance=NextGen_IGQSkipToInsurance_V1=NextGen_IGQSkipToInsurance_CONTROL=true" # sys @@ -25,4 +27,3 @@ ADMIN_SERVICE_API_URL="https://issadminapi.dev.sagaws.net/iss-admin/api/v1/" # API authentication (replace with actual value when running tests) CCIS_API_AUTH="undefined" -SKIP_CONTENT_SITE=false \ No newline at end of file From aa43e2941ae0f31fb2f011f7388b6f146432efb1 Mon Sep 17 00:00:00 2001 From: maguire-arman Date: Mon, 19 May 2025 16:26:41 -0400 Subject: [PATCH 9/9] Updates PayPal login flow Adds username input and "next" button handling to the PayPal login flow. This update is necessary to support the new PayPal login screen, which requires entering the username before the password. --- playwright-tests/business-logic/Data/PaymentData.ts | 1 + playwright-tests/pages/PaypalPage.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/playwright-tests/business-logic/Data/PaymentData.ts b/playwright-tests/business-logic/Data/PaymentData.ts index ae733d755..699a2eeb9 100644 --- a/playwright-tests/business-logic/Data/PaymentData.ts +++ b/playwright-tests/business-logic/Data/PaymentData.ts @@ -28,6 +28,7 @@ const defaultAfterpayDetails: IPaymentDetails = { const defaultPaypalDetails: IPaymentDetails = { paymentType: PaymentType.Paypal, + username: 'itqatest@safelite.com', password: 'Safelite1' } diff --git a/playwright-tests/pages/PaypalPage.ts b/playwright-tests/pages/PaypalPage.ts index 862a97edf..25a5be26b 100644 --- a/playwright-tests/pages/PaypalPage.ts +++ b/playwright-tests/pages/PaypalPage.ts @@ -5,6 +5,9 @@ import { IPaymentDetails } from '@business-logic/types/CustomerDetails'; export class PaypalPage extends BasePage { readonly page: Page; readonly loginWithPasswordButton: Locator; + readonly usernameTextBox: Locator; + readonly nextButton: Locator; + readonly usePasswordInsteadButton: Locator; readonly passwordTextBox: Locator; readonly paypalLoginButton: Locator; readonly completePurchaseButton: Locator; @@ -12,13 +15,19 @@ export class PaypalPage extends BasePage { constructor(page: Page) { super(page); this.page = page; - this.loginWithPasswordButton = page.getByRole('link', { name: 'Log in with a password instead' }); + this.usernameTextBox = page.getByPlaceholder('Email'); + this.nextButton = page.getByRole('button', { name: 'Next' }); + this.loginWithPasswordButton = page.getByRole('button', { name: 'Use Password Instead' }); this.passwordTextBox = page.getByPlaceholder('Password'); this.paypalLoginButton = page.getByRole('button', { name: 'Log In', exact: true }); - this.completePurchaseButton = page.getByTestId('submit-button-initial'); + this.completePurchaseButton = page.getByRole('button', { name: 'Pay $' }); } async completePaypalPurchase(paymentDetails: IPaymentDetails){ + if (!await this.loginWithPasswordButton.isVisible()) { + await this.usernameTextBox.fill(paymentDetails.username!); + await this.nextButton.click(); + } await this.loginWithPasswordButton.click(); await this.passwordTextBox.fill(paymentDetails.password!); await this.paypalLoginButton.click();