Merge branch 'develop' into feature/CASH-848

This commit is contained in:
maguire-arman 2025-08-01 13:19:41 -04:00
commit 455bc44195
35 changed files with 450 additions and 163 deletions

View file

@ -43,7 +43,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 66, statements: 64,
}, },
}, },
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit

View file

@ -30,6 +30,7 @@ import { VerifyDetailsPage } from "../pages/VerifyDetailsPage"
import { EndorsementsPage } from "../pages/EndorsementsPage" import { EndorsementsPage } from "../pages/EndorsementsPage"
import { PolicyDriverPage } from "../pages/PolicyDriverPage" import { PolicyDriverPage } from "../pages/PolicyDriverPage"
import { ServiceZipPage } from "../pages/ServiceZipPage" import { ServiceZipPage } from "../pages/ServiceZipPage"
import { MobileDetailsPage } from "pages/MobileDetailsPage";
export interface ITestPages { export interface ITestPages {
capabilityQuestionsPage: CapabilityQuestionsPage, capabilityQuestionsPage: CapabilityQuestionsPage,
@ -49,6 +50,7 @@ export interface ITestPages {
policyVehiclesPage: PolicyVehiclesPage, policyVehiclesPage: PolicyVehiclesPage,
recalibrationInfoPage: RecalibrationInfoPage, recalibrationInfoPage: RecalibrationInfoPage,
schedulePage: SchedulePage, schedulePage: SchedulePage,
mobileDetailsPage: MobileDetailsPage,
serviceLocationPage: ServiceLocationPage, serviceLocationPage: ServiceLocationPage,
servicePackagesPage: ServicePackagesPage, servicePackagesPage: ServicePackagesPage,
serviceZipPage: ServiceZipPage, serviceZipPage: ServiceZipPage,
@ -83,6 +85,7 @@ export const createTestPages: TestPagesFactory<ITestPages> = (page: Page) => {
policyVehiclesPage: new PolicyVehiclesPage(page), policyVehiclesPage: new PolicyVehiclesPage(page),
recalibrationInfoPage: new RecalibrationInfoPage(page), recalibrationInfoPage: new RecalibrationInfoPage(page),
schedulePage: new SchedulePage(page), schedulePage: new SchedulePage(page),
mobileDetailsPage: new MobileDetailsPage(page),
serviceLocationPage: new ServiceLocationPage(page), serviceLocationPage: new ServiceLocationPage(page),
servicePackagesPage: new ServicePackagesPage(page), servicePackagesPage: new ServicePackagesPage(page),
serviceZipPage: new ServiceZipPage(page), serviceZipPage: new ServiceZipPage(page),

View file

@ -19,7 +19,8 @@ export enum ProgressBarPercentages {
ServicePackagePage = '48%', ServicePackagePage = '48%',
InsuranceCompanyPage = '52%', InsuranceCompanyPage = '52%',
ServiceLocationPage = '60%', ServiceLocationPage = '60%',
SchedulePage = '72%', SchedulePage = '64%',
MobileDetailsPage = '76%',
ContactDetailsPage = '84%', ContactDetailsPage = '84%',
PaymentMethodPage = '92%', PaymentMethodPage = '92%',
OrderConfirmationPage = '100%' OrderConfirmationPage = '100%'

View file

@ -28,7 +28,7 @@ export class AfterpayPage extends BasePage {
this.expirationDateTextBox = page.getByTestId('payment-method-cardExpiry-input'); this.expirationDateTextBox = page.getByTestId('payment-method-cardExpiry-input');
this.cvvTextBox = page.getByTestId('payment-method-cardCvv-input'); this.cvvTextBox = page.getByTestId('payment-method-cardCvv-input');
this.confirmButton = page.getByRole('button', { name: 'Confirm' }); this.confirmButton = page.getByRole('button').filter({hasText: 'Confirm'});
} }
async login(password: string) { async login(password: string) {
@ -47,8 +47,6 @@ export class AfterpayPage extends BasePage {
async executeAfterpayPayment(paymentDetails: IPaymentDetails) { async executeAfterpayPayment(paymentDetails: IPaymentDetails) {
await this.login(paymentDetails.password!); await this.login(paymentDetails.password!);
await this.populateCardDetails(paymentDetails);
await this.confirmButton.click(); await this.confirmButton.click();
} }

View file

@ -103,32 +103,31 @@ export class BasePage {
} }
async mockScheduleResponseForEarlyBird(customerDetails: ICustomerDetails) { 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`; 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) => { await this.page.route(apiUrl, async (route) => {
const response = await route.fetch(); const currentDate = new Date().toISOString().split('T')[0]; // e.g., "2025-07-23"
const responseBody = await response.json(); if(route.request().postDataJSON().startDate === currentDate) {
const response = await route.fetch();
const responseBody = await response.json();
responseBody.days.forEach((day: any) => {
responseBody.days.forEach((day: any) => { day.timeSlots.forEach((slot: any) => {
day.timeSlots.forEach((slot: any) => { if (slot.id.includes("AM")) {
if (slot.id.includes("AM")) { slot.offerPremium = true;
slot.offerPremium = true; }
} });
}); });
});
customerDetails.apptDate = responseBody.days.find((day: any) => customerDetails.apptDate = responseBody.days.find((day: any) => day.timeSlots.some((slot: any) => slot.offerPremium === true)).date || undefined;
day.timeSlots.some((slot: any) => slot.offerPremium === true)
).date || undefined;
// Mock the response // Mock the response
await route.fulfill({ await route.fulfill({
response, response,
body: JSON.stringify(responseBody), body: JSON.stringify(responseBody),
}); });
}); }
} });
} }
async validateProgressBar(progressPercentage: string, timeout: number = 60000) { async validateProgressBar(progressPercentage: string, timeout: number = 60000) {

View file

@ -56,7 +56,7 @@ export class CoverageStatementPage extends InsuranceBasePage {
if (await unverifiedDeductibleElement.isVisible() || await this.noCompText.isVisible()) { if (await unverifiedDeductibleElement.isVisible() || await this.noCompText.isVisible()) {
// Click on continue button if unverified header is visible // Click on continue button if unverified header is visible
await this.continueButton.click(); // await this.continueButton.click();
} }
} }

View file

@ -0,0 +1,54 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { VehicleLookupType } from 'safelite-playwright-core';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
import { IVehicleDetails } from 'safelite-playwright-core';
import { VinLookupPage } from './VinLookupPage';
import { VehicleLookupAddressPage } from './VehicleLookupAddressPage';
import { VehicleLookupLicensePage } from './VehicleLookupLicensePage';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { faker } from '@faker-js/faker';
export class MobileDetailsPage extends BasePage {
readonly page: Page;
readonly streetAddressInputBox: Locator;
readonly CityInputBox: Locator;
readonly YesButton: Locator;
readonly NoButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.streetAddressInputBox = page.locator('#streetAddress');
this.CityInputBox = page.locator('#city');
this.YesButton = page.locator('label[buttonlabel="Yes"]');
this.NoButton = page.locator('label[buttonlabel="No"]');
}
async enterMobileDetails(testData: Partial<ITestData>) {
const { appointmentDetails, customerDetails } = testData;
if (appointmentDetails?.serviceAddress) {
await this.streetAddressInputBox.fill(appointmentDetails.serviceAddress!.street);
await this.CityInputBox.fill(appointmentDetails.serviceAddress!.city);
}
else
{
console.error('ServiceLocationPage >> Please supply an address')
}
if (faker.datatype.boolean()) {
await this.YesButton.check();
} else {
await this.NoButton.check();
}
}
@step("MobileDetailsPage >> Enter service address: ")
async handleMobileDetailsPage(testData: Partial<ITestData>) {
await this.validateProgressBar(ProgressBarPercentages.MobileDetailsPage);
await this.enterMobileDetails(testData);
await this.nextPage();
}
}

View file

@ -206,7 +206,7 @@ export class PaymentMethodPage extends BasePage {
break; break;
case PaymentType.AfterPay: case PaymentType.AfterPay:
await this.payInFourButton.click(); await this.payInFourButton.click();
await this.nextPage(); await this.continueButton.click();
// Capture popup // Capture popup
const afterpayPopup = await browserContext.waitForEvent('page'); const afterpayPopup = await browserContext.waitForEvent('page');

View file

@ -1,6 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage'; import { BasePage } from './BasePage';
import { IAppointmentDetails } from 'safelite-playwright-core'; import { IAppointmentDetails, ServiceLocation } from 'safelite-playwright-core';
import { formatDate, formatTime } from 'safelite-playwright-core'; import { formatDate, formatTime } from 'safelite-playwright-core';
import { AppointmentTimeslot } from 'safelite-playwright-core'; import { AppointmentTimeslot } from 'safelite-playwright-core';
import { ProgressBarPercentages } from 'framework/localTypes/Enums'; import { ProgressBarPercentages } from 'framework/localTypes/Enums';
@ -10,6 +10,18 @@ import { ITestData } from 'framework/TestData';
export class SchedulePage extends BasePage { export class SchedulePage extends BasePage {
readonly page: Page; 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 saveZipButton: Locator;
readonly changeShopLocationLink: Locator;
readonly selectAShopOptions: Locator;
readonly yourSafeliteShop: Locator;
readonly firstAvailableDate: Locator; readonly firstAvailableDate: Locator;
readonly firstAvailableTime: Locator; readonly firstAvailableTime: Locator;
readonly modalContinueButton: Locator; readonly modalContinueButton: Locator;
@ -18,11 +30,28 @@ export class SchedulePage extends BasePage {
readonly viewMoreDatesLink: Locator; readonly viewMoreDatesLink: Locator;
readonly appointmentDuration: Locator; readonly appointmentDuration: Locator;
readonly timeSlots: Locator; readonly timeSlots: Locator;
readonly allDayDropOffButton: Locator;
readonly pickATimeButton: Locator;
constructor(page: Page) { constructor(page: Page) {
super(page); super(page);
this.page = page; this.page = page;
this.firstAvailableDate = this.page.locator('.selectable-day').locator('nth=0');
this.inShopButton = this.page.getByText(/In-shop/);
this.mobileButton = this.page.locator('label[buttonlabel="Mobile"]');
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.saveZipButton = this.page.getByText('Save ZIP code', { exact: true });
this.changeShopLocationLink = this.page.locator(".shop-question a").filter({ hasText: "Change shop location " });
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 off all day']");
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.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.modalContinueButton = this.page.getByRole('dialog').getByRole('button', { name: 'Continue' });
this.dropOffButton = this.page.getByText('Drop off your vehicle', { exact: true }); this.dropOffButton = this.page.getByText('Drop off your vehicle', { exact: true });
@ -32,6 +61,49 @@ export class SchedulePage extends BasePage {
this.timeSlots = this.page.locator('fieldset[aria-labelledby=\'chooseTimeSlot\'] label'); this.timeSlots = this.page.locator('fieldset[aria-labelledby=\'chooseTimeSlot\'] label');
} }
async selectLocation(testData: Partial<ITestData>) {
const { appointmentDetails, customerDetails } = testData;
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.click();
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.changeZipButton.click();
await this.page.waitForTimeout(500);
await this.updateZipTextBox.fill(zipCode);
await this.saveZipButton.click();
}
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.yourSafeliteShop.getAttribute("buttonbodycopy") || "";
}
}
async scheduleMobile(testData: Partial<ITestData>) {
const { appointmentDetails, customerDetails } = testData;
if (appointmentDetails?.serviceAddress) {
await this.mobileButton.click();
}
}
async scheduleAppointment(appointmentDetails: IAppointmentDetails) { async scheduleAppointment(appointmentDetails: IAppointmentDetails) {
const formattedDate = formatDate(appointmentDetails.appointmentDate!); const formattedDate = formatDate(appointmentDetails.appointmentDate!);
const formattedTime = formatTime(appointmentDetails.appointmentDate!); const formattedTime = formatTime(appointmentDetails.appointmentDate!);
@ -49,26 +121,60 @@ export class SchedulePage extends BasePage {
async scheduleFirstAppointment(testData: Partial<ITestData>) { async scheduleFirstAppointment(testData: Partial<ITestData>) {
const { appointmentDetails, customerDetails } = testData; 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())) { while (!(await this.firstAvailableDate.isVisible())) {
if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) { if (appointmentDetails?.appointmentTimeSlot === AppointmentTimeslot.EarlyBird) {
await this.mockScheduleResponseForEarlyBird(customerDetails!); await this.mockScheduleResponseForEarlyBird(customerDetails!);
} }
await this.viewMoreDatesLink.click(); 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();
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) { if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) {
await this.page.locator(`.selectable-days, [id='${customerDetails?.apptDate}']`).click();
const earlyBirdTimeSlot = this.timeSlots.filter({ hasText: "Earlybird" }).first(); const earlyBirdTimeSlot = this.timeSlots.filter({ hasText: "Earlybird" }).first();
await earlyBirdTimeSlot.click().then(async () => { await earlyBirdTimeSlot.click().then(async () => {
customerDetails!.apptTime = await this.getFormattedTimeSlot(earlyBirdTimeSlot); 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 { else {
await this.firstAvailableDate.click().then(async () => {
customerDetails!.apptDate = `${await this.firstAvailableDate.getAttribute("id")}`
});
// const timeSlots = this.timeSlots; // const timeSlots = this.timeSlots;
const timeSlotCount = await this.timeSlots.count(); const timeSlotCount = await this.timeSlots.count();
const randomIndex = Math.floor(Math.random() * timeSlotCount); const randomIndex = Math.floor(Math.random() * timeSlotCount);
@ -80,7 +186,6 @@ export class SchedulePage extends BasePage {
// appointmentmentDetails.serviceLocation === ServiceLocation.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click(); // appointmentmentDetails.serviceLocation === ServiceLocation.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();
} }
async getFormattedTimeSlot(timeSlot: Locator) { async getFormattedTimeSlot(timeSlot: Locator) {
@ -103,8 +208,10 @@ export class SchedulePage extends BasePage {
@step("SchedulePage >> Schedule appointment: ") @step("SchedulePage >> Schedule appointment: ")
async handleSchedulePage(testData: Partial<ITestData>) { async handleSchedulePage(testData: Partial<ITestData>) {
const { appointmentDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.SchedulePage); await this.validateProgressBar(ProgressBarPercentages.SchedulePage);
await this.selectLocation(testData);
await this.scheduleFirstAppointment(testData); await this.scheduleFirstAppointment(testData);
await this.nextPage();
} }
} }

View file

@ -1,6 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage'; import { BasePage } from './BasePage';
import { ServicePackage, VehicleDamage } from 'safelite-playwright-core'; import { AppointmentTimeslot, ServicePackage, VehicleDamage } from 'safelite-playwright-core';
import { ProgressBarPercentages } from 'framework/localTypes/Enums'; import { ProgressBarPercentages } from 'framework/localTypes/Enums';
import { PaymentMethod } from "framework/localTypes/Enums"; import { PaymentMethod } from "framework/localTypes/Enums";
import { step } from 'framework/localTypes/Step'; import { step } from 'framework/localTypes/Step';
@ -189,7 +189,7 @@ export class ServicePackagesPage extends BasePage {
@step("ServicePackagePage >> Select Payment Method and Service Type: ") @step("ServicePackagePage >> Select Payment Method and Service Type: ")
async handleServicePackagePage(testData: Partial<ITestData>) { async handleServicePackagePage(testData: Partial<ITestData>) {
const { customerDetails, paymentMethod, paymentDetails, servicePackage, isCanNotRecal, isDynamicRecal, hasOemEndorsement, vehicleDamage } = testData; const { customerDetails, paymentMethod, paymentDetails, servicePackage, isCanNotRecal, isDynamicRecal, hasOemEndorsement, vehicleDamage, appointmentDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.ServicePackagePage); await this.validateProgressBar(ProgressBarPercentages.ServicePackagePage);
// Define repair damage types (vs. replacement types) // Define repair damage types (vs. replacement types)
@ -235,6 +235,10 @@ export class ServicePackagesPage extends BasePage {
if (hasOemEndorsement) { if (hasOemEndorsement) {
await this.verifyOEMPart(); await this.verifyOEMPart();
} }
if (appointmentDetails?.appointmentTimeSlot === AppointmentTimeslot.EarlyBird) {
await this.mockScheduleResponseForEarlyBird(customerDetails!);
}
await this.nextPage(); await this.nextPage();
} }

View file

@ -1,5 +1,5 @@
import { Page } from "@playwright/test"; import { Page } from "@playwright/test";
import { addSmokeTagToRandomTest, Flow } from 'safelite-playwright-core'; import { addSmokeTagToRandomTest, Flow, ServiceLocation } from 'safelite-playwright-core';
import { ValidationOptions } from 'safelite-playwright-core'; import { ValidationOptions } from 'safelite-playwright-core';
import heavyTruckTests from "./alert-validation/alert0001_HeavyTruck"; import heavyTruckTests from "./alert-validation/alert0001_HeavyTruck";
import repairAndReplaceTests from "./alert-validation/alert0002_RepairAndReplace"; import repairAndReplaceTests from "./alert-validation/alert0002_RepairAndReplace";
@ -288,14 +288,19 @@ async function runWorkflow(page: Page, testCase: TestCase) {
//============================= SERVICE SCHEDULING ============================= //============================= SERVICE SCHEDULING =============================
// Select service location // Select service location
let serviceLocationPage = testCase.pages.serviceLocationPage; // let serviceLocationPage = testCase.pages.serviceLocationPage;
await serviceLocationPage.handleServiceLocationPage(testCase.testData); // await serviceLocationPage.handleServiceLocationPage(testCase.testData);
// Schedule appointment // Schedule appointment
let schedulePage = testCase.pages.schedulePage; let schedulePage = testCase.pages.schedulePage;
await schedulePage.handleSchedulePage(testCase.testData); await schedulePage.handleSchedulePage(testCase.testData);
if (testCase.testData.appointmentDetails?.serviceLocation === ServiceLocation.Mobile) {
let mobileDetailsPage = testCase.pages.mobileDetailsPage;
await mobileDetailsPage.handleMobileDetailsPage(testCase.testData);
}
// Enter contact details // Enter contact details
let contactDetailsPage = testCase.pages.contactDetailsPage; let contactDetailsPage = testCase.pages.contactDetailsPage;
await contactDetailsPage.handleContactDetailsPage(testCase.testData); await contactDetailsPage.handleContactDetailsPage(testCase.testData);
@ -376,12 +381,14 @@ export async function handleInsuranceFlow(testCase: TestCase) {
// Handle scenario where user selected Cash to Insurance and needs to go back through the flow // Handle scenario where user selected Cash to Insurance and needs to go back through the flow
// right now we are choosing pay at appointment as payment method for this scenario // right now we are choosing pay at appointment as payment method for this scenario
if (isCashInsuranceFlow) { if (isCashInsuranceFlow) {
let serviceLocationPage = testCase.pages.serviceLocationPage; // Schedule appointment
await serviceLocationPage.nextPage();
//schedule
let schedulePage = testCase.pages.schedulePage; let schedulePage = testCase.pages.schedulePage;
await schedulePage.nextPage(); await schedulePage.handleSchedulePage(testCase.testData);
if (testCase.testData.appointmentDetails?.serviceLocation === ServiceLocation.Mobile) {
let mobileDetailsPage = testCase.pages.mobileDetailsPage;
await mobileDetailsPage.handleMobileDetailsPage(testCase.testData);
}
//customer dertails //customer dertails
let contactDetailsPage = testCase.pages.contactDetailsPage; let contactDetailsPage = testCase.pages.contactDetailsPage;
await contactDetailsPage.nextPage(); await contactDetailsPage.nextPage();

View file

@ -31,7 +31,6 @@ const cashRepairInShopAfterPayData : Partial<ITestData> = {
// Override appointment details // Override appointment details
appointmentDetails: { appointmentDetails: {
...getDefaultTestData().appointmentDetails!, ...getDefaultTestData().appointmentDetails!,
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
}, },
// Use predefined payment data // Use predefined payment data

View file

@ -15,7 +15,17 @@ const cashReplaceGlassPromoInshopData: Partial<ITestData> = {
// CASH Client // CASH Client
paymentMethod: PaymentMethod.SelfPay, paymentMethod: PaymentMethod.SelfPay,
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
street: '4076 Spectacle Dr',
city: 'Columbus',
state: 'Ohio',
postalCode: '43235',
}
},
// Flag for recalibration vehicle // Flag for recalibration vehicle
isRecalVehicle: true, isRecalVehicle: true,

View file

@ -34,7 +34,8 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
// Override for drop-off service // Override for drop-off service
appointmentDetails: { appointmentDetails: {
serviceLocation: ServiceLocation.DropOff, serviceLocation: ServiceLocation.DropOff,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
}, },
// Override vehicle details // Override vehicle details

View file

@ -1,9 +1,10 @@
//Imports here //Imports here
import { ITestData } from 'framework/TestData' import { ITestData } from 'framework/TestData'
import { ServiceLocation } from "safelite-playwright-core"; import { Flow, ServiceLocation } from "safelite-playwright-core";
import { DamageType, PartQuestionType, PaymentType } from 'safelite-playwright-core' import { DamageType, PartQuestionType, PaymentType } from 'safelite-playwright-core'
import { ITestCase } from '../framework/Typedefs' import { ITestCase } from '../framework/Typedefs'
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed for consistent data generation // Set the seed for consistent data generation
setFakerSeedFromTestName("CashReplaceSwitchToInsuranceProgressiveNoComp"); setFakerSeedFromTestName("CashReplaceSwitchToInsuranceProgressiveNoComp");
@ -13,6 +14,10 @@ const cashReplaceSwitchToInsuranceProgressiveNoCompData: Partial<ITestData> = {
...getDefaultTestData(), ...getDefaultTestData(),
isCashInsuranceFlow: true, isCashInsuranceFlow: true,
flow: Flow.Managed,
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Override customer details based on provided ZIP code // Override customer details based on provided ZIP code
customerDetails: { customerDetails: {

View file

@ -48,7 +48,8 @@ const cashReplaceWiperDropoffData: Partial<ITestData> = {
// Override for drop-off service // Override for drop-off service
appointmentDetails: { appointmentDetails: {
serviceLocation: ServiceLocation.DropOff, serviceLocation: ServiceLocation.DropOff,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
}, },
// Payment at service // Payment at service

View file

@ -1,35 +1,35 @@
export const pageProgressMapper = { export const pageProgressMapper = {
//Combine progress bar slide percent and button steps //Combine progress bar slide percent and button steps
vehicle: { vehicle: {
percent: 7,
step: 1,
},
"vehicle-damage": {
percent: 10, percent: 10,
step: 1, step: 1,
}, },
estimate: { "vehicle-damage": {
percent: 14, percent: 14,
step: 1, step: 1,
}, },
"service-zip": { estimate: {
percent: 18, percent: 18,
step: 1, step: 1,
}, },
"service-zip": {
percent: 22,
step: 1,
},
"vin-lookup": { "vin-lookup": {
percent: 18, percent: 22,
step: 1, step: 1,
}, },
"license-plate-lookup": { "license-plate-lookup": {
percent: 18, percent: 22,
step: 1, step: 1,
}, },
"address-lookup": { "address-lookup": {
percent: 18, percent: 22,
step: 1, step: 1,
}, },
"address-vehicles": { "address-vehicles": {
percent: 25, percent: 26,
step: 1, step: 1,
}, },
"part-questions": { "part-questions": {
@ -49,7 +49,7 @@ export const pageProgressMapper = {
step: 1, step: 1,
}, },
quote: { quote: {
percent: 39, //Must be 39 percent: 39, //The first Step 2 must be 39 so the animated bar lines up with the dot
step: 2, step: 2,
}, },
"insurance-company": { "insurance-company": {
@ -61,7 +61,7 @@ export const pageProgressMapper = {
step: 2, step: 2,
}, },
schedule: { schedule: {
percent: 70.5, //Must be 70.5 percent: 70.5, //The first Step 3 must be 70.5 so the animated bar lines up with the dot
step: 3, step: 3,
}, },
"mobile-details": { "mobile-details": {
@ -73,7 +73,7 @@ export const pageProgressMapper = {
step: 3, step: 3,
}, },
"payment-method": { "payment-method": {
percent: 92, //Must be 92 percent: 92, //The first Step 4 must be 92 so the animated bar lines up with the dot
step: 3, step: 3,
}, },
payment: { payment: {

View file

@ -7,7 +7,6 @@
<textBlock <textBlock
v-show="durationTextBlockCopy" v-show="durationTextBlockCopy"
:customText="durationTextBlockCopy" :customText="durationTextBlockCopy"
justifyText="center"
typeStyle="small" typeStyle="small"
marginTopSizeOverride="0" marginTopSizeOverride="0"
class="duration-text-block" /> class="duration-text-block" />
@ -1027,6 +1026,10 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.duration-text-block {
text-align: left;
}
.date-picker-header { .date-picker-header {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@ -1050,7 +1053,6 @@ export default {
} }
.calendar-grid-container { .calendar-grid-container {
margin: 0 auto 1rem auto; margin: 0 auto 1rem auto;
max-width: 360px;
position: relative; position: relative;
transition: transition:
height ease 2s, height ease 2s,
@ -1188,8 +1190,6 @@ export default {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-width: 2.5rem;
width: 2.5rem;
height: 2.5rem; height: 2.5rem;
border: 2px solid transparent; border: 2px solid transparent;
@ -1227,14 +1227,8 @@ export default {
&.selectable-day { &.selectable-day {
label { label {
background-color: $blue-100; background-color: $blue-100;
min-width: 2.75rem;
width: 2.75rem;
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
@include media-breakpoint-up(md) {
min-width: 3rem;
width: 3rem;
}
&:hover { &:hover {
border: 2px solid $blue; border: 2px solid $blue;
} }
@ -1413,18 +1407,13 @@ export default {
line-height: 1.2; line-height: 1.2;
justify-content: center; justify-content: center;
padding: 0.25rem; padding: 0.25rem;
margin: 0.15rem;
} }
&.selectable-day { &.selectable-day {
label { label {
color: $black; color: $black;
background-color: $blue-100; background-color: $blue-100;
min-width: 2.75rem;
width: 2.75rem;
border-radius: 0.25rem; border-radius: 0.25rem;
@include media-breakpoint-up(md) {
min-width: 3rem;
width: 3rem;
}
} }
} }
&.current-day { &.current-day {
@ -1432,7 +1421,6 @@ export default {
font-weight: 600; font-weight: 600;
font-family: "UrbanistSemibold"; font-family: "UrbanistSemibold";
color: $black; color: $black;
font-size-adjust: 0.5; // needed for Averta fonts baseline alignment
} }
} }
} }
@ -1443,7 +1431,6 @@ export default {
color: $black; color: $black;
font-weight: 400; font-weight: 400;
font-family: "UrbanistSemibold"; font-family: "UrbanistSemibold";
font-size-adjust: 0.5; // needed for Averta fonts baseline alignment
&.price { &.price {
font-weight: 400; font-weight: 400;

View file

@ -317,10 +317,18 @@ export default {
} }
} }
.custom-secondary { .custom-secondary {
&:hover { border-radius: 0.5rem;
color: $blue;
border: 1px solid $blue;
&:hover,
&:focus,
&:active {
color: $blue; color: $blue;
background: transparent; background: transparent;
border: 1px solid $blue; border: 1px solid $blue;
box-shadow:
0 0 0 3px #ffffff,
0 0 0 5.5px $blue;
} }
} }
} }

View file

@ -218,14 +218,17 @@ export default {
&.has-subheader { &.has-subheader {
& > .package-specs { & > .package-specs {
& > div:first-of-type { display: inline-flex;
display: flex; @include media-breakpoint-up(md) {
& > div:first-of-type {
display: flex;
}
} }
} }
} }
&.has-package-discount { &.has-package-discount {
min-height: 150px; min-height: 170px;
} }
&:before { &:before {
@ -267,10 +270,6 @@ export default {
+ .package-label { + .package-label {
.package-specs { .package-specs {
max-height: 1000px; max-height: 1000px;
.sub-label {
background-color: $green-200;
}
} }
} }
+ .package-label { + .package-label {
@ -281,11 +280,10 @@ export default {
} }
} }
+ .package-label { + .package-label {
background-color: $blue-100;
border: 1px solid $blue; border: 1px solid $blue;
max-height: 500px; max-height: 500px;
.special-save-box { .special-save-box {
background-color: $green-200; background-color: $green-100;
} }
} }
+ .package-label { + .package-label {
@ -336,12 +334,16 @@ export default {
&.sub-label { &.sub-label {
color: $green; color: $green;
background-color: $green-100;
padding: 0.25rem 0.5rem; padding: 0.25rem 0;
border-radius: 0.75rem;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.75rem; font-size: 0.75rem;
margin-left: auto; margin-left: auto;
@include media-breakpoint-up(md) {
background-color: $green-100;
padding: 0.25rem 0.5rem;
border-radius: 0.75rem;
}
} }
} }
span { span {
@ -429,14 +431,17 @@ export default {
background-color: $green-100; background-color: $green-100;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin: 0.5rem auto 1rem -1.25rem; margin: 0.25rem auto 0 -1.25rem;
padding: 0.5rem 1rem; padding: 0.25rem 1rem;
border-radius: 0.25rem; border-radius: 0.25rem;
span { span {
color: $green; color: $green;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
} }
@include media-breakpoint-up(md) {
margin: 1rem auto 0 -1.25rem;
}
} }
.pricing-info { .pricing-info {
color: $green; color: $green;
@ -447,6 +452,7 @@ export default {
margin-left: 1.25rem; margin-left: 1.25rem;
font-weight: 600; font-weight: 600;
line-height: 20px; line-height: 20px;
font-size: 1.25rem;
span.strikethrough-price { span.strikethrough-price {
text-decoration: line-through; text-decoration: line-through;

View file

@ -318,7 +318,6 @@ export default {
} }
} }
+ .package-label { + .package-label {
background-color: $blue-100;
border: 1px solid $blue; border: 1px solid $blue;
max-height: 500px; max-height: 500px;
.special-save-box { .special-save-box {

View file

@ -19,10 +19,10 @@
v-if="showThisQuestionChain(questionsDatum, i)" v-if="showThisQuestionChain(questionsDatum, i)"
:answerKey="questionsDatum.answerKey" :answerKey="questionsDatum.answerKey"
:validationRules="validationRules" /> :validationRules="validationRules" />
<!-- Save your progress slot -->
<slot></slot>
</div> </div>
<slot></slot>
<navbar <navbar
ref="navbar" ref="navbar"
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
@ -88,6 +88,12 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.nav-bar {
margin-top: 1rem;
}
.save-progress-modal-question {
margin: 2rem 0;
}
.questions-page { .questions-page {
.question-text { .question-text {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;

View file

@ -14,7 +14,7 @@
<buttonMain <buttonMain
ref="buttonMain" ref="buttonMain"
isPrimary isPrimary
:buttonText="buttonText" :buttonText="overrideButtonText || buttonText"
loaderColor="white" loaderColor="white"
:class="isForwardActionDisabled && 'form-test-invalid'" :class="isForwardActionDisabled && 'form-test-invalid'"
:aria-disabled="isForwardActionDisabled" :aria-disabled="isForwardActionDisabled"
@ -28,7 +28,7 @@
<div <div
v-if="!isBackButtonHidden" v-if="!isBackButtonHidden"
class="col-auto link-col py-1 text-break" class="col-auto link-col py-1 text-break"
:class="[buttonSize ? 'back-centered-below mt-5' : '']"> :class="[buttonSize ? 'mt-5' : '']">
<textLink <textLink
linkType="navigation" linkType="navigation"
:text="backLink" :text="backLink"
@ -56,6 +56,7 @@ export default {
cmsWidgetName: String, cmsWidgetName: String,
buttonSize: { type: Boolean, default: false }, buttonSize: { type: Boolean, default: false },
isSubmitHidden: { type: Boolean, default: false }, isSubmitHidden: { type: Boolean, default: false },
overrideButtonText: String,
}, },
components: { components: {
textLink, textLink,
@ -134,11 +135,6 @@ export default {
width: 100%; width: 100%;
} }
.back-centered-below {
display: flex;
justify-content: center;
}
@media only screen and (min-width: 340px) { @media only screen and (min-width: 340px) {
.col, .col,
.col-auto, .col-auto,
@ -147,11 +143,6 @@ export default {
justify-content: flex-end; justify-content: flex-end;
} }
.back-centered-below {
display: flex;
justify-content: center;
}
.btn-primary { .btn-primary {
width: auto; width: auto;
} }

View file

@ -1,15 +1,13 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="page-container-grouped-styles position-relative"> <loadingModal notFullScreen ref="loadingModal" />
<div> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container-fluid pb-2"> <div class="container page-container-grouped-styles">
<div class="row justify-content-center"> <div class="row">
<div class="col-md-6"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<funnelHeader <div class="header-container">
cmsWidgetName="FunnelHeaderWidget" <img :src="ScheduleConfirmationImage" />
ref="funnelHeader" <span v-html="ScheduleConfirmationText"></span>
hideSalesforceWebchatLaunchButton />
</div>
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6 col-xl-4"> <div class="col-md-6 col-xl-4">
@ -85,7 +83,63 @@
<hr class="mb-5" /> <hr class="mb-5" />
</div> </div>
<addToCalendar
mobileWidgetName="AddToCalendar_Mobile"
inShopWidgetName="AddToCalendar_InShop"
dropOffWidgetName="AddToCalendar_DropOff"
overnightDropOffWidgetName="AddToCalendar_OvernightDropOff"
allDayDropOffWidgetName="AddToCalendar_AllDayDropOff"
sameDayDropOffWidgetName="AddToCalendar_SameDayDropOff"
:serviceLocationFullAddress="ServiceLocationFullAddress"
:providerFullAddress="ProviderFullAddress"
:appointmentType="AppointmentType"
:scheduleDate="ScheduleDate"
:scheduleStartTime="ScheduleStartTime"
:scheduleEndTime="ScheduleEndTime" />
<div class="appointment-text" v-html="AppointmentWordingText"></div>
<textBlock
:customText="AppointmentDuration"
justifyText="center"
typeStyle="medium"
class="duration-text-block" />
</div> </div>
<div class="mt-3" v-if="shouldDisplayFosterLove">
<donationBlock
cmsWidgetName="DonationWidget"
v-model="donationAmount"
:donationValues="donationValues"
:showDonationSuccess="showDonationSuccess"
:showDonationError="showDonationError"
@DonationAddedEvent="addDonationToOrder"
ref="donationBlock" />
</div>
<hr class="mt-5 mb-0" />
<cart
v-if="ShowCart"
:damage="damageInfo"
:availableVaps="vaps"
:allowItemRemoval="false"
v-model="lineItemsWithoutDonation"
:donationCartItem="donationLineItem"
:showAsPaid="isPia"
servicePackageOptionsCmsName="ServicePackageTitle"
:isInsurance="isInsurance"
:insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName"
:showInsuranceCoverageAs="showInsuranceCoverageAs"
:shouldHideRecalibration="shouldHideRecalibration"
:isExpandedOnLoad="false"
:isItac="isItac"
:isNoComp="isNoComp"
:isMSRFeeApplicable="isMSRFeeApplicable" />
<hr class="mt-4 mb-5" />
</div> </div>
</div> </div>
</div> </div>
@ -94,7 +148,6 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import addToCalendar from "@/layouts/confirmation/add-to-calendar/add-to-calendar"; import addToCalendar from "@/layouts/confirmation/add-to-calendar/add-to-calendar";
import cart from "@/fmg-components/cart/cart"; import cart from "@/fmg-components/cart/cart";
import textBlock from "@/digital-components/text-block/text-block"; import textBlock from "@/digital-components/text-block/text-block";
@ -620,7 +673,8 @@ export default {
.header-container { .header-container {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 1rem 0; justify-content: flex-start;
padding: 1.5rem 0 0.5rem 0;
p { p {
display: inline-block; display: inline-block;

View file

@ -51,7 +51,7 @@
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction" />
<textBlock <textBlock
class="mb-5 disclaimer-block" class="mb-5"
cmsWidgetName="DisclaimerCopyWidget" cmsWidgetName="DisclaimerCopyWidget"
typeStyle="caption" /> typeStyle="caption" />
</div> </div>
@ -215,10 +215,3 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss" scoped>
.disclaimer-block {
position: relative;
z-index: 2;
}
</style>

View file

@ -44,7 +44,7 @@
</transition> </transition>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="row mb-4" aria-live="polite"> <div class="row mb-4" aria-live="polite">
<div class="col"> <div class="col-8">
<dropdownQuestion <dropdownQuestion
customDropdownId="state" customDropdownId="state"
cmsWidgetName="StateQuestionWidget" cmsWidgetName="StateQuestionWidget"
@ -56,7 +56,7 @@
:labelBold="labelBold" :labelBold="labelBold"
:isDisabled="this.isStateDisabled" /> :isDisabled="this.isStateDisabled" />
</div> </div>
<div class="col"> <div class="col-4">
<textboxQuestion <textboxQuestion
customInputId="zipCode" customInputId="zipCode"
cmsWidgetName="ZipQuestionWidget" cmsWidgetName="ZipQuestionWidget"

View file

@ -1,15 +1,15 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal ref="loadingModal" /> <loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles"> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="row justify-content-center"> <div class="container position-relative">
<div class="col-md-6"> <div class="row">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4 mt-4">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<hr class="my-3" />
<textBlock cmsWidgetName="AppointmentTimeOfService" class="time-header" />
<textBlock cmsWidgetName="AppointmentDateAndTime" />
<hr class="my-3" />
<div class="mobile-location-questions"> <div class="mobile-location-questions">
<div class="address-questions-container"> <div class="address-questions-container">
<mobileAddressQuestions <mobileAddressQuestions
@ -150,8 +150,11 @@ export default {
}, },
}, },
computed: { computed: {
headerText() { AppointmentDateAndTime() {
return this.getCmsContent("MobileLocationModalWidget", "HeaderText"); return this.getCmsContent("AppointmentDateAndTime", "Text");
},
AppointmentTimeOfService() {
return this.getCmsContent("AppointmentDateAndTime", "Text");
}, },
}, },
components: { components: {
@ -166,3 +169,11 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss" scoped>
.time-header {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
font-weight: 600;
color: $black;
}
</style>

View file

@ -963,5 +963,6 @@ function setupMocks({ customMountOptions }) {
const wrapper = shallowMount(quote, mountOptions); const wrapper = shallowMount(quote, mountOptions);
wrapper.vm.setCmsContent = jest.fn(); wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.getCmsContent = jest.fn();
return { wrapper }; return { wrapper };
} }

View file

@ -4,7 +4,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container quote position-relative"> <div class="container quote position-relative">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8 col-xl-7"> <div class="col-12">
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
</div> </div>
</div> </div>
@ -78,7 +78,10 @@
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton" :isBackButtonHidden="shouldHideBackButton"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction"
:overrideButtonText="
isInsuranceSelected ? isInsuranceContinueButtonText : ''
" />
<saveProgressModalQuestion <saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget" modalWidgetName="SaveProgressModalWidget"
@ -567,6 +570,9 @@ export default {
this.$refs?.servicePackage?.savePackageInfoToPageData?.(); this.$refs?.servicePackage?.savePackageInfoToPageData?.();
}, },
computed: { computed: {
isInsuranceContinueButtonText() {
return this.getCmsContent("isInsuranceContinueButtonText", "Text");
},
lineItemsCloneForWatcher() { lineItemsCloneForWatcher() {
return Object.assign({}, this.lineItems); return Object.assign({}, this.lineItems);
}, },
@ -616,7 +622,7 @@ export default {
methods: { methods: {
getColCount() { getColCount() {
if (this.packageNumber > 2) { if (this.packageNumber > 2) {
return "col-xl-10"; return "col-xl-12";
} else { } else {
return "col-xl-6"; return "col-xl-6";
} }
@ -927,6 +933,9 @@ export default {
} }
.funnel-sub-header { .funnel-sub-header {
:deep(h5) {
text-align: center;
}
p { p {
font-size: 1rem; font-size: 1rem;
} }

View file

@ -246,7 +246,6 @@ export default {
} }
} }
+ .package-label { + .package-label {
background-color: $blue-100;
border: 1px solid $blue; border: 1px solid $blue;
max-height: 500px; max-height: 500px;
.special-save-box { .special-save-box {

View file

@ -117,4 +117,10 @@ export default {
margin-top: 14rem; margin-top: 14rem;
} }
} }
:deep(.menu-modal-container) {
display: none;
}
:deep(.progress-bar-outer) {
opacity: 0;
}
</style> </style>

View file

@ -60,8 +60,8 @@
alertClass="alert-success" /> alertClass="alert-success" />
</div> </div>
</div> </div>
<div class="row justify-content-center appointment-type"> <div class="row appointment-type">
<div class="col-md-6"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<appointmentTypeQuestion <appointmentTypeQuestion
v-model="appointmentTypeFromAppointmentTypeQuestion" v-model="appointmentTypeFromAppointmentTypeQuestion"
v-show="isAppointmentTypeDisplayed" v-show="isAppointmentTypeDisplayed"
@ -78,9 +78,9 @@
</div> </div>
</div> </div>
<div <div
class="row justify-content-center your-shop-location" class="row your-shop-location"
v-if="appointmentType && appointmentType !== appointmentTypeStrings.MOBILE"> v-if="appointmentType && appointmentType !== appointmentTypeStrings.MOBILE">
<div class="col-md-6 col-xl-4"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<button-question <button-question
class="shop-question-button" class="shop-question-button"
ref="buttonQuestion" ref="buttonQuestion"
@ -91,9 +91,8 @@
:questionText="questionText" :questionText="questionText"
buttonTypeString="shopListButton" buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton" :buttonTypeObject="shopListButton"
groupName="chooseShop" groupName="chooseShop" />
textPosition="text-start" /> <div>
<div class="text-center">
<textBlock <textBlock
v-if="mobileFeeApplies && isMobileSelected" v-if="mobileFeeApplies && isMobileSelected"
:customText="mobileFeeText" :customText="mobileFeeText"
@ -113,8 +112,8 @@
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" /> <contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
</div> </div>
</div> </div>
<div class="row justify-content-center date-picker-wrapper"> <div class="row date-picker-wrapper">
<div class="col-md-6 col-xl-4"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" /> <locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePicker <datePicker
:currentZip="zipCode" :currentZip="zipCode"
@ -1169,7 +1168,7 @@ export default {
if (this.preSelectedDate) this.selectedDate = this.preSelectedDate; if (this.preSelectedDate) this.selectedDate = this.preSelectedDate;
if (!this.selectedDate) { if (!this.preSelectedDate) {
// if no date is preselected on load, then select the first available // if no date is preselected on load, then select the first available
let selectedDateMobile = this.getSelectedDateForMobile(); let selectedDateMobile = this.getSelectedDateForMobile();
let selectedDateInshop = this.getSelectedDateForInshop(); let selectedDateInshop = this.getSelectedDateForInshop();
@ -1656,6 +1655,17 @@ export default {
AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) && AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) &&
this.selectedProvider this.selectedProvider
) { ) {
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
this.lastSelectedInshopOrDropoffProvider = this.selectedProvider; this.lastSelectedInshopOrDropoffProvider = this.selectedProvider;
} }
this.appointmentType = AppointmentTypeStrings.MOBILE; this.appointmentType = AppointmentTypeStrings.MOBILE;
@ -1737,7 +1747,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.container-fluid { .container {
&.page-schedule { &.page-schedule {
padding: 0 1rem; padding: 0 1rem;
.text-link-small { .text-link-small {
@ -1774,6 +1784,10 @@ export default {
padding: 0 0.75rem; padding: 0 0.75rem;
} }
} }
.question-text > span {
display: flex;
justify-content: flex-start;
}
} }
.shop-question-button .list-button-content { .shop-question-button .list-button-content {
background: $blue-100; background: $blue-100;

View file

@ -187,10 +187,14 @@ export default {
let appointmentTypeCmsWidgetName; let appointmentTypeCmsWidgetName;
if ( if (
this.selectedDate == null ||
this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF || this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF ||
this.appointmentType === AppointmentTypeStrings.IN_SHOP || this.appointmentType === AppointmentTypeStrings.IN_SHOP ||
this.appointmentType === AppointmentTypeStrings.DROP_OFF this.appointmentType === AppointmentTypeStrings.DROP_OFF
) { ) {
if (!this.selectedAnswerForTimeSlots && !this.selectedAnswerForDropOffOrInshop) {
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
}
return null; return null;
// This is planned to be used again for drop-off // This is planned to be used again for drop-off
// Wrote this to be ready for that eventuality, is untested and no HTML work done yet // Wrote this to be ready for that eventuality, is untested and no HTML work done yet
@ -205,6 +209,10 @@ export default {
// ); // );
// } // }
} else { } else {
if (!this.selectedAnswerForTimeSlots && this.selectedRouteCode) {
// Clearing selectedRouteCode if no time slot is selected
this.resetSelectedTimeSlot();
}
appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes( appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes(
PREMIUM_TIME_SLOT_ID_FLAG PREMIUM_TIME_SLOT_ID_FLAG
) )
@ -394,6 +402,8 @@ export default {
this.setSelectedRouteCodeFromParent(); this.setSelectedRouteCodeFromParent();
}, },
async setSelectedTimeSlot() { async setSelectedTimeSlot() {
this.selectedRouteCode =
this.selectedAnswerForTimeSlots || this.selectedAnswerForDropOffOrInshop;
this.$emit( this.$emit(
"update:modelValue", "update:modelValue",
this.getSelectedTimeSlotInfoObject(this.selectedRouteCode) this.getSelectedTimeSlotInfoObject(this.selectedRouteCode)
@ -483,12 +493,13 @@ export default {
}, },
autoSelectTimeSlotIfOnlyOneIsAvailable() { autoSelectTimeSlotIfOnlyOneIsAvailable() {
const numberOfOptions = this.timeSlotsForSelectedDate?.timeSlots?.length; const numberOfOptions = this.timeSlotsForSelectedDate?.timeSlots?.length;
if (numberOfOptions === 1) { if (this.appointmentType && numberOfOptions === 1) {
if (this.availableTimeSlots.length > 0) { if (this.availableTimeSlots.length > 0) {
this.selectedAnswerForTimeSlots = this.availableTimeSlots[0].value; this.selectedAnswerForTimeSlots = this.availableTimeSlots[0].value;
} else { } else {
this.selectedAnswerForDropOffOrInshop = this.answersForDropOffQuestion[0].value; this.selectedAnswerForDropOffOrInshop = this.answersForDropOffQuestion[0].value;
} }
this.setSelectedTimeSlot();
} }
}, },
addPremiumFlagToInput(routeCode) { addPremiumFlagToInput(routeCode) {

View file

@ -57,6 +57,9 @@ export default {
font-size: 0.875rem; font-size: 0.875rem;
} }
.vin-toggle { .vin-toggle {
display: inline-flex;
justify-content: center;
align-items: center;
&:after { &:after {
content: ""; content: "";
transition: all 0.5s ease; transition: all 0.5s ease;

View file

@ -135,7 +135,7 @@ export default {
outline: none; outline: none;
box-shadow: none; box-shadow: none;
color: $white; color: $white;
background: linear-gradient(84.45deg, #125b7e 0%, #3b8fb8 100%); background: $blue;
border-radius: 0.5rem; border-radius: 0.5rem;
z-index: 2; z-index: 2;
} }