Merge branch 'develop' into CASH-1042-header-and-progress-bar-style-updates

This commit is contained in:
bmauger 2025-07-17 09:55:09 -04:00 committed by GitHub
commit 4b3572ec07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
98 changed files with 11520 additions and 34 deletions

9
.gitignore vendored
View file

@ -13,6 +13,15 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Playwright
/test-results/
/playwright-report/
/playwright-tests/test-results/
/blob-report/
/playwright/.cache/
artifacts/
ortoni-report/
# Editor directories and files
.idea
.vscode

18
Dockerfile.playwright Normal file
View file

@ -0,0 +1,18 @@
FROM mcr.microsoft.com/playwright:v1.48.0-noble
# Set the working directory in the container
WORKDIR /app
ARG NPM_CONFIG_USERCONFIG=/tmp/.npmrc
COPY playwright-tests/.npmrc /tmp/.npmrc
# Copy the application code
COPY . .
# Install dependencies
RUN npm ci
RUN npm --prefix playwright-tests ci
# Install Playwright browsers
RUN npx --prefix playwright-tests playwright install chromium --with-deps

View file

@ -0,0 +1,46 @@
trigger: none
schedules:
- cron: 0 9 * * MON-FRI
always: true
displayName: Daily Test Automation Run for FMG-NextGen
branches:
include:
- develop
pool: 'Default'
variables:
# - group: Digital-Infrastructure
# - group: FMG-BuildBranches
- name: dockerImageName
value: 'playwright-tests'
- name: imageTag
value: '$(Build.BuildId)'
- name: totalShards
value: '4'
- name: IS_REGRESSION
value: 'true'
- name: JIRA_BOARD_ID
value: '845'
- name: JIRA_EPIC_KEY
value: 'CASH-208'
- name: JIRA_PROJECT_KEY
value: 'CASH'
stages:
- stage: TestPr
displayName: Run Playwright Test
jobs:
# TODO: Change to ADO Playwright template
- template: temp/playwright-test.yml
parameters:
totalShards: ${{ variables.totalShards }}
targetUrl: $(BASE_URL)
dockerFileName: 'Dockerfile.playwright'
isRegression: ${{ variables.IS_REGRESSION }}
filterTags: ''
playwrightTestsPath: 'playwright-tests'
npmServePath: '.'
npmrcPath: 'playwright-tests/.npmrc'
secrets:
CCIS_API_AUTH: $(CCIS_API_AUTH)
JIRA_API_KEY: $(JIRA_API_KEY)

View file

@ -59,6 +59,22 @@ stages:
npmLocation: $(Build.SourcesDirectory)
testResultsFile: junit.xml
summaryFileLocation: $(Build.SourcesDirectory)/coverage/cobertura-coverage.xml
- stage: TestPrPlaywright
displayName: Run Playwright Tests For PullRequest
jobs:
- template: temp/playwright-test.yml
parameters:
totalShards: 2
targetUrl: $(BASE_URL)
dockerFileName: 'Dockerfile.playwright'
isRegression: false
filterTags: ''
playwrightTestsPath: 'playwright-tests'
npmServePath: '.'
npmrcPath: 'playwright-tests/.npmrc'
secrets:
CCIS_API_AUTH: $(CCIS_API_AUTH)
JIRA_API_KEY: $(JIRA_API_KEY)
- ${{ else }}:
# Dev Build/Deploy
- stage: Dev

View file

@ -0,0 +1,4 @@
.git
*Dockerfile*
*docker-compose*
node_modules

29
playwright-tests/.env Normal file
View file

@ -0,0 +1,29 @@
# .env
# Environment configuration
# Environment type
NODE_ENV="qa"
# Skip content site
SKIP_CONTENT_SITE=false
# Base URLs by environment
# qa
BASE_URL="https://www-qa2.safelite.com/"
# local version of FMG (after running the local server)
# BASE_URL="http://localhost:8080/fmg/"
# qa with skipToInsurance Turned Off
# BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true,NextGen_IGQSkipToInsurance=NextGen_IGQSkipToInsurance_V1=NextGen_IGQSkipToInsurance_CONTROL=true"
# sys
# BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle"
# dev
# BASE_URL="https://www-dev2.safelite.com/"
# dev with leadgen
# BASE_URL="https://www-dev2.safelite.com/?&experiments=LeadGenHeritage=LeadGenHeritageExp_V3=LeadGenHeritageTestV3=true"
# API endpoints
CCIS_API_URL="https://api.test.belronus.io"
ADMIN_SERVICE_API_URL="https://issadminapi.dev.sagaws.net/iss-admin/api/v1/"
# API authentication (replace with actual value when running tests)
CCIS_API_AUTH="undefined"

30
playwright-tests/.env.dev Normal file
View file

@ -0,0 +1,30 @@
# .env.dev
# Environment configuration
# Environment type
NODE_ENV="qa"
# Skip content site
SKIP_CONTENT_SITE=false
# Base URLs by environment
# qa
BASE_URL="https://www-qa2.safelite.com/"
# local version of FMG (after running the local server)
# BASE_URL="http://localhost:8080/fmg/"
# qa with skipToInsurance Turned Off
# BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true,NextGen_IGQSkipToInsurance=NextGen_IGQSkipToInsurance_V1=NextGen_IGQSkipToInsurance_CONTROL=true"
# sys
# BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle"
# dev
# BASE_URL="https://www-dev2.safelite.com/"
# dev with leadgen
# BASE_URL="https://www-dev2.safelite.com/?&experiments=LeadGenHeritage=LeadGenHeritageExp_V3=LeadGenHeritageTestV3=true"
# API endpoints
CCIS_API_URL="https://api.test.belronus.io"
ADMIN_SERVICE_API_URL="https://issadminapi.dev.sagaws.net/iss-admin/api/v1/"
# API authentication (replace with actual value when running tests)
CCIS_API_AUTH="undefined"

View file

@ -0,0 +1,29 @@
# .env
# Environment configuration
# Environment type
NODE_ENV="qa"
# Skip content site
SKIP_CONTENT_SITE=false
# Base URLs by environment
# qa
BASE_URL="https://www-qa2.safelite.com/"
# local version of FMG (after running the local server)
# BASE_URL="http://localhost:8080/fmg/"
# qa with skipToInsurance Turned Off
# BASE_URL="https://fixmyglassqa.safelite.com/?cns=all&experiments=ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_VinAndEmailOptional=true,NextGen_IGQSkipToInsurance=NextGen_IGQSkipToInsurance_V1=NextGen_IGQSkipToInsurance_CONTROL=true"
# sys
# BASE_URL="https://www-test2.safelite.com/fmg/?fmgPage=vehicle"
# dev
# BASE_URL="https://www-dev2.safelite.com/"
# dev with leadgen
# BASE_URL="https://www-dev2.safelite.com/?&experiments=LeadGenHeritage=LeadGenHeritageExp_V3=LeadGenHeritageTestV3=true"
# API endpoints
CCIS_API_URL="https://api.test.belronus.io"
ADMIN_SERVICE_API_URL="https://issadminapi.dev.sagaws.net/iss-admin/api/v1/"
# API authentication (replace with actual value when running tests)
CCIS_API_AUTH="undefined"

3
playwright-tests/.npmrc Normal file
View file

@ -0,0 +1,3 @@
registry=https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/
always-auth=true

266
playwright-tests/README.md Normal file
View file

@ -0,0 +1,266 @@
# QA Automation Framework
An automated testing framework for FMG using Playwright with TypeScript.
## Technology Stack
• Playwright - Core testing framework <br>
• TypeScript - Programming language <br>
• Node.js - Runtime environment <br>
• SauceLabs/Dockers - Cross-browser testing platform <br>
## Key Features
• Page Object Model implementation <br>
• Data-driven test approach <br>
• Cross-browser testing support <br>
• Parallel test execution <br>
• HTML report generation <br>
• Environment-specific configurations <br>
• SauceLabs integration <br>
## Test Scenarios
The framework includes various test scenarios covering essential flows like:
### Cash Payment - Repair Scenarios
1. **CashRepairMobileCreditCard**<br>
- Repair or Replace: Repair<br>
- Damage Type: Windshield Chip (1)<br>
- Vehicle Lookup Type: ZIP<br>
- CASH or Insurance: CASH<br>
- Service Type: Glass Service Only<br>
- Service Location Type: Mobile<br>
- Schedule Appointment Type: Range of Hours<br>
- Payment Type: PIA - Credit Card<br>
2. **CashRepairInShopAfterPay**<br>
- Repair or Replace: Repair<br>
- Damage Type: Windshield Chip (2)<br>
- Vehicle Lookup Type: ZIP<br>
- CASH or Insurance: CASH<br>
- Service Type: Standard<br>
- Service Location Type: In-Shop<br>
- Schedule Appointment Type: Hourly<br>
- Payment Type: PIA - AfterPay<br>
3. **CashRepairInShopPayPal**<br>
- Repair or Replace: Repair<br>
- Damage Type: Windshield Chip (3)<br>
- Vehicle Lookup Type: ZIP<br>
- CASH or Insurance: CASH<br>
- Service Type: Premium<br>
- Service Location Type: In-Shop<br>
- Schedule Appointment Type: Hourly<br>
- Payment Type: PIA - PayPal<br>
### Cash Payment - Single Glass Replacement Scenarios
4. **CashReplaceDynamicRecalMobile**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: VIN Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Glass Service Only<br>
- Service Location Type: Mobile<br>
- Schedule Appointment Type: Range of Hours<br>
- Payment Type: Pay At Service<br>
- Vehicle Info: Dynamic Recalibration Vehicle<br>
5. **CashReplaceGlassAddressLookupInshopAfterPay**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: Address Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Glass Service Only<br>
- Service Location Type: In-Shop<br>
- Schedule Appointment Type: Hourly<br>
- Payment Type: PIA - Afterpay<br>
6. **CashReplaceGlassLicensePlateLookupInshopPaypal**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: License Plate Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Glass Service Only<br>
- Service Location Type: In-Shop<br>
- Schedule Appointment Type: Hourly<br>
- Payment Type: PIA - PayPal<br>
7. **CashReplaceVinMobile**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: VIN Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Glass Service Only<br>
- Service Location Type: Mobile<br>
- Schedule Appointment Type: Range of Hours<br>
- Payment Type: Pay At Service<br>
8. **CashReplaceSafeliteCanNotRecalMobile**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: ZIP Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Glass Service Only<br>
- Service Location Type: Mobile<br>
- Schedule Appointment Type: Range of Hours<br>
- Payment Type: Pay At Service<br>
- Additional Info: Safelite Cannot Recalibrate Vehicle<br>
### Cash Payment - Multiple Glass Replacement Scenarios
9. **CashReplaceMultiGlassPromoInshop**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack, Driver Front Door, Driver Rear Door, Driver Vent Glass, Passenger Vent Glass, Rear Window<br>
- Vehicle Lookup Type: VIN Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Glass Service Only<br>
- Service Location Type: In-Shop<br>
- Schedule Appointment Type: Hourly<br>
- Payment Type: Pay At Service<br>
- Additional Info: Glass Promo<br>
10. **CashReplaceMultiSlidingGlassDropoff**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack, Passenger Front Door, Passenger Rear Door, Rear Glass With Slider<br>
- Vehicle Lookup Type: ZIP Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Standard<br>
- Service Location Type: Dropoff<br>
- Schedule Appointment Type: Dropoff<br>
- Payment Type: Pay At Service<br>
- Vehicle Info: Sliding glass vehicle, capability questions<br>
11. **CashReplaceMultiGlassMobile**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack, Driver Quarter Panel, Passenger Quarter Panel<br>
- Vehicle Lookup Type: ZIP Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Premium<br>
- Service Location Type: Mobile<br>
- Schedule Appointment Type: Range Of Hours<br>
- Payment Type: Pay At Service<br>
### Cash Payment - Promo Scenarios
12. **CashReplaceGlassPromoInshop**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: VIN Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Glass Service Only<br>
- Service Location Type: In-Shop<br>
- Schedule Appointment Type: Hourly<br>
- Payment Type: Pay At Service<br>
- Additional Info: Glass Promo<br>
13. **CashReplaceRainDefensePromoInshop**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: VIN Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Premium<br>
- Service Location Type: In-Shop<br>
- Schedule Appointment Type: Hourly<br>
- Payment Type: Pay At Service<br>
- Additional Info: Rain Defense Promo<br>
14. **CashReplaceWiperPromoInshop**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: VIN Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Standard<br>
- Service Location Type: In-Shop<br>
- Schedule Appointment Type: Hourly<br>
- Payment Type: Pay At Service<br>
- Vehicle Info: Wiper Promo<br>
15. **CashReplaceWiperDropoff**<br>
- Repair or Replace: Replace<br>
- Damage Type: Windshield Crack<br>
- Vehicle Lookup Type: ZIP Lookup<br>
- CASH or Insurance: CASH<br>
- Service Type: Standard<br>
- Service Location Type: Drop Off<br>
- Schedule Appointment Type: Drop Off<br>
- Payment Type: Pay At Service<br>
## Installation
### Install dependencies
npm install
npx playwright install
or
RUN npx playwright install chromium --with-deps
### Run Tests with script
npm run test:playwright
### Install SauceLabs CLI
npm install saucectl
## Configuration
• Environment configurations in .env files <br>
• SauceLabs configuration in config.yml <br>
• Playwright configuration in playwright.config.ts <br>
## Project Structure
├── tests/ # Test scenarios and cases <br>
├── pages/ # Page Object Models <br>
├── business-logic/ # Business logic and data models <br>
├── impl/ # Implementation utilities <br>
└── artifacts/ # Test artifacts and results <br>
## Running Tests
### Run all tests
npx playwright test
### Run a specific test file
npx playwright test tests/0000__M.test.ts
### Run tests with specific tag (cognitive approach)
npx playwright test --grep "@Alert"
## Test Reports
Test results are available in:
• ortoni report (summarized HTML) <br>
• HTML format (playwright-report) <br>
• JUnit format <br>
• SauceLabs dashboard <br>
## Reccomended Visual Studio Code Extensions
• JavaScript and TypeScript Nightly <br>
• Playwright Test for VSCode <br>
## Key Features
Page Object Model implementation <br>
Data-driven test approach <br>
Cross-browser testing support <br>
Integration with SauceLabs<br>
Parallel test execution <br>
HTML report generation <br>
Environment-specific configurations <br>
## Common Test Flows
• Vehicle lookup validation <br>
• Shop selection <br>
• Damage assessment <br>
• Appointment scheduling <br>
• Coverage verification <br>
• Payment processing <br>
## Contributing
1. Follow the established page object pattern
2. Add proper test documentation
3. Include appropriate test tags
4. Ensure tests are isolated and repeatable
## Environment Variables
BASE_URL=https://www-dev2.safelite.com<br>
CCIS_API_URL= <br>
ADMIN_SERVICE_API_URL= <br>
SAUCE_USERNAME= <br>
SAUCE_ACCESS_KEY= <br>
## CI/CD Integration
The project uses Azure Pipelines for continuous integration with configurations defined in azure-pipelines.yml.

View file

@ -0,0 +1,7 @@
import { ITestData as base } from 'safelite-playwright-core'
import { PaymentMethod } from './localTypes/Enums'
export interface ITestData extends base {
// Put any project-specific data here. Anything useful to other Safelite projects should be submitted as a pull request to safelite-playwright-core.
paymentMethod: PaymentMethod
}

View file

@ -0,0 +1,101 @@
import { Page } from "@playwright/test";
import { type TestPagesFactory } from 'safelite-playwright-core'
import { HomePage } from "../pages/HomePage"
import { LeadgenHomePage } from "../pages/LeadgenHomePage"
import { ServicePackagesPage } from "../pages/ServicePackagesPage"
import { VehicleDamagePage } from "../pages/VehicleDamagePage"
import { VehicleLookupAddressPage } from "../pages/VehicleLookupAddressPage"
import { VehicleLookupLicensePage } from "../pages/VehicleLookupLicensePage"
import { EstimatePage } from "../pages/EstimatePage"
import { VehicleSelectionPage } from "../pages/VehicleSelectionPage"
import { VinLookupPage } from "../pages/VinLookupPage"
import { ServiceLocationPage } from "../pages/ServiceLocationPage"
import { SchedulePage } from "../pages/SchedulePage"
import { ContactDetailsPage } from "../pages/ContactDetailsPage"
import { PaymentMethodPage } from "../pages/PaymentMethodPage"
import { ZipLookupPage } from "../pages/ZipLookupPage"
import { OrderConfirmationPage } from "../pages/OrderConfirmationPage"
import { PartQuestionsPage } from "../pages/PartQuestionPage"
import VehiclePartQuestionsPage from "../pages/VehiclePartsPage"
import CapabilityQuestionsPage from "../pages/CapabilityQuestionsPage"
import MoldingQuestionsPage from "../pages/MoldingQuestionsPage"
import { InsuranceCompanyPage } from "../pages/InsuranceCompanyPage"
import { CCPolicyInfoPage } from "../pages/CCPolicyInfoPage"
import { DuplicateCheckPage } from "../pages/DuplicateCheckPage"
import { PolicyVehiclesPage } from "../pages/PolicyVehiclesPage"
import { PolicyInfoSubmittedPage } from "../pages/PolicyInfoSubmittedPage"
import RecalibrationInfoPage from "../pages/RecalibrationInfoPage"
import { CoverageStatementPage } from "../pages/CoverageStatementPage"
import { VerifyDetailsPage } from "../pages/VerifyDetailsPage"
import { EndorsementsPage } from "../pages/EndorsementsPage"
import { PolicyDriverPage } from "../pages/PolicyDriverPage"
import { ServiceZipPage } from "../pages/ServiceZipPage"
export interface ITestPages {
capabilityQuestionsPage: CapabilityQuestionsPage,
ccPolicyInfoPage: CCPolicyInfoPage,
contactDetailsPage: ContactDetailsPage,
coverageStatementPage: CoverageStatementPage,
duplicateCheckPage: DuplicateCheckPage,
estimatePage: EstimatePage,
homePage: HomePage,
insuranceCompanyPage: InsuranceCompanyPage,
leadgenHomePage: LeadgenHomePage,
moldingQuestionsPage: MoldingQuestionsPage,
orderConfirmationPage: OrderConfirmationPage,
partQuestionsPage: PartQuestionsPage,
paymentMethodPage: PaymentMethodPage,
policyInfoSubmittedPage: PolicyInfoSubmittedPage,
policyVehiclesPage: PolicyVehiclesPage,
recalibrationInfoPage: RecalibrationInfoPage,
schedulePage: SchedulePage,
serviceLocationPage: ServiceLocationPage,
servicePackagesPage: ServicePackagesPage,
serviceZipPage: ServiceZipPage,
vehicleDamagePage: VehicleDamagePage,
vehicleLookupAddressPage: VehicleLookupAddressPage,
vehicleLookupLicensePage: VehicleLookupLicensePage,
vehiclePartsPage: VehiclePartQuestionsPage,
vehicleSelectionPage: VehicleSelectionPage,
verifyDetailsPage: VerifyDetailsPage,
vinLookupPage: VinLookupPage,
zipLookupPage: ZipLookupPage,
endorsementsPage: EndorsementsPage,
policyDriverPage: PolicyDriverPage,
}
export const createTestPages: TestPagesFactory<ITestPages> = (page: Page) => {
const pages: ITestPages = {
capabilityQuestionsPage: new CapabilityQuestionsPage(page),
ccPolicyInfoPage: new CCPolicyInfoPage(page),
contactDetailsPage: new ContactDetailsPage(page),
coverageStatementPage: new CoverageStatementPage(page),
duplicateCheckPage: new DuplicateCheckPage(page),
estimatePage: new EstimatePage(page),
homePage: new HomePage(page),
insuranceCompanyPage: new InsuranceCompanyPage(page),
leadgenHomePage: new LeadgenHomePage(page),
moldingQuestionsPage: new MoldingQuestionsPage(page),
orderConfirmationPage: new OrderConfirmationPage(page),
partQuestionsPage: new PartQuestionsPage(page),
paymentMethodPage: new PaymentMethodPage(page),
policyInfoSubmittedPage: new PolicyInfoSubmittedPage(page),
policyVehiclesPage: new PolicyVehiclesPage(page),
recalibrationInfoPage: new RecalibrationInfoPage(page),
schedulePage: new SchedulePage(page),
serviceLocationPage: new ServiceLocationPage(page),
servicePackagesPage: new ServicePackagesPage(page),
serviceZipPage: new ServiceZipPage(page),
vehicleDamagePage: new VehicleDamagePage(page),
vehicleLookupAddressPage: new VehicleLookupAddressPage(page),
vehicleLookupLicensePage: new VehicleLookupLicensePage(page),
vehiclePartsPage: new VehiclePartQuestionsPage(page),
vehicleSelectionPage: new VehicleSelectionPage(page),
verifyDetailsPage: new VerifyDetailsPage(page),
vinLookupPage: new VinLookupPage(page),
zipLookupPage: new ZipLookupPage(page),
endorsementsPage: new EndorsementsPage(page),
policyDriverPage: new PolicyDriverPage(page)
};
return pages;
}

View file

@ -0,0 +1,12 @@
import { getTestObject as getTestObject_T, prepareTest as prepareTest_T, RuleEngine as RuleEngine_T, TestCase as TestCase_T, TestInfo as TestInfo_T, ITestCase as ITestCase_T} from 'safelite-playwright-core'
import { ITestPages } from './TestPages'
import { ITestData } from './TestData'
export const getTestObject = getTestObject_T<ITestPages, ITestData>;
export const prepareTest = prepareTest_T<ITestPages, ITestData>;
export const RuleEngine = RuleEngine_T<TestCase_T<ITestPages, ITestData>>;
export class TestCase extends TestCase_T<ITestPages, ITestData> {};
export type TestInfo = TestInfo_T<ITestPages, ITestData>;
export type ITestCase = ITestCase_T<ITestPages, ITestData>;

View file

@ -0,0 +1,26 @@
export enum PaymentMethod {
Insurance = 'Insurance',
SelfPay = 'SelfPay'
}
export enum ProgressBarPercentages {
VehicleSelectionPage = '4%',
VehicleDamagePage = '16%',
EstimatePage = '28%',
ServiceZipPage = '32%',
VehicleLookupAddressPage = '32%',
VehicleLookupLicensePage = '32%',
VinLookupPage = '32%',
PartQuestionsPage = '40%',
MoldingQuestionsPage = '40%',
CapabilityQuestionsPage = '40%',
VehiclePartsPage = '40%',
ServicePackagePage = '48%',
InsuranceCompanyPage = '52%',
ServiceLocationPage = '60%',
SchedulePage = '72%',
ContactDetailsPage = '84%',
PaymentMethodPage = '92%',
OrderConfirmationPage = '100%'
}

View file

@ -0,0 +1,29 @@
import { getTestObject } from "framework/Typedefs";
const test = getTestObject()
export type Context = {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
has?(value: unknown): boolean;
};
private?: boolean;
static?: boolean;
addInitializer?(initializer: () => void): void;
};
export type Method = (...args: any[]) => any;
export function step(label: string): Method {
return <T extends Method>(value: T, context: Context): Method => {
return function (this: any, ...args: any[]): Promise<any> {
// console.log(`Method ${value.name} has been decorated`, context, args, label);
return test.step(label, async () => {
return value.call(this, ...args);
});
};
};
}

4076
playwright-tests/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
{
"name": "safelite-nextgen-playwright-tests",
"version": "0.1.0",
"description": "Automated testing framework for Safelite FMG using Playwright with TypeScript",
"directories": {
"test": "tests"
},
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:debug": "playwright test --debug",
"test:smoke": "playwright test --grep @smoke",
"test:report": "playwright test --grep @test_report",
"report": "playwright show-report",
"install": "playwright install",
"install:deps": "playwright install-deps"
},
"keywords": [],
"license": "ISC",
"dependencies": {
"axios": "^1.9.0",
"axios-retry": "^4.5.0"
},
"devDependencies": {
"@faker-js/faker": "^9.8.0",
"@playwright/test": "^1.52.0",
"@types/dotenv-safe": "^8.1.6",
"@types/node": "^22.15.32",
"dotenv-safe": "^9.1.0",
"eslint": "^9.28.0",
"luxon": "^3.6.1",
"ortoni-report": "^3.0.2",
"playwright-jira-reporter": "^1.0.4",
"safelite-playwright-core": "^1.0.26",
"typescript": "^5.8.3"
},
"private": "true"
}

View file

@ -0,0 +1,56 @@
import { Locator, Page } from "@playwright/test";
import { BasePage } from "./BasePage";
import { IPaymentDetails } from 'safelite-playwright-core';
export class AfterpayPage extends BasePage {
readonly page: Page;
readonly submitButton: Locator;
// Login
readonly passwordTextBox: Locator;
// Card details
readonly cardholderNameTextBox: Locator;
readonly cardNumberTextBox: Locator;
readonly expirationDateTextBox: Locator;
readonly cvvTextBox: Locator;
readonly confirmButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.passwordTextBox = page.getByTestId('login-password-input');
this.submitButton = page.getByRole('button', { name: 'Continue' });
this.cardholderNameTextBox = page.getByTestId('payment-method-cardHolderName-input');
this.cardNumberTextBox = page.getByTestId('payment-method-cardNumber-input');
this.expirationDateTextBox = page.getByTestId('payment-method-cardExpiry-input');
this.cvvTextBox = page.getByTestId('payment-method-cardCvv-input');
this.confirmButton = page.getByRole('button', { name: 'Confirm' });
}
async login(password: string) {
await this.passwordTextBox.fill(password);
await this.submitButton.click();
}
async populateCardDetails(paymentDetails: IPaymentDetails) {
await this.cardholderNameTextBox.fill('Roberts'); // TODO: Add cardholder name field
await this.cardNumberTextBox.fill(paymentDetails.cardNumber!);
await this.expirationDateTextBox.fill(`${paymentDetails.expirationMonth}/${paymentDetails.expirationYear}`);
await this.cvvTextBox.fill(paymentDetails.cvv!);
await this.submitButton.click();
}
async executeAfterpayPayment(paymentDetails: IPaymentDetails) {
await this.login(paymentDetails.password!);
await this.populateCardDetails(paymentDetails);
await this.confirmButton.click();
}
}

View file

@ -0,0 +1,21 @@
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export abstract class BaseHomePage extends BasePage {
readonly page: Page;
readonly url: string;
constructor(page: Page) {
super(page);
this.page = page;
}
abstract letsGetStarted(zip?: string): Promise<void>;
// Method to check if this is the correct homepage variant
abstract isCurrentVariant(): Promise<boolean>;
async goto() {
await this.page.goto(process.env['BASE_URL']!);
}
}

View file

@ -0,0 +1,155 @@
import { ICustomerDetails } from 'safelite-playwright-core';
import { Soft } from 'safelite-playwright-core';
import { waitUntil } from 'safelite-playwright-core';
import test, { expect, type Locator, type Page } from '@playwright/test';
import { error } from 'console';
/**
* Base class for all page objects.
* Provides common functionality for interacting with pages.
*/
export class BasePage {
// Common locators
readonly page: Page;
readonly continueButton: Locator;
readonly backButton: Locator;
readonly pageSpinner: Locator;
readonly buttonLoadSpin: Locator;
readonly hamburgerMenu: Locator;
readonly progressBar: Locator;
constructor(page: Page){
this.page = page;
this.continueButton = page.locator('[id="infoBox"]').getByRole('button');
this.backButton = page.locator('[id="infoBox"]').getByRole('link');
this.pageSpinner = page.getByRole('status');
this.buttonLoadSpin = page.getByRole('alert');
this.hamburgerMenu = this.page.getByRole('button', { name: 'Hamburger Menu (modal window)' });
this.progressBar = this.page.locator('.progress-bar-inner');
}
async nextPage() {
await waitUntil(async () => {;
return (await this.continueButton.getAttribute('aria-disabled')) !== 'true'
});
await this.continueButton.click();
await waitUntil(async () => {
let loaderElements = await this.page.locator('button .loader, .buy-loader, timeout, .modal-loader').all();
return !(await Promise.any(loaderElements.map(el => el.isVisible())).catch(() => false));
});
}
async previousPage() {
const startingUrl = this.page.url();
await expect(async () => {
const currentUrl = this.page.url();
if (currentUrl === startingUrl) {
await this.backButton.click({ timeout: 1000 });
}
expect(currentUrl).not.toEqual(startingUrl);
}).toPass({ timeout: 240_000 });
}
async fillAndValidate(element: Locator, value: string){
await expect(async () => {
var text = await element.textContent();
if(text !== value) {
await element.clear();
await element.fill(value);
}
await expect(element).toHaveValue(value);
}).toPass();
}
async clickWithRetry(element, page) {
const timeout = 5000; // milli seconds
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
if (await element.isEnabled()) {
await element.click();
await element.keyboard.press('Tab');
return; // Exit loop if click succeeds
}
} catch (error) {
// Ignore error and retry
}
await page.waitForTimeout(100); // Small delay before retrying
}
console.log(`Failed to click the the element within ${timeout/1000} seconds` + error);
}
async logReferralNumber() {
let mainLocalStorage = JSON.parse(await this.page.evaluate('localStorage.getItem(\'main\')'));
let referralNumber = mainLocalStorage.order.referralNumber as number;
let referralSequenceNumber = mainLocalStorage.order.referralSequenceNumber as number;
if (referralNumber == null) {
for (let i = 1; i <= 20; i++) {
if (!referralNumber == null) break;
await this.page.waitForTimeout(500);
mainLocalStorage = JSON.parse(await this.page.evaluate('localStorage.getItem(\'main\')'));
referralNumber = mainLocalStorage.order.referralNumber as number;
referralSequenceNumber = mainLocalStorage.order.referralSequenceNumber as number;
}
}
await test.step(`Referral Number:${referralNumber} Referral Sequence Number:${referralSequenceNumber}`, async () => {
console.log(`Referral Number:${referralNumber}`);
console.log(`Referral Sequence Number:${referralSequenceNumber}`);
});
}
async mockScheduleResponseForEarlyBird(customerDetails: ICustomerDetails) {
{
const apiUrl = `https://digitalapi.${process.env['NODE_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/schedule/api/v1/schedule/mobile-time-slots`;
await this.page.route(apiUrl, async (route) => {
const response = await route.fetch();
const responseBody = await response.json();
responseBody.days.forEach((day: any) => {
day.timeSlots.forEach((slot: any) => {
if (slot.id.includes("AM")) {
slot.offerPremium = true;
}
});
});
customerDetails.apptDate = responseBody.days.find((day: any) =>
day.timeSlots.some((slot: any) => slot.offerPremium === true)
).date || undefined;
// Mock the response
await route.fulfill({
response,
body: JSON.stringify(responseBody),
});
});
}
}
async validateProgressBar(progressPercentage: string, timeout: number = 60000) {
await waitUntil(async () => {
let loaderElements = await this.page.locator('button .loader, .buy-loader, timeout, .modal-loader').all();
return !(await Promise.any(loaderElements.map(el => el.isVisible())).catch(() => false));
});
// Take a screenshot of the page before validating the progress bar
await this.page.screenshot({ path: `test-results\\ortoni-data\\progress-bar-${Date.now()}.png`, fullPage: true });
// Wait until the progress bar element is attached and visible
const actualProgressPercentage = await this.progressBar.evaluate(
async (element) => {
await new Promise(resolve => setTimeout(resolve, 200));
return element.style.width || "Not Found";
}
);
Soft.expect(actualProgressPercentage).toBe(progressPercentage);
console.log(`Progress Bar Percentage: Actual - ${actualProgressPercentage} vs Expected - ${progressPercentage}`);
}
}

View file

@ -0,0 +1,86 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { IClaimDetails, ICustomerDetails } from 'safelite-playwright-core';
import { InsuranceBasePage } from './InsuranceBasePage';
import { STATE_ABBREVIATIONS } from 'safelite-playwright-core';
import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step';
export class CCPolicyInfoPage extends InsuranceBasePage {
readonly policyNumber: Locator;
readonly policyZip: Locator;
readonly damageDate: Locator;
readonly damageCause: Locator;
readonly phoneNumber: Locator;
readonly city: Locator;
readonly state: Locator;
readonly hasAdditionalDamage: Locator;
readonly isRentalVehicle: Locator;
readonly isOtherPartyResponsibleForCoverage: Locator;
readonly lossLocation: Locator;
constructor(page: Page) {
super(page);
this.policyNumber = page.locator('#PolicyNumber');
this.policyZip = page.locator('#PolicyZip');
this.damageDate = page.locator('#LossDate');
this.damageCause = page.locator('#LossCause');
this.phoneNumber = page.locator('#PrimaryPhone');
this.city = page.locator('#LossLocCity');
this.state = page.locator('#LossLocState');
this.hasAdditionalDamage = page.locator('#HasAdditionalDamage');
this.isRentalVehicle = page.locator('#IsRentalVehicle');
this.isOtherPartyResponsibleForCoverage = page.locator('#IsOtherPartyResponsibleForCoverage');
this.lossLocation = page.locator('#LossLocationType');
}
async hasCityInfo(): Promise<boolean> {
await expect.soft(this.damageCause).toBeVisible();
return this.city.isVisible();
}
private getStateAbbreviation(stateName: string): string {
const abbreviation = STATE_ABBREVIATIONS[stateName];
if (!abbreviation) {
throw new Error(`State "${stateName}" not found in mapping`);
}
return abbreviation;
}
async populatePage(customerDetails: ICustomerDetails, claimDetails: IClaimDetails, isFillCityInfo: boolean): Promise<void> {
try {
await this.policyNumber.fill(claimDetails.policyNumber);
await this.policyZip.fill(claimDetails.policyZip || customerDetails.address.postalCode);
await this.damageDate.click();
await this.damageDate.fill(claimDetails.damageDate);
await this.damageCause.selectOption(claimDetails.damageCause);
await this.damageCause.press('Tab');
await this.phoneNumber.fill(customerDetails.phoneNumber);
if (isFillCityInfo) {
await this.city.fill(customerDetails.address.city);
}
if (await this.state.isVisible()) {
const stateAbbreviation = this.getStateAbbreviation(customerDetails.address.state);
await this.state.selectOption({ value: stateAbbreviation });
}
if (claimDetails.lossLocation) {
await this.lossLocation.selectOption(claimDetails.lossLocation);
}
} catch (error) {
console.error('Error populating policy info page:', error);
throw error;
}
}
@step("CCPolicyInfoPage >> Fill out claim information")
async handleCCPolicyInfoPage(testData: Partial<ITestData>) {
const { customerDetails, claimDetails} = testData;
await this.populatePage(customerDetails!, claimDetails!, await this.hasCityInfo());
await this.nextPage();
}
}

View file

@ -0,0 +1,27 @@
import { Page } from "@playwright/test";
import { PartQuestionsPage } from "./PartQuestionPage";
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export default class CapabilityQuestionsPage extends PartQuestionsPage {
constructor(page: Page) {
super(page);
}
@step("CapabilityQuestionsPage >> Select Capability Questions: ")
async handleCapabilityQuestionsPage(testCase: Partial<ITestData>) {
const { capabilityQuestions } = testCase;
await this.validateProgressBar(ProgressBarPercentages.CapabilityQuestionsPage);
// Validate the capability questions are on the page
await this.validatePartQuestions(capabilityQuestions!);
// Select the capability question responses
await this.selectPartQuestionResponses(capabilityQuestions!);
// Proceed to the next page
await this.nextPage();
}
}

View file

@ -0,0 +1,53 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ICustomerDetails } from 'safelite-playwright-core';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export class ContactDetailsPage extends BasePage {
readonly page: Page;
// Contact details form
// TODO: Check if we can consolidate
readonly firstNameTextBox: Locator;
readonly lastNameTextBox: Locator;
readonly emailAddressTextBox: Locator;
readonly phoneNumberTextBox: Locator;
readonly notesTextBox: Locator;
constructor(page: Page) {
super(page);
this.page = page;
// Contact details form
this.firstNameTextBox = this.page.getByRole('textbox', { name: 'First name' });
this.lastNameTextBox = this.page.getByRole('textbox', { name: 'Last name' });
this.emailAddressTextBox = this.page.getByRole('textbox', { name: 'Email address' });
this.phoneNumberTextBox = this.page.getByRole('textbox', { name: 'Phone number' });
this.notesTextBox = this.page.getByRole('textbox', { name: 'Notes' });
}
async enterContactDetails(customerDetails: ICustomerDetails) {
await this.firstNameTextBox.fill(customerDetails.firstName);
await this.lastNameTextBox.fill(customerDetails.lastName);
await this.emailAddressTextBox.fill(customerDetails.email);
await this.phoneNumberTextBox.fill(customerDetails.phoneNumber);
}
async fillNotes(notes?: string) {
if (notes) {
await this.notesTextBox.fill(notes);
}
}
@step("ContactDetailsPage >> Enter contact details: ")
async handleContactDetailsPage(testData: Partial<ITestData>) {
const { customerDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.ContactDetailsPage);
await this.enterContactDetails(customerDetails!);
await this.nextPage();
}
}

View file

@ -0,0 +1,71 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { InsuranceBasePage } from './InsuranceBasePage';
import { IClaimDetails } from 'safelite-playwright-core';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class CoverageStatementPage extends InsuranceBasePage {
readonly page: Page;
readonly scheduleOnlineButton: Locator;
readonly cancelMyClaimButton: Locator;
readonly deductibleAmount: Locator;
readonly verifyingCoverageText: Locator;
readonly noCompText: Locator;
readonly continueButton: Locator; // For ITAC/NoComp
constructor(page: Page) {
super(page);
this.page = page;
this.scheduleOnlineButton = this.page.getByText('Continue to schedule online');
this.cancelMyClaimButton = this.page.getByText('Cancel my claim');
this.deductibleAmount = this.page.getByRole('heading', { name: '$' }).locator('span');
this.verifyingCoverageText = this.page.getByRole('heading', { name: 'We\'re verifying your coverage' });
this.noCompText = this.page.getByRole('heading', { name: 'Your policy doesn\'t cover this service' });
this.continueButton = page.getByRole('button', { name: 'Continue' });
}
async scheduleOnline(){
await this.scheduleOnlineButton.click();
}
async cancelMyClaim(){
await this.cancelMyClaimButton.click();
}
async validateDeductibleAmount(claimDetails: IClaimDetails) {
// Format the deductible number to have 2 decimal places
const formattedDeductible = typeof claimDetails.policyDeductible === 'number'
? claimDetails.policyDeductible.toFixed(2)
: Number(claimDetails.policyDeductible).toFixed(2);
// Add a regex to match the formatted value with commas
const expectedDeductibleRegex = formattedDeductible.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// Narrow down the selector to target the exact element
const deductibleElement = this.page.locator(
"span.deductible-text-black[data-bind='text: deductibleFormatted']"
);
const unverifiedDeductibleElement = this.verifyingCoverageText;
// Check if the locator is visible before running the expectation
if (await deductibleElement.isVisible()) {
// Check if the page contains the properly formatted deductible amount
await expect(deductibleElement).toContainText(`$${expectedDeductibleRegex}`);
}
if (await unverifiedDeductibleElement.isVisible() || await this.noCompText.isVisible()) {
// Click on continue button if unverified header is visible
await this.continueButton.click();
}
}
@step("CoverageStatementPage >> Next page: ")
async handleCoverageStatementPage(testData: Partial<ITestData>) {
const { claimDetails } = testData;
await this.validateDeductibleAmount(claimDetails!);
await this.nextPage();
}
}

View file

@ -0,0 +1,26 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { InsuranceBasePage } from './InsuranceBasePage';
import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step';
export class DuplicateCheckPage extends InsuranceBasePage {
readonly page: Page;
readonly newClaimButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.newClaimButton = page.locator('a').filter({ hasText: 'Start a new claim Start a new' });
}
async startNewClaim(){
await this.newClaimButton.click();
await this.page.waitForLoadState();
}
@step("DuplicateCheckPage >> Start a new claim: ")
async handleDuplicateCheckPage(testCase: Partial<ITestData>) {
await this.startNewClaim();
await this.nextPage();
}
}

View file

@ -0,0 +1,64 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IEndorsementDetails } from 'safelite-playwright-core';
import { EndorsementType } from 'safelite-playwright-core';
import { InsuranceBasePage } from './InsuranceBasePage';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class EndorsementsPage extends InsuranceBasePage {
readonly page: Page;
readonly educatorYesButton: Locator;
readonly educatorNoButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.educatorYesButton = this.page.getByRole('link', { name: 'Yes' });
this.educatorNoButton = this.page.getByRole('link', { name: 'Yes' });
}
async verifyEndorsements(endorsements: IEndorsementDetails[]) {
for (const endorsement of endorsements) {
switch(endorsement.endorsementType) {
case EndorsementType.Educator:
if (endorsement.isOnPolicy) {
await expect.soft(this.educatorYesButton).toBeAttached();
} else {
await expect.soft(this.educatorYesButton).not.toBeAttached();
}
break;
case EndorsementType.EmployeeParking:
// TODO: Implement
break;
}
}
}
async selectEndorsements(endorsements: IEndorsementDetails[]) {
for (const endorsement of endorsements) {
if (endorsement.isOnPolicy) {
switch (endorsement.endorsementType) {
case EndorsementType.Educator:
if (endorsement.isClickYes) {
await this.educatorYesButton.click();
} else {
await this.educatorNoButton.click();
}
break;
case EndorsementType.EmployeeParking:
// TODO: Implement
break;
}
}
}
}
@step("EndorsementsPage >> Select endorsements: ")
async handleEndorsementsPage(testData: Partial<ITestData>) {
const { endorsements } = testData;
await this.verifyEndorsements(endorsements!);
await this.selectEndorsements(endorsements!);
await this.nextPage();
}
}

View file

@ -0,0 +1,78 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { VehicleLookupType } from 'safelite-playwright-core';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
import { IVehicleDetails } from 'safelite-playwright-core';
import { VinLookupPage } from './VinLookupPage';
import { VehicleLookupAddressPage } from './VehicleLookupAddressPage';
import { VehicleLookupLicensePage } from './VehicleLookupLicensePage';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class EstimatePage extends BasePage {
readonly page: Page;
readonly vinLookupButton: Locator;
readonly addressLookupButton: Locator;
readonly licenseLookupButton: Locator;
readonly vinLookupPage: VinLookupPage;
readonly zipLookupButton: Locator;
readonly vehicleLookupAddressPage: VehicleLookupAddressPage;
readonly vehicleLookupLicensePage: VehicleLookupLicensePage;
constructor(page: Page) {
super(page);
this.page = page;
this.vinLookupButton = page.locator('label').filter({ hasText: 'Provide my VIN' }).locator('div');
this.zipLookupButton = page.locator('label').filter({ hasText: 'I\'d rather not share my VIN' }).locator('div');
this.addressLookupButton = page.getByLabel('Provide my home address', { exact: true });
this.licenseLookupButton = page.getByLabel('Provide my license plate #', { exact: true });
this.vinLookupPage = new VinLookupPage(page);
}
async vehicleLookup(vehicleDetails: IVehicleDetails) {
switch (vehicleDetails.vehicleLookupType) {
case VehicleLookupType.Address:
await this.selectAddressLookup();
await this.nextPage();
break;
case VehicleLookupType.LicensePlateNumber:
await this.selectLicenseLookup();
await this.nextPage();
break;
case VehicleLookupType.Vin:
await this.selectVinLookup();
await this.nextPage();
break;
case VehicleLookupType.Zip:
await this.selectZipLookup();
await this.nextPage();
break;
default:
console.error('EstimatePage >> DATA ISSUE: VehicleLookupType not provided');
break;
}
}
async selectVinLookup(){
await this.vinLookupButton.click();
}
async selectAddressLookup(){
await this.addressLookupButton.click();
}
async selectLicenseLookup(){
await this.licenseLookupButton.click();
}
async selectZipLookup(){
await this.zipLookupButton.click();
}
@step("EstimatePage >> Select Lookup Type")
async handleEstimatePage(testData: Partial<ITestData>) {
const { vehicleDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.EstimatePage);
await this.vehicleLookup(vehicleDetails!);
}
}

View file

@ -0,0 +1,79 @@
import test, { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class HomePage extends BasePage {
readonly page: Page;
readonly letsGetStartedButton: Locator;
readonly cusmodalPopup: Locator;
readonly closePopupButton: Locator;
//Quick Quote (LeadGen) Form
readonly yearDropdown: Locator;
readonly makeDropdown: Locator;
readonly modelDropdown: Locator;
readonly styleDropdown: Locator;
readonly damageTypeDropdown: Locator;
readonly zipCodeTextBox: Locator;
readonly phoneNumberTextBox: Locator;
readonly paymentOptionDropdown: Locator;
readonly viewQuoteButton: Locator;
//Zip Entry
readonly enterServiceZipTextBox: Locator;
readonly zipEntryLetsGetStartedButton: Locator;
readonly getQuoteAndScheduleButton: Locator;
url = process.env['BASE_URL']!;
constructor(page: Page) {
super(page);
this.page = page;
this.letsGetStartedButton = this.page.locator('a.btn.btn-primary.ghost');
this.cusmodalPopup = this.page.locator('#Cusmodalpopup');
this.closePopupButton = this.page.getByRole('button', { name: '×' });
//Quick Quote (LeadGen) Form
this.yearDropdown = this.page.locator('#year');
this.makeDropdown = this.page.locator('#make');
this.modelDropdown = this.page.locator('#model');
this.styleDropdown = this.page.locator('#style');
this.damageTypeDropdown = this.page.locator('#damage');
this.zipCodeTextBox = this.page.locator('#zipcodeinput');
this.phoneNumberTextBox = this.page.locator('#phonenumberinput');
this.paymentOptionDropdown = this.page.locator('#insuranceCheckbox');
this.viewQuoteButton = this.page.locator('#ctaSubmit');
//Zip Entry
this.enterServiceZipTextBox = this.page.locator('#zipCodeTextbox');
this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextboxButton');
this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' });
}
async goto() {
await this.page.goto(process.env['BASE_URL']!);
}
async isCurrentVariant(): Promise<boolean> {
return !(await this.page.getByPlaceholder('Enter service ZIP code').isVisible());
}
async letsGetStarted(zip: string, enterFunnelWithZip: boolean) {
if (enterFunnelWithZip) {
await this.letsGetStartedButton.evaluate((element, zip) => {
const currentHref = element.getAttribute('href') || '';
element.setAttribute('href', `${currentHref}&zipCode=${zip}`);
}, zip);
}
// Wait until modal pop up opens and close pop up
await this.page.waitForTimeout(5000);
if (await this.cusmodalPopup.isVisible()) {
await this.closePopupButton.click();
}
await this.letsGetStartedButton.click();
}
}

View file

@ -0,0 +1,142 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class InsuranceBasePage extends BasePage {
readonly cookieContinueButton: Locator;
readonly continueWithSafeliteButton: Locator;
constructor(page: Page) {
super(page);
// Targeting continue buttons with multiple selectors
this.cookieContinueButton = page.locator([
'button:has-text("Continue")',
'button#navNext.btn-success',
'button[name="navNext"][type="submit"]',
'.btn-success:has-text("Continue")'
].join(', '));
// Targeting "Continue with Safelite" buttons
this.continueWithSafeliteButton = page.locator([
'button:has-text("Continue with Safelite")',
'button#navNext:has-text("Safelite")',
'.btn-success:has-text("Continue with Safelite")'
].join(', '));
}
/**
* Navigates to the next page in the insurance flow
* Overrides the BasePage nextPage() method to handle insurance modal buttons
*/
async nextPage() {
const startingUrl = this.page.url();
let attemptCount = 0;
const maxAttempts = 3;
while (attemptCount < maxAttempts) {
try {
// Try navNext ID button first
const navNextButton = this.page.locator('#navNext:visible');
if (await navNextButton.count() > 0) {
await navNextButton.click();
await this.page.waitForTimeout(500);
}
// Try button with Continue text
else if (await this.cookieContinueButton.isVisible({ timeout: 2000 })) {
await this.cookieContinueButton.click();
await this.page.waitForTimeout(500);
}
// Try button with Continue with Safelite text
else if (await this.continueWithSafeliteButton.isVisible({ timeout: 2000 })) {
await this.continueWithSafeliteButton.click();
await this.page.waitForTimeout(500);
}
// Try any button that might work
else {
const possibleButtons = [
'button.btn-success',
'button[type="submit"]',
'button[name="navNext"]',
'.button-content',
'button:has-text("Continue")',
'button:has-text("Next")'
];
let buttonFound = false;
for (const selector of possibleButtons) {
const button = this.page.locator(selector);
if (await button.count() > 0 && await button.first().isVisible()) {
await button.first().click();
buttonFound = true;
await this.page.waitForTimeout(500);
break;
}
}
if (!buttonFound) {
await super.nextPage();
return;
}
}
// Check if URL changed
const currentUrl = this.page.url();
if (currentUrl !== startingUrl) {
break;
}
// Try Enter key on second attempt
if (attemptCount === 1) {
await this.page.keyboard.press('Enter');
await this.page.waitForTimeout(1000);
if (this.page.url() !== startingUrl) {
break;
}
}
attemptCount++;
} catch (error) {
attemptCount++;
if (attemptCount >= maxAttempts) {
await super.nextPage();
return;
}
await this.page.waitForTimeout(1000);
}
}
// Wait for spinner to disappear
try {
await expect(this.pageSpinner).toHaveCount(0, { timeout: 60000 });
} catch (error) {
// Continue if spinner check fails
}
// Final navigation stability check
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
// Continue if networkidle times out
});
}
/**
* Direct method to click Continue with Safelite button
*/
async clickContinueWithSafelite() {
try {
// Try direct ID selector first
if (await this.page.$('#navNext:has-text("Safelite")') !== null) {
await this.page.click('#navNext');
} else if (await this.continueWithSafeliteButton.isVisible({ timeout: 2000 })) {
await this.continueWithSafeliteButton.click();
} else {
await this.page.click('button:has-text("Continue with Safelite")');
}
return true;
} catch (error) {
return false;
}
}
}

View file

@ -0,0 +1,54 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export class InsuranceCompanyPage extends BasePage {
readonly page: Page;
readonly insuranceInputBox: Locator;
readonly payOnMyOwnLink: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.insuranceInputBox = this.page.getByRole('textbox', { name: 'Enter your insurance company' });
this.payOnMyOwnLink = this.page.getByRole('link', { name: 'Pay on my own' });
}
async enterInsuranceCompany(company: string): Promise<void> {
await this.insuranceInputBox.fill(company);
try {
// First attempt: Try to find and click an exact match
await this.page.getByText(company, { exact: true })
.locator('xpath=ancestor::div[contains(@class, "ui-menu-item-wrapper")]')
.click({ timeout: 2000 }); // Short timeout for quick fallback
} catch (error) {
// Fallback: If exact match fails, select the first option containing the text
const options = this.page.locator('div.ui-menu-item-wrapper', { hasText: company });
const count = await options.count();
if (count === 0) {
throw new Error(`No insurance company options found containing "${company}"`);
} else if (count === 1) {
// If only one option, click it
await options.click();
} else {
// If multiple options, click the first one
await options.first().click();
console.log(`Selected first match for "${company}" from ${count} options`);
}
}
}
@step("InsuranceCompanyPage >> Enter insurance company: ")
async handleInsuranceCompanyPage(testData: Partial<ITestData>) {
const { claimDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.InsuranceCompanyPage);
await this.enterInsuranceCompany(claimDetails!.client!);
await this.nextPage();
}
}

View file

@ -0,0 +1,34 @@
import test, { Locator, Page } from '@playwright/test';
import { BasePage } from './BasePage';
// This file is an example of a Page Object Model
export class LeadgenHomePage extends BasePage {
readonly page: Page;
readonly letsGetStartedButton: Locator;
readonly zipTextbox: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.letsGetStartedButton = this.page.locator('#zipCodeTextboxButton');
this.zipTextbox = this.page.getByPlaceholder('Enter service ZIP code');
}
async goto() {
await this.page.goto(process.env['BASE_URL']!);
}
async letsGetStarted(zip: string) {
await this.zipTextbox.fill(zip);
await this.letsGetStartedButton.click();
}
async isCurrentVariant(): Promise<boolean> {
return await this.zipTextbox.isVisible();
}
}

View file

@ -0,0 +1,130 @@
import { type Locator, type Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
import { IAlertFlags, TestSuccessAlert } from 'safelite-playwright-core';
import { VehicleLookupType } from 'safelite-playwright-core';
export class LookupPage extends BasePage {
protected serviceZipTextBox: Locator;
protected emailTextBox: Locator;
protected unserviceableZipAlertMessage: Locator;
protected invalidZipAlertMessage: Locator;
protected vinNotFoundAlertMessage: Locator;
constructor(page: Page) {
super(page);
this.initializeLocators(page);
}
protected initializeLocators(page: Page) {
this.serviceZipTextBox = page.getByRole('textbox', { name: 'Service ZIP' });
this.emailTextBox = page.getByRole('textbox', { name: 'Mobile phone number or email' });
this.unserviceableZipAlertMessage = this.page.locator('div.alert-danger.widget-name-widgetUndefined', { hasText: "We don't currently provide service within " });
this.invalidZipAlertMessage = this.page.getByText('ZIP code is not valid.Please');
this.vinNotFoundAlertMessage = this.page.locator('div.alert-danger.widget-name-AlertVinNotFoundWidget');
}
async enterZip(zip: string) {
await this.serviceZipTextBox.fill(zip);
}
async enterEmail(email: string | undefined) {
if (email != undefined) {
await this.emailTextBox.fill(email);
}
}
async checkForInvalidZipAlertMessage() {
await this.continueButton.click();
await expect(this.invalidZipAlertMessage).toBeVisible();
const alertMessage = await this.invalidZipAlertMessage.textContent();
console.log(`Alert encountered: ${alertMessage}`);
const expectedAlertMessage = `ZIP code is not valid.Please re-enter your correct service ZIP.`;
expect(alertMessage).toContain(expectedAlertMessage);
}
async checkForUnserviceableZipAlertMessage(zipCode: string) {
await this.continueButton.click();
await expect(this.unserviceableZipAlertMessage).toBeVisible();
const alertMessage = await this.unserviceableZipAlertMessage.textContent();
console.log(`Alert encountered: ${alertMessage}`);
const expectedAlertMessage = `We don't currently provide service within ${zipCode}.Please enter a different ZIP code where you'd like service so we can get you scheduled.`;
expect(alertMessage).toContain(expectedAlertMessage);
}
async checkForVinNotFoundAlertMessage(lookupType: VehicleLookupType) {
await this.continueButton.click();
await expect(this.vinNotFoundAlertMessage).toBeVisible();
const alertMessage = await this.vinNotFoundAlertMessage.textContent() || '';
console.log(`Alert encountered: ${alertMessage}`);
// Map lookup type enum to string
let lookupTypeString: string;
switch(lookupType) {
case VehicleLookupType.Address:
lookupTypeString = 'address';
break;
case VehicleLookupType.LicensePlateNumber:
lookupTypeString = 'license plate';
break;
case VehicleLookupType.Vin:
lookupTypeString = 'VIN';
break;
case VehicleLookupType.Zip:
lookupTypeString = 'ZIP code';
break;
default:
lookupTypeString = '';
}
// Map of expected messages by lookup type
const expectedMessages = {
'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.",
'license plate': "Your license plate didnt return a VIN match.Please re-enter the information above or provide your VIN in a different way.",
'VIN': "Your VIN didnt return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.",
'ZIP code': "Your ZIP code didn't return a match.Please re-enter the information above or provide your VIN in a different way."
};
if (lookupTypeString && expectedMessages[lookupTypeString]) {
// If lookup type is provided and has a defined message, check for exact match
console.log(`Checking for message: "${expectedMessages[lookupTypeString]}"`);
expect(alertMessage).toContain(expectedMessages[lookupTypeString]);
console.log(`VIN not found alert validation passed for ${lookupTypeString}`);
} else {
// Fall back to checking for key phrases if no specific lookup type match
const containsVinMatch = alertMessage.includes("didn't return a VIN match") ||
alertMessage.includes("didn't return a match");
const containsReenterInfo = alertMessage.includes("re-enter the information");
const containsProvideVin = alertMessage.includes("provide your VIN");
expect(containsVinMatch).toBeTruthy();
expect(containsReenterInfo).toBeTruthy();
expect(containsProvideVin).toBeTruthy();
console.log("VIN not found alert validation passed with generic check");
}
}
async handleZipValidation(zip: string, lookupType: VehicleLookupType, alertFlags?: IAlertFlags): Promise<boolean> {
// Only proceed with validation if alertFlags is provided
if (alertFlags) {
if (alertFlags.isUnserviceableZip) {
await this.checkForUnserviceableZipAlertMessage(zip);
throw new TestSuccessAlert('Unserviceable ZIP validation successful.');
} else if (alertFlags.isInvalidZip) {
await this.checkForInvalidZipAlertMessage();
throw new TestSuccessAlert('Invalid ZIP validation successful.');
} else if (alertFlags.isVinNotFound) {
await this.checkForVinNotFoundAlertMessage(lookupType);
throw new TestSuccessAlert('Vin not found validation successful.');
} else {
}
}
// If no alert flags or no matching condition, just continue
await this.nextPage();
return true;
}
}

View file

@ -0,0 +1,22 @@
import { Page } from "@playwright/test";
import { PartQuestionsPage } from "./PartQuestionPage";
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export default class MoldingQuestionsPage extends PartQuestionsPage {
constructor(page: Page) {
super(page);
}
@step("MoldingQuestionsPage >> Select Molding Questions: ")
async handleMoldingQuestionsPage(testCase: Partial<ITestData>) {
const { moldingQuestions } = testCase;
await this.validateProgressBar(ProgressBarPercentages.MoldingQuestionsPage);
await this.validatePartQuestions(moldingQuestions!);
await this.selectPartQuestionResponses(moldingQuestions!);
await this.nextPage();
}
}

View file

@ -0,0 +1,170 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { getTestObject } from 'framework/Typedefs';
import { ServicePackage, PaymentType, ServiceLocation, Soft } from 'safelite-playwright-core';
import { PaymentMethod, ProgressBarPercentages } from "framework/localTypes/Enums";
import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step';
const test = getTestObject();
export class OrderConfirmationPage extends BasePage {
readonly page: Page;
readonly serviceText: Locator;
readonly emailText: Locator;
readonly apptDateText: Locator;
readonly amountDueText: Locator;
readonly viewCartButton: Locator;
readonly deductibleText: Locator;
readonly subtotalText: Locator;
readonly finalAmountDue: Locator;
readonly cartServicePackageText: Locator;
readonly winshieldWiper: Locator;
readonly rainDefense: Locator;
readonly appointmentSummarySection: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.serviceText = this.page.locator('p', {
hasText: /to service your/
});
this.emailText = this.emailText = this.page.getByText('A confirmation email was sent');
this.apptDateText = this.page.locator('[class="scheduleText"]');
this.amountDueText = this.page.getByLabel('expand cart');
this.viewCartButton = this.page.locator('#cart-dropdown-head');
this.deductibleText = this.page.locator('.deductible');
this.subtotalText = this.page.locator('.sub-total');
this.finalAmountDue = this.page.locator('div.amount-due');
this.cartServicePackageText = this.cartServicePackageText = this.page.locator('.cart-panel');
this.appointmentSummarySection = this.page.locator('.main .scheduleText, .main .add-to-calendar-text, .main .appointment-text, .main .duration-text-block');
}
async validateOrderConfirmationPage(testData: Partial<ITestData>) {
// Destructure data we use
const { vehicleDetails, customerDetails, servicePackage, isCashInsuranceFlow,
isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified } = testData;
await this.serviceText.waitFor({ state: "visible" });
Soft.expect((await this.getActualAppointmentSummary()).map(item => item.toLowerCase())).toEqual((await this.getExpectedAppointmentSummary(testData)).map(item => item.toLowerCase()));
// Grab text
const emailTextValue = await this.emailText.textContent();
const servicePackageValue = await this.cartServicePackageText.textContent();
const amountDueValue = await this.amountDueText.textContent();
// const deductibleTextValue = await this.deductibleText.textContent();
const subtotalTextValue = await this.subtotalText.textContent();
const finalAmountDueValue = await this.finalAmountDue.textContent();
// Extract service package price
const servicePackageAmt = Number.parseFloat(servicePackageValue!.split('$')[1].replaceAll(',', ''));
// General Validations
Soft.expect(emailTextValue?.toLowerCase()).toContain(customerDetails!.email);
// Service package validations
await Soft.expect(servicePackageValue).toContain(`${servicePackage}`)
if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) {
Soft.expect(servicePackageValue).toContain('New wiper blades');
}
if (servicePackage === ServicePackage.Premium) {
Soft.expect(servicePackageValue).toContain('Rain Repel Treatment');
}
// Promo Code Validation
if (paymentDetails!.promoCode) {
expect.soft(servicePackageValue).toContain(`Promo code ${paymentDetails!.promoCode} applied`);
};
if (paymentMethod === PaymentMethod.SelfPay || (paymentDetails?.paymentType && paymentDetails?.paymentType !== PaymentType.PayWithInsurance)) {
// Cart validation for non insurance users
// Extract numbers
const amountDueAmt = Number.parseFloat(amountDueValue!.split('$')[1].replaceAll(',', ''));
// const deductibleAmt = deductibleTextValue? Number.parseFloat(deductibleTextValue.split('$')[1].replaceAll(',', '')): 0;
const subtotalAmt = Number.parseFloat(subtotalTextValue!.split('$')[1].replaceAll(',', ''));
subtotalTextValue?.replaceAll(',', '')
const finalAmountDueAmt = Number.parseFloat(finalAmountDueValue!.split('$')[1].replaceAll(',', ''));
Soft.expect(subtotalAmt).toBeGreaterThan(0);
// Soft.expect(deductibleAmt).toEqual(0);
if ((paymentDetails!.paymentType === PaymentType.PayAtService || isCashInsuranceFlow) && (servicePackageAmt > 0)) {
// Verify amount due > 0
Soft.expect(amountDueAmt).toBeGreaterThan(0);
Soft.expect(finalAmountDueAmt).toBeGreaterThan(0);
if (isPolicyUnverified && PaymentType.PayWithInsurance){
Soft.expect(finalAmountDueAmt).toContain('Verifying coverage')
}
} else {
// Verify amount due 0
Soft.expect(amountDueAmt).toEqual(0);
Soft.expect(finalAmountDueAmt).toEqual(0);
}
}
}
async getFormattedAppointmentDate(appointmentDate: string) {
// Parse the original date
let parsedAppointmentDate = new Date(`${appointmentDate}` + 'T00:00:00');
// Format the new date as a string
let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
return updatedAppointmentDate;
}
async logOrderNumber() {
const sessionStorage = JSON.parse(await this.page.evaluate('sessionStorage.getItem(\'submittedState\')'));
return sessionStorage.order.workOrderNumber;
}
async getActualAppointmentSummary(): Promise<string[]> {
let appointmentSummary: string[] = [];
for (let element of await this.appointmentSummarySection.all()){
let text = (await element.textContent() || "").replaceAll(/\u00A0|&nbsp;/g, ' ');
appointmentSummary.push(text.trim());
}
return appointmentSummary;
}
async getExpectedAppointmentSummary(testData: Partial<ITestData>): Promise<string[]> {
const { appointmentDetails, customerDetails, vehicleDetails } = testData;
let appointmentSummary: string[] = [];
// Format the expected appointment date
const formattedExpectedAppointmentDate = await this.getFormattedAppointmentDate(customerDetails!.apptDate!);
appointmentSummary.push(
appointmentDetails?.serviceLocation == ServiceLocation.Mobile
? formattedExpectedAppointmentDate + `${customerDetails!.apptTime?.replace("arriving between", "Between").replaceAll(":00", "")}`
: appointmentDetails?.serviceLocation == ServiceLocation.InShop
? formattedExpectedAppointmentDate + `${customerDetails!.apptTime}`
: formattedExpectedAppointmentDate + "Drop off before 9:30 AM"
);
appointmentSummary.push("Add to calendar");
appointmentSummary.push(
appointmentDetails?.serviceLocation == ServiceLocation.Mobile
? appointmentDetails?.serviceAddress
? ("We're coming to you at" + appointmentDetails.serviceAddress.street + ", " + appointmentDetails.serviceAddress.city + ", " + appointmentDetails.serviceAddress.state + " " + appointmentDetails.serviceAddress.postalCode + "to service your " + `${vehicleDetails!.year} ${vehicleDetails!.make} ${vehicleDetails!.model}`)
: ""
: appointmentDetails?.shopAddress
? ("You're going to a Safelite shop at" + appointmentDetails.shopAddress + "to service your " + `${vehicleDetails!.year} ${vehicleDetails!.make} ${vehicleDetails!.model}`)
: "");
appointmentSummary.push("Duration: " + customerDetails!.apptDuration!);
return appointmentSummary;
}
@step("OrderConfirmationPage >> Validate order")
async verifyOrderConfirmationPage(testData: Partial<ITestData>) {
await this.page.waitForURL(new RegExp('(.+)confirmation'), {timeout: 60000});
await this.validateProgressBar(ProgressBarPercentages.OrderConfirmationPage);
await this.validateOrderConfirmationPage(testData);
const workOrderNumber = await this.logOrderNumber();
await test.step(`Session Storage Work Order Number: ${workOrderNumber}`, async () => {
console.log(`Session Storage Work Order Number: ${workOrderNumber}`);
});
}
}

View file

@ -0,0 +1,71 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPartQuestion } from 'safelite-playwright-core';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export class PartQuestionsPage extends BasePage {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
async getLocalStorage() {
// Retrieve local storage entries
const localStorageData = await this.page.evaluate(() => {
const data: Record<string, string> = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
data[key] = localStorage.getItem(key) || '';
}
}
return data;
});
console.log('Local Storage Data:', localStorageData);
return localStorageData;
}
async saveStorageState(filePath: string) {
// Save the current browser context's storage state
await this.page.context().storageState({ path: filePath });
console.log(`Storage state saved to ${filePath}`);
}
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}"]`);
const partQuestionOptionButton = parentobject.locator(`[buttonlabel="${pq.optionToSelect}"]`);
await partQuestionOptionButton.click();
if (pq.secondaryQuestionOptionToSelect != null) {
const secondaryQuestionButton = parentobject.getByText(`${pq.secondaryQuestionOptionToSelect}`);
await secondaryQuestionButton.click();
}
}
}
@step("PartQuestionsPage >> Select Vehicle Part Question Responses")
async handlePartQuestionsPage(testData: Partial<ITestData>) {
const { partQuestions } = testData;
await this.validateProgressBar(ProgressBarPercentages.PartQuestionsPage);
await this.validatePartQuestions(partQuestions!);
await this.selectPartQuestionResponses(partQuestions!);
await this.nextPage();
}
}

View file

@ -0,0 +1,492 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPaymentDetails, Soft, waitUntil } from 'safelite-playwright-core';
import { AppointmentTimeslot, ServiceLocation, PaymentType, ServicePackage, VehicleDamage } from 'safelite-playwright-core';
import { PaymentMethod, ProgressBarPercentages } from 'framework/localTypes/Enums';
import { PaymentPage } from './PaymentPage';
import { AfterpayPage } from './AfterpayPage';
import { PaypalPage } from './PaypalPage';
import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step';
export class PaymentMethodPage extends BasePage {
readonly page: Page;
readonly payAtServiceButton: Locator;
// readonly paypalButton: Locator;
// readonly creditCardButton: Locator;
readonly payNowButton: Locator;
readonly payWithInsuranceButton: Locator;
readonly payInFourButton: Locator;
readonly amountDueTextField: Locator;
readonly amountDueDropDown: Locator;
readonly appointmentDetailsSection: Locator;
readonly appointmentDetailsDropdown: Locator;
readonly subtotalAmountTextField: Locator;
readonly submitButton: Locator;
readonly recalibrationCheckbox: Locator;
readonly paymentPage: PaymentPage;
readonly paypalPage: PaypalPage;
// Payment detail page validation locators
readonly reviewTable: Locator;
readonly vehicleSection: Locator;
readonly damageSection: Locator;
readonly serviceDetailsSection: Locator;
readonly serviceLocationSection: Locator;
readonly appointmentDateSection: Locator;
readonly contactDetailsSection: Locator;
readonly cartPanelDetails: Locator;
readonly subtotalText: Locator;
readonly finalAmountDueText: Locator;
readonly glassServiceText: Locator;
readonly wiperBladesText: Locator;
readonly rainDefenseText: Locator;
readonly deductibleText: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.payAtServiceButton = this.page.locator('[buttonlabel="Pay at my appointment"]'); //this.page.getByText('Pay at time of service');
this.amountDueTextField = this.page.getByLabel('expand cart').locator('.amount-due');
this.amountDueDropDown = this.page.getByLabel('expand cart');
this.appointmentDetailsSection = this.page.locator('div .appt-details-snapshot');
this.appointmentDetailsDropdown = this.page.getByLabel('expand appointment details');
this.payWithInsuranceButton = this.page.locator('div').filter({ hasText: /^Pay with insurance$/ }).nth(1);
this.subtotalAmountTextField = this.page.locator('.sub-total span').nth(1);
this.payNowButton = this.page.locator('[buttonlabel="Pay now"]');
this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]');
this.submitButton = this.page.locator('[data-test-id="nav-bar-main-button"]');
this.recalibrationCheckbox = this.page.getByLabel('I understand after windshield');
// this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1);
this.paymentPage = new PaymentPage(page);
this.paypalPage = new PaypalPage(page);
// Payment details validation locators
this.reviewTable = this.page.locator('div.review-table');
// Section locators - find by heading text
this.vehicleSection = this.page.locator('.review-table').locator('div', { hasText: 'Vehicle' }).first();
this.damageSection = this.page.locator('.review-table').locator('div', { hasText: 'Damage' }).first();
this.serviceDetailsSection = this.page.locator('.review-table').locator('div', { hasText: 'Glass service only' }).first();
this.serviceLocationSection = this.page.locator('.review-table').locator('div', { hasText: "We're coming to you" }).first();
this.appointmentDateSection = this.page.locator('.review-table').locator('div', { hasText: 'Appointment date + time' }).first();
this.contactDetailsSection = this.page.locator('.review-table').locator('div', { hasText: 'Contact details' }).first();
// Cart panel elements
this.cartPanelDetails = this.page.locator('.cart-panel');
this.subtotalText = this.page.locator('.sub-total');
this.finalAmountDueText = this.cartPanelDetails.locator('div.amount-due');
this.glassServiceText = this.page.locator('div', { hasText: /^Glass service only$/ });
this.wiperBladesText = this.page.locator('div', { hasText: /^New wiper blades$/ });
this.rainDefenseText = this.page.locator('div', { hasText: /^Rain Defense™$/ });
this.deductibleText = this.page.locator('#deductible-value');
}
async validatePaymentDetailsPage(testData: Partial<ITestData>) {
// Destructure data we use
const { vehicleDetails, customerDetails, servicePackage,
flow, claimDetails, paymentDetails, appointmentDetails,
isUseVehicleOnPolicy, paymentMethod, vehicleDamage } = testData;
// Wait for review table to be visible to ensure page is loaded
await this.appointmentDetailsDropdown.waitFor({state: "visible"});
await this.appointmentDetailsDropdown.click().then(
async () => {
await waitUntil(async () => {
return (await this.page.locator('.review-toggle.expanded').evaluate(el => window.getComputedStyle(el, ':after').transform)) === 'matrix(-1, 0, 0, -1, 0, 0)';
});
}
);
let expectedAppointmentDetails = new Map<string, string[]>();
expectedAppointmentDetails = await this.getExpectedVehicleDetails(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.getExpectedVehicleDamage(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.expectedServicePackageDetails(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.getExpectedServiceLocation(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.getExpectedAppointmentDate(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.getExpectedCustomerDetails(testData, expectedAppointmentDetails);
let actualAppointmentDetails = await this.getActualAppointmentDetails();
for (const key in expectedAppointmentDetails) {
Soft.expect(actualAppointmentDetails[key]?.map(item => item.toLowerCase()))
.toEqual(expectedAppointmentDetails[key].map(item => item.toLowerCase()));
}
// Expand cart to see all details
await this.amountDueDropDown.click();
await this.reviewTable.waitFor({ state: "visible" });
// Cart Validation
// Get pricing information from cart panel
if (await this.subtotalText.isVisible()) {
const subtotalValue = await this.subtotalText.innerText();
const finalAmountDueValue = await this.finalAmountDueText.innerText();
// Pricing validations differ by payment method
if (paymentMethod === PaymentMethod.SelfPay && paymentDetails?.paymentType !== PaymentType.PayWithInsurance) {
// Extract amounts for self-pay customers
const subtotalAmount = this.extractAmount(subtotalValue);
const finalAmountDueAmount = this.extractAmount(finalAmountDueValue);
// Subtotal should be greater than 0
Soft.expect(subtotalAmount).toBeGreaterThan(0);
// Final amount differs based on payment type
if (paymentDetails?.paymentType === PaymentType.PayAtService) {
Soft.expect(finalAmountDueAmount).toBeGreaterThan(0);
} else if (paymentDetails?.paymentType === PaymentType.Credit ||
paymentDetails?.paymentType === PaymentType.Paypal ||
paymentDetails?.paymentType === PaymentType.AfterPay) {
// For payment types that charge immediately, amount due could be 0
// This logic might need adjusting based on actual business rules
}
} else {
// For insurance payments, check for proper indicators
// TODO: Apply logic for deductible and insurance payment logic
}
// Service package validations
const servicePackageValue = await this.cartPanelDetails.textContent();
await Soft.expect(servicePackageValue).toContain(`${servicePackage}`)
if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) {
Soft.expect(servicePackageValue).toContain('New wiper blades');
}
if (servicePackage === ServicePackage.Premium) {
Soft.expect(servicePackageValue).toContain('Rain Repel Treatment');
}
// Promo Code Validation
if (paymentDetails!.promoCode) {
expect.soft(servicePackageValue).toContain(`Promo code ${paymentDetails!.promoCode} applied`);
};
// Early Bird line item validation
if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) {
Soft.expect(servicePackageValue).toContain('Early bird');
}
}
}
/**
* Helper method to extract numeric amount from price strings like "$123.45"
*/
private extractAmount(valueString: string): number {
const matches = valueString.match(/\$([0-9,]+(\.[0-9]{2})?)/);
if (matches && matches[1]) {
return parseFloat(matches[1].replace(/,/g, ''));
}
return 0;
}
async getAmountDue(): Promise<string> {
await expect(this.amountDueTextField).toBeVisible();
const text = await this.amountDueTextField.textContent();
if (!text) throw new Error("Amount due text is empty");
return text;
}
async getSubtotal(): Promise<string> {
await this.amountDueDropDown.click();
await expect(this.subtotalAmountTextField).toBeVisible();
const text = await this.subtotalAmountTextField.textContent();
if (!text) throw new Error("Subtotal text is empty");
return text;
}
async executePayment(paymentDetails: IPaymentDetails, isRecalVehicle: boolean) {
const browserContext = this.page.context();
switch(paymentDetails.paymentType) {
case PaymentType.Credit:
await this.selectCreditCard();
await this.nextPage();
await this.paymentPage.populateCreditCardDetails(paymentDetails);
break;
case PaymentType.AfterPay:
await this.payInFourButton.click();
await this.nextPage();
// Capture popup
const afterpayPopup = await browserContext.waitForEvent('page');
const afterpayPage = new AfterpayPage(afterpayPopup);
// Execute payment
await afterpayPage.executeAfterpayPayment(paymentDetails);
break;
case PaymentType.Paypal:
await this.selectPaypal();
// TODO: Click paypal button
await this.nextPage();
await this.paymentPage.navigateToPaypalCheckout();
await this.paypalPage.completePaypalPurchase(paymentDetails);
break;
case PaymentType.PayAtService:
await this.selectPayAtService(isRecalVehicle);
await this.nextPage();
break;
case PaymentType.PayWithInsurance:
await this.payWithInsuranceButton.click();
await this.nextPage();
break;
default:
console.error('PaymentMethodPage >> Logic for this payment method unimplemented');
break;
}
}
async selectPaypal(){
await this.payNowButton.click();
}
l
async selectCreditCard(){
await this.payNowButton.click();
}
async selectPayAtService(isRecalVehicle: boolean){
if (await this.payAtServiceButton.isVisible()) {
await this.payAtServiceButton.click();
} else if (isRecalVehicle) {
await this.recalibrationCheckbox.click();
}
}
async verifyVAPS(): Promise<void> {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate the count of FRONT WIPER parts
if (vuexState.order?.lineItems?.vaps?.length > 0) {
const frontWiperPartsCount = vuexState.order.lineItems.vaps.filter(vap =>
vap.partType === "FRONT WIPER"
).length;
await expect(frontWiperPartsCount).toBe(2);
} else {
throw new Error("No VAPS found in the order");
}
}
async getActualAppointmentDetails(): Promise<any> {
let actualAppointmentDetails = new Map<string, string[]>();
let appointmentDetailsSubSections = await this.appointmentDetailsSection.locator('.review-table .py-3').all();
for (let element of appointmentDetailsSubSections) {
let label = await element.locator('div .text-block').innerText();
//added to trim the text to remove any leading or trailing spaces (example: "expert installation " to "expert installation")
let value = (await element.locator('.review-block-content').allInnerTexts() as string[]).map(item => item.trim());
actualAppointmentDetails[label] = value;
}
return actualAppointmentDetails;
}
async getExpectedVehicleDetails(testdata: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const { vehicleDetails } = testdata;
expectedServicePackageDetails["Vehicle"] = [vehicleDetails?.year + " " + vehicleDetails?.make + " " + vehicleDetails?.model];
return expectedServicePackageDetails;
}
async getExpectedVehicleDamage(testdata: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const {vehicleDamage} = testdata;
let vehicleDamageText: string[] = [];
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldOneChip)) {
vehicleDamageText.push("Windshield repair - 1 chip");
}
else if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldTwoChips)) {
vehicleDamageText.push("Windshield repair - 2 chips");
}
else if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldThreeChips)) {
vehicleDamageText.push("Windshield repair - 3 chips");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldCrack)) {
vehicleDamageText.push("Windshield crack");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverVentGlass)) {
vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nVent glass" : vehicleDamageText.push("Side door - Driver side", "Vent glass");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverFrontDoor)) {
vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nFront door" : vehicleDamageText.push("Side door - Driver side", "Front door");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverRearDoor)) {
vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nBack door" : vehicleDamageText.push("Side door - Driver side", "Back door");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverQuarterPanel)) {
vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nQuarter panel" : vehicleDamageText.push("Side door - Driver side", "Quarter panel");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerVentGlass)) {
vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nVent glass" : vehicleDamageText.push("Side door - Passenger side", "Vent glass");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerFrontDoor)) {
vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nFront door" : vehicleDamageText.push("Side door - Passenger side", "Front door");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerRearDoor)) {
vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nBack door" : vehicleDamageText.push("Side door - Passenger side", "Back door");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerQuarterPanel)) {
vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nQuarter panel" : vehicleDamageText.push("Side door - Passenger side", "Quarter panel");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.RearWindow || item == VehicleDamage.RearSliding)) {
vehicleDamageText.push("Rear window");
}
expectedServicePackageDetails["Damage"] = vehicleDamageText;
return expectedServicePackageDetails;
}
async expectedServicePackageDetails(testData: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const {servicePackage} = testData;
let localStorage= JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
let isRepair = localStorage.order.damage.isRepair as boolean;
let isInsurance = localStorage.order.payment.isInsurance as boolean;
let hasNonWindshieldGlass: boolean = false;
if (!isRepair) {
hasNonWindshieldGlass = localStorage.order.lineItems.glassParts.find((item: any) => item.partType !== "WINDSHIELD") ? true : false;
}
let isCaliforniaState = localStorage.order.serviceLocation.state as string == 'CA' ? true : false;
let hasRecalPart: boolean = false;
let canSafeliteRecalibrate: boolean = false;
if (!isRepair) {
hasRecalPart = localStorage.order.lineItems.glassParts.find((item: any) => item.requiresRecalibration === true) ? true : false;
canSafeliteRecalibrate = localStorage.order.lineItems.glassParts.find((item: any) => item.canSafeliteRecalibrate === true) ? true : false;
}
let recalRequired = !isRepair && (isInsurance || isCaliforniaState) && hasRecalPart && canSafeliteRecalibrate
let stringForRepair: string[] = ["Expert windshield repair", "Exclusive resin sealant", "Nationwide lifetime guarantee"];
let stringIfRecal = isRepair
? ""
: recalRequired
? " and recalibration"
: "";
let stringForReplace: string[] = [hasNonWindshieldGlass ? "New replacement glass" : "New replacement windshield", "Expert installation" + `${stringIfRecal}`, "Nationwide lifetime warranty"];
switch (servicePackage)
{
case ServicePackage.GlassOnly:
expectedServicePackageDetails["Glass service only"] = isRepair
? stringForRepair
: stringForReplace;
break;
case ServicePackage.Standard:
stringForRepair.push("New wiper blades");
stringForReplace.push("New wiper blades");
expectedServicePackageDetails["Standard service"] = isRepair
? stringForRepair
: stringForReplace;
break;
case ServicePackage.Premium:
stringForRepair.push("New wiper blades", "Rain repel treatment");
stringForReplace.push("New wiper blades", "Rain repel treatment");
expectedServicePackageDetails["Premium service"] = isRepair
? stringForRepair
: stringForReplace;
break;
}
return expectedServicePackageDetails;
}
async getExpectedServiceLocation(testData: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const { appointmentDetails } = testData;
let serviceLocationTitle = appointmentDetails?.serviceLocation == ServiceLocation.Mobile
? "We're coming to you"
: "You're going to a Safelite shop";
let serviceLocation: string[] = [];
serviceLocation.push(
appointmentDetails?.serviceLocation == ServiceLocation.Mobile
? appointmentDetails?.serviceAddress
? appointmentDetails.serviceAddress.street + ", " + appointmentDetails.serviceAddress.city + ", " + appointmentDetails.serviceAddress.state + " " + appointmentDetails.serviceAddress.postalCode
: ""
: appointmentDetails?.shopAddress
? appointmentDetails.shopAddress
: ""
);
expectedServicePackageDetails[serviceLocationTitle] = serviceLocation;
return expectedServicePackageDetails;
}
async getExpectedAppointmentDate(testData: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const { customerDetails, appointmentDetails } = testData;
let appointmentDateText: string[] = [];
const isMobileAppointment = appointmentDetails?.serviceLocation == ServiceLocation.Mobile
if (isMobileAppointment) {
let localStorage= JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
let jobMinMinutes = localStorage.order.schedule.jobMinMinutes as number;
let jobMaxMinutes = localStorage.order.schedule.jobMaxMinutes as number;
if (jobMinMinutes < 60) {
customerDetails!.apptDuration = `${jobMinMinutes} - ${jobMaxMinutes} minutes`;
} else {
const minHours = Math.floor(jobMinMinutes / 60);
const maxHours = Math.floor(jobMaxMinutes / 60);
customerDetails!.apptDuration = `${minHours} - ${maxHours} hours`;
}
}
appointmentDateText.push(
customerDetails?.apptDate ? await this.getFormattedAppointmentDate(customerDetails.apptDate) + " " + customerDetails?.apptTime?.replace("-", "—") : "",
customerDetails?.apptDuration ? "Estimated appointment length: " + customerDetails.apptDuration : ""
);
expectedServicePackageDetails["Appointment date + time"] = appointmentDateText;
return expectedServicePackageDetails;
}
async getExpectedCustomerDetails(testData: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const { customerDetails } = testData;
let customerDetailsText: string[] = [];
customerDetailsText.push(
customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(),
customerDetails?.email ? customerDetails?.email.toUpperCase() : "",
customerDetails?.phoneNumber ? customerDetails?.phoneNumber : "",
"Opted out of text message updates"
);
expectedServicePackageDetails["Contact details"] = customerDetailsText;
return expectedServicePackageDetails;
}
async getFormattedAppointmentDate(appointmentDate: string) {
// Parse the original date
let parsedAppointmentDate = new Date(`${appointmentDate}` + 'T00:00:00');
// Format the new date as a string
let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
return updatedAppointmentDate;
}
@step("PaymentMethodPage >> Select Payment Method: ")
async handlePaymentMethodPage(testData: Partial<ITestData>) {
const { servicePackage, isRecalVehicle, paymentDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage);
await this.validatePaymentDetailsPage(testData);
// Verify VAPS wipers on backend for standard and premium packages
if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) {
await this.verifyVAPS();
}
if (paymentDetails?.paymentType) {
await this.executePayment(paymentDetails!, isRecalVehicle!);
} else {
await this.nextPage();
}
}
}

View file

@ -0,0 +1,50 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPaymentDetails } from 'safelite-playwright-core';
export class PaymentPage extends BasePage {
readonly page: Page;
readonly cardNumberTextField: Locator;
readonly expirationMonthDropDown: Locator;
readonly expirationYearDropDown: Locator;
readonly cvvTextField: Locator;
readonly billingAddressTextField: Locator;
readonly cityTextField: Locator;
readonly stateDropDown: Locator;
readonly billingZipTextField: Locator;
readonly submitPaymentButton: Locator;
readonly payPalButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.cardNumberTextField = page.frameLocator('iframe[name="card-frame"]').getByLabel('Card number*');
this.expirationMonthDropDown = page.frameLocator('iframe[name="card-frame"]').getByRole('combobox', { name: 'Expiration month' });
this.expirationYearDropDown = page.frameLocator('iframe[name="card-frame"]').getByRole('combobox', { name: 'Expiration year' });
this.cvvTextField = page.frameLocator('iframe[name="card-frame"]').getByRole('textbox', { name: 'CVV' });
this.billingAddressTextField = page.frameLocator('iframe[name="card-frame"]').getByRole('textbox', { name: 'Billing address' });
this.cityTextField = page.frameLocator('iframe[name="card-frame"]').getByRole('textbox', { name: 'City' });
this.stateDropDown = page.frameLocator('iframe[name="card-frame"]').getByRole('combobox', { name: 'State' });
this.billingZipTextField = page.frameLocator('iframe[name="card-frame"]').getByRole('textbox', { name: 'Billing ZIP code' });;
this.submitPaymentButton = page.frameLocator('iframe[name="card-frame"]').locator('#buttonContainer');
this.payPalButton = page.frameLocator('iframe[name="card-frame"]').locator('#paypalParentLink');
}
async populateCreditCardDetails(paymentDetails: IPaymentDetails){
await this.cardNumberTextField.fill(paymentDetails.cardNumber || '');
await this.expirationMonthDropDown.selectOption(paymentDetails.expirationMonth!);
await this.expirationYearDropDown.selectOption(paymentDetails.expirationYear!);
await this.cvvTextField.fill(paymentDetails.cvv!);
await this.billingAddressTextField.fill(paymentDetails.billingAddress!.street);
await this.cityTextField.fill(paymentDetails.billingAddress!.city);
await this.stateDropDown.selectOption(paymentDetails.billingAddress!.state);
await this.billingZipTextField.fill(paymentDetails.billingAddress!.postalCode);
await this.submitPaymentButton.click();
}
async navigateToPaypalCheckout() {
await this.payPalButton.click();
}
}

View file

@ -0,0 +1,51 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPaymentDetails } from 'safelite-playwright-core';
export class PaypalPage extends BasePage {
readonly page: Page;
readonly loginWithPasswordButton: Locator;
readonly usernameTextBox: Locator;
readonly nextButton: Locator;
readonly usePasswordInsteadButton: Locator;
readonly passwordTextBox: Locator;
readonly paypalLoginButton: Locator;
readonly completePurchaseButton: Locator;
readonly payWithRadioButton: Locator;
readonly payButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.usernameTextBox = page.getByPlaceholder('Email');
this.nextButton = page.getByRole('button', { name: 'Next' });
this.loginWithPasswordButton = page.getByRole('link', { name: 'Log in with a password instead' })
this.usePasswordInsteadButton = page.getByRole('button', { name: 'Use Password Instead' });
this.passwordTextBox = page.getByPlaceholder('Password');
this.paypalLoginButton = page.getByRole('button', { name: 'Log In', exact: true });
this.completePurchaseButton = page.getByTestId('submit-button-initial')
this.payWithRadioButton = page.locator('.py-4').first();
this.payButton = page.getByRole('button', { name: 'Pay $' });
}
async completePaypalPurchase(paymentDetails: IPaymentDetails){
// Wait for the PayPal login page to load
await this.page.waitForFunction(() => window.location.href.includes('sandbox.paypal.com'), null, { timeout: 10000 });
// handle flow with and without the "Log in with a password instead" link
if (await this.loginWithPasswordButton.isVisible()) {
await this.loginWithPasswordButton.click();
await this.passwordTextBox.fill(paymentDetails.password!);
await this.paypalLoginButton.click();
await this.completePurchaseButton.click();
} else {
await this.usernameTextBox.fill(paymentDetails.username!);
await this.nextButton.click();
await this.usePasswordInsteadButton.click();
await this.passwordTextBox.fill(paymentDetails.password!);
await this.paypalLoginButton.click();
await this.payWithRadioButton.click();
await this.payButton.click();
}
}
}

View file

@ -0,0 +1,56 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { InsuranceBasePage } from './InsuranceBasePage';
import { ICustomerDetails } from 'safelite-playwright-core';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class PolicyDriverPage extends InsuranceBasePage {
readonly page: Page;
readonly driverListItems: Locator;
readonly driverNotListedOption: Locator;
readonly vehicleParkedOption: Locator;
constructor(page: Page) {
super(page);
this.page = page;
// Locators for driver list items (the clickable areas)
this.driverListItems = this.page.locator('.list-group-item');
// Locators for driver options
this.driverNotListedOption = this.page.getByRole('link', { name: 'Driver not listed' });
this.vehicleParkedOption = this.page.getByRole('link', { name: 'Vehicle was parked' });
}
async selectPolicyDriver(customerDetails: ICustomerDetails): Promise<void> {
// Format the customer name for matching
const customerFullName = `${customerDetails.firstName} ${customerDetails.lastName}`.toUpperCase();
// Get all driver list items
const driverItems = await this.driverListItems.all();
let driverFound = false;
// Check each driver item for a match with our customer
for (const item of driverItems) {
const nameText = await item.locator('.third-span').textContent();
if (nameText && nameText.toUpperCase().includes(customerFullName)) {
// If we found a match, click the list-group-item (the whole row)
await item.click();
driverFound = true;
break;
}
}
// If no match was found, select "Driver not listed"
if (!driverFound) {
await this.driverNotListedOption.click();
}
}
@step("PolicyDriverPage >> Select policy driver: ")
async handlePolicyDriverPage(testData: Partial<ITestData>) {
const { customerDetails } = testData;
await this.selectPolicyDriver(customerDetails!);
await this.nextPage();
}
}

View file

@ -0,0 +1,26 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { InsuranceBasePage } from './InsuranceBasePage';
import { step } from 'framework/localTypes/Step';
export class PolicyInfoSubmittedPage extends InsuranceBasePage {
readonly page: Page;
readonly policyInfoMessage: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.policyInfoMessage = this.page.getByRole('heading', { name: 'Your policy and vehicle' });
}
async verifyPolicyInfoSubmitted(): Promise<void> {
await expect(this.policyInfoMessage).toBeVisible();
const policyInfoMessage = await this.policyInfoMessage.textContent();
expect(policyInfoMessage).toBe("Your policy and vehicle information has been submitted for coverage verification")
}
@step("PolicyInfoSubmittedPage >> Verify policy info submitted: ")
async handlePolicyInfoSubmittedPage() {
await this.verifyPolicyInfoSubmitted();
await this.nextPage();
}
}

View file

@ -0,0 +1,54 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { IVehicleDetails } from 'safelite-playwright-core';
import { InsuranceBasePage } from './InsuranceBasePage';
import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step';
export class PolicyVehiclesPage extends InsuranceBasePage {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
async validateVehicleIsOnPolicy(vehicleDetails: IVehicleDetails) {
const vehicleRegExp = new RegExp(`${vehicleDetails.year} .+ ${vehicleDetails.model}`, 'i');
await expect.soft(this.page.getByRole('radio', {name: vehicleRegExp})).toBeAttached();
}
async selectVehicle(vehicleDetails: IVehicleDetails){
const baseModel = vehicleDetails.model.split(' ').pop() || vehicleDetails.model;
const vehicleRegExp = new RegExp(`${vehicleDetails.year}.*?${vehicleDetails.make}.*?${baseModel}`, 'i');
await this.page.getByRole('radio', {name: vehicleRegExp}).click();
}
async selectVehicleNotListed(){
await this.page.getByText('Vehicle not listed').click();
}
@step("PolicyVehiclesPage >> Select Vehicle: ")
async handlePolicyVehiclesPage(testCase: Partial<ITestData>) {
const { vehicleDetails, otherVehiclesOnPolicy, isUseVehicleOnPolicy } = testCase;
// Validate other vehicles on policy
if (otherVehiclesOnPolicy && otherVehiclesOnPolicy.length > 0) {
for (const vehicle of otherVehiclesOnPolicy) {
await this.validateVehicleIsOnPolicy(vehicle);
}
}
// If vehicle is not on the policy, select new vehicle
if (!(isUseVehicleOnPolicy ?? true)) {
await this.selectVehicleNotListed();
await this.nextPage();
await this.selectVehicle(vehicleDetails!);
await this.nextPage();
} else {
// Otherwise, select the vehicle entered in Safelite.com
await this.selectVehicle(vehicleDetails!);
await this.nextPage();
}
}
}

View file

@ -0,0 +1,19 @@
import { Locator, Page } from "@playwright/test";
import { InsuranceBasePage } from "./InsuranceBasePage";
import { step } from 'framework/localTypes/Step';
export default class RecalibrationInfoPage extends InsuranceBasePage {
readonly continueButton: Locator;
constructor(page: Page) {
super(page);
this.continueButton = page.getByRole('button', { name: 'Continue' });
}
@step("RecalibrationInfoPage >> Click continue button: ")
async handleRecalibrationInfoPage() {
await this.nextPage();
}
}

View file

@ -0,0 +1,110 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IAppointmentDetails } from 'safelite-playwright-core';
import { formatDate, formatTime } from 'safelite-playwright-core';
import { AppointmentTimeslot } from 'safelite-playwright-core';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
import { time } from 'console';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class SchedulePage extends BasePage {
readonly page: Page;
readonly firstAvailableDate: Locator;
readonly firstAvailableTime: Locator;
readonly modalContinueButton: Locator;
readonly dropOffButton: Locator;
readonly dateText: Locator;
readonly viewMoreDatesLink: Locator;
readonly appointmentDuration: Locator;
readonly timeSlots: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.firstAvailableDate = this.page.locator('.selectable-day').locator('nth=0');
this.firstAvailableTime = this.page.locator('label').filter({ hasText: /AM|PM/ }).locator('div').locator('nth=0');
this.modalContinueButton = this.page.getByRole('dialog').getByRole('button', { name: 'Continue' });
this.dropOffButton = this.page.getByText('Drop off your vehicle', { exact: true });
this.dateText = this.page.locator('label.modal-title');
this.viewMoreDatesLink = this.page.getByText(/View more dates/).first();
this.appointmentDuration = this.page.locator('.duration-text-block');
this.timeSlots = this.page.locator('fieldset[aria-labelledby=\'chooseTimeSlot\'] label');
}
async scheduleAppointment(appointmentDetails: IAppointmentDetails) {
const formattedDate = formatDate(appointmentDetails.appointmentDate!);
const formattedTime = formatTime(appointmentDetails.appointmentDate!);
const dateInput = this.page.locator(`div[id="${formattedDate}"]`);
const timeButton = this.page.locator(`div[aria-label="${formattedTime}"]`);
if (await dateInput.isVisible()) {
await dateInput.click();
} else {
await this.viewMoreDatesLink.click();
await dateInput.click();
}
await timeButton.click();
await this.modalContinueButton.click();
}
async scheduleFirstAppointment(testData: Partial<ITestData>) {
const { appointmentDetails, customerDetails } = testData;
while (!(await this.firstAvailableDate.isVisible())) {
if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) {
await this.mockScheduleResponseForEarlyBird(customerDetails!);
}
await this.viewMoreDatesLink.click();
}
if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) {
await this.page.locator(`.selectable-days, [id='${customerDetails?.apptDate}']`).click();
const earlyBirdTimeSlot = this.timeSlots.filter({ hasText: "Earlybird" }).first();
await earlyBirdTimeSlot.click().then(async () => {
customerDetails!.apptTime = await this.getFormattedTimeSlot(earlyBirdTimeSlot);
});
}
else {
await this.firstAvailableDate.click().then(async () => {
customerDetails!.apptDate = `${await this.firstAvailableDate.getAttribute("id")}`
});
// const timeSlots = this.timeSlots;
const timeSlotCount = await this.timeSlots.count();
const randomIndex = Math.floor(Math.random() * timeSlotCount);
const timeSlot = this.timeSlots.nth(randomIndex);
await timeSlot.click().then(async () => {
customerDetails!.apptTime = await this.getFormattedTimeSlot(timeSlot);
});
}
// appointmentmentDetails.serviceLocation === ServiceLocation.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click();
customerDetails!.apptDuration = (await this.appointmentDuration.innerText()).replace("Duration: ", "");
await this.nextPage();
}
async getFormattedTimeSlot(timeSlot: Locator) {
const selectedTimeSlot = await timeSlot.innerText();
let formattedTimeSlot: string = "";
if (selectedTimeSlot.toLowerCase().includes("drop off"))
{
formattedTimeSlot = selectedTimeSlot.includes("overnight") ? "drop off by 5:30 pm on the night of your scheduled appointment. Pick-up time dependent on shop schedule" : "drop off before 9:30 AM";
}
else if (selectedTimeSlot.includes("-") || selectedTimeSlot.includes("Earlybird"))
{
formattedTimeSlot = selectedTimeSlot.includes("Earlybird") ? "arriving between 8:00 AM - 12:00 PM" : `arriving between ${selectedTimeSlot}`;
}
else
{
formattedTimeSlot = `at ${selectedTimeSlot}`;
}
return formattedTimeSlot;
}
@step("SchedulePage >> Schedule appointment: ")
async handleSchedulePage(testData: Partial<ITestData>) {
await this.validateProgressBar(ProgressBarPercentages.SchedulePage);
await this.scheduleFirstAppointment(testData);
}
}

View file

@ -0,0 +1,168 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IAppointmentDetails } from 'safelite-playwright-core';
import { AppointmentTimeslot, ServiceLocation } from 'safelite-playwright-core';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
import { AddressForm } from './forms/AddressForm';
import { faker } from '@faker-js/faker';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class ServiceLocationPage extends BasePage {
readonly page: Page;
readonly addressForm: AddressForm;
// Initial selection
readonly inShopButton: Locator;
readonly mobileButton: Locator;
readonly dropOffButton: Locator;
readonly RecalWarningMessage1: Locator;
readonly RecalWarningMessage2: Locator;
readonly militaryWarningMessage: Locator;
// For in-shop and drop off
readonly selectAShopOptions: Locator;
readonly firstAppointmentButton: Locator;
readonly changeZipButton: Locator;
readonly updateZipTextBox: Locator;
readonly saveZipButton: Locator;
// For mobile
readonly enterServiceAddressButton: Locator;
readonly serviceAddressTextBox: Locator;
readonly aptNumberTextBox: Locator;
readonly cityTextBox: Locator;
readonly stateDropDown: Locator;
readonly zipCodeTextBox: Locator;
readonly vehicleProtectedYesButton: Locator;
readonly vehicleProtectedNoButton: Locator;
readonly repeatedClicksModalCloseButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.addressForm = new AddressForm(page);
// Initial selection
this.inShopButton = this.page.getByText(/In-shop/);
this.mobileButton = this.page.locator('label[buttonlabel="Mobile"]');
this.dropOffButton = this.page.getByText(/Drop-off/);
this.RecalWarningMessage1 = this.page.getByText(/We're not able to provide mobile service/);
this.RecalWarningMessage2 = this.page.getByText(/advanced safety system recalibration needs to be done in our shop./);
this.militaryWarningMessage = this.page.locator('[class*="widget-name-AlertMilitaryBaseZipWidget"]');
// For in-shop and drop off
this.selectAShopOptions = this.page.locator('[class="shop-question"]');
this.firstAppointmentButton = this.page.locator('fieldset:has(legend#chooseShop)').locator('label').first();
this.changeZipButton = this.page.locator('a:has(span.sr-only:has-text("edit zip code"))');
this.updateZipTextBox = this.page.locator('#serviceZipCode');
this.saveZipButton = this.page.getByText('Save ZIP code', { exact: true });
// For mobile
this.enterServiceAddressButton = this.page.getByRole('link', { name: 'Enter your service address ' });
this.serviceAddressTextBox = this.page.getByRole('textbox', { name: 'Street Address' });
this.aptNumberTextBox = this.page.getByRole('textbox', { name: 'Apt. number'});
this.cityTextBox = this.page.getByRole('textbox', { name: 'City' });
this.stateDropDown = this.page.getByRole('combobox', { name: 'State' });
this.zipCodeTextBox = this.page.getByRole('textbox', { name: 'Zip code' });
this.vehicleProtectedYesButton = this.page.locator('label').filter({ hasText: 'Yes' }).locator('div');
this.vehicleProtectedNoButton = this.page.locator('label').filter({ hasText: 'No' }).locator('div');
this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']');
}
async selectLocation(testData: Partial<ITestData>) {
const { appointmentDetails, customerDetails } = testData;
switch(appointmentDetails?.serviceLocation) {
case ServiceLocation.Mobile:
await this.scheduleMobile(testData);
break;
case ServiceLocation.InShop:
await this.scheduleInShop(appointmentDetails);
break;
case ServiceLocation.DropOff:
await this.scheduleDropOff(appointmentDetails);
break;
}
}
async scheduleInShop(appointmentDetails?: IAppointmentDetails) {
await this.inShopButton.click();
if (appointmentDetails && appointmentDetails.shopAddress) {
const zipCodeMatch = appointmentDetails.shopAddress.match(/\b\d{5}$/);
if (zipCodeMatch) {
const zipCode = zipCodeMatch[0];
// Enter the ZIP code into the updateZipTextBox
await this.changeZipButton.click();
await this.page.waitForTimeout(500);
await this.updateZipTextBox.fill(zipCode);
await this.saveZipButton.click();
}
await this.inShopButton.click();
await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).scrollIntoViewIfNeeded().then(() => this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).click());
} else {
await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => {
await this.firstAppointmentButton.click();
if (appointmentDetails) {
appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText();
}
});
}
}
async scheduleMobile(testData: Partial<ITestData>) {
const { appointmentDetails, customerDetails } = testData;
if (appointmentDetails?.serviceAddress) {
await this.mobileButton.click();
// await this.enterServiceAddressButton.click();
await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! });
if (await this.repeatedClicksModalCloseButton.isVisible()) {
await this.repeatedClicksModalCloseButton.click();
}
if (faker.datatype.boolean()) {
await this.vehicleProtectedYesButton.check();
} else {
await this.vehicleProtectedNoButton.check();
}
if (appointmentDetails.appointmentTimeSlot == AppointmentTimeslot.EarlyBird)
{
await this.mockScheduleResponseForEarlyBird(customerDetails!);
}
} else {
console.error('ServiceLocationPage >> Please supply an address')
}
}
async scheduleDropOff(appointmentDetails?: IAppointmentDetails){
await this.dropOffButton.click();
if (appointmentDetails && appointmentDetails.shopAddress) {
await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check();
} else {
// await this.firstAppointmentButton.click();
// await this.clickWithRetry(this.firstAppointmentButton, this.page);
await this.firstAppointmentButton.scrollIntoViewIfNeeded().then(async () => {
await this.firstAppointmentButton.click();
if (appointmentDetails) {
appointmentDetails.shopAddress = await this.firstAppointmentButton.locator('.row-two').innerText();
}
});
}
}
async validateRecalWarning(){
await expect(this.RecalWarningMessage1).toBeVisible();
await expect(this.RecalWarningMessage2).toBeVisible();
}
@step("ServiceLocationPage >> Select service location: ")
async handleServiceLocationPage(testData: Partial<ITestData>) {
await this.validateProgressBar(ProgressBarPercentages.ServiceLocationPage);
await this.selectLocation(testData);
await this.nextPage();
}
}

View file

@ -0,0 +1,241 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ServicePackage, VehicleDamage } from 'safelite-playwright-core';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
import { PaymentMethod } from "framework/localTypes/Enums";
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class ServicePackagesPage extends BasePage {
readonly page: Page;
readonly standardPackageButton: Locator;
readonly premiumPackageButton: Locator;
readonly glassOnlyButton: Locator;
readonly payOnMyOwnButton: Locator;
readonly paywithInsuranceButton: Locator;
readonly iHavePromoCodeButton: Locator;
//Your quote is almost ready modal
readonly skipQuoteEmailButton: Locator;
readonly emailInput: Locator;
readonly getMyQuoteButton: Locator;
readonly closeButton: Locator;
//Enter a promo code modal
readonly promoCodeTextbox: Locator;
readonly applyPromoButton: Locator;
readonly repeatedClicksModalCloseButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.standardPackageButton = this.page.getByText('Standard');
this.premiumPackageButton = this.page.getByText('Premium');
this.glassOnlyButton = this.page.getByText('Glass service only', { exact: true });
this.payOnMyOwnButton = this.page.locator('label').filter({ hasText: 'Pay on my own' }).locator('div');
this.paywithInsuranceButton = this.page.locator('label').filter({ hasText: 'Pay with insurance' }).locator('div');
this.iHavePromoCodeButton = this.page.getByRole('link', { name: 'I have a promo code' });
this.skipQuoteEmailButton = this.page.getByRole('button', { name: 'Skip' });
this.emailInput = this.page.getByRole('textbox', { name: 'Enter your email address' });
this.getMyQuoteButton = this.page.getByRole('button', { name: 'Get my quote' });
this.closeButton = this.page. getByRole('dialog').locator('button').filter({ hasText: 'Close' });
this.promoCodeTextbox = this.page.getByLabel('Enter a promo code');
this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' });
this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']');
}
async selectPaymentMethod(method: PaymentMethod): Promise<void> {
const locators = {
[PaymentMethod.Insurance]: this.paywithInsuranceButton,
[PaymentMethod.SelfPay]: this.payOnMyOwnButton
}
await locators[method].click();
}
async selectServicePackage(servicePackage: ServicePackage): Promise<void> {
const locators = {
[ServicePackage.GlassOnly]: this.glassOnlyButton,
[ServicePackage.Premium]: this.premiumPackageButton,
[ServicePackage.Standard]: this.standardPackageButton,
}
if (servicePackage != null) {
await locators[servicePackage].click();
}
}
async handleQuotePopup(email?: string): Promise<void> {
await this.emailInput.waitFor({ state: 'visible' });
if (await this.emailInput.isVisible()) {
if (email) {
await this.emailInput.fill(email);
await this.getMyQuoteButton.click();
if (await this.repeatedClicksModalCloseButton.isVisible()) {
await this.repeatedClicksModalCloseButton.click();
}
await this.page.waitForTimeout(3000);
await this.closeButton.click();
} else {
await this.skipQuoteEmailButton.click();
}
}
}
async enterPromo(promoCode: string): Promise<void> {
await this.iHavePromoCodeButton.click();
await this.promoCodeTextbox.fill(promoCode);
await this.applyPromoButton.click();
}
async verifyCanNotRecal(): Promise<boolean> {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate in the backend to make sure the can safelite recalibrate data is correct
if (vuexState.order?.lineItems?.glassParts?.length > 0) {
for (const glassPart of vuexState.order.lineItems.glassParts) {
await expect(glassPart.canSafeliteRecalibrate).toBe(false);
await expect(glassPart.requiresRecalibration).toBe(true);
}
return true;
}
return false;
}
async verifyDynamicRecal(): Promise<void> {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate in the backend to make sure the dynamic recalibration part is present
if (vuexState.order?.lineItems?.glassParts?.length > 0) {
const hasDynamicRecalPart = vuexState.order.lineItems.glassParts.some(glassPart =>
glassPart.childParts.some(childPart =>
childPart.partNumber.includes("RECAL DYNAMIC")
)
);
await expect(hasDynamicRecalPart).toBe(true);
console.log("Recal part line item is verified");
} else {
throw new Error("No glass parts found in the order");
}
}
async verifyIsRepair(isRepair: boolean, vehicleDamage: VehicleDamage[]): Promise<void> {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate in the backend to make sure isRepair is aligned with the data
if (isRepair) {
await expect(vuexState.order.damage.isRepair).toBe(true);
// Check the number of chips based on vehicleDamage array
if (vehicleDamage.includes(VehicleDamage.WindshieldOneChip)) {
await expect(vuexState.order.damage.numberOfChips).toBe(1);
}
if (vehicleDamage.includes(VehicleDamage.WindshieldTwoChips)) {
await expect(vuexState.order.damage.numberOfChips).toBe(2);
}
if (vehicleDamage.includes(VehicleDamage.WindshieldThreeChips)) {
await expect(vuexState.order.damage.numberOfChips).toBe(3);
}
} else {
await expect(vuexState.order.damage.isRepair).toBe(false);
}
}
async verifyVehicleParts(vehicleDamage: VehicleDamage[]): Promise<void> {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate the presence of specific parts in the glassParts array
const glassParts = vuexState.order?.lineItems?.glassParts;
const WINDSHIELD_TYPES = [
  "SINGLE WINDSHIELD",
  "DRIVER SPLIT WINDSHIELD",
  "PASSENGER SPLIT WINDSHIELD"
];
if (glassParts?.length > 0) {
for (const partType of vehicleDamage) {
// Normalize part type to "WINDSHIELD" if it matches any of the defined types (Split Windshield types)
  const normalizedPartType = WINDSHIELD_TYPES.includes(partType) ? "WINDSHIELD" : partType;
const hasPartType = glassParts.some(glassPart => glassPart.partType === normalizedPartType);
await expect(hasPartType, `Expected part type: ${partType}`).toBe(true);
}
} else {
throw new Error("No glass parts found in the order");
}
}
async verifyOEMPart(): Promise<void> {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate the presence of an OEM part
if (vuexState.order?.lineItems?.glassParts?.length > 0) {
const firstGlassPartNumber = vuexState.order.lineItems.glassParts[0].partNumber;
await expect(firstGlassPartNumber.includes("OEM")).toBe(true);
} else {
throw new Error("No glass parts found in the order");
}
}
@step("ServicePackagePage >> Select Payment Method and Service Type: ")
async handleServicePackagePage(testData: Partial<ITestData>) {
const { customerDetails, paymentMethod, paymentDetails, servicePackage, isCanNotRecal, isDynamicRecal, hasOemEndorsement, vehicleDamage } = testData;
await this.validateProgressBar(ProgressBarPercentages.ServicePackagePage);
// Define repair damage types (vs. replacement types)
const repairTypes: VehicleDamage[] = [
VehicleDamage.WindshieldOneChip,
VehicleDamage.WindshieldTwoChips,
VehicleDamage.WindshieldThreeChips
];
// Determine if we're replacing or repairing
const isReplace = !repairTypes.some(damageType => {
return vehicleDamage!.includes(damageType);
});
await this.handleQuotePopup(customerDetails!.email!);
await this.selectPaymentMethod(paymentMethod!);
await this.selectServicePackage(servicePackage!);
// Enter promo code
if (paymentDetails?.promoCode) {
await this.enterPromo(paymentDetails.promoCode);
}
// Backend Validations
// Validate backend for can not recal if applicable
if (isCanNotRecal) {
await this.verifyCanNotRecal();
}
// Validate backend for dynamic recal if applicable
if (isDynamicRecal) {
await this.verifyDynamicRecal();
}
// Validate backend for repair info (including chip verification)
await this.verifyIsRepair(!isReplace, vehicleDamage!);
if (isReplace) {
// Validate backend for parts info
await this.verifyVehicleParts(vehicleDamage!);
}
// Validate backend for OEM endorsement
if (hasOemEndorsement) {
await this.verifyOEMPart();
}
await this.nextPage();
}
}

View file

@ -0,0 +1,20 @@
import { Page } from "@playwright/test";
import { LookupPage } from "./LookupPage";
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export class ServiceZipPage extends LookupPage {
constructor(page: Page) {
super(page);
}
@step("ZipLookupPage >> Lookup by service ZIP: ")
async handleServiceZipPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
await this.validateProgressBar(ProgressBarPercentages.ServiceZipPage);
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
}
}

View file

@ -0,0 +1,219 @@
import { type Locator, type Page, expect, test } from '@playwright/test';
import { BasePage } from './BasePage';
import { VehicleDamage } from 'safelite-playwright-core';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
import { TestSuccessAlert } from 'safelite-playwright-core';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class VehicleDamagePage extends BasePage {
readonly page: Page;
readonly windshieldChkBox: Locator;
readonly crackButton: Locator;
readonly chipButton: Locator;
readonly sideDoorButton: Locator;
readonly driverSideButton: Locator;
readonly passengerSideButton: Locator;
readonly driverQuarterPanelChkBox: Locator;
readonly driverFrontDoorChkBox: Locator;
readonly driverBackDoorChkBox: Locator;
readonly driverSlidingDoorChkBox: Locator;
readonly driverVentGlassChkBox: Locator;
readonly passengerQuarterPanelChkBox: Locator;
readonly passengerFrontDoorChkBox: Locator;
readonly passengerBackDoorChkBox: Locator;
readonly passengerVentGlassChkBox: Locator;
readonly rearWindowChkBox: Locator;
readonly rearStationaryBttn: Locator;
readonly rearSlidingGlassBttn: Locator;
readonly editVehicleLink: Locator;
readonly singleSplitWindshieldChkBox: Locator;
readonly driverSplitWindshieldChkBox: Locator;
readonly passengerSplitWindshieldChkBox: Locator;
readonly noReplacementAvailableAlert: Locator;
readonly bothReplaceRepairAlert: Locator;
readonly splitWindshieldAlert: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.windshieldChkBox = this.page.locator('[buttonlabel="Windshield"]');
this.crackButton = this.page.locator('[buttonlabel="Crack"]');
this.chipButton = this.page.locator('[buttonlabel="Chip(s)"]');
this.sideDoorButton = this.page.locator('[buttonlabel="Side door"]');
this.driverSideButton = this.page.locator('[buttonlabel="Driver side"]');
this.passengerSideButton = this.page.locator('[buttonlabel="Passenger side"]');
this.driverQuarterPanelChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Quarter panel"]');
this.driverFrontDoorChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Front door"]');
this.driverBackDoorChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Back door"]');
this.driverVentGlassChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Vent glass"]');
this.driverSlidingDoorChkBox = this.page.locator('[aria-labelledby="driverSideOptions"]').locator('[buttonlabel="Sliding door"]');
this.passengerQuarterPanelChkBox = this.page.locator('[aria-labelledby="passengerSideOptions"]').locator('[buttonlabel="Quarter panel"]');
this.passengerFrontDoorChkBox = this.page.locator('[aria-labelledby="passengerSideOptions"]').locator('[buttonlabel="Front door"]');
this.passengerVentGlassChkBox = this.page.locator('[aria-labelledby="passengerSideOptions"]').locator('[buttonlabel="Vent glass"]');
this.passengerBackDoorChkBox = this.page.locator('[aria-labelledby="passengerSideOptions"]').locator('[buttonlabel="Back door"]');
this.rearWindowChkBox = this.page.locator('[buttonlabel="Rear window"]');
this.singleSplitWindshieldChkBox = this.page.locator('[aria-labelledby="WindshieldReplaceOptions"]').locator('[buttonlabel="Single windshield"]');
this.driverSplitWindshieldChkBox = this.page.locator('[aria-labelledby="WindshieldReplaceOptions"]').locator('[buttonlabel="Split windshield, driver side"]');
this.passengerSplitWindshieldChkBox = this.page.locator('[aria-labelledby="WindshieldReplaceOptions"]').locator('[buttonlabel="Split windshield, passenger side"]');
this.noReplacementAvailableAlert = this.page.locator('.alert-danger.widget-name-NoReplacementAvailableError');
this.bothReplaceRepairAlert = this.page.locator('div.alert-danger.widget-name-HasReplacementConflict');
this.splitWindshieldAlert = this.page.locator('.alert-danger.widget-name-SplitSingleConflict');
this.rearStationaryBttn = this.page.locator('label').filter({ hasText: 'Stationary' });
this.rearSlidingGlassBttn = this.page.locator('label').filter({ hasText: 'Glass with slider' });
this.editVehicleLink = this.page.getByRole('link', { name: 'Edit vehicle' });
}
async selectDamage(vehicleDamage: VehicleDamage[]) {
for (const damage of vehicleDamage) {
switch(damage) {
case VehicleDamage.WindshieldOneChip:
await this.windshieldChkBox.check();
await this.selectChips('1');
break;
case VehicleDamage.WindshieldTwoChips:
await this.windshieldChkBox.check();
await this.selectChips('2');
break;
case VehicleDamage.WindshieldThreeChips:
await this.windshieldChkBox.check();
await this.selectChips('3');
break;
case VehicleDamage.WindshieldCrack:
await this.windshieldChkBox.check();
await this.selectCrack();
break;
case VehicleDamage.SingleSplitWindshield:
await this.windshieldChkBox.check();
await this.selectCrack();
await this.singleSplitWindshieldChkBox.check();
break;
case VehicleDamage.DriverSplitWindshield:
await this.windshieldChkBox.check();
await this.selectCrack();
await this.driverSplitWindshieldChkBox.check();
break;
case VehicleDamage.PassengerSplitWindshield:
await this.windshieldChkBox.check();
await this.selectCrack();
await this.passengerSplitWindshieldChkBox.check();
break;
case VehicleDamage.DriverFrontDoor:
await this.sideDoorButton.check();
await this.driverSideButton.check();
await this.driverFrontDoorChkBox.check();
break;
case VehicleDamage.DriverRearDoor:
await this.sideDoorButton.check();
await this.driverSideButton.check();
await this.driverBackDoorChkBox.check();
break;
case VehicleDamage.DriverQuarterPanel:
await this.sideDoorButton.check();
await this.driverSideButton.check();
await this.driverQuarterPanelChkBox.check();
break;
case VehicleDamage.DriverVentGlass:
await this.sideDoorButton.check();
await this.driverSideButton.check();
await this.driverVentGlassChkBox.check();
break;
case VehicleDamage.DriverSlidingDoor:
await this.sideDoorButton.check();
await this.driverSideButton.check();
await this.driverSlidingDoorChkBox.check();
break;
case VehicleDamage.PassengerFrontDoor:
await this.sideDoorButton.check();
await this.passengerSideButton.check();
await this.passengerFrontDoorChkBox.check();
break;
case VehicleDamage.PassengerRearDoor:
await this.sideDoorButton.check();
await this.passengerSideButton.check();
await this.passengerBackDoorChkBox.check();
break;
case VehicleDamage.PassengerQuarterPanel:
await this.sideDoorButton.check();
await this.passengerSideButton.check();
await this.passengerQuarterPanelChkBox.click();
break;
case VehicleDamage.PassengerVentGlass:
await this.sideDoorButton.check();
await this.passengerSideButton.check();
await this.passengerVentGlassChkBox.check();
break;
case VehicleDamage.RearWindow:
await this.selectRearWindowDamage();
if (await this.rearStationaryBttn.isVisible()) {
await this.rearStationaryBttn.click();
}
break;
case VehicleDamage.RearSliding:
await this.selectRearWindowDamage();
if (await this.rearSlidingGlassBttn.isVisible()) {
await this.rearSlidingGlassBttn.click();
}
break;
}
}
}
async selectCrack(){
await this.crackButton.check();
}
async selectChips(numChips: string){
await this.chipButton.check();
await this.page.getByText(numChips, {exact: true}).click();
}
async selectRearWindowDamage(){
await this.rearWindowChkBox.check();
}
async checkForBothRepairReplaceAlertMessage(): Promise<void> {
await expect(this.bothReplaceRepairAlert).toBeVisible();
const alertMessage = await this.bothReplaceRepairAlert.textContent();
console.log(`Alert encountered: ${alertMessage}`);
expect(alertMessage).toContain("You'll need to schedule separate appointmentsVehicle service requiring both glass repair and replacement must be scheduled separately, as they're performed by different technicians. Continue scheduling your first service now, and then come back to schedule the second service.")
}
async checkForRepairOnlyAlertMessage(): Promise<void> {
await expect(this.noReplacementAvailableAlert).toBeVisible();
const alertMessage = await this.noReplacementAvailableAlert.textContent();
console.log(`Alert encountered: ${alertMessage}`);
expect(alertMessage).toContain("Service not availableWe're sorry, but we currently offer only repair service for your vehicle type. Need help with next steps? Call us at800-394-0288.")
}
async checkForSplitWindshieldAlertMessage(): Promise<void> {
await expect(this.splitWindshieldAlert).toBeVisible();
const alertMessage = await this.splitWindshieldAlert.textContent();
console.log(`Alert encountered: ${alertMessage}`);
expect(alertMessage).toContain("Single-piece or split?Please select just one windshield option: single-piece or split.")
}
@step('VehicleDamagePage >> Select Damage')
async handleVehicleDamagePage(testData: Partial<ITestData>): Promise<void> {
const {vehicleDamage} = testData;
const {isRepairReplace, isRepairOnly, isSplitWindshield} = testData.alertFlags || {};
await this.validateProgressBar(ProgressBarPercentages.VehicleDamagePage);
await this.selectDamage(vehicleDamage!);
// Handle alert conditions for vehicle damage
if (isRepairReplace) {
await this.checkForBothRepairReplaceAlertMessage();
throw new TestSuccessAlert('Both assertions are met successfully.');
}
if (isRepairOnly) {
await this.checkForRepairOnlyAlertMessage();
throw new TestSuccessAlert('Both assertions are met successfully.');
}
if (isSplitWindshield) {
await this.checkForSplitWindshieldAlertMessage();
throw new TestSuccessAlert('Both assertions are met successfully.');
}
await this.nextPage();
}
}

View file

@ -0,0 +1,38 @@
import { type Page, Locator } from '@playwright/test';
import { LookupPage } from './LookupPage';
import { AddressForm } from './forms/AddressForm';
import { VehicleSelectionForm } from './forms/VehicleSelectionForm';
import { ICustomerDetails, IVehicleDetails } from 'safelite-playwright-core';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export class VehicleLookupAddressPage extends LookupPage {
readonly addressForm: AddressForm;
readonly vehicleSelectionForm: VehicleSelectionForm;
constructor(page: Page) {
super(page);
this.addressForm = new AddressForm(page);
this.vehicleSelectionForm = new VehicleSelectionForm(page);
// Override the unserviceableZipAlertMessage for address-specific message
this.unserviceableZipAlertMessage = this.page.locator('div.alert-danger.widget-name-AlertVinNotFoundWidget',
{ hasText: "Your address didn't return a VIN match." });
}
async lookupVehicleByAddress(customerDetails: ICustomerDetails, vehicleDetails: IVehicleDetails) {
await this.addressForm.populateAddress(customerDetails);
//await this.nextPage();
//await this.vehicleSelectionForm.selectVehicle(vehicleDetails);
}
@step("VehicleLookupAddressPage >> Lookup by address: ")
async handleVehicleLookupAddressPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupAddressPage);
await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
}
}

View file

@ -0,0 +1,44 @@
import { type Locator, type Page } from '@playwright/test';
import { LookupPage } from './LookupPage';
import { IVehicleDetails } from 'safelite-playwright-core';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export class VehicleLookupLicensePage extends LookupPage {
readonly licensePlateNumTextBox: Locator;
readonly licensePlateStateDrpDwn: Locator;
constructor(page: Page) {
super(page);
this.licensePlateNumTextBox = page.getByRole('textbox', { name: 'License plate number'});
this.licensePlateStateDrpDwn = page.getByRole('combobox', { name: 'License plate state'});
// Override the service zip textbox locator for license-specific label
this.serviceZipTextBox = page.getByRole('textbox', { name: 'ZIP code on vehicle' });
// Override the unserviceableZipAlertMessage for license-specific message
this.unserviceableZipAlertMessage = this.page.locator('div.alert-danger.widget-name-AlertVinNotFoundWidget',
{ hasText: "Your license plate didn't return a VIN match." });
}
async enterPlateDetails(vehicleDetails: IVehicleDetails) {
let licensePlateNumber: string;
if (Array.isArray(vehicleDetails.licensePlate)) {
licensePlateNumber = vehicleDetails.licensePlate[0].licensePlateNumber;
} else {
licensePlateNumber = vehicleDetails.licensePlate?.licensePlateNumber || '';
}
await this.licensePlateNumTextBox.fill(licensePlateNumber);
}
@step("VehicleLookupLicensePage >> Lookup by license plate: ")
async handleVehicleLookupLicensePage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupLicensePage);
await this.enterPlateDetails(vehicleDetails!);
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
}
}

View file

@ -0,0 +1,22 @@
import { Page } from "@playwright/test";
import { PartQuestionsPage } from "./PartQuestionPage";
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export default class VehiclePartQuestionsPage extends PartQuestionsPage{
constructor(page: Page) {
super(page);
}
@step("VehiclePartsPage >> Select Vehicle Part Questions: ")
async handleVehiclePartsPage(testCase: Partial<ITestData>) {
const { vehiclePartQuestions } = testCase;
await this.validateProgressBar(ProgressBarPercentages.VehiclePartsPage);
await this.validatePartQuestions(vehiclePartQuestions!);
await this.selectPartQuestionResponses(vehiclePartQuestions!);
await this.nextPage();
}
}

View file

@ -0,0 +1,69 @@
import { type Locator, type Page, expect, test } from '@playwright/test';
import { BasePage } from './BasePage';
import { IVehicleDetails } from 'safelite-playwright-core';
import { TestSuccessAlert } from 'safelite-playwright-core';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export class VehicleSelectionPage extends BasePage {
readonly page: Page;
readonly yearDropdown: Locator;
readonly makeDropdown: Locator;
readonly modelDropdown: Locator;
readonly styleDropdown: Locator;
readonly zipCodeTextBox: Locator;
readonly discontinuedServiceAlert: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.yearDropdown = this.page.locator('#yearQuestionField');
this.makeDropdown = this.page.locator('#makeQuestionField');
this.modelDropdown = this.page.locator('#modelQuestionField');
this.styleDropdown = this.page.locator('#styleQuestionField');
this.zipCodeTextBox = this.page.getByRole('textbox', { name: 'Zip code' });
this.discontinuedServiceAlert = this.page.locator('.alert-danger.widget-name-AlertNoServiceWidget');
}
async selectVehicle(vehicleDetails: IVehicleDetails) {
await this.yearDropdown.selectOption(vehicleDetails.year);
await this.yearDropdown.press('Tab');
await this.makeDropdown.selectOption(vehicleDetails.make);
await this.yearDropdown.press('Tab');
await this.modelDropdown.selectOption(vehicleDetails.model);
await this.yearDropdown.press('Tab');
if (vehicleDetails.style != undefined) {
await this.styleDropdown.selectOption(vehicleDetails.style);
}
}
async checkForAlertMessages() {
await expect(this.discontinuedServiceAlert).toBeVisible();
const alertMessage = await this.discontinuedServiceAlert.textContent();
await console.log(`Alert encountered: ${alertMessage}`);
await expect(alertMessage).toContain('Service not available in your areaWe do not offer glass service for your vehicle in your ZIP code. We apologize for the inconvenience.');
}
@step("VehicleSelectionPage >> Select Vehicle: ")
async handleVehicleSelectionPage(testData: Partial<ITestData>) {
await this.validateProgressBar(ProgressBarPercentages.VehicleSelectionPage);
const { vehicleDetails, isHeavyTruck, customerDetails } = testData;
const { isHeavyTruckVehicleAlert, isSplitWindshield } = testData.alertFlags || {};
await this.selectVehicle(vehicleDetails!);
if (isHeavyTruck) {
await this.zipCodeTextBox.fill(customerDetails?.address.postalCode!);
}
// Handle alert conditions for vehicle selection
if (isHeavyTruckVehicleAlert) {
await this.continueButton.click();
await this.checkForAlertMessages();
throw new TestSuccessAlert('Both assertions are met successfully.');
}
await this.nextPage();
}
}

View file

@ -0,0 +1,83 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { IClaimDetails, ICustomerDetails } from 'safelite-playwright-core';
import { InsuranceBasePage } from './InsuranceBasePage';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
export class VerifyDetailsPage extends InsuranceBasePage {
readonly page: Page;
readonly policyNumberTextBox: Locator;
readonly policyZipTextBox: Locator;
readonly damageDate: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.policyNumberTextBox = page.locator('#PolicyNumber');
this.policyZipTextBox = page.locator('#PolicyZip');
this.damageDate = page.locator('#LossDate');
}
async verifyPolicyDetails(customerDetails: ICustomerDetails, claimDetails: IClaimDetails): Promise<void> {
// Format the date from YYYY-MM-DD to MM/DD/YYYY if needed
const formattedDate = this.formatDate(claimDetails.damageDate);
// Check and fill policy number
await this.verifyAndFillField(
this.policyNumberTextBox,
claimDetails.policyNumber,
'Policy Number'
);
// Check and fill zip code
await this.verifyAndFillField(
this.policyZipTextBox,
customerDetails.address.postalCode,
'Policy ZIP'
);
// Check and fill damage date
await this.verifyAndFillField(
this.damageDate,
formattedDate,
'Damage Date'
);
}
private async verifyAndFillField(element: Locator, expectedValue: string, fieldName: string): Promise<void> {
await element.waitFor({ state: 'visible' });
const currentValue = await element.inputValue();
if (currentValue !== expectedValue) {
console.log(`${fieldName} requires correction: Current value '${currentValue}' doesn't match expected '${expectedValue}'`);
await element.clear();
await element.fill(expectedValue);
console.log(`${fieldName} updated to: ${expectedValue}`);
} else {
console.log(`${fieldName} verified: ${currentValue}`);
}
}
private formatDate(date: string): string {
// If the date is already in MM/DD/YYYY format, return it as is
if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(date)) {
return date;
}
// If the date is in YYYY-MM-DD format, convert it
if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(date)) {
const [year, month, day] = date.split('-');
return `${month}/${day}/${year}`;
}
// Return the original string if format is unknown
return date;
}
@step("VerifyDetailsPage >> Verify policy details: ")
async handleVerifyDetailsPage(testData: Partial<ITestData>) {
const { customerDetails, claimDetails } = testData;
await this.verifyPolicyDetails(customerDetails!, claimDetails!);
await this.nextPage();
}
}

View file

@ -0,0 +1,41 @@
import { type Locator, type Page } from '@playwright/test';
import { LookupPage } from './LookupPage';
import { step } from 'framework/localTypes/Step';
import { ITestData } from 'framework/TestData';
import { ProgressBarPercentages } from 'framework/localTypes/Enums';
export class VinLookupPage extends LookupPage {
readonly vinLookupTextBox: Locator;
readonly invalidVinInlineError: Locator;
readonly cameraButton: Locator;
readonly whereCanIFindMyVinLink: Locator;
constructor(page: Page) {
super(page);
this.vinLookupTextBox = page.getByRole('textbox', { name: 'Enter your VIN' });
this.invalidVinInlineError = this.page.locator('span.d-inline-flex.small.mt-1:has-text("Invalid VIN")');
this.cameraButton = this.page.getByRole('textbox', { name: 'Camera icon/button' });
this.whereCanIFindMyVinLink = this.page.getByRole('link', { name: 'Where can I find my VIN?', exact: true });
}
async enterVin(vin: string) {
await this.vinLookupTextBox.fill(vin);
}
@step("VinLookupPage >> Lookup by VIN: ")
async handleVehicleLookupVinPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
let vin: string;
if (Array.isArray(vehicleDetails?.vin)) {
vin = vehicleDetails.vin[0];
} else {
vin = vehicleDetails?.vin || '';
}
await this.validateProgressBar(ProgressBarPercentages.VinLookupPage);
await this.enterVin(vin);
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
}
}

View file

@ -0,0 +1,10 @@
import { type Page } from '@playwright/test';
import { LookupPage } from './LookupPage';
export class ZipLookupPage extends LookupPage {
constructor(page: Page) {
super(page);
}
// No additional methods needed as all functionality is inherited from LookupPage
}

View file

@ -0,0 +1,86 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from '../BasePage';
import { ICustomerDetails } from 'safelite-playwright-core';
import { waitUntil } from 'safelite-playwright-core';
export class AddressForm extends BasePage {
readonly page: Page;
readonly streetAddressTextBox: Locator;
readonly cityTextBox: Locator;
readonly stateDrpDwn: Locator;
readonly zipCodeTextBox: Locator;
readonly firstNameTextBox: Locator;
readonly lastNameTextBox: Locator;
readonly addressNotFoundMsg: Locator;
readonly addressSuggestionList: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.streetAddressTextBox = page.getByRole('textbox', { name: /Street (a|A)ddress$/ });
this.cityTextBox = page.getByRole('textbox', { name: 'City' });
this.stateDrpDwn = page.getByRole('combobox', { name: 'State' });
this.zipCodeTextBox = page.getByRole('textbox', { name: 'ZIP code' });
this.firstNameTextBox = page.getByRole('textbox', { name: 'First name' });
this.lastNameTextBox = page.getByRole('textbox', { name: 'Last name' });
this.addressNotFoundMsg = page.getByText('Address not found.');
this.addressSuggestionList = page.locator('.pac-container .pac-item').first();
}
async forceAddressFormToAppear() {
await expect(async () => {
await this.streetAddressTextBox.click();
await this.streetAddressTextBox.pressSequentially('7400 Safelite Way');
await this.streetAddressTextBox.press('Tab');
await expect(this.zipCodeTextBox).toBeVisible({ timeout: 100 });
}).toPass();
}
async populateAddress(customerDetails: Partial<ICustomerDetails>) {
if (customerDetails.address) {
// Force address form to appear
// await this.forceAddressFormToAppear();
await this.streetAddressTextBox.click();
await this.streetAddressTextBox.pressSequentially(`${customerDetails.address.street}, ${customerDetails.address.city}, ${customerDetails.address.state} ${customerDetails.address.postalCode}`).then( async() => {
await this.streetAddressTextBox.dispatchEvent('keydown', { key: 'ArrowLeft' } );
await this.streetAddressTextBox.dispatchEvent('keyup', { key: 'ArrowLeft' });
});
await waitUntil(async () => {
await this.addressSuggestionList.waitFor({ state: 'visible', timeout: 5000 });
let text = await this.addressSuggestionList.textContent() || '';
return (
text.includes(customerDetails.address!.street) &&
text.includes(customerDetails.address!.city) &&
text.includes(customerDetails.address!.state)
);
});
await this.page.hover(".pac-container .pac-item");
await new Promise(resolve => setTimeout(resolve, 1000));
await this.addressSuggestionList.click(); // Select the first suggestion
//await this.streetAddressTextBox.press('Tab'); // Move focus to ZIP code field
// Fill address
await this.stateDrpDwn.waitFor({ state: 'visible', timeout: 5000 }).then(async () => {
const selectedState = await this.stateDrpDwn.inputValue();
if (selectedState !== customerDetails.address!.state) {
await this.stateDrpDwn.selectOption(customerDetails.address!.state);
}
})
await this.fillAndValidate(this.streetAddressTextBox, customerDetails.address.street);
await this.fillAndValidate(this.zipCodeTextBox, customerDetails.address.postalCode)
await this.fillAndValidate(this.cityTextBox, customerDetails.address.city);
}
if (customerDetails.firstName) {
await this.fillAndValidate(this.firstNameTextBox, customerDetails.firstName);
}
if (customerDetails.lastName) {
await this.fillAndValidate(this.lastNameTextBox, customerDetails.lastName);
}
}
}

View file

@ -0,0 +1,16 @@
import { type Page } from '@playwright/test';
import { BasePage } from '../BasePage';
import { IVehicleDetails } from 'safelite-playwright-core';
export class VehicleSelectionForm extends BasePage {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
async selectVehicle(vehicleDetails: IVehicleDetails){
await this.page.locator('label').filter({hasText: vehicleDetails.model}).locator('div').first().click();
}
}

View file

@ -0,0 +1,149 @@
import { formatDateForFilename } from 'safelite-playwright-core';
import { defineConfig, devices } from '@playwright/test';
import { JiraReporterConfig } from 'playwright-jira-reporter'
import dotenv from 'dotenv-safe';
import { OrtoniReportConfig } from "ortoni-report";
import path from 'path';
if (!process.env.CI) {
// Environment variables are present in CI environment, no need to read from file
const basePath = __dirname; // This gets the directory where the config file is located
if (process.env.NODE_ENV == 'undefined' || process.env.NODE_ENV == null) {
dotenv.config({
path: path.join(basePath, '.env.dev'),
example: path.join(basePath, '.env.example')
});
}
else {
dotenv.config({
path: path.join(basePath, `.env.${process.env.NODE_ENV}`),
example: path.join(basePath, '.env.example')
});
}
}
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
// Ortoni config
const ortoniReportConfig: OrtoniReportConfig = {
open: "never",
folderPath: process.env.CI? 'ortoni-report': 'test-results',
title: "Test Report",
filename: `ortoni_report_${formatDateForFilename(new Date())}.html`,
showProject: false,
projectName: "FMG-Nextgen-Playwright-Report",
testType: `E2E- Environment: ${process.env.NODE_ENV} `,
preferredTheme: "light",
base64Image: true,
}
// Jira Report config
const jiraReportConfig: JiraReporterConfig = {
// Jira Reporter Config
isRegressionRun: process.env.IS_REGRESSION === 'true',
jiraProjectKey: process.env.JIRA_PROJECT_KEY || '',
jiraEpicKey: process.env.JIRA_EPIC_KEY || '',
jiraCardNumber: process.env.JIRA_CARD_NUMBER || '',
jiraApiUtilConfig: {
jiraUrl: process.env.JIRA_SERVER || '',
jiraUsername: process.env.JIRA_USERNAME || '',
jiraApiKey: process.env.JIRA_API_KEY || '',
jiraBoardId: process.env.JIRA_BOARD_ID || ''
},
jiraCreationPermissions: {
isCreateTestSubtasks: process.env.IS_REGRESSION !== 'true', // TODO: Change back to process.env.IS_REGRESSION !== 'true',
isCreateBugs: process.env.IS_REGRESSION !== 'true' // TODO: Change back to process.env.IS_REGRESSION !== 'true'
},
// Wrapped ortoni config
...ortoniReportConfig
};
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* 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,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 4 : 5,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: process.env.CI? [
['junit'],
['playwright-jira-reporter', jiraReportConfig],
['ortoni-report', ortoniReportConfig]
]: [
['ortoni-report', ortoniReportConfig],
['junit'],
['list']
],
timeout: 240_000,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
headless: process.env.CI ? true : false,
screenshot: "only-on-failure",
actionTimeout: 60_000,
navigationTimeout: 60_000,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View file

@ -0,0 +1,393 @@
import { Page } from "@playwright/test";
import { addSmokeTagToRandomTest, Flow } from 'safelite-playwright-core';
import { ValidationOptions } from 'safelite-playwright-core';
import heavyTruckTests from "./alert-validation/alert0001_HeavyTruck";
import repairAndReplaceTests from "./alert-validation/alert0002_RepairAndReplace";
import splitWindshieldTests from "./alert-validation/alert0003_SplitWindshield";
import repairOnlyTests from "./alert-validation/alert0004_RepairOnly";
import unserviceableZipTests from "./alert-validation/alert0005_UnserviceableZip";
import invalidZipTests from "./alert-validation/alert0006_InvalidZip";
import vinNotFoundTests from "./alert-validation/alert0007_VinNotFound";
import { TestSuccessAlert } from 'safelite-playwright-core';
import { VehicleLookupType, VehicleDamage, PaymentType } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import cashRepairMobileCCTests from "./CashRepairMobileCreditCard";
import cashReplaceDynamicRecalMobileTests from "./CashReplaceDynamicRecalMobile";
import cashReplaceGlassAddressLookupInshopAfterPayTests from "./CashReplaceGlassAddressLookupInshopAfterPay";
import cashReplaceGlassLicensePlateLookupInshopPaypalTests from "./CashReplaceGlassLicensePlateLookupInshopPaypal";
import { ApiResponseInterceptUtil } from 'safelite-playwright-core';
import cashReplaceGlassPromoInshopTests from "./CashReplaceGlassPromoInshop";
import cashReplaceMultiGlassPromoInshopTests from "./CashReplaceMultiGlassPromoInshop";
import cashReplaceRainDefensePromoInshopTests from "./CashReplaceRainDefensePromoInshop";
import cashReplaceSafeliteCanNotRecalMobileTests from "./CashReplaceSafeliteCanNotRecalMobile";
import cashReplaceVinMobileTests from "./CashReplaceVinMobile";
import cashReplaceWiperDropoffTests from "./CashReplaceWiperDropoff";
import cashReplaceWiperPromoInShopTests from "./CashReplaceWiperPromoInshop";
import insuranceAcuityPaypalTests from "./InsuranceAcuityPaypal";
import insuranceITAC21stCenturyTests from "./InsuranceITAC21stCentury";
import insuranceGeicoTests from "./InsuranceGeico";
import insuranceITACOptimizedPriceValidationAllStateTests from "./InsuranceITACOptimizedPriceValidationAllState";
import insuranceNoCompProgressiveTests from "./InsuranceNoCompProgressive";
import cashRepairInShopAfterPayTests from "./CashRepairInShopAfterPay";
import cashRepairInShopPayPalTests from "./CashRepairInShopPayPal";
import cashReplaceMultiSlidingGlassDropoffTests from "./CashReplaceMultiSlidingGlassDropoff";
import cashReplaceMultiGlassMobileTests from "./CashReplaceMultiGlassMobile";
import { getTestObject, TestCase, prepareTest, RuleEngine, TestInfo } from 'framework/Typedefs';
import { createTestPages } from "framework/TestPages";
import cashReplaceSwitchToInsuranceProgressiveNoCompTests from "./CashReplaceSwitchToInsuranceProgressiveNoComp";
import insuranceBigTruckVerifiedTests from "./InsuranceBigTruckVerified";
import insuranceUnverifiedTests from "./InsuranceUnverified";
import CashReplaceSplitWindshieldTests from "./CashReplaceSplitWindshield";
import insuranceMeemicNearSchoolVerifiedTests from "./InsuranceMeemicNearSchoolVerified";
const test = getTestObject();
/**
* Master Test Runner
*
* This file orchestrates the execution of all test scenarios and defines the primary test workflow.
* It imports all test cases and executes them through a common test runner function to ensure
* consistent behavior across different test scenarios.
*/
// Initialize shared rule engine and validation options
const ruleEngine = new RuleEngine();
const options = new ValidationOptions();
// Add smoke tag to selected tests for CI/CD pipelines
addSmokeTagToRandomTest(splitWindshieldTests);
// Standard test scenarios
const allStandardTests = [
{name: "CashRepairMobileCreditCard", tests: cashRepairMobileCCTests},
{name: "CashRepairInShopAfterPay", tests: cashRepairInShopAfterPayTests},
{name: "CashRepairInShopPayPal", tests: cashRepairInShopPayPalTests},
{name: "CashReplaceDynamicRecalMobile", tests: cashReplaceDynamicRecalMobileTests},
{name: "CashReplaceGlassAddressLookupInshopAfterPay", tests: cashReplaceGlassAddressLookupInshopAfterPayTests},
{name: "CashReplaceGlassLicensePlateLookupInshopPaypal", tests: cashReplaceGlassLicensePlateLookupInshopPaypalTests},
{name: "CashReplaceGlassPromoInShop", tests: cashReplaceGlassPromoInshopTests},
{name: "CashReplaceMultiGlassPromoInShop", tests: cashReplaceMultiGlassPromoInshopTests},
{name: "CashReplaceMultiSlidingGlassDropoff",tests: cashReplaceMultiSlidingGlassDropoffTests},
{name: "CashReplaceMultiGlassMobile",tests: cashReplaceMultiGlassMobileTests},
{name: "CashReplaceRainDefensePromoInshop", tests: cashReplaceRainDefensePromoInshopTests},
{name: "CashReplaceSafeliteCanNotRecalMobile", tests: cashReplaceSafeliteCanNotRecalMobileTests},
{name: "CashReplaceVinMobile", tests: cashReplaceVinMobileTests},
{name: "CashReplaceWiperDropoff", tests: cashReplaceWiperDropoffTests},
{name: "CashReplaceWiperPromoInshop", tests: cashReplaceWiperPromoInShopTests},
{name: "CashReplaceSwitchToInsuranceProgressiveNoComp", tests: cashReplaceSwitchToInsuranceProgressiveNoCompTests},
// TODO: Uncomment when QA is ready to run heavy truck tests
// {name: "CashReplaceSplitWindshield", tests: CashReplaceSplitWindshieldTests},
{name: "InsuranceAcuityPaypal", tests: insuranceAcuityPaypalTests},
{name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests},
{name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests},
// {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests},
{name: "InsuranceUnverified", tests: insuranceUnverifiedTests},
// {name: "InsuranceGeico", tests: insuranceGeicoTests},
// {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests}
{name: "InsuranceMeemicNearSchoolVerified", tests: insuranceMeemicNearSchoolVerifiedTests},
];
// Alert validation scenarios
const allAlertTests = [
//TODO: Uncomment when QA is ready to run heavy truck tests
// { name: "Alert Scenario 1: Heavy Truck", tests: heavyTruckTests },
{ name: "Alert Scenario 2: Repair and Replace", tests: repairAndReplaceTests },
// { name: "Alert Scenario 3: Split Windshield", tests: splitWindshieldTests },
{ name: "Alert Scenario 4: Repair Only", tests: repairOnlyTests },
{ name: "Alert Scenario 5: Unserviceable Zip", tests: unserviceableZipTests },
{ name: "Alert Scenario 6: Invalid Zip", tests: invalidZipTests },
{ name: "Alert Scenario 7: VIN Not Found", tests: vinNotFoundTests }
];
// Standard test cases
test.describe.parallel('Standard E2E Test Flows', () => {
allStandardTests.forEach(scenario => {
scenario.tests.forEach(testCase => {
test(...prepareTest(testCase, createTestPages, run, options, ruleEngine));
});
});
});
// Alert validation test cases
test.describe.parallel('Alert Validation Tests', () => {
allAlertTests.forEach(scenario => {
test.describe(scenario.name, () => {
scenario.tests.forEach(testCase => {
test(...prepareTest(testCase, createTestPages, run, options, ruleEngine));
});
});
});
});
/**
* After each test, capture screenshot and handle cleanup
*/
test.afterEach(async ({ page, testInfo }) => {
await TestCase.afterEachMethod(page, testInfo);
});
/**
* Main test runner function that executes each test case
* @param page - The Playwright page object
* @param testInfo - Test information and context
*/
async function run(page: Page, testInfo: TestInfo): Promise<void> {
try {
await testInfo.testCase.setup();
testInfo.testCase.setupPages(page, createTestPages);
await testInfo.testCase.pages.homePage.goto();
await runWorkflow(page, testInfo.testCase);
} catch (error) {
// Catch successful alert tests and log message
if (error instanceof TestSuccessAlert) {
console.log(error.message);
// Catch and throw other errors
} else {
throw error;
}
}
}
/**
* Main workflow that executes the test steps in sequence
*
* This function contains the common test flow for all test scenarios:
* 1. Start at home page and enter vehicle/damage information
* 2. Select lookup method and provide details
* 3. Answer part questions if applicable
* 4. Select service package and payment method
* 5. Handle insurance flow if applicable
* 6. Select service location and schedule appointment
* 7. Complete order and validate confirmation
*
* @param page - The Playwright page object
* @param testCase - The test case to execute
*/
async function runWorkflow(page: Page, testCase: TestCase) {
// Intercept API Responses
const apiResponseInterceptUtil = new ApiResponseInterceptUtil(testCase.testData);
page.on('response', apiResponseInterceptUtil.handleInterceptResponse);
// Destructure test data for easier access
const {
paymentMethod, customerDetails, vehicleDetails, vehicleDamage,
paymentDetails, partQuestions, isEnterFunnelWithZip, capabilityQuestions,
vehiclePartQuestions, moldingQuestions, isSkipEstimatePage
} = testCase.testData;
// Check if the vehicle damage includes a windshield crack
const hasWindshieldCrack = vehicleDamage!.some(damage =>
damage === VehicleDamage.WindshieldCrack ||
damage === VehicleDamage.DriverSplitWindshield ||
damage === VehicleDamage.PassengerSplitWindshield ||
damage === VehicleDamage.SingleSplitWindshield
);
//============================= TEST WORKFLOW STEPS =============================
// Use Environment Variable to decide whether or not we want to skip content site aka home page
if (process.env.SKIP_CONTENT_SITE == "false") {
await test.step('HomePage >> Lets Get Started', async () => {
let homePage = testCase.pages.homePage;
console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`);
await homePage.letsGetStarted(customerDetails?.address.postalCode!, !!isEnterFunnelWithZip!);
});
} else {
// If enterFunnelWithZip is true, add zip code to url
if (!!isEnterFunnelWithZip!) {
const currentUrl = process.env['BASE_URL']!;
const zipParam = `&zipCode=${customerDetails?.address.postalCode!}`;
if (!currentUrl.includes('zipCode=')) {
await page.goto(currentUrl + zipParam);
}
}
console.log(`Customer for this test: ${customerDetails?.firstName} ${customerDetails?.lastName}`);
}
// Handle vehicle selection page
let vehicleSelectionPage = testCase.pages.vehicleSelectionPage;
await vehicleSelectionPage.handleVehicleSelectionPage(testCase.testData);
// Handle vehicle damage page
let vehicleDamagePage = testCase.pages.vehicleDamagePage;
await vehicleDamagePage.handleVehicleDamagePage(testCase.testData);
// If the vehicle has a windshield crack as part of its damage, go to estimate page and select lookup type
if (hasWindshieldCrack && !isSkipEstimatePage) {
let estimatePage = testCase.pages.estimatePage;
await estimatePage.handleEstimatePage(testCase.testData);
// Handle different vehicle lookup methods
switch (vehicleDetails!.vehicleLookupType!) {
case VehicleLookupType.Address:
let vehicleLookupAddressPage = testCase.pages.vehicleLookupAddressPage;
await vehicleLookupAddressPage.handleVehicleLookupAddressPage(testCase.testData);
break;
case VehicleLookupType.LicensePlateNumber:
let vehicleLookupLicensePage = testCase.pages.vehicleLookupLicensePage;
await vehicleLookupLicensePage.handleVehicleLookupLicensePage(testCase.testData);
break;
case VehicleLookupType.Vin:
let vinLookupPage = testCase.pages.vinLookupPage;
await vinLookupPage.handleVehicleLookupVinPage(testCase.testData);
break;
case VehicleLookupType.Zip:
let serviceZipPage = testCase.pages.serviceZipPage;
await serviceZipPage.handleServiceZipPage(testCase.testData);
break;
}
} else {
// Otherwise just use zip lookup for service zip page
let serviceZipPage = testCase.pages.serviceZipPage;
await serviceZipPage.handleServiceZipPage(testCase.testData);
}
// Handle part questions if applicable
if (partQuestions && partQuestions.length > 0) {
let partQuestionsPage = testCase.pages.partQuestionsPage;
await partQuestionsPage.handlePartQuestionsPage(testCase.testData);
}
// Handle molding questions if applicable
if (moldingQuestions && moldingQuestions.length > 0) {
let moldingQuestionsPage = testCase.pages.moldingQuestionsPage;
await moldingQuestionsPage.handleMoldingQuestionsPage(testCase.testData);
}
// Handle vehicle part questions if applicable
if (vehiclePartQuestions && vehiclePartQuestions.length > 0) {
let vehiclePartsPage = testCase.pages.vehiclePartsPage;
await vehiclePartsPage.handleVehiclePartsPage(testCase.testData);
}
// Handle capability questions if applicable
if (capabilityQuestions && capabilityQuestions.length > 0) {
let capabilityQuestionsPage = testCase.pages.capabilityQuestionsPage;
await capabilityQuestionsPage.handleCapabilityQuestionsPage(testCase.testData);
}
// Select service package and payment method
let servicePackagesPage = testCase.pages.servicePackagesPage;
await servicePackagesPage.handleServicePackagePage(testCase.testData);
//============================= INSURANCE FLOW =============================`
// Insurance flow - if user selected Insurance as Payment Method
if (paymentMethod == PaymentMethod.Insurance) {
await handleInsuranceFlow(testCase);
}
//============================= SERVICE SCHEDULING =============================
// Select service location
let serviceLocationPage = testCase.pages.serviceLocationPage;
await serviceLocationPage.handleServiceLocationPage(testCase.testData);
// Schedule appointment
let schedulePage = testCase.pages.schedulePage;
await schedulePage.handleSchedulePage(testCase.testData);
// Enter contact details
let contactDetailsPage = testCase.pages.contactDetailsPage;
await contactDetailsPage.handleContactDetailsPage(testCase.testData);
//============================= PAYMENT PROCESSING =============================
// Handle payment
let paymentMethodPage = testCase.pages.paymentMethodPage;
await paymentMethodPage.handlePaymentMethodPage(testCase.testData);
// If user selected Pay with Insurance as Payment Method, Enter Insurance Flow
if (paymentDetails?.paymentType === PaymentType.PayWithInsurance) {
await handleInsuranceFlow(testCase);
}
//============================= ORDER CONFIRMATION =============================
// Validate order confirmation
let orderConfirmationPage = testCase.pages.orderConfirmationPage;
await orderConfirmationPage.verifyOrderConfirmationPage(testCase.testData);
}
export async function handleInsuranceFlow(testCase: TestCase) {
const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, flow } = testCase.testData;
// Check if the insurance policy has endorsements
const hasEndorsements = endorsements && endorsements.length > 0;
// Handle insurance company page
let insuranceCompanyPage = testCase.pages.insuranceCompanyPage;
await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData);
// Handle ccPolicyInfoPage
let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage;
await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData);
let duplicateCheckPage = testCase.pages.duplicateCheckPage;
if (duplicateCheckPage.page.url().includes('DuplicateCheck.aspx')) {
await duplicateCheckPage.handleDuplicateCheckPage(testCase.testData);
}
if ( flow === Flow.Managed ) {
let policyVehiclesPage = testCase.pages.policyVehiclesPage;
await policyVehiclesPage.handlePolicyVehiclesPage(testCase.testData);
// Handle policy driver selection if applicable
if (isPolicyDriver) {
let policyDriverPage = testCase.pages.policyDriverPage;
await policyDriverPage.handlePolicyDriverPage(testCase.testData);
}
// Handle endorsements if applicable
if (hasEndorsements) {
let endorsementsPage = testCase.pages.endorsementsPage;
await endorsementsPage.handleEndorsementsPage(testCase.testData);
}
} else {
//Handle verify details page
let verifyDetailsPage = testCase.pages.verifyDetailsPage;
await verifyDetailsPage.handleVerifyDetailsPage(testCase.testData);
}
// Handle Policy info submitted page
let policyInfoSubmittedPage = testCase.pages.policyInfoSubmittedPage;
await policyInfoSubmittedPage.handlePolicyInfoSubmittedPage();
if(isRecalVehicle)
{
let recalibrationInfoPage = testCase.pages.recalibrationInfoPage;
await recalibrationInfoPage.handleRecalibrationInfoPage();
}
//handle coveraage statement page
let coverageStatementPage = testCase.pages.coverageStatementPage;
await coverageStatementPage.handleCoverageStatementPage(testCase.testData);
// Handle scenario where user selected Cash to Insurance and needs to go back through the flow
// right now we are choosing pay at appointment as payment method for this scenario
if (isCashInsuranceFlow) {
let serviceLocationPage = testCase.pages.serviceLocationPage;
await serviceLocationPage.nextPage();
//schedule
let schedulePage = testCase.pages.schedulePage;
await schedulePage.nextPage();
//customer dertails
let contactDetailsPage = testCase.pages.contactDetailsPage;
await contactDetailsPage.nextPage();
//paymentmethods
let paymentMethodPage = testCase.pages.paymentMethodPage;
await paymentMethodPage.nextPage();
}
}

View file

@ -0,0 +1,50 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { VehicleDamage, ServiceLocation, ServicePackage } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { ClientData } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashRepairInShopAfterPay");
// Now get the test data with the seeded faker
const cashRepairInShopAfterPayData : Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
//Override default vehicle damage (Windshield Crack)
vehicleDamage: [VehicleDamage.WindshieldTwoChips],
// Key feature: Standard Package
servicePackage: ServicePackage.Standard,
// Override specific fields with test-specific data
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
vin: '1HGCV2E35LA005448'
},
// Override appointment details
appointmentDetails: {
...getDefaultTestData().appointmentDetails!,
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
},
// Use predefined payment data
paymentDetails: ClientData.getDefaultAfterpayDetails()
}
const cashRepairInShopAfterPayTests: ITestCase[] = [];
const tc = {
name: `CashRepairInShopAfterPay`,
tags: ['@E2E','@CashRepairInShopAfterPay', '@test_report', '@CASH', '@CASH-1187'],
testData: cashRepairInShopAfterPayData
};
cashRepairInShopAfterPayTests.push(tc);
export default cashRepairInShopAfterPayTests;

View file

@ -0,0 +1,50 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { VehicleDamage, ServicePackage } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { ClientData } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashRepairInShopPayPal");
// Now get the test data with the seeded faker
const cashRepairInShopPayPalData : Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
//Override default vehicle damage (Windshield Crack)
vehicleDamage: [VehicleDamage.WindshieldThreeChips],
// Key feature: Standard Package
servicePackage: ServicePackage.Premium,
// Override specific fields with test-specific data
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
vin: '1HGCV2E35LA005448'
},
// Override appointment details
appointmentDetails: {
...getDefaultTestData().appointmentDetails!,
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
},
// Use predefined payment data
paymentDetails: ClientData.getDefaultPaypalDetails()
}
const cashRepairInShopPayPalTests: ITestCase[] = [];
const tc = {
name: `CashRepairInShopPayPal`,
tags: ['@E2E','@CashRepairInShopPayPal', '@test_report', '@CASH'],
testData: cashRepairInShopPayPalData
};
cashRepairInShopPayPalTests.push(tc);
export default cashRepairInShopPayPalTests;

View file

@ -0,0 +1,63 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { VehicleDamage, ServiceLocation } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { ClientData } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashRepairMobileCreditCard");
// Now get the test data with the seeded faker
const cashRepairMobileCCData : Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Override default vehicle damage (Windshield Crack)
vehicleDamage: [VehicleDamage.WindshieldOneChip],
// Override customer postal code
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '91710'
}
},
// Override specific fields with test-specific data
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
vin: '1HGCV2E35LA005448'
},
// Override appointment details
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
serviceAddress: {
street: '13735 San Antonio Ave',
city: 'Chino',
state: 'CA',
postalCode: '91710',
country: 'United States'
}
},
// Use predefined payment data
paymentDetails: ClientData.getDefaultCreditCardDetails()
}
const cashRepairMobileCCTests: ITestCase[] = [];
const tc = {
name: `CashRepairMobileCreditCard`,
tags: ['@E2E','@CashRepairMobileCreditCard', '@test_report', '@CASH'],
testData: cashRepairMobileCCData
};
cashRepairMobileCCTests.push(tc);
export default cashRepairMobileCCTests;

View file

@ -0,0 +1,75 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceDynamicRecalMobile");
// Now get the test data with the seeded faker
const cashReplaceDynamicRecalMobileData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Flag for recalibration vehicle
isRecalVehicle: true,
// Flag for dynamic Recalibration vehicle
isDynamicRecal: true,
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '21237'
}
},
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2018',
make: 'Ford',
model: 'Expedition',
style: '4 door utility',
vin: '1FMJU1JT2JEA59123',
vehicleLookupType: VehicleLookupType.Vin
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override appointment details
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
serviceAddress: {
// Use street address from current faker seed
street: "5050 Silver Oak Dr",
city: 'Rosedale',
state: 'MD',
postalCode: '21237',
country: 'United States'
},
},
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService
}
}
const cashReplaceDynamicRecalMobileTests: ITestCase[] = [];
const tc = {
name: `CashReplaceDynamicRecalMobile`,
tags: ['@E2E','@CashReplaceDynamicRecalMobile', '@test_report', '@CASH'],
testData: cashReplaceDynamicRecalMobileData
};
cashReplaceDynamicRecalMobileTests.push(tc);
export default cashReplaceDynamicRecalMobileTests;

View file

@ -0,0 +1,66 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { ClientData } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceGlassAddressLookupInshopAfterPay");
// Now get the test data with the seeded faker
const cashReplaceGlassAddressLookupInshopAfterPayData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Enable entering funnel with ZIP
isEnterFunnelWithZip: true,
// Override customer details
customerDetails: {
...getDefaultTestData().customerDetails!,
lastName: 'Patel',
address: {
street: '4076 Spectacle Dr',
city: 'Columbus',
state: 'Ohio',
postalCode: '43230',
country: 'United States'
}
},
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2013',
make: 'Hyundai',
model: 'Sonata',
style: '4 door sedan',
vehicleLookupType: VehicleLookupType.Address
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override appointment details
appointmentDetails: {
...getDefaultTestData().appointmentDetails!,
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
},
// Override payment details
paymentDetails: ClientData.getDefaultAfterpayDetails()
}
const cashReplaceGlassAddressLookupInshopAfterPayTests: ITestCase[] = [];
const tc = {
name: `CashReplaceGlassAddressLookupInshopAfterPay`,
tags: ['@E2E','@CashReplaceGlassAddressLookupInshopAfterPay', '@test_report', '@CASH'],
testData: cashReplaceGlassAddressLookupInshopAfterPayData
};
cashReplaceGlassAddressLookupInshopAfterPayTests.push(tc);
export default cashReplaceGlassAddressLookupInshopAfterPayTests;

View file

@ -0,0 +1,57 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { ClientData } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceGlassLicensePlateLookupInshopPaypal");
// Now get the test data with the seeded faker
const cashReplaceGlassLicensePlateLookupInshopPaypalData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Override customer details - using a different ZIP
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
street: '4076 Spectacle Dr',
city: 'Columbus',
state: 'Ohio',
postalCode: '44125',
country: 'United States'
}
},
// Override vehicle details with license plate lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2013',
make: 'Hyundai',
model: 'Sonata',
style: '4 door sedan',
licensePlate: { licensePlateNumber: 'FTY 7776', licensePlateState: '' },
vehicleLookupType: VehicleLookupType.LicensePlateNumber
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override payment details to use PayPal
paymentDetails: ClientData.getDefaultPaypalDetails()
}
const cashReplaceGlassLicensePlateLookupInshopPaypalTests: ITestCase[] = [];
const tc = {
name: `CashReplaceGlassLicensePlateLookupInshopPaypal`,
tags: ['@E2E','@CashReplaceGlassLicensePlateLookupInshopPaypal', '@test_report', '@CASH'],
testData: cashReplaceGlassLicensePlateLookupInshopPaypalData
};
cashReplaceGlassLicensePlateLookupInshopPaypalTests.push(tc);
export default cashReplaceGlassLicensePlateLookupInshopPaypalTests;

View file

@ -0,0 +1,58 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceGlassPromoInshop");
// Now get the test data with the seeded faker
const cashReplaceGlassPromoInshopData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Flag for recalibration vehicle
isRecalVehicle: true,
// Override vehicle details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2020',
make: 'Ford',
model: 'Fusion',
style: '4 door sedan',
vin: '3FA6P0HD8LR234510',
vehicleLookupType: VehicleLookupType.Vin
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override appointment details for in-shop service
appointmentDetails: {
...getDefaultTestData().appointmentDetails!,
shopAddress: "6826 Sawmill Rd, Columbus, OH 43235"
},
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService,
// Add promo code - key feature of this test
promoCode: '20CALL',
}
}
const cashReplaceGlassPromoInshopTests: ITestCase[] = [];
const tc = {
name: `CashReplaceGlassPromoInshop`,
tags: ['@E2E','@CashReplaceGlassPromoInshop', '@test_report', '@CASH'],
testData: cashReplaceGlassPromoInshopData
};
cashReplaceGlassPromoInshopTests.push(tc);
export default cashReplaceGlassPromoInshopTests;

View file

@ -0,0 +1,103 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { VehicleDamage, PartQuestionType, PaymentType, ServicePackage, ServiceLocation } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceMultiGlassMobile");
// Now get the test data with the seeded faker
const cashReplaceMultiGlassMobileData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '21237'
}// Override customer postal code
},
// Flag for recalibration vehicle
isRecalVehicle: true,
// Key feature: Premium Package
servicePackage: ServicePackage.Premium,
// Override appointment details
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
serviceAddress: {
// Use street address from current faker seed
street: "5050 Silver Oak Dr",
city: 'Rosedale',
state: 'MD',
postalCode: '21237',
country: 'United States'
},
},
// Override vehicle details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2014',
make: 'Jeep',
model: 'Cherokee',
style: '4 door utility',
vin: '1C4PJLCS6EW288461',
vehicleLookupType: VehicleLookupType.Vin
},
// Key feature: multiple damaged glasses
vehicleDamage: [
VehicleDamage.WindshieldCrack,
VehicleDamage.DriverQuarterPanel,
VehicleDamage.PassengerQuarterPanel,
],
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService
},
// Vehicle part questions for multiple glass parts
vehiclePartQuestions: [
{
partQuestionType: PartQuestionType.WindshieldColor,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'solar'
},
{
partQuestionType: PartQuestionType.DriverQuarterColor,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'solar, driver side, encap, chrome molding'
},
{
partQuestionType: PartQuestionType.PassengerQuarterColor,
isOnPage: true,
optionToSelect: 'Gray Tint Privacy',
secondaryQuestionOptionToSelect: 'solar, passenger side, encap, chrome molding'
}
]
}
const cashReplaceMultiGlassMobileTests: ITestCase[] = [];
const tc = {
name: `CashReplaceMultiGlassMobile`,
tags: ['@E2E','@CashReplaceMultiGlassMobile', '@test_report', '@CASH'],
testData: cashReplaceMultiGlassMobileData
};
cashReplaceMultiGlassMobileTests.push(tc);
export default cashReplaceMultiGlassMobileTests;

View file

@ -0,0 +1,109 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { VehicleDamage, PartQuestionType, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceMultiGlassPromoInshop");
// Now get the test data with the seeded faker
const cashReplaceMultiGlassPromoInshopData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Flag for recalibration vehicle
isRecalVehicle: true,
// Override customer details - different postal code
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '43085'
}
},
// Override vehicle details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2019',
make: 'Subaru',
model: 'Outback',
style: '4 door station wagon',
vin: '4S4BSENC4K3221004',
vehicleLookupType: VehicleLookupType.Vin
},
// Key feature: multiple damaged glasses
vehicleDamage: [
VehicleDamage.WindshieldCrack,
VehicleDamage.DriverFrontDoor,
VehicleDamage.DriverRearDoor,
VehicleDamage.DriverVentGlass,
VehicleDamage.PassengerVentGlass,
VehicleDamage.RearWindow
],
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService,
// Add promo code
promoCode: '20CALL',
},
// Vehicle part questions for multiple glass parts
vehiclePartQuestions: [
{
partQuestionType: PartQuestionType.WindshieldColor,
isOnPage: true,
optionToSelect: 'Green Tint, Blue Shade',
secondaryQuestionOptionToSelect: 'solar, lane departure warning system, heated glass wiper park, high beam assist, soundproofing'
},
{
partQuestionType: PartQuestionType.DriverFrontColor,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'solar, driver side, front, soundproofing, laminated'
},
{
partQuestionType: PartQuestionType.DriverRearColor,
isOnPage: true,
optionToSelect: 'Gray Tint Privacy',
secondaryQuestionOptionToSelect: 'solar, driver side, rear'
},
{
partQuestionType: PartQuestionType.DriverVentColor,
isOnPage: true,
optionToSelect: 'Gray Tint Privacy',
secondaryQuestionOptionToSelect: 'solar, driver side, rear'
},
{
partQuestionType: PartQuestionType.PassengerVentColor,
isOnPage: true,
optionToSelect: 'Gray Tint Privacy',
secondaryQuestionOptionToSelect: 'solar, passenger side, rear'
},
{
partQuestionType: PartQuestionType.RearWindowColor,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'heated glass, solar, antenna, manual liftgate, 1 hole'
}
]
}
const cashReplaceMultiGlassPromoInshopTests: ITestCase[] = [];
const tc = {
name: `CashReplaceMultiGlassPromoInshop`,
tags: ['@E2E','@CashReplaceMultiGlassPromoInshop', '@test_report', '@CASH'],
testData: cashReplaceMultiGlassPromoInshopData
};
cashReplaceMultiGlassPromoInshopTests.push(tc);
export default cashReplaceMultiGlassPromoInshopTests;

View file

@ -0,0 +1,119 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { VehicleDamage, PartQuestionType, PaymentType, ServicePackage, ServiceLocation } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceMultiSlidingGlassDropoff");
// Now get the test data with the seeded faker
const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Override customer postal code
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '43085'
}
},
// Flag for recalibration vehicle
isRecalVehicle: true,
// Key feature: Standard Package
servicePackage: ServicePackage.Standard,
// Override for drop-off service
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2018',
make: 'Ford',
model: 'F Series F150',
style: '4 door crew cab',
vehicleLookupType: VehicleLookupType.Zip
},
// Key feature: multiple damaged glasses
vehicleDamage: [
VehicleDamage.WindshieldCrack,
VehicleDamage.PassengerFrontDoor,
VehicleDamage.PassengerRearDoor,
VehicleDamage.RearSliding
],
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService
},
// Part questions related to rain sensing
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
],
// Capability Questions
capabilityQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
],
// Vehicle part questions for multiple glass parts
vehiclePartQuestions: [
{
partQuestionType: PartQuestionType.WindshieldColor,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'rain sensor, solar, soundproofing, third visor frit, lane departure warning system, heated glass wiper park, w/combination bracket'
},
{
partQuestionType: PartQuestionType.PassengerFrontColor,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'solar, passenger side, front, laminated, soundproofing'
},
{
partQuestionType: PartQuestionType.PassengerRearColor,
isOnPage: true,
optionToSelect: 'Gray Tint Privacy',
secondaryQuestionOptionToSelect: 'solar, passenger side, rear, platinum edition, 2 hole'
},
{
partQuestionType: PartQuestionType.RearSlidingWindowColor,
isOnPage: true,
optionToSelect: 'Gray Tint Privacy',
secondaryQuestionOptionToSelect: 'heated glass, solar, slider, power, kit'
}
]
}
const cashReplaceMultiSlidingGlassDropoffTests: ITestCase[] = [];
const tc = {
name: `CashReplaceMultiSlidingGlassDropoff`,
tags: ['@E2E','@CashReplaceMultiSlidingGlassDropoff', '@test_report', '@CASH'],
testData: cashReplaceMultiSlidingGlassDropoffData
};
cashReplaceMultiSlidingGlassDropoffTests.push(tc);
export default cashReplaceMultiSlidingGlassDropoffTests;

View file

@ -0,0 +1,64 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServicePackage, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceRainDefensePromoInshop");
// Now get the test data with the seeded faker
const cashReplaceRainDefensePromoInshopData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Key feature: Premium package with Rain Defense
servicePackage: ServicePackage.Premium,
// Flag for recalibration vehicle
isRecalVehicle: true,
// Override customer details
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '43085'
}
},
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2020',
make: 'Ford',
model: 'Fusion',
style: '4 door sedan',
vin: '3FA6P0HD8LR234510',
vehicleLookupType: VehicleLookupType.Vin
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Payment at service
paymentDetails: {
paymentType: PaymentType.PayAtService,
// Rain Defense promo code
promoCode: 'rd50'
},
}
const cashReplaceRainDefensePromoInshopTests: ITestCase[] = [];
const tc = {
name: `CashReplaceRainDefensePromoInshop`,
tags: ['@E2E','@CashReplaceRainDefensePromoInshop', '@test_report', '@CASH'],
testData: cashReplaceRainDefensePromoInshopData
};
cashReplaceRainDefensePromoInshopTests.push(tc);
export default cashReplaceRainDefensePromoInshopTests;

View file

@ -0,0 +1,85 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, PartQuestionType, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceSafeliteCanNotRecalMobile");
// Now get the test data with the seeded faker
const cashReplaceSafeliteCanNotRecalMobileData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Special flag to skip estimate page
isSkipEstimatePage: true,
// Special flag to verify safelite can not recalibrate in the backend
isCanNotRecal: true,
// Override customer postal code
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '21237'
}
},
// Override vehicle details - vehicle with recalibration that Safelite cannot perform
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2009',
make: 'Volkswagen',
model: 'Passat CC',
style: '4 door sedan',
vin: 'VWML73C79E552439',
vehicleLookupType: VehicleLookupType.Zip
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override for mobile service
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
serviceAddress: {
// Use street address from current faker seed
street: "5050 Silver Oak Dr",
city: 'Rosedale',
state: 'MD',
postalCode: '21237',
country: 'United States'
},
},
// Payment at service
paymentDetails: {
paymentType: PaymentType.PayAtService
},
// Part questions related to recalibration
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
]
}
const cashReplaceSafeliteCanNotRecalMobileTests: ITestCase[] = [];
const tc = {
name: `CashReplaceSafeliteCanNotRecalMobile`,
tags: ['@E2E','@CashReplaceSafeliteCanNotRecalMobile', '@test_report', '@CASH'],
testData: cashReplaceSafeliteCanNotRecalMobileData
};
cashReplaceSafeliteCanNotRecalMobileTests.push(tc);
export default cashReplaceSafeliteCanNotRecalMobileTests;

View file

@ -0,0 +1,74 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { PartQuestionType, PaymentType, VehicleDamage } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed before generating any data
setFakerSeedFromTestName("CashReplaceSplitWindshield.ts");
// Now get the test data with the seeded faker
const CashReplaceSplitWindshieldData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
isHeavyTruck: true, // Flag for big truck
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '55414'
}
},
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2006',
make: 'Navistar',
model: '5000 I',
style: '2 door conventional cab',
vehicleLookupType: VehicleLookupType.Zip
},
vehiclePartQuestions: [
{
partQuestionType: PartQuestionType.DriverWindshield,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'driver side, encap, asymmetrically strengthen'
},
{
partQuestionType: PartQuestionType.PassengerWindshield,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'passenger side, encap, asymmetrically strengthen'
},
],
// Override default vehicle damage (Split Windshield)
vehicleDamage: [VehicleDamage.DriverSplitWindshield, VehicleDamage.PassengerSplitWindshield],
// Override appointment details
appointmentDetails: {
...getDefaultTestData().appointmentDetails!,
shopAddress: "504 Malcolm Ave Se, Minneapolis, MN 55414"
},
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService
}
}
const CashReplaceSplitWindshieldTests: ITestCase[] = [];
const tc = {
name: `CashReplaceSplitWindshield`,
tags: ['@E2E','@CashReplaceSplitWindshield', '@test_report', '@CASH'],
testData: CashReplaceSplitWindshieldData
};
CashReplaceSplitWindshieldTests.push(tc);
export default CashReplaceSplitWindshieldTests;

View file

@ -0,0 +1,81 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation } from "safelite-playwright-core";
import { DamageType, PartQuestionType, PaymentType } from 'safelite-playwright-core'
import { ITestCase } from '../framework/Typedefs'
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed for consistent data generation
setFakerSeedFromTestName("CashReplaceSwitchToInsuranceProgressiveNoComp");
// Define insurance test data for Progressive
const cashReplaceSwitchToInsuranceProgressiveNoCompData: Partial<ITestData> = {
...getDefaultTestData(),
isCashInsuranceFlow: true,
// Override customer details based on provided ZIP code
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: "70458",
},
},
// Vehicle details for a 2012 Chrysler 300
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: "2012",
make: "Chrysler",
model: "300",
style: "4 door sedan"
},
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
],
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayWithInsurance, // Ensures insurance payment method is selected
},
// Insurance claim details
claimDetails: {
client: "Progressive",
policyNumber: "Mock495646B",
policyDeductible: 0,
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric" }),
damageCause: DamageType.Rock,
},
isPolicyFound: true,
isUseVehicleOnPolicy: true,
isPolicyDriver: true,
// Appointment details for in-shop service
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: "8985 Yellow Brick Rd, Rosedale, MD 21237",
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
},
};
// Create and register the test case
const cashReplaceSwitchToInsuranceProgressiveNoCompTests: ITestCase[] = [];
const tc = {
name: `CashReplaceSwitchToInsuranceProgressiveNoComp`,
tags: ["@E2E", "@CashReplaceSwitchToInsuranceProgressiveNoComp", "@test_report", "@CASH"],
testData: cashReplaceSwitchToInsuranceProgressiveNoCompData,
};
cashReplaceSwitchToInsuranceProgressiveNoCompTests.push(tc);
export default cashReplaceSwitchToInsuranceProgressiveNoCompTests;

View file

@ -0,0 +1,74 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { AppointmentTimeslot, ServiceLocation, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("CashReplaceVinMobile");
// Now get the test data with the seeded faker
const cashReplaceVinMobileData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Flag for recalibration vehicle
isRecalVehicle: true,
// Override customer postal code
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '21237'
}
},
// Override vehicle details - using VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2012',
make: 'Honda',
model: 'Accord',
style: '4 door sedan',
vin: '1HGCP3F83CA040466',
vehicleLookupType: VehicleLookupType.Vin
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override for mobile service
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
serviceAddress: {
// Use street address from current faker seed
street: "5050 Silver Oak Dr",
city: 'Rosedale',
state: 'MD',
postalCode: '21237',
country: 'United States'
},
appointmentTimeSlot: AppointmentTimeslot.EarlyBird
},
// Payment at service
paymentDetails: {
paymentType: PaymentType.PayAtService
},
}
const cashReplaceVinMobileTests: ITestCase[] = [];
const tc = {
name: `CashReplaceVinMobile`,
tags: ['@E2E','@CashReplaceVinMobile', '@test_report', '@CASH'],
testData: cashReplaceVinMobileData
};
cashReplaceVinMobileTests.push(tc);
export default cashReplaceVinMobileTests;

View file

@ -0,0 +1,83 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServicePackage, ServiceLocation, PartQuestionType, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("CashReplaceWiperDropoff");
// Now get the test data with the seeded faker
const cashReplaceWiperDropoffData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Key feature: Standard package which includes wipers
servicePackage: ServicePackage.Standard,
// Special flags
isSkipEstimatePage: true,
isRecalVehicle: true,
// Override customer postal code
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '43085'
}
},
// Override vehicle details - 2023 Audi A8
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2023',
make: 'Audi',
model: 'A8',
style: '4 door sedan',
vin: 'WAULDAF81PN003021',
vehicleLookupType: VehicleLookupType.Zip
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override for drop-off service
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Payment at service
paymentDetails: {
paymentType: PaymentType.PayAtService
},
// Part questions related to recalibration
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
{
partQuestionType: PartQuestionType.GeneralQuestion2,
isOnPage: false,
optionToSelect: 'Yes'
},
]
}
const cashReplaceWiperDropoffTests: ITestCase[] = [];
const tc = {
name: `CashReplaceWiperDropoff`,
tags: ['@E2E','@CashReplaceWiperDropoff', '@test_report', '@CASH'],
testData: cashReplaceWiperDropoffData
};
cashReplaceWiperDropoffTests.push(tc);
export default cashReplaceWiperDropoffTests;

View file

@ -0,0 +1,61 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServicePackage, ServiceLocation, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
import { PaymentMethod } from 'framework/localTypes/Enums';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("CashReplaceWiperPromoInShop");
// Now get the test data with the seeded faker
const cashReplaceWiperPromoInShopData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// CASH Client
paymentMethod: PaymentMethod.SelfPay,
// Key feature: Standard package with wipers
servicePackage: ServicePackage.Standard,
// Override customer postal code
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '43085'
}
},
// Override vehicle details - Truck with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2015',
make: 'Ford',
model: 'F Series F150',
style: '2 door standard cab',
vin: '1FTMF1C87FKD85044',
vehicleLookupType: VehicleLookupType.Vin
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Payment at service
paymentDetails: {
paymentType: PaymentType.PayAtService,
// Specific wiper promo code
promoCode: '1WIPER0',
}
}
const cashReplaceWiperPromoInShopTests: ITestCase[] = [];
const tc = {
name: `CashReplaceWiperPromoInShop`,
tags: ['@E2E','@CashReplaceWiperPromoInShop', '@test_report', '@CASH'],
testData: cashReplaceWiperPromoInShopData
};
cashReplaceWiperPromoInShopTests.push(tc);
export default cashReplaceWiperPromoInShopTests;

View file

@ -0,0 +1,98 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, DamageType, PartQuestionType, VehicleDamage, PaymentType, Flow } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("InsuranceAcuityPaypal");
// Now get the test data with the seeded faker
const insuranceAcuityPaypalData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// Key feature: Insurance flow with Acuity
paymentMethod: PaymentMethod.Insurance,
// Insurance claim flags
isDuplicateClaim: true,
flow: Flow.Managed,
isUseVehicleOnPolicy: true,
// Override customer details for Kentucky location
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
street: getDefaultTestData().customerDetails!.address.street,
city: 'Concord',
state: 'Kentucky',
postalCode: '42071',
country: 'United States'
}
},
// Insurance claim details
claimDetails: {
client: 'ACUITY INSURANCE',
policyNumber: 'Mock532809F',
policyDeductible: 0,
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Hail
},
// Heavy duty truck details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2006',
make: 'Ford',
model: 'F Series F550',
style: '2 door standard cab',
vin: '1FDAF57P86EA68105',
vehicleLookupType: VehicleLookupType.Vin,
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override for in-shop appointment
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '8985 Yellow Brick Rd, Rosedale, MD 21237',
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Part questions for windshield
vehiclePartQuestions: [
{
partQuestionType: PartQuestionType.WindshieldColor,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'solar, third visor frit, aftermarket'
},
],
// Other vehicles on policy
otherVehiclesOnPolicy: [
{
year: '2000',
make: 'Ford',
model: 'F250 Super Duty',
vehicleLookupType: VehicleLookupType.Vin
}
],
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
}
const insuranceAcuityPaypalTests: ITestCase[] = [];
const tc = {
name: `InsuranceAcuityPaypal`,
tags: ['@E2E','@InsuranceAcuityPaypal', '@test_report', '@Insurance', '@CASH-1187'],
testData: insuranceAcuityPaypalData
};
insuranceAcuityPaypalTests.push(tc);
export default insuranceAcuityPaypalTests;

View file

@ -0,0 +1,82 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, DamageType, PaymentType, PartQuestionType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { PaymentMethod } from "framework/localTypes/Enums";
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("InsuranceBigTruckVerified");
// Now get the test data with the seeded faker
const insuranceBigTruckVerifiedData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// Key feature: Insurance flow with GEICO
paymentMethod: PaymentMethod.Insurance,
// Insurance claim flags
isDuplicateClaim: true,
isPolicyFound: true,
isUseVehicleOnPolicy: true,
isHeavyTruck: true,
// Override customer details with specific name and California location
customerDetails: {
...getDefaultTestData().customerDetails!,
firstName: 'Big',
lastName: 'Truck',
address: {
...getDefaultTestData().customerDetails!.address,
city: 'Ontario',
state: 'California',
postalCode: '43085'
}
},
// Insurance claim details
claimDetails: {
client: 'USAA',
policyNumber: 'Mock900040BigTruck',
policyDeductible: 2000.00,
policyZip: '55414',
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Rock
},
// Hyundai vehicle details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2025',
make: 'Peterbilt',
model: '579',
style: 'conventional cab',
vin: '1XPBDP9X6SD693446',
vehicleLookupType: VehicleLookupType.Vin,
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override for in-shop appointment
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '5719 Brandt Pike, Dayton, OH 45424',
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
}
const insuranceBigTruckVerifiedTests: ITestCase[] = [];
const tc = {
name: `InsuranceBigTruckVerified`,
tags: ['@E2E','@InsuranceBigTruckVerified', '@test_report', '@Insurance'],
testData: insuranceBigTruckVerifiedData
};
insuranceBigTruckVerifiedTests.push(tc);
export default insuranceBigTruckVerifiedTests;

View file

@ -0,0 +1,79 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, DamageType, Flow } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("InsuranceGeico");
// Now get the test data with the seeded faker
const insuranceGeicoData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// Key feature: Insurance flow with GEICO
paymentMethod: PaymentMethod.Insurance,
// Insurance claim flags
isDuplicateClaim: true,
flow: Flow.Managed,
isUseVehicleOnPolicy: true,
// Override customer details with specific name and California location
customerDetails: {
...getDefaultTestData().customerDetails!,
firstName: 'Albina',
lastName: 'Klint',
address: {
...getDefaultTestData().customerDetails!.address,
city: 'Ontario',
state: 'California',
postalCode: '91761'
}
},
// Insurance claim details
claimDetails: {
client: 'GEICO',
policyNumber: 'Mock250034C',
policyDeductible: 0,
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Hail
},
// Hyundai vehicle details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2018',
make: 'Hyundai',
model: 'Elantra',
style: '4 door sedan',
vin: '5NPD84LFXJH285828',
vehicleLookupType: VehicleLookupType.Vin,
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override for in-shop appointment
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '8985 Yellow Brick Rd, Rosedale, MD 21237',
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
}
const insuranceGeicoTests: ITestCase[] = [];
const tc = {
name: `InsuranceGeico`,
tags: ['@E2E','@InsuranceGeico', '@test_report', '@Insurance'],
testData: insuranceGeicoData
};
insuranceGeicoTests.push(tc);
export default insuranceGeicoTests;

View file

@ -0,0 +1,83 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, DamageType, PartQuestionType, Flow } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("InsuranceITAC21stCentury");
// Now get the test data with the seeded faker
const insuranceITAC21stCenturyData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// Key feature: Insurance flow with 21st Century - ITAC (Insurance Total Assumption of Coverage)
paymentMethod: PaymentMethod.Insurance,
// Insurance claim flags
isDuplicateClaim: true,
flow: Flow.Managed,
isUseVehicleOnPolicy: true,
isRecalVehicle: true, // Special flag for recalibration notification
// Override customer details for California location
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
city: 'Ontario',
state: 'California',
postalCode: '91761'
}
},
// Insurance claim details with high deductible
claimDetails: {
client: '21st Century',
policyNumber: 'Mock200122P',
policyDeductible: 3500, // High deductible test case
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Hail
},
// Vehicle details using ZIP lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2018',
make: 'Honda',
model: 'Accord',
style: '4 door sedan',
vehicleLookupType: VehicleLookupType.Zip,
},
// No need to override vehicleDamage as it already defaults to WindshieldCrack
// Override for in-shop appointment with specific shop
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
shopAddress: "8160 Masi Dr, Rancho Cucamonga, CA 91730"
},
// Part questions related to recalibration
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
]
}
const insuranceITAC21stCenturyTests: ITestCase[] = [];
const tc = {
name: `InsuranceITAC21stCentury`,
tags: ['@E2E','@InsuranceITAC21stCentury', '@test_report', '@Insurance'],
testData: insuranceITAC21stCenturyData
};
insuranceITAC21stCenturyTests.push(tc);
export default insuranceITAC21stCenturyTests;

View file

@ -0,0 +1,80 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { VehicleDamage, ServiceLocation, DamageType, Flow } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("InsuranceITACOptimizedPriceValidationAllState");
// Now get the test data with the seeded faker
const insuranceITACOptimizedPriceValidationAllStateData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// Key feature: Insurance flow with Allstate - ITAC with Optimized Price Validation
paymentMethod: PaymentMethod.Insurance,
// Insurance claim flags
isDuplicateClaim: true,
flow: Flow.Managed,
isUseVehicleOnPolicy: true,
// Override customer details with specific name and location
customerDetails: {
...getDefaultTestData().customerDetails!,
firstName: 'Deborah L',
lastName: 'Grist',
address: {
...getDefaultTestData().customerDetails!.address,
city: 'Franklin County',
state: 'Ohio',
postalCode: '43235'
}
},
// Insurance claim details
claimDetails: {
client: 'Allstate Insurance',
policyNumber: 'Mock294523C',
policyDeductible: 0,
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Rock
},
// Vehicle details for truck with ZIP lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2015',
make: 'Toyota',
model: 'Tacoma Pickup',
style: '4 door crew cab',
vehicleLookupType: VehicleLookupType.Zip,
},
// Driver door damage instead of windshield
vehicleDamage: [
VehicleDamage.DriverFrontDoor,
],
// Override for in-shop appointment with specific shop
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '8985 Yellow Brick Rd, Rosedale, MD 21237',
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
}
const insuranceITACOptimizedPriceValidationAllStateTests: ITestCase[] = [];
const tc = {
name: `InsuranceITACOptimizedPriceValidationAllState`,
tags: ['@E2E','@InsuranceITACOptimizedPriceValidationAllState', '@test_report', '@Insurance'],
testData: insuranceITACOptimizedPriceValidationAllStateData
};
insuranceITACOptimizedPriceValidationAllStateTests.push(tc);
export default insuranceITACOptimizedPriceValidationAllStateTests;

View file

@ -0,0 +1,77 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, DamageType, Flow, PartQuestionType, LossLocation } from 'safelite-playwright-core';
import { PaymentMethod } from "framework/localTypes/Enums";
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Seed for consistent test data generation
setFakerSeedFromTestName("InsuranceMeemicNearSchoolVerified");
// Now get the test data with the seeded faker
const insuranceMeemicNearSchoolVerifiedData: Partial<ITestData> = {
...getDefaultTestData(),
paymentMethod: PaymentMethod.Insurance,
isDuplicateClaim: false,
flow: Flow.Managed,
isPolicyFound: true,
isUseVehicleOnPolicy: true,
isRecalVehicle: false,
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '48382'
}
},
claimDetails: {
client: 'Meemic',
policyNumber: 'Mock427352LD',
policyDeductible: 0,
damageDate: new Date().toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Rock,
lossLocation: LossLocation.School
},
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2015',
make: 'Jeep',
model: 'Wrangler',
style: '4 door utility',
vehicleLookupType: VehicleLookupType.Zip,
},
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
shopAddress: "3451 Washtenaw Ave, Ann Arbor, MI 48104"
},
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'No'
},
],
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
};
const insuranceMeemicNearSchoolVerifiedTests: ITestCase[] = [];
const tc = {
name: `InsuranceMeemicNearSchoolVerified`,
tags: ['@E2E', '@InsuranceMeemicNearSchool', '@test_report', '@Insurance'],
testData: insuranceMeemicNearSchoolVerifiedData
};
insuranceMeemicNearSchoolVerifiedTests.push(tc);
export default insuranceMeemicNearSchoolVerifiedTests;

View file

@ -0,0 +1,91 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, DamageType, PaymentType, PartQuestionType, Flow } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { PaymentMethod } from "framework/localTypes/Enums";
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("InsuranceNoCompProgressive");
// Now get the test data with the seeded faker
const insuranceNoCompProgressiveData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// Key feature: Insurance flow with GEICO
paymentMethod: PaymentMethod.Insurance,
// Insurance claim flags
isDuplicateClaim: true,
flow: Flow.Managed,
isPolicyFound: true,
isUseVehicleOnPolicy: true,
isHeavyTruck: false,
isPolicyDriver: true,
isRecalVehicle: true,
// Override customer details with specific name and California location
customerDetails: {
...getDefaultTestData().customerDetails!,
firstName: 'SHERI',
lastName: 'SCHATZ',
address: {
...getDefaultTestData().customerDetails!.address,
city: 'Slidell',
state: 'Louisiana',
postalCode: '70458'
}
},
// Insurance claim details
claimDetails: {
client: 'Progressive',
policyNumber: 'Mock495646B',
policyDeductible: 1722.20,
policyZip: '70458',
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Rock
},
// Hyundai vehicle details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2015',
make: 'Acura',
model: 'MDX',
style: '4 door utility',
vehicleLookupType: VehicleLookupType.Zip,
},
// Part questions related to recalibration
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
],
// Override for in-shop appointment
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '56705 Garrett Road, Slidell, LA 70458',
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
}
const insuranceNoCompProgressiveTests: ITestCase[] = [];
const tc = {
name: `InsuranceNoCompProgressive`,
tags: ['@E2E','@InsuranceNoCompProgressive', '@test_report', '@Insurance'],
testData: insuranceNoCompProgressiveData
};
insuranceNoCompProgressiveTests.push(tc);
export default insuranceNoCompProgressiveTests;

View file

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

View file

@ -0,0 +1,55 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { VehicleDamage, ServiceLocation } from 'safelite-playwright-core';
import { ITestCase } from '../../framework/Typedefs';
import { getNextWeekday } from 'safelite-playwright-core';
import { defaultTestData } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Heavy Truck (alert) Test Data
const heavyTruckData: Partial<ITestData> = {
...defaultTestData, // Start with all defaults
isHeavyTruck: true, // Flag for heavy truck vehicle
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '21237'
}
},
// Override specific fields with test-specific data
vehicleDetails: {
...defaultTestData.vehicleDetails!,
year: '2017',
make: 'Freightliner',
model: '114sd',
style: 'conventional cab'
},
alertFlags: {
isHeavyTruckVehicleAlert: true
},
vehicleDamage: [
VehicleDamage.WindshieldOneChip,
VehicleDamage.RearWindow
],
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: getNextWeekday()
}
};
const heavyTruckTests: ITestCase[] = [];
const tc: ITestCase = {
name: `alert0001 Heavy Truck`,
tags: ['@Alert', '@HeavyTruck', '@test_report', '@CASH-1187'],
testData: heavyTruckData
};
heavyTruckTests.push(tc);
export default heavyTruckTests;

View file

@ -0,0 +1,42 @@
// Imports here
import { ITestData } from 'framework/TestData';
import { VehicleDamage, ServiceLocation } from 'safelite-playwright-core';
import { ITestCase } from '../../framework/Typedefs'
import { getNextWeekday } from 'safelite-playwright-core';
import { defaultTestData } from 'safelite-playwright-core';
// Both repair and replace selected (alert) Test Data
const repairAndReplaceData: Partial<ITestData> = {
...defaultTestData, // Start with all defaults
// Override specific fields with test-specific data
vehicleDetails: {
...defaultTestData.vehicleDetails!,
year: '2020',
make: 'Acura',
model: 'ILX',
style: '4 door sedan'
},
vehicleDamage: [
VehicleDamage.WindshieldOneChip,
VehicleDamage.RearWindow
],
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: getNextWeekday()
},
alertFlags: {
isRepairReplace: true
}
};
const repairAndReplaceTests: ITestCase[] = [];
const tc = {
name: `alert0002 Both repair and replace selected`,
tags: ['@Alert', '@RepairReplace', '@test_report'],
testData: repairAndReplaceData
};
repairAndReplaceTests.push(tc);
export default repairAndReplaceTests;

View file

@ -0,0 +1,74 @@
// Imports here
import { ITestData } from 'framework/TestData';
import { ITestCase, TestCase } from '../../framework/Typedefs'
import { VehicleDamage } from 'safelite-playwright-core';
import { defaultTestData } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Windshield selected, when vehicle has split windshield (alert) Test Data
const splitWindshieldData: Partial<ITestData> = {
...defaultTestData, // Start with all defaults
// Override specific fields with test-specific data
vehicleDamage: [
VehicleDamage.SingleSplitWindshield,
VehicleDamage.DriverSplitWindshield,
VehicleDamage.PassengerSplitWindshield
],
alertFlags: {
isSplitWindshield: true
},
customerDetails: {
...getDefaultTestData().customerDetails!,
address: {
...getDefaultTestData().customerDetails!.address,
postalCode: '55414'
}
},
isHeavyTruck: true, // Flag for heavy truck vehicle
};
const vehiclesToTest = [
{
year: '2000',
make: 'Kenworth',
model: 'T450',
style: 'conventional cab'
},
{
year: '2006',
make: 'Navistar',
model: '5000 I',
style: '2 door conventional cab'
},
{
year: '1990',
make: 'Kenworth',
model: 'T600',
style: 'conventional cab'
},
];
const splitWindshieldTests: ITestCase[] = [];
// Generate test cases for each vehicle
vehiclesToTest.forEach((vehicle, index) => {
    const testData = {
        ...splitWindshieldData,
        vehicleDetails: {
            ...defaultTestData.vehicleDetails!,
            ...vehicle
        },
    }
const tc = {
name: `alert0003_${vehicle.make.toLowerCase()}_${vehicle.model.toLowerCase()} Windshield selected, when vehicle has split windshield - ${vehicle.make} ${vehicle.model} ${vehicle.year}`,
tags: ['@Alert', '@SplitWindshield', '@test_report', `@${vehicle.make.toLowerCase()}`],
testData: testData
};
splitWindshieldTests.push(tc);
});
export default splitWindshieldTests;

View file

@ -0,0 +1,41 @@
// Imports here
import { ITestData } from 'framework/TestData';
import { VehicleDamage, ServiceLocation, VehicleLookupType } from 'safelite-playwright-core';
import { ITestCase } from '../../framework/Typedefs'
import { getNextWeekday } from 'safelite-playwright-core';
import { defaultTestData } from 'safelite-playwright-core';
// Replace selected when not offered (alert) Test Data
const repairOnlyData: Partial<ITestData> = {
...defaultTestData, // Start with all defaults
// Override specific fields with test-specific data
vehicleDetails: {
...defaultTestData.vehicleDetails!,
year: '2023',
make: 'Motor Home',
model: 'Motor Home',
style: 'motor home'
},
vehicleDamage: [
VehicleDamage.WindshieldCrack
],
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: getNextWeekday()
},
alertFlags: {
isRepairOnly: true
}
};
const repairOnlyTests: ITestCase[] = [];
const tc = {
name: `alert0004 Replace selected when not offered`,
tags: ['@Alert', '@RepairOnly', '@test_report', '@CASH-1187'],
testData: repairOnlyData
};
repairOnlyTests.push(tc);
export default repairOnlyTests;

View file

@ -0,0 +1,118 @@
// Imports here
import { ITestData } from 'framework/TestData';
import { VehicleDamage, ServiceLocation, VehicleLookupType } from 'safelite-playwright-core';
import { ITestCase } from '../../framework/Typedefs'
import { getNextWeekday } from 'safelite-playwright-core';
import { faker } from "@faker-js/faker";
import { defaultTestData } from 'safelite-playwright-core';
// Unserviceable zip (alert) - Combined test cases for all lookup types
const nextWeekday = getNextWeekday();
const baseUnserviceableZipData: Partial<ITestData> = {
...defaultTestData, // Start with all defaults
// Override specific fields with test-specific data
customerDetails: {
...defaultTestData.customerDetails!,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
address: {
street: faker.location.streetAddress(),
city: 'Saco',
state: 'Montana',
postalCode: '59261',
country: 'United States'
}
},
vehicleDamage: [
VehicleDamage.WindshieldCrack
],
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: nextWeekday
},
alertFlags: {
isUnserviceableZip: true
}
};
interface LookupTestCase {
type: VehicleLookupType;
vehicleDetails: {
year: string;
make: string;
model: string;
style: string;
licensePlateNumber?: string;
licensePlateState?: string;
vin?: string;
};
enterFunnelWithZip?: boolean;
customerDetails?: {
address: {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
};
};
tag: string;
}
const lookupTypesToTest: LookupTestCase[] = [
{
type: VehicleLookupType.Zip,
vehicleDetails: {
year: '2020',
make: 'Acura',
model: 'ILX',
style: '4 door sedan'
},
tag: 'Zip'
},
{
type: VehicleLookupType.Vin,
vehicleDetails: {
year: '2019',
make: 'Toyota',
model: 'C-HR',
style: '4 door hatchback',
vin: 'NMTKHMBX5KR086519'
},
tag: 'Vin'
}
];
const unserviceableZipTests: ITestCase[] = [];
// Generate test cases for each lookup type
lookupTypesToTest.forEach((lookupType, index) => {
const testData = {
...baseUnserviceableZipData,
vehicleDetails: {
...lookupType.vehicleDetails,
vehicleLookupType: lookupType.type
},
enterFunnelWithZip: lookupType.enterFunnelWithZip
};
// Handle special case for Address lookup which has different customer details
if (lookupType.type === VehicleLookupType.Address && lookupType.customerDetails) {
testData.customerDetails = {
...baseUnserviceableZipData.customerDetails!,
address: lookupType.customerDetails.address
};
}
const tc = {
name: `alert0005 ${lookupType.tag} Lookup Unserviceable zip`,
tags: ['@Alert', `@UnserviceableZip_${lookupType.tag}`, '@test_report'],
testData: testData
};
unserviceableZipTests.push(tc);
});
export default unserviceableZipTests;

View file

@ -0,0 +1,133 @@
// Imports here
import { ITestData } from 'framework/TestData';
import { VehicleDamage, ServiceLocation, VehicleLookupType, ILicensePlate } from 'safelite-playwright-core';
import { ITestCase } from '../../framework/Typedefs'
import { getNextWeekday } from 'safelite-playwright-core';
import { faker } from "@faker-js/faker";
import { defaultTestData } from 'safelite-playwright-core';
// Invalid zip (alert) - Combined test cases for all lookup types
const nextWeekday = getNextWeekday();
const baseInvalidZipData: Partial<ITestData> = {
...defaultTestData, // Start with all defaults
// Override specific fields with test-specific data
customerDetails: {
...defaultTestData.customerDetails!,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
address: {
street: faker.location.streetAddress(),
city: 'Saco',
state: 'Montana',
postalCode: '99999',
country: 'United States'
}
},
vehicleDamage: [
VehicleDamage.WindshieldCrack
],
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: nextWeekday
},
alertFlags: {
isInvalidZip: true
}
};
interface LookupTestCase {
type: VehicleLookupType;
vehicleDetails: {
year: string,
make: string,
model: string,
style?: string,
vin?: string|string[],
licensePlate?: ILicensePlate | ILicensePlate[],
vehicleLookupType?: VehicleLookupType,
};
enterFunnelWithZip?: boolean;
customerDetails?: {
address: {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
};
};
tag: string;
displayName: string;
}
const lookupTypesToTest: LookupTestCase[] = [
{
type: VehicleLookupType.Zip,
displayName: 'Zip',
tag: 'Zip',
vehicleDetails: {
year: '2020',
make: 'Acura',
model: 'ILX',
style: '4 door sedan'
}
},
{
type: VehicleLookupType.LicensePlateNumber,
displayName: 'License Plate',
tag: 'Plate',
vehicleDetails: {
year: '2013',
make: 'Hyundai',
model: 'Sonata',
style: '4 door sedan',
licensePlate: { licensePlateNumber: 'FTY 7776', licensePlateState: 'Texas' },
}
},
{
type: VehicleLookupType.Vin,
displayName: 'VIN',
tag: 'Vin',
vehicleDetails: {
year: '2019',
make: 'Toyota',
model: 'C-HR',
style: '4 door hatchback',
vin: 'NMTKHMBX5KR086519'
}
}
];
const invalidZipTests: ITestCase[] = [];
// Generate test cases for each lookup type
lookupTypesToTest.forEach((lookupType) => {
const testData = {
...baseInvalidZipData,
vehicleDetails: {
...lookupType.vehicleDetails,
vehicleLookupType: lookupType.type
},
enterFunnelWithZip: lookupType.enterFunnelWithZip
};
// Handle special case for Address lookup which has different customer details
if (lookupType.type === VehicleLookupType.Address && lookupType.customerDetails) {
testData.customerDetails = {
...baseInvalidZipData.customerDetails!,
address: lookupType.customerDetails.address
};
}
const tc = {
name: `alert0006 ${lookupType.displayName} Lookup Invalid zip`,
tags: ['@Alert', `@InvalidZip_${lookupType.tag}`, '@test_report'],
testData: testData
};
invalidZipTests.push(tc);
});
export default invalidZipTests;

View file

@ -0,0 +1,143 @@
// Imports here
import { ITestData } from 'framework/TestData';
import { VehicleDamage, ServiceLocation, VehicleLookupType, ILicensePlate } from 'safelite-playwright-core';
import { ITestCase } from '../../framework/Typedefs'
import { getNextWeekday } from 'safelite-playwright-core';
import { faker } from "@faker-js/faker";
import { defaultTestData } from 'safelite-playwright-core';
// Vin not found (alert) - Combined test cases for all lookup types
const nextWeekday = getNextWeekday();
const baseVinNotFoundData: Partial<ITestData> = {
...defaultTestData, // Start with all defaults
// Override specific fields with test-specific data
customerDetails: {
...defaultTestData.customerDetails!,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
address: {
street: faker.location.streetAddress(),
city: 'Raleigh',
state: 'North Carolina',
postalCode: '44089',
country: 'United States'
}
},
vehicleDamage: [
VehicleDamage.WindshieldCrack
],
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: nextWeekday
},
alertFlags: {
isVinNotFound: true
}
};
interface LookupTestCase {
type: VehicleLookupType;
vehicleDetails: {
year: string,
make: string,
model: string,
style?: string,
vin?: string|string[],
licensePlate?: ILicensePlate | ILicensePlate[],
vehicleLookupType?: VehicleLookupType,
};
enterFunnelWithZip?: boolean;
customerDetails?: {
address: {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
};
};
tag: string;
displayName: string;
}
const lookupTypesToTest: LookupTestCase[] = [
{
type: VehicleLookupType.Address,
displayName: 'Address',
tag: 'Addr',
vehicleDetails: {
year: '2013',
make: 'Hyundai',
model: 'Sonata',
style: '4 door sedan'
},
customerDetails: {
address: {
street: '4076 Spectacle Drive',
city: 'Columbus',
state: 'OH',
postalCode: '59261',
country: 'United States'
}
},
enterFunnelWithZip: true
},
{
type: VehicleLookupType.LicensePlateNumber,
displayName: 'License Plate',
tag: 'Plate',
vehicleDetails: {
year: '2013',
make: 'Hyundai',
model: 'Sonata',
style: '4 door sedan',
licensePlate: { licensePlateNumber: '6WYN462', licensePlateState: 'Texas' },
}
},
{
type: VehicleLookupType.Vin,
displayName: 'VIN',
tag: 'Vin',
vehicleDetails: {
year: '2019',
make: 'Toyota',
model: 'C-HR',
style: '4 door hatchback',
vin: '1MMHK8F81CGA45982'
}
}
];
const vinNotFoundTests: ITestCase[] = [];
// Generate test cases for each lookup type
lookupTypesToTest.forEach((lookupType) => {
const testData = {
...baseVinNotFoundData,
vehicleDetails: {
...lookupType.vehicleDetails,
vehicleLookupType: lookupType.type
},
isEnterFunnelWithZip: lookupType.enterFunnelWithZip
};
// Handle special case for Address lookup which has different customer details
if (lookupType.type === VehicleLookupType.Address && lookupType.customerDetails) {
testData.customerDetails = {
...baseVinNotFoundData.customerDetails!,
address: lookupType.customerDetails.address,
};
}
const tc = {
name: `alert0007 ${lookupType.displayName} Lookup Vin Not Found`,
tags: ['@Alert', `@VinNotFound_${lookupType.tag}`, '@test_report'],
testData: testData
};
vinNotFoundTests.push(tc);
});
export default vinNotFoundTests;

View file

@ -0,0 +1,58 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"strictNullChecks": true,
"noImplicitAny": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@impl/*": [
"./impl/*"
],
"@api/*": [
"./impl/api/*"
],
"@controller/*": [
"./impl/api/controller/*"
],
"@model/*": [
"./impl/api/model/*"
],
"@gui/*": [
"./impl/gui/*"
],
"@lfm/*": [
"./impl/gui/lfm/*"
],
"@pom/*": [
"./impl/gui/pom/*"
],
"@mixins/*": [
"./impl/gui/mixins"
],
"@helpers/*": [
"./helpers/*"
],
"@tests/*": [
"./tests/*"
],
"@utils/*": [
"./impl/utils/*"
]
}
},
"include": [
"**/*.ts",
"**/*.js"
],
"exclude": [
"node_modules",
"artifacts"
]
}

View file

@ -44,21 +44,40 @@ export default {
},
getSierraContext() {
const glassDamage = store.getters?.order?.damage?.glassToReplace;
const glassType =
glassDamage && glassDamage.length > 0
? glassDamage.length > 1
? "Multiglass"
: glassDamage[0].glassType // todo: clarify?
: "";
let glassType = "";
for (let i = 0; i < glassDamage?.length; i++) {
if (i !== 0) {
glassType += "/";
}
glassType += glassDamage[i].glassLocation;
}
const damageType = store.getters.order?.damage?.isRepair ? "repair" : "replace";
const hasGlassDamage = glassDamage?.length > 0;
const hasRepairFlag = store.getters.order?.damage?.isRepair;
const paymentType = store.getters.oreder?.payment?.isInsurance ? "insurance" : "cash";
let damageType = "";
if (hasRepairFlag) {
damageType = "repair";
} else if (hasGlassDamage) {
damageType = "replace";
}
const isInsurance = store.getters.order?.payment?.isInsurance;
let paymentType = "";
if (isInsurance) {
paymentType = "insurance";
} else if (isInsurance !== null && isInsurance !== undefined) {
paymentType = "cash";
}
const glassParts = store.getters.order?.lineItems?.glassParts;
const partId = glassParts && glassParts.length > 0 ? glassParts[0].partId : "";
const partNumber = glassParts && glassParts.length > 0 ? glassParts[0].partNumber : "";
const packageData = deepClone(store.getters?.pageData(routeData.QUOTE.name));
const packageData = deepClone(store.getters?.pageData(routeData.QUOTE.name)?.packages);
const recalFlag = store.getters?.isRecalibrationOnOrder;
const displayRecalFlag = glassParts?.length > 0 ? recalFlag : "";
return {
current_page: this.pageName,
@ -66,13 +85,13 @@ export default {
vehicle_make: store.getters.order?.vehicle?.make ?? "",
vehicle_model: store.getters.order?.vehicle?.model ?? "",
vehicle_style: store.getters.order?.vehicle?.style ?? "",
service_zip: store.getters.order?.serviceLocation?.serviceZip ?? "",
service_zip: store.getters.order?.serviceLocation?.zipCode ?? "",
glass_type: glassType ?? "",
damage_type: damageType ?? "",
payment_type: paymentType ?? "",
quote: packageData ?? [],
part_id: partId ?? "",
recalibration: store.getters?.isRecalibrationOnOrder ?? "",
part_id: partNumber ?? "",
recalibration: displayRecalFlag ?? "",
};
},
launchSierraChat() {

View file

@ -169,6 +169,7 @@ import { routeData } from "@/router/constants/routes";
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
import { storeMutations } from "@/constants/store-mutations";
import { debugLog } from "@/helpers/debug-log-helper";
import { savePageData } from "@/router/methods/helpers/save-page-data";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@ -666,14 +667,7 @@ export default {
async closeSaveProgressPopup() {
this.showSaveProgressPopup = false;
await this.dispatchStoreAction(
storeActions.SAVE_PAGE_DATA,
{
page: routeData.QUOTE.name,
data: { saveProgressPopupSkipped: true },
},
false
);
savePageData(this.pageName, { saveProgressPopupSkipped: true }, true);
if (this.skipToInsurance) {
// skip ahead now if in IGQ skip experiment

View file

@ -442,7 +442,7 @@ export default {
price: ans.buttonAuxillaryCopy,
}));
await savePageData(this.pageName, { packages: toLog });
await savePageData(this.pageName, { packages: toLog }, true);
},
},
components: {

View file

@ -1,10 +1,15 @@
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
export async function savePageData(page, data) {
export async function savePageData(page, data, merge = false) {
const existingPageData = store.getters.pageData(page);
const dataToSave = Object.keys(data).length > 0 ? data : (existingPageData ?? {});
let dataToSave;
if (merge) {
dataToSave = Object.assign(existingPageData ?? {}, data ?? {});
} else {
dataToSave = Object.keys(data).length > 0 ? data : (existingPageData ?? {});
}
// Save data if non-null
store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: dataToSave });

View file

@ -3311,16 +3311,17 @@ export const actions = {
}
//Optional for specific YMMSs
switch (context.state.order.vehicle.make.toLowerCase()) {
case "mercedes benz":
case "volkswagen":
case "audi":
case "porsche":
return true;
default:
}
if (
context.state.order.vehicle.make.toLowerCase() === "bmw" &&
context.state.order.vehicle.year <= 2017
(context.state.order.vehicle.make.toLowerCase() === "bmw" &&
context.state.order.vehicle.year < 2010) ||
(context.state.order.vehicle.make.toLowerCase() === "volkswagen" &&
context.state.order.vehicle.year < 2010) ||
(context.state.order.vehicle.make.toLowerCase() === "audi" &&
context.state.order.vehicle.year < 2010)
)
return true;

View file

@ -4242,12 +4242,13 @@ describe("isVinOptionalVehicle", () => {
const testVehicles = [
["2017", "acura", false],
["2018", "bmw", false],
["2017", "bmw", true],
["2016", "bmw", true],
["2016", "mercedes benz", true],
["2016", "volkswagen", true],
["2016", "audi", true],
["2009", "bmw", true],
["2016", "bmw", false],
["2016", "mercedes benz", false],
["2016", "volkswagen", false],
["2016", "audi", false],
["2016", "porsche", true],
["2009", "audi", true],
];
test.each(testVehicles)(
"%s %s should skip vin lookup is %s",

318
temp/playwright-test.yml Normal file
View file

@ -0,0 +1,318 @@
parameters:
- name: totalShards
type: number
default: 2
values: [1, 2, 4, 8]
- name: targetUrl
type: string
- name: dockerFileName
type: string
default: 'Dockerfile.playwright'
- name: isRegression
type: boolean
default: false
- name: filterTags
type: string
default: '@smoke'
- name: playwrightTestsPath
type: string
default: 'playwright-tests'
- name: npmServePath
type: string
default: '.'
- name: npmrcPath
type: string
default: 'playwright-tests/.npmrc'
- name: secrets
type: object
default: {}
- name: dockerImageName
type: string
default: 'playwright-tests'
- name: imageTag
type: string
default: '$(Build.BuildId)'
- name: envFileName
type: string
default: '.env.ci'
jobs:
- job: playwright_tests
continueOnError: true
strategy:
matrix:
${{ if eq(parameters.totalShards, 1) }}:
shard1:
shardNumber: 1
${{ if eq(parameters.totalShards, 2) }}:
shard1:
shardNumber: 1
shard2:
shardNumber: 2
${{ if eq(parameters.totalShards, 4) }}:
shard1:
shardNumber: 1
shard2:
shardNumber: 2
shard3:
shardNumber: 3
shard4:
shardNumber: 4
${{ if eq(parameters.totalShards, 8) }}:
shard1:
shardNumber: 1
shard2:
shardNumber: 2
shard3:
shardNumber: 3
shard4:
shardNumber: 4
shard5:
shardNumber: 5
shard6:
shardNumber: 6
shard7:
shardNumber: 7
shard8:
shardNumber: 8
steps:
- task: npmAuthenticate@0
inputs:
workingFile: ${{ parameters.npmrcPath }}
- script: |
echo "Starting Docker cleanup to free space..."
# Remove all stopped containers
echo "Removing stopped containers..."
docker container prune -f
# Remove dangling images (untagged)
echo "Removing dangling images..."
docker image prune -f
# Remove unused volumes
echo "Removing unused volumes..."
docker volume prune -f
# Show disk usage after cleanup
echo "Docker system usage after cleanup:"
docker system df
echo "Docker cleanup completed!"
displayName: "Docker Cleanup"
- task: Docker@2
displayName: 'Build Docker Image'
inputs:
command: build
dockerfile: ${{ parameters.dockerFileName }}
repository: ${{ parameters.dockerImageName }}
tags: ${{ parameters.imageTag }}
arguments: '--no-cache --pull'
- script: |
printenv > "${{ parameters.envFileName }}"
JIRA_CARD_NUMBER=""
IS_REGRESSION="${{ parameters.isRegression }}"
echo "isRegression: $IS_REGRESSION";
if [[ "${{ parameters.isRegression }}" == "False" ]]; then
branch_name=$(System.PullRequest.SourceBranch)
echo "Retrieved branch name: '$branch_name'"
JIRA_CARD_NUMBER="${branch_name##*/}"
echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'"
fi
FILTER=""
if [[ -n "${{ parameters.filterTags }}" && -n "$JIRA_CARD_NUMBER" ]]; then
echo "Filtering by filterTags and Jira Card Number..."
FILTER="--grep '@$JIRA_CARD_NUMBER|${{ parameters.filterTags }}'"
elif [[ -n "${{ parameters.filterTags }}" ]]; then
echo "Filtering by filterTags only..."
FILTER="--grep '${{ parameters.filterTags }}'"
elif [[ -n "$JIRA_CARD_NUMBER" ]]; then
echo "Filtering by Jira card number only..."
FILTER="--grep '@$JIRA_CARD_NUMBER'"
else
echo "No filter. Running all test cases..."
FILTER=""
fi
echo "Filter value: $FILTER"
TARGET_URL=${{ parameters.targetUrl }}
echo "BASE_URL value: $BASE_URL"
# Create container and run tests
if [[ "$TARGET_URL" == *localhost* ]]; then
echo 'Npx version:'
npx --version
port="$TARGET_URL"
echo "URL: '$port'"
echo "URL '$port' contains 'localhost'. Extracting port... "
port=$(echo "$port" | sed -E 's/.*:([0-9]+).*/\1/')
echo "Port is '$port'"
TARGET_URL="$TARGET_URL"
JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
FILTER="$FILTER"
PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
SERVE_PATH="${{ parameters.npmServePath }}"
SHARD_NUMBER=$(shardNumber)
TOTAL_SHARDS=${{ parameters.totalShards }}
container_id=$(docker create \
--ipc=host \
--env CI=true \
--env-file "${{ parameters.envFileName }}" \
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
npx concurrently -k -n 'server,playwright' \
"npm --prefix $SERVE_PATH run serve -- --port=$port" \
"npx --prefix $PLAYWRIGHT_PATH wait-on $TARGET_URL && PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx --prefix $PLAYWRIGHT_PATH playwright test $PLAYWRIGHT_PATH --config=$PLAYWRIGHT_PATH/playwright.config.ts --shard=$SHARD_NUMBER/$TOTAL_SHARDS --reporter=list,blob $FILTER" \
)
else
export TARGET_URL="$TARGET_URL"
export JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
export FILTER="$FILTER"
export PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
export SERVE_PATH="${{ parameters.npmServePath }}"
export SHARD_NUMBER=$(shardNumber)
export TOTAL_SHARDS=${{ parameters.totalShards }}
export ENV_FILE="${{ parameters.envFileName }}"
container_id=$(docker create \
--ipc=host \
--env CI=true \
--env-file "${{ parameters.envFileName }}" \
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
bash -c "
set -a
source $ENV_FILE
set +a
PLAYWRIGHT_BLOB_OUTPUT_DIR='/app/blob-report' npx --prefix $PLAYWRIGHT_PATH playwright test $PLAYWRIGHT_PATH --config=$PLAYWRIGHT_PATH/playwright.config.ts --shard=$SHARD_NUMBER/$TOTAL_SHARDS --reporter=list,blob $FILTER
" \
)
fi
# Start container and stream logs
echo "Starting tests for shard $(shardNumber)..."
docker start -a $container_id
# Create directory for test results
echo "Creating test results directory..."
mkdir -p $(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber)
# Copy test results from container
echo "Copying test results..."
docker cp $container_id:/app/blob-report/. $(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber)/
# Remove container
echo "Cleaning up container..."
docker rm $container_id
# Check if tests failed
if [ $? -ne 0 ]; then
echo "Tests failed in shard $(shardNumber) or tests don't exist for this shard number!"
exit 0 # Suppress error. It will be visible in report.
fi
displayName: 'Run Playwright Tests - Shard $(shardNumber)'
env: ${{ parameters.secrets }}
- task: PublishPipelineArtifact@1
displayName: 'Publish Test Reports - Shard $(shardNumber)'
condition: always()
inputs:
targetPath: '$(System.DefaultWorkingDirectory)/blob-reports/shard-$(shardNumber)'
artifact: 'playwright-report-shard-$(shardNumber)'
publishLocation: 'pipeline'
- script: |
docker rmi ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} -f
displayName: 'Cleanup Docker Image'
condition: always()
- job: download_and_merge_reports
dependsOn: playwright_tests
timeoutInMinutes: 8
cancelTimeoutInMinutes: 10
steps:
- task: npmAuthenticate@0
inputs:
workingFile: ${{ parameters.npmrcPath }}
- task: DownloadPipelineArtifact@2
inputs:
targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports'
- task: Docker@2
displayName: 'Build Docker Image'
inputs:
command: build
dockerfile: ${{ parameters.dockerFileName }}
repository: ${{ parameters.dockerImageName }}
tags: ${{ parameters.imageTag }}
arguments: '--no-cache --pull'
- bash: |
printenv > "${{ parameters.envFileName }}"
branch_name=$(System.PullRequest.SourceBranch)
echo "Retrieved branch name: '$branch_name'"
JIRA_CARD_NUMBER="${branch_name##*/}"
echo "Extracted JIRA Card number: '$JIRA_CARD_NUMBER'"
export PLAYWRIGHT_PATH="${{ parameters.playwrightTestsPath }}"
export JIRA_CARD_NUMBER="$JIRA_CARD_NUMBER"
export ENV_FILE="${{ parameters.envFileName }}"
container_id=$(docker create \
--ipc=host \
--env CI=true \
--env-file "${{ parameters.envFileName }}" \
${{ parameters.dockerImageName }}:${{ parameters.imageTag }} \
bash -c "
set -a
source $ENV_FILE
set +a
echo 'Moving Playwright reports out of subfolders...'
find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \;
echo 'Value of JIRA_CARD_NUMBER in bash -c command: $JIRA_CARD_NUMBER'
echo 'Merging reports...'
JIRA_CARD_NUMBER='$JIRA_CARD_NUMBER' PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx --prefix $PLAYWRIGHT_PATH playwright merge-reports --config=$PLAYWRIGHT_PATH/playwright.config.ts ./playwright-reports
"
)
# Start container and stream logs
echo "Starting merge"
docker start -a $container_id
# Create directory for test results
echo "Creating test results directory..."
mkdir -p $(System.DefaultWorkingDirectory)/ortoni-report
mkdir -p $(System.DefaultWorkingDirectory)/test-results
# Copy test results from container
echo "Copying test results..."
docker cp $container_id:/app/ortoni-report/. $(System.DefaultWorkingDirectory)/ortoni-report
docker cp $container_id:/app/test-results/junit_results.xml $(System.DefaultWorkingDirectory)/test-results
# Remove container
echo "Cleaning up container..."
docker rm $container_id
displayName: merge_and_publish_results_to_jira
env: ${{ parameters.secrets }}
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
searchFolder: 'test-results'
testResultsFormat: 'JUnit'
testResultsFiles: 'junit_results.xml'
mergeTestResults: true
failTaskOnFailedTests: false
testRunTitle: 'Playwright Tests'
condition: succeededOrFailed()
- task: PublishPipelineArtifact@1
displayName: 'Publish Merged Report'
condition: always()
inputs:
targetPath: '$(System.DefaultWorkingDirectory)/ortoni-report'
artifact: 'playwright-merged-report'
publishLocation: 'pipeline'
- script: |
docker rmi ${{ parameters.dockerImageName }}:${{ parameters.imageTag }} -f
displayName: 'Cleanup Docker Image'
condition: always()