Merge branch 'develop' into dependabot/npm_and_yarn/multi-75e6bc5210

This commit is contained in:
matthew-sykes 2026-02-23 16:45:07 -05:00 committed by GitHub
commit 8544592a5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
141 changed files with 4290 additions and 3920 deletions

View file

@ -13,6 +13,8 @@ const environmentVariables = {
['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx",
['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30,
['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "",
['__VUE_APP_ADYEN_ENVIRONMENT__']: "test",
['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
};
const keysOnly = Object.keys(environmentVariables);

View file

@ -10,7 +10,6 @@ module.exports = {
},
transformIgnorePatterns: ["'/node_modules/(?!vee-validate)"],
moduleFileExtensions: ["js", "vue"],
modulePathIgnorePatterns: ["vin-lookup"],
collectCoverageFrom: [
"src/**/*.{js,vue}",
"!src/main.js",
@ -33,11 +32,7 @@ module.exports = {
"!src/layouts/payment-pia-return/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development
"!src/layouts/schedule/**/*.vue", // Temp test exclusion while in development
"!src/digital-components/date-picker/**/*.vue", // Temp test exclusion while in development
"!src/**/*-june-2025.vue", // Exclude these temporary files for CASH-845 project
"!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development
"!src/experiment-components/*.vue",
// END
@ -45,7 +40,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 64,
statements: 68,
},
},
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit

41
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "Safelite",
"version": "0.1.0",
"dependencies": {
"@adyen/adyen-web": "^6.27.0",
"@iframe-resizer/child": "^5.3.3",
"axios": "^0.30.2",
"bootstrap": "^5.3.3",
@ -59,6 +60,18 @@
"node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22"
}
},
"node_modules/@adyen/adyen-web": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/@adyen/adyen-web/-/adyen-web-6.27.0.tgz",
"integrity": "sha512-6E6hqEkMkoY/FevjO53zhgeCWMt+i/PDc11MX6eVM7Fvd+Sdgc9cIwTBz+NKIXs4ZerG6n2gd+ZIZEywqF/aBQ==",
"license": "MIT",
"dependencies": {
"@types/applepayjs": "14.0.9",
"@types/googlepay": "0.7.8",
"classnames": "2.5.1",
"preact": "10.22.1"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@ -3173,6 +3186,12 @@
"node": ">=10.13.0"
}
},
"node_modules/@types/applepayjs": {
"version": "14.0.9",
"resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-14.0.9.tgz",
"integrity": "sha512-xEprYbb0TEP/XIiDPbVnTYpDai8fTFpsQfVSfTd81Is2GOMUy7ie019eyX6Mz2ECxfjoUVKaiGSL577roIeHCg==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3328,6 +3347,12 @@
"@types/send": "*"
}
},
"node_modules/@types/googlepay": {
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.8.tgz",
"integrity": "sha512-EHl7G7jIeit/Px/fi/Du/19SZDvqrRhy0DjJX40Vzs/97m3sro9mzVyAYNC0pfzpWGDY+zPjtTLEfhV+OYWUcQ==",
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@ -5789,6 +5814,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@ -13681,6 +13712,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/preact": {
"version": "10.22.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz",
"integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View file

@ -12,6 +12,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@adyen/adyen-web": "^6.27.0",
"@iframe-resizer/child": "^5.3.3",
"axios": "^0.30.2",
"bootstrap": "^5.3.3",

View file

@ -5,7 +5,11 @@
PLAYWRIGHT_ENV="qa"
# Skip content site
SKIP_CONTENT_SITE=false
SKIP_CONTENT_SITE="false"
# Experiments Flag
IS_MOBILEFIRST="false"
IS_ADYENPAYMENTS="false"
# Base URLs by environment
# qa
@ -13,7 +17,8 @@ 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"
# BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true"
# sys
# BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle"
# dev

View file

@ -5,15 +5,20 @@
PLAYWRIGHT_ENV="qa"
# Skip content site
SKIP_CONTENT_SITE=false
SKIP_CONTENT_SITE="false"
# Experiments Flag
IS_MOBILEFIRST="false"
IS_ADYENPAYMENTS="false"
# Base URLs by environment
# qa
# BASE_URL="https://www-qa2.safelite.com/"
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://www-qa2.safelite.com/?&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true"
# BASE_URL="https://www-qa2.safelite.com/?&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true"
# sys
# BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle"
# dev

View file

@ -1,9 +1,22 @@
import { ITestData as base } from 'safelite-playwright-core'
import { ITestData as base, getDefaultTestData } from 'safelite-playwright-core'
import { PaymentMethod } from './localTypes/Enums'
import { IExperiments } from './localTypes/IExperiments';
export interface ITestData extends base {
// Put any project-specific data here. Anything useful to other Safelite projects should be submitted as a pull request to safelite-playwright-core.
paymentMethod: PaymentMethod
isOptedInForTextMessages: boolean
totalAmount?: number
}
paymentMethod: PaymentMethod,
isOptedInForTextMessages: boolean,
totalAmount?: number,
isForcedOEM?: boolean,
mockFirstInshopCallNoSchedule?: boolean,
handleMobileFirstModal?: boolean,
experiments?: IExperiments
}
export function getDefaultExperimentsData(): IExperiments {
return {
isAdyenPayments: !!process.env.IS_ADYENPAYMENTS && process.env.IS_ADYENPAYMENTS !== "" ? process.env.IS_ADYENPAYMENTS === "true" : false,
isMobileFirst: !!process.env.IS_MOBILEFIRST && process.env.IS_MOBILEFIRST !== "" ? process.env.IS_MOBILEFIRST === "true" : false,
}
}

View file

@ -24,6 +24,7 @@ import { CCPolicyInfoPage } from "../pages/CCPolicyInfoPage"
import { DuplicateCheckPage } from "../pages/DuplicateCheckPage"
import { PolicyVehiclesPage } from "../pages/PolicyVehiclesPage"
import { PolicyInfoSubmittedPage } from "../pages/PolicyInfoSubmittedPage"
import { HeritageProblemGlassQuestionsPage } from "../pages/HeritageProblemGlassQuestionsPage"
import RecalibrationInfoPage from "../pages/RecalibrationInfoPage"
import { CoverageStatementPage } from "../pages/CoverageStatementPage"
import { VerifyDetailsPage } from "../pages/VerifyDetailsPage"
@ -48,6 +49,7 @@ export interface ITestPages {
paymentMethodPage: PaymentMethodPage,
policyInfoSubmittedPage: PolicyInfoSubmittedPage,
policyVehiclesPage: PolicyVehiclesPage,
heritageProblemGlassQuestionsPage: HeritageProblemGlassQuestionsPage,
recalibrationInfoPage: RecalibrationInfoPage,
schedulePage: SchedulePage,
mobileDetailsPage: MobileDetailsPage,
@ -83,6 +85,7 @@ export const createTestPages: TestPagesFactory<ITestPages> = (page: Page) => {
paymentMethodPage: new PaymentMethodPage(page),
policyInfoSubmittedPage: new PolicyInfoSubmittedPage(page),
policyVehiclesPage: new PolicyVehiclesPage(page),
heritageProblemGlassQuestionsPage: new HeritageProblemGlassQuestionsPage(page),
recalibrationInfoPage: new RecalibrationInfoPage(page),
schedulePage: new SchedulePage(page),
mobileDetailsPage: new MobileDetailsPage(page),

View file

@ -0,0 +1,4 @@
export interface IExperiments {
isMobileFirst: boolean,
isAdyenPayments: boolean
}

View file

@ -18,6 +18,7 @@ export class BasePage {
readonly buttonLoadSpin: Locator;
readonly hamburgerMenu: Locator;
readonly progressBar: Locator;
readonly loaders: Locator;
constructor(page: Page) {
this.page = page;
@ -27,6 +28,7 @@ export class BasePage {
this.buttonLoadSpin = page.getByRole('alert');
this.hamburgerMenu = this.page.getByRole('button', { name: 'Hamburger Menu (modal window)' });
this.progressBar = this.page.locator('.progress-bar-outer .progress-bar-inner');
this.loaders = this.page.locator('.spinner-border, .loader');
}
async nextPage() {
@ -131,6 +133,47 @@ export class BasePage {
});
}
async mockScheduleResponseForFirstInshopCallNoSchedule(customerDetails: ICustomerDetails) {
let fistInshopScheduleCall = true;
const apiUrl = `https://digitalapi.${process.env['PLAYWRIGHT_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/schedule/api/v1/schedule/shop-time-slots`;
await this.page.route(apiUrl, async (route) => {
if (!fistInshopScheduleCall) {
return route.continue();
}
// const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
// const startDate = new Date();
//const beginningOfWeek = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() - startDate.getDay());
// const endDate = new Date(beginningOfWeek.getFullYear(), beginningOfWeek.getMonth(), beginningOfWeek.getDate() + 13);
// e.g., "2025-07-23"
/*if (route.request().postDataJSON().startDate === fmt(startDate) && route.request().postDataJSON().endDate === fmt(endDate)) {
const response = await route.fetch();
const responseBody = await response.json();
responseBody.days = [];
// Mock the response
await route.fulfill({
response,
body: JSON.stringify(responseBody),
});
}*/
const response = await route.fetch();
const responseBody = await response.json();
responseBody.days = [];
// Mock the response
await route.fulfill({
response,
body: JSON.stringify(responseBody),
});
fistInshopScheduleCall = false;
});
}
async getRepairPartsTotal(testData: Partial<ITestData>) {
const apiUrl = `https://digitalapi.${process.env['PLAYWRIGHT_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/price/api/v1/price/order-items`;
await this.page.on('response', async (response) => {
@ -169,4 +212,58 @@ export class BasePage {
Soft.expect(actualProgressPercentage).toBe(progressPercentage);
console.log(`Progress Bar Percentage: Actual - ${actualProgressPercentage} vs Expected - ${progressPercentage}`);
}
async waitForPageOrComponentload(): Promise<void> {
await Promise.all([
(async () => {
const loaderCount = await this.loaders.count();
if (loaderCount > 0) {
for (let i = 0; i < loaderCount; i++) {
await this.loaders.nth(i).waitFor({ state: 'hidden' });
}
}
})(),
this.page.waitForLoadState('load'),
this.page.waitForLoadState('networkidle')
]);
}
async validateOEMPart(isOEM: boolean): Promise<void> {
if (isOEM!) {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate the presence of an OEM part
if (vuexState.order?.lineItems?.glassParts?.length > 0) {
const firstGlassPartNumber = vuexState.order.lineItems.glassParts[0].partNumber;
await expect(firstGlassPartNumber.includes("OEM")).toBe(true);
} else {
throw new Error("No glass parts found in the order");
}
}
}
async buildExperimentUrl(testData: Partial<ITestData>): Promise<string> {
let experimentsURLExtension = "";
const { experiments } = testData;
if (experiments !== undefined) {
experimentsURLExtension += "?cns=all&experiments="
experimentsURLExtension += experiments?.isMobileFirst
? "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_TEST=true"
: "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_CONTROL=true";
experimentsURLExtension += experiments?.isAdyenPayments
? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)"
: ",Adyen%20Payments=Adyen%20Payment%20Test=CyberSource%20(Control)";
} else {
console.log("Url extension without query string");
}
// Convert to HTML encoding before returning
return experimentsURLExtension;
}
}

View file

@ -0,0 +1,42 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { InsuranceBasePage } from './InsuranceBasePage';
import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step';
export class HeritageProblemGlassQuestionsPage extends InsuranceBasePage {
readonly provideVinManuallyButton: Locator;
readonly provideLicensePlateButton: Locator;
readonly provideHomeAddressButton: Locator;
readonly continueOnVinPopUpButton: Locator;
readonly dontShareVinButton: Locator;
readonly yestoGlassQuestionButton: Locator;
readonly notoGlassQuestionButton: Locator;
readonly continueButton: Locator;
constructor(page: Page) {
super(page);
this.provideVinManuallyButton = page.locator('label[for="VINManually"]');
this.provideLicensePlateButton = page.locator('label[for="licensePlate"]');
this.provideHomeAddressButton = page.locator('label[for="HomeAddress"]');
this.continueOnVinPopUpButton = page.locator('button:has-text("Continue")');
this.dontShareVinButton = page.getByRole('button', { name: "I would rather not share my VIN" });
this.yestoGlassQuestionButton = page.locator('label:has-text("Yes")');
this.notoGlassQuestionButton = page.locator('label:has-text("No")');
this.continueButton = page.locator('button[type="submit"].btn-success.nav-continue');
}
@step("HeritageProblemGlassQuestionsPage >> Handle VIN popup and answer glass question on heritage")
// Handles OEM scenario with vehicle that has only one glass question
async handleHeritageProblemGlassQuestionsPage(testData: Partial<ITestData>){
await this.dontShareVinButton.click();
await this.yestoGlassQuestionButton.click();
await this.continueButton.click();
}
}

View file

@ -1,5 +1,6 @@
import test, { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ITestData } from 'framework/TestData';
export class HomePage extends BasePage {
@ -30,7 +31,7 @@ export class HomePage extends BasePage {
super(page);
this.page = page;
this.letsGetStartedButton = this.page.locator('.hero-content #zipCodeTextboxButton');
this.letsGetStartedButton = this.page.locator('#zipCodeTextboxButton');
this.cusmodalPopup = this.page.locator('#Cusmodalpopup');
this.closePopupButton = this.page.getByRole('button', { name: '×' });
@ -47,12 +48,13 @@ export class HomePage extends BasePage {
//Zip Entry
this.enterServiceZipTextBox = this.page.locator('#zipCodeTextbox');
this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextboxButton');
this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextbox');
this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' });
}
async goto() {
await this.page.goto(process.env['BASE_URL']!);
async goto(testData: Partial<ITestData>) {
const experimentUrlExtension = await this.buildExperimentUrl(testData);
await this.page.goto(process.env['BASE_URL']! + experimentUrlExtension);
}
async isCurrentVariant(): Promise<boolean> {
@ -61,10 +63,11 @@ export class HomePage extends BasePage {
async letsGetStarted(zip: string, enterFunnelWithZip: boolean) {
if (enterFunnelWithZip) {
await this.letsGetStartedButton.evaluate((element, zip) => {
await this.zipEntryLetsGetStartedButton.fill(zip);
/* await this.letsGetStartedButton.evaluate((element, zip) => {
const currentHref = element.getAttribute('href') || '';
element.setAttribute('href', `${currentHref}&zipCode=${zip}`);
}, zip);
element.setAttribute('href', `${currentHref}?zipCode=${zip}`);
}, zip);*/
}
// Wait until modal pop up opens and close pop up
@ -74,6 +77,4 @@ export class HomePage extends BasePage {
}
await this.letsGetStartedButton.click();
}
}

View file

@ -81,9 +81,9 @@ export class LookupPage extends BasePage {
// Map of expected messages by lookup type
const expectedMessages = {
'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.",
'license plate': "Your license plate didnt return a VIN match.Please re-enter the information above or provide your VIN in a different way.",
'VIN': "Your VIN didnt return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.",
'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.",
'license plate': "Your license plate didnt return a VIN match.Please re-enter the information above or provide your VIN in a different way.",
'VIN': "Your VIN didnt return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.",
'ZIP code': "Your ZIP code didn't return a match.Please re-enter the information above or provide your VIN in a different way."
};
@ -106,19 +106,19 @@ export class LookupPage extends BasePage {
}
}
async handleZipValidation(testData: Partial<ITestData>, zip: string, lookupType: VehicleLookupType, alertFlags?: IAlertFlags): Promise<boolean> {
async handleZipValidation(testData: Partial<ITestData>): Promise<boolean> {
let { vehicleDamage } = testData;
let { customerDetails,vehicleDamage, vehicleDetails, alertFlags } = testData;
// Only proceed with validation if alertFlags is provided
if (alertFlags) {
if (alertFlags.isUnserviceableZip) {
await this.checkForUnserviceableZipAlertMessage(zip);
await this.checkForUnserviceableZipAlertMessage(customerDetails!.address.postalCode!);
throw new TestSuccessAlert('Unserviceable ZIP validation successful.');
} else if (alertFlags.isInvalidZip) {
await this.checkForInvalidZipAlertMessage();
throw new TestSuccessAlert('Invalid ZIP validation successful.');
} else if (alertFlags.isVinNotFound) {
await this.checkForVinNotFoundAlertMessage(lookupType);
await this.checkForVinNotFoundAlertMessage(vehicleDetails!.vehicleLookupType!);
throw new TestSuccessAlert('Vin not found validation successful.');
} else {

View file

@ -47,7 +47,7 @@ export class OrderConfirmationPage extends BasePage {
async validateOrderConfirmationPage(testData: Partial<ITestData>) {
// Destructure data we use
const { vehicleDetails, customerDetails, servicePackage, isCashInsuranceFlow,
isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified } = testData;
isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified} = testData;
await this.serviceText.waitFor({ state: "visible" });
Soft.expect((await this.getActualAppointmentSummary()).map(item => item.toLowerCase())).toEqual((await this.getExpectedAppointmentSummary(testData)).map(item => item.toLowerCase()));
@ -108,7 +108,7 @@ export class OrderConfirmationPage extends BasePage {
}
}
async getFormattedAppointmentDate(appointmentDate: string) {
/*async getFormattedAppointmentDate(appointmentDate: string) {
// Parse the original date
let parsedAppointmentDate = new Date(`${appointmentDate}` + 'T00:00:00');
@ -116,6 +116,24 @@ export class OrderConfirmationPage extends BasePage {
// Format the new date as a string
let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
return updatedAppointmentDate;
}*/
async getFormattedAppointmentDate(appointmentDate: string) {
let formattedAppointmentDate: string;
switch (true) {
case /^\d{4}-\d{2}-\d{2}$/.test(appointmentDate):
let parsedAppointmentDate = new Date(`${appointmentDate}` + 'T00:00:00');
formattedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
break;
case /[A-Za-z]+,\s+[A-Za-z]+\s+\d{1,2}/.test(appointmentDate):
formattedAppointmentDate = appointmentDate;
break;
default:
throw new Error('Unsupported date format: ' + appointmentDate);
}
return formattedAppointmentDate;
}
async toggleOrderDetailsSection() {

View file

@ -44,6 +44,7 @@ export class PartQuestionsPage extends BasePage {
} else {
await expect(partQuestionOptions).not.toBeAttached();
}
await this.selectPartQuestionResponses(partQuestions!);
}
}
@ -65,7 +66,6 @@ export class PartQuestionsPage extends BasePage {
await this.validateProgressBar(ProgressBarPercentages.PartQuestionsPage);
await this.validatePartQuestions(partQuestions!);
await this.selectPartQuestionResponses(partQuestions!);
await this.nextPage();
}
}

View file

@ -487,7 +487,7 @@ export class PaymentMethodPage extends BasePage {
}
}
async getFormattedAppointmentDate(appointmentDate: string) {
/*async getFormattedAppointmentDate(appointmentDate: string) {
// Parse the original date
let parsedAppointmentDate = new Date(`${appointmentDate}` + 'T00:00:00');
@ -495,16 +495,48 @@ export class PaymentMethodPage extends BasePage {
// Format the new date as a string
let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
return updatedAppointmentDate;
}*/
async getFormattedAppointmentDate(appointmentDate: string) {
let formattedAppointmentDate: Date;
const now = new Date();
switch (true) {
case /^\d{4}-\d{2}-\d{2}$/.test(appointmentDate):
formattedAppointmentDate = new Date(appointmentDate + 'T00:00:00');
break;
case /[A-Za-z]+,\s+[A-Za-z]+\s+\d{1,2}/.test(appointmentDate):
const [, month, day] = appointmentDate.match(/[A-Za-z]+,\s+([A-Za-z]+)\s+(\d{1,2})/) || [];
formattedAppointmentDate = new Date(`${month} ${day}, ${now.getFullYear()}`);
if (formattedAppointmentDate < now) formattedAppointmentDate.setFullYear(now.getFullYear() + 1); // rollover
break;
default:
throw new Error('Unsupported date format: ' + appointmentDate);
}
return formattedAppointmentDate.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
@step("PaymentMethodPage >> Select Payment Method: ")
async handlePaymentMethodPage(testData: Partial<ITestData>) {
const { servicePackage, isRecalVehicle, paymentDetails } = testData;
const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM } = testData;
await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage);
await this.validatePaymentDetailsPage(testData);
await this.ValidateAfterPayBreakOutSection();
if (isForcedOEM) {
await this.validateOEMPart(isForcedOEM);
}
// Verify VAPS wipers on backend for standard and premium packages
if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) {
await this.verifyVAPS();

View file

@ -26,7 +26,7 @@ export class PaypalPage extends BasePage {
this.paypalLoginButton = page.getByRole('button', { name: 'Log In', exact: true });
this.completePurchaseButton = page.getByTestId('submit-button-initial')
this.payWithRadioButton = page.getByRole('button').filter({ hasText: 'Pay with' });
this.payButton = page.locator('#one-time-cta');
this.payButton = page.locator('footer #one-time-cta');
this.tryAnotherWayButton = page.getByRole('button', { name: 'Try another way' });
}
@ -41,6 +41,8 @@ export class PaypalPage extends BasePage {
await this.paypalLoginButton.click();
await this.completePurchaseButton.click();
} else {
await this.usernameTextBox.waitFor({ state: 'visible' });
await this.page.screenshot({ path: `test-results\\ortoni-data\\paypal-username-${Date.now()}.png`, fullPage: true });
await this.usernameTextBox.fill(paymentDetails.username!);
await this.nextButton.click();
await this.tryAnotherWayButton.click();
@ -48,8 +50,8 @@ export class PaypalPage extends BasePage {
await this.passwordTextBox.fill(paymentDetails.password!);
await this.paypalLoginButton.click();
await this.payWithRadioButton.click();
await this.page.waitForTimeout(2000); // wait for 2 seconds to ensure the Pay button is clickable
await this.page.waitForTimeout(2000);
await this.payButton.dblclick();
}
}
}
}

View file

@ -35,6 +35,11 @@ export class SchedulePage extends BasePage {
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);
@ -65,6 +70,11 @@ export class SchedulePage extends BasePage {
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<ITestData>) {
@ -96,6 +106,7 @@ export class SchedulePage extends BasePage {
await this.safeliteShopsList.first().click();
// Click the save location button
await this.saveLocationButton.click();
await this.waitForPageOrComponentload();
}
// await this.inShopButton.click();
@ -225,12 +236,42 @@ export class SchedulePage extends BasePage {
return formattedTimeSlot;
}
async handleMobileFirstPopUp(testData: Partial<ITestData>) {
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<ITestData>) {
const { appointmentDetails } = testData;
const { appointmentDetails, isCanNotRecal, experiments, handleMobileFirstModal } = testData;
await this.waitForPageOrComponentload();
await this.validateProgressBar(ProgressBarPercentages.SchedulePage);
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();
}
}
}

View file

@ -44,7 +44,7 @@ export class ServicePackagesPage extends BasePage {
this.promoCodeTextbox = this.page.getByLabel('Enter a promo code');
this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' });
this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']');
this.afterPayBanner = this.page.locator('#afterpay-banner');
this.afterPayBanner = this.page.locator('div.afterpay-modal-banner');
this.glassOnlypackagePrice = this.page.locator('fieldset:has(legend#ServicePackageQuestion) label').filter({ hasText: 'Glass service only' }).locator('.pricing-info');
}
@ -298,6 +298,10 @@ export class ServicePackagesPage extends BasePage {
await this.mockScheduleResponseForEarlyBird(customerDetails!);
}
if (testData.mockFirstInshopCallNoSchedule) {
await this.mockScheduleResponseForFirstInshopCallNoSchedule(testData.customerDetails!);
}
await this.nextPage();
}
}

View file

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

View file

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

View file

@ -34,11 +34,11 @@ export class VehicleLookupLicensePage extends LookupPage {
@step("VehicleLookupLicensePage >> Lookup by license plate: ")
async handleVehicleLookupLicensePage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
const { customerDetails, vehicleDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupLicensePage);
await this.enterPlateDetails(vehicleDetails!);
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
await this.handleZipValidation(testData);
}
}

View file

@ -24,11 +24,11 @@ export class VinLookupPage extends LookupPage {
@step("VinLookupPage >> Lookup by VIN: ")
async handleVehicleLookupVinPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
const { customerDetails, vehicleDetails } = testData;
let vin: string;
if (Array.isArray(vehicleDetails?.vin)) {
vin = vehicleDetails.vin[0];
vin = vehicleDetails!.vin[0];
} else {
vin = vehicleDetails?.vin || '';
}
@ -36,6 +36,6 @@ export class VinLookupPage extends LookupPage {
await this.validateProgressBar(ProgressBarPercentages.VinLookupPage);
await this.enterVin(vin);
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
await this.handleZipValidation(testData);
}
}

View file

@ -57,7 +57,7 @@ export class AddressForm extends BasePage {
);
});
await this.page.hover(".pac-container .pac-item");
await this.addressSuggestionList.hover();
await new Promise(resolve => setTimeout(resolve, 1000));
await this.addressSuggestionList.click(); // Select the first suggestion
//await this.streetAddressTextBox.press('Tab'); // Move focus to ZIP code field

View file

@ -78,7 +78,7 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 1 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 4 : 5,
workers: process.env.CI ? 4 : 3,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: process.env.CI? [
['junit'],

View file

@ -1,4 +1,4 @@
import { Page } from "@playwright/test";
import { Browser, Page } from "@playwright/test";
import { addSmokeTagToRandomTest, Flow, ServiceLocation } from 'safelite-playwright-core';
import { ValidationOptions } from 'safelite-playwright-core';
import heavyTruckTests from "./alert-validation/alert0001_HeavyTruck";
@ -18,6 +18,7 @@ import cashReplaceGlassLicensePlateLookupInshopPaypalTests from "./CashReplaceGl
import { ApiResponseInterceptUtil } from 'safelite-playwright-core';
import cashReplaceGlassPromoInshopTests from "./CashReplaceGlassPromoInshop";
import cashReplaceMultiGlassPromoInshopTests from "./CashReplaceMultiGlassPromoInshop";
import cashReplaceStaticInshopTests from "./CashReplaceStaticInshop";
import cashReplaceRainDefensePromoInshopTests from "./CashReplaceRainDefensePromoInshop";
import cashReplaceSafeliteCanNotRecalMobileTests from "./CashReplaceSafeliteCanNotRecalMobile";
import cashReplaceVinMobileTests from "./CashReplaceVinMobile";
@ -28,8 +29,10 @@ import insuranceITAC21stCenturyTests from "./InsuranceITAC21stCentury";
import insuranceGeicoTests from "./InsuranceGeico";
import insuranceITACOptimizedPriceValidationAllStateTests from "./InsuranceITACOptimizedPriceValidationAllState";
import insuranceNoCompProgressiveTests from "./InsuranceNoCompProgressive";
import insuranceOEMAllstateTests from "./InsuranceOEMAllState";
import cashRepairInShopAfterPayTests from "./CashRepairInShopAfterPay";
import cashRepairInShopPayPalTests from "./CashRepairInShopPayPal";
import cashReplaceMobileFirstModalTests from "./CashReplaceMobileFirstModal";
import cashReplaceMultiSlidingGlassDropoffTests from "./CashReplaceMultiSlidingGlassDropoff";
import cashReplaceMultiGlassMobileTests from "./CashReplaceMultiGlassMobile";
import { getTestObject, TestCase, prepareTest, RuleEngine, TestInfo } from 'framework/Typedefs';
@ -60,32 +63,35 @@ addSmokeTagToRandomTest(splitWindshieldTests);
// Standard test scenarios
const allStandardTests = [
{name: "CashRepairMobileCreditCard", tests: cashRepairMobileCCTests},
{name: "CashRepairInShopAfterPay", tests: cashRepairInShopAfterPayTests},
{name: "CashRepairInShopPayPal", tests: cashRepairInShopPayPalTests},
{name: "CashReplaceDynamicRecalMobile", tests: cashReplaceDynamicRecalMobileTests},
{name: "CashReplaceGlassAddressLookupInshopAfterPay", tests: cashReplaceGlassAddressLookupInshopAfterPayTests},
{name: "CashReplaceGlassLicensePlateLookupInshopPaypal", tests: cashReplaceGlassLicensePlateLookupInshopPaypalTests},
{name: "CashReplaceGlassPromoInShop", tests: cashReplaceGlassPromoInshopTests},
{name: "CashReplaceMultiGlassPromoInShop", tests: cashReplaceMultiGlassPromoInshopTests},
{name: "CashReplaceMultiSlidingGlassDropoff",tests: cashReplaceMultiSlidingGlassDropoffTests},
{name: "CashReplaceMultiGlassMobile",tests: cashReplaceMultiGlassMobileTests},
{name: "CashReplaceRainDefensePromoInshop", tests: cashReplaceRainDefensePromoInshopTests},
{name: "CashReplaceSafeliteCanNotRecalMobile", tests: cashReplaceSafeliteCanNotRecalMobileTests},
{name: "CashReplaceVinMobile", tests: cashReplaceVinMobileTests},
{name: "CashReplaceWiperDropoff", tests: cashReplaceWiperDropoffTests},
{name: "CashReplaceWiperPromoInshop", tests: cashReplaceWiperPromoInShopTests},
{name: "CashReplaceSwitchToInsuranceProgressiveNoComp", tests: cashReplaceSwitchToInsuranceProgressiveNoCompTests},
{ name: "CashRepairMobileCreditCard", tests: cashRepairMobileCCTests },
{ name: "CashRepairInShopAfterPay", tests: cashRepairInShopAfterPayTests },
{ name: "CashRepairInShopPayPal", tests: cashRepairInShopPayPalTests },
{ name: "CashReplaceDynamicRecalMobile", tests: cashReplaceDynamicRecalMobileTests },
{ name: "CashReplaceGlassAddressLookupInshopAfterPay", tests: cashReplaceGlassAddressLookupInshopAfterPayTests },
{ name: "CashReplaceGlassLicensePlateLookupInshopPaypal", tests: cashReplaceGlassLicensePlateLookupInshopPaypalTests },
{ name: "CashReplaceGlassPromoInShop", tests: cashReplaceGlassPromoInshopTests },
{ name: "CashReplaceMultiGlassPromoInShop", tests: cashReplaceMultiGlassPromoInshopTests },
{ name: "CashReplaceMultiSlidingGlassDropoff", tests: cashReplaceMultiSlidingGlassDropoffTests },
{ name: "CashReplaceMultiGlassMobile", tests: cashReplaceMultiGlassMobileTests },
{ name: "CashReplaceRainDefensePromoInshop", tests: cashReplaceRainDefensePromoInshopTests },
{ name: "CashReplaceSafeliteCanNotRecalMobile", tests: cashReplaceSafeliteCanNotRecalMobileTests },
{ name: "CashReplaceVinMobile", tests: cashReplaceVinMobileTests },
{ name: "CashReplaceWiperDropoff", tests: cashReplaceWiperDropoffTests },
{ name: "CashReplaceStaticInshop", tests: cashReplaceStaticInshopTests },
{ name: "CashReplaceWiperPromoInshop", tests: cashReplaceWiperPromoInShopTests },
{ name: "CashReplaceSwitchToInsuranceProgressiveNoComp", tests: cashReplaceSwitchToInsuranceProgressiveNoCompTests },
{ name: "CashReplaceMobileFirstModal", tests: cashReplaceMobileFirstModalTests },
// TODO: Uncomment when QA is ready to run heavy truck tests
// {name: "CashReplaceSplitWindshield", tests: CashReplaceSplitWindshieldTests},
{name: "InsuranceAcuityPaypal", tests: insuranceAcuityPaypalTests},
{name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests},
{name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests},
{ name: "InsuranceAcuityPaypal", tests: insuranceAcuityPaypalTests },
{ name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests },
{ name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests },
// {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests},
{name: "InsuranceUnverified", tests: insuranceUnverifiedTests},
{ name: "InsuranceOEMAllstate", tests: insuranceOEMAllstateTests },
{ name: "InsuranceUnverified", tests: insuranceUnverifiedTests },
// {name: "InsuranceGeico", tests: insuranceGeicoTests},
// {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests}
{name: "InsuranceMeemicNearSchoolVerified", tests: insuranceMeemicNearSchoolVerifiedTests},
{ name: "InsuranceMeemicNearSchoolVerified", tests: insuranceMeemicNearSchoolVerifiedTests },
];
@ -138,13 +144,13 @@ async function run(page: Page, testInfo: TestInfo): Promise<void> {
try {
await testInfo.testCase.setup();
testInfo.testCase.setupPages(page, createTestPages);
await testInfo.testCase.pages.homePage.goto();
await testInfo.testCase.pages.homePage.goto(testInfo.testCase.testData);
await runWorkflow(page, testInfo.testCase);
} catch (error) {
// Catch successful alert tests and log message
if (error instanceof TestSuccessAlert) {
console.log(error.message);
// Catch and throw other errors
// Catch and throw other errors
} else {
throw error;
}
@ -173,11 +179,11 @@ async function runWorkflow(page: Page, testCase: TestCase) {
page.on('response', apiResponseInterceptUtil.handleInterceptResponse);
// Destructure test data for easier access
const {
paymentMethod, customerDetails, vehicleDetails, vehicleDamage,
paymentDetails, partQuestions, isEnterFunnelWithZip, capabilityQuestions,
const {
paymentMethod, customerDetails, vehicleDetails, vehicleDamage,
paymentDetails, partQuestions, isEnterFunnelWithZip, capabilityQuestions,
vehiclePartQuestions, moldingQuestions, isSkipEstimatePage
} = testCase.testData;
} = testCase.testData;
// Check if the vehicle damage includes a windshield crack
const hasWindshieldCrack = vehicleDamage!.some(damage =>
@ -191,7 +197,7 @@ async function runWorkflow(page: Page, testCase: TestCase) {
//============================= TEST WORKFLOW STEPS =============================
// Use Environment Variable to decide whether or not we want to skip content site aka home page
if (process.env.SKIP_CONTENT_SITE == "false") {
if (process.env.SKIP_CONTENT_SITE == "false") {
await test.step('HomePage >> Lets Get Started', async () => {
let homePage = testCase.pages.homePage;
console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`);
@ -209,6 +215,16 @@ async function runWorkflow(page: Page, testCase: TestCase) {
console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`);
}
if (page.url().includes('ServiceDetails.aspx')) {
// Take a screenshot for diagnostics
await page.screenshot({ path: `test-results\\ortoni-data\\service-details-detected-${Date.now()}.png`, fullPage: true });
console.log("ServiceDetails.aspx detected");
await page.goBack();
await page.reload();
let homePage = testCase.pages.homePage;
await homePage.letsGetStarted(customerDetails?.address.postalCode!, !!isEnterFunnelWithZip!);
}
// Handle vehicle selection page
let vehicleSelectionPage = testCase.pages.vehicleSelectionPage;
await vehicleSelectionPage.handleVehicleSelectionPage(testCase.testData);
@ -221,24 +237,24 @@ async function runWorkflow(page: Page, testCase: TestCase) {
if (hasWindshieldCrack && !isSkipEstimatePage) {
let estimatePage = testCase.pages.estimatePage;
await estimatePage.handleEstimatePage(testCase.testData);
// Handle different vehicle lookup methods
switch (vehicleDetails!.vehicleLookupType!) {
case VehicleLookupType.Address:
let vehicleLookupAddressPage = testCase.pages.vehicleLookupAddressPage;
await vehicleLookupAddressPage.handleVehicleLookupAddressPage(testCase.testData);
break;
case VehicleLookupType.LicensePlateNumber:
let vehicleLookupLicensePage = testCase.pages.vehicleLookupLicensePage;
await vehicleLookupLicensePage.handleVehicleLookupLicensePage(testCase.testData);
break;
case VehicleLookupType.Vin:
let vinLookupPage = testCase.pages.vinLookupPage;
await vinLookupPage.handleVehicleLookupVinPage(testCase.testData);
break;
case VehicleLookupType.Zip:
let serviceZipPage = testCase.pages.serviceZipPage;
await serviceZipPage.handleServiceZipPage(testCase.testData);
@ -255,7 +271,7 @@ async function runWorkflow(page: Page, testCase: TestCase) {
let partQuestionsPage = testCase.pages.partQuestionsPage;
await partQuestionsPage.handlePartQuestionsPage(testCase.testData);
}
// Handle molding questions if applicable
if (moldingQuestions && moldingQuestions.length > 0) {
let moldingQuestionsPage = testCase.pages.moldingQuestionsPage;
@ -279,14 +295,14 @@ async function runWorkflow(page: Page, testCase: TestCase) {
await servicePackagesPage.handleServicePackagePage(testCase.testData);
//============================= INSURANCE FLOW =============================`
// Insurance flow - if user selected Insurance as Payment Method
if (paymentMethod == PaymentMethod.Insurance) {
await handleInsuranceFlow(testCase);
}
//============================= SERVICE SCHEDULING =============================
// Select service location
// let serviceLocationPage = testCase.pages.serviceLocationPage;
// await serviceLocationPage.handleServiceLocationPage(testCase.testData);
@ -305,7 +321,7 @@ async function runWorkflow(page: Page, testCase: TestCase) {
await contactDetailsPage.handleContactDetailsPage(testCase.testData);
//============================= PAYMENT PROCESSING =============================
// Handle payment
let paymentMethodPage = testCase.pages.paymentMethodPage;
await paymentMethodPage.handlePaymentMethodPage(testCase.testData);
@ -316,84 +332,90 @@ async function runWorkflow(page: Page, testCase: TestCase) {
}
//============================= ORDER CONFIRMATION =============================
// Validate order confirmation
let orderConfirmationPage = testCase.pages.orderConfirmationPage;
await orderConfirmationPage.verifyOrderConfirmationPage(testCase.testData);
}
export async function handleInsuranceFlow(testCase: TestCase) {
const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, flow } = testCase.testData;
// Check if the insurance policy has endorsements
const hasEndorsements = endorsements && endorsements.length > 0;
async function handleInsuranceFlow(testCase: TestCase) {
const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, isForcedOEM, flow } = testCase.testData;
// Handle insurance company page
let insuranceCompanyPage = testCase.pages.insuranceCompanyPage;
await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData);
// Check if the insurance policy has endorsements
const hasEndorsements = endorsements && endorsements.length > 0;
// Handle ccPolicyInfoPage
let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage;
await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData);
// Handle insurance company page
let insuranceCompanyPage = testCase.pages.insuranceCompanyPage;
await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData);
let duplicateCheckPage = testCase.pages.duplicateCheckPage;
if (duplicateCheckPage.page.url().includes('DuplicateCheck.aspx')) {
await duplicateCheckPage.handleDuplicateCheckPage(testCase.testData);
// Handle OEM scenario
if (isForcedOEM) {
// User should be on heritage problem glass questions page after selecting Allstate on insurance company page
let HeritageProblemGlassQuestionsPage = testCase.pages.heritageProblemGlassQuestionsPage;
await HeritageProblemGlassQuestionsPage.handleHeritageProblemGlassQuestionsPage(testCase.testData);
}
// Handle ccPolicyInfoPage
let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage;
await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData);
let duplicateCheckPage = testCase.pages.duplicateCheckPage;
if (duplicateCheckPage.page.url().includes('DuplicateCheck.aspx')) {
await duplicateCheckPage.handleDuplicateCheckPage(testCase.testData);
}
if (flow === Flow.Managed) {
let policyVehiclesPage = testCase.pages.policyVehiclesPage;
await policyVehiclesPage.handlePolicyVehiclesPage(testCase.testData);
// Handle policy driver selection if applicable
if (isPolicyDriver) {
let policyDriverPage = testCase.pages.policyDriverPage;
await policyDriverPage.handlePolicyDriverPage(testCase.testData);
}
if ( flow === Flow.Managed ) {
let policyVehiclesPage = testCase.pages.policyVehiclesPage;
await policyVehiclesPage.handlePolicyVehiclesPage(testCase.testData);
// Handle policy driver selection if applicable
if (isPolicyDriver) {
let policyDriverPage = testCase.pages.policyDriverPage;
await policyDriverPage.handlePolicyDriverPage(testCase.testData);
}
// Handle endorsements if applicable
if (hasEndorsements) {
let endorsementsPage = testCase.pages.endorsementsPage;
await endorsementsPage.handleEndorsementsPage(testCase.testData);
}
} else {
//Handle verify details page
let verifyDetailsPage = testCase.pages.verifyDetailsPage;
await verifyDetailsPage.handleVerifyDetailsPage(testCase.testData);
// Handle endorsements if applicable
if (hasEndorsements) {
let endorsementsPage = testCase.pages.endorsementsPage;
await endorsementsPage.handleEndorsementsPage(testCase.testData);
}
} else {
// Handle Policy info submitted page
let policyInfoSubmittedPage = testCase.pages.policyInfoSubmittedPage;
await policyInfoSubmittedPage.handlePolicyInfoSubmittedPage();
//Handle verify details page
let verifyDetailsPage = testCase.pages.verifyDetailsPage;
await verifyDetailsPage.handleVerifyDetailsPage(testCase.testData);
}
if(isRecalVehicle)
{
let recalibrationInfoPage = testCase.pages.recalibrationInfoPage;
await recalibrationInfoPage.handleRecalibrationInfoPage();
// Handle Policy info submitted page
let policyInfoSubmittedPage = testCase.pages.policyInfoSubmittedPage;
await policyInfoSubmittedPage.handlePolicyInfoSubmittedPage();
if (isRecalVehicle) {
let recalibrationInfoPage = testCase.pages.recalibrationInfoPage;
await recalibrationInfoPage.handleRecalibrationInfoPage();
}
//handle coveraage statement page
let coverageStatementPage = testCase.pages.coverageStatementPage;
await coverageStatementPage.handleCoverageStatementPage(testCase.testData);
// 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
if (isCashInsuranceFlow) {
// Schedule appointment
let schedulePage = testCase.pages.schedulePage;
await schedulePage.handleSchedulePage(testCase.testData);
if (testCase.testData.appointmentDetails?.serviceLocation === ServiceLocation.Mobile) {
let mobileDetailsPage = testCase.pages.mobileDetailsPage;
await mobileDetailsPage.handleMobileDetailsPage(testCase.testData);
}
//customer dertails
let contactDetailsPage = testCase.pages.contactDetailsPage;
await contactDetailsPage.nextPage();
//handle coveraage statement page
let coverageStatementPage = testCase.pages.coverageStatementPage;
await coverageStatementPage.handleCoverageStatementPage(testCase.testData);
// 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
if (isCashInsuranceFlow) {
// Schedule appointment
let schedulePage = testCase.pages.schedulePage;
await schedulePage.handleSchedulePage(testCase.testData);
if (testCase.testData.appointmentDetails?.serviceLocation === ServiceLocation.Mobile) {
let mobileDetailsPage = testCase.pages.mobileDetailsPage;
await mobileDetailsPage.handleMobileDetailsPage(testCase.testData);
}
//customer dertails
let contactDetailsPage = testCase.pages.contactDetailsPage;
await contactDetailsPage.nextPage();
//paymentmethods
let paymentMethodPage = testCase.pages.paymentMethodPage;
await paymentMethodPage.nextPage();
}
//paymentmethods
let paymentMethodPage = testCase.pages.paymentMethodPage;
await paymentMethodPage.nextPage();
}
}

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { VehicleDamage, ServiceLocation, ServicePackage } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { ClientData } from 'safelite-playwright-core';
@ -34,7 +34,12 @@ const cashRepairInShopAfterPayData : Partial<ITestData> = {
},
// Use predefined payment data
paymentDetails: ClientData.getDefaultAfterpayDetails()
paymentDetails: ClientData.getDefaultAfterpayDetails(),
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashRepairInShopAfterPayTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { VehicleDamage, ServicePackage } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { ClientData } from 'safelite-playwright-core';
@ -35,7 +35,12 @@ const cashRepairInShopPayPalData : Partial<ITestData> = {
},
// Use predefined payment data
paymentDetails: ClientData.getDefaultPaypalDetails()
paymentDetails: ClientData.getDefaultPaypalDetails(),
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashRepairInShopPayPalTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { VehicleDamage, ServiceLocation } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { ClientData } from 'safelite-playwright-core';
@ -48,7 +48,12 @@ const cashRepairMobileCCData : Partial<ITestData> = {
},
// Use predefined payment data
paymentDetails: ClientData.getDefaultCreditCardDetails()
paymentDetails: ClientData.getDefaultCreditCardDetails(),
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashRepairMobileCCTests: ITestCase[] = [];

View file

@ -1,6 +1,6 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, PaymentType } from 'safelite-playwright-core';
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, ServicePackage, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
@ -29,7 +29,8 @@ const cashReplaceDynamicRecalMobileData: Partial<ITestData> = {
postalCode: '21237'
}
},
servicePackage: ServicePackage.Premium,
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
@ -59,7 +60,14 @@ const cashReplaceDynamicRecalMobileData: Partial<ITestData> = {
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService
paymentType: PaymentType.PayAtService,
// promoCode: 'digitalrain'
promoCode: 'rain50'
},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { ClientData } from 'safelite-playwright-core';
@ -43,7 +43,12 @@ const cashReplaceGlassAddressLookupInshopAfterPayData: Partial<ITestData> = {
},
// Override payment details
paymentDetails: ClientData.getDefaultAfterpayDetails()
paymentDetails: ClientData.getDefaultAfterpayDetails(),
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceGlassAddressLookupInshopAfterPayTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { ClientData } from 'safelite-playwright-core';
@ -42,7 +42,12 @@ const cashReplaceGlassLicensePlateLookupInshopPaypalData: Partial<ITestData> = {
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override payment details to use PayPal
paymentDetails: ClientData.getDefaultPaypalDetails()
paymentDetails: ClientData.getDefaultPaypalDetails(),
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceGlassLicensePlateLookupInshopPaypalTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -55,6 +55,11 @@ const cashReplaceGlassPromoInshopData: Partial<ITestData> = {
paymentType: PaymentType.PayAtService,
// Add promo code - key feature of this test
promoCode: '20CALL',
},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}

View file

@ -0,0 +1,85 @@
//Imports here
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, ServicePackage, PaymentType, PartQuestionType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceMobileFirstModal");
// Now get the test data with the seeded faker
const cashReplaceMobileFirstModalData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '91761'
}
},
servicePackage: ServicePackage.GlassOnly,
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2018',
make: 'Honda',
model: 'Accord',
style: '4 door sedan',
},
handleMobileFirstModal: true,
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
],
mockFirstInshopCallNoSchedule: true,
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override appointment details
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
serviceAddress: {
// Use street address from current faker seed
street: "2064 S Sultana Ave",
city: 'Ontario',
state: 'CA',
postalCode: '91761',
country: 'United States'
},
},
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService
},
// Experiments
experiments: {
...getDefaultExperimentsData(),
isMobileFirst: true
}
}
const cashReplaceMobileFirstModal: ITestCase[] = [];
const tc = {
name: `CashReplaceMobileFirstModal`,
tags: ['@E2E','@CashReplaceMobileFirstModal', '@test_report', '@CASH'],
testData: cashReplaceMobileFirstModalData
};
cashReplaceMobileFirstModal.push(tc);
export default cashReplaceMobileFirstModal;

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { VehicleDamage, PartQuestionType, PaymentType, ServicePackage, ServiceLocation } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -88,7 +88,12 @@ const cashReplaceMultiGlassMobileData: Partial<ITestData> = {
optionToSelect: 'Gray Tint Privacy',
secondaryQuestionOptionToSelect: 'solar, passenger side, encap, chrome molding'
}
]
],
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceMultiGlassMobileTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { VehicleDamage, PartQuestionType, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -94,7 +94,12 @@ const cashReplaceMultiGlassPromoInshopData: Partial<ITestData> = {
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'heated glass, solar, antenna, manual liftgate, 1 hole'
}
]
],
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceMultiGlassPromoInshopTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { VehicleDamage, PartQuestionType, PaymentType, ServicePackage, ServiceLocation } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -35,7 +35,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
shopAddress: "6826 sawmill rd, Columbus, OH 43235"
shopAddress: "1343 cameron ave, lewis center, oh 43035"
},
// Override vehicle details
@ -105,7 +105,12 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
optionToSelect: 'Gray Tint Privacy',
secondaryQuestionOptionToSelect: 'heated glass, solar, slider, power, kit'
}
]
],
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceMultiSlidingGlassDropoffTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServicePackage, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -50,6 +50,11 @@ const cashReplaceRainDefensePromoInshopData: Partial<ITestData> = {
// Rain Defense promo code
promoCode: 'rd50'
},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceRainDefensePromoInshopTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, PartQuestionType, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -70,7 +70,12 @@ const cashReplaceSafeliteCanNotRecalMobileData: Partial<ITestData> = {
isOnPage: true,
optionToSelect: 'Yes'
},
]
],
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceSafeliteCanNotRecalMobileTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { PartQuestionType, PaymentType, VehicleDamage } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -59,6 +59,11 @@ const CashReplaceSplitWindshieldData: Partial<ITestData> = {
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService
},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}

View file

@ -0,0 +1,83 @@
//Imports here
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServicePackage, PaymentType, PartQuestionType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceStaticInshop");
// Now get the test data with the seeded faker
const CashReplaceStaticInshopData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Key feature: Premium package with Rain Defense
servicePackage: ServicePackage.Standard,
// Flag for recalibration vehicle
isRecalVehicle: true,
// Override customer details
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '43085'
}
},
// override part questions
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
{
partQuestionType: PartQuestionType.GeneralQuestion2,
isOnPage: true,
optionToSelect: 'Yes'
}
],
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2022',
make: 'Mazda',
model: 'CX-9',
style: '4 door utility',
vin: '3FA6P0HD8LR234510',
vehicleLookupType: VehicleLookupType.Zip
},
mockFirstInshopCallNoSchedule: true,
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Payment at service
paymentDetails: {
paymentType: PaymentType.PayAtService
},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceStaticInshopTests: ITestCase[] = [];
const tc = {
name: `CashReplaceStaticInshop`,
tags: ['@E2E','@CashReplaceStaticInshop', '@test_report', '@CASH'],
testData: CashReplaceStaticInshopData
};
cashReplaceStaticInshopTests.push(tc);
export default cashReplaceStaticInshopTests;

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { Flow, ServiceLocation } from "safelite-playwright-core";
import { DamageType, PartQuestionType, PaymentType } from 'safelite-playwright-core'
import { ITestCase } from '../framework/Typedefs'
@ -70,7 +70,10 @@ const cashReplaceSwitchToInsuranceProgressiveNoCompData: Partial<ITestData> = {
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
};
// Create and register the test case

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { AppointmentTimeslot, ServiceLocation, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -60,6 +60,11 @@ const cashReplaceVinMobileData: Partial<ITestData> = {
paymentDetails: {
paymentType: PaymentType.PayAtService
},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceVinMobileTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServicePackage, ServiceLocation, PartQuestionType, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -66,10 +66,15 @@ const cashReplaceWiperDropoffData: Partial<ITestData> = {
},
{
partQuestionType: PartQuestionType.GeneralQuestion2,
isOnPage: false,
isOnPage: true,
optionToSelect: 'Yes'
},
]
],
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const cashReplaceWiperDropoffTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServicePackage, ServiceLocation, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
@ -46,6 +46,11 @@ const cashReplaceWiperPromoInShopData: Partial<ITestData> = {
paymentType: PaymentType.PayAtService,
// Specific wiper promo code
promoCode: '1WIPER0',
},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, DamageType, PartQuestionType, VehicleDamage, PaymentType, Flow } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
@ -83,7 +83,12 @@ const insuranceAcuityPaypalData: Partial<ITestData> = {
],
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
paymentDetails: {},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const insuranceAcuityPaypalTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, DamageType, PaymentType, PartQuestionType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { PaymentMethod } from "framework/localTypes/Enums";
@ -67,7 +67,12 @@ const insuranceBigTruckVerifiedData: Partial<ITestData> = {
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
paymentDetails: {},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const insuranceBigTruckVerifiedTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, DamageType, Flow } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
@ -64,7 +64,12 @@ const insuranceGeicoData: Partial<ITestData> = {
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
paymentDetails: {},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const insuranceGeicoTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, DamageType, PartQuestionType, Flow } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
@ -68,7 +68,12 @@ const insuranceITAC21stCenturyData: Partial<ITestData> = {
isOnPage: true,
optionToSelect: 'Yes'
},
]
],
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const insuranceITAC21stCenturyTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { VehicleDamage, ServiceLocation, DamageType, Flow } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
@ -65,7 +65,12 @@ const insuranceITACOptimizedPriceValidationAllStateData: Partial<ITestData> = {
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
paymentDetails: {},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const insuranceITACOptimizedPriceValidationAllStateTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, DamageType, Flow, PartQuestionType, LossLocation } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
@ -62,7 +62,12 @@ const insuranceMeemicNearSchoolVerifiedData: Partial<ITestData> = {
],
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
paymentDetails: {},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
};
const insuranceMeemicNearSchoolVerifiedTests: ITestCase[] = [];

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, DamageType, PaymentType, PartQuestionType, Flow } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { PaymentMethod } from "framework/localTypes/Enums";
@ -32,10 +32,11 @@ const insuranceNoCompProgressiveData: Partial<ITestData> = {
lastName: 'SCHATZ',
address: {
...getDefaultTestData().customerDetails!.address,
city: 'Slidell',
state: 'Louisiana',
postalCode: '70458'
}
// city: 'Slidell',
// state: 'Louisiana',
// postalCode: '70458'
postalCode: '43085'
},
},
@ -71,12 +72,17 @@ const insuranceNoCompProgressiveData: Partial<ITestData> = {
// Override for in-shop appointment
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '56705 Garrett Road, Slidell, LA 70458',
// shopAddress: '56705 Garrett Road, Slidell, LA 70458',
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
paymentDetails: {},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const insuranceNoCompProgressiveTests: ITestCase[] = [];

View file

@ -0,0 +1,93 @@
//Imports here
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, DamageType, PaymentType, PartQuestionType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { PaymentMethod } from "framework/localTypes/Enums";
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("InsuranceOEMAllstate");
// Now get the test data with the seeded faker
const insuranceOEMAllstateData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// Key feature: Insurance flow with GEICO
paymentMethod: PaymentMethod.Insurance,
// Insurance claim flags
isPolicyFound: false,
isPolicyUnverified: true,
isRecalVehicle: true,
isForcedOEM: true,
// Override customer details with specific name and California location
customerDetails: {
...getDefaultTestData().customerDetails!,
firstName: 'Jane',
lastName: 'OEMTest',
address: {
...getDefaultTestData().customerDetails!.address,
city: 'Richmond',
state: 'Virginia',
postalCode: '23219'
}
},
// Insurance claim details
claimDetails: {
client: 'Allstate',
policyNumber: 'MockOEMTest',
policyDeductible: "Unverified",
policyZip: '43085',
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Rock
},
// Hyundai vehicle details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2024',
make: 'Jeep',
model: 'Wrangler',
style: '4 door utility',
vehicleLookupType: VehicleLookupType.Zip,
},
// Part questions related to recalibration
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
],
// Override for in-shop appointment
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '5719 Brandt Pike, Dayton, OH 45424',
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const insuranceOEMAllstateTests: ITestCase[] = [];
const tc = {
name: `InsuranceOEMAllstate`,
tags: ['@E2E','@InsuranceOEMAllstate', '@test_report', '@Insurance'],
testData: insuranceOEMAllstateData
};
insuranceOEMAllstateTests.push(tc);
export default insuranceOEMAllstateTests;

View file

@ -1,5 +1,5 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestData, getDefaultExperimentsData } from 'framework/TestData'
import { ServiceLocation, DamageType, PaymentType, PartQuestionType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { PaymentMethod } from "framework/localTypes/Enums";
@ -72,7 +72,12 @@ const insuranceUnverifiedData: Partial<ITestData> = {
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
paymentDetails: {},
// Experiments
experiments: {
...getDefaultExperimentsData()
}
}
const insuranceUnverifiedTests: ITestCase[] = [];

View file

@ -75,10 +75,10 @@ const lookupTypesToTest: LookupTestCase[] = [
},
customerDetails: {
address: {
street: '4076 Spectacle Drive',
street: '4219 Turpin Ln',
city: 'Columbus',
state: 'OH',
postalCode: '59261',
postalCode: '43230',
country: 'United States'
}
},

View file

@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta name="robots" content="noindex" />
<script>
window.dataLayer = [{}];
</script>

View file

@ -545,7 +545,6 @@
{ path: '/parts/parts-or-questions', method: 'POST', body: {"carId":"CR00046193","glassPieces":[{"location":"Windshield","name":"Single"}],"zip":"43015","vin":null,"serviceType":null,"referralSeqNumber":environmentContext.referralNumber,"AppName":"FixMyGlass"} },
{ path: '/parts/wipers/CR00046193/43015', method: 'GET' },
{ path: '/parts/mobile-premium-fee/Cash/Replace', method: 'GET' },
{ path: '/parts/service-package-discount', method: 'POST', body: {"glassPieces":[{"location":"Windshield","name":"Single"}],"damageType":"Replace","AppName":"FixMyGlass"} },
{ path: '/parts/mobile-fee/?serviceType=Install&facilityType=Mobile&parentAccountNumber=167132&billToAccountNumber=87291&providerNumber=03357&isItacOptimized=false&zipCode=43015', method: 'GET' },
]
},

View file

@ -14,6 +14,8 @@ const GaCategories = {
EVOX: "Evox",
APPOINTMENT: "Appointment",
SERVICE_LOCATION: "service-location",
CONFIRMATION_CLICKED: "confirmation clicked",
RECAL_DISCLAIMER: "Recalibration disclaimer",
};
const GaActions = {
@ -26,6 +28,9 @@ const GaActions = {
MOBILE_AVAILABLE: "mobile_available",
SHOPS_FIRST_DISPLAYED: "shops_first_displayed",
MORE_LOCATIONS_CLICKED: "more_locations_clicked",
YES: "yes",
NO: "no",
ACKNOWLEDGED: "Acknowledged",
};
const GaLabels = {

View file

@ -22,6 +22,7 @@ const applicationConfig = {
PIA_ERROR_URL:
location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel",
CONFIRMATION_URL: location.protocol + "//" + location.host + "/fmg/confirmation",
PIA_ADYEN_RETURN_URL: location.protocol + "//" + location.host + "/fmg/virtual/payment/return",
GOOGLE_CALENDAR: "https://www.google.com/calendar/render?action=TEMPLATE",
YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60",
OUTLOOK_CALENDAR:
@ -31,6 +32,8 @@ const applicationConfig = {
COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__,
RETURN_USER_PAGE: "/fmg/return-user",
ADYEN_CLIENT_KEY: process.env.__VUE_APP_ADYEN_CLIENT_KEY__,
ADYEN_ENVIRONMENT: process.env.__VUE_APP_ADYEN_ENVIRONMENT__,
};
export { applicationConfig };

View file

@ -3,7 +3,7 @@ const cartItemCategories = {
GLASS_PARTS: "glassParts",
SUPPORTING_ITEMS: "supportingItems",
PROMOS: "promos",
SERVICE_PACKAGE_DISCOUNT: "servicePackageDiscount",
QUOTE_PAGE_DISCOUNT: "quotePageDiscount",
};
export { cartItemCategories };

View file

@ -10,7 +10,7 @@ const cartItemTypes = {
PROMOS: "PROMOS",
EARLY_BIRD: "EARLY BIRD",
SUPPLIES_REPAIR: "SUPPLIES-REPAIR",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
DONATION: "DONATION",
};

View file

@ -76,10 +76,6 @@ const endpoints = {
url: "/parts/api/v1/parts/pricing-by-day-part-number",
method: "GET",
},
GetServicePackageDiscountPart: {
url: "/parts/api/v1/parts/service-package-discount",
method: "POST",
},
GetServiceabilityDetails: {
url: "/location/api/v1/location/serviceability-details",
method: "GET",
@ -124,6 +120,14 @@ const endpoints = {
url: "/order/api/v1/order/save-session/fmg",
method: "POST",
},
SubmitOrder: {
url: "/order/api/v1/order/submit-order",
method: "POST",
},
SubmitOrderForPia: {
url: "/order/api/v1/order/submit-order-for-pia",
method: "POST",
},
LoadSession: {
url: "/order/api/v1/order/load-session",
method: "POST",
@ -154,7 +158,7 @@ const endpoints = {
},
TaxOrderItems: {
url: "/price/api/v1/price/taxed-order-items",
method: "GET",
method: "POST",
},
LogExperimentExposureIfAssigned: {
url: "/experiments/api/v1/experiments/log-exposure",
@ -208,6 +212,18 @@ const endpoints = {
url: "/order/api/v1/order/add-donation-part",
method: "POST",
},
GetOemAfterMarketParts: {
url: "/parts/api/v1/parts/oem-after-market-parts",
method: "POST",
},
InitializeAdyenPayment: {
url: "/payment/api/v1/payment/payment/session/initialize-session",
method: "POST",
},
GetAdyenSessionResult: {
url: "/payment/api/v1/payment/payment/session/session-result",
method: "POST",
},
};
export { endpoints };

View file

@ -12,6 +12,9 @@ const experimentSettings = {
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
PIA_EXPERIENCE: "PIA Experience",
CONDITIONAL_PROVIDER_NUMBERS: "ConditionalProviderNumbers",
CONDITIONAL_PROVIDER_NUMBERS_2: "ConditionalProviderNumbers2",
CONDITIONAL_PROVIDER_SHUTOFF_DATE: "ConditionalProviderShutoffDate",
PIA_INSURANCE: "DisplayPIAInsurance",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional",
@ -33,6 +36,11 @@ const experimentSettings = {
SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays",
SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays",
SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays",
SHOW_MOBILE_FIRST_POPUP_REPAIR: "Show_Mobile_First_Popup_Repair",
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR",
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR",
OFFER_OEM: "OfferOem",
USE_ADYEN_PAYMENT: "UseAdyenPayment",
};
const experimentTriggers = {

View file

@ -10,7 +10,9 @@ const partTypeStrings = {
REPAIR_FEE: "REPAIR FEE",
EARLY_BIRD: "EARLY BIRD",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
DONATION: "DONATION",
WINDSHIELD: "WINDSHIELD",
};
export { partTypeStrings };

View file

@ -48,6 +48,10 @@ export const pageProgressMapper = {
percent: 30,
step: 1,
},
"policy-info": {
percent: 55,
step: 2,
},
quote: {
percent: 60,
step: 2,
@ -80,6 +84,10 @@ export const pageProgressMapper = {
percent: 98.5,
step: 4,
},
"payment-adyen": {
percent: 98.5,
step: 4,
},
confirmation: {
percent: 100,
step: 4,

View file

@ -0,0 +1,105 @@
import { packageNames } from "./package-names";
import { partTypeStrings } from "./part-type-strings";
export const quotePageDiscountTable = [
// Repair
{
name: "Repair Glass Only",
experimentCode: "Show_GlassOnly_Repair_Discount",
partNumber: "GL RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Repair Standard",
experimentCode: "Show_Standard_Repair_Discount",
partNumber: "STD RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Repair Premium",
experimentCode: "Show_Premium_Repair_Discount",
partNumber: "PRM RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
// Replace - No recal
{
name: "Replace Glass Only",
experimentCode: "Show_GlassOnly_Replace_Discount",
partNumber: "GL RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Replace Standard",
experimentCode: "Show_Standard_Replace_Discount",
partNumber: "STD RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Replace Premium",
experimentCode: "Show_Premium_Replace_Discount",
partNumber: "PRM RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
// Replace - with recal
{
name: "Recal Glass Only",
experimentCode: "Show_GlassOnly_Recal_Discount",
partNumber: "GL RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Recal Standard",
experimentCode: "Show_Standard_Recal_Discount",
partNumber: "STD RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Recal Premium",
experimentCode: "Show_Premium_Recal_Discount",
partNumber: "PRM RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
// Replace - Curve Tempered
{
name: "Curve Tempered Glass Only",
experimentCode: "Show_GlassOnly_Replace_CT_Discount",
partNumber: "GL CT CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS CT GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Curve Tempered Standard",
experimentCode: "Show_Standard_Replace_CT_Discount",
partNumber: "STD CT CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD CT GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Curve Tempered Premium",
experimentCode: "Show_Premium_Replace_CT_Discount",
partNumber: "PRM CT CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM CT GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
];

View file

@ -11,6 +11,8 @@ const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
const RECAL_ACK_YES = "Yes";
const RouteCodeFlags = {
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
@ -31,4 +33,5 @@ export {
PRICING_BY_DAY_PART_TYPE,
RouteCodeFlags,
cmsWidgetFieldMappings,
RECAL_ACK_YES,
};

View file

@ -23,6 +23,7 @@ const storeActions = {
GET_ALERT_REASONS_BY_CTU: "getAlertReasonsByCtu",
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
GET_PARTS: "getParts",
GET_OEM_AFTERMARKET_PARTS: "getOemAfterMarketParts",
GET_WIPERS: "getWipers",
GET_RAIN_REPEL: "getRainRepel",
GET_SUPPORTING_ITEMS: "getSupportingItems",
@ -32,7 +33,6 @@ const storeActions = {
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
GET_MOBILE_FEE_PART: "getMobileFeePart",
GET_PRICING_BY_DAY_PART: "getPricingByDayPart",
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",
@ -111,6 +111,8 @@ const storeActions = {
SAVE_PAYPAL_TOKEN: "savePaypalToken",
SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount",
SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn",
SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling",
SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected",
SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable",
CREATE_SUBMITTED_STATE: "createSubmittedState",

View file

@ -69,6 +69,8 @@ const storeMutations = {
LOCK_TOKEN: "updateLockToken",
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn",
UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling",
UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected",
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",
@ -117,6 +119,7 @@ const storeMutations = {
UPDATE_EXTERNAL_PARAMETER_SERVICE_PACKAGE: "updateExternalParameterServicePackage",
UPDATE_EXTERNAL_PARAMETER_SOURCE: "updateExternalParameterSource",
UPDATE_EXTERNAL_PARAMETER_PHONE_NUMBER: "updateExternalParameterPhoneNumber",
UPDATE_EXTERNAL_PARAMETER_QS_STASH: "updateExternalParameterQsStash",
//RESET EXTERNAL_PARAMETER MUTATIONS
RESET_EXTERNAL_PARAMETER_VEHICLE_STATE: "resetExternalParameterVehicleState",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,18 @@
<template>
<div
class="date-picker text-center"
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`">
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass} ${loadingStatus === 'reinitial' ? 'loading-reinitial' : ''}`">
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
<legend class="sr-only">Select a day and time</legend>
<div
v-for="month in months"
v-for="(month, i) in months"
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
class="calendar-grid-container"
class="calendar-grid-container month"
:class="[
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
month.monthClass,
i === 0 ? 'first-month-shown' : '',
]">
<div class="month-year body-small d-flex align-items-center">
{{ month.monthLabel }} {{ month.yearNum?.toString() }}
@ -114,7 +115,8 @@
</div>
</div>
<loader
:class="[!isLoading ? 'date-picker-hidden' : '']"
class="date-picker-loader"
:class="[loadingStatus !== 'none' ? 'show-loader' : '']"
loaderColor="blue"
loaderPosition="center" />
<div class="row form-test-error" id="date-of-month-error">
@ -125,7 +127,7 @@
type="button"
class="btn btn-link view-more-dates"
id="viewMoreDates"
:disabled="isLoading"
:disabled="loadingStatus !== 'none'"
@click="showAnotherMonth">
View more dates
</button>
@ -160,7 +162,6 @@ export default {
name: "datePicker",
data() {
return {
isLoading: true,
months: null,
disableViewMoreDatesButton: false,
hideSomeDaysForInitialView: null,
@ -169,7 +170,10 @@ export default {
};
},
props: {
customComponentId: String,
customComponentId: {
type: String,
default: "",
},
selectableDatesSetting: {
type: String,
validator(value) {
@ -200,6 +204,10 @@ export default {
pricingByDayUpcharge: Number,
isPricingByDayExperiment: Boolean,
isMobileSelected: Boolean,
loadingStatus: {
type: String,
default: "none",
},
},
setup(props) {
const uuid = uuidv4();
@ -281,7 +289,7 @@ export default {
if (event.screenX === 0 && event.screenY === 0) {
return;
}
this.$emit("date-clicked", date);
this.$emit("date-selected", date);
},
getWeekStartDate(dateString) {
const date = convertDateStringToDate(dateString);
@ -324,9 +332,9 @@ export default {
let preSelectedDateMonthEnd = this.getMonthEnd(
preSelectedDate.replace("-mobile", "")
);
let weekIncludesPreSelectedMonthEnd = false;
let monthEndsByThisWeek = false;
let i = 0;
while (!weekIncludesPreSelectedMonthEnd) {
while (!monthEndsByThisWeek && i < 52) {
if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate);
@ -338,7 +346,12 @@ export default {
preSelectedDateMonthEnd === weekEndDate
) {
weekEndDate = preSelectedDateMonthEnd;
weekIncludesPreSelectedMonthEnd = true;
monthEndsByThisWeek = true;
} else if (preSelectedDateMonthEnd < weekEndDate) {
// Additional case: if month ends before this week (but not necessarily during it),
// still cut the loop here, but don't truncate the week.
monthEndsByThisWeek = true;
}
}
weeks.push({
@ -466,7 +479,7 @@ export default {
const response = config.getSelectableDatesCallback({
startDateString: initialViewStartDate,
endDateString: initialViewEndDate,
providerNumber: config.providerNumber,
inshopProviderNumber: config.inshopProviderNumber,
zipCode: config.zipCode,
includeMobileTimeSlots: config.includeMobileTimeSlots,
includeInshopTimeSlots: config.includeInshopTimeSlots,
@ -489,7 +502,6 @@ export default {
});
},
async setCalendarData(config = {}) {
this.isLoading = true;
this.selectableDatesInshop = [];
this.selectableDatesMobile = [];
this.months = [];
@ -541,7 +553,6 @@ export default {
// }
}
this.months = months;
this.isLoading = false;
if (config.preSelectedDate) {
this.$nextTick(() => {
@ -729,7 +740,6 @@ export default {
},
async showAnotherMonth() {
if (!this.months) return;
this.isLoading = true;
let monthToShow;
let monthStartDate;
@ -774,7 +784,6 @@ export default {
monthToShowDates[monthToShowDates.length - 1].inputValue
);
this.isLoading = false;
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
this.scrollToElement(monthToShow.monthString);
@ -787,7 +796,6 @@ export default {
},
async updateSelectableDates(monthStart, monthEnd) {
const moreSelectableDates = await this.getMoreDatesCallback(monthStart, monthEnd);
moreSelectableDates.inshopTimeSlotsData.days.forEach((selectableDate) => {
const index = this.selectableDatesInshop.findIndex(
(dateObj) => dateObj.date === selectableDate.date
@ -846,12 +854,6 @@ export default {
},
},
watch: {
isLoading(newValue) {
// if done loading dates, then set to first available date
if (newValue === false && this.firstAvailableSelectableDate) {
this.selectedDate = this.firstAvailableSelectableDate;
}
},
modelValue(newValue) {
this.resetField({
value: newValue,
@ -872,9 +874,14 @@ export default {
</script>
<style lang="scss" scoped>
.date-picker-hidden {
.date-picker-loader {
opacity: 0;
max-height: 0;
&.show-loader {
opacity: 1;
max-height: none;
}
}
.date-picker {
position: relative;
@ -903,12 +910,12 @@ export default {
height: 1.5rem;
}
}
.calendar-grid-container {
.month {
margin: 0 auto 1rem auto;
position: relative;
transition:
height ease 2s,
opacity ease 2s;
height ease 1s,
max-height ease 1s;
display: grid;
grid-template-columns: repeat(7, 1fr);
justify-content: center;
@ -1157,7 +1164,7 @@ export default {
}
.past {
.calendar-grid-container {
.month {
.month-year {
grid-area: 1 / 1 / 2 / 6;
}
@ -1214,7 +1221,7 @@ export default {
}
// new calendar styling overrides
&.calendar-2025 {
.calendar-grid-container .grid-item {
.month .grid-item {
margin: 0 0 0.15rem 0;
}
.month-year {
@ -1316,6 +1323,33 @@ export default {
}
}
}
&.loading-reinitial {
.month {
opacity: 0;
max-height: 0;
}
.month.first-month-shown {
opacity: 1;
max-height: none;
.grid-item.radio-wrapper.selectable-day {
input[type="radio"]:checked + label,
label {
background-color: $white;
border: none;
color: initial;
cursor: default;
&:hover {
border: none;
}
span {
font-family: UrbanistRegular;
}
}
}
}
}
}
:deep(.timeslots .button-inner-wrapper) {
display: flex;

View file

@ -160,6 +160,10 @@ export default {
return this.staticBackdrop ? "static" : "true";
},
},
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.closeModal();
},
components: {
modalButtonMain,
},

View file

@ -1,4 +1,9 @@
/* eslint-disable */
// ***************************************************************
// This file wraps the Salesforce generated code.
// See the comment below for where you need to paste in the Salesforce generated code.
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
// ***************************************************************
function setupCustomSettings(
modifyEmbeddedSvcCallback,
@ -7,90 +12,38 @@ function setupCustomSettings(
onWebchatCloseCallback
) {
modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
}
/*
The code below is generated from salesforce but modified in the following ways:
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
* CSS is not copied in
* First <script> tag loading in the file from the CDN is not copied in
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForDev
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
Actual call to paste in:
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
*/
export function initializeSalesforceWebchatForDev(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
var initESW = function (gslbBaseURL) {
embedded_svc.settings.displayHelpButton = true; //Or false
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading)
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session)
// Settings for Chat
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) {
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form.
// Returns a valid button ID.
//};
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
// START OF SALESFORCE GENERATED CODE
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
try {
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
embedded_svc.settings.entryFeature = "LiveAgent";
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
embedded_svc.init(
"https://safelite2--dev.sandbox.my.salesforce.com",
"https://safelite2--dev.sandbox.my.salesforce-sites.com/chat",
gslbBaseURL,
embeddedservice_bootstrap.init(
"00D6s0000008r6A",
"Sales_Central_Snap_in",
"Sales_Central_TFN",
"https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477",
{
baseLiveAgentContentURL:
"https://c.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/content",
deploymentId: "572Wr0000017xKX",
buttonId: "573Wr000000CpgH",
baseLiveAgentURL: "https://d.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/chat",
eswLiveAgentDevName:
"EmbeddedServiceLiveAgent_Parent04IWr000003yKq9MAE_1961681659a",
isOfflineSupportEnabled: false,
scrt2URL: "https://safelite2--dev.sandbox.my.salesforce-scrt.com",
}
);
};
if (!window.embedded_svc) {
var s = document.createElement("script");
s.setAttribute(
"src",
"https://safelite2--dev.sandbox.my.salesforce.com/embeddedservice/5.0/esw.min.js"
);
s.onload = function () {
initESW(null);
};
document.body.appendChild(s);
} else {
initESW("https://service.force.com");
} catch (err) {
console.error("Error loading Embedded Messaging: ", err);
}
// END OF SALESFORCE GENERATED CODE
}

View file

@ -1,4 +1,9 @@
/* eslint-disable */
// ***************************************************************
// This file wraps the Salesforce generated code.
// See the comment below for where you need to paste in the Salesforce generated code.
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
// ***************************************************************
function setupCustomSettings(
modifyEmbeddedSvcCallback,
@ -7,91 +12,39 @@ function setupCustomSettings(
onWebchatCloseCallback
) {
modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
}
/*
The code below is generated from salesforce but modified in the following ways:
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
* CSS is not copied in
* First <script> tag loading in the file from the CDN is not copied in
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForDev
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
Actual call to paste in:
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
*/
export function initializeSalesforceWebchatForProd(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
var initESW = function (gslbBaseURL) {
let embedded_svc = window.embedded_svc;
embedded_svc.settings.displayHelpButton = true; //Or false
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
// START OF SALESFORCE GENERATED CODE
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
try {
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading)
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session)
// Settings for Chat
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) {
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form.
// Returns a valid button ID.
//};
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
embedded_svc.settings.entryFeature = "LiveAgent";
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
embedded_svc.init(
"https://safelite2.my.salesforce.com",
"https://safelite2.my.salesforce-sites.com/chat",
gslbBaseURL,
embeddedservice_bootstrap.init(
"00D3t000004nvsT",
"Sales_Central_Snap_in",
"Sales_Central_TFN",
"https://safelite2.my.site.com/ESWSalesCentralTFN1769108630294",
{
baseLiveAgentContentURL:
"https://c.la13-core2.sfdc-lywfpd.salesforceliveagent.com/content",
deploymentId: "5723t0000008wWB",
buttonId: "5733t000000D90z",
baseLiveAgentURL: "https://d.la13-core2.sfdc-lywfpd.salesforceliveagent.com/chat",
eswLiveAgentDevName:
"EmbeddedServiceLiveAgent_Parent04I3t000000Cq0SEAS_188de1871ea",
isOfflineSupportEnabled: false,
scrt2URL: "https://safelite2.my.salesforce-scrt.com",
}
);
};
if (!window.embedded_svc) {
var s = document.createElement("script");
s.setAttribute("src", "https://safelite2.my.salesforce.com/embeddedservice/5.0/esw.min.js");
s.onload = function () {
initESW(null);
};
document.body.appendChild(s);
} else {
initESW("https://service.force.com");
} catch (err) {
console.error("Error loading Embedded Messaging: ", err);
}
// END OF SALESFORCE GENERATED CODE
}

View file

@ -1,4 +1,9 @@
/* eslint-disable */
// ***************************************************************
// This file wraps the Salesforce generated code.
// See the comment below for where you need to paste in the Salesforce generated code.
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
// ***************************************************************
function setupCustomSettings(
modifyEmbeddedSvcCallback,
@ -7,93 +12,38 @@ function setupCustomSettings(
onWebchatCloseCallback
) {
modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
}
/*
The code below is generated from salesforce but modified in the following ways:
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
* CSS is not copied in
* First <script> tag loading in the file from the CDN is not copied in
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForQa
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
Actual call to paste in:
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
*/
export function initializeSalesforceWebchatForQa(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
var initESW = function (gslbBaseURL) {
embedded_svc.settings.displayHelpButton = true; //Or false
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
// START OF SALESFORCE GENERATED CODE
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
try {
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading)
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session)
// Settings for Chat
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) {
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form.
// Returns a valid button ID.
//};
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
embedded_svc.settings.entryFeature = "LiveAgent";
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
embedded_svc.init(
"https://safelite2--safeliteua.sandbox.my.salesforce.com",
"https://safelite2--safeliteua.sandbox.my.salesforce-sites.com/chat",
gslbBaseURL,
"00DDJ000000HRqO",
"Sales_Central_Snap_in",
embeddedservice_bootstrap.init(
"00DTI000005x7c7",
"Sales_Central_TFN",
"https://safelite2--qa.sandbox.my.site.com/ESWSalesCentralTFN1768832680884",
{
baseLiveAgentContentURL:
"https://c.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/content",
deploymentId: "572DJ00000001j3",
buttonId: "573DJ00000001ur",
baseLiveAgentURL: "https://d.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/chat",
eswLiveAgentDevName:
"EmbeddedServiceLiveAgent_Parent04IDJ0000008OZk2AM_1883057c9a6",
isOfflineSupportEnabled: false,
scrt2URL: "https://safelite2--qa.sandbox.my.salesforce-scrt.com",
}
);
};
if (!window.embedded_svc) {
var s = document.createElement("script");
s.setAttribute(
"src",
"https://safelite2--safeliteua.sandbox.my.salesforce.com/embeddedservice/5.0/esw.min.js"
);
s.onload = function () {
initESW(null);
};
document.body.appendChild(s);
} else {
initESW("https://service.force.com");
} catch (err) {
console.error("Error loading Embedded Messaging: ", err);
}
// END OF SALESFORCE GENERATED CODE
}

View file

@ -0,0 +1,50 @@
/* eslint-disable */
// ***************************************************************
// This file wraps the Salesforce generated code.
// See the comment below for where you need to paste in the Salesforce generated code.
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
// ***************************************************************
function setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
modifyEmbeddedSvcCallback();
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
}
export function initializeSalesforceWebchatForSys(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
// START OF SALESFORCE GENERATED CODE
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
try {
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
embeddedservice_bootstrap.init(
"00DcY0000067qKz",
"Sales_Central_TFN",
"https://safelite2--sys.sandbox.my.site.com/ESWSalesCentralTFN1767797265631",
{
scrt2URL: "https://safelite2--sys.sandbox.my.salesforce-scrt.com",
}
);
} catch (err) {
console.error("Error loading Embedded Messaging: ", err);
}
// END OF SALESFORCE GENERATED CODE
}

View file

@ -2,10 +2,20 @@ import { shallowMount } from "@vue/test-utils";
import salesforceWebchat from "./salesforce-webchat.vue";
import * as devHelper from "./salesforce-helper-dev";
import * as qaHelper from "./salesforce-helper-qa";
import * as sysHelper from "./salesforce-helper-sys";
import * as prodHelper from "./salesforce-helper-prod";
import { applicationConfig } from "../../constants/application-config";
describe("salesforceWebchat.vue", () => {
beforeEach(() => {
window.embeddedservice_bootstrap = {
settings: {},
};
});
afterEach(() => {
delete window.embeddedservice_bootstrap;
});
it("renders correctly", () => {
const wrapper = shallowMount(salesforceWebchat);
expect(wrapper.exists()).toBe(true);
@ -14,7 +24,7 @@ describe("salesforceWebchat.vue", () => {
it("has the correct default data", () => {
const wrapper = shallowMount(salesforceWebchat);
expect(wrapper.vm.$data).toEqual({
isAgentAvailable: false,
isWebchatLoaded: false,
isWebchatOpen: false,
});
});
@ -37,6 +47,12 @@ describe("salesforceWebchat.vue", () => {
wrapper.vm.initializeSalesforceWebchat();
expect(mockQa).toHaveBeenCalled();
applicationConfig.CURRENT_ENVIRONMENT = "SysTest";
wrapper = shallowMount(salesforceWebchat);
const mockSys = jest.spyOn(sysHelper, "initializeSalesforceWebchatForSys");
wrapper.vm.initializeSalesforceWebchat();
expect(mockSys).toHaveBeenCalled();
applicationConfig.CURRENT_ENVIRONMENT = "Dev";
wrapper = shallowMount(salesforceWebchat);
const mockDev = jest.spyOn(devHelper, "initializeSalesforceWebchatForDev");

View file

@ -6,6 +6,7 @@
// Supporting files
import { initializeSalesforceWebchatForDev } from "./salesforce-helper-dev";
import { initializeSalesforceWebchatForQa } from "./salesforce-helper-qa";
import { initializeSalesforceWebchatForSys } from "./salesforce-helper-sys";
import { initializeSalesforceWebchatForProd } from "./salesforce-helper-prod";
import { applicationConfig } from "@/constants/application-config";
import { webchatHelper } from "@/helpers/webchat-helper";
@ -16,7 +17,7 @@ export default {
mixins: [analyticsMixin],
data() {
return {
isAgentAvailable: false,
isWebchatLoaded: false,
isWebchatOpen: false,
};
},
@ -33,32 +34,13 @@ export default {
mounted() {
// Load in script from salesforce CDN
const script = document.createElement("script");
script.src = "https://service.force.com/embeddedservice/5.0/esw.min.js";
script.src = this.webChatScriptSrc;
script.onload = this.initializeSalesforceWebchat;
document.body.appendChild(script);
// Notify funnel-header about Salesforce chat open/close state
this._salesforceChatOpenObserver = new MutationObserver(() => {
const isOpen = !!document.querySelector(".embeddedServiceSidebar");
window.dispatchEvent(
new CustomEvent("salesforce-chat-visibility", { detail: { open: isOpen } })
);
});
this._salesforceChatOpenObserver.observe(document.body, {
childList: true,
subtree: true,
});
},
beforeUnmount() {
// Clean up event listeners and observers
window.removeEventListener(
"salesforce-chat-visibility",
this._handleSalesforceChatVisibility
);
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
if (this._salesforceChatOpenObserver) {
this._salesforceChatOpenObserver.disconnect();
}
},
methods: {
initializeSalesforceWebchat() {
@ -72,7 +54,6 @@ export default {
);
break;
case "QA":
case "SysTest":
initializeSalesforceWebchatForQa(
this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback,
@ -80,8 +61,17 @@ export default {
this.onWebchatCloseCallback
);
break;
case "SysTest":
initializeSalesforceWebchatForSys(
this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback,
this.onWebchatOpenCallback,
this.onWebchatCloseCallback
);
break;
case "Dev":
case "Localhost":
default:
initializeSalesforceWebchatForDev(
this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback,
@ -92,16 +82,13 @@ export default {
}
},
modifyEmbeddedSvcCallback() {
window.embedded_svc.settings.avatarImgURL = require("@/assets/img/salesforce-webchat/avatar_webchat.png");
window.embedded_svc.settings.prechatBackgroundImgURL = require("@/assets/img/salesforce-webchat/safelite_logo_webchat.png");
window.embedded_svc.settings.smallCompanyLogoImgURL = require("@/assets/img/salesforce-webchat/minimized_chat_bubble_webchat.png");
window.embedded_svc.settings.displayHelpButton = false;
window.embedded_svc.addEventHandler("onChatEstablished", () => {
window.embeddedservice_bootstrap.settings.hideChatButtonOnLoad = true;
window.addEventListener("onEmbeddedMessagingConversationStarted", () => {
this.pushEventForChatsToGA("support_chat", "chat_opened", "Live Agent", true);
});
},
finishedLoadingCallback(data) {
this.isAgentAvailable = data.isAgentAvailable;
this.isWebchatLoaded = true;
this.updateShouldShowWebchatButton();
},
onWebchatOpenCallback() {
@ -117,16 +104,35 @@ export default {
updateShouldShowWebchatButton() {
this.webchatGlobalNonpersistedState.showWebchatButton =
!this.hideSalesforceWebchatLaunchButton &&
this.isAgentAvailable &&
this.isWebchatLoaded &&
!this.isWebchatOpen;
},
},
computed: {},
computed: {
webChatScriptSrc() {
// These URLs are from the src tag on the script tag in the Salesforce generated code.
switch (applicationConfig.CURRENT_ENVIRONMENT) {
case "Prod":
return "https://safelite2.my.site.com/ESWSalesCentralTFN1769108630294/assets/js/bootstrap.min.js";
case "QA":
return "https://safelite2--qa.sandbox.my.site.com/ESWSalesCentralTFN1768832680884/assets/js/bootstrap.min.js";
case "SysTest":
return "https://safelite2--sys.sandbox.my.site.com/ESWSalesCentralTFN1767797265631/assets/js/bootstrap.min.js";
case "Dev":
case "Localhost":
default:
return "https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477/assets/js/bootstrap.min.js";
}
},
},
components: {},
};
</script>
<style lang="scss">
.embedded-messaging > .embeddedMessagingFrame[class~="isMaximized"] {
z-index: 9999 !important;
}
.dockableContainer {
&.showDockableContainer {
color: $gray-600;

View file

@ -59,14 +59,6 @@ describe("sierraWebchat.vue", () => {
expect(window.sierraChat.closeChatModal).toHaveBeenCalled();
});
it("dispatches sierra-chat-closed on handleSierraOnClose", () => {
const eventSpy = jest.spyOn(window, "dispatchEvent");
wrapper.vm.createSierraConfig().onClose();
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({ type: "sierra-chat-closed" })
);
});
it("dispatches sierra-chat-transfer on handleSierraOnTransfer", () => {
const eventSpy = jest.spyOn(window, "dispatchEvent");
const transfer = {
@ -77,25 +69,4 @@ describe("sierraWebchat.vue", () => {
expect.objectContaining({ type: "sierra-chat-transfer" })
);
});
it("handles sierra-chat-transfer event and starts Salesforce chat with prepopulated fields", () => {
const transfer = {
data: {
first_name: "A",
last_name: "B",
email: "a@b.com",
chat_summary: "summary",
},
};
const event = new CustomEvent("sierra-chat-transfer", { detail: transfer });
window.dispatchEvent(event);
expect(window.sierraChat.closeChatModal).toHaveBeenCalled();
expect(window.embedded_svc.settings.prepopulatedPrechatFields).toEqual({
FirstName: "A",
LastName: "B",
Email: "a@b.com",
Subject: "summary",
});
expect(window.embedded_svc.liveAgentAPI.startChat).toHaveBeenCalled();
});
});

View file

@ -7,10 +7,20 @@ import analyticsMixin from "@/mixins/analytics-mixin";
import store from "@/store";
import { routeData } from "@/router/constants/routes";
import { deepClone } from "@/helpers/object-helper";
import { webchatHelper } from "@/helpers/webchat-helper";
export default {
name: "sierraWebchat",
mixins: [analyticsMixin],
data() {
return {
transferInProgress: false,
};
},
setup() {
const { webchatGlobalNonpersistedState, launchWebchat } = webchatHelper();
return { webchatGlobalNonpersistedState, launchWebchat };
},
methods: {
openSierraChatModal() {
const sierra = window.sierraChat || window.sierra || window.SierraChat;
@ -35,11 +45,18 @@ export default {
display: "corner",
onLoad: () => this.openSierraChatModal(),
onOpen: () => {},
onClose: () => window.dispatchEvent(new CustomEvent("sierra-chat-closed")),
onTransfer: (transfer) =>
onClose: () => {
if (!this.transferInProgress) {
this.webchatGlobalNonpersistedState.showWebchatButton = true;
this.transferInProgress = false;
}
},
onTransfer: (transfer) => {
this.transferInProgress = true;
window.dispatchEvent(
new CustomEvent("sierra-chat-transfer", { detail: transfer })
),
);
},
};
},
getSierraContext() {
@ -138,10 +155,7 @@ export default {
}
// Pass data to Salesforce chat if available
const transfer = event.detail;
if (
window.embedded_svc &&
typeof window.embedded_svc.bootstrapEmbeddedService === "function"
) {
if (window.embeddedservice_bootstrap && window.embeddedservice_bootstrap.prechatAPI) {
if (
transfer &&
transfer.data &&
@ -149,51 +163,22 @@ export default {
transfer.data.last_name &&
transfer.data.email
) {
window.embedded_svc.settings.prepopulatedPrechatFields = {
window.embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields({
ScarlettAISummary: transfer.data.chat_summary,
FirstName: transfer.data.first_name,
LastName: transfer.data.last_name,
Email: transfer.data.email,
Subject: transfer.data.chat_summary,
};
window.embedded_svc.settings.extraPrechatFormDetails = [
{
label: "Scarlett AI Summary",
value: transfer.data.chat_summary,
transcriptFields: ["Scarlett_AI_Summary__c"],
displayToAgent: true,
},
];
if (
window.embedded_svc.liveAgentAPI &&
typeof window.embedded_svc.liveAgentAPI.startChat === "function"
) {
window.embedded_svc.liveAgentAPI.startChat();
return;
}
});
}
window.embedded_svc.bootstrapEmbeddedService();
this.webchatGlobalNonpersistedState.shouldLaunchSierra = false;
this.launchWebchat();
}
};
window.addEventListener("sierra-chat-transfer", this._handleSierraTransfer);
// Notify funnel-header about Sierra chat open/close state
this._sierraChatContainerObserver = new MutationObserver(() => {
const isOpen = !!document.querySelector("[data-sierra-chat-container]");
window.dispatchEvent(
new CustomEvent("sierra-chat-visibility", { detail: { open: isOpen } })
);
});
this._sierraChatContainerObserver.observe(document.body, {
childList: true,
subtree: true,
});
},
beforeUnmount() {
window.removeEventListener("launch-sierra-webchat", this.triggerSierraChat);
window.removeEventListener("sierra-chat-transfer", this._handleSierraTransfer);
if (this._sierraChatContainerObserver) {
this._sierraChatContainerObserver.disconnect();
}
},
};
</script>

View file

@ -145,9 +145,7 @@ export default {
return "$" + decimalPrice.toFixed(2);
},
hasPackageDiscount() {
return (
this.additionalButtonData.servicePackageDiscount && this.additionalButtonData.Text
);
return this.additionalButtonData.hasDiscount && this.additionalButtonData.Text;
},
},
};

View file

@ -4,11 +4,11 @@
class="package-label pricing-by-day-pkg-lbl"
:class="[
this.buttonLabelSubCopy ? 'has-subheader' : '',
!this.additionalButtonData.servicePackageDiscount ? 'adjust-top' : '',
!this.additionalButtonData.hasDiscount ? 'adjust-top' : '',
]"
for="testradio">
<div class="package-specs">
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row">
<div v-if="this.additionalButtonData.hasDiscount" class="row">
<div class="col md-6">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
@ -84,10 +84,7 @@
</div>
<div
v-if="
this.additionalButtonData.servicePackageDiscount &&
this.additionalButtonData.Text
"
v-if="this.additionalButtonData.hasDiscount && this.additionalButtonData.Text"
class="special-save-box">
<span v-html="this.additionalButtonData.Text"></span>
</div>

View file

@ -190,6 +190,7 @@ import { cartItemCategories } from "@/constants/cart-item-categories";
import { cartItemTypes } from "@/constants/cart-item-types";
import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance";
import { experimentSettings } from "@/constants/experiments";
import { quotePageDiscountTable } from "@/constants/quote-page-discounts";
export default {
name: "cart",
@ -256,11 +257,11 @@ export default {
return subTotal;
},
getServicePackageDiscount() {
const servicePackageDiscountLineItems = this.servicePackageDiscountCartItem;
getQuotePageDiscount() {
const quotePageDiscountLineItems = this.quotePageDiscountCartItem;
let subTotal = 0;
subTotal += servicePackageDiscountLineItems?.subTotal ?? 0;
subTotal += quotePageDiscountLineItems?.subTotal ?? 0;
return subTotal;
},
getVapsCartItemsForSelectedPackage(packageName) {
@ -303,7 +304,8 @@ export default {
getLineItemAmount(amount, useVerifyingText, category) {
if (useVerifyingText) return this.verifyingCoverageText;
return category == "promos" || category == "servicePackageDiscount"
return category == cartItemCategories.PROMOS ||
category == cartItemCategories.QUOTE_PAGE_DISCOUNT
? "(" + this.currencyFormatter.format(amount * -1) + ")"
: this.currencyFormatter.format(amount);
},
@ -316,16 +318,21 @@ export default {
if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) {
this.saveVaps(this.lineItems);
// Check if service package discount should be removed after vaps change
if (
this.servicePackageDiscountCartItem &&
this.discountPackageNames != this.packageLevel
) {
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
(lineItemsToKeep) =>
lineItemsToKeep.cartItemType !=
this.servicePackageDiscountCartItem.cartItemType
if (this.quotePageDiscountCartItem) {
const existingLineItem = this.quotePageDiscountCartItem.lineItems.at(0);
const discountInfo = quotePageDiscountTable.find(
(info) => info.partNumber === existingLineItem?.partNumber
);
shouldSaveSupportingItems = true;
const packageLevelForDiscount = discountInfo?.packageLevel;
if (this.packageLevel !== packageLevelForDiscount) {
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
(lineItemsToKeep) =>
lineItemsToKeep.cartItemType !=
this.quotePageDiscountCartItem.cartItemType
);
shouldSaveSupportingItems = true;
}
}
}
if (shouldSaveSupportingItems) {
@ -483,8 +490,8 @@ export default {
cartItems.push(this.mobileFeeCartItem);
}
if (this.servicePackageDiscountCartItem) {
cartItems.push(this.servicePackageDiscountCartItem);
if (this.quotePageDiscountCartItem) {
cartItems.push(this.quotePageDiscountCartItem);
}
if (this.premiumAppointmentDiscountCartItem) {
@ -500,12 +507,6 @@ export default {
return cartItems;
},
},
discountPackageNames() {
const discountServicePackage = experimentMixin.methods.getSettingValue(
experimentSettings.PROMO_ON_PACKAGE
);
return getDiscountedPackageName(discountServicePackage);
},
servicePackageTitleWidget() {
const servicePackageNames = this.getCmsContent(
this.servicePackageOptionsCmsName,
@ -550,8 +551,8 @@ export default {
}
packagePrice += this.getVapsPrice(this.packageLevel);
if (this.servicePackageDiscountCartItem) {
packagePrice -= this.getServicePackageDiscount();
if (this.quotePageDiscountCartItem) {
packagePrice -= this.getQuotePageDiscount();
}
return packagePrice;
@ -928,25 +929,25 @@ export default {
}
return cartItem;
},
servicePackageDiscountCartItemName() {
quotePageDiscountCartItemName() {
return this.getCmsContent("ServicePackageDiscountTextWidget", "Text");
},
servicePackageDiscountCartItem() {
quotePageDiscountCartItem() {
let cartItem = null;
const servicePackageDiscountLineItem = this.supportingItems.find(
(lineItem) => lineItem.partType == partTypeStrings.SERVICE_PACKAGE_DISCOUNT
const quotePageDiscountLineItem = this.supportingItems.find(
(lineItem) => lineItem.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT
);
if (servicePackageDiscountLineItem) {
if (quotePageDiscountLineItem) {
cartItem = {
name:
this.servicePackageDiscountCartItemName +
this.quotePageDiscountCartItemName +
Math.abs(
baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem)
baseMixin.methods.getTotalLineItemPrice(quotePageDiscountLineItem)
),
category: cartItemCategories.SERVICE_PACKAGE_DISCOUNT,
cartItemType: cartItemTypes.SERVICE_PACKAGE_DISCOUNT,
category: cartItemCategories.QUOTE_PAGE_DISCOUNT,
cartItemType: cartItemTypes.QUOTE_PAGE_DISCOUNT,
isDisplayed: true,
isRemovable: false,
subTotal: 0,
@ -955,15 +956,15 @@ export default {
isCoveredByInsurance: false,
};
servicePackageDiscountLineItem.cartItemType = cartItem.cartItemType;
cartItem.lineItems.push(servicePackageDiscountLineItem);
quotePageDiscountLineItem.cartItemType = cartItem.cartItemType;
cartItem.lineItems.push(quotePageDiscountLineItem);
cartItem.subTotal +=
(servicePackageDiscountLineItem.kitPrice ?? 0) +
(servicePackageDiscountLineItem.laborAmount ?? 0) +
(servicePackageDiscountLineItem.sellingPrice ?? 0);
(quotePageDiscountLineItem.kitPrice ?? 0) +
(quotePageDiscountLineItem.laborAmount ?? 0) +
(quotePageDiscountLineItem.sellingPrice ?? 0);
cartItem.salesTax += servicePackageDiscountLineItem.salesTax ?? 0;
cartItem.salesTax += quotePageDiscountLineItem.salesTax ?? 0;
}
return cartItem;
@ -972,7 +973,7 @@ export default {
let cartItem = null;
const otherSupportingItems = this.supportingItems.filter((item) => {
return item.partType != partTypeStrings.SERVICE_PACKAGE_DISCOUNT;
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
});
// Don't include fees they are part of the package total
let otherSupportingItemsLineItems =

View file

@ -75,17 +75,12 @@ export default {
:deep(a) {
text-decoration: none;
&.new-window-link {
font-family: Urbanist, Arial, Helvetica, sans-serif;
margin: 0 0.75rem;
color: $gray-600;
font-weight: 400;
}
&.new-window-link,
&.navigation-link {
font-family: Urbanist, Arial, Helvetica, sans-serif;
font-family: UrbanistRegular, Arial, Helvetica, sans-serif;
margin: 0 0.75rem;
color: $gray-600;
font-weight: 400;
font-weight: initial;
}
}

View file

@ -59,7 +59,6 @@ export default {
return {
globalAlertMessages: [],
sierraChatOpen: false,
salesforceChatOpen: false,
isSalesforceTransferInProgress: false,
isScrolled: false,
};
@ -80,20 +79,7 @@ export default {
return this.getCmsContent(this.cmsWidgetName, "LogoImage");
},
shouldShowWebchatButton() {
// Hide the chat icon if Sierra chat is open
if (this.sierraChatOpen) {
return false;
}
// Hide the chat icon if Salesforce chat is open or transfer is in progress
if (this.salesforceChatOpen || this.isSalesforceTransferInProgress) {
return false;
}
// Show if either Sierra is enabled or Salesforce button is available and not hidden by page
return (
(this.webchatGlobalNonpersistedState.shouldLaunchSierra ||
this.webchatGlobalNonpersistedState.showWebchatButton) &&
!this.shouldHideWebchatButtonOnPage
);
return this.webchatGlobalNonpersistedState.showWebchatButton;
},
},
methods: {
@ -121,10 +107,6 @@ export default {
webchatClicked(event) {
event.preventDefault();
this.launchWebchat();
// Only set sierraChatOpen if Sierra experiment is active
if (this.webchatGlobalNonpersistedState.shouldLaunchSierra) {
this.sierraChatOpen = true;
}
},
syncSierraExperimentFlag() {
const experiments = store.getters.applicationUser.experiments;
@ -164,37 +146,6 @@ export default {
unknownAlertEvent.displayAlert = true;
this.globalAlertMessages.push(unknownAlertEvent);
}
// Listen for Sierra chat open/close events from sierra-webchat
this._handleSierraChatVisibility = (event) => {
this.sierraChatOpen = !!(event.detail && event.detail.open);
if (this.sierraChatOpen) {
this.isSalesforceTransferInProgress = false;
}
};
window.addEventListener("sierra-chat-visibility", this._handleSierraChatVisibility);
// Listen for Salesforce chat open/close events from salesforce-webchat
this._handleSalesforceChatVisibility = (event) => {
this.salesforceChatOpen = !!(event.detail && event.detail.open === true);
};
window.addEventListener("salesforce-chat-visibility", this._handleSalesforceChatVisibility);
// Listen for transfer event to set transfer-in-progress flag
this._handleSierraToSalesforceTransfer = () => {
this.isSalesforceTransferInProgress = true;
};
window.addEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
// Reset transfer flag when Salesforce chat opens
this._handleSalesforceChatOpened = (event) => {
if (event.detail && event.detail.open === true) {
this.isSalesforceTransferInProgress = false;
this.salesforceChatOpen = true;
} else {
this.salesforceChatOpen = false;
}
};
window.addEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
// Add scroll listener for header shadow effect
this._handleScroll = () => {
@ -203,14 +154,7 @@ export default {
window.addEventListener("scroll", this._handleScroll);
},
beforeUnmount() {
window.removeEventListener("sierra-chat-closed", this._handleSierraChatClosed);
window.removeEventListener("sierra-chat-visibility", this._handleSierraChatVisibility);
window.removeEventListener(
"salesforce-chat-visibility",
this._handleSalesforceChatVisibility
);
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
window.removeEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
window.removeEventListener("scroll", this._handleScroll);
},
};

View file

@ -57,10 +57,6 @@ export default {
modalWidgetName: String,
pageName: String,
},
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.modal?.closeModal();
},
computed: {
buttonText() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");

View file

@ -25,7 +25,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
/^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);

View file

@ -54,10 +54,6 @@ export default {
mounted() {
if (this.showSaveProgressPopup) this.modal.openModal();
},
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.modal.closeModal();
},
emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools
props: {
modelValue: Object,

View file

@ -101,7 +101,13 @@ export default {
return resolve(response);
},
(error) => {
if (endpoint.toLowerCase().includes(endpoints.LogDigitalConsumer.url)) {
if (
endpoint.toLowerCase().includes(endpoints.LogDigitalConsumer.url) ||
endpoint.toLowerCase().includes(endpoints.LogFmgSessionData.url) ||
endpoint.toLowerCase().includes(endpoints.LogPageView.url) ||
endpoint.toLowerCase().includes(endpoints.LogCustomEvent.url) ||
endpoint.toLowerCase().includes(endpoints.LogPartQuestions.url)
) {
return resolve({ data: null, error: "Ignore errors when logging" });
} else {
if (logApiCall) {

168
src/helpers/adyen-helper.js Normal file
View file

@ -0,0 +1,168 @@
import { AdyenCheckout } from "@adyen/adyen-web/auto";
import store from "@/store";
import globalMethods from "@/global-methods";
import { endpoints } from "@/constants/endpoints";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { paymentMethods } from "@/constants/payment-method-constants";
import { applicationConfig } from "../constants/application-config";
export async function getNewSession(sessionConfig) {}
export async function getSessionInfo(sessionId, encryptedSessionResult, pageNameToLog = "") {
const order = store.getters.order;
const location = getLocationInfo(order);
const workOrder = getWorkOrderLastSixDigits(order);
const system = getSourceSystem();
const request = {
sessionId: sessionId,
sessionResult: encryptedSessionResult,
zipCodeCtu: location.zipCodeCtu,
workOrderNumber: workOrder,
sourceSystem: system,
};
const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url,
payload: request,
logApiCall: true,
pageNameToLog: pageNameToLog,
});
return response?.data;
}
export async function createAdyenCheckout({
session: { id, sessionData },
handlers: { onPaymentCompleted, onPaymentFailed, onError },
options: { showPayButton = true, amount },
}) {
const config = {
session: {
id: id,
sessionData: sessionData,
},
clientKey: applicationConfig.ADYEN_CLIENT_KEY,
environment: applicationConfig.ADYEN_ENVIRONMENT,
locale: "en_US",
countryCode: "US",
showPayButton: showPayButton,
translations: {
en_US: {
"creditCard.securityCode.label": "CVV/CVC",
},
},
onPaymentCompleted: onPaymentCompleted,
onPaymentFailed: onPaymentFailed,
onError: onError,
};
if (amount !== undefined && amount !== null) {
config.amount = {
value: amount,
currency: "USD",
};
}
console.log(`Creating checkout with configuration:`);
console.log(config);
return await AdyenCheckout(config);
}
export function mapAdyenToFmgPaymentMethod(adyenMethod) {
const paymentMethodMap = {
["scheme"]: paymentMethods.CREDIT_CARD,
["afterpaytouch"]: paymentMethods.AFTERPAY,
["paypal"]: paymentMethods.PAYPAL,
["applepay"]: paymentMethods.APPLE_PAY,
};
const match = paymentMethodMap[adyenMethod];
return match ?? paymentMethods.CREDIT_CARD;
}
export function mapFmgToAdyenPaymentMethod(fmgMethod) {
const paymentMethodMap = {
[paymentMethods.CREDIT_CARD]: "scheme",
[paymentMethods.AFTERPAY]: "afterpaytouch",
[paymentMethods.PAYPAL]: "paypal",
[paymentMethods.APPLE_PAY]: "applepay",
[paymentMethods.PAY_NOW]: null,
[paymentMethods.LATER]: null,
};
const match = paymentMethodMap[fmgMethod];
return match;
}
export function generateCcToken(adyenSessionInfo) {
const order = store.getters.order;
const locationInfo = getLocationInfo(order);
const ccToken = {
subscriptionId: adyenSessionInfo?.storedToken,
expMonth: adyenSessionInfo?.cardExpiryMonth,
expYear: adyenSessionInfo?.cardExpiryYear,
cardType: adyenSessionInfo?.cardType,
billToPostalCode: locationInfo.zipCode, // TODO - real data?
billToFirstName: order.customer.firstName, // TODO - real data?
billToLastName: order.customer.lastName, // TODO - real data?
referenceNumber: adyenSessionInfo?.workOrderNumber,
authCode: adyenSessionInfo?.authCode,
transactionId: adyenSessionInfo?.transactionReference,
transReferenceNumber: adyenSessionInfo?.transactionReference,
lastFour: adyenSessionInfo?.last4DigitsOfCard,
};
return ccToken;
}
function getLocationInfo(order) {
const serviceLocation = order.serviceLocation;
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
if (isMobile) {
return {
address: serviceLocation.address,
address2: serviceLocation.address2,
city: serviceLocation.city,
state: serviceLocation.state,
zipCode: serviceLocation.zipCode,
zipCodeCtu: serviceLocation.zipCodeCtu,
};
} else {
const providerInfo = serviceLocation.provider.address;
return {
address: providerInfo.streetAddress,
address2: "",
city: providerInfo.city,
state: providerInfo.state,
zipCode: providerInfo.zipCode,
zipCodeCtu: providerInfo.zipCodeCtu,
};
}
}
function getWorkOrderLastSixDigits(order) {
const workOrderNumber = order.workOrderNumber ?? "";
const tokens = workOrderNumber.split("-");
const lastSegment = tokens[tokens.length - 1] ?? "";
const numDigits = lastSegment.length;
const numZeroes = 6 - numDigits;
let result = "";
for (let i = 0; i < numZeroes; i++) {
result += "0";
}
result += lastSegment;
return result;
}
function getSourceSystem() {
return "FMG-2.0";
}

View file

@ -3,6 +3,14 @@ import { partTypeStrings } from "@/constants/part-type-strings";
const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION];
export function anyPartWithRequiresRecalFlag(lineItems) {
if (!lineItems) {
return false;
}
return lineItems.glassParts?.some((li) => li.requiresRecalibration === true);
}
export function isRecalPartOrHasChildRecalPart(lineItem) {
if (lineItem.childParts && lineItem.childParts.length > 0) {
return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts);

View file

@ -254,6 +254,19 @@ export function getDiscountedPackageName(discountPackage) {
return package_names[discountPackage] || null;
}
export function getPackageLabel(packageName) {
switch (packageName) {
case packageNames.TIER_ONE:
return externalParamPackageLabels.GLASS_ONLY;
case packageNames.TIER_TWO:
return externalParamPackageLabels.STANDARD;
case packageNames.TIER_THREE:
return externalParamPackageLabels.PREMIUM;
default:
return null;
}
}
function maxTier(tierA, tierB) {
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
return packageNames.TIER_THREE;

View file

@ -8,10 +8,11 @@ const webchatGlobalNonpersistedState = reactive({
export const webchatHelper = () => {
function launchWebchat() {
webchatGlobalNonpersistedState.showWebchatButton = false;
if (webchatGlobalNonpersistedState.shouldLaunchSierra) {
window.dispatchEvent(new CustomEvent("launch-sierra-webchat")); // launches the sierra chat
} else {
window.embedded_svc.bootstrapEmbeddedService(); // launches the salesForce prechat form
window.embeddedservice_bootstrap.utilAPI.launchChat(); // launches the salesForce prechat form
}
}

View file

@ -91,7 +91,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
/^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);

View file

@ -70,7 +70,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
/^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);

View file

@ -164,6 +164,8 @@ export default {
// await any pending save-sessions. if you don't, we will save the new parent account into vuex and then a pending async save-session response overwrites it
await flushSaveSessionQueue();
await this.removeQPDLineItemIfExists();
await this.dispatchStoreAction(
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
this.parentAccountNumber.toString(),
@ -193,6 +195,17 @@ export default {
handlePageShow() {
window.location.reload();
},
async removeQPDLineItemIfExists() {
const lineItems = deepClone(store.getters.order.lineItems);
lineItems.supportingItems = baseMixin.methods.filterOutQuotePageDiscountPart(
lineItems.supportingItems
);
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
lineItems.supportingItems,
false
);
},
},
unmounted() {
window.removeEventListener("pageshow", this.handlePageShow);

View file

@ -25,6 +25,11 @@
cmsWidgetName="PleaseProvideAddressWidget"
class="provide-address-header" />
<textBlock
v-if="isMSRFeeApplicable()"
cmsWidgetName="VehicleKeysMSRMessageWidget"
class="keys-message" />
<textBlock
v-else
cmsWidgetName="VehicleKeysMessageWidget"
class="keys-message" />
<mobileAddressQuestions
@ -143,6 +148,9 @@ export default {
getIsVehicleProtectedFromStore() {
return store.getters.order.serviceLocation.isVehicleProtected;
},
isMSRFeeApplicable() {
return store.getters.order.isMSRFeeApplicable;
},
async forwardButtonAction() {
await this.dispatchStoreAction(
this.storeActions.SAVE_MOBILE_DETAILS,

View file

@ -306,6 +306,12 @@ describe("partQuestions.vue...", () => {
//Assert
expect(spy).toHaveBeenNthCalledWith(
1,
storeActions.SAVE_IS_OEM_GLASS_SELECTED,
false,
false
);
expect(spy).toHaveBeenNthCalledWith(
2,
"savePartQuestionAnswers",
[
{

View file

@ -188,6 +188,8 @@ export default {
}
});
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
// save to vuex store as order.damage.partQuestionAnswers (array)
// used in GET_PARTS call following this one
await this.dispatchStoreAction(

Some files were not shown because too many files have changed in this diff Show more