diff --git a/.gitignore b/.gitignore index fc42c5243..b01672f1a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Dockerfile.playwright b/Dockerfile.playwright new file mode 100644 index 000000000..0f5f6af3e --- /dev/null +++ b/Dockerfile.playwright @@ -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 diff --git a/azure-pipelines-automated-testing.yml b/azure-pipelines-automated-testing.yml new file mode 100644 index 000000000..34cda2917 --- /dev/null +++ b/azure-pipelines-automated-testing.yml @@ -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) \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3867fdcd6..9774bd48a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -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 diff --git a/playwright-tests/.dockerignore b/playwright-tests/.dockerignore new file mode 100644 index 000000000..3f9fe19df --- /dev/null +++ b/playwright-tests/.dockerignore @@ -0,0 +1,4 @@ +.git +*Dockerfile* +*docker-compose* +node_modules \ No newline at end of file diff --git a/playwright-tests/.env b/playwright-tests/.env new file mode 100644 index 000000000..5f37c7ddf --- /dev/null +++ b/playwright-tests/.env @@ -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" diff --git a/playwright-tests/.env.dev b/playwright-tests/.env.dev new file mode 100644 index 000000000..0f8e62d0c --- /dev/null +++ b/playwright-tests/.env.dev @@ -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" + diff --git a/playwright-tests/.env.example b/playwright-tests/.env.example new file mode 100644 index 000000000..5f37c7ddf --- /dev/null +++ b/playwright-tests/.env.example @@ -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" diff --git a/playwright-tests/.npmrc b/playwright-tests/.npmrc new file mode 100644 index 000000000..13a1273bf --- /dev/null +++ b/playwright-tests/.npmrc @@ -0,0 +1,3 @@ +registry=https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/ + +always-auth=true \ No newline at end of file diff --git a/playwright-tests/README.md b/playwright-tests/README.md new file mode 100644 index 000000000..b9124727e --- /dev/null +++ b/playwright-tests/README.md @@ -0,0 +1,266 @@ +# QA Automation Framework +An automated testing framework for FMG using Playwright with TypeScript. + +## Technology Stack +• Playwright - Core testing framework
+• TypeScript - Programming language
+• Node.js - Runtime environment
+• SauceLabs/Dockers - Cross-browser testing platform
+ +## Key Features +• Page Object Model implementation
+• Data-driven test approach
+• Cross-browser testing support
+• Parallel test execution
+• HTML report generation
+• Environment-specific configurations
+• SauceLabs integration
+ +## Test Scenarios +The framework includes various test scenarios covering essential flows like: + +### Cash Payment - Repair Scenarios +1. **CashRepairMobileCreditCard**
+ - Repair or Replace: Repair
+ - Damage Type: Windshield Chip (1)
+ - Vehicle Lookup Type: ZIP
+ - CASH or Insurance: CASH
+ - Service Type: Glass Service Only
+ - Service Location Type: Mobile
+ - Schedule Appointment Type: Range of Hours
+ - Payment Type: PIA - Credit Card
+ +2. **CashRepairInShopAfterPay**
+ - Repair or Replace: Repair
+ - Damage Type: Windshield Chip (2)
+ - Vehicle Lookup Type: ZIP
+ - CASH or Insurance: CASH
+ - Service Type: Standard
+ - Service Location Type: In-Shop
+ - Schedule Appointment Type: Hourly
+ - Payment Type: PIA - AfterPay
+ +3. **CashRepairInShopPayPal**
+ - Repair or Replace: Repair
+ - Damage Type: Windshield Chip (3)
+ - Vehicle Lookup Type: ZIP
+ - CASH or Insurance: CASH
+ - Service Type: Premium
+ - Service Location Type: In-Shop
+ - Schedule Appointment Type: Hourly
+ - Payment Type: PIA - PayPal
+ +### Cash Payment - Single Glass Replacement Scenarios +4. **CashReplaceDynamicRecalMobile**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: VIN Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Glass Service Only
+ - Service Location Type: Mobile
+ - Schedule Appointment Type: Range of Hours
+ - Payment Type: Pay At Service
+ - Vehicle Info: Dynamic Recalibration Vehicle
+ +5. **CashReplaceGlassAddressLookupInshopAfterPay**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: Address Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Glass Service Only
+ - Service Location Type: In-Shop
+ - Schedule Appointment Type: Hourly
+ - Payment Type: PIA - Afterpay
+ +6. **CashReplaceGlassLicensePlateLookupInshopPaypal**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: License Plate Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Glass Service Only
+ - Service Location Type: In-Shop
+ - Schedule Appointment Type: Hourly
+ - Payment Type: PIA - PayPal
+ +7. **CashReplaceVinMobile**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: VIN Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Glass Service Only
+ - Service Location Type: Mobile
+ - Schedule Appointment Type: Range of Hours
+ - Payment Type: Pay At Service
+ +8. **CashReplaceSafeliteCanNotRecalMobile**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: ZIP Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Glass Service Only
+ - Service Location Type: Mobile
+ - Schedule Appointment Type: Range of Hours
+ - Payment Type: Pay At Service
+ - Additional Info: Safelite Cannot Recalibrate Vehicle
+ +### Cash Payment - Multiple Glass Replacement Scenarios +9. **CashReplaceMultiGlassPromoInshop**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack, Driver Front Door, Driver Rear Door, Driver Vent Glass, Passenger Vent Glass, Rear Window
+ - Vehicle Lookup Type: VIN Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Glass Service Only
+ - Service Location Type: In-Shop
+ - Schedule Appointment Type: Hourly
+ - Payment Type: Pay At Service
+ - Additional Info: Glass Promo
+ +10. **CashReplaceMultiSlidingGlassDropoff**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack, Passenger Front Door, Passenger Rear Door, Rear Glass With Slider
+ - Vehicle Lookup Type: ZIP Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Standard
+ - Service Location Type: Dropoff
+ - Schedule Appointment Type: Dropoff
+ - Payment Type: Pay At Service
+ - Vehicle Info: Sliding glass vehicle, capability questions
+ +11. **CashReplaceMultiGlassMobile**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack, Driver Quarter Panel, Passenger Quarter Panel
+ - Vehicle Lookup Type: ZIP Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Premium
+ - Service Location Type: Mobile
+ - Schedule Appointment Type: Range Of Hours
+ - Payment Type: Pay At Service
+ +### Cash Payment - Promo Scenarios +12. **CashReplaceGlassPromoInshop**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: VIN Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Glass Service Only
+ - Service Location Type: In-Shop
+ - Schedule Appointment Type: Hourly
+ - Payment Type: Pay At Service
+ - Additional Info: Glass Promo
+ +13. **CashReplaceRainDefensePromoInshop**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: VIN Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Premium
+ - Service Location Type: In-Shop
+ - Schedule Appointment Type: Hourly
+ - Payment Type: Pay At Service
+ - Additional Info: Rain Defense Promo
+ +14. **CashReplaceWiperPromoInshop**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: VIN Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Standard
+ - Service Location Type: In-Shop
+ - Schedule Appointment Type: Hourly
+ - Payment Type: Pay At Service
+ - Vehicle Info: Wiper Promo
+ +15. **CashReplaceWiperDropoff**
+ - Repair or Replace: Replace
+ - Damage Type: Windshield Crack
+ - Vehicle Lookup Type: ZIP Lookup
+ - CASH or Insurance: CASH
+ - Service Type: Standard
+ - Service Location Type: Drop Off
+ - Schedule Appointment Type: Drop Off
+ - Payment Type: Pay At Service
+ +## 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
+• SauceLabs configuration in config.yml
+• Playwright configuration in playwright.config.ts
+ +## Project Structure + +├── tests/ # Test scenarios and cases
+├── pages/ # Page Object Models
+├── business-logic/ # Business logic and data models
+├── impl/ # Implementation utilities
+└── artifacts/ # Test artifacts and results
+ +## 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)
+• HTML format (playwright-report)
+• JUnit format
+• SauceLabs dashboard
+ +## Reccomended Visual Studio Code Extensions +• JavaScript and TypeScript Nightly
+• Playwright Test for VSCode
+ +## Key Features +Page Object Model implementation
+Data-driven test approach
+Cross-browser testing support
+Integration with SauceLabs
+Parallel test execution
+HTML report generation
+Environment-specific configurations
+ +## Common Test Flows +• Vehicle lookup validation
+• Shop selection
+• Damage assessment
+• Appointment scheduling
+• Coverage verification
+• Payment processing
+ + +## 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
+CCIS_API_URL=
+ADMIN_SERVICE_API_URL=
+SAUCE_USERNAME=
+SAUCE_ACCESS_KEY=
+ + +## CI/CD Integration +The project uses Azure Pipelines for continuous integration with configurations defined in azure-pipelines.yml. + diff --git a/playwright-tests/framework/TestData.ts b/playwright-tests/framework/TestData.ts new file mode 100644 index 000000000..8f00bb276 --- /dev/null +++ b/playwright-tests/framework/TestData.ts @@ -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 +} \ No newline at end of file diff --git a/playwright-tests/framework/TestPages.ts b/playwright-tests/framework/TestPages.ts new file mode 100644 index 000000000..cfc7940bd --- /dev/null +++ b/playwright-tests/framework/TestPages.ts @@ -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 = (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; +} \ No newline at end of file diff --git a/playwright-tests/framework/Typedefs.ts b/playwright-tests/framework/Typedefs.ts new file mode 100644 index 000000000..8584c9984 --- /dev/null +++ b/playwright-tests/framework/Typedefs.ts @@ -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; +export const prepareTest = prepareTest_T; + +export const RuleEngine = RuleEngine_T>; +export class TestCase extends TestCase_T {}; +export type TestInfo = TestInfo_T; + +export type ITestCase = ITestCase_T; diff --git a/playwright-tests/framework/localTypes/Enums.ts b/playwright-tests/framework/localTypes/Enums.ts new file mode 100644 index 000000000..067011f5e --- /dev/null +++ b/playwright-tests/framework/localTypes/Enums.ts @@ -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%' +} \ No newline at end of file diff --git a/playwright-tests/framework/localTypes/Step.ts b/playwright-tests/framework/localTypes/Step.ts new file mode 100644 index 000000000..ee62fd152 --- /dev/null +++ b/playwright-tests/framework/localTypes/Step.ts @@ -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 (value: T, context: Context): Method => { + return function (this: any, ...args: any[]): Promise { + // console.log(`Method ${value.name} has been decorated`, context, args, label); + return test.step(label, async () => { + return value.call(this, ...args); + }); + }; + }; +} \ No newline at end of file diff --git a/playwright-tests/package-lock.json b/playwright-tests/package-lock.json new file mode 100644 index 000000000..72efb6878 --- /dev/null +++ b/playwright-tests/package-lock.json @@ -0,0 +1,4076 @@ +{ + "name": "safelite-nextgen-playwright-tests", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "safelite-nextgen-playwright-tests", + "version": "0.1.0", + "hasInstallScript": true, + "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" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.14.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz", + "integrity": "sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/dotenv-safe": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@types/dotenv-safe/-/dotenv-safe-8.1.6.tgz", + "integrity": "sha512-ftZXu3WGT6ALq+f98IX2gWriGMPds+0ku5h8kZewNpY47ua+Z+XNcin9apZ2kVd4B9LV1vMfUOyDf1/hhreR0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "dotenv": "^8.2.0" + } + }, + "node_modules/@types/dotenv-safe/node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.32", + "resolved": "https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/@types/node/-/node-22.15.32.tgz", + "integrity": "sha1-wwHMInW1NaXlS7gdUWsdLpr+BuU=", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "entities": "^2.2.0" + }, + "bin": { + "ansi-to-html": "bin/ansi-to-html" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-safe": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/dotenv-safe/-/dotenv-safe-9.1.0.tgz", + "integrity": "sha512-2qwVAnUN+EDpu41pIK1XiJpHXKHV9Dnti3cE1EnUXT1/BV5+B7xuSZtgZ/4LExkCpp5F6BGikraezQL+8hKCOA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "dotenv": ">= 8.2.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ortoni-report": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/ortoni-report/-/ortoni-report-3.0.2.tgz", + "integrity": "sha512-d/UpsJczsDTrP1WTat0ZKNTwqKVPQQ8O8eBPq6UAc51dFPIoXm6iIV6zmQ0B6+KHAwsbysUfv9UqwepmGXbo8Q==", + "dev": true, + "license": "GPL-3.0-only", + "bin": { + "ortoni-report": "dist/cli/cli.js" + }, + "peerDependencies": { + "ansi-to-html": "^0.7.2", + "commander": "^12.1.0", + "express": "^4.21.1", + "handlebars": "^4.7.8", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright-jira-reporter": { + "version": "1.0.4", + "resolved": "https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/playwright-jira-reporter/-/playwright-jira-reporter-1.0.4.tgz", + "integrity": "sha1-uwnLFtj3Pnq5XXMFotlfHItucAw=", + "dev": true, + "license": "ISC", + "dependencies": { + "@playwright/test": "^1.48.0", + "axios": "^1.9.0", + "axios-retry": "^4.5.0", + "form-data": "^4.0.2", + "ortoni-report": "^3.0.2" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "peer": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/safelite-playwright-core": { + "version": "1.0.26", + "resolved": "https://pkgs.dev.azure.com/Safelite/Digital/_packaging/DigitalQA/npm/registry/safelite-playwright-core/-/safelite-playwright-core-1.0.26.tgz", + "integrity": "sha1-Nbm2PMNSqBQ9raXKVR4kacu2BP0=", + "dev": true, + "license": "ISC", + "dependencies": { + "@faker-js/faker": "^9.8.0", + "@playwright/test": "^1.52.0", + "axios": "^1.9.0", + "axios-retry": "^4.5.0", + "luxon": "^3.6.1" + }, + "bin": { + "safelite-playwright-init": "scripts/init.js" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/sqlite": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz", + "integrity": "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/playwright-tests/package.json b/playwright-tests/package.json new file mode 100644 index 000000000..2c9c47d00 --- /dev/null +++ b/playwright-tests/package.json @@ -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" +} diff --git a/playwright-tests/pages/AfterpayPage.ts b/playwright-tests/pages/AfterpayPage.ts new file mode 100644 index 000000000..b26949e50 --- /dev/null +++ b/playwright-tests/pages/AfterpayPage.ts @@ -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(); + } + + +} \ No newline at end of file diff --git a/playwright-tests/pages/BaseHomepage.ts b/playwright-tests/pages/BaseHomepage.ts new file mode 100644 index 000000000..e84d25908 --- /dev/null +++ b/playwright-tests/pages/BaseHomepage.ts @@ -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; + + // Method to check if this is the correct homepage variant + abstract isCurrentVariant(): Promise; + + async goto() { + await this.page.goto(process.env['BASE_URL']!); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts new file mode 100644 index 000000000..2d8d2b07a --- /dev/null +++ b/playwright-tests/pages/BasePage.ts @@ -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}`); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/CCPolicyInfoPage.ts b/playwright-tests/pages/CCPolicyInfoPage.ts new file mode 100644 index 000000000..557ba00f7 --- /dev/null +++ b/playwright-tests/pages/CCPolicyInfoPage.ts @@ -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 { + 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 { + 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) { + const { customerDetails, claimDetails} = testData; + + await this.populatePage(customerDetails!, claimDetails!, await this.hasCityInfo()); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/CapabilityQuestionsPage.ts b/playwright-tests/pages/CapabilityQuestionsPage.ts new file mode 100644 index 000000000..dd648282a --- /dev/null +++ b/playwright-tests/pages/CapabilityQuestionsPage.ts @@ -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) { + 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(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/ContactDetailsPage.ts b/playwright-tests/pages/ContactDetailsPage.ts new file mode 100644 index 000000000..543ffe1b2 --- /dev/null +++ b/playwright-tests/pages/ContactDetailsPage.ts @@ -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) { + const { customerDetails } = testData; + + await this.validateProgressBar(ProgressBarPercentages.ContactDetailsPage); + await this.enterContactDetails(customerDetails!); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/CoverageStatementPage.ts b/playwright-tests/pages/CoverageStatementPage.ts new file mode 100644 index 000000000..032104ec1 --- /dev/null +++ b/playwright-tests/pages/CoverageStatementPage.ts @@ -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) { + const { claimDetails } = testData; + + await this.validateDeductibleAmount(claimDetails!); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/DuplicateCheckPage.ts b/playwright-tests/pages/DuplicateCheckPage.ts new file mode 100644 index 000000000..088f7fb6a --- /dev/null +++ b/playwright-tests/pages/DuplicateCheckPage.ts @@ -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) { + await this.startNewClaim(); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/EndorsementsPage.ts b/playwright-tests/pages/EndorsementsPage.ts new file mode 100644 index 000000000..d5396180b --- /dev/null +++ b/playwright-tests/pages/EndorsementsPage.ts @@ -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) { + const { endorsements } = testData; + await this.verifyEndorsements(endorsements!); + await this.selectEndorsements(endorsements!); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/EstimatePage.ts b/playwright-tests/pages/EstimatePage.ts new file mode 100644 index 000000000..b004c3c47 --- /dev/null +++ b/playwright-tests/pages/EstimatePage.ts @@ -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) { + const { vehicleDetails } = testData; + await this.validateProgressBar(ProgressBarPercentages.EstimatePage); + await this.vehicleLookup(vehicleDetails!); + } +} diff --git a/playwright-tests/pages/HomePage.ts b/playwright-tests/pages/HomePage.ts new file mode 100644 index 000000000..3f934f9c9 --- /dev/null +++ b/playwright-tests/pages/HomePage.ts @@ -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 { + 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(); + } + + +} \ No newline at end of file diff --git a/playwright-tests/pages/InsuranceBasePage.ts b/playwright-tests/pages/InsuranceBasePage.ts new file mode 100644 index 000000000..b055bad0f --- /dev/null +++ b/playwright-tests/pages/InsuranceBasePage.ts @@ -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; + } + } +} \ No newline at end of file diff --git a/playwright-tests/pages/InsuranceCompanyPage.ts b/playwright-tests/pages/InsuranceCompanyPage.ts new file mode 100644 index 000000000..62c199da8 --- /dev/null +++ b/playwright-tests/pages/InsuranceCompanyPage.ts @@ -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 { + + 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) { + const { claimDetails } = testData; + + await this.validateProgressBar(ProgressBarPercentages.InsuranceCompanyPage); + await this.enterInsuranceCompany(claimDetails!.client!); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/LeadgenHomePage.ts b/playwright-tests/pages/LeadgenHomePage.ts new file mode 100644 index 000000000..8d2197728 --- /dev/null +++ b/playwright-tests/pages/LeadgenHomePage.ts @@ -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 { + return await this.zipTextbox.isVisible(); + } + +} \ No newline at end of file diff --git a/playwright-tests/pages/LookupPage.ts b/playwright-tests/pages/LookupPage.ts new file mode 100644 index 000000000..b964b3452 --- /dev/null +++ b/playwright-tests/pages/LookupPage.ts @@ -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 didn’t return a VIN match.Please re-enter the information above or provide your VIN in a different way.", + 'VIN': "Your VIN didn’t 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 { + + // 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; + } +} \ No newline at end of file diff --git a/playwright-tests/pages/MoldingQuestionsPage.ts b/playwright-tests/pages/MoldingQuestionsPage.ts new file mode 100644 index 000000000..3be6cd2f2 --- /dev/null +++ b/playwright-tests/pages/MoldingQuestionsPage.ts @@ -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) { + const { moldingQuestions } = testCase; + + await this.validateProgressBar(ProgressBarPercentages.MoldingQuestionsPage); + await this.validatePartQuestions(moldingQuestions!); + await this.selectPartQuestionResponses(moldingQuestions!); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/OrderConfirmationPage.ts b/playwright-tests/pages/OrderConfirmationPage.ts new file mode 100644 index 000000000..1f316dfad --- /dev/null +++ b/playwright-tests/pages/OrderConfirmationPage.ts @@ -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) { + // 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 { + let appointmentSummary: string[] = []; + for (let element of await this.appointmentSummarySection.all()){ + let text = (await element.textContent() || "").replaceAll(/\u00A0| /g, ' '); + appointmentSummary.push(text.trim()); + } + return appointmentSummary; + } + + async getExpectedAppointmentSummary(testData: Partial): Promise { + 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) { + 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}`); + }); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/PartQuestionPage.ts b/playwright-tests/pages/PartQuestionPage.ts new file mode 100644 index 000000000..a6ecbc0bd --- /dev/null +++ b/playwright-tests/pages/PartQuestionPage.ts @@ -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 = {}; + 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) { + const { partQuestions } = testData; + + await this.validateProgressBar(ProgressBarPercentages.PartQuestionsPage); + await this.validatePartQuestions(partQuestions!); + await this.selectPartQuestionResponses(partQuestions!); + await this.nextPage(); + } +} diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts new file mode 100644 index 000000000..87bf89c5e --- /dev/null +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -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) { + // 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(); + 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 { + 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 { + 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 { + // 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 { + let actualAppointmentDetails = new Map(); + 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, expectedServicePackageDetails: Map): Promise { + const { vehicleDetails } = testdata; + expectedServicePackageDetails["Vehicle"] = [vehicleDetails?.year + " " + vehicleDetails?.make + " " + vehicleDetails?.model]; + return expectedServicePackageDetails; + } + + async getExpectedVehicleDamage(testdata: Partial, expectedServicePackageDetails: Map): Promise { + + 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, expectedServicePackageDetails: Map): Promise { + 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, expectedServicePackageDetails: Map): Promise { + 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, expectedServicePackageDetails: Map): Promise { + 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, expectedServicePackageDetails: Map): Promise { + 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) { + 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(); + } + } +} \ No newline at end of file diff --git a/playwright-tests/pages/PaymentPage.ts b/playwright-tests/pages/PaymentPage.ts new file mode 100644 index 000000000..9bc95fb66 --- /dev/null +++ b/playwright-tests/pages/PaymentPage.ts @@ -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(); + } + +} \ No newline at end of file diff --git a/playwright-tests/pages/PaypalPage.ts b/playwright-tests/pages/PaypalPage.ts new file mode 100644 index 000000000..07e2cd1e6 --- /dev/null +++ b/playwright-tests/pages/PaypalPage.ts @@ -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(); + } + } +} diff --git a/playwright-tests/pages/PolicyDriverPage.ts b/playwright-tests/pages/PolicyDriverPage.ts new file mode 100644 index 000000000..f1c97e3ad --- /dev/null +++ b/playwright-tests/pages/PolicyDriverPage.ts @@ -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 { + // 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) { + const { customerDetails } = testData; + await this.selectPolicyDriver(customerDetails!); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/PolicyInfoSubmittedPage.ts b/playwright-tests/pages/PolicyInfoSubmittedPage.ts new file mode 100644 index 000000000..08134ea57 --- /dev/null +++ b/playwright-tests/pages/PolicyInfoSubmittedPage.ts @@ -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 { + 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(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/PolicyVehiclesPage.ts b/playwright-tests/pages/PolicyVehiclesPage.ts new file mode 100644 index 000000000..ddc2b1b6b --- /dev/null +++ b/playwright-tests/pages/PolicyVehiclesPage.ts @@ -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) { + + 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(); + } + } +} \ No newline at end of file diff --git a/playwright-tests/pages/RecalibrationInfoPage.ts b/playwright-tests/pages/RecalibrationInfoPage.ts new file mode 100644 index 000000000..2671367b6 --- /dev/null +++ b/playwright-tests/pages/RecalibrationInfoPage.ts @@ -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(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts new file mode 100644 index 000000000..0966e9984 --- /dev/null +++ b/playwright-tests/pages/SchedulePage.ts @@ -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) { + 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) { + + await this.validateProgressBar(ProgressBarPercentages.SchedulePage); + await this.scheduleFirstAppointment(testData); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/ServiceLocationPage.ts b/playwright-tests/pages/ServiceLocationPage.ts new file mode 100644 index 000000000..65c30481c --- /dev/null +++ b/playwright-tests/pages/ServiceLocationPage.ts @@ -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) { + 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) { + 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) { + + await this.validateProgressBar(ProgressBarPercentages.ServiceLocationPage); + await this.selectLocation(testData); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts new file mode 100644 index 000000000..71f246da5 --- /dev/null +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -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 { + const locators = { + [PaymentMethod.Insurance]: this.paywithInsuranceButton, + [PaymentMethod.SelfPay]: this.payOnMyOwnButton + } + + await locators[method].click(); + } + + async selectServicePackage(servicePackage: ServicePackage): Promise { + 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 { + 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 { + await this.iHavePromoCodeButton.click(); + await this.promoCodeTextbox.fill(promoCode); + await this.applyPromoButton.click(); + } + + async verifyCanNotRecal(): Promise { + // 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 { + // 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 { + // 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 { + // 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 { + // 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) { + 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(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/ServiceZipPage.ts b/playwright-tests/pages/ServiceZipPage.ts new file mode 100644 index 000000000..2aa446eca --- /dev/null +++ b/playwright-tests/pages/ServiceZipPage.ts @@ -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) { + 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!); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/VehicleDamagePage.ts b/playwright-tests/pages/VehicleDamagePage.ts new file mode 100644 index 000000000..5de6cb1d4 --- /dev/null +++ b/playwright-tests/pages/VehicleDamagePage.ts @@ -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 { + 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 { + 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 { + 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): Promise { + 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(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/VehicleLookupAddressPage.ts b/playwright-tests/pages/VehicleLookupAddressPage.ts new file mode 100644 index 000000000..c5f37ee3d --- /dev/null +++ b/playwright-tests/pages/VehicleLookupAddressPage.ts @@ -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) { + const { customerDetails, vehicleDetails, alertFlags } = testData; + + await this.validateProgressBar(ProgressBarPercentages.VehicleLookupAddressPage); + await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!); + await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/VehicleLookupLicensePage.ts b/playwright-tests/pages/VehicleLookupLicensePage.ts new file mode 100644 index 000000000..e20828b6e --- /dev/null +++ b/playwright-tests/pages/VehicleLookupLicensePage.ts @@ -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) { + 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!); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/VehiclePartsPage.ts b/playwright-tests/pages/VehiclePartsPage.ts new file mode 100644 index 000000000..63d079f87 --- /dev/null +++ b/playwright-tests/pages/VehiclePartsPage.ts @@ -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) { + const { vehiclePartQuestions } = testCase; + + await this.validateProgressBar(ProgressBarPercentages.VehiclePartsPage); + await this.validatePartQuestions(vehiclePartQuestions!); + await this.selectPartQuestionResponses(vehiclePartQuestions!); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/VehicleSelectionPage.ts b/playwright-tests/pages/VehicleSelectionPage.ts new file mode 100644 index 000000000..b83a85351 --- /dev/null +++ b/playwright-tests/pages/VehicleSelectionPage.ts @@ -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) { + 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(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/VerifyDetailsPage.ts b/playwright-tests/pages/VerifyDetailsPage.ts new file mode 100644 index 000000000..6b825e4b6 --- /dev/null +++ b/playwright-tests/pages/VerifyDetailsPage.ts @@ -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 { + // 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 { + 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) { + const { customerDetails, claimDetails } = testData; + await this.verifyPolicyDetails(customerDetails!, claimDetails!); + await this.nextPage(); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/VinLookupPage.ts b/playwright-tests/pages/VinLookupPage.ts new file mode 100644 index 000000000..3b69fc2d6 --- /dev/null +++ b/playwright-tests/pages/VinLookupPage.ts @@ -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) { + 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!); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/ZipLookupPage.ts b/playwright-tests/pages/ZipLookupPage.ts new file mode 100644 index 000000000..33122b852 --- /dev/null +++ b/playwright-tests/pages/ZipLookupPage.ts @@ -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 +} \ No newline at end of file diff --git a/playwright-tests/pages/forms/AddressForm.ts b/playwright-tests/pages/forms/AddressForm.ts new file mode 100644 index 000000000..4ddb6a512 --- /dev/null +++ b/playwright-tests/pages/forms/AddressForm.ts @@ -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) { + + 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); + } + } +} \ No newline at end of file diff --git a/playwright-tests/pages/forms/VehicleSelectionForm.ts b/playwright-tests/pages/forms/VehicleSelectionForm.ts new file mode 100644 index 000000000..361e42cf3 --- /dev/null +++ b/playwright-tests/pages/forms/VehicleSelectionForm.ts @@ -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(); + } +} \ No newline at end of file diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts new file mode 100644 index 000000000..2fe027426 --- /dev/null +++ b/playwright-tests/playwright.config.ts @@ -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, + // }, +}); diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts new file mode 100644 index 000000000..86cc54c81 --- /dev/null +++ b/playwright-tests/tests/0000__M.test.ts @@ -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 { + 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(); + } +} \ No newline at end of file diff --git a/playwright-tests/tests/CashRepairInShopAfterPay.ts b/playwright-tests/tests/CashRepairInShopAfterPay.ts new file mode 100644 index 000000000..bd5ff6abd --- /dev/null +++ b/playwright-tests/tests/CashRepairInShopAfterPay.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashRepairInShopPayPal.ts b/playwright-tests/tests/CashRepairInShopPayPal.ts new file mode 100644 index 000000000..2a36127f4 --- /dev/null +++ b/playwright-tests/tests/CashRepairInShopPayPal.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashRepairMobileCreditCard.ts b/playwright-tests/tests/CashRepairMobileCreditCard.ts new file mode 100644 index 000000000..b776f7fa5 --- /dev/null +++ b/playwright-tests/tests/CashRepairMobileCreditCard.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts b/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts new file mode 100644 index 000000000..7b16f582b --- /dev/null +++ b/playwright-tests/tests/CashReplaceDynamicRecalMobile.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts b/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts new file mode 100644 index 000000000..cfc273c1b --- /dev/null +++ b/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceGlassLicensePlateLookupInshopPaypal.ts b/playwright-tests/tests/CashReplaceGlassLicensePlateLookupInshopPaypal.ts new file mode 100644 index 000000000..defa4b36c --- /dev/null +++ b/playwright-tests/tests/CashReplaceGlassLicensePlateLookupInshopPaypal.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceGlassPromoInshop.ts b/playwright-tests/tests/CashReplaceGlassPromoInshop.ts new file mode 100644 index 000000000..8d3679f1b --- /dev/null +++ b/playwright-tests/tests/CashReplaceGlassPromoInshop.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceMultiGlassMobile.ts b/playwright-tests/tests/CashReplaceMultiGlassMobile.ts new file mode 100644 index 000000000..f67f44234 --- /dev/null +++ b/playwright-tests/tests/CashReplaceMultiGlassMobile.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts b/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts new file mode 100644 index 000000000..7237bb530 --- /dev/null +++ b/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts b/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts new file mode 100644 index 000000000..d2a54d58f --- /dev/null +++ b/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceRainDefensePromoInshop.ts b/playwright-tests/tests/CashReplaceRainDefensePromoInshop.ts new file mode 100644 index 000000000..d9fbd1a91 --- /dev/null +++ b/playwright-tests/tests/CashReplaceRainDefensePromoInshop.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts b/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts new file mode 100644 index 000000000..3729ff72b --- /dev/null +++ b/playwright-tests/tests/CashReplaceSafeliteCanNotRecalMobile.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceSplitWindshield.ts b/playwright-tests/tests/CashReplaceSplitWindshield.ts new file mode 100644 index 000000000..d1f12fef4 --- /dev/null +++ b/playwright-tests/tests/CashReplaceSplitWindshield.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceSwitchToInsuranceProgressiveNoComp.ts b/playwright-tests/tests/CashReplaceSwitchToInsuranceProgressiveNoComp.ts new file mode 100644 index 000000000..a6494792a --- /dev/null +++ b/playwright-tests/tests/CashReplaceSwitchToInsuranceProgressiveNoComp.ts @@ -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 = { + ...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; diff --git a/playwright-tests/tests/CashReplaceVinMobile.ts b/playwright-tests/tests/CashReplaceVinMobile.ts new file mode 100644 index 000000000..d47ab608f --- /dev/null +++ b/playwright-tests/tests/CashReplaceVinMobile.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceWiperDropoff.ts b/playwright-tests/tests/CashReplaceWiperDropoff.ts new file mode 100644 index 000000000..695eaba9a --- /dev/null +++ b/playwright-tests/tests/CashReplaceWiperDropoff.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceWiperPromoInshop.ts b/playwright-tests/tests/CashReplaceWiperPromoInshop.ts new file mode 100644 index 000000000..12cbbd6f1 --- /dev/null +++ b/playwright-tests/tests/CashReplaceWiperPromoInshop.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceAcuityPaypal.ts b/playwright-tests/tests/InsuranceAcuityPaypal.ts new file mode 100644 index 000000000..0a97fc794 --- /dev/null +++ b/playwright-tests/tests/InsuranceAcuityPaypal.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceBigTruckVerified.ts b/playwright-tests/tests/InsuranceBigTruckVerified.ts new file mode 100644 index 000000000..f31918279 --- /dev/null +++ b/playwright-tests/tests/InsuranceBigTruckVerified.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceGeico.ts b/playwright-tests/tests/InsuranceGeico.ts new file mode 100644 index 000000000..f71917ff2 --- /dev/null +++ b/playwright-tests/tests/InsuranceGeico.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceITAC21stCentury.ts b/playwright-tests/tests/InsuranceITAC21stCentury.ts new file mode 100644 index 000000000..cb11b4083 --- /dev/null +++ b/playwright-tests/tests/InsuranceITAC21stCentury.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceITACOptimizedPriceValidationAllState.ts b/playwright-tests/tests/InsuranceITACOptimizedPriceValidationAllState.ts new file mode 100644 index 000000000..5924830ba --- /dev/null +++ b/playwright-tests/tests/InsuranceITACOptimizedPriceValidationAllState.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceMeemicNearSchoolVerified.ts b/playwright-tests/tests/InsuranceMeemicNearSchoolVerified.ts new file mode 100644 index 000000000..c57526e1a --- /dev/null +++ b/playwright-tests/tests/InsuranceMeemicNearSchoolVerified.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceNoCompProgressive.ts b/playwright-tests/tests/InsuranceNoCompProgressive.ts new file mode 100644 index 000000000..e609d4120 --- /dev/null +++ b/playwright-tests/tests/InsuranceNoCompProgressive.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceUnverified.ts b/playwright-tests/tests/InsuranceUnverified.ts new file mode 100644 index 000000000..34d06697d --- /dev/null +++ b/playwright-tests/tests/InsuranceUnverified.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/alert-validation/alert0001_HeavyTruck.ts b/playwright-tests/tests/alert-validation/alert0001_HeavyTruck.ts new file mode 100644 index 000000000..e81038f44 --- /dev/null +++ b/playwright-tests/tests/alert-validation/alert0001_HeavyTruck.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/alert-validation/alert0002_RepairAndReplace.ts b/playwright-tests/tests/alert-validation/alert0002_RepairAndReplace.ts new file mode 100644 index 000000000..060f04b50 --- /dev/null +++ b/playwright-tests/tests/alert-validation/alert0002_RepairAndReplace.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/alert-validation/alert0003_SplitWindshield.ts b/playwright-tests/tests/alert-validation/alert0003_SplitWindshield.ts new file mode 100644 index 000000000..e79c0c175 --- /dev/null +++ b/playwright-tests/tests/alert-validation/alert0003_SplitWindshield.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/alert-validation/alert0004_RepairOnly.ts b/playwright-tests/tests/alert-validation/alert0004_RepairOnly.ts new file mode 100644 index 000000000..d4f1c2545 --- /dev/null +++ b/playwright-tests/tests/alert-validation/alert0004_RepairOnly.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/alert-validation/alert0005_UnserviceableZip.ts b/playwright-tests/tests/alert-validation/alert0005_UnserviceableZip.ts new file mode 100644 index 000000000..a4e3d44b3 --- /dev/null +++ b/playwright-tests/tests/alert-validation/alert0005_UnserviceableZip.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/alert-validation/alert0006_InvalidZip.ts b/playwright-tests/tests/alert-validation/alert0006_InvalidZip.ts new file mode 100644 index 000000000..df7a87cd0 --- /dev/null +++ b/playwright-tests/tests/alert-validation/alert0006_InvalidZip.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tests/alert-validation/alert0007_VinNotFound.ts b/playwright-tests/tests/alert-validation/alert0007_VinNotFound.ts new file mode 100644 index 000000000..d88ceeab6 --- /dev/null +++ b/playwright-tests/tests/alert-validation/alert0007_VinNotFound.ts @@ -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 = { + ...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; \ No newline at end of file diff --git a/playwright-tests/tsconfig.json b/playwright-tests/tsconfig.json new file mode 100644 index 000000000..717c7217e --- /dev/null +++ b/playwright-tests/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/src/digital-components/sierra-webchat/sierra-webchat.vue b/src/digital-components/sierra-webchat/sierra-webchat.vue index 77e5675f1..cc88b5433 100644 --- a/src/digital-components/sierra-webchat/sierra-webchat.vue +++ b/src/digital-components/sierra-webchat/sierra-webchat.vue @@ -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() { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 8435a7fb9..d80184901 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -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 diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue index cc343455e..1ea1b7cf2 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -442,7 +442,7 @@ export default { price: ans.buttonAuxillaryCopy, })); - await savePageData(this.pageName, { packages: toLog }); + await savePageData(this.pageName, { packages: toLog }, true); }, }, components: { diff --git a/src/router/methods/helpers/save-page-data.js b/src/router/methods/helpers/save-page-data.js index 1299f8900..9b57c635b 100644 --- a/src/router/methods/helpers/save-page-data.js +++ b/src/router/methods/helpers/save-page-data.js @@ -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 }); diff --git a/src/store/index.js b/src/store/index.js index 4a428c9e2..3797cea98 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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; diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 2119f1e8b..71e2ec894 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -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", diff --git a/temp/playwright-test.yml b/temp/playwright-test.yml new file mode 100644 index 000000000..5c6692586 --- /dev/null +++ b/temp/playwright-test.yml @@ -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() \ No newline at end of file