From 74afade26a52e7b017d6deff39dd0ea459789800 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Tue, 7 Apr 2026 09:06:42 -0400 Subject: [PATCH 01/47] Add secret for regression runs --- azure-pipelines-automated-testing.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-automated-testing.yml b/azure-pipelines-automated-testing.yml index df7663f6..cf585a17 100644 --- a/azure-pipelines-automated-testing.yml +++ b/azure-pipelines-automated-testing.yml @@ -52,4 +52,5 @@ stages: npmrcPath: 'playwright-tests/.npmrc' secrets: CCIS_API_AUTH: $(CCIS_API_AUTH) - JIRA_API_KEY: $(JIRA_API_KEY) \ No newline at end of file + JIRA_API_KEY: $(JIRA_API_KEY) + IS_REGRESSION: $(IS_REGRESSION) \ No newline at end of file From f6e5805b00e4f29c6496dee58229198d608bf62b Mon Sep 17 00:00:00 2001 From: JennyNou Date: Tue, 7 Apr 2026 10:15:45 -0400 Subject: [PATCH 02/47] Change method to fix flakiness with address selection --- playwright-tests/pages/PolicyHolderDetailsPage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/pages/PolicyHolderDetailsPage.ts b/playwright-tests/pages/PolicyHolderDetailsPage.ts index 2cca536a..d7ab5125 100644 --- a/playwright-tests/pages/PolicyHolderDetailsPage.ts +++ b/playwright-tests/pages/PolicyHolderDetailsPage.ts @@ -45,7 +45,7 @@ export class PolicyHolderDetailsPage extends BasePage { // For essential flows, when address fields are not pre-filled if (!addressValue || addressValue.trim() === '') { await this.addressInputBox.click(); - await this.addressInputBox.fill(customerDetails.address.street); + await this.addressInputBox.pressSequentially(customerDetails.address.street); // Wait for suggestions to load (Google Places has a slight delay) await this.page.locator('.pac-item').first().waitFor({ state: 'visible', timeout: 6000 }); From e3f0eb44c88a935dd8f47a946593ae0586954794 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 09:45:18 -0400 Subject: [PATCH 03/47] Update flow for look up vin on alert --- playwright-tests/pages/VinLookupPage.ts | 2 +- playwright-tests/tests/0000__M.test.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/playwright-tests/pages/VinLookupPage.ts b/playwright-tests/pages/VinLookupPage.ts index 825b37bd..39257997 100644 --- a/playwright-tests/pages/VinLookupPage.ts +++ b/playwright-tests/pages/VinLookupPage.ts @@ -35,7 +35,7 @@ export class VinLookupPage extends BasePage { } } - async triggerBailout() { + async returnToVehicleLookupPage() { await this.continueButton.click(); await this.lookupVinForMe.click(); } diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index b7c69e7d..c579734f 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -573,11 +573,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { }); await test.step('Click Invalid vin Link', async () => { - await vinLookupPage.triggerBailout(); - }); - - await test.step('BailoutPage >> Validate Bailout-' + BailoutCode.VehicleNotFound, async () => { - await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.VehicleNotFound); + await vinLookupPage.returnToVehicleLookupPage(); }); return; From 6b07ec4c33f63102e572af2f418242167a77594d Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 09:45:43 -0400 Subject: [PATCH 04/47] Fix flaky locators --- playwright-tests/pages/SchedulePage.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 8c8f3f8c..ede5668b 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -31,8 +31,8 @@ export class SchedulePage extends BasePage { super(page); this.page = page; - this.inShopButton = this.page.locator('[buttonlabel="At a Safelite shop"]'); - this.mobileButton = this.page.locator('[buttonlabel="Have Safelite come to me"]'); + this.inShopButton = this.page.getByText('At a Safelite shop'); + this.mobileButton = this.page.getByText('Have Safelite come to me'); // For in shop this.moreLocationsButton = this.page.getByRole('button', { name: 'More Locations' }); // For mobile @@ -77,6 +77,7 @@ export class SchedulePage extends BasePage { await (await this.getFirstNonDropOffTimeSlot()).click(); } } else { + await this.inShopButton.click(); await (await this.getFirstNonDropOffTimeSlot()).click(); } } else { From b9f6372a68434516448d433aa7a0e9bce7054b28 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 09:46:19 -0400 Subject: [PATCH 05/47] Update locator to fix flakiness when selecting address --- playwright-tests/pages/PolicyHolderDetailsPage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright-tests/pages/PolicyHolderDetailsPage.ts b/playwright-tests/pages/PolicyHolderDetailsPage.ts index d7ab5125..dd708f64 100644 --- a/playwright-tests/pages/PolicyHolderDetailsPage.ts +++ b/playwright-tests/pages/PolicyHolderDetailsPage.ts @@ -48,8 +48,8 @@ export class PolicyHolderDetailsPage extends BasePage { await this.addressInputBox.pressSequentially(customerDetails.address.street); // Wait for suggestions to load (Google Places has a slight delay) - await this.page.locator('.pac-item').first().waitFor({ state: 'visible', timeout: 6000 }); - + await this.page.locator('.pac-item').first().waitFor({ state: 'attached', timeout: 6000 }); + await this.page.locator('.pac-item').first().isVisible(); // Click the first result await this.page.locator('.pac-item').first().click(); From 25472fabef4f75c7712bf06afbad1b1793d57c5e Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 09:48:50 -0400 Subject: [PATCH 06/47] Remove jira secret --- azure-pipelines-automated-testing.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azure-pipelines-automated-testing.yml b/azure-pipelines-automated-testing.yml index cf585a17..df7663f6 100644 --- a/azure-pipelines-automated-testing.yml +++ b/azure-pipelines-automated-testing.yml @@ -52,5 +52,4 @@ stages: npmrcPath: 'playwright-tests/.npmrc' secrets: CCIS_API_AUTH: $(CCIS_API_AUTH) - JIRA_API_KEY: $(JIRA_API_KEY) - IS_REGRESSION: $(IS_REGRESSION) \ No newline at end of file + JIRA_API_KEY: $(JIRA_API_KEY) \ No newline at end of file From 6ca20a80a08029558b53c52b2ea248c571fdb1dc Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 11:22:57 -0400 Subject: [PATCH 07/47] Test change to see if pipeline flakiness improves --- playwright-tests/pages/BasePage.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index bf950f07..395d5823 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -129,9 +129,6 @@ export class BasePage { } async waitForURLToChange(startingUrl: string) { - await expect(async () => { - const currentUrl = this.page.url(); - expect(currentUrl).not.toEqual(startingUrl); - }).toPass({ timeout: 70_000 }); + await this.page.waitForURL(url => url.href !== startingUrl, { timeout: 70_000 }); } } \ No newline at end of file From e8cfb14a77dee2dc578128d4a557308e7287be3e Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 12:49:45 -0400 Subject: [PATCH 08/47] Update to test timeout increase --- playwright-tests/pages/BasePage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index 395d5823..f3746e4c 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -129,6 +129,9 @@ export class BasePage { } async waitForURLToChange(startingUrl: string) { - await this.page.waitForURL(url => url.href !== startingUrl, { timeout: 70_000 }); + await expect(async () => { + const currentUrl = this.page.url(); + expect(currentUrl).not.toEqual(startingUrl); + }).toPass({ timeout: 80_000 }); } } \ No newline at end of file From 45082aa2f2d2da58a0d7490071fa7d9fc9fa20d5 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 19:42:39 -0400 Subject: [PATCH 09/47] Reduce timeout --- playwright-tests/pages/BasePage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index f3746e4c..bf950f07 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -132,6 +132,6 @@ export class BasePage { await expect(async () => { const currentUrl = this.page.url(); expect(currentUrl).not.toEqual(startingUrl); - }).toPass({ timeout: 80_000 }); + }).toPass({ timeout: 70_000 }); } } \ No newline at end of file From 46e27bc106755f18797462ed27ce406431a329ad Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 19:43:48 -0400 Subject: [PATCH 10/47] Rename test file to match scenario --- ...0_EssentialPartQuestionsAndNotShareVin.ts} | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) rename playwright-tests/tests/{0020_EssentialVehicleLookupBailout.ts => 0020_EssentialPartQuestionsAndNotShareVin.ts} (65%) diff --git a/playwright-tests/tests/0020_EssentialVehicleLookupBailout.ts b/playwright-tests/tests/0020_EssentialPartQuestionsAndNotShareVin.ts similarity index 65% rename from playwright-tests/tests/0020_EssentialVehicleLookupBailout.ts rename to playwright-tests/tests/0020_EssentialPartQuestionsAndNotShareVin.ts index a70e8da0..1d97684b 100644 --- a/playwright-tests/tests/0020_EssentialVehicleLookupBailout.ts +++ b/playwright-tests/tests/0020_EssentialPartQuestionsAndNotShareVin.ts @@ -1,23 +1,31 @@ import ClientData from "@business-logic/data/ClientData"; import TestCase from "@business-logic/types/TestCase"; -import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums"; +import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType, PartQuestionType } from "@business-logic/types/Enums"; import { ITestData } from "@business-logic/types/ITestData" import { faker } from "@faker-js/faker"; import { getNextWeekday } from "@impl/utils/DateUtils"; const nextWeekday = getNextWeekday(); -const essentialVehicleLookupBailoutData: Partial = { +const essentialPartQuestionsAndNotShareVinData: Partial = { clientTag: 'ALL_ESSENTIAL', isDuplicateClaim: false, isPolicyFound: false, endorsements: [], isReplace: false, - partQuestions: undefined, + partQuestions: [ + { + partQuestionType: PartQuestionType.GeneralQuestion1, + isOnPage: true, + optionToSelect: 'Yes' + }, + { + partQuestionType: PartQuestionType.GeneralQuestion2, + isOnPage: true, + optionToSelect: 'Yes' + } + ], isSafelite: true, - bailoutFlags: { - isVehicleLookupBailout: true - }, servicePackage: faker.helpers.enumValue(ServicePackage), customerDetails: { firstName: faker.person.firstName(), @@ -43,19 +51,10 @@ const essentialVehicleLookupBailoutData: Partial = { year: '2020', make: 'BMW', model: '740', - style: '4 door sedan' // TODO: Check correctness of vehicle style + style: '4 door sedan', + vehicleLookupType: VehicleLookupType.RatherNotShareVin, }, - vehicleDamage: [ - // VehicleDamage.WindshieldThreeChips, - VehicleDamage.WindshieldCrack, - // VehicleDamage.DriverFrontDoor, - // VehicleDamage.DriverQuarterPanel, - // VehicleDamage.DriverRearDoor, - // VehicleDamage.PassengerFrontDoor, - // VehicleDamage.PassengerQuarterPanel, - // VehicleDamage.PassengerRearDoor, - // VehicleDamage.RearWindow - ], + vehicleDamage: [VehicleDamage.WindshieldCrack], appointmentDetails: { serviceLocation: ServiceLocation.InShop, shopAddress: '6826 Sawmill Rd, Columbus, OH 43235', @@ -65,17 +64,17 @@ const essentialVehicleLookupBailoutData: Partial = { } const essentialClients = ClientData.getEssentialClients(); -const essentialVehicleLookupBailoutTests: TestCase[] = []; +const essentialPartQuestionsAndNotShareVinTests: TestCase[] = []; for (const client of essentialClients) { - const data = {...essentialVehicleLookupBailoutData}; + const data = {...essentialPartQuestionsAndNotShareVinData}; data.clientTag = client.clientTag; data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false const tc = new TestCase({ - name: `0020 Essential Vehicle Lookup Bailout Client: "${client.accountName}"`, - tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Bailout', '@VehicleLookup', '@Essentials'], + name: `0020 Essential Part Questions and Not Share Vin Client: "${client.accountName}"`, + tags: [`@${client.clientTag}`, `@${client.accountName}`, '@VehicleLookup', '@Essentials'], testData: data }, undefined, '0020'); - essentialVehicleLookupBailoutTests.push(tc); + essentialPartQuestionsAndNotShareVinTests.push(tc); } -export default essentialVehicleLookupBailoutTests; \ No newline at end of file +export default essentialPartQuestionsAndNotShareVinTests; \ No newline at end of file From 512df54bad38cf58ac04ed9b01521e57523a1ce1 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 19:44:28 -0400 Subject: [PATCH 11/47] Update test name and flow in master file --- playwright-tests/tests/0000__M.test.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index c579734f..8d949baf 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -24,7 +24,7 @@ import advancedScenario0001TestCases from "./advanced/0001a_ReplaceInShopCredit" import advancedScenario0003TestCases from "./advanced/0003a_MobileAfterpay"; import essentialReplaceDynamicAdasTests from "./0005_EssentialReplaceDynamicAdas"; import essentialReplaceStaticAdasTests from "./0001_EssentialReplaceStatisAdas"; -import essentialVehicleLookupBailoutTests from "./0020_EssentialVehicleLookupBailout"; +import essentialPartQuestionsAndNotShareVinTests from "./0020_EssentialPartQuestionsAndNotShareVin"; import essentialServiceableBigTruckTestCases from "./0022_EssentialServiceableBigTruck"; import essentialNonServiceableBigTruckTestCases from "./0023_EssentialNonServiceableBigTruck"; import essentialNonServiceableBigTruckVinTestCases from "./0024_EssentialNonServiceableBigTruckVin"; @@ -85,7 +85,7 @@ test.describe.parallel('ISS QA Automation Regression', () => { addSmokeTagToRandomTest(essentialTpaNotEnabledReplace_0017); addSmokeTagToRandomTest(essentialTpaEnabledReplace_0018); addSmokeTagToRandomTest(essentialTpaEnabledReplaceRecal_0019); - addSmokeTagToRandomTest(essentialVehicleLookupBailoutTests); + addSmokeTagToRandomTest(essentialPartQuestionsAndNotShareVinTests); addSmokeTagToRandomTest(essentialServiceableBigTruckTestCases); addSmokeTagToRandomTest(essentialNonServiceableBigTruckTestCases); addSmokeTagToRandomTest(essentialNonServiceableBigTruckVinTestCases); @@ -131,7 +131,7 @@ test.describe.parallel('ISS QA Automation Regression', () => { for (const testCase of essentialUniqueGlassTests) { test(...prepareTest(testCase, run, options, ruleEngine)); } - //Scenario 13 //defect# SSR-2009 + //Scenario 13 for (const testCase of essentialReplaceTestCases) { test(...prepareTest(testCase, run, options, ruleEngine)); } @@ -160,8 +160,8 @@ test.describe.parallel('ISS QA Automation Regression', () => { test(...prepareTest(testCase, run, options, ruleEngine)); } //Scenario 20 - for (const testCase of essentialVehicleLookupBailoutTests) { - test(...prepareTest(testCase, run, options, ruleEngine)); //skipping this until SSR-2004 is fixed + for (const testCase of essentialPartQuestionsAndNotShareVinTests) { + test(...prepareTest(testCase, run, options, ruleEngine)); } // Scenario 22 for (const testCase of essentialServiceableBigTruckTestCases) { @@ -191,7 +191,6 @@ test.describe.parallel('ISS QA Automation Regression', () => { } // Scenario 0003a - // Note: payment will fail in dev. Payment (PIA) works fine in SYS for (const testCase of advancedScenario0003TestCases) { test(...prepareTest(testCase, run, options, ruleEngine)); } @@ -256,7 +255,6 @@ test.describe.parallel('ISS QA Automation Regression', () => { } // Scenario 0016a - // FIXME: Defect was created INSR-2138 for (const testCase of advancedScenario0016TestCases) { test(...prepareTest(testCase, run, options, ruleEngine)); } @@ -276,7 +274,7 @@ test.describe.parallel('ISS QA Automation Regression', () => { test(...prepareTest(testCase, run, options, ruleEngine)); } - // Scenario 0020a : There's a defect open INSR-2149 + // Scenario 0020a for (const testCase of advancedScenario0020TestCases) { test(...prepareTest(testCase, run, options, ruleEngine)); } @@ -645,14 +643,12 @@ async function runWorkflow(page: Page, testCase: TestCase) { if (capabilityQuestions && capabilityQuestions.length > 0) { await capabilityQuestionsPage.validateURL(capabilityQuestionsPage.issPageValue); - await capabilityQuestionsPage.validatePartQuestions(capabilityQuestions); await capabilityQuestionsPage.selectPartQuestionResponses(capabilityQuestions); await capabilityQuestionsPage.nextPage(); } if (partQuestions && partQuestions.length > 0) { await partQuestionsPage.validateURL(partQuestionsPage.issPageValue); - await partQuestionsPage.validatePartQuestions(partQuestions); await partQuestionsPage.selectPartQuestionResponses(partQuestions); await partQuestionsPage.nextPage(); } From afe20817df3c7b37f1b71ed88c8eb644d743fcc7 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 19:44:57 -0400 Subject: [PATCH 12/47] Add no vin flow on lookup page --- playwright-tests/business-logic/types/Enums.ts | 3 ++- playwright-tests/pages/VehicleLookupPage.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/playwright-tests/business-logic/types/Enums.ts b/playwright-tests/business-logic/types/Enums.ts index 9280ab00..da4f2f1e 100644 --- a/playwright-tests/business-logic/types/Enums.ts +++ b/playwright-tests/business-logic/types/Enums.ts @@ -68,7 +68,8 @@ export enum VehicleDamage { export enum VehicleLookupType { Vin, Address, - LicensePlateNumber + LicensePlateNumber, + RatherNotShareVin } export enum ServiceLocation { diff --git a/playwright-tests/pages/VehicleLookupPage.ts b/playwright-tests/pages/VehicleLookupPage.ts index 8e8433d3..b847918e 100644 --- a/playwright-tests/pages/VehicleLookupPage.ts +++ b/playwright-tests/pages/VehicleLookupPage.ts @@ -12,6 +12,7 @@ export class VehicleLookupPage extends BasePage { readonly addressLookupButton: Locator; readonly licenseLookupButton: Locator; readonly vinLookupPage: VinLookupPage; + readonly ratherNotShareVinButton: Locator; readonly vehicleLookupAddressPage: VehicleLookupAddressPage; readonly vehicleLookupLicensePage: VehicleLookupLicensePage; issPageValue = 'vehicle-lookup'; @@ -22,6 +23,7 @@ export class VehicleLookupPage extends BasePage { this.vinLookupButton = page.getByLabel('Provide my VIN', { exact: true }); this.addressLookupButton = page.getByLabel('Provide my home address', { exact: true }); this.licenseLookupButton = page.getByLabel('Provide my license plate #', { exact: true }); + this.ratherNotShareVinButton = page.getByText(/rather not share my vin/i); this.vinLookupPage = new VinLookupPage(page); // this.validateURL(this.url); @@ -41,6 +43,10 @@ export class VehicleLookupPage extends BasePage { await this.selectVinLookup(); await this.nextPage(); break; + case VehicleLookupType.RatherNotShareVin: + await this.selectRatherNotShareVin(); + await this.nextPage(); + break; default: console.error('VehicleLookupPage >> DATA ISSUE: VehicleLookupType not provided'); break; @@ -58,4 +64,8 @@ export class VehicleLookupPage extends BasePage { async selectLicenseLookup(){ await this.licenseLookupButton.click(); } + + async selectRatherNotShareVin(){ + await this.ratherNotShareVinButton.click(); + } } From f8b6d7f3726f6eca9674c676143590210659d4f8 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 8 Apr 2026 19:45:38 -0400 Subject: [PATCH 13/47] Update parts page --- playwright-tests/pages/PartQuestionsPage.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/playwright-tests/pages/PartQuestionsPage.ts b/playwright-tests/pages/PartQuestionsPage.ts index 1591b132..ed18eabb 100644 --- a/playwright-tests/pages/PartQuestionsPage.ts +++ b/playwright-tests/pages/PartQuestionsPage.ts @@ -11,17 +11,6 @@ export class PartQuestionsPage extends BasePage { this.page = page; } - async validatePartQuestions(partQuestions: IPartQuestion[]) { - for (const pq of partQuestions) { - const partQuestionOptions = this.page.locator(`fieldset[aria-labelledby="${pq.partQuestionType}"]`); - if (pq.isOnPage) { - await expect(partQuestionOptions).toBeAttached(); - } else { - await expect(partQuestionOptions).not.toBeAttached(); - } - } - } - async selectPartQuestionResponses(partQuestions: IPartQuestion[]) { for (const pq of partQuestions) { const parentobject=this.page.locator(`fieldset[aria-labelledby="${pq.partQuestionType}"]`); From cd17eb0bcd20ac09dfb7f77e0bfee23c48cad660 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Thu, 9 Apr 2026 10:12:01 -0400 Subject: [PATCH 14/47] Add retry to test pipeline flakiness --- playwright-tests/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts index 94842ffb..0ae2e894 100644 --- a/playwright-tests/playwright.config.ts +++ b/playwright-tests/playwright.config.ts @@ -68,7 +68,7 @@ export default defineConfig({ /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 1 : 0, + retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 4 : 5, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ From 6181e0d13d49ba69da3f6cb3097b1028c4bdb595 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Thu, 9 Apr 2026 11:18:46 -0400 Subject: [PATCH 15/47] Increase timeout for debugging --- playwright-tests/pages/BasePage.ts | 4 ++-- playwright-tests/playwright.config.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index bf950f07..6a9d61f0 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -30,7 +30,7 @@ export class BasePage { await expect(async () => { const currentUrl = this.page.url(); if (currentUrl === startingUrl) { - await this.continueButton.click({ timeout: 1000 }); + await this.continueButton.click({ timeout: 5_000 }); } //this causes the schedule page to fail //await expect(this.buttonLoadSpin).toHaveCount(0, {timeout: 180000}); @@ -101,7 +101,7 @@ export class BasePage { let referralSequenceNumber = mainSessionStorage.order.referralSequenceNumber as number; if (referralNumber == null) { for (let i = 1; i <= 20; i++) { - if (!referralNumber == null) break; + if (referralNumber !== null) break; await this.page.waitForTimeout(500); mainSessionStorage = JSON.parse(await this.page.evaluate('localStorage.getItem(\'main\')')); referralNumber = mainSessionStorage.order.referralNumber as number; diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts index 0ae2e894..480ca1c7 100644 --- a/playwright-tests/playwright.config.ts +++ b/playwright-tests/playwright.config.ts @@ -91,9 +91,11 @@ export default defineConfig({ baseURL: process.env.BASE_URL || 'https://selfservice.test.glassclaim.com', trace: 'on-first-retry', headless: process.env.CI ? true : false, + video: 'retain-on-failure', + viewport: { width: 1920, height: 1080 }, screenshot: "only-on-failure", - actionTimeout: 60_000, - navigationTimeout: 60_000 + actionTimeout: process.env.CI ? 90_000 : 60_000, + navigationTimeout: process.env.CI ? 90_000 : 60_000 }, /* Configure projects for major browsers */ From 221c8ef2ce12491e81d80ef1a1b9c78011c73ab6 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Thu, 9 Apr 2026 11:21:45 -0400 Subject: [PATCH 16/47] INSR-2124 | Remove conditional env logic There is no longer a mismatch between environments that must be rectified on the front-end --- src/constants/endpoints.js | 33 ++++++++++++-------------------- src/store/index.js | 39 -------------------------------------- 2 files changed, 12 insertions(+), 60 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index d18d073e..0f75c323 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -1,5 +1,3 @@ -import applicationConfig from './application-config'; - const ACCOUNT_BASE_URL = '/account/api/v1/account'; const ANALYTICS_BASE_URL = '/analytics/api/v1/analytics'; const CLIENT_AUTH_BASE_URL = '/clientauth/api/v1/clientauth'; @@ -9,18 +7,11 @@ const EXPERIMENTS_BASE_URL = '/experiments/api/v1/experiments'; const LOCATION_BASE_URL = '/location/api/v1/location'; const ORDER_BASE_URL = '/order/api/v1/order'; const PARTS_BASE_URL = '/parts/api/v2/parts'; -const PARTS_V1_BASE_URL = '/parts/api/v1/parts'; const PRICE_BASE_URL = '/price/api/v1/price'; const SCHEDULE_BASE_URL = '/schedule/api/v1/schedule'; const VEHICLE_BASE_URL = '/vehicle/api/v1/vehicle'; const PAYMENT_BASE_URL = '/payment/api/v1/payment'; -const isLocalOrDev = (() => { - const currentEnv = (applicationConfig?.CURRENT_ENVIRONMENT || '').toLowerCase(); - return currentEnv === 'localhost' || currentEnv === 'dev'; -})(); -const PARTS_EFFECTIVE_BASE_URL = isLocalOrDev ? PARTS_BASE_URL : PARTS_V1_BASE_URL; - const endpoints = Object.freeze({ GetRouteInfo: { url: (applicationAbbreviation) => `${CONTENT_BASE_URL}/${applicationAbbreviation}/RouteInfo`, @@ -51,7 +42,7 @@ const endpoints = Object.freeze({ method: 'POST' }, GetMobilePremiumFee: { - url: `${PARTS_EFFECTIVE_BASE_URL}/mobile-premium-fee`, + url: `${PARTS_BASE_URL}/mobile-premium-fee`, method: 'GET' }, GetVehicleYears: { @@ -71,15 +62,15 @@ const endpoints = Object.freeze({ method: 'GET' }, GetDamageOptions: { - url: `${PARTS_EFFECTIVE_BASE_URL}/damage-options`, + url: `${PARTS_BASE_URL}/damage-options`, method: 'GET' }, GetPartsOrQuestions: { - url: `${PARTS_EFFECTIVE_BASE_URL}/parts-or-questions`, + url: `${PARTS_BASE_URL}/parts-or-questions`, method: 'POST' }, GetParts: { - url: `${PARTS_EFFECTIVE_BASE_URL}/parts`, + url: `${PARTS_BASE_URL}/parts`, method: 'POST' }, GetITACPriceOrderItems: { @@ -108,19 +99,19 @@ const endpoints = Object.freeze({ method: 'GET' }, GetCapabilityQuestions: { - url: `${PARTS_EFFECTIVE_BASE_URL}/capability-questions`, + url: `${PARTS_BASE_URL}/capability-questions`, method: 'GET' }, GetPartFromCapabilityAnswer: { - url: `${PARTS_EFFECTIVE_BASE_URL}/part-from-capability-answer`, + url: `${PARTS_BASE_URL}/part-from-capability-answer`, method: 'POST' }, GetWipers: { - url: `${PARTS_EFFECTIVE_BASE_URL}/wipers`, + url: `${PARTS_BASE_URL}/wipers`, method: 'GET' }, GetRainDefense: { - url: `${PARTS_EFFECTIVE_BASE_URL}/rain-repel`, + url: `${PARTS_BASE_URL}/rain-repel`, method: 'GET' }, GetRecalParts: { @@ -132,19 +123,19 @@ const endpoints = Object.freeze({ zipCode, applicationName, referralSequenceNumber - ) => `${PARTS_EFFECTIVE_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`, + ) => `${PARTS_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`, method: 'GET' }, GetGlassFees: { - url: `${PARTS_EFFECTIVE_BASE_URL}/glass-fees`, + url: `${PARTS_BASE_URL}/glass-fees`, method: 'GET' }, GetSupportingItems: { - url: `${PARTS_EFFECTIVE_BASE_URL}/supporting-items`, + url: `${PARTS_BASE_URL}/supporting-items`, method: 'POST' }, GetMobileFeePart: { - url: `${PARTS_EFFECTIVE_BASE_URL}/mobile-fee`, + url: `${PARTS_BASE_URL}/mobile-fee`, method: 'GET' }, GetServiceabilityDetails: { diff --git a/src/store/index.js b/src/store/index.js index 2aa8248a..930a0ecc 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1348,45 +1348,6 @@ export const useMainStore = defineStore({ }); }, getServiceabilityDetails(serviceZipCode) { - console.log('BEEP BOOP'); - console.log('--ENVIRONMENT DETECTOR ROBOT ENGAGED--'); - const currentEnvironment = applicationConfig.CURRENT_ENVIRONMENT; - console.log('I HAVE DETERMINED THAT THE CURRENT ENVIRONMENT IS:', currentEnvironment); - console.log('---'); - console.log('---'); - if (currentEnvironment === 'Localhost' || currentEnvironment === 'Dev') { - console.log('NEW SERVICEABILITY DETAILS METHOD ACTIVATED'); - return this.getServiceabilityDetailsNewMethod(serviceZipCode); - } else { - console.log('OLD SERVICEABILITY DETAILS METHOD ACTIVATED'); - return this.getServiceabilityDetailsOldMethod(serviceZipCode); - } - }, - getServiceabilityDetailsOldMethod(serviceZipCode) { - const { vehicle, damage, parentAccountNumber, referralSequenceNumber, lineItems } = this.order; - const { carId } = vehicle; - const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace); - const lineItemParts = [...(lineItems.glassParts || []), ...(lineItems.supportingItems || [])].map((part) => ({ - partNumber: part.partNumber, - recalibrationType: part.recalibrationType - })); - - const params = buildURLSearchParams({ - applicationName: applicationConfig.APPLICATION_NAME, - parentAccountNumber, - referralSequenceNumber, - zip: serviceZipCode, - carId, - glassPieces: glassArray, - lineItems: lineItemParts - }); - - return globalMethods.callHttpClient({ - method: endpoints.GetServiceabilityDetails.method, - endpoint: `${endpoints.GetServiceabilityDetails.url}?${params.toString()}` - }); - }, - getServiceabilityDetailsNewMethod(serviceZipCode) { const { damage, lineItems, parentAccountNumber, vehicle } = this.order; const { carId } = vehicle; const flattenedGlassParts = getLineItemsFlattened(lineItems.glassParts); From 8dfde74672171ce55d0f47aba5e95d733161ecd2 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Thu, 9 Apr 2026 12:22:04 -0400 Subject: [PATCH 17/47] Test flakiness --- playwright-tests/pages/BasePage.ts | 2 +- playwright-tests/playwright.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index 6a9d61f0..a1e068af 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -132,6 +132,6 @@ export class BasePage { await expect(async () => { const currentUrl = this.page.url(); expect(currentUrl).not.toEqual(startingUrl); - }).toPass({ timeout: 70_000 }); + }).toPass({ timeout: 90_000 }); } } \ No newline at end of file diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts index 480ca1c7..13912c86 100644 --- a/playwright-tests/playwright.config.ts +++ b/playwright-tests/playwright.config.ts @@ -70,7 +70,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 4 : 5, + workers: process.env.CI ? 2 : 5, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI? [ ['junit'], From 9b849aac04c38046ca932ce0b8520a364750a5cd Mon Sep 17 00:00:00 2001 From: JennyNou Date: Thu, 9 Apr 2026 13:05:42 -0400 Subject: [PATCH 18/47] Try 1 worker instead of 2 --- playwright-tests/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts index 13912c86..5e632267 100644 --- a/playwright-tests/playwright.config.ts +++ b/playwright-tests/playwright.config.ts @@ -70,7 +70,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 2 : 5, + workers: process.env.CI ? 1 : 5, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI? [ ['junit'], From 0a631a6dc46dc3f32613a686fcc8bb7e87fc6fbc Mon Sep 17 00:00:00 2001 From: Jeremy-Z Date: Fri, 10 Apr 2026 11:41:33 -0400 Subject: [PATCH 19/47] Initial updates. --- src/mixins/analytics-mixin.js | 251 ++++++++++++++++++++++++++++++++++ src/router/index.js | 3 + 2 files changed, 254 insertions(+) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index b619b6b4..ec213fbc 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -24,6 +24,7 @@ import { getCartTotal, getSubtotal } from '@/helpers/cart-helper'; import { getRecalPartNumbers } from "@/helpers/recal-helper"; import coverageStatuses from '@/constants/coverage-statuses'; import coverageType from '@/constants/coverage-type'; +import { AppointmentTypeStrings } from '@/constants/schedule-constants'; import issPageValues from '@/router/router-constants/issPage-values'; import { useMainStore } from '@/store'; @@ -140,6 +141,256 @@ export default { this.pushGenericObjectToGA(gaServiceType); } }, + + pushOrderToDataLayer() { + // helper check for if an object is defined (but maybe falsey) + const isDefined = (x) => x !== null && x !== undefined; + const store = useMainStore(); + + // Get correct order object + const hasSubmittedOrder = store.hasSubmittedOrder(); + const submittedOrder = store.getSubmittedOrder(); + const order = hasSubmittedOrder ? submittedOrder : store.order; + + // Begin assembling payload for data layer + const payload = {}; + + // Service Zip + if ( + order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE && + isDefined(order.serviceLocation.zipCode) + ) { + payload.serviceZipCode = order.serviceLocation.zipCode; + } else if ( + isDefined(order.serviceLocation.appointmentType) && + order.serviceLocation.appointmentType !== AppointmentTypeStrings.MOBILE && + isDefined(order.serviceLocation.provider.address.zipCode) + ) { + payload.serviceZipCode = order.serviceLocation.provider.address.zipCode; + } else { + payload.serviceZipCode = ""; + } + + // Damage Type + if (isDefined(order.damage.isRepair)) { + payload.damageType = order.damage.isRepair ? "repair" : "replace"; + } else { + payload.damageType = ""; + } + + // Account Type + if (isDefined(order.payment.isInsurance)) { + payload.accountType = order.payment.isInsurance ? "insurance" : "cash"; + } else { + payload.accountType = ""; + } + + // Promo Codes + const promos = order.lineItems.promos ?? []; + if (promos.length === 0) { + payload.promoCodes = ""; + } else { + const promoCodes = promos.map((promo) => promo.promoCode); + const promoString = promoCodes.reduce((prev, next) => `${prev},${next}`); + payload.promoCodes = promoString; + } + + // Vehicle info + if (isDefined(order.vehicle.year)) { + // Ensure cast to string. + payload.vehicleYear = `${order.vehicle.year}`; + } else { + payload.vehicleYear = ""; + } + + if (isDefined(order.vehicle.make)) { + payload.vehicleMake = order.vehicle.make; + } else { + payload.vehicleMake = ""; + } + + if (isDefined(order.vehicle.model)) { + payload.vehicleModel = order.vehicle.model; + } else { + payload.vehicleModel = ""; + } + + if (isDefined(order.vehicle.style)) { + payload.vehicleStyle = order.vehicle.style; + } else { + payload.vehicleStyle = ""; + } + + // Glass pieces + const glass = order.damage.glassToReplace ?? []; + if (glass.length === 0) { + payload.glassToReplace = ""; + } else { + const glassNames = glass.map((g) => `${g.glassLocation}/${g.glassName}`); + const glassString = glassNames.reduce((prev, next) => `${prev},${next}`); + + payload.glassToReplace = glassString; + } + + //EON + if (order.eon) { + payload.eon = order.eon; + } else { + payload.eon = ""; + } + + // Work Order Id + if (order.workOrderId) { + const parsedId = parseInt(order.workOrderId); + if (!isNaN(parsedId)) { + payload.workOrderId = parsedId; + } else { + payload.workOrderId = ""; + } + } else { + payload.workOrderId = ""; + } + + // Provider Ctu + if (isDefined(order.serviceLocation.zipCodeCtu)) { + payload.providerCtu = order.serviceLocation.zipCodeCtu; + } else { + payload.providerCtu = ""; + } + + // Work Order Number + if (order.workOrderNumber) { + payload.orderNumber = order.workOrderNumber; + } else { + payload.orderNumber = ""; + } + + // Pricing + // Only fire for completed orders? + + const lineItems = order.lineItems ?? {}; + const combinedLineItems = [ + ...(lineItems.glassParts ?? []), + ...(lineItems.supportingItems ?? []), + ...(lineItems.vaps ?? []), + ...(lineItems.promos ?? []), + ]; + + const isPricingAvailable = + combinedLineItems.length > 0 && + combinedLineItems.every( + (lineItem) => + isDefined(lineItem.kitPrice) && + isDefined(lineItem.laborAmount) && + isDefined(lineItem.sellingPrice) + ); + const isTaxAvailable = + isPricingAvailable && + combinedLineItems.every((lineItem) => isDefined(lineItem.salesTax)); + + //unverified (in scenarios we don’t display the price) + if ( + order.payment.isInsurance && + isDefined(order.payment.insuranceCoverage.isVerified) && + !order.payment.insuranceCoverage.isVerified + ) { + payload.priceSubTotal = ""; + } + //deductible (in scenarios we don’t display the price) + // TODO: Need to redo this for ISS 2.0 + else if ( + order.payment.isInsurance && + isDefined(order.payment.insuranceCoverage.isVerified) && + order.payment.insuranceCoverage.isVerified && + isDefined(order.policy.currentDeductible) && + order.policy.currentDeductible >= 0 && + !order.policy.isItac && + !order.policy.isNoComp + ) { + payload.priceSubTotal = ""; + } else if (isPricingAvailable) { + const subtotal = getSubtotal(order).toFixed(2); + + payload.priceSubTotal = parseFloat(subtotal); + } else { + payload.priceSubTotal = ""; + } + + // Cash Quote or Cash Price Sub Total + payload.cashPriceSubTotal = order?.cashPriceSubTotal ?? ""; + + //unverified (in scenarios we don’t display the price) + if ( + order.payment.isInsurance && + isDefined(order.payment.insuranceCoverage.isVerified) && + !order.payment.insuranceCoverage.isVerified + ) { + payload.priceTotal = ""; + } + //deductible (in scenarios we don’t display the price) + else if ( + order.payment.isInsurance && + isDefined(order.payment.insuranceCoverage.isVerified) && + order.payment.insuranceCoverage.isVerified && + isDefined(order.policy.currentDeductible) && + order.policy.currentDeductible >= 0 && + !order.policy.isItac && + !order.policy.isNoComp + ) { + payload.priceTotal = ""; + } else if (isTaxAvailable) { + const total = getCartTotal(order).toFixed(2); + + payload.priceTotal = parseFloat(total); + } else { + payload.priceTotal = ""; + } + + // Recalibration + // TODO: Find equivalent ISS methods. + if (hasSubmittedOrder) { + payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnSubmittedState; + } else { + payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder; + } + + // Appointment Type + if (isDefined(order.serviceLocation.appointmentType)) { + payload.appointmentType = order.serviceLocation.appointmentType; + } else { + payload.appointmentType = ""; + } + //Insurance + // TODO: Find equivalent ISS methods. + if (order.payment.isInsurance) { + payload.isInsuranceVerified = order.payment.insuranceCoverage.isVerified ?? ""; + payload.insuranceCompanyName = order.policy.insuranceCompanyName ?? ""; + if (!order.payment.insuranceCoverage.isVerified) { + payload.isInsuranceItac = ""; + payload.isInsuranceNoComp = ""; + } else { + payload.isInsuranceItac = order.policy.isItac ?? ""; + payload.isInsuranceNoComp = order.policy.isNoComp ?? ""; + } + if ( + order.policy.isItac || + order.policy.isNoComp || + !order.payment.insuranceCoverage.isVerified + ) { + payload.insuranceDeductible = ""; + } else { + payload.insuranceDeductible = order.policy.currentDeductible ?? ""; + } + } else { + payload.isInsuranceVerified = ""; + payload.insuranceDeductible = ""; + payload.isInsuranceItac = ""; + payload.isInsuranceNoComp = ""; + payload.insuranceCompanyName = ""; + } + + pushToDataLayerIfDefined(payload); + }, pushExperimentsToDataLayer() { const { experiments } = useMainStore().applicationUser; diff --git a/src/router/index.js b/src/router/index.js index eeca615e..4fd5306e 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -192,6 +192,9 @@ router.afterEach(async (to, from) => { // Push values to GA analyticsMixin.methods.pushValueToGA(); + + // Push current order status to Data Layer + analyticsMixin.methods.pushOrderToDataLayer(); } }); From 4ddd961d18cd5454dad2a4f23ee56d14a77336e9 Mon Sep 17 00:00:00 2001 From: Jeremy-Z Date: Fri, 10 Apr 2026 12:12:09 -0400 Subject: [PATCH 20/47] Updates on some of the logic. --- src/mixins/analytics-mixin.js | 87 +++++++++++++---------------------- 1 file changed, 31 insertions(+), 56 deletions(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index ec213fbc..25e06531 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -178,12 +178,8 @@ export default { payload.damageType = ""; } - // Account Type - if (isDefined(order.payment.isInsurance)) { - payload.accountType = order.payment.isInsurance ? "insurance" : "cash"; - } else { - payload.accountType = ""; - } + // Account Type - always insurance for ISS + payload.accountType = "insurance"; // Promo Codes const promos = order.lineItems.promos ?? []; @@ -289,23 +285,16 @@ export default { combinedLineItems.every((lineItem) => isDefined(lineItem.salesTax)); //unverified (in scenarios we don’t display the price) - if ( - order.payment.isInsurance && - isDefined(order.payment.insuranceCoverage.isVerified) && - !order.payment.insuranceCoverage.isVerified - ) { + if (!store.isVerified) { payload.priceSubTotal = ""; } //deductible (in scenarios we don’t display the price) // TODO: Need to redo this for ISS 2.0 else if ( - order.payment.isInsurance && - isDefined(order.payment.insuranceCoverage.isVerified) && - order.payment.insuranceCoverage.isVerified && - isDefined(order.policy.currentDeductible) && - order.policy.currentDeductible >= 0 && - !order.policy.isItac && - !order.policy.isNoComp + store.isVerified && + store.currentDeductible >= 0 && + !store.isItac && + !store.isNoComp ) { payload.priceSubTotal = ""; } else if (isPricingAvailable) { @@ -317,25 +306,18 @@ export default { } // Cash Quote or Cash Price Sub Total - payload.cashPriceSubTotal = order?.cashPriceSubTotal ?? ""; + payload.cashPriceSubTotal = getSubtotal(order).toString(); //unverified (in scenarios we don’t display the price) - if ( - order.payment.isInsurance && - isDefined(order.payment.insuranceCoverage.isVerified) && - !order.payment.insuranceCoverage.isVerified - ) { + if (!store.isVerified) { payload.priceTotal = ""; } //deductible (in scenarios we don’t display the price) else if ( - order.payment.isInsurance && - isDefined(order.payment.insuranceCoverage.isVerified) && - order.payment.insuranceCoverage.isVerified && - isDefined(order.policy.currentDeductible) && - order.policy.currentDeductible >= 0 && - !order.policy.isItac && - !order.policy.isNoComp + store.isVerified && + store.currentDeductible >= 0 && + !store.isItac && + !store.isNoComp ) { payload.priceTotal = ""; } else if (isTaxAvailable) { @@ -351,7 +333,7 @@ export default { if (hasSubmittedOrder) { payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnSubmittedState; } else { - payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder; + payload.isRecalibrationOnOrder = store.hasRecalibrationPart && containsRecalParts(order.lineItems); } // Appointment Type @@ -360,33 +342,26 @@ export default { } else { payload.appointmentType = ""; } + //Insurance - // TODO: Find equivalent ISS methods. - if (order.payment.isInsurance) { - payload.isInsuranceVerified = order.payment.insuranceCoverage.isVerified ?? ""; - payload.insuranceCompanyName = order.policy.insuranceCompanyName ?? ""; - if (!order.payment.insuranceCoverage.isVerified) { - payload.isInsuranceItac = ""; - payload.isInsuranceNoComp = ""; - } else { - payload.isInsuranceItac = order.policy.isItac ?? ""; - payload.isInsuranceNoComp = order.policy.isNoComp ?? ""; - } - if ( - order.policy.isItac || - order.policy.isNoComp || - !order.payment.insuranceCoverage.isVerified - ) { - payload.insuranceDeductible = ""; - } else { - payload.insuranceDeductible = order.policy.currentDeductible ?? ""; - } - } else { - payload.isInsuranceVerified = ""; - payload.insuranceDeductible = ""; + // TODO: Find equivalent ISS methods. Using store atm need to run off order object. + payload.isInsuranceVerified = store.isVerified; + payload.insuranceCompanyName = store.issConfig.clientName ?? ""; + if (!store.isVerified) { payload.isInsuranceItac = ""; payload.isInsuranceNoComp = ""; - payload.insuranceCompanyName = ""; + } else { + payload.isInsuranceItac = store.isItac ?? ""; + payload.isInsuranceNoComp = store.isNoComp ?? ""; + } + if ( + store.isItac || + store.isNoComp || + !store.isVerified + ) { + payload.insuranceDeductible = ""; + } else { + payload.insuranceDeductible = store.currentDeductible ?? ""; } pushToDataLayerIfDefined(payload); From ec3c83adfe681ca2350e923fe5fdee1bc31bac9d Mon Sep 17 00:00:00 2001 From: JennyNou Date: Fri, 10 Apr 2026 12:29:01 -0400 Subject: [PATCH 21/47] Add flag for when Safelite can recalibrate --- .../business-logic/types/ITestData.ts | 1 + playwright-tests/pages/SchedulePage.ts | 17 +++++++++++++++++ playwright-tests/tests/0000__M.test.ts | 16 +++++++++------- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/playwright-tests/business-logic/types/ITestData.ts b/playwright-tests/business-logic/types/ITestData.ts index d3a19d34..79598973 100644 --- a/playwright-tests/business-logic/types/ITestData.ts +++ b/playwright-tests/business-logic/types/ITestData.ts @@ -39,6 +39,7 @@ export interface ITestData { isRecalNotification: boolean, isRecalWarning: boolean, isRecalVehicle: boolean, + isCanSafeliteRecalibrate: boolean, // Can Safelite recalibrate the vehicle? If no, modal displays after clicking time slot and continue isSeparateApptsWarning: boolean, // IF true, check for the separate appts warning on VehicleDamagePage isAuthenticationRequired: boolean, isMoldingQuestion: boolean, diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index ede5668b..921cddb9 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -25,6 +25,11 @@ export class SchedulePage extends BasePage { readonly dateText: Locator; readonly viewMoreDatesLink: Locator; + readonly recalAcknowledgementModal: Locator; + readonly learnMoreLinkRecalAcknowledgementModal: Locator; + readonly recalAcknowledgementCheckBox: Locator; + readonly continueButtonRecalAcknowledgementModal: Locator; + readonly continueButton: Locator; constructor(page: Page) { @@ -50,6 +55,12 @@ export class SchedulePage extends BasePage { this.dateText = this.page.locator('[class="modal-header mb-2 mt-2"]'); this.viewMoreDatesLink = this.page.getByRole('button', { name: 'More Right arrow icon' }); + // Recal acknowledgement modal when Safelite cannot recalibrate the vehicle + this.recalAcknowledgementModal = this.page.getByLabel('ModalComponentLabel'); + this.learnMoreLinkRecalAcknowledgementModal = this.page.getByRole('link', { name: 'Learn more' }); + this.recalAcknowledgementCheckBox = this.page.getByRole('checkbox', { name: /I acknowledge/i }); + this.continueButtonRecalAcknowledgementModal = this.page.locator('#RecalAckModalWidget').getByRole('button', { name: 'Continue' }); + this.continueButton = this.page.locator('#stacked').locator('button:has-text("Continue")'); } @@ -111,4 +122,10 @@ export class SchedulePage extends BasePage { const nonDropOff = timeSlots.filter({ hasNotText: /Drop & Go/i }); return nonDropOff.first(); } + + async validateRecalAcknowledgementModal() { + await this.learnMoreLinkRecalAcknowledgementModal.click(); + await this.recalAcknowledgementCheckBox.click(); + await this.continueButtonRecalAcknowledgementModal.click(); + } } \ No newline at end of file diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 8d949baf..1d3dca5a 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -379,7 +379,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { isRecalWarning, servicePackage, hasOemEndorsement, hasStateLawPopup, otherVehiclesOnPolicy, isUseVehicleFromAddressLookup, isSeparateApptsWarning, vehiclePartQuestions, editVehicleDetails, isAddressLookupValidations, hasMilitaryWarning, capabilityQuestions, isUseVehicleOnPolicy, - isVehicleLookupValidations, isMoldingQuestion, isNonServiceable, isNonServiceableVin, isRecalVehicle } = testCase.testData; + isVehicleLookupValidations, isMoldingQuestion, isNonServiceable, isNonServiceableVin, isRecalVehicle, isCanSafeliteRecalibrate } = testCase.testData; let { isPolicyFound } = testCase.testData; // Allow isPolicyFound to be re-assigned @@ -732,7 +732,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { await expect(async () => { await providerPreferencePage.stateLawModalOkayButton.waitFor({ state: 'visible' }); await providerPreferencePage.stateLawModalOkayButton.click(); - }).toPass({ timeout: 30000 }); + }).toPass({ timeout: 60_000 }); }); } @@ -829,13 +829,15 @@ async function runWorkflow(page: Page, testCase: TestCase) { await test.step('SchedulePage >> Select day and time', async () => { await schedulePage.validateURL(schedulePage.issPageValue); - if (appointmentDetails?.serviceLocation === ServiceLocation.Mobile) { - await schedulePage.scheduleMobile(appointmentDetails); + if (isCanSafeliteRecalibrate === false) { + await schedulePage.validateRecalAcknowledgementModal(); + await schedulePage.scheduleInShop(appointmentDetails!); + } else if (appointmentDetails?.serviceLocation === ServiceLocation.InShop || appointmentDetails?.serviceLocation === ServiceLocation.DropOff) { + await schedulePage.scheduleInShop(appointmentDetails!); + } else { + await schedulePage.scheduleMobile(appointmentDetails!); } - if (appointmentDetails?.serviceLocation === ServiceLocation.InShop || appointmentDetails?.serviceLocation === ServiceLocation.DropOff) { - await schedulePage.scheduleInShop(appointmentDetails); - } }); From 9b5ff7488aa1455d3d6c58075d7db22b457887d1 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Fri, 10 Apr 2026 12:29:18 -0400 Subject: [PATCH 22/47] Remove extra click --- playwright-tests/pages/ServicePackagesPage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 83c0b133..98070447 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -32,7 +32,7 @@ export class ServicePackagesPage extends BasePage { try {await this.wiperModal.waitFor({ state: 'visible', timeout: 5000 }); await this.wiperModalCloseButton.click(); } catch { - await this.continueButton.click(); + console.log('Wiper modal not displayed'); } } else if (servicePackage === ServicePackage.GlassOnly) { await this.glassOnlyPackageButton.check(); From 2e7f660f61f0c6e94c6f45f35cc74bc67d20c41c Mon Sep 17 00:00:00 2001 From: JennyNou Date: Fri, 10 Apr 2026 12:29:38 -0400 Subject: [PATCH 23/47] Increase timeout to fix flaky test --- playwright-tests/pages/VehicleSelectionPage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/pages/VehicleSelectionPage.ts b/playwright-tests/pages/VehicleSelectionPage.ts index 9c61f3de..1dc752e2 100644 --- a/playwright-tests/pages/VehicleSelectionPage.ts +++ b/playwright-tests/pages/VehicleSelectionPage.ts @@ -27,7 +27,7 @@ export class VehicleSelectionPage extends BasePage { await this.yearDropdown.selectOption(vehicleDetails.year); await this.yearDropdown.press('Tab'); await this.makeDropdown.selectOption(vehicleDetails.make); - await expect(this.modelDropdown).toBeEditable({ timeout: 5000 }); + await expect(this.modelDropdown).toBeEditable({ timeout: 6000 }); await this.makeDropdown.press('Tab'); await this.modelDropdown.selectOption(vehicleDetails.model); await expect(this.styleDropdown).toBeEditable({ timeout: 2000 }); From f2a3b0910200a0ae658de474ce12b949147d6780 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Fri, 10 Apr 2026 12:31:05 -0400 Subject: [PATCH 24/47] Add can Safelite recalibrate to big truck test scenarios --- playwright-tests/tests/0022_EssentialServiceableBigTruck.ts | 4 +--- .../tests/advanced/0031a_ReplaceServiceableBigTruck.ts | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/playwright-tests/tests/0022_EssentialServiceableBigTruck.ts b/playwright-tests/tests/0022_EssentialServiceableBigTruck.ts index f3014dee..3d4da669 100644 --- a/playwright-tests/tests/0022_EssentialServiceableBigTruck.ts +++ b/playwright-tests/tests/0022_EssentialServiceableBigTruck.ts @@ -13,9 +13,6 @@ const essentialServiceableBigTruckData: Partial = { isPolicyFound: false, endorsements: [], isReplace: true, - bailoutFlags: { - isHeavyTruckVehicleBailout: true, - }, vehiclePartQuestions: [ { partQuestionType: PartQuestionType.WindshieldColor, @@ -24,6 +21,7 @@ const essentialServiceableBigTruckData: Partial = { }, ], isSafelite: true, + isCanSafeliteRecalibrate: false, servicePackage: faker.helpers.enumValue(ServicePackage), customerDetails: { firstName: faker.person.firstName(), diff --git a/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts b/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts index b4fb227e..aa28bae3 100644 --- a/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts +++ b/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts @@ -31,6 +31,7 @@ const advancedScenario0031Data: Partial = { isDuplicateClaim: false, isPolicyFound: true, isNoComp: false, + isCanSafeliteRecalibrate: false, hasStateLawPopup: false, endorsements: undefined, vehiclePartQuestions: [ From 7125e1fa97ecc12486d03b4f256a10026ef06e07 Mon Sep 17 00:00:00 2001 From: Jeremy-Z Date: Fri, 10 Apr 2026 14:47:14 -0400 Subject: [PATCH 25/47] Added more fields. --- src/mixins/analytics-mixin.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 25e06531..08c1a6e0 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -129,11 +129,6 @@ export default { }, pushValueToGA() { - const gaSiteType = { - ['siteType']: useMainStore().issConfig.siteType - }; - this.pushGenericObjectToGA(gaSiteType); - const gaServiceType = { ['service_type']: useMainStore().order?.serviceLocation?.appointmentType?.toLowerCase() }; @@ -155,6 +150,10 @@ export default { // Begin assembling payload for data layer const payload = {}; + payload.appName = "ISS"; + payload.siteType = store.issConfig.siteType; + payload.pageName = this.getPageNameByQueryString(); + // Service Zip if ( order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE && From 2754f7be9c875b43cc135c4d61be5b5be540133a Mon Sep 17 00:00:00 2001 From: Jeremy-Z Date: Fri, 10 Apr 2026 15:20:14 -0400 Subject: [PATCH 26/47] Some additional fields. --- src/mixins/analytics-mixin.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 08c1a6e0..b1d7d8cf 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -146,6 +146,8 @@ export default { const hasSubmittedOrder = store.hasSubmittedOrder(); const submittedOrder = store.getSubmittedOrder(); const order = hasSubmittedOrder ? submittedOrder : store.order; + const deviceId = getDeviceIdValue(); + const sid = getSessionIdValue(); // Begin assembling payload for data layer const payload = {}; @@ -153,7 +155,11 @@ export default { payload.appName = "ISS"; payload.siteType = store.issConfig.siteType; payload.pageName = this.getPageNameByQueryString(); - + payload.deviceId = deviceId; + payload.sessionId = sid; + payload.clientName = store.issConfig.clientName; + payload.lossCause = order.policy?.damageCause ?? ""; + // Service Zip if ( order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE && From d27b07d517ca9b9bb7b30c285800abbb1db4e08d Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 09:21:40 -0400 Subject: [PATCH 27/47] INSR-8882: Events added to payment page and order confirmation page --- src/constants/analytics.js | 19 +++- .../order-confirmation/order-confirmation.vue | 96 +++++++++++++++++-- .../payment-page-adyen/payment-page-adyen.vue | 13 +++ .../payment-return-adyen.vue | 16 ++++ 4 files changed, 134 insertions(+), 10 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 8cf147e8..7649a96b 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -1,3 +1,6 @@ +import packageNames from "./package-names"; +import { paymentMethods } from "./payment-method-constants"; + const analyticsPageEvents = Object.freeze({ ENTRY: 'ENTRY', EVENT: 'EVENT' @@ -40,4 +43,18 @@ const ValueToLogTypes = Object.freeze({ LAST_5: 'last_5' }); -export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes }; +const analyticsPaymentTypeMap = new Map([ + [paymentMethods.AFTERPAY, 'after_pay'], + [paymentMethods.CREDIT_CARD, 'credit_card'], + [paymentMethods.PAY_AT_TIME_OF_SERVICE, 'pay_at_service'], + [paymentMethods.PAYPAL, 'pay_pal'], + [paymentMethods.APPLEPAY, 'apple_pay'], +]) + +const analyticsServicePackageMap = new Map([ + [packageNames.TIER_ONE, 'basic'], + [packageNames.TIER_TWO, 'essentials'], + [packageNames.TIER_THREE, 'essentialsplus'] +]) + +export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap }; diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue index 21ce4fc7..dd8a3198 100644 --- a/src/layouts/order-confirmation/order-confirmation.vue +++ b/src/layouts/order-confirmation/order-confirmation.vue @@ -55,7 +55,7 @@ :isRepair="isRepair" />
{{ wipersTitle }} {{ description }}
{{ rainRepelTitle }} {{ rainRepelBody }} @@ -135,6 +135,10 @@ import widgetFields from '@/constants/cms-widget-fields.js'; import { getGlassList } from '@/helpers/damage-helper'; import partTypeStrings from '@/constants/part-type-strings'; import coverageType from '@/constants/coverage-type'; +import { analyticsPaymentTypeMap, analyticsServicePackageMap } from '@/constants/analytics'; +import { containsRecalParts } from '@/helpers/recal-helper'; +import issPageValues from '@/router/router-constants/issPage-values'; +import { getCartTotal } from '@/helpers/cart-helper'; export default { name: 'order-confirmation', @@ -199,6 +203,7 @@ export default { hasRecalibrationPart, referralNumber: referralNumber?.toString(), isNoComp: this.submittedOrder.insuranceCoverage.coverageType === coverageType.NO_COMP, + isITAC: this.submittedOrder.insuranceCoverage.coverageType === coverageType.ITAC, widgets: { siteHeader: 'SiteHeaderWidget', emailConfirmation: 'EmailConfirmationWordingWidget', @@ -418,10 +423,16 @@ export default { // Temporarily set return true to see cart in localhost or dev environment return false; }, - displayWipers() { + hasWipers() { const hasWiperPart = this.submittedOrder.lineItems.vaps.some((part) => part.partType.toLowerCase().includes('wiper')); return hasWiperPart; }, + hasFrontWiper() { + return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER); + }, + hasRearWiper() { + return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER); + }, wipersTitle() { return this.getCmsContent( this.widgets.wipersText, @@ -431,13 +442,10 @@ export default { wipersBody() { const wiperTypesOnOrder = []; - const hasFrontWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER); - const hasRearWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER); - - if (hasFrontWiper) { + if (this.hasFrontWiper) { wiperTypesOnOrder.push(partTypeStrings.FRONT_WIPER); } - if (hasRearWiper) { + if (this.hasRearWiper) { wiperTypesOnOrder.push(partTypeStrings.REAR_WIPER); } @@ -447,7 +455,7 @@ export default { return wiperDescriptions; }, - displayRainRepel() { + hasRainRepel() { return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.RAIN_DEFENSE); }, rainRepelTitle() { @@ -508,6 +516,10 @@ export default { if (this.carrierUrl) { this.$refs.siteFooter.updateButtonText(`Go back to ${this.carrierName}`); } + + const isFirstLoad = this.mainStore.pageData(issPageValues.ORDER_CONFIRMATION)?.isFirstLoad ?? true; + this.savePageDataToStore(issPageValues.ORDER_CONFIRMATION, { isFirstLoad: false }); + this.handleAnalyticsEvents(isFirstLoad); }, methods: { arePagePrerequisitesValid() { @@ -611,6 +623,72 @@ export default { const vapsTypeDescription = vapsItemDescriptions?.find((entry) => entry?.Name === vapsPartType); return vapsTypeDescription?.Text ?? ''; + }, + handleAnalyticsEvents(isFirstLoad) { + const mobileOrInshop = this.submittedOrder.isMobileAppointment ? 'mobile' : 'in_shop'; + const verifiedOrNotVerified = this.submittedOrder.isVerified ? 'verified' : 'not_verified'; + const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; + this.pushEventToGA('confirmation', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true); + + if (this.payment.isPayInAdvance) { + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.payment.paymentMethod); + this.pushEventToGA('payment_page', 'pia_successful', paymentMethodForAnalytics, true); + } + + if (this.submittedOrder.servicePackage) { + const servicePackageForAnalytics = analyticsServicePackageMap.get(this.submittedOrder.servicePackage); + this.pushEventToGA('service_package', 'package_purchased', servicePackageForAnalytics, true); + } + + if (containsRecalParts(this.submittedOrder.lineItems)) { + let coverageType = ''; + if (this.isITAC) { + coverageType = 'ITAC'; + } else if (this.isNoComp) { + coverageType = 'no_comp'; + } else if (this.submittedOrder.isVerified) { + coverageType = 'verified'; + } else { + coverageType = 'unverified'; + } + const recalibrationType = this.submittedOrder.lineItems.glassParts.find((part) => part.requiresRecalibration).recalibrationType; + this.pushEventToGA(`recalibration_scheduled_${coverageType}`, this.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true); + } + + const ymms = `${this.vehicle.year}_${this.vehicle.make}_${this.vehicle.model}_${this.vehicle.style}`; + if (isFirstLoad && getCartTotal(this.submittedOrder) > 0) { + this.pushEventToGA('total_price', getCartTotal(this.submittedOrder).toString(), ymms, true); + } + + this.pushEventToGA('rain_defense', this.hasRainRepel ? 'purchased' : 'no_purchase', ymms, true); + + // TODO: Check if we're coming from SFA and add relevant events + // eslint-disable-next-line + if (false) { + this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'SFA', null); + } + else { + this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'ClientSite', true); + } + + this.pushEventToGA('visitor_info_confirmation', 'client_name', this.mainStore.accountNameForEvents, true); + + const wiperAction = this.hasWipers ? 'purchased' : 'no_purchase'; + + let wiperLabel = this.wipersBody.join('_').toLowerCase().replace('
', '').replace(/beam/g, '').replace(/blades/g, '').trim().replace(/ /g, '_'); + if (wiperLabel.indexOf('front') < 0 && wiperLabel.indexOf('rear') < 0) { + wiperLabel = 'none_none'; + } else { + if (wiperLabel.indexOf('front') < 0) { + wiperLabel = 'front_none_' + wiperLabel; + } + if (wiperLabel.indexOf('rear') < 0) { + wiperLabel = wiperLabel + '_rear_none'; + } + } + wiperLabel = wiperLabel.replace(/__/g, '_'); + + this.pushEventToGA('wipers', wiperAction, wiperLabel, true); } } }; diff --git a/src/layouts/payment-page-adyen/payment-page-adyen.vue b/src/layouts/payment-page-adyen/payment-page-adyen.vue index 91462f51..cdf6529f 100644 --- a/src/layouts/payment-page-adyen/payment-page-adyen.vue +++ b/src/layouts/payment-page-adyen/payment-page-adyen.vue @@ -75,6 +75,7 @@ import { mapAdyenToIssPaymentMethod, mapIssToAdyenPaymentMethod } from '@/helper import { createAdyenCheckout } from "@/helpers/adyen-helper"; import { Dropin } from "@adyen/adyen-web/auto"; import applicationConfig from '@/constants/application-config'; +import { analyticsPaymentTypeMap } from '@/constants/analytics'; export default { name: 'payment-page-adyen', @@ -118,6 +119,7 @@ export default { }, mounted() { showIssLoadingModal(true); + this.pushEventToGA('payment_page', 'Mode', 'Adyen', true); this.initializeAdyen().finally(() => { showIssLoadingModal(false); }); @@ -353,6 +355,8 @@ export default { }, async paymentFailedPayLater() { this.showIssLoadingModal(true); + const paymentMethod = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', 'save_your_appointment_clicked', `${paymentMethod}_declined`, true); this.mainStore.savePaymentMethodChoice(paymentMethods.PAY_AT_TIME_OF_SERVICE); await submitWorkOrder({ submitType: submitType.SAFELITE }); this.$router.navigate( @@ -523,6 +527,8 @@ export default { const paymentMethodFromSession = adyenResponse?.paymentMethod; const paymentMethod = mapAdyenToIssPaymentMethod(paymentMethodFromSession); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod); + this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); this.mainStore.savePaymentMethodChoice(paymentMethod); @@ -544,6 +550,10 @@ export default { await global.$logger.logError(stringToLog); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); + this.hasPaymentFailureError = true; } @@ -556,6 +566,9 @@ export default { const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`; await global.$logger.logError(stringToLog); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); this.hasPaymentFailureError = true; } diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue index d306857d..33c35194 100644 --- a/src/layouts/payment-return-adyen/payment-return-adyen.vue +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -18,6 +18,7 @@ import navigationScenarios from '@/router/router-constants/navigation-scenarios' import submitType from '@/constants/submit-type'; import showIssLoadingModal from '@/helpers/loading-modal-helper'; import { paymentMethods } from '@/constants/payment-method-constants'; +import { analyticsPaymentTypeMap } from '@/constants/analytics'; export default { name: 'payment-return-adyen', @@ -58,6 +59,8 @@ export default { const sessionInfo = await getSessionInfo(sessionId, result.sessionResult); const paymentMethod = mapAdyenToIssPaymentMethod(sessionInfo?.paymentMethod); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod); + this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); const amountDue = getCartTotal(store.order); const ccToken = generateCcToken(sessionInfo); if (paymentMethod === paymentMethods.AFTERPAY) { @@ -95,6 +98,11 @@ export default { } ); }, + computed: { + piaType() { + return this.mainStore.payment.paymentMethod; + } + }, methods: { finalizeAdyenPayment(sessionId, redirectResult) { return new Promise((resolve, reject) => { @@ -111,6 +119,10 @@ export default { const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${sessionId}`; global.$logger.logError(stringToLog); + + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); } reject({ @@ -123,6 +135,10 @@ export default { const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`; global.$logger.logError(stringToLog); + + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); } reject({ From 50404eb4e6239381046f3367f27d14243a3700e1 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 09:43:06 -0400 Subject: [PATCH 28/47] INSR-8882: Fix failing tests --- .../order-confirmation/order-confirmation.spec.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index be64fdc8..af243e29 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -27,7 +27,8 @@ jest.mock('@/helpers/cms-content-helper', () => ({ const mockMixin = { methods: { getCmsContent: jest.fn(), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn() } }; @@ -778,7 +779,8 @@ describe('OrderConfirmation.vue', () => { } return ''; }), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn() } }; }); @@ -827,7 +829,8 @@ describe('OrderConfirmation.vue', () => { } return ''; }), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn() } }; }); From 5eb4ef0418bae67e24bb1d8f3c3b80917d97d9a1 Mon Sep 17 00:00:00 2001 From: Jeremy-Z Date: Mon, 13 Apr 2026 10:25:48 -0400 Subject: [PATCH 29/47] Moved some common methods around. --- src/helpers/object-helper.js | 14 ++++++++ src/helpers/recal-helper.js | 27 +++++++++++++- src/mixins/analytics-mixin.js | 68 ++++++----------------------------- src/store/index.js | 40 +++------------------ 4 files changed, 56 insertions(+), 93 deletions(-) diff --git a/src/helpers/object-helper.js b/src/helpers/object-helper.js index 37ee46be..e142c236 100644 --- a/src/helpers/object-helper.js +++ b/src/helpers/object-helper.js @@ -86,3 +86,17 @@ export function getPropertyCaseInsensitive(obj, property) { while (prop = props.pop()) if (prop.toLowerCase() === property.toLowerCase()) return prop; return null; } + +export function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) { + return (array ?? []).map((x) => x[propertyName]).filter((x) => x); +} + +export function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) { + if (!arrayOfObjects) return null; + + return arrayOfObjects.sort((a, b) => { + if (a[propertyName] < b[propertyName]) return -1; + if (a[propertyName] > b[propertyName]) return 1; + return 0; + }); +} diff --git a/src/helpers/recal-helper.js b/src/helpers/recal-helper.js index f364bf68..029aee25 100644 --- a/src/helpers/recal-helper.js +++ b/src/helpers/recal-helper.js @@ -1,5 +1,5 @@ import partTypeStrings from '@/constants/part-type-strings'; -import { deepClone } from '@/helpers/object-helper'; +import { deepClone, getNonFalseValuesOfPropertyInArrayOfObjects } from '@/helpers/object-helper'; const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION]; @@ -73,6 +73,31 @@ export function containsRecalParts(lineItems) { } } +export function isRecalOrder(lineItems) { + return (containsRecalParts(lineItems) && getHasRecalibrationPart(lineItems)); +} + +export function getHasRecalibrationPart(lineItems) { + const hasRequiresRecalibration = getNonFalseValuesOfPropertyInArrayOfObjects(lineItems.glassParts, 'requiresRecalibration')?.length > 0; + const hasRecalibrationType = getNonFalseValuesOfPropertyInArrayOfObjects(lineItems.glassParts, 'recalibrationType')?.length > 0; + + if (hasRequiresRecalibration) { + if (hasRecalibrationType) { + // Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType' + return ( + getNonFalseValuesOfPropertyInArrayOfObjects( + lineItems.glassParts, + 'recalibrationType' + )[0].toLowerCase() !== 'unknown' + ); + } + // Has 'requiresRecalibration' but no 'recalibrationType' at all + return true; + } + // Does not have 'requiresRecalibration' + return false; +} + export function anyPartWithRequiresRecalFlag(lineItems) { if (!lineItems) { return false; diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index b1d7d8cf..4827a559 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -21,7 +21,7 @@ import { ValueToLogTypes } from '@/constants/analytics'; import { getCartTotal, getSubtotal } from '@/helpers/cart-helper'; -import { getRecalPartNumbers } from "@/helpers/recal-helper"; +import { getRecalPartNumbers, isRecalOrder } from "@/helpers/recal-helper"; import coverageStatuses from '@/constants/coverage-statuses'; import coverageType from '@/constants/coverage-type'; import { AppointmentTypeStrings } from '@/constants/schedule-constants'; @@ -266,35 +266,12 @@ export default { payload.orderNumber = ""; } - // Pricing - // Only fire for completed orders? - - const lineItems = order.lineItems ?? {}; - const combinedLineItems = [ - ...(lineItems.glassParts ?? []), - ...(lineItems.supportingItems ?? []), - ...(lineItems.vaps ?? []), - ...(lineItems.promos ?? []), - ]; - - const isPricingAvailable = - combinedLineItems.length > 0 && - combinedLineItems.every( - (lineItem) => - isDefined(lineItem.kitPrice) && - isDefined(lineItem.laborAmount) && - isDefined(lineItem.sellingPrice) - ); - const isTaxAvailable = - isPricingAvailable && - combinedLineItems.every((lineItem) => isDefined(lineItem.salesTax)); - - //unverified (in scenarios we don’t display the price) + // Unverified (no price or deductible displayed) if (!store.isVerified) { payload.priceSubTotal = ""; + payload.priceTotal = ""; } - //deductible (in scenarios we don’t display the price) - // TODO: Need to redo this for ISS 2.0 + // Deductible case (no price is displayed, only deductible) else if ( store.isVerified && store.currentDeductible >= 0 && @@ -302,44 +279,23 @@ export default { !store.isNoComp ) { payload.priceSubTotal = ""; - } else if (isPricingAvailable) { + payload.priceTotal = ""; + // ITAC or NoComp (where cash price is shown) + } else if (store.isVerified && (store.isItac || store.isNoComp)) { const subtotal = getSubtotal(order).toFixed(2); - payload.priceSubTotal = parseFloat(subtotal); + const total = getCartTotal(order).toFixed(2); + payload.priceTotal = parseFloat(total); } else { payload.priceSubTotal = ""; + payload.priceTotal = ""; } // Cash Quote or Cash Price Sub Total payload.cashPriceSubTotal = getSubtotal(order).toString(); - //unverified (in scenarios we don’t display the price) - if (!store.isVerified) { - payload.priceTotal = ""; - } - //deductible (in scenarios we don’t display the price) - else if ( - store.isVerified && - store.currentDeductible >= 0 && - !store.isItac && - !store.isNoComp - ) { - payload.priceTotal = ""; - } else if (isTaxAvailable) { - const total = getCartTotal(order).toFixed(2); - - payload.priceTotal = parseFloat(total); - } else { - payload.priceTotal = ""; - } - // Recalibration - // TODO: Find equivalent ISS methods. - if (hasSubmittedOrder) { - payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnSubmittedState; - } else { - payload.isRecalibrationOnOrder = store.hasRecalibrationPart && containsRecalParts(order.lineItems); - } + payload.isRecalibrationOnOrder = isRecalOrder(order.lineItems); // Appointment Type if (isDefined(order.serviceLocation.appointmentType)) { @@ -348,8 +304,6 @@ export default { payload.appointmentType = ""; } - //Insurance - // TODO: Find equivalent ISS methods. Using store atm need to run off order object. payload.isInsuranceVerified = store.isVerified; payload.insuranceCompanyName = store.issConfig.clientName ?? ""; if (!store.isVerified) { diff --git a/src/store/index.js b/src/store/index.js index db5894c6..a4689af7 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -22,11 +22,12 @@ import { repairWaivedForSelectedVehicle } from '@/helpers/policy-vehicle-helper'; import { buildURLSearchParams, getPartNumbersListForQueryString } from '@/helpers/querystring-helper'; -import { getRecalPartNumbers, getTopLevelGlassPartsWithRecal } from '@/helpers/recal-helper'; +import { getRecalPartNumbers, getTopLevelGlassPartsWithRecal, getHasRecalibrationPart } from '@/helpers/recal-helper'; import { getDateForSavedSessionTimeout } from '@/helpers/session-helper'; import { isMobileDevice } from '@/helpers/useragent-helper'; import issPageValues from '@/router/router-constants/issPage-values'; import CoverageStatuses from '@/constants/coverage-statuses'; +import { getNonFalseValuesOfPropertyInArrayOfObjects, sortArrayOfObjectsByPropertyValue } from '@/helpers/object-helper'; const storeId = 'main'; @@ -276,7 +277,7 @@ export const useMainStore = defineStore({ state: () => state, getters: { billToAccountNumber: (storeState) => storeState.issConfig.billToAccountNumber, - hasRecalibrationPart: (storeState) => getHasRecalibrationPart(storeState), + hasRecalibrationPart: (storeState) => getHasRecalibrationPartOnOrder(storeState), vehicle: (storeState) => storeState.order.vehicle, damage: (storeState) => storeState.order.damage, lineItems: (state) => state.order.lineItems, @@ -3179,39 +3180,8 @@ export const useMainStore = defineStore({ // Private Functions -function getHasRecalibrationPart(state) { - const hasRequiresRecalibration = getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, 'requiresRecalibration')?.length > 0; - const hasRecalibrationType = getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, 'recalibrationType')?.length > 0; - - if (hasRequiresRecalibration) { - if (hasRecalibrationType) { - // Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType' - return ( - getNonFalseValuesOfPropertyInArrayOfObjects( - state.order.lineItems.glassParts, - 'recalibrationType' - )[0].toLowerCase() !== 'unknown' - ); - } - // Has 'requiresRecalibration' but no 'recalibrationType' at all - return true; - } - // Does not have 'requiresRecalibration' - return false; -} - -function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) { - return (array ?? []).map((x) => x[propertyName]).filter((x) => x); -} - -function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) { - if (!arrayOfObjects) return null; - - return arrayOfObjects.sort((a, b) => { - if (a[propertyName] < b[propertyName]) return -1; - if (a[propertyName] > b[propertyName]) return 1; - return 0; - }); +function getHasRecalibrationPartOnOrder(state) { + return getHasRecalibrationPart(state.order.lineItems); } function convertGlassPieceNamingForApi(glassArray) { From d4f7213a251c21b01929e8d7976b06e37a2187c0 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 11:17:42 -0400 Subject: [PATCH 30/47] INSR-8882: add unit tests, address possible errors, formatting fixes --- src/constants/analytics.js | 8 +- .../order-confirmation.spec.js | 249 +++++++++++++++++- .../order-confirmation/order-confirmation.vue | 4 +- .../payment-page-adyen/payment-page-adyen.vue | 4 +- .../payment-return-adyen.vue | 4 +- 5 files changed, 258 insertions(+), 11 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 7649a96b..1cb1a160 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -1,5 +1,5 @@ -import packageNames from "./package-names"; -import { paymentMethods } from "./payment-method-constants"; +import packageNames from './package-names'; +import { paymentMethods } from './payment-method-constants'; const analyticsPageEvents = Object.freeze({ ENTRY: 'ENTRY', @@ -49,12 +49,12 @@ const analyticsPaymentTypeMap = new Map([ [paymentMethods.PAY_AT_TIME_OF_SERVICE, 'pay_at_service'], [paymentMethods.PAYPAL, 'pay_pal'], [paymentMethods.APPLEPAY, 'apple_pay'], -]) +]); const analyticsServicePackageMap = new Map([ [packageNames.TIER_ONE, 'basic'], [packageNames.TIER_TWO, 'essentials'], [packageNames.TIER_THREE, 'essentialsplus'] -]) +]); export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap }; diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index af243e29..ed1f1922 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -14,6 +14,8 @@ import coverageType from '@/constants/coverage-type'; import { deepClone } from '@/helpers/object-helper'; import { AppointmentTypeStrings } from '@/constants/schedule-constants'; import { paymentMethods } from '@/constants/payment-method-constants'; +import partTypeStrings from '@/constants/part-type-strings'; +import packageNames from '@/constants/package-names'; jest.mock('@/helpers/layout-helper.js', () => jest.fn()); @@ -28,7 +30,8 @@ const mockMixin = { methods: { getCmsContent: jest.fn(), setCmsContent: jest.fn(), - savePageDataToStore: jest.fn() + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() } }; @@ -958,6 +961,250 @@ describe('OrderConfirmation.vue', () => { }); }); describe('Methods', () => { + describe('handleAnalyticsEvents', () => { + let mixin; + beforeEach(() => { + mixin = { + methods: { + getCmsContent: jest.fn(), + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() + } + }; + }); + test('should call pushEventToGA with confirmation event for mobile repair', () => { + // Arrange + const order = deepClone(sessionStorage); + order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE; + order.damage.isRepair = true; + order.isVerified = false; + order.isMobileAppointment = true; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('mobile'), true); + }); + test('should call pushEventToGA with confirmation event for in-shop replace', () => { + // Arrange + const order = deepClone(sessionStorage); + order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP; + order.damage.isRepair = false; + order.isVerified = true; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('replace'), true); + }); + test('should call pushEventToGA for payment PIA successful when payment.isPayInAdvance is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.payment.isPayInAdvance = true; + order.payment.paymentMethod = paymentMethods.CREDIT_CARD; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('payment_page', 'pia_successful', expect.any(String), true); + }); + test('should not call pushEventToGA for PIA when payment.isPayInAdvance is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.payment.isPayInAdvance = false; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const paymentPageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'payment_page'); + expect(paymentPageCalls.length).toBe(0); + }); + test('should call pushEventToGA for service package when submittedOrder.servicePackage exists', () => { + // Arrange + const order = deepClone(sessionStorage); + order.servicePackage = packageNames.TIER_ONE; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('service_package', 'package_purchased', expect.any(String), true); + }); + test('should not call pushEventToGA for service package when submittedOrder.servicePackage is null', () => { + // Arrange + const order = deepClone(sessionStorage); + order.servicePackage = null; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const servicePackageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'service_package'); + expect(servicePackageCalls.length).toBe(0); + }); + test('should call pushEventToGA for total price when isFirstLoad is true and cart total greater than 0', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 100; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // Act + wrapper.vm.handleAnalyticsEvents(true); + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('total_price', expectedTotal.toString(), expect.any(String), true); + }); + test('should not call pushEventToGA for total price when isFirstLoad is false', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 100; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + useMainStore().pageData = jest.fn().mockReturnValue({ isFirstLoad: false }); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price'); + expect(totalPriceCalls.length).toBe(0); + }); + test('should not call pushEventToGA for total price when cart total is 0', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 0; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: 0 }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price'); + expect(totalPriceCalls.length).toBe(0); + }); + test('should call pushEventToGA for rain defense purchased when hasRainRepel is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = [{ partType: partTypeStrings.RAIN_DEFENSE }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'purchased', expect.any(String), true); + }); + test('should call pushEventToGA for rain defense no_purchase when hasRainRepel is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = []; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'no_purchase', expect.any(String), true); + }); + test('should call pushEventToGA for visitor_info_confirmation with ClientSite referring_site', () => { + // Arrange + const order = deepClone(sessionStorage); + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'referring_site', 'ClientSite', true); + }); + test('should call pushEventToGA for visitor_info_confirmation with client_name', () => { + // Arrange + const order = deepClone(sessionStorage); + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'client_name', expect.any(String), true); + }); + test('should call pushEventToGA for wipers purchased when hasWipers is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = [{ partType: partTypeStrings.FRONT_WIPER }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const mixin = { + methods: { + getCmsContent: jest.fn().mockReturnValue([{ Text: 'Front Beam' }]), + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() + } + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'purchased', expect.any(String), true); + }); + test('should call pushEventToGA for wipers no_purchase when hasWipers is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = []; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'no_purchase', 'none_none', true); + }); + });; describe('formatDate', () => { test.each([ ['Wednesday, April 22, 2020', '2020-04-22'], diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue index dd8a3198..063346f2 100644 --- a/src/layouts/order-confirmation/order-confirmation.vue +++ b/src/layouts/order-confirmation/order-confirmation.vue @@ -627,7 +627,7 @@ export default { handleAnalyticsEvents(isFirstLoad) { const mobileOrInshop = this.submittedOrder.isMobileAppointment ? 'mobile' : 'in_shop'; const verifiedOrNotVerified = this.submittedOrder.isVerified ? 'verified' : 'not_verified'; - const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; + const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; this.pushEventToGA('confirmation', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true); if (this.payment.isPayInAdvance) { @@ -651,7 +651,7 @@ export default { } else { coverageType = 'unverified'; } - const recalibrationType = this.submittedOrder.lineItems.glassParts.find((part) => part.requiresRecalibration).recalibrationType; + const recalibrationType = this.submittedOrder.lineItems.glassParts?.find((part) => part.requiresRecalibration)?.recalibrationType; this.pushEventToGA(`recalibration_scheduled_${coverageType}`, this.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true); } diff --git a/src/layouts/payment-page-adyen/payment-page-adyen.vue b/src/layouts/payment-page-adyen/payment-page-adyen.vue index cdf6529f..8608bc29 100644 --- a/src/layouts/payment-page-adyen/payment-page-adyen.vue +++ b/src/layouts/payment-page-adyen/payment-page-adyen.vue @@ -551,7 +551,7 @@ export default { await global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); this.hasPaymentFailureError = true; @@ -567,7 +567,7 @@ export default { await global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); this.hasPaymentFailureError = true; diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue index 33c35194..85e1888b 100644 --- a/src/layouts/payment-return-adyen/payment-return-adyen.vue +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -121,7 +121,7 @@ export default { global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); } @@ -137,7 +137,7 @@ export default { global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); } From 568ecd30047e0dd2d48782073d3516dc299c2448 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 11:23:04 -0400 Subject: [PATCH 31/47] INSR-9928: Add forward_progress GA event on vehicle damage page --- src/layouts/vehicle-damage/vehicle-damage.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index 9833cdcd..42ae3603 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -487,6 +487,7 @@ export default { logEvents() { const vehicleString = this.mainStore.order.vehicle.make + '_' + this.mainStore.order.vehicle.model + '_' + this.mainStore.order.vehicle.style; this.pushEventToGA("CAR SUBMISSION", this.mainStore.order.vehicle.year?.toString(), vehicleString, true, null, 0); + this.pushEventToGA('forward_progress', 'continue_clicked', 'vehicle_damage_2', true); if (this.isWindshieldRepair) { this.pushEventToGA("damage", "selected", "repair", true, null, null); From 7146bc1656131264dcfd167ec696f54eb41496e6 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 11:58:57 -0400 Subject: [PATCH 32/47] INSR-8882: Fix test that failed when run in larger suite --- src/layouts/order-confirmation/order-confirmation.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index ed1f1922..df85a7ec 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -156,7 +156,8 @@ const sessionStorage = { currentDeductible: { replace: 100, repair: 0 - } + }, + isUnverified: false }; function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}, mixin = mockMixin) { @@ -1103,6 +1104,7 @@ describe('OrderConfirmation.vue', () => { const order = deepClone(sessionStorage); const expectedTotal = 0; order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: 0 }]; + order.insuranceCoverage.coverageType = coverageType.ITAC; const mockStoreActions = () => { useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); }; From c17c57dbedf305f45436e7ce71af43fd0000465c Mon Sep 17 00:00:00 2001 From: Jeremy-Z Date: Mon, 13 Apr 2026 12:13:22 -0400 Subject: [PATCH 33/47] ITAC method call fix. --- src/mixins/analytics-mixin.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 4827a559..e35d1319 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -275,13 +275,13 @@ export default { else if ( store.isVerified && store.currentDeductible >= 0 && - !store.isItac && + !store.isITAC && !store.isNoComp ) { payload.priceSubTotal = ""; payload.priceTotal = ""; // ITAC or NoComp (where cash price is shown) - } else if (store.isVerified && (store.isItac || store.isNoComp)) { + } else if (store.isVerified && (store.isITAC || store.isNoComp)) { const subtotal = getSubtotal(order).toFixed(2); payload.priceSubTotal = parseFloat(subtotal); const total = getCartTotal(order).toFixed(2); @@ -310,11 +310,11 @@ export default { payload.isInsuranceItac = ""; payload.isInsuranceNoComp = ""; } else { - payload.isInsuranceItac = store.isItac ?? ""; + payload.isInsuranceItac = store.isITAC ?? ""; payload.isInsuranceNoComp = store.isNoComp ?? ""; } if ( - store.isItac || + store.isITAC || store.isNoComp || !store.isVerified ) { @@ -530,7 +530,7 @@ export default { sessionData.coverageSubStatus = coverageType.mapToApi(order?.insuranceCoverage.coverageType); sessionData.isNoComp = store.isNoComp; - sessionData.isItac = store.isITAC; + sessionData.isItac = store.isITAC; sessionData.subTotalPrice = getSubtotal(order).toString(); sessionData.totalPrice = getCartTotal(order).toString(); sessionData.cashPriceSubTotal = getSubtotal(order).toString(); From 3c3e16667134ad179b70d94330229ebefb76bc0a Mon Sep 17 00:00:00 2001 From: Jeremy-Z Date: Mon, 13 Apr 2026 12:17:05 -0400 Subject: [PATCH 34/47] Enforcement check. --- src/mixins/analytics-mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index e35d1319..81d63e41 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -274,7 +274,7 @@ export default { // Deductible case (no price is displayed, only deductible) else if ( store.isVerified && - store.currentDeductible >= 0 && + (typeof store.currentDeductible === 'number' && store.currentDeductible >= 0) && !store.isITAC && !store.isNoComp ) { From 7e37f4b7fe429cdd4f9664ea7c6584471b8c577d Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 12:31:10 -0400 Subject: [PATCH 35/47] INSR-8882: absolute pathing for imports in analytics constants file --- src/constants/analytics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 1cb1a160..dc023b15 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -1,5 +1,5 @@ -import packageNames from './package-names'; -import { paymentMethods } from './payment-method-constants'; +import packageNames from '@/constants/package-names'; +import { paymentMethods } from '@/constants/payment-method-constants'; const analyticsPageEvents = Object.freeze({ ENTRY: 'ENTRY', From db4584bc00d5427358295f36897f0862eb9eb1a8 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 12:33:13 -0400 Subject: [PATCH 36/47] INSR-8882: Move method call after const declaration --- src/layouts/order-confirmation/order-confirmation.spec.js | 2 +- src/layouts/payment-return-adyen/payment-return-adyen.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index df85a7ec..9438b038 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -1206,7 +1206,7 @@ describe('OrderConfirmation.vue', () => { // Assert expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'no_purchase', 'none_none', true); }); - });; + }); describe('formatDate', () => { test.each([ ['Wednesday, April 22, 2020', '2020-04-22'], diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue index 85e1888b..ce1ae7e5 100644 --- a/src/layouts/payment-return-adyen/payment-return-adyen.vue +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -60,7 +60,6 @@ export default { const paymentMethod = mapAdyenToIssPaymentMethod(sessionInfo?.paymentMethod); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod); - this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); const amountDue = getCartTotal(store.order); const ccToken = generateCcToken(sessionInfo); if (paymentMethod === paymentMethods.AFTERPAY) { @@ -70,6 +69,7 @@ export default { ccToken.expMonth = "03"; ccToken.expYear = "2030"; } + this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); store.savePaymentMethodChoice(paymentMethod); store.updateCreditCardToken(ccToken); From 1bd17570839ca4ea1b2fdd7f644e936ccf03d74c Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Tue, 14 Apr 2026 08:12:56 -0400 Subject: [PATCH 37/47] Change versioning Some parts v2 will not be ready for our 4.23 release --- src/constants/endpoints.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 0f75c323..59515bd3 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -6,7 +6,8 @@ const COVERAGE_BASE_URL = '/coverage/api/v1/coverage'; const EXPERIMENTS_BASE_URL = '/experiments/api/v1/experiments'; const LOCATION_BASE_URL = '/location/api/v1/location'; const ORDER_BASE_URL = '/order/api/v1/order'; -const PARTS_BASE_URL = '/parts/api/v2/parts'; +const PARTS_V1_BASE_URL = '/parts/api/v1/parts'; +const PARTS_V2_BASE_URL = '/parts/api/v2/parts'; const PRICE_BASE_URL = '/price/api/v1/price'; const SCHEDULE_BASE_URL = '/schedule/api/v1/schedule'; const VEHICLE_BASE_URL = '/vehicle/api/v1/vehicle'; @@ -42,7 +43,7 @@ const endpoints = Object.freeze({ method: 'POST' }, GetMobilePremiumFee: { - url: `${PARTS_BASE_URL}/mobile-premium-fee`, + url: `${PARTS_V1_BASE_URL}/mobile-premium-fee`, method: 'GET' }, GetVehicleYears: { @@ -62,15 +63,15 @@ const endpoints = Object.freeze({ method: 'GET' }, GetDamageOptions: { - url: `${PARTS_BASE_URL}/damage-options`, + url: `${PARTS_V1_BASE_URL}/damage-options`, method: 'GET' }, GetPartsOrQuestions: { - url: `${PARTS_BASE_URL}/parts-or-questions`, + url: `${PARTS_V1_BASE_URL}/parts-or-questions`, method: 'POST' }, GetParts: { - url: `${PARTS_BASE_URL}/parts`, + url: `${PARTS_V1_BASE_URL}/parts`, method: 'POST' }, GetITACPriceOrderItems: { @@ -99,19 +100,19 @@ const endpoints = Object.freeze({ method: 'GET' }, GetCapabilityQuestions: { - url: `${PARTS_BASE_URL}/capability-questions`, + url: `${PARTS_V1_BASE_URL}/capability-questions`, method: 'GET' }, GetPartFromCapabilityAnswer: { - url: `${PARTS_BASE_URL}/part-from-capability-answer`, + url: `${PARTS_V1_BASE_URL}/part-from-capability-answer`, method: 'POST' }, GetWipers: { - url: `${PARTS_BASE_URL}/wipers`, + url: `${PARTS_V1_BASE_URL}/wipers`, method: 'GET' }, GetRainDefense: { - url: `${PARTS_BASE_URL}/rain-repel`, + url: `${PARTS_V1_BASE_URL}/rain-repel`, method: 'GET' }, GetRecalParts: { @@ -123,19 +124,19 @@ const endpoints = Object.freeze({ zipCode, applicationName, referralSequenceNumber - ) => `${PARTS_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`, + ) => `${PARTS_V1_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`, method: 'GET' }, GetGlassFees: { - url: `${PARTS_BASE_URL}/glass-fees`, + url: `${PARTS_V1_BASE_URL}/glass-fees`, method: 'GET' }, GetSupportingItems: { - url: `${PARTS_BASE_URL}/supporting-items`, + url: `${PARTS_V1_BASE_URL}/supporting-items`, method: 'POST' }, GetMobileFeePart: { - url: `${PARTS_BASE_URL}/mobile-fee`, + url: `${PARTS_V2_BASE_URL}/mobile-fee`, method: 'GET' }, GetServiceabilityDetails: { From 8e419769de9fc11be122b583d8b9d96f5e9d2903 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Tue, 14 Apr 2026 10:09:52 -0400 Subject: [PATCH 38/47] Fix TPA repair flow --- .../business-logic/types/ITestData.ts | 1 + playwright-tests/pages/TpaSubmitPage.ts | 2 ++ playwright-tests/tests/0000__M.test.ts | 9 ++++++++- .../tests/advanced/0008a_RepairTpa.ts | 16 ++-------------- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/playwright-tests/business-logic/types/ITestData.ts b/playwright-tests/business-logic/types/ITestData.ts index 79598973..2214c384 100644 --- a/playwright-tests/business-logic/types/ITestData.ts +++ b/playwright-tests/business-logic/types/ITestData.ts @@ -45,4 +45,5 @@ export interface ITestData { isMoldingQuestion: boolean, isNonServiceable: boolean, // For the flow where a a non-serviceable vehicle is selected on lookup isNonServiceableVin: boolean, // For the flow where a serviceable vehicle is selected on lookup, but then the VIN of a non-serviceable vehicle is entered + isRepairTPA: boolean, } diff --git a/playwright-tests/pages/TpaSubmitPage.ts b/playwright-tests/pages/TpaSubmitPage.ts index 49891145..fe7743f8 100644 --- a/playwright-tests/pages/TpaSubmitPage.ts +++ b/playwright-tests/pages/TpaSubmitPage.ts @@ -24,6 +24,8 @@ export class TpaSubmitPage extends BasePage { async validateDeductible(claimDetails: IClaimDetails, isUnverifiedPolicyAfterVehicleLookup: boolean, isPolicyFound: boolean) { const deductibleTextValue = await this.deductible.textContent(); + await expect(this.deductible).toBeVisible(); + if (claimDetails.policyDeductible !== -1 && !isUnverifiedPolicyAfterVehicleLookup) { expect.soft(deductibleTextValue).toContain(claimDetails.policyDeductible.toLocaleString()); } else diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 1d3dca5a..96bc1a98 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -379,7 +379,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { isRecalWarning, servicePackage, hasOemEndorsement, hasStateLawPopup, otherVehiclesOnPolicy, isUseVehicleFromAddressLookup, isSeparateApptsWarning, vehiclePartQuestions, editVehicleDetails, isAddressLookupValidations, hasMilitaryWarning, capabilityQuestions, isUseVehicleOnPolicy, - isVehicleLookupValidations, isMoldingQuestion, isNonServiceable, isNonServiceableVin, isRecalVehicle, isCanSafeliteRecalibrate } = testCase.testData; + isVehicleLookupValidations, isMoldingQuestion, isNonServiceable, isNonServiceableVin, isRecalVehicle, isCanSafeliteRecalibrate, isRepairTPA } = testCase.testData; let { isPolicyFound } = testCase.testData; // Allow isPolicyFound to be re-assigned @@ -788,6 +788,13 @@ async function runWorkflow(page: Page, testCase: TestCase) { await providerPreferencePage.scheduleTPAWithoutAdas(); }); } + + if (!isSafelite && isRepairTPA) { + await test.step('ProviderPreferencePage >> Schedule repair with TPA without Adas', async () => { + await providerPreferencePage.validateURL(providerPreferencePage.issPageValue); + await providerPreferencePage.scheduleTPAWithoutAdas(); + }); + } } // TPA Flow if (!isPolicyFound || !(isItac || isNoComp) || isUnverifiedPolicyAfterVehicleLookup) { diff --git a/playwright-tests/tests/advanced/0008a_RepairTpa.ts b/playwright-tests/tests/advanced/0008a_RepairTpa.ts index 85b353be..621ea38d 100644 --- a/playwright-tests/tests/advanced/0008a_RepairTpa.ts +++ b/playwright-tests/tests/advanced/0008a_RepairTpa.ts @@ -33,20 +33,8 @@ const advancedScenario0008Data: Partial = { isDuplicateClaim: false, isPolicyFound: true, isNoComp: false, - hasStateLawPopup: true, - endorsements: undefined, - vehiclePartQuestions: [ - // { - // partQuestionType: PartQuestionType.WindshieldColor, - // isOnPage: true, - // optionToSelect: 'Green Tint' - // }, - // { - // partQuestionType: PartQuestionType.PassengerRearColor, - // isOnPage: true, - // optionToSelect: 'Green Tint' - // }, - ], + hasStateLawPopup: false, + isRepairTPA: true, isSafelite: false, servicePackage: faker.helpers.enumValue(ServicePackage), customerDetails: customerDetails, From 7481abb6202685b1269c926b78b2ca188ccba647 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 15 Apr 2026 08:05:42 -0400 Subject: [PATCH 39/47] Add Adyen experiment to test suite --- .../experiments/api/v1/experiments/run.json | 15 +++++++++++++++ .../experiments/api/v1/experiments/run.json | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/playwright-tests/tests/mockResponses/0002a_Advanced_Replace_Deductible_Client/experiments/api/v1/experiments/run.json b/playwright-tests/tests/mockResponses/0002a_Advanced_Replace_Deductible_Client/experiments/api/v1/experiments/run.json index f3d25831..fffb8f9b 100644 --- a/playwright-tests/tests/mockResponses/0002a_Advanced_Replace_Deductible_Client/experiments/api/v1/experiments/run.json +++ b/playwright-tests/tests/mockResponses/0002a_Advanced_Replace_Deductible_Client/experiments/api/v1/experiments/run.json @@ -14,6 +14,21 @@ "settings": { "DisplayPIAInsurance": "true" } + }, + { + "universeName": "NextGenAdyenPaymentTest", + "universeId": 848, + "testName": "NextGenAdyenPaymentIntegration_V1", + "testId": 727, + "variationName": "YesShowAdyenPaymentIntergration_V1_TEST", + "variationId": 1855, + "isActive": true, + "isExposed": true, + "userPartitionNumber": 26, + "assignmentId": 16698635, + "settings": { + "ISS_Enable_Adyen_V1": "true" + } } ] } \ No newline at end of file diff --git a/playwright-tests/tests/mockResponses/common/experiments/api/v1/experiments/run.json b/playwright-tests/tests/mockResponses/common/experiments/api/v1/experiments/run.json index 38a3b7f8..0472dc19 100644 --- a/playwright-tests/tests/mockResponses/common/experiments/api/v1/experiments/run.json +++ b/playwright-tests/tests/mockResponses/common/experiments/api/v1/experiments/run.json @@ -14,6 +14,21 @@ "settings": { "DisplayPIAInsurance": "true" } + }, + { + "universeName": "NextGenAdyenPaymentTest", + "universeId": 848, + "testName": "NextGenAdyenPaymentIntegration_V1", + "testId": 727, + "variationName": "YesShowAdyenPaymentIntergration_V1_TEST", + "variationId": 1855, + "isActive": true, + "isExposed": true, + "userPartitionNumber": 26, + "assignmentId": 16698635, + "settings": { + "ISS_Enable_Adyen_V1": "true" + } } ] } \ No newline at end of file From 00427136bf514185d451940f8ad0267809105949 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 15 Apr 2026 08:06:54 -0400 Subject: [PATCH 40/47] Update payment details to use Adyen in each test scenario --- playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts | 2 +- .../tests/advanced/0002a_ReplaceOemEndorsement.ts | 2 +- playwright-tests/tests/advanced/0003a_MobileAfterpay.ts | 2 +- playwright-tests/tests/advanced/0010a_RearGlass.ts | 2 +- playwright-tests/tests/advanced/0011a_ItacNoAdas.ts | 2 +- playwright-tests/tests/advanced/0012a_ItacDropOff.ts | 4 ++-- playwright-tests/tests/advanced/0014a_NoCompAdas.ts | 2 +- playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts | 2 +- playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts | 2 +- playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts | 2 +- playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts | 2 +- playwright-tests/tests/advanced/0030a_APIErrorBailout.ts | 2 +- .../tests/advanced/0031a_ReplaceServiceableBigTruck.ts | 2 +- .../tests/advanced/0032a_DeclineNonServiceableBigTruck.ts | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts b/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts index 0a7c5d4d..1c6d70dd 100644 --- a/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts +++ b/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts @@ -59,7 +59,7 @@ const advancedScenario0001Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getDefaultCreditCardDetailsAdyen() } // TODO: Add validation for deductible/covered amount diff --git a/playwright-tests/tests/advanced/0002a_ReplaceOemEndorsement.ts b/playwright-tests/tests/advanced/0002a_ReplaceOemEndorsement.ts index 07adbdf3..c124598f 100644 --- a/playwright-tests/tests/advanced/0002a_ReplaceOemEndorsement.ts +++ b/playwright-tests/tests/advanced/0002a_ReplaceOemEndorsement.ts @@ -106,7 +106,7 @@ const advancedScenario0002Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0003a_MobileAfterpay.ts b/playwright-tests/tests/advanced/0003a_MobileAfterpay.ts index 6243fa2e..9ea2e07b 100644 --- a/playwright-tests/tests/advanced/0003a_MobileAfterpay.ts +++ b/playwright-tests/tests/advanced/0003a_MobileAfterpay.ts @@ -74,7 +74,7 @@ const advancedScenario0003Data: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultAfterpayDetails() + paymentDetailsAdyen: ClientData.getAfterpayDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0010a_RearGlass.ts b/playwright-tests/tests/advanced/0010a_RearGlass.ts index c793e224..bc4e0c8f 100644 --- a/playwright-tests/tests/advanced/0010a_RearGlass.ts +++ b/playwright-tests/tests/advanced/0010a_RearGlass.ts @@ -54,7 +54,7 @@ const advancedScenario0010Data: Partial = { // }, ], isSafelite: true, - servicePackage: faker.helpers.enumValue(ServicePackage), + servicePackage: ServicePackage.Premium, customerDetails: customerDetails, claimDetails: { policyNumber: policyNumber, diff --git a/playwright-tests/tests/advanced/0011a_ItacNoAdas.ts b/playwright-tests/tests/advanced/0011a_ItacNoAdas.ts index 8bffcbf0..7d3a1b04 100644 --- a/playwright-tests/tests/advanced/0011a_ItacNoAdas.ts +++ b/playwright-tests/tests/advanced/0011a_ItacNoAdas.ts @@ -94,7 +94,7 @@ const advancedScenario0011Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0012a_ItacDropOff.ts b/playwright-tests/tests/advanced/0012a_ItacDropOff.ts index 213ec490..9f393b66 100644 --- a/playwright-tests/tests/advanced/0012a_ItacDropOff.ts +++ b/playwright-tests/tests/advanced/0012a_ItacDropOff.ts @@ -44,7 +44,7 @@ const advancedScenario0012Data: Partial = { ], partQuestions: undefined, isSafelite: true, - servicePackage: faker.helpers.enumValue(ServicePackage), + servicePackage: ServicePackage.GlassOnly, customerDetails: customerDetails, claimDetails: { policyNumber: policyNumber, @@ -68,7 +68,7 @@ const advancedScenario0012Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultAfterpayDetails() + paymentDetailsAdyen: ClientData.getAfterpayDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0014a_NoCompAdas.ts b/playwright-tests/tests/advanced/0014a_NoCompAdas.ts index 919833c0..a88d2ba9 100644 --- a/playwright-tests/tests/advanced/0014a_NoCompAdas.ts +++ b/playwright-tests/tests/advanced/0014a_NoCompAdas.ts @@ -68,7 +68,7 @@ const advancedScenario0014Data: Partial = { alternateServiceZip: '43016', appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultAfterpayDetails() + paymentDetailsAdyen: ClientData.getAfterpayDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts b/playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts index e336e3ea..23796265 100644 --- a/playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts +++ b/playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts @@ -103,7 +103,7 @@ const advancedScenario0016Data: Partial = { serviceAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts b/playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts index 94619a0b..9cb32f2a 100644 --- a/playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts +++ b/playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts @@ -71,7 +71,7 @@ const advancedScenario0018Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultAfterpayDetails() + paymentDetailsAdyen: ClientData.getAfterpayDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts b/playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts index d9b4c818..48ce03b8 100644 --- a/playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts +++ b/playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts @@ -73,7 +73,7 @@ const advancedScenario0019Data: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts b/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts index 358dfbd5..9e3eb79e 100644 --- a/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts +++ b/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts @@ -72,7 +72,7 @@ const advancedScenario0028aData: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getDefaultCreditCardDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts b/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts index e392adc2..d7712082 100644 --- a/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts +++ b/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts @@ -67,7 +67,7 @@ const advancedScenario0030aData: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getDefaultCreditCardDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts b/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts index aa28bae3..bf4218e5 100644 --- a/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts +++ b/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts @@ -66,7 +66,7 @@ const advancedScenario0031Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails()//ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } // TODO: Add validation for deductible/covered amount diff --git a/playwright-tests/tests/advanced/0032a_DeclineNonServiceableBigTruck.ts b/playwright-tests/tests/advanced/0032a_DeclineNonServiceableBigTruck.ts index b2433ced..20185f72 100644 --- a/playwright-tests/tests/advanced/0032a_DeclineNonServiceableBigTruck.ts +++ b/playwright-tests/tests/advanced/0032a_DeclineNonServiceableBigTruck.ts @@ -63,7 +63,7 @@ const advancedScenario0032Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails(),//ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen(), isNonServiceable: true, } From 61028aa66249eff871b25a50a2d80da284a1d006 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 15 Apr 2026 08:16:28 -0400 Subject: [PATCH 41/47] Update client data with Adyen payment methods --- .../business-logic/data/ClientData.ts | 39 ++++++++++++++++++- .../advanced/0001a_ReplaceInShopCredit.ts | 2 +- .../tests/advanced/0028a_ItacCancelMyClaim.ts | 2 +- .../tests/advanced/0030a_APIErrorBailout.ts | 2 +- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/playwright-tests/business-logic/data/ClientData.ts b/playwright-tests/business-logic/data/ClientData.ts index 1f1c2da6..a6948d69 100644 --- a/playwright-tests/business-logic/data/ClientData.ts +++ b/playwright-tests/business-logic/data/ClientData.ts @@ -1,5 +1,5 @@ import { IClient } from "@business-logic/types/Client"; -import { IPaymentDetails } from "@business-logic/types/CustomerDetails"; +import { IPaymentDetails, IPaymentDetailsAdyen } from "@business-logic/types/CustomerDetails"; import { PaymentType } from "@business-logic/types/Enums"; const essentialClients: IClient[] = [ @@ -223,6 +223,19 @@ const defaultCreditCardDetails: IPaymentDetails = { } } +const creditCardDetailsAdyen: IPaymentDetailsAdyen = { + paymentType: PaymentType.Credit, + cardNumber: '4151500000000008', + expirationDate: '03/30', + cvv: '737', + billingAddress: { + street: '123 Test Road', + city: 'Columbus', + state: 'Ohio', + postalCode: '43028', + } +} + const defaultAfterpayDetails: IPaymentDetails = { paymentType: PaymentType.AfterPay, username: 'itqatest@safelite.com', @@ -233,12 +246,24 @@ const defaultAfterpayDetails: IPaymentDetails = { cvv: '000' } +const afterpayDetailsAdyen: IPaymentDetailsAdyen = { + paymentType: PaymentType.AfterPay, + username: 'itqatest@safelite.com', + password: 'Safelite1', +} + const defaultPaypalDetails: IPaymentDetails = { paymentType: PaymentType.Paypal, username:'Itqatest@safelite.com', password: 'Safelite1' } +const paypalDetailsAdyen: IPaymentDetailsAdyen = { + paymentType: PaymentType.Paypal, + username: 'Itqatest@safelite.com', + password: 'Safelite1' +} + export default class ClientData { static getEssentialClients() { return essentialClients; @@ -264,10 +289,22 @@ export default class ClientData { return defaultCreditCardDetails; } + static getCreditCardDetailsAdyen() { + return creditCardDetailsAdyen; + } + static getDefaultAfterpayDetails() { return defaultAfterpayDetails; } + static getAfterpayDetailsAdyen() { + return afterpayDetailsAdyen; + } + + static getPaypalDetailsAdyen() { + return paypalDetailsAdyen; + } + static getDefaultPaypalDetails() { return defaultPaypalDetails; } diff --git a/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts b/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts index 1c6d70dd..baa6a433 100644 --- a/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts +++ b/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts @@ -59,7 +59,7 @@ const advancedScenario0001Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetailsAdyen: ClientData.getDefaultCreditCardDetailsAdyen() + paymentDetailsAdyen: ClientData.getCreditCardDetailsAdyen() } // TODO: Add validation for deductible/covered amount diff --git a/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts b/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts index 9e3eb79e..56e5888b 100644 --- a/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts +++ b/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts @@ -72,7 +72,7 @@ const advancedScenario0028aData: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetailsAdyen: ClientData.getDefaultCreditCardDetailsAdyen() + paymentDetailsAdyen: ClientData.getCreditCardDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts b/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts index d7712082..295c3aed 100644 --- a/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts +++ b/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts @@ -67,7 +67,7 @@ const advancedScenario0030aData: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetailsAdyen: ClientData.getDefaultCreditCardDetailsAdyen() + paymentDetailsAdyen: ClientData.getCreditCardDetailsAdyen() } From b8f4ac13e584f4e0c3c5034210e296b972894b7f Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 15 Apr 2026 08:18:39 -0400 Subject: [PATCH 42/47] Add assertions to fix flaky tests --- playwright-tests/pages/ProviderPreferencePage.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playwright-tests/pages/ProviderPreferencePage.ts b/playwright-tests/pages/ProviderPreferencePage.ts index 50b1a617..d35c54ad 100644 --- a/playwright-tests/pages/ProviderPreferencePage.ts +++ b/playwright-tests/pages/ProviderPreferencePage.ts @@ -42,6 +42,8 @@ export class ProviderPreferencePage extends BasePage { async selectProvider(isSafelite: boolean) { if (isSafelite) { + await expect(this.scheduleNowButton).toBeVisible(); + await expect(this.scheduleNowButton).toBeEnabled(); await this.scheduleNowButton.click(); await this.nextPage(); } From 1c178c7761fb8a164601f57099e299d0964a8b96 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 15 Apr 2026 08:19:16 -0400 Subject: [PATCH 43/47] Add payment adyen page --- playwright-tests/pages/PaymentAdyenPage.ts | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 playwright-tests/pages/PaymentAdyenPage.ts diff --git a/playwright-tests/pages/PaymentAdyenPage.ts b/playwright-tests/pages/PaymentAdyenPage.ts new file mode 100644 index 00000000..94c00e6f --- /dev/null +++ b/playwright-tests/pages/PaymentAdyenPage.ts @@ -0,0 +1,46 @@ +import { type Locator, type Page } from '@playwright/test'; +import { BasePage } from './BasePage'; +import { IPaymentDetailsAdyen } from '@business-logic/types/CustomerDetails'; + +export class PaymentAdyenPage extends BasePage { + readonly page: Page; + + readonly cardNumberTextField: Locator; + readonly expiryDateTextField: Locator; + readonly securityCodeTextField: Locator; + readonly billingAddressTextField: Locator; + readonly cityTextField: Locator; + readonly stateDropDown: Locator; + readonly payButton: Locator; + + readonly afterPayButton: Locator; + readonly afterPayLaunchButton: Locator; + readonly applePayButton: Locator; + + issPageValue = 'payment-page-adyen'; + + constructor(page: Page) { + super(page); + this.page = page; + + //Credit Card Fields + this.cardNumberTextField = this.page.locator('iframe[title="Iframe for card number"]').contentFrame().getByRole('textbox', { name: 'Card number' }); + this.expiryDateTextField = this.page.locator('iframe[title="Iframe for expiry date"]').contentFrame().getByRole('textbox', { name: 'Expiry date' }); + this.securityCodeTextField = this.page.locator('iframe[title="Iframe for security code"]').contentFrame().getByRole('textbox', { name: 'CVV/CVC' }); + this.billingAddressTextField = this.page.getByRole('textbox', { name: 'Address' }); + this.cityTextField = this.page.getByRole('textbox', { name: 'City' }); + this.stateDropDown = this.page.getByRole('combobox', { name: 'State' }); + this.payButton = this.page.getByRole('button', { name: 'Pay $' }); + } + + async populateCreditCardDetails(paymentDetailsAdyen: IPaymentDetailsAdyen){ + await this.cardNumberTextField.fill(paymentDetailsAdyen.cardNumber!); + await this.expiryDateTextField.fill(paymentDetailsAdyen.expirationDate!); + await this.securityCodeTextField.fill(paymentDetailsAdyen.cvv!); + await this.billingAddressTextField.fill(paymentDetailsAdyen.billingAddress!.street); + await this.cityTextField.fill(paymentDetailsAdyen.billingAddress!.city); + await this.stateDropDown.fill(paymentDetailsAdyen.billingAddress!.state); + await this.stateDropDown.press('Enter'); + } + +} \ No newline at end of file From 10a761d007235d7fd5617ddc008201b80d88f78e Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 15 Apr 2026 08:20:24 -0400 Subject: [PATCH 44/47] Update payment steps to use Adyen --- playwright-tests/pages/AfterpayPage.ts | 6 +- playwright-tests/pages/PaymentMethodPage.ts | 61 ++++++++++++++++----- playwright-tests/pages/PaymentPage.ts | 1 - playwright-tests/pages/PaypalPage.ts | 10 ++-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/playwright-tests/pages/AfterpayPage.ts b/playwright-tests/pages/AfterpayPage.ts index 2f13ed04..e70ef387 100644 --- a/playwright-tests/pages/AfterpayPage.ts +++ b/playwright-tests/pages/AfterpayPage.ts @@ -1,6 +1,6 @@ import { Locator, Page } from "@playwright/test"; import { BasePage } from "./BasePage"; -import { IClaimDetails, IPaymentDetails } from "@business-logic/types/CustomerDetails"; +import { IClaimDetails, IPaymentDetails, IPaymentDetailsAdyen } from "@business-logic/types/CustomerDetails"; import { ServicePackage } from "@business-logic/types/Enums"; export class AfterpayPage extends BasePage { @@ -33,11 +33,11 @@ export class AfterpayPage extends BasePage { await this.submitButton.click(); } - async executeAfterpayPayment(paymentDetails: IPaymentDetails, claimDetails: IClaimDetails, servicePackage: ServicePackage) { + async executeAfterpayPayment(paymentDetailsAdyen: IPaymentDetailsAdyen, claimDetails: IClaimDetails, servicePackage: ServicePackage) { const confirmButtonOrPaymentOptions = this.confirmButton.or(this.selectAfterpayWithoutInterestButton); - await this.login(paymentDetails.password!); + await this.login(paymentDetailsAdyen.password!); await this.page.locator('div[data-testid=\'loading-icon-svg\']').filter({ visible: true}).first().waitFor({ state: 'hidden' }); await confirmButtonOrPaymentOptions.waitFor({ state: 'visible' }); diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index d7c7ef9c..b74dd22c 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -1,18 +1,24 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; -import { IClaimDetails, IPaymentDetails } from '@business-logic/types/CustomerDetails'; +import { IClaimDetails, IPaymentDetails, IPaymentDetailsAdyen } from '@business-logic/types/CustomerDetails'; import { PaymentType, ServicePackage } from '@business-logic/types/Enums'; import { PaymentPage } from './PaymentPage'; import { AfterpayPage } from './AfterpayPage'; import { PaypalPage } from './PaypalPage'; +import { PaymentAdyenPage } from './PaymentAdyenPage'; export class PaymentMethodPage extends BasePage { readonly page: Page; readonly paypalPage: PaypalPage; readonly paymentPage: PaymentPage; + readonly paymentAdyenPage: PaymentAdyenPage; readonly payNowButton: Locator; readonly paypalButton: Locator; + readonly payPalAdyenButton: Locator; + readonly payPalLaunchAdyenButton: Locator; + readonly afterpayAdyenButton: Locator; + readonly afterpayLaunchAdyenButton: Locator; readonly payInFourButton: Locator; readonly payAtAppointmentButton: Locator; @@ -28,9 +34,20 @@ export class PaymentMethodPage extends BasePage { this.page = page; this.paypalPage = new PaypalPage(page); this.paymentPage = new PaymentPage(page); + this.paymentAdyenPage = new PaymentAdyenPage(page); this.payNowButton = this.page.locator('[buttonlabel="Pay now"]'); // credit and paypal options behind this button this.paypalButton = this.page.frameLocator('iframe[name="card-frame"]').locator('div[id="paypalParentDiv"]'); + + // Ayden Paypal button + this.payPalAdyenButton = this.page.getByRole('radio', { name: 'PayPal' }); + this.payPalLaunchAdyenButton = this.page.frameLocator('iframe[title="PayPal-paypal"]:first-of-type').locator('div[role="link"][class*="paypal-button"]'); + + // Ayden Afterpay buttons + this.afterpayAdyenButton = this.page.getByRole('radio', { name: 'Afterpay' }); + this.afterpayLaunchAdyenButton = this.page.getByRole('button', { name: 'Continue to Afterpay' }); + + this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]'); // Afterpay this.payAtAppointmentButton = this.page.locator('[buttonlabel="Pay at my appointment"]'); @@ -40,22 +57,24 @@ export class PaymentMethodPage extends BasePage { this.submitButton = this.page.getByRole('button', { name: 'Submit' }); } - async executePayment(paymentDetails: IPaymentDetails, claimDetails: IClaimDetails, servicePackage: ServicePackage) { + async executePayment(paymentDetails: IPaymentDetails, paymentDetailsAdyen: IPaymentDetailsAdyen, claimDetails: IClaimDetails, servicePackage: ServicePackage) { const browserContext = this.page.context(); - switch (paymentDetails.paymentType) { - case PaymentType.Credit: + switch (paymentDetailsAdyen?.paymentType ?? paymentDetails?.paymentType) { case PaymentType.Credit: await this.payNowButton.click(); await this.textReminderNoButton.click(); await this.continueToCheckoutButton.click(); - await this.selectCreditCard(paymentDetails); + await this.selectCreditCard(paymentDetailsAdyen); break; case PaymentType.Paypal: await this.payNowButton.click(); await this.textReminderNoButton.click(); await this.continueToCheckoutButton.click(); - await this.selectPaypal(); - await this.paypalPage.completePaypalPurchase(paymentDetails); + + const paypalPopup = await this.selectPaypal(); + const paypalPage = new PaypalPage(paypalPopup); + + await paypalPage.completePaypalPurchase(paymentDetailsAdyen); break; case PaymentType.AfterPay: await this.payInFourButton.click(); @@ -63,11 +82,11 @@ export class PaymentMethodPage extends BasePage { await this.continueToCheckoutButton.click(); // Capture popup - const afterpayPopup = await browserContext.waitForEvent('page'); - const afterpayPage = new AfterpayPage(afterpayPopup); + const afterpayPage = await this.selectAfterpayAdyen(); + const afterpayAdyenPage = new AfterpayPage(afterpayPage); // Execute payment - await afterpayPage.executeAfterpayPayment(paymentDetails, claimDetails, servicePackage); + await afterpayAdyenPage.executeAfterpayPayment(paymentDetailsAdyen, claimDetails, servicePackage); break; case PaymentType.PayAtService: @@ -80,13 +99,25 @@ export class PaymentMethodPage extends BasePage { break; } } - - async selectPaypal() { - await this.paypalButton.click(); + async selectAfterpayAdyen(): Promise { + await this.afterpayAdyenButton.click(); + await this.afterpayLaunchAdyenButton.click(); + //await this.page.waitForLoadState('networkidle'); + return this.page; } - async selectCreditCard(paymentDetails: IPaymentDetails) { - await this.paymentPage.populateCreditCardDetails(paymentDetails); + async selectPaypal(): Promise { + await this.payPalAdyenButton.click(); + const paypalPage = this.page.waitForEvent('popup'); + await this.payPalLaunchAdyenButton.click(); + const paypalPopup = await paypalPage; + await paypalPopup.waitForLoadState(); + return paypalPopup; + } + + async selectCreditCard(paymentDetailsAdyen: IPaymentDetailsAdyen) { + await this.paymentAdyenPage.populateCreditCardDetails(paymentDetailsAdyen); + await this.paymentAdyenPage.payButton.click(); } async selectPayAtAppointment() { diff --git a/playwright-tests/pages/PaymentPage.ts b/playwright-tests/pages/PaymentPage.ts index 4ba47ee6..fff2edd3 100644 --- a/playwright-tests/pages/PaymentPage.ts +++ b/playwright-tests/pages/PaymentPage.ts @@ -1,6 +1,5 @@ import { type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; -import { AfterpayPage } from './AfterpayPage'; import { IPaymentDetails } from '@business-logic/types/CustomerDetails'; export class PaymentPage extends BasePage { diff --git a/playwright-tests/pages/PaypalPage.ts b/playwright-tests/pages/PaypalPage.ts index 05178dd9..8f97a019 100644 --- a/playwright-tests/pages/PaypalPage.ts +++ b/playwright-tests/pages/PaypalPage.ts @@ -1,6 +1,6 @@ import { expect, type Locator, type Page } from "@playwright/test"; import { BasePage } from "./BasePage"; -import { IPaymentDetails } from "@business-logic/types/CustomerDetails"; +import { IPaymentDetailsAdyen } from "@business-logic/types/CustomerDetails"; export class PaypalPage extends BasePage { readonly page: Page; @@ -13,7 +13,7 @@ export class PaypalPage extends BasePage { constructor(page: Page) { super(page); this.page = page; - this.usernameTextBox = page.locator("#email"); + this.usernameTextBox = page.getByRole("textbox", { name: "Email or mobile number" }); this.nextButton = page.getByRole("button", { name: "Next" }); this.passwordTextBox = page.getByRole("textbox", { name: "Password" }); this.paypalLoginButton = page.getByRole("button", { @@ -23,16 +23,16 @@ export class PaypalPage extends BasePage { this.payButton = page.getByRole("button", { name: "Pay $" }); } - async completePaypalPurchase(paymentDetails: IPaymentDetails) { + async completePaypalPurchase(paymentDetailsAdyen: IPaymentDetailsAdyen) { await expect(async () => { - await this.usernameTextBox.fill(paymentDetails.username!); + await this.usernameTextBox.fill(paymentDetailsAdyen.username!); await this.nextButton.waitFor({ state: 'visible', timeout: 5000 }); await this.nextButton.click(); await expect(this.passwordTextBox).toBeVisible({ timeout: 5000 }); }).toPass({ timeout: 30000 }); - await this.passwordTextBox.fill(paymentDetails.password!); + await this.passwordTextBox.fill(paymentDetailsAdyen.password!); await this.paypalLoginButton.click(); await this.payButton.click(); } From 2ebc4b9c770b18cb82698bb589f34abbc5f73a39 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 15 Apr 2026 08:23:26 -0400 Subject: [PATCH 45/47] Add Adyen type --- .../business-logic/types/CustomerDetails.ts | 10 ++++++++++ playwright-tests/business-logic/types/ITestData.ts | 5 +++-- playwright-tests/tests/0000__M.test.ts | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/playwright-tests/business-logic/types/CustomerDetails.ts b/playwright-tests/business-logic/types/CustomerDetails.ts index 1020694f..f722565a 100644 --- a/playwright-tests/business-logic/types/CustomerDetails.ts +++ b/playwright-tests/business-logic/types/CustomerDetails.ts @@ -72,6 +72,16 @@ export interface IPaymentDetails { billingAddress?: IAddress, } +export interface IPaymentDetailsAdyen { + paymentType: PaymentType, + username?: string, + password?: string, + cardNumber?: string, + expirationDate?: string, + cvv?: string, + billingAddress?: IAddress, +} + // export interface IVehicleDamage { // isRearWindowDamage?: boolean, // windshieldDamage?: WindshieldDamage, diff --git a/playwright-tests/business-logic/types/ITestData.ts b/playwright-tests/business-logic/types/ITestData.ts index 2214c384..a457644c 100644 --- a/playwright-tests/business-logic/types/ITestData.ts +++ b/playwright-tests/business-logic/types/ITestData.ts @@ -1,5 +1,5 @@ import { ServicePackage, VehicleDamage } from "./Enums" -import { IAppointmentDetails, IClaimDetails, ICustomerDetails, IEndorsementDetails, IPartQuestion, IPaymentDetails, IVehicleDetails, IAddressVehicleDetails } from "./CustomerDetails" +import { IAppointmentDetails, IClaimDetails, ICustomerDetails, IEndorsementDetails, IPartQuestion, IPaymentDetails, IPaymentDetailsAdyen, IVehicleDetails, IAddressVehicleDetails } from "./CustomerDetails" import IBailoutFlags from "./IBailoutFlags" export interface ITestData { @@ -35,7 +35,8 @@ export interface ITestData { otherVehiclesOnPolicy: IVehicleDetails[], // IF defined, we validate that the vehicles are present. vehicleDamage: VehicleDamage[], // Array of vehicle damage appointmentDetails: IAppointmentDetails, - paymentDetails: IPaymentDetails // Payment information + paymentDetails: IPaymentDetails, // Payment information + paymentDetailsAdyen: IPaymentDetailsAdyen, // Adyen Payment information isRecalNotification: boolean, isRecalWarning: boolean, isRecalVehicle: boolean, diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 96bc1a98..787b5704 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -375,7 +375,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { // Destructure data for easy access const { customerDetails, claimDetails, vehicleDetails, addressVehicleDetails, vehicleDamage, appointmentDetails, isSafelite, endorsements, - partQuestions, paymentDetails, isNoComp, isItac, isRecalNotification, isUnverifiedPolicyAfterVehicleLookup, + partQuestions, paymentDetails, paymentDetailsAdyen, isNoComp, isItac, isRecalNotification, isUnverifiedPolicyAfterVehicleLookup, isRecalWarning, servicePackage, hasOemEndorsement, hasStateLawPopup, otherVehiclesOnPolicy, isUseVehicleFromAddressLookup, isSeparateApptsWarning, vehiclePartQuestions, editVehicleDetails, isAddressLookupValidations, hasMilitaryWarning, capabilityQuestions, isUseVehicleOnPolicy, @@ -879,7 +879,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { if (isPolicyFound && (isUseVehicleOnPolicy ?? true) && claimDetails!.policyDeductible > 0) { await test.step('PaymentMethodPage >> Execute Payment', async () => { await paymentMethodPage.validateURL(paymentMethodPage.issPageValue); - await paymentMethodPage.executePayment(paymentDetails!, claimDetails!, servicePackage!); + await paymentMethodPage.executePayment(paymentDetails!, paymentDetailsAdyen!, claimDetails!, servicePackage!); await paymentMethodPage.nextPage(); }); From e26f4edb598bed6419d163f68f26b067dadb2dfa Mon Sep 17 00:00:00 2001 From: JennyNou <167806377+JennyNou@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:41:46 -0400 Subject: [PATCH 46/47] Turn video off for regression run pipeline --- playwright-tests/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts index 5e632267..682ee0fc 100644 --- a/playwright-tests/playwright.config.ts +++ b/playwright-tests/playwright.config.ts @@ -91,7 +91,7 @@ export default defineConfig({ baseURL: process.env.BASE_URL || 'https://selfservice.test.glassclaim.com', trace: 'on-first-retry', headless: process.env.CI ? true : false, - video: 'retain-on-failure', + video: 'off', viewport: { width: 1920, height: 1080 }, screenshot: "only-on-failure", actionTimeout: process.env.CI ? 90_000 : 60_000, From 22afb1e398b565d12729b847766704a4884257f0 Mon Sep 17 00:00:00 2001 From: JennyNou <167806377+JennyNou@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:04:05 -0400 Subject: [PATCH 47/47] Update logic to use session storage and reduce retries --- playwright-tests/pages/BasePage.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index a1e068af..d8452aec 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -96,24 +96,23 @@ export class BasePage { } async logReferralNumber() { - let mainSessionStorage = JSON.parse(await this.page.evaluate('sessionStorage.getItem(\'main\')')); - let referralNumber = mainSessionStorage.order.referralNumber as number; - let referralSequenceNumber = mainSessionStorage.order.referralSequenceNumber as number; - if (referralNumber == null) { - for (let i = 1; i <= 20; i++) { - if (referralNumber !== null) break; - await this.page.waitForTimeout(500); - mainSessionStorage = JSON.parse(await this.page.evaluate('localStorage.getItem(\'main\')')); - referralNumber = mainSessionStorage.order.referralNumber as number; - referralSequenceNumber = mainSessionStorage.order.referralSequenceNumber as number; - } + let referralNumber: number | null = null; + let referralSequenceNumber: number | null = null; + for (let i = 0; i < 3; i++) { + const main = JSON.parse(await this.page.evaluate(() => sessionStorage.getItem('main')) ?? 'null'); + referralNumber = main?.order?.referralNumber ?? null; + referralSequenceNumber = main?.order?.referralSequenceNumber ?? null; + if (referralNumber !== null) break; + await this.page.waitForTimeout(500); + } + if (referralNumber === null) { + console.warn('Referral Number could not be retrieved after retries.'); + return; } - await test.step(`Referral Number:${referralNumber} Referral Sequence Number:${referralSequenceNumber}`, async () => { console.log(`Referral Number:${referralNumber}`); console.log(`Referral Sequence Number:${referralSequenceNumber}`); }); - } async validateURL(issPageValue: string) {