Merge pull request #2511 from Safelite/feature/CASH-530

adding changes for earlybird coverage and progress bar validation
This commit is contained in:
maguire-arman 2025-05-13 15:52:44 -04:00 committed by GitHub
commit 7ddb3618ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 134 additions and 33 deletions

View file

@ -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 {

View file

@ -124,5 +124,11 @@ export enum AppointmentType{
DropOff = "Drop-off"
}
export enum AppointmentTimeslot{
EarlyBird = "EarlyBird",
DropOff = "DropOff",
overnight = "Overnight"
}

View file

@ -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}`);
}
}

View file

@ -14,6 +14,7 @@ export default class CapabilityQuestionsPage extends PartQuestionsPage {
async handleCapabilityQuestionsPage(testCase: Partial<ITestData>) {
const { capabilityQuestions } = testCase;
await this.validateProgressBar("40");
// Validate the capability questions are on the page
await this.validatePartQuestions(capabilityQuestions!);

View file

@ -45,6 +45,8 @@ export class ContactDetailsPage extends BasePage {
@step("ContactDetailsPage >> Enter contact details: ")
async handleContactDetailsPage(testData: Partial<ITestData>) {
const { customerDetails } = testData;
await this.validateProgressBar("84");
await this.enterContactDetails(customerDetails!);
await this.nextPage();
}

View file

@ -74,6 +74,7 @@ export class EstimatePage extends BasePage {
@step("EstimatePage >> Select Lookup Type")
async handleEstimatePage(testData: Partial<ITestData>) {
const { vehicleDetails } = testData;
await this.validateProgressBar("28");
await this.vehicleLookup(vehicleDetails!);
}
}

View file

@ -52,6 +52,7 @@ export class InsuranceCompanyPage extends BasePage {
async handleInsuranceCompanyPage(testData: Partial<ITestData>) {
const { claimDetails } = testData;
await this.validateProgressBar("52");
await this.enterInsuranceCompany(claimDetails!.client!);
await this.nextPage();
}

View file

@ -13,6 +13,8 @@ export default class MoldingQuestionsPage extends PartQuestionsPage {
@step("MoldingQuestionsPage >> Select Molding Questions: ")
async handleMoldingQuestionsPage(testCase: Partial<ITestData>) {
const { moldingQuestions } = testCase;
await this.validateProgressBar("40");
await this.validatePartQuestions(moldingQuestions!);
await this.selectPartQuestionResponses(moldingQuestions!);
await this.nextPage();

View file

@ -178,6 +178,8 @@ export class OrderConfirmationPage extends BasePage {
@step("OrderConfirmationPage >> Validate order")
async verifyOrderConfirmationPage(testData: Partial<ITestData>) {
await this.validateProgressBar("100");
await this.validateOrderConfirmationPage(testData);
const workOrderNumber = await this.logOrderNumber();
await test.step(`Session Storage Work Order Number: ${workOrderNumber}`, async () => {

View file

@ -62,6 +62,8 @@ export class PartQuestionsPage extends BasePage {
@step("PartQuestionsPage >> Select Vehicle Part Question Responses")
async handlePartQuestionsPage(testData: Partial<ITestData>) {
const { partQuestions } = testData;
await this.validateProgressBar("40");
await this.validatePartQuestions(partQuestions!);
await this.selectPartQuestionResponses(partQuestions!);
await this.nextPage();

View file

@ -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<any> {
let actualAppointmentDetails = new Map<string, string[]>();
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<ITestData>) {
const { servicePackage, isRecalVehicle, paymentDetails } = testData;
await this.validateProgressBar("92");
await this.validatePaymentDetailsPage(testData);
// Verify VAPS wipers on backend for standard and premium packages

View file

@ -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<ITestData>) {
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<ITestData>) {
const { customerDetails } = testData;
await this.scheduleFirstAppointment(customerDetails!);
await this.validateProgressBar("72");
await this.scheduleFirstAppointment(testData);
}
}

View file

@ -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<ITestData>) {
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<ITestData>) {
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<ITestData>) {
const { appointmentDetails } = testData;
await this.selectLocation(appointmentDetails!);
await this.validateProgressBar("60");
await this.selectLocation(testData);
await this.nextPage();
}
}

View file

@ -185,6 +185,7 @@ export class ServicePackagesPage extends BasePage {
async handleServicePackagePage(testData: Partial<ITestData>) {
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,

View file

@ -14,6 +14,7 @@ export class ServiceZipPage extends LookupPage {
@step("ZipLookupPage >> Lookup by service ZIP: ")
async handleServiceZipPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
await this.validateProgressBar("32");
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
}

View file

@ -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) {

View file

@ -31,6 +31,8 @@ export class VehicleLookupAddressPage extends LookupPage {
@step("VehicleLookupAddressPage >> Lookup by address: ")
async handleVehicleLookupAddressPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
await this.validateProgressBar("32");
await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
}

View file

@ -30,6 +30,8 @@ export class VehicleLookupLicensePage extends LookupPage {
@step("VehicleLookupLicensePage >> Lookup by license plate: ")
async handleVehicleLookupLicensePage(testData: Partial<ITestData>) {
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!);

View file

@ -13,6 +13,8 @@ export default class VehiclePartQuestionsPage extends PartQuestionsPage{
@step("VehiclePartsPage >> Select Vehicle Part Questions: ")
async handleVehiclePartsPage(testCase: Partial<ITestData>) {
const { vehiclePartQuestions } = testCase;
await this.validateProgressBar("40");
await this.validatePartQuestions(vehiclePartQuestions!);
await this.selectPartQuestionResponses(vehiclePartQuestions!);
await this.nextPage();

View file

@ -47,6 +47,7 @@ export class VehicleSelectionPage extends BasePage {
@step("VehicleSelectionPage >> Select Vehicle: ")
async handleVehicleSelectionPage(testData: Partial<ITestData>) {
await this.validateProgressBar("4");
const { vehicleDetails } = testData;
const { isHeavyTruckVehicle, isSplitWindshield } = testData.alertFlags || {};

View file

@ -26,6 +26,8 @@ export class VinLookupPage extends LookupPage {
@step("VinLookupPage >> Lookup by VIN: ")
async handleVehicleLookupVinPage(testData: Partial<ITestData>) {
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!);

View file

@ -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<ITestData> = {
state: 'MD',
postalCode: '21237',
country: 'United States'
}
},
appointmentTimeSlot: AppointmentTimeslot.EarlyBird
},
// Payment at service