import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IAppointmentDetails, ServiceLocation } from 'safelite-playwright-core'; import { formatDate, formatTime } from 'safelite-playwright-core'; import { AppointmentTimeslot } from 'safelite-playwright-core'; import { ProgressBarPercentages, PaymentMethod } from 'framework/localTypes/Enums'; import { time } from 'console'; import { step } from 'framework/localTypes/Step'; import { ITestData } from 'framework/TestData'; export class SchedulePage extends BasePage { readonly page: Page; readonly inShopButton: Locator; readonly mobileButton: Locator; readonly RecalWarningMessage1: Locator; readonly RecalWarningMessage2: Locator; readonly militaryWarningMessage: Locator; readonly changeZipButton: Locator; readonly updateZipTextBox: Locator; readonly safeliteShopsList: Locator; readonly searchButton: Locator; readonly storeAddressText: Locator; readonly saveLocationButton: Locator; readonly moreLocationsButton: Locator; readonly selectAShopOptions: Locator; readonly yourSafeliteShop: Locator; readonly firstAvailableDate: Locator; readonly firstAvailableTime: Locator; readonly modalContinueButton: Locator; readonly dropOffButton: Locator; readonly dateText: Locator; readonly viewMoreDatesLink: Locator; readonly appointmentDuration: Locator; readonly timeSlots: Locator; readonly allDayDropOffButton: Locator; readonly pickATimeButton: Locator; readonly mobileFirstModalCloseButton: Locator; readonly yesButtonRecalAckModal: Locator; readonly noButtonRecalAckModal: Locator; readonly continueButtonRecalAckModal: Locator; readonly mobileFirstModal: Locator; constructor(page: Page) { super(page); this.page = page; this.inShopButton = this.page.locator('label[buttonlabel="You come to our store"]'); this.mobileButton = this.page.locator('label[buttonlabel="Our mobile shop comes to you"]'); 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./); this.militaryWarningMessage = this.page.locator('[class*="widget-name-AlertMilitaryBaseZipWidget"]'); this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))'); this.updateZipTextBox = this.page.locator('#serviceZipCode').filter({ visible: true }); this.safeliteShopsList = this.page.locator('fieldset:has(#chooseShop) label'); this.searchButton = this.page.locator('.search-icon-button'); this.storeAddressText = this.page.locator('#shop-location label:not(:first-child)'); this.saveLocationButton = this.page.getByText('Save location', { exact: true }); this.moreLocationsButton = this.page.locator(".shop-question a").filter({ hasText: "More locations " }); this.selectAShopOptions = this.page.locator('[class="shop-question"]'); this.yourSafeliteShop = this.page.locator("fieldset:has(#chooseShop) label"); this.allDayDropOffButton = this.page.locator("label[buttonlabel*='Drop']"); this.pickATimeButton = this.page.locator("label[buttonlabel='Pick a time']"); this.firstAvailableDate = this.page.locator('.selectable-day').filter({ visible: true}).locator('nth=0'); this.firstAvailableTime = this.page.locator('label').filter({ hasText: /AM|PM/ }).locator('div').locator('nth=0'); this.modalContinueButton = this.page.getByRole('dialog').getByRole('button', { name: 'Continue' }); 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#chooseTimeSlot) label').filter({ visible: true}); this.mobileFirstModalCloseButton = this.page.locator('#mobile-first-modal-container .btn-close'); this.yesButtonRecalAckModal = this.page.locator("label[buttonlabel='Yes']"); this.noButtonRecalAckModal = this.page.locator("label[buttonlabel='No']"); this.continueButtonRecalAckModal = this.page.locator("#recal-ack-modal-container .modal-footer button"); this.mobileFirstModal = this.page.locator("#mobile-first-modal-container"); } async selectLocation(testData: Partial) { const { appointmentDetails, customerDetails } = testData; if (testData.isMSRGlassPart || testData.isDualRecal) { if (testData.isMSRZip) { expect(await this.mobileButton.isVisible()).toBe(true); } else { expect(await this.mobileButton.isVisible()).toBe(false); expect(await this.yellowAlertMessage.locator('.alert-heading').filter({ hasText: ' We\'re not able to provide mobile service.' }).isVisible()).toBe(true); } } switch(appointmentDetails?.serviceLocation) { case ServiceLocation.Mobile: await this.scheduleMobile(testData); break; case ServiceLocation.InShop: case ServiceLocation.DropOff: await this.scheduleInShop(appointmentDetails); break; } } async scheduleInShop(appointmentDetails?: IAppointmentDetails) { await this.inShopButton.isVisible() ? await this.inShopButton.click() : null; if (appointmentDetails && appointmentDetails.shopAddress) { const zipCodeMatch = appointmentDetails.shopAddress.match(/\b\d{5}$/); if (zipCodeMatch) { const zipCode = zipCodeMatch[0]; // Enter the ZIP code into the updateZipTextBox await this.moreLocationsButton.click(); await this.page.waitForTimeout(500); await this.updateZipTextBox.fill(zipCode); await this.searchButton.click().then( async() => await this.page.locator('loader-wrapper').waitFor({state: 'hidden'})); await this.safeliteShopsList.first().click(); // Click the save location button await this.saveLocationButton.click(); await this.waitForPageOrComponentload(); } // await this.inShopButton.click(); // await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click()); } if (appointmentDetails && (appointmentDetails.shopAddress === "" || appointmentDetails.shopAddress === undefined )) { appointmentDetails.shopAddress = ''; await this.storeAddressText.first().waitFor({ state: 'visible' }); const elements = await this.storeAddressText.all(); for (let element of elements) { let addressLine = await element.textContent() || ""; appointmentDetails.shopAddress += addressLine + ", " }; appointmentDetails.shopAddress = appointmentDetails.shopAddress?.slice(0, -2); } } async scheduleMobile(testData: Partial) { const { appointmentDetails, customerDetails } = testData; if (appointmentDetails?.serviceAddress) { await this.mobileButton.click(); } } async scheduleAppointment(appointmentDetails: IAppointmentDetails) { const formattedDate = formatDate(appointmentDetails.appointmentDate!); const formattedTime = formatTime(appointmentDetails.appointmentDate!); const dateInput = this.page.locator(`div[id="${formattedDate}"]`); const timeButton = this.page.locator(`div[aria-label="${formattedTime}"]`); if (await dateInput.isVisible()) { await dateInput.click(); } else { await this.viewMoreDatesLink.click(); await dateInput.click(); } await timeButton.click(); await this.modalContinueButton.click(); } async scheduleFirstAppointment(testData: Partial) { const { appointmentDetails, customerDetails } = testData; // await this.page.waitForTimeout(1000); await this.page.waitForSelector('.date-picker', { state: 'visible' }); await this.page.locator('#date-picker-fieldset .loader').filter({visible: true}).waitFor({ state: 'hidden' }); while (!(await this.firstAvailableDate.isVisible())) { if (appointmentDetails?.appointmentTimeSlot === AppointmentTimeslot.EarlyBird) { await this.mockScheduleResponseForEarlyBird(customerDetails!); } await this.viewMoreDatesLink.click(); } switch(appointmentDetails?.serviceLocation) { case ServiceLocation.InShop: await this.firstAvailableDate.click().then(async () => { customerDetails!.apptDate = `${await this.firstAvailableDate.getAttribute("id")}` }); if (await this.pickATimeButton.isVisible()) { await this.pickATimeButton.click(); } break; case ServiceLocation.DropOff: const inshopAvailableDates = this.page.locator('.selectable-day').filter({ visible: true}).all(); for (const inshopAvailableDate of await inshopAvailableDates) { await inshopAvailableDate.click(); await this.page.waitForTimeout(500); if (await this.allDayDropOffButton.isVisible()) { customerDetails!.apptDate = `${await inshopAvailableDate.getAttribute("id")}`; break; } } break; case ServiceLocation.Mobile: if (appointmentDetails?.appointmentTimeSlot === AppointmentTimeslot.EarlyBird) { await this.page.locator(`.selectable-days, [id='${customerDetails?.apptDate}-mobile']`).click(); } else { await this.firstAvailableDate.click().then(async () => { customerDetails!.apptDate = `${await this.firstAvailableDate.getAttribute("id")}`.replace("-mobile", ""); }); } break; } if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) { const earlyBirdTimeSlot = this.timeSlots.filter({ hasText: "Earlybird" }).first(); await earlyBirdTimeSlot.click().then(async () => { customerDetails!.apptTime = await this.getFormattedTimeSlot(earlyBirdTimeSlot); }) } else if (appointmentDetails?.serviceLocation == ServiceLocation.DropOff) { await this.allDayDropOffButton.click().then(async () => { customerDetails!.apptTime = await this.getFormattedTimeSlot(this.allDayDropOffButton); }); } else { // 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 === ServiceLocation.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click(); customerDetails!.apptDuration = (await this.appointmentDuration.innerText()).replace("Your service will take approximately ", "").replace("Duration: ", ""); } async getFormattedTimeSlot(timeSlot: Locator) { const selectedTimeSlot = await timeSlot.innerText(); let formattedTimeSlot: string = ""; if (selectedTimeSlot.toLowerCase().includes("drop")) { formattedTimeSlot = selectedTimeSlot.includes("overnight") ? "drop off by 5:30 pm on the night of your scheduled appointment. Pick-up time dependent on shop schedule" : "before 9:30 AM" // "drop off before 9:30 AM"; } else if (selectedTimeSlot.includes("-") || selectedTimeSlot.includes("Earlybird")) // - means Mobile time slot where service time is between 8:00 AM - 12:00 PM { formattedTimeSlot = selectedTimeSlot.includes("Earlybird") ? "arriving between 8:00 AM - 12:00 PM" : `arriving between ${selectedTimeSlot}`; } else { formattedTimeSlot = `at ${selectedTimeSlot}`; } return formattedTimeSlot; } async handleMobileFirstPopUp(testData: Partial) { const { customerDetails } = testData; customerDetails!.apptDate = (await this.mobileFirstModal.locator('li', { hasText: 'Date:' }).innerText()).replace('Date:', '').trim(); customerDetails!.apptTime = (await this.mobileFirstModal.locator('li', { hasText: 'Appointment window:' }).innerText()).replace('Appointment window:', 'arriving between').trim(); customerDetails!.apptDuration = (await this.mobileFirstModal.locator('li', { hasText: 'Estimate length:' }).innerText()).replace('Estimate length:', '').trim(); await this.mobileFirstModal.getByRole("button", { name: 'Confirm appointment'}).click(); } @step("SchedulePage >> Schedule appointment: ") async handleSchedulePage(testData: Partial) { const { appointmentDetails, isCanNotRecal, experiments, handleMobileFirstModal } = testData; await this.waitForPageOrComponentload(); await this.validateProgressBar(ProgressBarPercentages.SchedulePage); //Labor2 fee will show for cash orders and some insurance orders but USAA will not show it if (testData.isHeavyTruck && (testData.paymentMethod !== PaymentMethod.Insurance || testData.claimDetails?.client !== 'USAA')) { const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); expect(vuexState.order.lineItems.supportingItems[0].partNumber).toBe("LABOR2"); } if (handleMobileFirstModal) { return await this.handleMobileFirstPopUp(testData); } if (experiments?.isMobileFirst && await this.mobileFirstModalCloseButton.isVisible()) { await this.mobileFirstModalCloseButton.click(); } await this.selectLocation(testData); if (experiments?.isMobileFirst && await this.mobileFirstModalCloseButton.isVisible({timeout: 5000})) { await this.page.waitForTimeout(1000); await this.mobileFirstModalCloseButton.dblclick(); } await this.scheduleFirstAppointment(testData); await this.nextPage(); if (isCanNotRecal) { await this.yesButtonRecalAckModal.click(); await this.continueButtonRecalAckModal.click(); } } }