Added playwright tests into repo (#923)

* Initial import of playwright tests

* Pipeline changes for automated tests

* Modified pipeline for testing

* Attempt #2

* Attempt #3

* Added missing paren

* Removed debug stuff from pipeline

* Changes from playwright repo

* Changed pipeline for debugging

* Fix for ServiceLocationPage playwright locators

* Change pipeline to run with TEST APIs

* Moved more of Siraj's changes to this repo

* Changed where updating env occurs

* Changed location of env update again

* Escaped double quotes

* Added visible report in Azure

* Moved changes into main pipeline

* Made it so dotenv only runs config in local
This commit is contained in:
chase-safelite 2025-02-14 09:54:11 -05:00 committed by GitHub
parent 1cdcaa7bd6
commit 5d3ab0ef59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
135 changed files with 11467 additions and 137 deletions

7
.gitignore vendored
View file

@ -22,6 +22,13 @@ pnpm-debug.log*
*.sln
*.sw?
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
artifacts/
# Misc
coverage/*
junit.xml

21
Dockerfile.playwright Normal file
View file

@ -0,0 +1,21 @@
FROM node:16
FROM mcr.microsoft.com/playwright:v1.48.0-noble
# Set the working directory in the container
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Install Playwright browsers
RUN npx playwright install --with-deps
# Copy the rest of the application code
COPY . .
# Run Playwright tests
CMD ["npx", "playwright", "test"]

View file

@ -31,12 +31,18 @@ resources:
variables:
- group: Digital-Infrastructure
- group: ISS-BuildBranches
- name: dockerImageName
value: 'playwright-tests'
- name: imageTag
value: '$(Build.BuildId)'
- name: totalShards
value: 4
stages:
# PR's
- ${{ if eq(variables['Build.Reason'], 'PullRequest') }}:
- stage: TestPr
displayName: Run Unit Tests For PullRequest
displayName: Run Tests For PullRequest
jobs:
- template: templates/digital/vue-jest-run-unit-tests.yml@AzureDevOps
parameters:
@ -44,6 +50,123 @@ stages:
npmLocation: $(Build.SourcesDirectory)
testResultsFile: junit.xml
summaryFileLocation: $(Build.SourcesDirectory)/coverage/cobertura-coverage.xml
- job: playwright_tests
continueOnError: true
strategy:
matrix:
shard1:
shardNumber: 1
shard2:
shardNumber: 2
shard3:
shardNumber: 3
shard4:
shardNumber: 4
steps:
- task: Docker@2
displayName: 'Build Docker Image'
inputs:
command: build
dockerfile: Dockerfile.playwright
repository: $(dockerImageName)
tags: $(imageTag)
arguments: '--no-cache --pull'
- script: |
# Create container and run tests
container_id=$(docker create \
--ipc=host \
-e CCIS_API_AUTH=$(CCIS_API_AUTH) \
-e BASE_URL=$(BASE_URL) \
-e CCIS_API_URL=$(CCIS_API_URL) \
-e ADMIN_SERVICE_API_URL=$(ADMIN_SERVICE_API_URL) \
-e SHARD=$(shardNumber) \
-e CI=true \
-e NODE_ENV=$(NODE_ENV) \
$(dockerImageName):$(imageTag) \
npx concurrently -k -n "server,playwright"\
"sed -i \"s|^process\.env\.VUE_APP_CONSUMER_CF_DISTRO = .*|process\.env\.VUE_APP_CONSUMER_CF_DISTRO='https://digitalapi.test.safelite.io'|\" \"./vue.config.js\" && echo \"Updated config file to use TEST APIs\" && npm run serve -- --port=8080"\
"npx wait-on http://localhost:8080 && npm run test:playwright -- --shard=$(shardNumber)/$(totalShards) --reporter=list,blob --grep \"@smoke | @Advanced\"")
# 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)!"
exit 1
fi
displayName: 'Run Playwright Tests - Shard $(shardNumber)'
- 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 $(dockerImageName):$(imageTag) -f
displayName: 'Cleanup Docker Image'
condition: always()
- job: download_and_merge_reports
container:
image: mcr.microsoft.com/playwright:v1.48.0-noble
dependsOn: playwright_tests
steps:
- task: DownloadPipelineArtifact@2
inputs:
targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports'
- script: |
npm i ortoni-report &&
for dir in $(System.DefaultWorkingDirectory)/playwright-reports/*/; do
if [ -d "$dir" ]; then
mv "$dir"* $(System.DefaultWorkingDirectory)/playwright-reports/
rmdir "$dir"
fi
done
npx playwright merge-reports --reporter=ortoni-report,junit $(System.DefaultWorkingDirectory)/playwright-reports
ls
displayName: merge_reports
env:
PLAYWRIGHT_JUNIT_OUTPUT_FILE: "test-results/results.xml"
# TODO: Enable later when working on PR gate
# - task: PublishTestResults@2
# displayName: 'Publish test results'
# inputs:
# searchFolder: 'test-results'
# testResultsFormat: 'JUnit'
# testResultsFiles: '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'
- ${{ else }}:
# Dev Build/Deploy

2317
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,8 @@
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit --coverage --ci --colors",
"test:unit:lite": "vue-cli-service test:unit --ci"
"test:unit:lite": "vue-cli-service test:unit --ci",
"test:playwright": "playwright test --config=playwright-tests/playwright.config.ts"
},
"dependencies": {
"axios": "^1.4.0",
@ -29,11 +30,16 @@
"vue-router": "4.2.4"
},
"devDependencies": {
"@faker-js/faker": "^9.0.3",
"@pinia/testing": "0.1.2",
"@playwright/test": "^1.48.0",
"@rushstack/eslint-patch": "^1.3.2",
"@saucelabs/playwright-reporter": "^1.5.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/user-event": "14.4.3",
"@testing-library/vue": "6.6.1",
"@types/dotenv-safe": "^8.1.6",
"@types/node": "^22.7.5",
"@vitejs/plugin-vue": "4.2.3",
"@vitest/coverage-v8": "^0.34.1",
"@vue/cli-plugin-babel": "^5.0.8",
@ -42,8 +48,11 @@
"@vue/cli-service": "~5.0.0",
"@vue/test-utils": "^2.4.1",
"@vue/vue3-jest": "^27.0.0-alpha.1",
"axios": "^1.7.8",
"axios-mock-adapter": "^1.21.5",
"babel-jest": "^27.0.6",
"concurrently": "^9.1.2",
"dotenv-safe": "^9.1.0",
"eslint": "^8.45.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-import-resolver-alias": "1.1.2",
@ -55,10 +64,15 @@
"jest-serializer-vue": "^3.1.0",
"jsdoc": "^4.0.2",
"jsdom": "^22.1.0",
"luxon": "^3.5.0",
"ortoni-report": "^2.0.8",
"sass": "^1.77.8",
"sass-loader": "^12.0.0",
"saucectl": "^0.188.0",
"typescript-eslint": "^8.11.0",
"vite": "^4.5.9",
"vitest": "^0.33.0",
"volar-service-vetur": "latest"
"volar-service-vetur": "latest",
"wait-on": "^8.0.2"
}
}

View file

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

14
playwright-tests/.env Normal file
View file

@ -0,0 +1,14 @@
# CLIENT_NAME="undefined" # TODO: evaluate necessity of this
# CLIENT_TAG="undefined"
# BASE_URL="https://selfservice.dev.glassclaim.com"
CCIS_API_AUTH= "" # Input manually
# DEV
BASE_URL="https://selfservice.test.glassclaim.com"
CCIS_API_URL="https://api.test.belronus.io" # Test API connects to our dev environment for some reason
ADMIN_SERVICE_API_URL="https://issadminapi.dev.sagaws.net/iss-admin/api/v1/"
# QA
# BASE_URL="https://selfservice.test.glassclaim.com"
# CCIS_API_URL="https://api.test.belronus.io"

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

@ -0,0 +1,14 @@
# CLIENT_NAME="undefined" # TODO: evaluate necessity of this
# CLIENT_TAG="undefined"
# BASE_URL="https://selfservice.dev.glassclaim.com"
CCIS_API_AUTH="" # Input manually
# DEV
BASE_URL="https://selfservice.dev.glassclaim.com"
CCIS_API_URL="https://api.test.belronus.io" # Test API connects to our dev environment for some reason
ADMIN_SERVICE_API_URL="https://issadminapi.dev.sagaws.net/iss-admin/api/v1/"
# QA
# BASE_URL="https://selfservice.test.glassclaim.com"
# CCIS_API_URL="https://api.test.belronus.io"

View file

@ -0,0 +1,10 @@
CCIS_API_AUTH=
# DEV
BASE_URL="https://selfservice.dev.glassclaim.com"
CCIS_API_URL="https://api.dev.belronus.io"
# QA
# BASE_URL="https://selfservice.test.glassclaim.com"
# CCIS_API_URL="https://api.test.belronus.io"

6
playwright-tests/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
artifacts/

View file

@ -0,0 +1,74 @@
apiVersion: v1alpha
kind: playwright
showConsoleLog: true
sauce:
region: us-west-1
concurrency: 20
sauceignore: .sauceignore
playwright:
version: 1.48.2
configFile: playwright.config.ts
suites:
- name: 'ISS-NextGen-chromium'
numShards: 20
testMatch:
- tests/0000__M.test.ts
platformName: Windows 10
env:
DEBUG: "pw:worker"
SAUCE_USERNAME: $SAUCE_USERNAME
SAUCE_ACCESS_KEY: $SAUCE_ACCESS_KEY
params:
browserName: chrome
project: "chromium"
artifacts: "**test-results\\index.html"
artifacts:
cleanup: true
download:
match:
- '*'
when: always
directory: ./artifacts
# - name: 'Mobile Android Chrome Tests'
# shard: spec
# testMatch:
# - e2e/
# platformName: Windows 11
# params:
# browserName: chrome
# project: "Mobile Android Tests"
# - name: 'Desktop Safari'
# testMatch:
# - .ts
# platformName: Windows 10
# params:
# browserName: webkit
# project: "webkit"
# - name: 'Mobile iOS Tests'
# shard: spec
# testMatch:
# - e2e/
# platformName: macOS 13
# params:
# browserName: webkit
# project: "Mobile iOS Tests"
# env:
# DEBUG: "pw:worker"
docker:
file: Dockerfile
image: isscqa-playwright-image
rootDir: ./
reporters:
spotlight: # Prints an overview of failed or otherwise interesting jobs.
enabled: true
npm:
dependencies:
- "package.json"

View file

@ -0,0 +1,16 @@
# This file instructs saucectl to not package any files mentioned here.
.git/
.github/
.DS_Store
.hg/
.vscode/
.idea/
.gitignore
.hgignore
.gitlab-ci.yml
.npmrc
*.gif
screenshots
artifacts
# Remove this to have node_modules uploaded with code
# node_modules/

View file

@ -0,0 +1,273 @@
import { IClient } from "@business-logic/types/Client";
import { IPaymentDetails } from "@business-logic/types/CustomerDetails";
import { PaymentType } from "@business-logic/types/Enums";
const essentialClients: IClient[] = [
{
clientTag: 'A8156D39-5943-4D3B-88C3-0D1A8810B51',
accountName: 'Acadia',
clientFlags: {}
},
{
clientTag: '4fc3010a-ef5f-434d-93aa-7fdf4878d667',
accountName: 'AIG Private Client',
clientFlags: {}
},
{
clientTag: '93FE1F55-19FA-4B7D-8F49-6E6C9F0E2B42',
accountName: 'Alfa Alliance',
clientFlags: {}
},
{
clientTag: '1FC7999E-E0AC-484C-9AC9-267D1A2E5493',
accountName: 'American Family Insurance',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: '65FD47E0-F0DB-47FD-8CBB-E6F4F3BBF457',
accountName: 'Apparent Insurance',
clientFlags: {}
},
{
clientTag: '009E3AE9-8E4E-40B7-9C7B-4516F640F10',
accountName: 'Berkley One',
clientFlags: {}
},
{
clientTag: '0B3A3B02-5267-4302-A305-E742E1F1BFC5',
accountName: 'Branch Insurance',
clientFlags: {}
},
{
clientTag: '7DF15A1A-0FD3-42D3-A6B1-26FB36550BDC',
accountName: 'Brethren Mutual',
clientFlags: {}
},
{
clientTag: 'E2D26369-1739-402C-9A85-F5F748C72647',
accountName: 'Certainly',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: '394EF217-7882-4281-B4B8-FA11FDA7D500',
accountName: 'Continental Western',
clientFlags: {}
},
{
clientTag: 'BF29B5A9-EFD8-4138-B301-CAD9C2495E74',
accountName: 'Donegal Mutual',
clientFlags: {}
},
{
clientTag: 'FDEAB808-9922-4851-A692-7128ECF9F879',
accountName: 'Elephant Insurance',
clientFlags: {}
},
{
clientTag: '42AC5AFB-0A08-4845-935A-9FA3A1BBF2D0',
accountName: 'Encompass',
clientFlags: {}
},
{
clientTag: '1BED97B1-4593-4E09-AF1A-9DDD2CCAD0A4',
accountName: 'Farm Bureau',
clientFlags: {}
},
{
clientTag: '05CC1609-3631-4044-B45A-E78E13343B9A',
accountName: 'Federated Insurance',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: 'C05C2F61-26EB-4849-99DD-0172EDA4C70F',
accountName: 'Fremont Insurance',
clientFlags: {}
},
{
clientTag: 'B6911ACB-4B6A-4549-B674-0FC79AA6E782',
accountName: 'Grange Insurance Association',
clientFlags: {}
},
{
clientTag: 'BDA2D606-D34F-4ED3-83DD-7BF56AC9C2CE',
accountName: 'GuideOne Insurance',
clientFlags: {}
},
{
clientTag: '3CC58C8F-9207-44AF-B5F1-E1BA84CFADD1',
accountName: 'Hagerty',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: '6D13AEF6-CE16-400F-9A29-A1AED5C47D48',
accountName: 'Liberty Mutual',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: 'F680CCE4-6D88-4845-9FA4-746CF54B4AC',
accountName: 'M Plate',
clientFlags: {}
},
{
clientTag: '988C2DCB-C95E-452D-9AE3-23B719FB3991',
accountName: 'Main Street America',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: '1873b283-a0b5-4d10-b112-b6bab94cd9dd',
accountName: 'Maine Mutual Group',
clientFlags: {}
},
{
clientTag: 'A639E2E5-6347-4E38-92B3-37DEF73BE51A',
accountName: 'Merchants Insurance Group',
clientFlags: {}
},
{
clientTag: '9B37A4B6-B4C2-4FCA-9E7C-FF8F0AB48067',
accountName: 'Midvale',
clientFlags: {}
},
{
clientTag: 'F2617197-C39D-451C-A3D1-61C39BA0BD57',
accountName: 'M-Plate',
clientFlags: {}
},
{
clientTag: 'f4730f4c-f83f-41de-a543-3ce761f66802',
accountName: 'Mutual Benefit',
clientFlags: {}
},
{
clientTag: '1DE5F9C7-34F0-4115-9582-4BFDF89F8504',
accountName: 'Nationwide Insurance',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: 'EA776DDA-0686-43F3-B222-70F1531B9610',
accountName: 'Nationwide Private Client',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: 'D3AA7E5D-2184-4761-99C3-69ED8515916E',
accountName: 'Northstar',
clientFlags: {}
},
{
clientTag: '8879E236-AFEA-4B46-A94D-06078B84B641',
accountName: 'OnStar Insurance',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: '29EF1D8A-C3AF-4037-8FDB-AC7DBD50B1B5',
accountName: 'Pioneer State Mutual',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: 'BBC83F09-82D7-492D-A8E8-64594215E559',
accountName: 'Preferred Mutual',
clientFlags: {}
},
{
clientTag: 'D27EBC04-A912-4974-A9D1-E3EBC2B2575B',
accountName: 'QBE',
clientFlags: {}
},
{
clientTag: 'A5F7D473-29C4-4E2C-AD08-A3AB13FE0314',
accountName: 'Safeco Insurance',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: '32978a45-dd2e-4fc6-b2ca-7178efb40373',
accountName: 'Secura',
clientFlags: {}
},
{
clientTag: 'C471BDE5-7005-47C2-A544-933D988BCB7F',
accountName: 'Travelers Insurance',
clientFlags: { isTpaEnabled: true }
},
{
clientTag: 'd6ba651f-1611-4f32-a6f6-0267e1968eed',
accountName: 'Utica',
clientFlags: { isAuthenticationEnabled: true}
},
{
clientTag: '1065734B-2292-4392-8CAF-205F07752BBE',
accountName: 'Wayne Insurance',
clientFlags: {}
}
]
const advancedClients: IClient[] = [
{
clientTag: '1216EA5F-64D1-462A-A03F-34C0A652430E',
accountName: 'Liberty Mutual',
clientFlags: {}
}
]
const defaultCreditCardDetails: IPaymentDetails = {
paymentType: PaymentType.Credit,
cardNumber: '4111111111111111',
expirationMonth: '12 - December',
expirationYear: '2029',
cvv: '555',
billingAddress: {
street: '2088 Tuller St',
city: 'Columbus',
state: 'OH',
postalCode: '43028',
country: 'US'
}
}
const defaultAfterpayDetails: IPaymentDetails = {
paymentType: PaymentType.AfterPay,
username: 'itqatest@safelite.com',
password: 'Safelite1',
cardNumber: '4111 1111 1111 1111',
expirationMonth: '12',
expirationYear: '34',
cvv: '000'
}
const defaultPaypalDetails: IPaymentDetails = {
paymentType: PaymentType.Paypal,
password: 'Safelite1'
}
export default class ClientData {
static getEssentialClients() {
return essentialClients.sort(() => 0.5 - Math.random());
}
static getEssentialClientsWithTpaEnabled() {
return essentialClients.sort(() => 0.5 - Math.random()).filter(value => {
return value.clientFlags.isTpaEnabled;
});
}
static getEssentialClientsWithTpaDisabled() {
return essentialClients.sort(() => 0.5 - Math.random()).filter(value => {
return !value.clientFlags.isTpaEnabled;
});
}
static getAdvancedClients() {
return advancedClients;
}
static getDefaultCreditCardDetails() {
return defaultCreditCardDetails;
}
static getDefaultAfterpayDetails() {
return defaultAfterpayDetails;
}
static getDefaultPaypalDetails() {
return defaultPaypalDetails;
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,70 @@
//import { AircraftTypes, AssignmentTypes, EnhancementBundleOptions, EnhancementOptions, ModificationTypes, OpportunityTypes, ProductAttributes, ProgramTypes, TransactionSubTypes, TwentyFiveHrLeaseProducts } from "@business-logic/types/Enums";
import TestCase from "@business-logic/types/TestCase";
import { Rule } from "../types/RuleEngine";
import EnumUtils from "../../impl/utils/EnumUtils";
//import { validateEnhancementOptions, validateProductAttributes } from "./ApplicableModifications";
//import TransactionData from "@business-logic/types/TransactionData";
export enum BuiltInRules {
TransactionExists = 1000,
PricingExists = 1001,
TerminationIsMod = 1002,
TerminationHasReason = 1003,
AircraftRequirement = 1004,
ModificationTypeAssignment = 1005,
ModificationTypeTrade = 1006,
TwentyFiveHourLeaseSpecialty = 1007,
TransactionPremiumOrNonPremiumOnly = 1008,
OldTransactionPremiumOrNonPremiumOnly = 1009,
PremiumSelectionRequirement = 1010,
OldTransactionRequiresPremiumSelection = 1011,
TerminationAndRepurchaseAdjustmentAmount = 1012,
ProgramSpecificAttributes = 1013,
ProgramSpecificEnhancements = 1014,
InterimLeaseHasAircraftRate = 1015,
ShareBinderNoPremiumSelection = 1016,
TwentyFiveHourLeaseMustDefineTwentyFiveHourProduct = 1017,
PartialAssignmentHoursToBeAssigned = 1018,
MinicartLineItemsCannotBeEmpty = 1019,
WaiveInternationalFeesValue = 1020,
EarlyOutOptionTimeframeAndFeeStatus = 1021,
DelayedStartDateOffsetRequiresValue = 1020,
EnhancementsRequireAValue = 1021,
NonPremiumMMFIncentiveRequirements = 1022,
QSExecutiveRequirements = 1023
}
// Built-Ins shouldn't depend on other rules, custom rules however are supposed to depend on them
export const builtInRules: Rule<TestCase>[] =
[
//{
// id: BuiltInRules.TransactionExists,
// name: "Transaction Exists Rule",
// check: (testCase: TestCase) => {
// return testCase.transaction != null;
// }
// }, {
// id: BuiltInRules.PricingExists,
// name: "Pricing must exist on Transaction Rule",
// check: (testCase: TestCase) => {
// return testCase.transaction != null && testCase.transaction.pricing != null;
// }
// }, {
// id: BuiltInRules.TerminationIsMod,
// name: "OpportunityType Termination requires TransactionSubType Mod",
// check: (testCase: TestCase) => {
// // Check old transactions
// for (var transaction of testCase.oldTransactions) {
// if (transaction.opportunityType == OpportunityTypes.Termination)
// return transaction.subType == TransactionSubTypes.Mod;
// }
// // Check transaction
// if (testCase.transaction.opportunityType == OpportunityTypes.Termination)
// return testCase.transaction.subType == TransactionSubTypes.Mod;
// return true;
// },
// dependsOn: [BuiltInRules.TransactionExists]
// }, {
];

View file

@ -0,0 +1,40 @@
import { Authentication, CertificateType, SignatureAlgorithm, SubType } from "./Enums"
export interface IClientSignatureRequest {
clientTag: string,
token: string,
certificateFileName: string,
certificateKey: string,
certificateAlgorithm: SignatureAlgorithm,
certificateType: CertificateType
}
export interface IClientSignatureResponse {
signature: string,
encrypted: string
}
export interface ICertificateInfo {
name: string;
key: string;
type: CertificateType;
algorithm: SignatureAlgorithm;
}
interface IClientAuthenticationFlags {
claimRegistrationRequired: boolean;
tpaEnabled: boolean;
clientDisplayName: string;
}
export interface IClientAuthentication {
clientTag: string;
accountName: string;
accountNumber: string;
active: boolean;
authentication: Authentication;
certificateInfo: ICertificateInfo[];
clientFlags: IClientAuthenticationFlags;
parameters: string[];
subType: SubType;
}

View file

@ -0,0 +1,12 @@
export interface IPostSaveFakeResponseRequestBody {
accountNumber: string,
responseType: string,
key: string,
value: string
}
export interface IDeleteFakeResponseParams {
accountNumber: string,
key: string,
responseType: string
}

View file

@ -0,0 +1,10 @@
export interface IClient {
clientTag: string,
accountName: string
clientFlags: Partial<IClientFlags>
}
export interface IClientFlags {
isTpaEnabled: boolean;
isAuthenticationEnabled: boolean;
}

View file

@ -0,0 +1,81 @@
import { DamageLocation, DamageSubLocation, DamageType as DamageCause, WindshieldDamage, ServiceLocation, EndorsementType, VehicleLookupType, PartQuestionType, PaymentType } from "./Enums";
import { IAddress } from "./IAddress";
export interface ICustomerDetails {
firstName: string,
lastName: string,
email: string,
phoneNumber: string,
notes: string,
address: IAddress,
apptDate?: string,
packagePrice?: String
}
export interface IClaimDetails {
policyNumber: string,
policyDeductible: number,
damageDate: string,
damageCause: DamageCause
}
export interface IEndorsementDetails {
endorsementType: EndorsementType,
isOnPolicy: boolean, // Should we expect this endorsement to appear?
isClickYes: boolean // Should we click Yes or No?
}
export interface IVehicleDetails {
year: string,
make: string,
model: string,
style?: string,
vin?: string,
licensePlateNumber?: string,
licensePlateState?: string,
vehicleLookupType?: VehicleLookupType,
}
export interface IAppointmentDetails {
serviceLocation: ServiceLocation,
appointmentDate?: Date,
shopAddress?: string, // Used for in-shop
serviceAddress?: IAddress, // Used for mobile
isVehicleProtected?: boolean // Used for mobile
alternateServiceZip?: string // Used for in-shop and drop-off if doing service in a different ZIP
}
export interface IPartQuestion {
isOnPage: boolean,
optionToSelect: string,
partQuestionType: PartQuestionType,
secondaryQuestionOptionToSelect?: string
}
export interface IPaymentDetails {
paymentType: PaymentType,
username?: string,
password?: string,
cardNumber?: string,
expirationMonth?: string,
expirationYear?: string,
cvv?: string,
billingAddress?: IAddress,
}
// export interface IVehicleDamage {
// isRearWindowDamage?: boolean,
// windshieldDamage?: WindshieldDamage,
// windowDamage?: IWindowDamage
// }
// interface ISideDoorDamage {
// isFrontDoor: boolean,
// isBackDoor: boolean,
// isQuarterPanel: boolean
// }
// export interface IWindowDamage {
// driverSideDamage?: ISideDoorDamage,
// passengerSideDamage?: ISideDoorDamage
// }

View file

@ -0,0 +1,38 @@
export interface IPartsOrQuestionsResponse {
partsOrQuestions: IPartOrQuestion[]
}
export interface IPartOrQuestion {
glassPiece: IGlassPiece,
parts: IPart[],
partQuestions: IPartQuestion[] | null;
}
export interface IGlassPiece {
name: string,
location: string
}
export interface IPart {
childPartQuestions: IPartQuestion[],
basePartNumber: string,
safelitePartNumber: string,
color: string,
requiresRecalibration: boolean,
recalibrationType: null, // TODO: Add types
canSafeliteRecalibrate: boolean,
requiresCapabilityQuestions: boolean,
childParts: IChildPart[],
partNumber: string,
description: string,
partType: string
}
export interface IPartQuestion {
// TODO: Define
}
export interface IChildPart {
partNumber: string,
safelitePartNumber: string
}

View file

@ -0,0 +1,169 @@
export enum DamageLocation {}
export enum DamageSubLocation {}
export enum DamageType {
Rock = 'Rock from road',
Vandalism = 'Vandalism',
Theft = 'Attempted theft or theft',
Hail = 'Hailstorm',
HurricaneStorm = 'Hurricane/storm',
Collision = 'Collision',
Object = 'Object hit glass',
Other = 'Other/unknown'
}
export enum ServiceLocation {}
export enum ServicePackage {
GlassOnly = 'Glass service only',
Standard = 'Standard',
Premium = 'Premium'
}
export enum EndorsementType {
Educator = '01',
EmployeeParking = '03'
}
export enum ConsoleColor {
Default = "",
Red = "\x1b[31m",
Green = "\x1b[32m",
Yellow = "\x1b[33m",
Orange = "\x1b[202m",
Blue = "\x1b[34m",
Magenta = "\x1b[35m",
Cyan = "\x1b[36m",
Reset = "\x1b[0m"
}
export enum ResultTypes {
None = 0,
Skipped = 1,
Success = 2,
Failure = 3
}
export enum VehicleDamage {
WindshieldOneChip,
WindshieldTwoChips,
WindshieldThreeChips,
WindshieldCrack,
RearWindow,
DriverFrontDoor,
DriverRearDoor,
DriverVentGlass,
DriverQuarterPanel,
DriverSlidingDoor,
PassengerFrontDoor,
PassengerRearDoor,
PassengerVentGlass,
PassengerQuarterPanel
}
export enum VehicleLookupType {
Vin,
Address,
LicensePlateNumber
}
export enum ServiceLocation {
Mobile,
InShop,
DropOff
}
export enum PartQuestionType {
WindshieldColor = 'Windshield-Single',
DriverFrontColor = 'Driver-Front',
DriverQuarterColor = 'Driver-Quarter',
DriverRearColor = 'Driver-Back',
DriverVentColor = 'Driver-Vent',
PassengerFrontColor = 'Passenger-Front',
PassengerQuarterColor = 'Passenger-Quarter',
PassengerRearColor = 'Passenger-Back',
PassengerVentColor = 'Passenger-Vent',
RearWindowColor = 'Rear-Stationary',
LeatherSeats = 'question-0-1',
DriverSideColor = 'Driver-SideDoor',
LaneKeepAssist = 'question-0-1'
}
export enum PaymentType{
Credit = "Credit",
AfterPay = "AfterPay",
Paypal = "Paypal",
PayAtService = "Pay at Service"
}
// Enums from original ISSCQA Project
// TODO: re-evaluate
export enum WindshieldDamage{
Crack,
OneChip,
TwoChips,
ThreeChips
}
export enum NumChips{
One = 1,
Two,
Three
}
export enum AppointmentType{
Inshop = "Inshop",
Mobile = "Mobile",
DropOff = "Drop-off"
}
export enum ServiceProvider{
Safelite = "Safelite",
ThirdParty = "Third Party"
}
export enum SideDoorDamage{
Passenger = "Passenger",
Driver = "Driver"
}
export enum BailoutCode {
Unknown = 0,
SaveSessionError,
VehicleNotFound,
VehicleLookupError,
CoverageStatementInvalidState,
DoNotSeeMyShop,
PricingResponseError,
TPANotEnabled,
RequestCallback,
HeavyTruckVehicle,
NoPartsAvailable,
PartsServiceError,
SafeliteNotTheProvider
}
export enum SignatureAlgorithm {
SHA1,
SHA256,
SHA512
}
export enum CertificateType {
RSA
}
export enum SubType {
Advanced = 'Advanced',
Essential = 'Essential',
Unknown = 'Unknown'
}
export enum Authentication {
None = 'None',
RSAToken = 'RSAToken',
RSATokenEncParams = 'RSATokenEncParams',
RSATokenEncParamsOneTimeUse = 'RSATokenEncParamsOneTimeUse',
Unknown = 'Unknown',
}

View file

@ -0,0 +1,11 @@
type FrameworkConfig = {
// company: NetJetsCompanies;
// companyString: string;
// currency: CurrencyTypes;
createResources: boolean;
destroyResources: boolean;
maxAllotmentHours: number;
};
export default FrameworkConfig;

View file

@ -0,0 +1,7 @@
export interface IAddress {
street: string,
city: string,
state: string,
postalCode: string,
country: string
}

View file

@ -0,0 +1,11 @@
export default interface IBailoutFlags {
isVehicleSelectBailout: boolean,
isDoNotSeeMyShopBailout: boolean,
isTpaNotEnabledBailout: boolean,
isRequestCallbackBailout: boolean,
isHeavyTruckVehicleBailout: boolean,
isPartsServiceErrorBailout: boolean,
isSafeliteNotTheProviderBailout: boolean,
isVehicleLookupBailout: boolean,
isPriceServiceErrorBailout: boolean
}

View file

@ -0,0 +1,29 @@
import LoggingUtils from "@impl/utils/LoggingUtils";
import { ConsoleColor } from "@business-logic/types/Enums";
import TestCase from "@business-logic/types/TestCase";
export interface IDisposable {
disposeAll(): void;
setupAll(): void;
}
export abstract class DisposableBase implements IDisposable {
protected abstract setup(): Promise<void>;
protected abstract dispose(): Promise<void>;
public async setupAll(): Promise<void> {
if (!TestCase.FrameworkConfig.destroyResources) {
LoggingUtils.log(TestCase.Constants.CREATION_HALTED, ConsoleColor.Yellow);
return;
}
await this.setup();
}
public async disposeAll(): Promise<void> {
if (!TestCase.FrameworkConfig.destroyResources || !TestCase.FrameworkConfig.createResources) {
LoggingUtils.log(TestCase.Constants.DISPOSE_HALTED, ConsoleColor.Yellow);
return;
}
await this.dispose();
}
}

View file

@ -0,0 +1,16 @@
import ITestPages from "@business-logic/types/ITestPages";
import Validations from "./Validations";
import { ITestData } from "./ITestData";
export default interface ITestCase {
readonly testID?: string;
readonly name: string;
readonly tags: string[];
readonly validations?: Validations;
readonly tempData?: any[]
readonly testData: Partial<ITestData>;
pages?: ITestPages;
}

View file

@ -0,0 +1,38 @@
import { ServicePackage, VehicleDamage } from "./Enums"
import { IAppointmentDetails, IClaimDetails, ICustomerDetails, IEndorsementDetails, IPartQuestion, IPaymentDetails, IVehicleDetails } from "./CustomerDetails"
import IBailoutFlags from "./IBailoutFlags"
import { IPart } from "./DigitalApi"
export interface ITestData {
isMockTesting: boolean,
clientTag: string,
isDuplicateClaim: boolean,
isPolicyFound: boolean, // Effective difference between advanced and essential
isUseVehicleOnPolicy: boolean, // Should we use the vehicle on the policy?
isNoComp: boolean, // Is this a NoComp policy?
isItac: boolean, // Is this an ITAC scenario?
hasStateLawPopup: boolean, // Are we expecting a state law pop-up on ProviderSelectionPage?
hasOemEndorsement: boolean, // OEM Endorsement does not appear on endorsements page, so it has a separate flag.
hasMilitaryWarning: boolean, // Are we expecting military base warning on the Service Location page?
bailoutFlags: Partial<IBailoutFlags>,
endorsements: IEndorsementDetails[],
isReplace: boolean,
partQuestions: IPartQuestion[], // part-questions page has unrelated part questions like leather seats
vehiclePartQuestions: IPartQuestion[], // vehicle-parts page usually has glass question
capabilityQuestions: IPartQuestion[], // Capability questions page usually has questions about autonomous driving features
policySoap: string, // Include policy soap if you want to create a mock policy
isSafelite: boolean,
servicePackage: ServicePackage,
customerDetails: ICustomerDetails,
claimDetails: IClaimDetails,
vehicleDetails: IVehicleDetails,
editVehicleDetails: IVehicleDetails, // Vehicle details entered after clicking "Edit vehicle" on Vehicle Details page
otherVehiclesOnPolicy: IVehicleDetails[], // IF defined, we validate that the vehicles are present.
vehicleDamage: VehicleDamage[], // Array of vehicle damage
appointmentDetails: IAppointmentDetails,
paymentDetails: IPaymentDetails // Payment information
isRecalNotification: boolean,
isRecalWarning: boolean,
isSeparateApptsWarning: boolean, // IF true, check for the separate appts warning on VehicleDamagePage
isAuthenticationRequired: boolean
}

View file

@ -0,0 +1,61 @@
import { BailoutPage } from "../../pages/BailoutPage";
import CapabilityQuestionsPage from "../../pages/CapabilityQuestionsPage";
import { ContactConfirmationPage } from "../../pages/ContactConfirmationPage";
import { ContactDetailsPage } from "../../pages/ContactDetailsPage";
import { CoverageStatementPage } from "../../pages/CoverageStatementPage";
import { DuplicateCheckPage } from "../../pages/DuplicateCheckPage";
import { EndorsementsPage } from "../../pages/EndorsementsPage";
import { OrderConfirmationPage } from "../../pages/OrderConfirmationPage";
import { PartQuestionsPage } from "../../pages/PartQuestionsPage";
import { PaymentMethodPage } from "../../pages/PaymentMethodPage";
import { PaymentPage } from "../../pages/PaymentPage";
import { PaypalPage } from "../../pages/PaypalPage";
import { PolicyHolderDetailsPage } from "../../pages/PolicyHolderDetailsPage";
import { PolicyVehiclesPage } from "../../pages/PolicyVehiclesPage";
import { ProviderPreferencePage } from "../../pages/ProviderPreferencePage";
import { SchedulePage } from "../../pages/SchedulePage";
import { ServiceLocationPage } from "../../pages/ServiceLocationPage";
import { ServicePackagesPage } from "../../pages/ServicePackagesPage";
import { TpaConfirmationPage } from "../../pages/TpaConfirmationPage";
import { TpaSearchPage } from "../../pages/TpaSearchPage";
import { TpaSubmitPage } from "../../pages/TpaSubmitPage";
import { VehicleDamagePage } from "../../pages/VehicleDamagePage";
import { VehicleLookupAddressPage } from "../../pages/VehicleLookupAddressPage";
import { VehicleLookupLicensePage } from "../../pages/VehicleLookupLicensePage";
import { VehicleLookupPage } from "../../pages/VehicleLookupPage";
import VehiclePartQuestionsPage from "../../pages/VehiclePartsPage";
import { VehicleSelectionPage } from "../../pages/VehicleSelectionPage";
import { VinLookupPage } from "../../pages/VinLookupPage";
import { WelcomePage } from "../../pages/WelcomePage";
export default interface ITestPages {
bailoutPage: BailoutPage,
capabilityQuestionsPage: CapabilityQuestionsPage,
contactConfirmationPage: ContactConfirmationPage,
contactDetailsPage: ContactDetailsPage,
coverageStatementPage: CoverageStatementPage,
duplicateCheckPage: DuplicateCheckPage,
endorsementsPage: EndorsementsPage,
orderConfirmationPage: OrderConfirmationPage,
partQuestionsPage: PartQuestionsPage,
paymentMethodPage: PaymentMethodPage,
paymentPage: PaymentPage,
paypalPage: PaypalPage,
policyHolderDetailsPage: PolicyHolderDetailsPage,
policyVehiclesPage: PolicyVehiclesPage,
providerPreferencePage: ProviderPreferencePage,
schedulePage: SchedulePage,
serviceLocationPage: ServiceLocationPage,
servicePackagesPage: ServicePackagesPage,
tpaConfirmationPage: TpaConfirmationPage,
tpaSearchPage: TpaSearchPage,
tpaSubmitPage: TpaSubmitPage,
vehicleDamagePage: VehicleDamagePage,
vehicleLookupPage: VehicleLookupPage,
vehiclePartQuestionsPage: VehiclePartQuestionsPage,
vehicleSelectionPage: VehicleSelectionPage,
vinLookupPage: VinLookupPage,
vehicleLookupAddressPage: VehicleLookupAddressPage,
vehicleLookupLicensePage: VehicleLookupLicensePage,
welcomePage: WelcomePage
}

View file

@ -0,0 +1,11 @@
export default interface IValidationExpectation {
// readonly Minicart: boolean;
// readonly SummaryTotals: boolean;
// readonly SummarySection: boolean;
// readonly AgreementPricing: boolean;
// readonly AllotmentStatus: boolean;
// readonly ChevronStatus: boolean;
// readonly AgreementDates: boolean;
// readonly LineItems: boolean;
// readonly AccountingTotal: boolean;
}

View file

@ -0,0 +1,13 @@
export default interface IValidations {
// readonly minicartLineItems: boolean;
// readonly minicartTotals: boolean;
// readonly cartSummary: boolean;
// readonly agreementPricing: boolean;
// readonly dealDescription: boolean;
// readonly allotmentStatus: boolean;
// readonly chevronStatus: boolean;
// readonly agreementDates: boolean;
// readonly lineItems: boolean;
// readonly accountingTotal: boolean;
}

View file

@ -0,0 +1,241 @@
import { BuiltInRules, builtInRules } from "@business-logic/rules/RuleEngineBuiltins";
import LoggingUtils from "@impl/utils/LoggingUtils";
import { ConsoleColor, ResultTypes } from "@business-logic/types/Enums";
export type Rule<T> = {
id: number;
name: string;
check: (obj: T) => boolean;
dependsOn?: number[];
};
export class ValidationOptions {
public skipBuiltIns: boolean;
public exclude: number[];
public throwOnError: boolean = true;
public errorsOnly: boolean = false;
constructor(options: {
throwOnError?: boolean;
errorsOnly?: boolean;
skipBuiltIns?: boolean;
exclude?: number[];
} = {}) {
this.throwOnError = options.throwOnError ?? true, this.errorsOnly = options.errorsOnly ?? true, this.skipBuiltIns = options.skipBuiltIns ?? false;
this.exclude = options.exclude ?? [];
}
}
export class RuleEngine<T> {
private rules: Rule<T>[] = [];
private nextCustomRuleId = 1;
static readonly BuiltInRuleIds: Record<BuiltInRules, number> = {} as Record<BuiltInRules, number>;
constructor() {
this.addBuiltInRules(builtInRules as { id: BuiltInRules; name: string; check: (obj: T) => boolean; dependsOn?: BuiltInRules[]; }[]);
}
private addBuiltInRules(builtInRules: { id: BuiltInRules; name: string; check: (obj: T) => boolean; dependsOn?: BuiltInRules[]; }[]): void {
builtInRules.forEach(rule => this.addRuleInternal(rule.name, rule.check, rule.id, rule.dependsOn));
}
private addRuleInternal(name: string, check: (obj: T) => boolean, builtInId: BuiltInRules, dependsOn?: BuiltInRules[]): number {
const id = builtInId;
const dependsOnIds = dependsOn?.map(dep => dep as number);
this.rules.push({ id, name, check, dependsOn: dependsOnIds });
RuleEngine.BuiltInRuleIds[builtInId] = id;
return id;
}
addRule(name: string, check: (obj: T) => boolean, options?: { dependsOn?: (number | BuiltInRules)[]; }): Rule<T> {
if (this.nextCustomRuleId >= 1000)
throw new Error("Maximum number of custom rules (999) has been reached.");
const id = this.nextCustomRuleId++;
const dependsOn = options?.dependsOn?.map(dep => typeof dep === "number" ? dep : RuleEngine.BuiltInRuleIds[dep]);
const rule: Rule<T> = { id, name, check, dependsOn };
this.rules.push(rule);
return rule;
}
when(condition: (obj: T) => boolean): WhenClause<T> {
return new WhenClause(this, condition);
}
checkRuleById(id: number | BuiltInRules, obj: T): boolean {
const ruleId = typeof id === "number" ? id : RuleEngine.BuiltInRuleIds[id];
const rule = this.rules.find(r => r.id === ruleId);
return rule ? rule.check(obj) : true;
}
validate(obj: T): ValidationResult[] {
const sortedRules = this.sortRules();
const results = new Map<number, boolean>();
const resultSummary: ValidationResult[] = [];
sortedRules.forEach(rule => {
const dependenciesValid = rule.dependsOn ? rule.dependsOn.every(dep => results.get(dep)) : true;
const result = dependenciesValid && rule.check(obj);
results.set(rule.id, result);
const resultType: ResultTypes = dependenciesValid ? result ? ResultTypes.Success : ResultTypes.Failure : ResultTypes.Skipped;
resultSummary.push(ValidationResult.FromRule(rule.name, rule.id, resultType, `Rule: "${rule.name}". Result: ${ResultTypes[resultType]}`));
});
return resultSummary;
}
private sortRules(): Rule<T>[] {
const sortedRules: Rule<T>[] = [];
const rulesMap = new Map(this.rules.map(rule => [rule.id, rule]));
const visit = (rule: Rule<T>, visited: Set<number>, stack: Set<number>) => {
if (stack.has(rule.id))
throw new Error(`Circular dependency detected in rule: ${rule.name}!`);
if (!visited.has(rule.id)) {
stack.add(rule.id);
if (rule.dependsOn) {
for (const dependency of rule.dependsOn) {
const dependencyRule = rulesMap.get(dependency);
if (dependencyRule)
visit(dependencyRule, visited, stack);
}
}
stack.delete(rule.id);
visited.add(rule.id);
sortedRules.push(rule);
}
};
const visited = new Set<number>();
for (const rule of this.rules)
visit(rule, visited, new Set<number>());
return sortedRules;
}
getRulesByName(name: string): Rule<T>[] {
return this.rules.filter(rule => rule.name === name);
}
getRulesByID(id: number): Rule<T>[] {
return this.rules.filter(rule => rule.id === id);
}
static PrintValidationResults(results: ValidationResult[], options: ValidationOptions = new ValidationOptions()) {
if (options.skipBuiltIns)
results = results.filter(x => x.ruleID < 1000);
if (options.exclude)
results = results.filter(x => !options.exclude!.includes(x.ruleID));
if (options.errorsOnly)
results = results.filter(x => x.result == ResultTypes.Failure);
if (results.length > 0) {
LoggingUtils.log("Rule Validation:", ConsoleColor.Blue);
results.forEach(x => LoggingUtils.log(` ${LoggingUtils.icon(x.result!)} Rule: [${x.ruleID}] "${x.ruleName}". Result: ${ResultTypes[x.result!]}`, x.result == ResultTypes.Success ? ConsoleColor.Green : ConsoleColor.Red));
console.log();
}
if (options.throwOnError && results.filter(x => x.result != ResultTypes.Success).length > 0)
throw new Error("Rule Validation Errors");
}
}
export class WhenClause<T> {
constructor(private ruleEngine: RuleEngine<T>, private condition: (obj: T) => boolean) { }
then(consequent: (obj: T) => boolean): DescriptionClause<T> {
return new DescriptionClause(this.ruleEngine, (obj: T) => {
return !this.condition(obj) || consequent(obj);
}, []);
}
}
export class DescriptionClause<T> {
private dependencies: (number | BuiltInRules)[] = [];
constructor(private ruleEngine: RuleEngine<T>, private check: (obj: T) => boolean, dependencies: (number | BuiltInRules)[] = []) {
this.dependencies = dependencies;
}
because(description: string): Rule<T> {
return this.ruleEngine.addRule(description, this.check, { dependsOn: this.dependencies });
}
dependsOn(...dependencies: (number | BuiltInRules | (number | BuiltInRules)[])[]): DescriptionClause<T> {
const flatDependencies = dependencies.flat();
const tmp = (obj: T) => {
const dependenciesMet = flatDependencies.every(depId => this.ruleEngine.checkRuleById(depId, obj));
return dependenciesMet && this.check(obj);
};
return new DescriptionClause(this.ruleEngine, tmp, [...this.dependencies, ...flatDependencies]);
}
}
export class ValidationResult {
value: any;
ruleName: string;
ruleID: number;
error: string | null;
message: string;
result: ResultTypes | null;
public static FromRule(ruleName: string, ruleID: number, result: ResultTypes, message: string) {
const retval = new ValidationResult();
retval.ruleName = ruleName;
retval.ruleID = ruleID;
retval.result = result;
retval.message = message;
return retval;
}
public static FromSuccess(value: any, message: string): ValidationResult {
const retval = new ValidationResult();
retval.value = value;
retval.message = message;
retval.result = ResultTypes.Success;
return retval;
}
public static FromFailure(error: string): ValidationResult {
const retval = new ValidationResult();
retval.error = error;
retval.result = ResultTypes.Failure;
return retval;
}
public static PrintValidationResults(results: ValidationResult[], options: ValidationOptions) {
var errors = results.filter(x => x.result == ResultTypes.Failure);
if (errors.length > 0) {
LoggingUtils.log("Type Validation Errors:", ConsoleColor.Red);
errors.forEach((message) => LoggingUtils.log(` ${LoggingUtils.icon(false)} ${message.error}`));
console.log();
}
var successes = results.filter(x => x.result != ResultTypes.Failure);
const suffix = "is valid.";
successes.sort((a, b) => {
const aa = a.message.endsWith(suffix);
const bb = b.message.endsWith(suffix);
return Number(bb) - Number(aa);
});
if (!options.errorsOnly && successes.length > 0) {
LoggingUtils.log("Type Validation Messages:", ConsoleColor.Green);
successes.forEach((message) => LoggingUtils.log(` ${LoggingUtils.icon(true)} ${message.message}`));
console.log();
}
if (options.throwOnError && errors.length > 0)
throw new Error("Validation Errors");
}
public static HasError(results: ValidationResult[]): boolean {
return results.filter(x => x.result == ResultTypes.Failure).length > 0;
}
}

View file

@ -0,0 +1,44 @@
import { test as base } from "@playwright/test";
import type { Page, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo as PlaywrightTestInfo } from "@playwright/test";
import ITestCase from "@business-logic/types/ITestCase";
import { RuleEngine, ValidationOptions } from "@business-logic/types/RuleEngine";
import TestCase from "@business-logic/types/TestCase";
import Soft from "@business-logic/validations/Soft";
import FakerUtils from "@impl/utils/FakerUtils";
import LoggingUtils from "@impl/utils/LoggingUtils";
export type TestFunction = (args: PlaywrightTestArgs & PlaywrightTestOptions & PlaywrightWorkerArgs & PlaywrightWorkerOptions, testInfo: TestInfo) => void | Promise<void>;
export type TestRunnerFunction = (page: Page, testInfo: TestInfo, /*testCase: TestCase*/) => void | Promise<void>;
export interface TestInfo extends PlaywrightTestInfo {
testCase: TestCase;
}
export const test = base.extend<{ testInfo: TestInfo; }>({
// Do not use the fixture because it is not extended, and will cause a circular reference error
// Use ({}, use, testInfo) not ({ testInfo }, use)
testInfo: async ({}, use, testInfo) => {
await use(testInfo as TestInfo);
}
});
export function addSmokeTagToRandomTest(testCases: ITestCase[]) {
const index = Math.floor(Math.random() * testCases.length);
testCases.at(index)?.tags.push("@smoke");
}
export function prepareTest(testData: ITestCase, testRunner: TestRunnerFunction, validationOptions: ValidationOptions, ruleEngine: RuleEngine<TestCase>): [string, object, TestFunction] {
const name = testData.name;
const attributes = { tag: TestCase.getTags(testData) };
const testFunction: TestFunction = ({ page }, testInfo) => {
// Do not instantiate TestCase outside of this function,
// otherwise it will be instantiated several times for each test case
Soft.initialize(testInfo, page);
testInfo.testCase = new TestCase(testData, validationOptions, FakerUtils.getRandomTestID());
const results = ruleEngine.validate(testInfo.testCase);
RuleEngine.PrintValidationResults(results, validationOptions);
console.log(LoggingUtils.logValidate(`TestID: ${testInfo.testCase.testID}`, true));
return testRunner(page, testInfo, /*testInfo.testCase*/);
};
return [name, attributes, testFunction];
}

View file

@ -0,0 +1,227 @@
import PropertyUtils from "@impl/utils/PropertyUtils";
import { formatTag } from "@impl/utils/TaggingUtils";
import { Page } from "@playwright/test";
import { TestInfo } from "@business-logic/types/Test";
import { ConsoleColor } from "@business-logic/types/Enums";
import FrameworkConfig from "@business-logic/types/FrameworkConfig";
import { DisposableBase } from "@business-logic/types/IDisposable";
import ITestCase from "@business-logic/types/ITestCase";
import { ValidationOptions, ValidationResult } from "@business-logic/types/RuleEngine";
import Soft, { SoftError } from "@business-logic/validations/Soft";
import Validations from "./Validations";
import FakerUtils from "@impl/utils/FakerUtils";
import { ITestData } from "./ITestData";
import { BailoutPage } from "../../pages/BailoutPage";
import { ContactConfirmationPage } from "../../pages/ContactConfirmationPage";
import { CoverageStatementPage } from "../../pages/CoverageStatementPage";
import ITestPages from "./ITestPages";
import { DuplicateCheckPage } from "../../pages/DuplicateCheckPage";
import { OrderConfirmationPage } from "../../pages/OrderConfirmationPage";
import { PartQuestionsPage } from "../../pages/PartQuestionsPage";
import { PaymentMethodPage } from "../../pages/PaymentMethodPage";
import { PaymentPage } from "../../pages/PaymentPage";
import { PaypalPage } from "../../pages/PaypalPage";
import { PolicyHolderDetailsPage } from "../../pages/PolicyHolderDetailsPage";
import { PolicyVehiclesPage } from "../../pages/PolicyVehiclesPage";
import { ProviderPreferencePage } from "../../pages/ProviderPreferencePage";
import { SchedulePage } from "../../pages/SchedulePage";
import { ServiceLocationPage } from "../../pages/ServiceLocationPage";
import { ServicePackagesPage } from "../../pages/ServicePackagesPage";
import { TpaConfirmationPage } from "../../pages/TpaConfirmationPage";
import { TpaSearchPage } from "../../pages/TpaSearchPage";
import { TpaSubmitPage } from "../../pages/TpaSubmitPage";
import { VehicleDamagePage } from "../../pages/VehicleDamagePage";
import { VehicleLookupPage } from "../../pages/VehicleLookupPage";
import { VehicleSelectionPage } from "../../pages/VehicleSelectionPage";
import { VinLookupPage } from "../../pages/VinLookupPage";
import { WelcomePage } from "../../pages/WelcomePage";
import { ContactDetailsPage } from "../../pages/ContactDetailsPage";
import { EndorsementsPage } from "../../pages/EndorsementsPage";
import { VehicleLookupAddressPage } from "../../pages/VehicleLookupAddressPage";
import { VehicleLookupLicensePage } from "../../pages/VehicleLookupLicensePage";
import CcisApiUtil from "@impl/api/CcisApiUtil";
import MockPolicyData from "@business-logic/data/MockPolicyData";
import VehiclePartQuestionsPage from "../../pages/VehiclePartsPage";
import CapabilityQuestionsPage from "../../pages/CapabilityQuestionsPage";
export default class TestCase extends DisposableBase implements ITestCase {
public static FrameworkConfig: FrameworkConfig = {
// company: NetJetsCompanies.NJA,
// companyString: NetJetsCompanies[NetJetsCompanies.NJA],
// currency: CurrencyTypes.USD,
createResources: true, //process.env.FW_CREATE_RESOURCES! === "true",
destroyResources: true, // process.env.FW_DESTROY_RESOURCES! === "true",
maxAllotmentHours: Number(process.env.FW_MAX_ALLOTMENT_HOURS)
};
public static readonly Constants = class {
static readonly DISPOSE_HALTED: string = "FrameworkConfig is set to NOT destroy resources. Teardown halted!";
static readonly CREATION_HALTED: string = "FrameworkConfig is set to NOT create resources. Preparation halted!";
};
public readonly testID?: string;
public readonly name: string;
public readonly tags: string[];
public readonly testData: Partial<ITestData>;
public readonly validations?: Validations;
public readonly tempData?: any[] = [];
public location: Location;
public alternateLocation?: Location;
public pages: ITestPages;
public constructor(data: ITestCase, validationOptions: ValidationOptions = new ValidationOptions(), testID: string) {
super();
Object.assign(this, data);
this.testID = FakerUtils.getRandomTestID();
const validationResults: ValidationResult[] = [];
this.name = PropertyUtils.getValue<this>(data, TestCase.name, validationResults, { isRequired: true }, x => x.name);
this.tags = PropertyUtils.getValue<this>(data, TestCase.name, validationResults, { isRequired: true }, x => x.tags);
this.validations = PropertyUtils.getValue<this>(data, TestCase.name, validationResults, { isRequired: false }, x => x.validations);
// if (PropertyUtils.hasProperty<this>(data, TestCase.name, validationResults, { isRequired: false }, x => x.oldTransactions))
// this.oldTransactions = data.oldTransactions.map((item: any) => new TransactionData(item, validationResults, this.testID));
// if (PropertyUtils.hasProperty<this>(data, TestCase.name, validationResults, { isRequired: true }, x => x.transaction))
// this.transaction = new TransactionData(data.transaction, validationResults, this.testID);
ValidationResult.PrintValidationResults(validationResults, validationOptions);
}
public static getTags(testCase: ITestCase): string[] {
let retval = [
...testCase.tags,
formatTag(testCase.name),
// formatTag(testCase.transaction.programType)
];
// if (testCase.transaction.aircraft)
// retval.push(formatTag(testCase.transaction.aircraft.type));
// testCase.oldTransactions.forEach((t) => {
// retval.push(formatTag(t.programType));
// if (t.aircraft)
// retval.push(formatTag(t.aircraft.type));
// });
return retval;
}
public static async afterEachMethod(page: Page, testInfo: TestInfo) {
const originalStatus = testInfo.status;
if (Soft.hasFailedAssertions())
testInfo.status = "failed";
for (let a of Soft.getFailedAssertions())
testInfo.errors.push(new SoftError(a));
// NOTE: This try catch is here because the tests when locally, frequently
// fail tests on screenshot, which we do not want, is it make it
// harder to parse any other real errors we do care about. T.S. 9.5.2024
try {
await testInfo.attach("End of Test Screenshot", {
body: await page.screenshot({ fullPage: true }),
contentType: 'image/png'
});
} catch (error) {
console.error(error);
}
if (testInfo.testCase)
await testInfo.testCase.disposeAll();
const seconds: string = String(testInfo.duration / 1000);
const minutes: string = (testInfo.duration / 1000 / 60).toFixed(2);
const validationErrors: string = Soft.hasFailedAssertions() ? ` with ${Soft.getFailureCount()} validation errors` : "";
if (originalStatus == "failed")
console.log(`${ConsoleColor.Red}Test failed after ${seconds} seconds, or roughly ${minutes} minutes${validationErrors}.${ConsoleColor.Reset}`);
else {
if (testInfo.status == "passed")
console.log(`${ConsoleColor.Green}Test finished successfully in ${seconds} seconds, or roughly ${minutes} minutes.${ConsoleColor.Reset}`);
else
console.log(`${ConsoleColor.Orange}Test finished in ${seconds} seconds, or roughly ${minutes} minutes${validationErrors}.${ConsoleColor.Reset}`);
}
console.log(page.url());
}
public async setup(): Promise<void> {
if (this.testData.policySoap && this.testData.claimDetails) {
const apiUtil = new CcisApiUtil();
// Create Policy
const req = MockPolicyData.generateCreateMockPolicyRequest(this.testData.claimDetails.policyNumber, this.testData.policySoap);
console.log('Policy Number: ' + this.testData.claimDetails.policyNumber + ' Damage Date: ' + this.testData.claimDetails.damageDate + ' Postal code: ' + this.testData.customerDetails?.address.postalCode!);
const res = await apiUtil.createFakeResponse(req);
console.log("Fake Policy creation status" + res.status); // For debug use console.dir(res);
// Create Claim Registration
const claimRegReq = MockPolicyData.generateCreateClaimRegistrationRequest(this.testData.claimDetails.policyNumber);
const claimRegRes = await apiUtil.createFakeResponse(claimRegReq);
console.log("Fake claim registration creation status: " + claimRegRes.status); //For debug use console.dir(claimRegRes);
}
}
public setupPages(page: Page): void {
this.pages = {
bailoutPage: new BailoutPage(page),
capabilityQuestionsPage: new CapabilityQuestionsPage(page),
contactConfirmationPage: new ContactConfirmationPage(page),
contactDetailsPage: new ContactDetailsPage(page),
coverageStatementPage: new CoverageStatementPage(page),
duplicateCheckPage: new DuplicateCheckPage(page),
endorsementsPage: new EndorsementsPage(page),
orderConfirmationPage: new OrderConfirmationPage(page),
partQuestionsPage: new PartQuestionsPage(page),
paymentMethodPage: new PaymentMethodPage(page),
paymentPage: new PaymentPage(page),
paypalPage: new PaypalPage(page),
policyHolderDetailsPage: new PolicyHolderDetailsPage(page),
policyVehiclesPage: new PolicyVehiclesPage(page),
providerPreferencePage: new ProviderPreferencePage(page),
schedulePage: new SchedulePage(page),
serviceLocationPage: new ServiceLocationPage(page),
servicePackagesPage: new ServicePackagesPage(page),
tpaConfirmationPage: new TpaConfirmationPage(page),
tpaSearchPage: new TpaSearchPage(page),
tpaSubmitPage: new TpaSubmitPage(page),
vehicleDamagePage: new VehicleDamagePage(page),
vehicleLookupPage: new VehicleLookupPage(page),
vehiclePartQuestionsPage: new VehiclePartQuestionsPage(page),
vehicleSelectionPage: new VehicleSelectionPage(page),
vinLookupPage: new VinLookupPage(page),
vehicleLookupAddressPage: new VehicleLookupAddressPage(page),
vehicleLookupLicensePage: new VehicleLookupLicensePage(page),
welcomePage: new WelcomePage(page)
};
}
protected async dispose(): Promise<void> {
if (this.testData.policySoap && this.testData.claimDetails) {
const apiUtil = new CcisApiUtil();
// Delete policy
const policyDeleteRes = await apiUtil.deleteFakeResponse({
accountNumber: '550036', // TODO: Change when we have more clients
key: this.testData.claimDetails.policyNumber,
responseType: 'Policy'
});
console.log("Delete 'Fake policy' status: " + policyDeleteRes.status); //For debug use console.dir(policyDeleteRes);
// Delete Claim Registration
const crDeleteRes = await apiUtil.deleteFakeResponse({
accountNumber: '550036', // TODO: Change when we have more clients
key: this.testData.claimDetails.policyNumber,
responseType: 'ClaimRegistration'
});
console.log("Delete 'Fake Claim Registration policy' status: " + crDeleteRes.status); //For debug use console.dir(crDeleteRes);
}
}
}

View file

@ -0,0 +1,29 @@
import PropertyUtils from "@impl/utils/PropertyUtils";
import IValidations from "./IValidations";
import { ValidationResult } from "./RuleEngine";
export default class Validations implements IValidations {
public readonly minicartLineItems: boolean;
public readonly minicartTotals: boolean;
public readonly cartSummary: boolean;
public readonly agreementPricing: boolean;
public readonly dealDescription: boolean;
public readonly allotmentStatus: boolean;
public readonly chevronStatus: boolean;
public readonly agreementDates: boolean;
public readonly lineItems: boolean;
public readonly accountingTotal: boolean;
public constructor(json: any, validationResults: ValidationResult[]) {
this.minicartLineItems = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.minicartLineItems);
this.minicartTotals = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.minicartTotals);
this.cartSummary = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.cartSummary);
this.dealDescription = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.dealDescription);
this.agreementPricing = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.agreementPricing);
this.allotmentStatus = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.allotmentStatus);
this.chevronStatus = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.chevronStatus);
this.agreementDates = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.agreementDates);
this.lineItems = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.lineItems);
this.accountingTotal = PropertyUtils.getValue<this>(json, Validations.name, validationResults, { isRequired: true }, x => x.accountingTotal);
}
}

View file

@ -0,0 +1,179 @@
import LoggingUtils from '@impl/utils/LoggingUtils';
import { Page } from '@playwright/test';
import { TestInfo } from '@business-logic/types/Test';
import { TestInfoError } from "@playwright/test";
import { DateTime } from 'luxon';
import { expect as pw_expect } from '@playwright/test';
export default class Soft {
private static _instance: Soft | null = null;
private testInfo: TestInfo;
private page: Page;
private failedAssertions: string[] = [];
private errorCounter: number = 0;
private errorsOnly: boolean = false;
private constructor(testInfo: TestInfo, page: Page) {
this.testInfo = testInfo;
this.page = page;
}
public static initialize(testInfo: TestInfo, page: Page): void {
Soft._instance = new Soft(testInfo, page);
}
public static setOptions(options: { errorsOnly: boolean }): void {
Soft.getInstance().errorsOnly = options.errorsOnly;
}
public static getOptions(): { errorsOnly: boolean } {
return { errorsOnly: Soft.getInstance().errorsOnly };
}
private static getInstance(): Soft {
if (!Soft._instance)
throw new Error("Soft is not initialized. Call Soft.initialize(testInfo, page) first!");
return Soft._instance;
}
public async handleAssertion(
matcherFull: string,
matcherDisplay: string,
matcherFunction: () => Promise<void>,
reason?: string): Promise<void> {
reason = reason ? reason : ''
const reasonText = reason ? `'${reason}' ` : '';
try {
await matcherFunction();
if (!this.errorsOnly)
console.log(LoggingUtils.logValidate(`Validation ${reasonText}passed: ${matcherDisplay}!`, true));
} catch (error) {
console.log(LoggingUtils.logValidate(`Validation ${reasonText}failed: ${matcherDisplay}!`, false));
//const errorMessage = error instanceof Error ? error.message : String(error);
this.failedAssertions.push(`\n${++this.errorCounter}_Validation ${reasonText}failed:\n${LoggingUtils.replaceEmptyLinesWithMiddleDot(matcherFull)}!\n${error.stack}\n`);
// NOTE: I find it more helpful for the stack trace to be included here, so we know which line the validation is failing
try {
const screenshot: Buffer = await this.page.screenshot({ fullPage: true });
const name: string = LoggingUtils.sanitizeFileName(`${this.errorCounter}_Validation_${reason}${DateTime.now().valueOf()}`);
await this.testInfo.attach(name, {
body: screenshot,
contentType: 'image/png'
});
} catch(err){
// ohwell
console.error(error)
}
}
}
public static expect(value: any, reason?: string): ExpectationChain {
return new ExpectationChain(value, Soft.getInstance(), reason);
}
public static getFailedAssertions(): string[] {
return Soft.getInstance().failedAssertions;
}
public static hasFailedAssertions(): boolean {
return Soft.getInstance().failedAssertions.length > 0;
}
public static getFailureCount(): number {
return Soft.getInstance().failedAssertions.length;
}
public static clearFailedAssertions(): void {
Soft.getInstance().failedAssertions = [];
}
}
export class SoftError implements TestInfoError {
public readonly message?: string | undefined;
constructor(msg: string) {
this.message = msg;
}
}
export class ExpectationChain {
constructor(private value: any, private soft: Soft, private reason?: string) { }
private formatValue(value: any): string {
return LoggingUtils.truncateString(value);
}
public async toBe(expected: any): Promise<ExpectationChain> {
await this.soft.handleAssertion(
`expect(${this.value}).toBe(${expected})`,
`expect(${this.formatValue(this.value)}).toBe(${this.formatValue(expected)})`,
async () => await pw_expect(this.value).toBe(expected),
this.reason
);
return this;
}
public async toEqual(expected: any): Promise<ExpectationChain> {
await this.soft.handleAssertion(
`expect(${JSON.stringify(this.value)}).toEqual(${expected})`,
`expect(${this.formatValue(JSON.stringify(this.value))}).toEqual(${this.formatValue(expected)})`,
async () => await pw_expect(this.value).toEqual(expected),
this.reason
);
return this;
}
public async toContain(expected: any): Promise<ExpectationChain> {
await this.soft.handleAssertion(
`expect(${this.value}).toContain(${expected})`,
`expect(${this.formatValue(this.value)}).toContain(${this.formatValue(expected)})`,
async () => await pw_expect(this.value).toContain(expected),
this.reason
);
return this;
}
public async toHaveText(expected: string): Promise<ExpectationChain> {
await this.soft.handleAssertion(
`expect(${this.value}).toHaveText(${expected})`,
`expect(${this.formatValue(this.value)}).toHaveText(${this.formatValue(expected)})`,
async () => {
if (typeof this.value.textContent !== 'function') {
throw new Error('value does not have a textContent method');
}
const text = await this.value.textContent();
await pw_expect(text).toHaveText(expected);
}, this.reason
);
return this;
}
public async toBeGreaterThan(expected: number): Promise<ExpectationChain> {
await this.soft.handleAssertion(
`expect(${this.value}).toBeGreaterThan(${expected})`,
`expect(${this.formatValue(this.value)}).toBeGreaterThan(${this.formatValue(expected)})`,
async () => await pw_expect(this.value).toBeGreaterThan(expected),
this.reason
);
return this;
}
public async toBeTruthy(): Promise<ExpectationChain> {
await this.soft.handleAssertion(
`expect(${this.value}).toBeTruthy`,
`expect(${this.formatValue(this.value)}).toBeTruthy`,
async () => await pw_expect(this.value).toBeTruthy(),
this.reason
);
return this;
}
public async toBeFalsy(): Promise<ExpectationChain> {
await this.soft.handleAssertion(
`expect(${this.value}).toBeFalsy`,
`expect(${this.formatValue(this.value)}).toBeFalsy`,
async () => await pw_expect(this.value).toBeFalsy(),
this.reason
);
return this;
}
}

View file

@ -0,0 +1,4 @@
// import tsEslint from "typescript-eslint"
const tsEslint = require('typescript-eslint');
module.exports =
tsEslint.configs.strict

View file

@ -0,0 +1,21 @@
import axios, { type AxiosInstance } from 'axios';
import { buildQueryString, httpGet } from '@utils/HttpUtils'
import { IClientAuthentication, IClientSignatureRequest, IClientSignatureResponse } from '@business-logic/types/Authentication';
const adminServiceUrl = process.env.ADMIN_SERVICE_API_URL || '';
const axiosInstance: AxiosInstance = axios.create({
baseURL: adminServiceUrl,
headers: {'Content-Type': 'application/json'}
});
const INSURANCE_SERVICE_ROUTE = 'insurance';
export const getClientAuthByClientTag = async (clientTag: string): Promise<IClientAuthentication> => {
return await httpGet<IClientAuthentication>(axiosInstance, `${INSURANCE_SERVICE_ROUTE}/client-info?clientTag=${clientTag}`);
}
export const getClientSignature = async (request: IClientSignatureRequest): Promise<IClientSignatureResponse> => {
const params = buildQueryString(request);
return await httpGet<IClientSignatureResponse>(axiosInstance, `${INSURANCE_SERVICE_ROUTE}/signature?${params.toString()}`);
}

View file

@ -0,0 +1,47 @@
import { IPartsOrQuestionsResponse } from "@business-logic/types/DigitalApi";
import { ITestData } from "@business-logic/types/ITestData";
import { expect, Response } from "@playwright/test";
export default class ApiResponseInterceptUtil {
readonly testData: Partial<ITestData>;
constructor(testData: Partial<ITestData>) {
this.testData = testData;
// Bind callback functions to the class instance so 'this' is usable within a callback
this.handleInterceptResponse = this.handleInterceptResponse.bind(this);
this.handlePartsOrQuestionsResponse = this.handlePartsOrQuestionsResponse.bind(this);
}
async handleInterceptResponse(response: Response) {
if (!response.url().includes('safelite.io')) {
return;
}
const urlPath = response.url().split('safelite.io')[1]; // get api path
switch(urlPath) {
case '/parts/api/v1/parts/parts-or-questions':
await this.handlePartsOrQuestionsResponse(response);
break;
default:
break;
}
if (response.url().endsWith('/parts/parts-or-questions') && response.status() === 200) {
}
}
async handlePartsOrQuestionsResponse(response: Response) {
if (this.testData.hasOemEndorsement) {
const partsRes = (await response.json()) as IPartsOrQuestionsResponse;
for ( const partOrQuestion of partsRes.partsOrQuestions) {
for (const part of partOrQuestion.parts) {
expect.soft(part.partNumber.endsWith('OEM'),
`handlePartsOrQuestionsResponse>> OEM endorsement was expected, but "${part.partType}" with part number "${part.partNumber}" is not OEM.`
).toEqual(true);
}
}
}
}
}

View file

@ -0,0 +1,28 @@
import { IDeleteFakeResponseParams, IPostSaveFakeResponseRequestBody } from "@business-logic/types/CcisApi";
import axios from "axios";
const authToken = process.env.CCIS_API_AUTH || '';
const ccisUrl = process.env.CCIS_API_URL || '';
export default class CcisApiUtil {
readonly baseUrl = ccisUrl;
readonly postCreateMockPolicyUrl = `${this.baseUrl}/ccis/api/v1/admin/fakeResponses/save`
readonly deleteFakeResponseUrl = `${this.baseUrl}/ccis/api/v1/admin/fakeResponses`
createFakeResponse(requestBody: IPostSaveFakeResponseRequestBody) {
return axios.post(this.postCreateMockPolicyUrl, requestBody, {
headers: {
'X-Mule-Origin-Verify': authToken
}
});
}
deleteFakeResponse(params: IDeleteFakeResponseParams) {
const deleteUrl = `${this.deleteFakeResponseUrl}/${params.accountNumber}/${params.key}/${params.responseType}`;
return axios.delete(deleteUrl, {
headers: {
'X-Mule-Origin-Verify': authToken
}
});
}
}

View file

@ -0,0 +1,26 @@
export function formatDate(date: Date) {
const isoString = date.toISOString();
return isoString.slice(0, 10);
}
export function formatTime(date: Date) {
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
export function getNextWeekday(date?: Date) {
if (!date) {
date = new Date();
date.setHours(8,0,0,0);
}
const dayOfWeek = date.getDay();
const daysToAdd = dayOfWeek === 5? 3: 1; // Add 3 days if today is friday. Otherwise add 1.
const nextDay = new Date(date);
nextDay.setDate(date.getDate() + daysToAdd);
return nextDay;
}

View file

@ -0,0 +1,76 @@
function isFlags(enumObj: object): boolean {
const values = Object.values(enumObj).filter(v => typeof v === "number");
return values.some(v => v !== 0 && (v & (v - 1)) === 0);
}
function isValidEnumValue(value: any, enumType: object): boolean {
if (isFlags(enumType)) {
const allFlags = Object.values(enumType).reduce((acc, val) => typeof val === "number" ? acc | val : acc, 0);
return typeof value === "number" && (value & allFlags) === value;
} else {
return Object.values(enumType).includes(value);
}
}
function getEnumValues(enumObj: object): string[] | number[] {
if (!isFlags(enumObj))
return Object.values(enumObj);
return Object.values(enumObj).filter(value => typeof value === "number") as number[];
}
function getEnumString<T extends { [key: string]: string | number }>(enumObj: T, flags: number): string {
if (flags === 0)
return Object.keys(enumObj).find(key => enumObj[key] === 0) || 'None';
const attributes = Object.entries(enumObj).filter(([key, value]) =>
typeof value === 'number' && value !== 0 && (flags & value) === value)
.map(([key]) => key);
return attributes.length === 1 ? attributes[0] : attributes.join(', ');
}
function validateEnumProperty(json: any, property: string, enumType: object): number | string | null {
if (json.hasOwnProperty(property) && isValidEnumValue(json[property], enumType))
return json[property];
return null;
}
function hasFlag(value: number | string, flag: number | string, enumType: object): boolean {
if (isFlags(enumType))
return typeof value === "number" && typeof flag === "number" && (value & flag) === flag;
else
return value === flag;
}
function addFlag(value: number, flag: number, enumType: object): number {
if (isFlags(enumType))
return value | flag;
else
throw new Error("Attempted to add flag on non-flags enum");
}
function removeFlag(value: number, flag: number, enumType: object): number {
if (isFlags(enumType))
return value & ~flag;
else
throw new Error("Attempted to remove flag on non-flags enum");
}
function toggleFlag(value: number, flag: number, enumType: object): number {
if (isFlags(enumType))
return value ^ flag;
else
throw new Error("Attempted to toggle flag on non-flags enum");
}
const EnumUtils = {
hasFlag,
addFlag,
removeFlag,
toggleFlag,
validateEnumProperty,
getEnumValues,
getEnumString
};
export default EnumUtils;

View file

@ -0,0 +1,56 @@
export default class FakerUtils {
private static NUMBERS = '0123456789';
private static UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
private static LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
private static ALPHABET = FakerUtils.UPPERCASE + FakerUtils.LOWERCASE;
private static ALPHANUMERIC = FakerUtils.NUMBERS + FakerUtils.ALPHABET;
private static generateRandomString(length: number, characters: string): string {
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
.map(byte => characters[byte % characters.length])
.join('');
}
public static generateRandomNumber(min: number, max: number): number {
const range = max - min + 1;
const bytesNeeded = Math.ceil(Math.log2(range) / 8);
const randomBytes = new Uint8Array(bytesNeeded);
crypto.getRandomValues(randomBytes);
const randomValue = randomBytes.reduce((acc, byte) => (acc << 8) + byte, 0);
return min + (randomValue % range);
}
public static getRandomTestID(): string {
return FakerUtils.generateRandomString(8, FakerUtils.ALPHANUMERIC);
}
public static getRandomProperty(obj: Record<string, any>): string {
const keys = Object.keys(obj);
const randomIndex = FakerUtils.generateRandomNumber(0, keys.length - 1);
return keys[randomIndex];
}
public static getRandomTail(registrationPrefix: string = "XX", testID: string = ""): string {
return FakerUtils.formatString(FakerUtils.generateRandomString(8, FakerUtils.ALPHANUMERIC));
}
public static getRandomEmail(domainSuffix: string = "@test.com", testID: string = ""): string {
const retval = FakerUtils.formatString("{0}{1}", FakerUtils.generateRandomString(8, FakerUtils.ALPHANUMERIC), domainSuffix);
return retval;
}
public static getRandomLastName(testID: string = " - "): string {
return FakerUtils.formatString(" - {0}", FakerUtils.generateRandomString(21, FakerUtils.ALPHABET));
}
public static getObjectName(prefix: string, testID: string = ""): string {
return FakerUtils.formatString("{0} - {1}", prefix, FakerUtils.generateRandomString(21, FakerUtils.ALPHANUMERIC));
}
private static formatString(template: string, ...args: (string | (() => string))[]): string {
return template.replace(/\{(\d+)\}/g, (match, index) => {
const arg = args[parseInt(index)];
return typeof arg === 'function' ? arg() : arg || '';
});
}
}

View file

@ -0,0 +1,22 @@
import fs from 'fs';
import path from 'path';
export function writeFileToLocalCache(fileNameWithExtension: string, fileContents: string) {
const folderPath = path.join(process.cwd(), '.debug', '.cache');
const filePath = path.join(folderPath, fileNameWithExtension);
try {
// Create the folder if it doesn't exist
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}
// Write the data to the file
fs.writeFileSync(filePath, fileContents);
console.log(`File "${filePath}" created successfully.`);
} catch (error) {
console.error('Error creating the file:', error);
}
}

View file

@ -0,0 +1,92 @@
import { Page } from '@playwright/test';
import { type AxiosInstance, type AxiosResponse } from 'axios';
import * as fs from 'fs';
import * as path from 'path';
export async function httpGet<T>(client: AxiosInstance, url: string): Promise<T> {
const [isSuccess, response] = await handleHttp(client.get<T>(url));
if(isSuccess) {
return response;
}
console.error(`An error occurred calling GET ${url}\nError:${response}`);
throw response;
}
export async function httpPost<T, D>(client: AxiosInstance, url: string, data: D): Promise<T> {
const [isSuccess, response] = await handleHttp(client.post<T>(url, data));
if(isSuccess) {
return response;
}
console.error(`An error occurred calling POST ${url}\nError:${response}`);
throw response;
}
export function handleHttp<T>(request: Promise<AxiosResponse<T>>): Promise<[isSuccess: true, data: T] | [isSuccess: false, error: Error]> {
return request.then(data => {
return [true, data.data] as [true, T]
}).catch((error: Error) => {
return [false, error] as [false, Error]
})
}
export function buildQueryString<T>(data: T): URLSearchParams {
const params: Record<string, string> = {};
for (const key in data) {
const value = data[key];
params[key] = `${value}`;
}
return new URLSearchParams(params);
}
export function forceAPIError(page: Page, endpoint: string) {
page.route('**/*', (route) => {
return route.request().url().includes(endpoint)
? route.abort()
: route.continue()
});
}
// Utility method to return mock response based on endpoint and scenario
export function getMockedApiResponse(endpoint: string, scenario: string): object | null {
const mockResponsesDir = path.resolve(__dirname, '../../tests/mockResponses');
const configFilePath = path.join(mockResponsesDir, 'mockResponsesConfig.json');
if (fs.existsSync(configFilePath)) {
const config = JSON.parse(fs.readFileSync(configFilePath, 'utf-8'));
const scenarioConfig = config[scenario];
const commonConfig = config['common'];
let mockFilePath = scenarioConfig ? scenarioConfig[endpoint] : null;
if (!mockFilePath && commonConfig) {
mockFilePath = commonConfig[endpoint];
}
if (mockFilePath) {
const filePath = path.join(mockResponsesDir, mockFilePath);
if (fs.existsSync(filePath)) {
const mockResponse = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(mockResponse);
}
}
}
return null;
}
// Utility method for mocking API responses
export function mockApiResponse(page: Page, endpoint: string, scenario: string, mockTestingFlag: boolean) {
const mockResponse = getMockedApiResponse(endpoint, scenario);
page.route(`**/${endpoint}`, route => {
if (mockResponse && mockTestingFlag) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockResponse)
});
} else {
route.continue();
}
});
}

View file

@ -0,0 +1,128 @@
import { ConsoleColor, ResultTypes } from "@business-logic/types/Enums";
export default class LoggingUtils {
public static CONSOLE_WIDTH: number = 100;
//public static ICON_OK: string = "✅";
public static ICON_OK: string = "\u2705";
//public static ICON_SKIP: string = "⏩";
public static ICON_SKIP: string = "\u23ED";
//public static ICON_WARNING: string = "⚠️";
public static ICON_WARNING: string = "\u26A0\uFE0F";
//public static ICON_FAIL: string = "❗";
public static ICON_FAIL: string = "\u2757";
public static log(message: string | null, color: ConsoleColor = ConsoleColor.Default): void {
if (message == null)
return;
console.log(`${color}%s${ConsoleColor.Reset}`, message);
}
public static icon(value: boolean): string;
public static icon(value: ResultTypes): string;
public static icon(value: any): string {
if (typeof value === "boolean")
return value ? this.ICON_OK : this.ICON_FAIL;
switch (value) {
case ResultTypes.Failure:
return this.ICON_FAIL;
case ResultTypes.Success:
return this.ICON_OK;
case ResultTypes.Skipped:
return this.ICON_SKIP;
}
return "";
}
public static logFunc(name: string, value: string | null = null, result: boolean | null = null): string {
var icon = this.ICON_SKIP;
if (result != null)
icon = result ? this.ICON_OK : this.ICON_FAIL;
if (value != null)
return `${this.getShortDateTime()} [${this.centerPadString(`${name}: ${this.truncateString(value)}`)}] -> ${icon}`;
return `${this.getShortDateTime()} [${this.centerPadString(name)}] -> ${icon}`;
}
public static logValidate(text: string, success: boolean) {
return `${this.getShortDateTime()} [${this.centerPadString(text)}] -> ${success ? this.ICON_OK : this.ICON_FAIL}`;
}
public static truncateString(value: any, maxLength: number = this.CONSOLE_WIDTH): string {
let str = typeof value === 'string' ? value : String(value);
str = str.replace(/\s+/g, ' ').trim();
if (str.length <= maxLength) {
return str;
}
return str.slice(0, maxLength - 2) + '..';
}
public static sanitizeFileName(input: string): string {
// Remove characters that are invalid in both Windows and Linux file systems
let sanitized = input.replace(/[<>:"/\\|?*\x00-\x1F]/g, '');
// Remove leading and trailing spaces and dots
sanitized = sanitized.trim().replace(/^\.+|\.+$/g, '');
// Replace remaining dots and spaces with underscores
sanitized = sanitized.replace(/[\s.]+/g, '_');
// Ensure the name isn't empty after sanitization
if (sanitized.length === 0) {
sanitized = 'unnamed';
}
// Truncate to a reasonable maximum length (e.g., 255 characters)
sanitized = sanitized.slice(0, 255);
return sanitized;
}
public static replaceEmptyLinesWithMiddleDot(input: string): string {
const emptyLinesRegex: RegExp = /(.+?)(\n\s*\n)+/g;
return input.replace(emptyLinesRegex, (_match, line) => {
return line + '·\n';
}).replace(/\n$/, '');
}
private static centerPadString(str: string, length: number = this.CONSOLE_WIDTH): string {
if (str.length >= length) {
return this.truncateString(str);
}
str = this.truncateString(str);
const totalPadding = length - str.length;
const leftPadding = Math.ceil(totalPadding / 2);
const rightPadding = Math.floor(totalPadding / 2);
return ' '.repeat(leftPadding) + str + ' '.repeat(rightPadding);
}
private static getShortDateTime() {
return new Date().toLocaleString('en-US', {
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
public static normalizeSalesForceType(input: string, prefix: string = "Apttus_Config2__", suffix: string = "__c") {
let result = input;
if (result.startsWith(prefix))
result = result.slice(prefix.length);
if (result.endsWith(suffix))
result = result.slice(0, -suffix.length);
return result;
}
}

View file

@ -0,0 +1,34 @@
/**
* Parses a string containing a representation of currency, and returns a typed number. Can handle
* different types of currency represenations.
*
* USD 12,345.65 -> 12345.65
* (USD 12345.00) -> -12345
*
* @param text String containing currency representation
* @param currencyCode Optionally define different currency code
* @returns Parsed currency with type number
*/
export function parseCurrency(text: string, currencyCode: string = "USD"): number {
// TODO: Add ability to handle null fields/not treat null as 0 - KK 9/12/24
// Base case, if text can be cast as a number then work is done
if (!isNaN(+text)) return +text;
// Remove parentheses and continue parsing, multiply return by -1 to preserve negative value
if (text.charAt(0) === '(') return -1 * parseCurrency(text.substring(1, text.length - 1));
// Remove currency code prefix and continue parsing
if (text.split(' ')[0] === currencyCode) return parseCurrency(text.split(' ')[1]);
// Remove commas and cast to number
return Number(text.split(',').join(''));
}
/**
* @param text
* @returns
*/
export function parseNumberOrCurrency(text: string): Number | string {
if (!isNaN(+text)) return +text;
else if (text.charAt(0) === '(') return -1 * parseCurrency(text.substring(1, text.length - 1));
else if (text.split(' ')[0] === "USD") return parseCurrency(text);
else return text;
}

View file

@ -0,0 +1,111 @@
import { ValidationResult } from "@business-logic/types/RuleEngine";
import EnumUtils from "./EnumUtils";
export type ExtractName<T> = { [K in keyof T]: () => K; };
type GetValueOptions = {
isRequired: boolean;
};
function isNullOrWhiteSpace(input: unknown): boolean {
if (typeof input !== "string")
return input == null;
return input.trim().length === 0;
}
// Matches: '() => _Enums.*****', e.g.: '() => _Enums.ModificationTypes'
// Matches: '() => *****', e.g.: '() => ModificationTypes'
// Returns: EnumName, e.g.: 'ModificationTypes'
function getEnumName(propertySelector: () => object): string {
const functionString = propertySelector.toString();
const match = functionString.match(/\(\s*\)\s*=>\s*(?:_?[A-Z]\w*\.)?(\w+)(?::)?/);
return match ? match[1] : "Unknown";
}
// Matches: 'x => x.*****', e.g.: 'x => x.name', 'x => x.tags'
// Returns: PropertyName, e.g.: 'name', 'tags'
function getNameof<T>(propertySelector: (obj: T) => any): string {
const propertyString = propertySelector.toString();
const match = propertyString.match(/(?:=>|return)\s*([\w\s.]+)/);
if (match && match[1]) {
const parts = match[1].split('.');
return parts[parts.length - 1].trim();
}
throw new Error(`Invalid property selector: ${propertyString}`);
}
function getValueOrNull<T>(json: any, propertySelector: (obj: T) => any): any | null {
if (json == null)
return null;
const property = getNameof(propertySelector);
if (json.hasOwnProperty(property))
return json[property];
return null;
}
function getValue<T>(json: any, typeName: string, validationResults: ValidationResult[], options: GetValueOptions, propertySelector: (obj: T) => any): any | null {
const retval = getValueOrNull(json, propertySelector);
if (retval == null)
validationResults.push(options.isRequired ? ValidationResult.FromFailure(`Value for ${typeName}.${getNameof(propertySelector)} is null but is required!`) : ValidationResult.FromSuccess(retval, `Value for ${typeName}.${getNameof(propertySelector)} is null but is NOT required.`));
else {
if (isNullOrWhiteSpace(retval))
validationResults.push(ValidationResult.FromFailure(`Value for ${typeName}.${getNameof(propertySelector)} is empty!`));
else
validationResults.push(ValidationResult.FromSuccess(retval, `Value for ${typeName}.${getNameof(propertySelector)} is valid.`));
}
return retval;
}
function hasProperty<T>(json: any, typeName: string, validationResults: ValidationResult[], options: GetValueOptions, propertySelector: (obj: ExtractName<T>) => () => keyof T): boolean {
const retval = json.hasOwnProperty(getNameof(propertySelector));
if (retval)
validationResults.push(ValidationResult.FromSuccess(retval, `Value for ${typeName}.${getNameof(propertySelector)} is valid.`));
else
validationResults.push(options.isRequired ? ValidationResult.FromSuccess(retval, `Value for ${typeName}.${getNameof(propertySelector)} is not defined but is required!`) : ValidationResult.FromFailure(`Value for ${typeName}.${getNameof(propertySelector)} is not defined but is NOT required!`));
return retval;
}
function getEnumValueOrNull<T>(json: any, enumType: object, propertySelector: (obj: T) => any): any | null {
if (json == null)
return null;
const property = getNameof(propertySelector);
const retval = EnumUtils.validateEnumProperty(json, property, enumType);
if (retval != null)
return retval;
else if (Object.values(enumType).includes(json[property]))
return json[property];
return null;
}
function getEnumValue<T>(json: any, typeName: string, validationResults: ValidationResult[], enumType: object, enumTypeInstance: () => object, options: GetValueOptions, propertySelector: (obj: T) => any): any | null {
const retval = getEnumValueOrNull(json, enumType, propertySelector);
if (retval == null)
validationResults.push(options.isRequired ? ValidationResult.FromFailure(`Value for ${typeName}.${getNameof(propertySelector)} [${getEnumName(enumTypeInstance)}] is null but is required!`) : ValidationResult.FromSuccess(retval, `Value for ${typeName}.${getNameof(propertySelector)} [${getEnumName(enumTypeInstance)}] is null but is NOT required.`));
else
validationResults.push(ValidationResult.FromSuccess(retval, `Value for ${typeName}.${getNameof(propertySelector)} is valid.`));
return retval;
}
export { getEnumName, getEnumValueOrNull, getNameof, getValueOrNull };
const PropertyUtils = {
hasProperty,
getValue,
getEnumValue
};
export default PropertyUtils;

View file

@ -0,0 +1,32 @@
export function formatTag(text: string): string {
return "@" + text.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, (match, index) => {
if (+match === 0) return ""; // Remove non-alphanumeric characters
return index === 0 ? match.toLowerCase() : match.toUpperCase();
});
}
/**
* @description - Use this to add calculated/standardized/randomized tags to scenarioData prior to running. Randomly adds a smoke tag to 1 of the testCases.
* @param scenarioData - the data from your test case
* @returns scenarioData, but with added tags.
*/
export function getRandomTestName(scenarioData: any) : string {
const testCaseNames: string[] = Object.keys(scenarioData);
const totalKeys = testCaseNames.length
const randomIndex = Math.floor(Math.random() * totalKeys) - 1;
const randomKey = testCaseNames[randomIndex];
return randomKey;
}
export function metaTags(currentTestName: string, randomTestName: string) : string [] {
return currentTestName == randomTestName ? ["@smoke", "@standardRegression"] : ["@standardRegression"]
}
export function matchAndReplaceContactDataTag(data: string[], replacement: string): string[] {
return data.map((value) => value.replace(/[{]{2}contact[}]{2}/, replacement));
}
export function matchAndReplaceAccountDataTag(data: string[], replacement: string): string[] {
return data.map((value) => value.replace(/[{]{2}account[}]{2}/, replacement));
}

View file

@ -0,0 +1,12 @@
import { error } from "console";
export function throwIf(conditionFunction: () => boolean, errorMessage: string): void {
if (conditionFunction())
throw new Error(errorMessage);
}
export function throwNotYetImplemented(nameOfThingNotImplemented: string) {
throw new Error(`${nameOfThingNotImplemented} has not yet been implemented.`)
}

View file

@ -0,0 +1,273 @@
import { expect, Locator, Page } from "@playwright/test";
import LoggingUtils from "./LoggingUtils";
import { DateTime } from "luxon";
export type TimeoutOpts = {
/**
* @description timeout Commonly referred to for an entire method; exists to allow developers to specify their own timeout whout overriding defaults
*/
timeout: number
/**
* @description timeout_tiny use for exceedingly small waits, as in, waiting for label to contain the test you just typed into it.
*/
timeoutTiny: number
/**
* @description timeout_short for relatively quick opperations, such as waiting for a dropdown to render
*/
timeoutShort: number
/**
* @description timeout_medium for moderately slow operations, such as a new modal rendering, or a calculated field value being updated, or an API Call
*/
timeoutMedium: number
/**
* @description timeout_long for high-risk, slow operations. Waiting for the minicart to load, waiting for login, or waiting for screen-to-screen navigation.
*/
timeoutLong: number
/**
* @description for when things are really, really bad.
*/
timeoutConga: number
}
export const timeoutOptDefaults: TimeoutOpts = {
timeout: 60_000,
timeoutTiny: +(process.env.TIMEOUT_TINY ?? 500),
timeoutShort: +(process.env.TIMEOUT_SHORT ?? 5000),
timeoutMedium: +(process.env.TIMEOUT_MEDIUM ?? 30_000),
timeoutLong: +(process.env.TIMEOUT_LONG ?? 180_000),
timeoutConga: +(process.env.TIMEOUT_CONGA ?? 500_000),
}
export type WaitUntilOpts = {
delayBetweenChecks: number,
continueOnTimeoutError: boolean,
anticipatedConditionResult: boolean,
conditionName: string,
beginWaitingMessage: string,
delayBetweenChecksMessage: string,
timeoutErrorMessage: string,
successMessage: string,
ignoreErrorsFromConditionFunction: boolean,
}
export const waitUntilOptDefaults: WaitUntilOpts = {
delayBetweenChecks: 3000,
continueOnTimeoutError: false,
anticipatedConditionResult: true,
conditionName: "",
beginWaitingMessage: "",
delayBetweenChecksMessage: "",
timeoutErrorMessage: "Timed Out",
successMessage: "",
ignoreErrorsFromConditionFunction: true,
}
/**
* @description repeatedly execute an asynchronous conditional lambda until a given outcome occurs, or the method times-out. Useful for hedgning against GUI race conditions.
* @param conditionFunction the condition lambda. Example: ()=>{await return myPage.someButton.isVisible()}
* @param options standard Timeout and WaitUntil Options.
* @returns true or false - the outcome of the waituntil.
*/
export async function waitUntil(conditionFunction: (...args: any[]) => Promise<boolean>, options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<boolean> {
const opts = { ...waitUntilOptDefaults, ...timeoutOptDefaults, ...options }
let timeoutAt = Date.now() + opts.timeout;
let waitUntilHasTimedOut = false;
let conditionHasBeenMet = false;
do {
try {
const conditionResult = await conditionFunction()
conditionHasBeenMet = conditionResult == opts.anticipatedConditionResult
}
catch (e) {
if (!opts.ignoreErrorsFromConditionFunction) {
throw e
}
}
waitUntilHasTimedOut = Date.now() > timeoutAt
if (!waitUntilHasTimedOut && !conditionHasBeenMet) {
await delay(opts.delayBetweenChecks)
}
else if (waitUntilHasTimedOut && !opts.continueOnTimeoutError) {
throw new Error(opts.timeoutErrorMessage)
}
}
while (!conditionHasBeenMet || waitUntilHasTimedOut)
return conditionHasBeenMet;
}
/**
* @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order.
* @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sqeuance until all have passed.
* @param options standard Timeout and WaitUntil options
*/
export async function waitUntilValueStopsChanging(mercurialValueFunction: (...args: any[]) => Promise<any>, options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
const opts = { ...waitUntilOptDefaults, ...timeoutOptDefaults, ...options }
let lastFoundValue: any = undefined;
const valueHasStoppedChanging = async () => {
const newFoundValue = await mercurialValueFunction();
if (opts.delayBetweenChecksMessage.length > 0) console.log(`${opts.delayBetweenChecksMessage} - Last Value: ${lastFoundValue}`)
const valueIsStable = (newFoundValue != undefined) && (newFoundValue == lastFoundValue);
lastFoundValue = newFoundValue;
return valueIsStable;
}
await waitUntil(valueHasStoppedChanging, opts)
return lastFoundValue;
}
/**
* @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23
* @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order.
* @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sqeuance until all have passed.
* @param options standard Timeout and WaitUntil options
*/
export async function waitUntilEach(sequentialConditionFunctions: ((...args: any[]) => Promise<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
for (const conditionFunction of sequentialConditionFunctions) {
await waitUntil(conditionFunction, options);
}
}
/**
* @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23
* @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order.
* @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sequence until all have passed.
* @param options standard Timeout and WaitUntil options
*/
export async function waitUntilAll(sequentialConditionFunctions: ((...args: any[]) => Promise<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
for (const conditionFunction of sequentialConditionFunctions) {
await waitUntil(conditionFunction, options);
}
}
/**
* @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23
* @description Order DOES NOT Matter; WaitUntil ANY condition passes before completing. Use when multiple conditions can give confidence that sufficient waiting has occured.
* @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in psudo-parallel until at least one has passed.
* @param options standard Timeout and WaitUntil options
*/
export async function waitUntilAny(multipleRequiredConditionFunctions: ((...args: any[]) => Promise<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
const anyConditionMet = async (): Promise<boolean> => {
return multipleRequiredConditionFunctions.filter(async (fun: (...args: any[]) => Promise<boolean>): Promise<boolean> => await Function.call(fun)).length > 0;
};
waitUntil(anyConditionMet, options)
}
/**
* @param milliseconds delay duration
* @param logDelay defaults to false; if true, logs a waiting message.
*/
export async function delay(milliseconds: number, logDelay = false): Promise<void> {
if (logDelay) { console.log(`delaying ${milliseconds} milliseconds before continuing...`) }
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
/**
* Waits for the page url
* @param partialUrl The string we are looking for in the url, to know we have transitioned to the correct page.
* @param timeout the amount of milliseconds to wait before giving up.
*/
export async function waitForUrlPartialMatch(page: Page, firstPartialUrl: string, timeout = 120_000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (page.url().includes(firstPartialUrl)) {
return; // URL matches the partial string, exit the function
}
await page.waitForTimeout(100); // Wait for 100 milliseconds before checking again
}
throw new Error(`Timed out waiting for URL to match '${firstPartialUrl}'`);
}
/**
* Run a function repeatedly until it returns true or the timeout is reached.
*
* This function executes the provided asynchronous block function in a loop until it returns true or the specified
* timeout duration has elapsed. Between each attempt, it waits for a specified delay.
*
* @param block - An asynchronous function that returns a boolean value. This function will be executed repeatedly until it returns true.
* @param timeout - The maximum duration to keep attempting to run the block function, in milliseconds. Default is 150000 (150 seconds).
* @param delayMs - The delay duration between each attempt, in milliseconds. Default is 3000 (3 seconds).
* @returns A promise that resolves to a boolean value indicating whether the block function eventually returned true.
*
* @example
* // Example usage:
* const blockFunction = async () => {
* // Some asynchronous condition check
* return await someConditionCheck();
* };
* const result = await runUntilTrue(blockFunction, 10000, 1000);
* console.log(result); // Outputs true if blockFunction returned true within the timeout, otherwise false.
*/
export async function runUntilTrue(block: () => Promise<boolean>, timeout: number = 150000, delayMs: number = 3000){
const startTime = DateTime.now();
let attempts = 0;
let evaluatesToTrue = false;
do {
// If timeout duration has elapsed, stop making attempts
if (DateTime.now().diff(startTime).as('milliseconds') > timeout) {
break;
}
attempts++;
// If retrying, wait delay duration
if (attempts > 1) await delay(delayMs);
evaluatesToTrue = await block();
} while (!evaluatesToTrue);
return evaluatesToTrue;
}
// TODO move this to impl/utils/WaitingUtils when that related pr is available in devleop branch - T.S. 5/20/24
export async function waitForEither(block1: () => Promise<any>, block2: () => Promise<any>, timeOut: number = 180_000): Promise<void> {
const startTime = Date.now();
while (true) {
try {
const result1 = await block1();
const result2 = await block2();
// Check if either result is truthy (i.e., not falsy or undefined)
if (result1 || result2) {
// At least one block returned a truthy value, resolve the promise
return;
}
} catch (error) {
// Handle errors thrown by either block
console.error("An error occurred:", error);
}
// Check if the timeout has been reached
if (Date.now() - startTime >= timeOut) {
throw new Error(`Timeout of ${timeOut} ms exceeded`);
}
// Add some delay before checking again
await new Promise(resolve => setTimeout(resolve, 1000)); // Adjust delay as needed
}
}
/**
* Waits for a specific locator to show up on screen, then disappear. Typically used for things like progress bars.
* @param locator The locator we want to become visible and then become hidden
*/
export async function waitToAppearAndDisappear(locator: Locator): Promise<void> {
try {
await expect(locator).toBeVisible({ timeout: 60000 });
await expect(locator).toBeHidden({ timeout: 60000 });
} catch (err) {
if (err instanceof Error)
console.log(LoggingUtils.logFunc(waitToAppearAndDisappear.name, err.message, false));
else
console.log(LoggingUtils.logFunc(waitToAppearAndDisappear.name, null, false));
}
}

View file

@ -0,0 +1,34 @@
import { IClientAuthentication } from "@business-logic/types/Authentication";
import { Authentication } from "@business-logic/types/Enums";
export function buildToken(clientAuth: IClientAuthentication, formData: Map<string, string>): string {
if(clientAuth.authentication === undefined
|| clientAuth.authentication === Authentication.None
|| clientAuth.authentication === Authentication.Unknown
) {
return '';
}
let token = (formData.get('Token') ?? formData.get("Timestamp") ?? '');
if(clientAuth.authentication === Authentication.RSAToken) {
return token;
}
for(const [id, value] of formData.entries()) {
token += `|${id.toLowerCase()}=${value}`
}
return token;
}
export function getTimestamp(): string {
const time = new Date(Date.now());
const formatOptions :Intl.DateTimeFormatOptions = {
hour12: false,
dateStyle: "short",
timeStyle: "medium"
}
const string = time.toLocaleString('en-US', formatOptions);
let timestamp = string.replaceAll("/","").replaceAll(", ","").replaceAll(":","");
return timestamp;
}

View file

@ -0,0 +1,132 @@
import LoggingUtils from "./LoggingUtils";
import { delay, timeoutOptDefaults } from "./TimingUtils";
/**
* @description For situations where a Test Framework Exception (i.e., a locator timeout) could be incorrectly thrown based on the state of the Target-Application (i.e., a missing tail number). Use this to throw clearer 'Application Exception' errors under such circumstances.
* @param actionVerb What are you trying to do? Could be 'gotoCatalog', 'addAnEnhancement', etc. Logged as `Attempting to ${actionVerb}`
* @param failureExplenation Be descriptive. What is the context of the failure? If someone unfamiliar with the code base were to see this, how would they know if the error was caused by their code, or by an underlying problem with the Target Application?
* @param actionToAttempt a lambda for the flaky action.
* @returns
*/
export async function tryBusinessAction(actionVerb: string, failureExplenation: string, actionToAttempt: (...args: any[]) => Promise<any>): Promise<any> {
let actionResult: any;
console.log(`Attempting to ${actionVerb}...`)
try {
actionResult = await actionToAttempt();
} catch (e) {
if (e instanceof Error) {
e.message = e.message + ">>Failure Explenation>> " + failureExplenation;
throw (e);
} else {
throw (new Error("Unknown 'AttemptBusinessAction' State..."))
}
}
console.log(`Successfully executed ${actionVerb}`)
return actionResult;
}
/**
* @description Brute-Force Flaky GUI activities by reseting and retrying.
* @param actionToTry lambda for whatever flaky action you're trying to take
* @param resetAction lambda for backing out of the problem and returning to a known state. Often, refreshing a browser, or closing a popup.
* @param maxRetries number of times to retry the action
* @param delayBetweenRetries milliseconds between retries
*/
export async function tryResetAndRetry(
actionVerb: string,
actionToTry: (...args: any[]) => Promise<any>,
resetAction: (...args: any[]) => Promise<any>,
maxRetries = 2,
delayBetweenRetries = timeoutOptDefaults.timeoutMedium): Promise<any> {
for (let i = 1; i <= maxRetries; i++) {
try {
await actionToTry()
} catch {
await delay(delayBetweenRetries);
resetAction();
}
}
}
/**
* @description For situations where a Test Framework Exception (i.e., a locator timeout) could be incorrectly thrown based on the state of the Target-Application (i.e., a missing tail number). Use this to throw clearer 'Application Exception' errors under such circumstances.
* @param actionVerb What are you trying to do? Could be 'gotoCatalog', 'addAnEnhancement', etc. Logged as `Attempting to ${actionVerb}`
* @param failureExplenation Be descriptive. What is the context of the failure? If someone unfamiliar with the code base were to see this, how would they know if the error was caused by their code, or by an underlying problem with the Target Application?
* @param actionToAttempt a lambda for the flaky action.
* @returns
*/
export async function tryBusinessActionWithRetries(actionVerb: string, failureExplenation: string, actionToAttempt: (...args: any[]) => Promise<any>, attempts = 3): Promise<any> {
let actionResult: any;
let isSuccessful: boolean = false;
//actionVerb is already a logFunc string
console.log(actionVerb);
for (let i = 0; i < attempts; i++) {
try {
actionResult = await actionToAttempt();
isSuccessful = true;
break; // Break loop if actionToAttempt is successful
} catch (e) {
if (e instanceof Error) {
e.message = e.message + ">>Failure Explanation>> " + failureExplenation;
} else {
throw (new Error("Unknown 'AttemptBusinessAction' State"))
}
}
}
if (!isSuccessful) {
throw new Error(`Failed to ${actionVerb}`);
}
//actionVerb is already a logFunc string
console.log(actionVerb);
return actionResult;
}
/**
* Tries to execute a block of code with chances to retry.
* @param {Function} block The block of code to be executed.
* @param {string} [blockDescription=''] A text description of the block (optional).
* @param {number} [maxRetries=3] The maximum number of retry attempts (optional).
* @param {number} [delayMs=1000] The delay between retry attempts in milliseconds (optional).
*/
export async function retry<T>(block: () => Promise<T>, blockDescription: string = '', maxRetries: number = 3, delayMs: number = 1000): Promise<T> {
let retries: number = 0;
if (!blockDescription.length) {
blockDescription = block.toString();
}
while (retries < maxRetries) {
console.log(LoggingUtils.logFunc(retry.name, blockDescription));
try {
return await block();
} catch (error) {
if (retries === maxRetries - 1) {
throw new Error(`Max retries (${maxRetries}) exceeded. Last error: ${error}`);
}
// wait between retries
await new Promise(resolve => setTimeout(resolve, delayMs));
retries++;
}
}
// This should not be reached, but just in case
throw new Error(`Unexpected code execution. Max retries (${maxRetries}) exceeded.`);
}
export async function tryWithRetries(actionBlock: Function, attempts = 3, waitInterval = 1000) {
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
await actionBlock();
if (attempt > 1)
console.warn(`Had to retry but attempt ${attempt} succeeded!`);
break; // Exit the loop if the action is successful
} catch (error) {
console.error(`Attempt ${attempt} failed!`);
if (attempt < attempts) {
await new Promise(resolve => setTimeout(resolve, waitInterval));
}
}
}
}

View file

@ -0,0 +1,56 @@
import { Locator, Page } from "@playwright/test";
import { BasePage } from "./BasePage";
import { IPaymentDetails } from "@business-logic/types/CustomerDetails";
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.getByRole('textbox', { name: 'Please enter your password' });
this.submitButton = page.getByRole('button', { name: 'Continue' });
this.cardholderNameTextBox = page.getByTestId('payment-method-cardHolderName-input');
this.cardNumberTextBox = page.getByTestId('payment-method-cardNumber-input');
this.expirationDateTextBox = page.getByTestId('payment-method-cardExpiry-input');
this.cvvTextBox = page.getByTestId('payment-method-cardCvv-input');
this.confirmButton = page.getByRole('button', { name: 'Confirm' });
}
async login(password: string) {
await this.passwordTextBox.fill(password);
await this.submitButton.click();
}
async populateCardDetails(paymentDetails: IPaymentDetails) {
await this.cardholderNameTextBox.fill('Roberts'); // TODO: Add cardholder name field
await this.cardNumberTextBox.fill(paymentDetails.cardNumber!);
await this.expirationDateTextBox.fill(`${paymentDetails.expirationMonth}/${paymentDetails.expirationYear}`);
await this.cvvTextBox.fill(paymentDetails.cvv!);
await this.submitButton.click();
}
async executeAfterpayPayment(paymentDetails: IPaymentDetails) {
await this.login(paymentDetails.password!);
await this.populateCardDetails(paymentDetails);
await this.confirmButton.click();
}
}

View file

@ -0,0 +1,49 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ICustomerDetails } from '@business-logic/types/CustomerDetails';
export class BailoutPage extends BasePage {
readonly page: Page;
readonly firstNameTextBox: Locator;
readonly lastNameTextBox: Locator;
readonly phoneNumberTextBox: Locator;
readonly emailAddressTextBox: Locator;
url = process.env['BASE_URL']! + '/?issPage=bailout-page';
constructor(page: Page) {
super(page);
this.page = page;
this.firstNameTextBox = this.page.locator('#firstNameField');
this.lastNameTextBox = this.page.locator('#lastNameField');
this.phoneNumberTextBox = this.page.locator('#phoneNumberField');
this.emailAddressTextBox = this.page.locator('#emailAddressField');
// this.page.waitForLoadState();
// this.validateURL(this.url);
}
async validateBailoutDetails(customerDetails: ICustomerDetails, bailoutCode: number) {
await this.firstNameTextBox.waitFor({state:'visible'});
await expect.soft(this.firstNameTextBox).toHaveValue(customerDetails.firstName);
await expect.soft(this.lastNameTextBox).toHaveValue(customerDetails.lastName);
await expect.soft(this.phoneNumberTextBox).toHaveValue(customerDetails.phoneNumber);
await expect.soft(this.emailAddressTextBox).toHaveValue(customerDetails.email);
await expect.soft(await this.getBailoutCode()).toEqual(bailoutCode);
}
async validateBailoutDetailsNotNull(){
await this.firstNameTextBox.waitFor({state:'visible'})
expect(this.firstNameTextBox.inputValue()).not.toBe('');
expect(this.lastNameTextBox.inputValue()).not.toBe('');
expect(this.phoneNumberTextBox.inputValue()).not.toBe('');
expect(this.emailAddressTextBox.inputValue()).not.toBe('');
}
async getBailoutCode() {
const mainLocalStorage = JSON.parse(await this.page.evaluate('localStorage.getItem(\'main\')'));
const bailoutCode = mainLocalStorage.applicationUser.pageData['bailout-page'].bailoutCode as number;
return bailoutCode;
}
}

View file

@ -0,0 +1,41 @@
import { expect, type Locator, type Page } from '@playwright/test';
export class BasePage {
readonly page: Page;
readonly continueButton: Locator;
readonly pageSpinner: Locator;
readonly buttonLoadSpin: Locator;
constructor(page: Page){
this.page = page;
this.continueButton = page.locator('[id="infoBox"]').getByRole('button');
this.pageSpinner = page.getByRole('status');
this.buttonLoadSpin = page.getByRole('alert');
}
async nextPage() {
const startingUrl = this.page.url();
await expect(async () => {
const currentUrl = this.page.url();
if (currentUrl === startingUrl) {
await this.continueButton.click({ timeout: 1000 });
}
//this causes the schedule page to fail
//await expect(this.buttonLoadSpin).toHaveCount(0, {timeout: 180000});
expect(currentUrl).not.toEqual(startingUrl);
}).toPass({ timeout: 240_000 });
}
async validateURL(url:string){
await expect(this.pageSpinner).toHaveCount(0, {timeout: 60000});
await this.page.waitForURL(url);
}
async fillAndValidate(element: Locator, value: string){
await expect(async () => {
await element.clear();
await element.fill(value);
await expect(element).toHaveValue(value);
}).toPass();
}
}

View file

@ -0,0 +1,10 @@
import { Page } from "@playwright/test";
import { PartQuestionsPage } from "./PartQuestionsPage";
export default class CapabilityQuestionsPage extends PartQuestionsPage {
url = process.env['BASE_URL']! + '/?issPage=capability-questions';
constructor(page: Page) {
super(page);
}
}

View file

@ -0,0 +1,17 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ICustomerDetails } from '@business-logic/types/CustomerDetails';
export class ContactConfirmationPage extends BasePage {
readonly page: Page;
readonly ConfirmMessageLabel: Locator;
url = process.env['BASE_URL']! + '/?issPage=contact-confirmation';
constructor(page: Page) {
super(page);
this.page = page;
this.ConfirmMessageLabel = this.page.getByText("Your callback request has been sent!");
}
}

View file

@ -0,0 +1,45 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ICustomerDetails } from '@business-logic/types/CustomerDetails';
export class ContactDetailsPage extends BasePage {
readonly page: Page;
url = process.env['BASE_URL']! + '/?issPage=contact-details';
// 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 getContactDetails() {
const customerDetails: Partial<ICustomerDetails> = {};
customerDetails.firstName = await this.firstNameTextBox.inputValue();
customerDetails.lastName = await this.lastNameTextBox.inputValue();
customerDetails.email = await this.emailAddressTextBox.inputValue();
customerDetails.phoneNumber = await this.phoneNumberTextBox.inputValue();
return customerDetails;
}
async fillNotes(notes?: string) {
if (notes) {
await this.notesTextBox.fill(notes);
}
}
}

View file

@ -0,0 +1,39 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class CoverageStatementPage extends BasePage {
readonly page: Page;
readonly scheduleOnlineButton: Locator;
readonly cancelMyClaimButton: Locator;
readonly deductibleAmount: Locator;
readonly verfiyingCoverageText: Locator;
readonly continueToScheduleButton: Locator; // For ITAC/NoComp
url = process.env['BASE_URL']! + '/?issPage=coverage-statement';
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.getByText('$');
this.verfiyingCoverageText = this.page.getByRole('heading', { name: 'Were verifying your coverage' });
this.continueToScheduleButton = page.locator('div[class*="button-content"]', {hasText:'Continue to schedule online'});
// this.validateURL(this.url);
}
async scheduleOnline(){
await this.scheduleOnlineButton.click();
}
async cancelMyClaim(){
await this.cancelMyClaimButton.click();
}
async validateDeductibleAmount(customer){
await expect(this.deductibleAmount). toContainText(`$${customer.deductibleAmount}`, {timeout: 60000});
}
async validateUnverifiedText(){
await expect(this.verfiyingCoverageText).toBeEnabled();
}
}

View file

@ -0,0 +1,20 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class DuplicateCheckPage extends BasePage {
readonly page: Page;
readonly newClaimButton: Locator;
url = process.env['BASE_URL']! + '/?issPage=duplicate-check';
constructor(page: Page) {
super(page);
this.page = page;
this.newClaimButton = page.locator('label').filter({ hasText: 'Start a new claim' }).locator('div');
// this.validateURL(this.url);
}
async startNewClaim(){
await this.newClaimButton.click();
await this.page.waitForLoadState();
}
}

View file

@ -0,0 +1,54 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IEndorsementDetails } from '@business-logic/types/CustomerDetails';
import { EndorsementType } from '@business-logic/types/Enums';
export class EndorsementsPage extends BasePage {
readonly page: Page;
readonly educatorYesButton: Locator;
readonly educatorNoButton: Locator;
url = process.env['BASE_URL']! + '/?issPage=policy-endorsements';
constructor(page: Page) {
super(page);
this.page = page;
this.educatorYesButton = this.page.locator('label[for="schoolProperty-Yes"]');
this.educatorNoButton = this.page.locator('label[for="schoolProperty-No"]');
}
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;
}
}
}
}
}

View file

@ -0,0 +1,115 @@
import test, { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ICustomerDetails, IVehicleDetails } from '@business-logic/types/CustomerDetails';
import { PaymentType, ServicePackage } from '@business-logic/types/Enums';
import { ITestData } from '@business-logic/types/ITestData';
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;
url = process.env['BASE_URL']! + '/?issPage=order-confirmation';
constructor(page: Page) {
super(page);
this.page = page;
this.serviceText = this.page.locator('[class="appointment-text text-center lh-base"]');
this.emailText = this.page.locator('[class="email-confirmation-text"]');
this.apptDateText = this.page.locator('[class="appointment-date-time text-center mt-4"]');
this.amountDueText = this.page.getByLabel('expand cart details');
this.viewCartButton = this.page.locator('#cart-dropdown-head');
this.deductibleText = this.page.locator('#deductible-value');
this.subtotalText = this.page.locator("#subtotal-value");
this.finalAmountDue = this.page.locator('#bottom-amount-due-value');
this.cartServicePackageText = this.page.locator('#cart-service-package');
// this.validateURL(this.url);
}
async validateOrderConfirmationPage(testData: Partial<ITestData>) {
// Destructure data we use
const { vehicleDetails, customerDetails, servicePackage, isItac,
isNoComp, isPolicyFound, claimDetails, paymentDetails } = testData;
await this.serviceText.waitFor({ state: "visible" });
await this.logOrderNumber();
// Grab text
const serviceTextValue = await this.serviceText.textContent();
const apptDateValue = await this.apptDateText.textContent();
const emailTextValue = await this.emailText.textContent();
const servicePackageValue = await this.cartServicePackageText.textContent();
const amountDueValue = await this.amountDueText.textContent();
const deductibleTextValue = (isItac || isNoComp)? null: 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
expect.soft(serviceTextValue).toContain(`${vehicleDetails!.year} ${vehicleDetails!.make} ${vehicleDetails!.model}`);
expect.soft(apptDateValue).toContain(customerDetails!.apptDate);
expect.soft(emailTextValue).toContain(customerDetails!.email);
// Service package validations
await expect.soft(this.cartServicePackageText).toContainText(`${servicePackage}`)
if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) {
expect.soft(servicePackageValue).toContain('New wiper blades');
}
if (servicePackage === ServicePackage.Premium) {
expect.soft(servicePackageValue).toContain('Rain Defense™ treatment');
}
// Price validations
if (servicePackage === ServicePackage.GlassOnly) {
expect.soft(servicePackageAmt).toEqual(0);
} else {
expect.soft(servicePackageAmt).toBeGreaterThan(0);
}
if (isPolicyFound) {
// 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(',', ''));
if (!(isItac || isNoComp)) {
expect.soft(subtotalAmt).toEqual(claimDetails!.policyDeductible + servicePackageAmt);
expect.soft(deductibleAmt).toEqual(claimDetails!.policyDeductible);
} else {
expect.soft(subtotalAmt).toBeGreaterThan(0);
expect.soft(deductibleAmt).toEqual(0);
}
if (paymentDetails!.paymentType === PaymentType.PayAtService && (claimDetails!.policyDeductible > 0 || servicePackageAmt > 0)) {
// Verify amount due > 0
expect.soft(amountDueAmt).toBeGreaterThan(0);
expect.soft(finalAmountDueAmt).toBeGreaterThan(0);
} else {
// Verify amount due 0
expect.soft(amountDueAmt).toEqual(0);
expect.soft(finalAmountDueAmt).toEqual(0);
}
} else {
expect.soft(amountDueValue).toContain('Verifying coverage');
expect.soft(subtotalTextValue).toEqual('Verifying coverage');
expect.soft(finalAmountDueValue).toEqual('Verifying coverage');
}
}
async logOrderNumber() {
const sessionStorage = JSON.parse(await this.page.evaluate('sessionStorage.getItem(\'submittedOrder\')'));
const workOrderNumber = sessionStorage.workOrderNumber;
await test.step(`SessionStorage Work Order Number:${workOrderNumber}`, async () => {
console.log(`SessionStorage Work Order Number:${workOrderNumber}`);
});
}
}

View file

@ -0,0 +1,35 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPartQuestion } from '@business-logic/types/CustomerDetails';
export class PartQuestionsPage extends BasePage {
readonly page: Page;
url = process.env['BASE_URL']! + '/?issPage=part-questions';
constructor(page: Page) {
super(page);
this.page = page;
}
async validatePartQuestions(partQuestions: IPartQuestion[]) {
for (const pq of partQuestions) {
const partQuestionOptions = this.page.locator(`fieldset[aria-labelledby="${pq.partQuestionType}"]`);
if (pq.isOnPage) {
await expect(partQuestionOptions).toBeAttached();
} else {
await expect(partQuestionOptions).not.toBeAttached();
}
}
}
async selectPartQuestionResponses(partQuestions: IPartQuestion[]) {
for (const pq of partQuestions) {
const partQuestionOptionButton = this.page.locator(`fieldset[aria-labelledby="${pq.partQuestionType}"]`).locator(`[buttonlabel="${pq.optionToSelect}"]`);
await partQuestionOptionButton.click();
if (pq.secondaryQuestionOptionToSelect != null) {
const secondaryQuestionButton = this.page.getByText(`${pq.secondaryQuestionOptionToSelect}`);
secondaryQuestionButton.click();
}
}
}
}

View file

@ -0,0 +1,82 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPaymentDetails } from '@business-logic/types/CustomerDetails';
import { PaymentType } from '@business-logic/types/Enums';
import { PaymentPage } from './PaymentPage';
import { AfterpayPage } from './AfterpayPage';
import { PaypalPage } from './PaypalPage';
export class PaymentMethodPage extends BasePage {
readonly page: Page;
readonly payAtServiceButton: Locator;
readonly payNowButton: Locator;
readonly payInFourButton: Locator;
readonly paypalButton: Locator;
readonly paymentPage: PaymentPage;
readonly paypalPage: PaypalPage;
url = process.env['BASE_URL']! + '/?issPage=payment-method';
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.payNowButton = this.page.locator('[buttonlabel="Pay now"]');
this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]');
this.paypalButton = this.page.frameLocator('iframe[name="card-frame"]').locator('div[id="paypalParentDiv"]');
this.paymentPage = new PaymentPage(page);
this.paypalPage = new PaypalPage(page);
}
async executePayment(paymentDetails: IPaymentDetails) {
const browserContext = this.page.context();
switch (paymentDetails.paymentType) {
case PaymentType.Credit:
await this.selectCreditCard();
await this.paymentPage.populateCreditCardDetails(paymentDetails);
break;
case PaymentType.Paypal:
await this.selectPaypal();
await this.paypalPage.completePaypalPurchase(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.PayAtService:
await this.selectPayAtService();
break;
default:
console.error('PaymentMethodPage >> Logic for this payment method unimplemented');
break;
}
}
async selectPaypal() {
await this.payNowButton.click();
await this.nextPage();
await this.paypalButton.click();
}
async selectCreditCard() {
await this.payNowButton.click();
await this.nextPage();
}
async selectPayAtService() {
await this.payAtServiceButton.click();
}
// async validateAmountDue(customer){
// await this.amountDueDropDown.click();
// await expect(this.deductibleAmountTextField).toContainText(`${customer.deductibleAmount}`);
// }
}

View file

@ -0,0 +1,45 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPaymentDetails } from '@business-logic/types/CustomerDetails';
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;
url = process.env['BASE_URL']! + '/?issPage=payment-page';
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.validateURL(this.url);
}
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();
}
}

View file

@ -0,0 +1,27 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPaymentDetails } from '@business-logic/types/CustomerDetails';
export class PaypalPage extends BasePage {
readonly page: Page;
readonly loginWithPasswordButton: Locator;
readonly passwordTextBox: Locator;
readonly paypalLoginButton: Locator;
readonly completePurchaseButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.loginWithPasswordButton = page.getByRole('link', { name: 'Log in with a password instead' });
this.passwordTextBox = page.getByPlaceholder('Password');
this.paypalLoginButton = page.getByRole('button', { name: 'Log In', exact: true });
this.completePurchaseButton = page.getByTestId('submit-button-initial');
}
async completePaypalPurchase(paymentDetails: IPaymentDetails){
await this.loginWithPasswordButton.click();
await this.passwordTextBox.fill(paymentDetails.password!);
await this.paypalLoginButton.click();
await this.completePurchaseButton.click();
}
}

View file

@ -0,0 +1,20 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ICustomerDetails } from '@business-logic/types/CustomerDetails';
import { AddressForm } from './forms/AddressForm';
export class PolicyHolderDetailsPage extends BasePage {
readonly page: Page;
readonly addressForm: AddressForm;
readonly url = process.env['BASE_URL']! + '/?issPage=policy-holder-details';
constructor(page: Page) {
super(page);
this.page = page;
this.addressForm = new AddressForm(page);
}
async fillCustomerDetails(customerDetails: ICustomerDetails){
await this.addressForm.populateAddress(customerDetails);
}
}

View file

@ -0,0 +1,27 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IVehicleDetails } from '@business-logic/types/CustomerDetails';
export class PolicyVehiclesPage extends BasePage {
readonly page: Page;
url = process.env['BASE_URL']! + '/?issPage=policy-vehicles';
constructor(page: Page) {
super(page);
this.page = page;
// this.validateURL(this.url);
}
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 vehicleRegExp = new RegExp(`${vehicleDetails.year} .+ ${vehicleDetails.model}`, 'i');
await this.page.locator('label').filter({hasText: vehicleRegExp}).locator('div').click();
}
async selectVehicleNotListed(){
await this.page.getByText('Vehicle not listed').click();
}
}

View file

@ -0,0 +1,50 @@
import test, { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class ProviderPreferencePage extends BasePage {
readonly page: Page;
readonly scheduleWithSafelite: Locator;
readonly scheduleWithOther: Locator;
readonly acknowledgeAdasButton: Locator;
readonly gotItButton: Locator;
readonly acknowledgeCheckbox: Locator;
readonly stateLawModalHeading: Locator;
url = process.env['BASE_URL']! + '/?issPage=provider-preference';
constructor(page: Page) {
super(page);
this.page = page;
// this.scheduleWithSafelite = this.page.locator('div').filter({ hasText: /Schedule online with |Safelite AutoGlass/ }).first();
this.scheduleWithSafelite = this.page.getByText(/Schedule online with|Safelite AutoGlass/).first();
this.scheduleWithOther = this.page.getByText(/Find another shop|Choose my own shop/).first();
this.acknowledgeAdasButton = this.page.getByLabel('I acknowledge that my vehicle');
this.gotItButton = this.page.getByRole('button', { name: 'Got it' });
this.acknowledgeCheckbox = this.page.locator('#tpaAcknowledgement');
this.stateLawModalHeading = this.page.getByRole('heading').filter({ hasText: /.* State Law/});
}
async selectProvider(isSafelite = true) {
if (isSafelite) {
await this.scheduleWithSafelite.click();
await this.nextPage();
}
else {
await this.scheduleWithOther.waitFor({ state: 'visible' });
await this.scheduleWithOther.click();
await this.continueButton.click();
//if(await this.acknowledgeAdasButton.isEnabled({timeout: 2500})){
// await this.acknowledgeAdasButton.click();
// await this.gotItButton.click();
// await this.page.waitForTimeout(1000);
//}
}
}
async acknowledgeRecalNotificaiton() {
await this.acknowledgeAdasButton.click();
await this.gotItButton.click();
}
async validateStateLawModalIsVisible() {
await expect.soft(this.stateLawModalHeading).toBeVisible();
}
}

View file

@ -0,0 +1,58 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IAppointmentDetails } from '@business-logic/types/CustomerDetails';
import { formatDate, formatTime } from '@impl/utils/DateUtils';
import { ServiceLocation } from '@business-logic/types/Enums';
export class SchedulePage extends BasePage {
readonly page: Page;
url = process.env['BASE_URL']! + '/?issPage=schedule-page';
readonly firstAvailableDate: Locator;
readonly firstAvailableTime: Locator;
readonly modalContinueButton: Locator;
readonly dropOffButton: Locator;
readonly dateText: Locator;
readonly viewMoreDatesLink: 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.locator('#modalbtn');
this.dropOffButton = this.page.getByText('Drop off your vehicle', { exact: true });
this.dateText = this.page.locator('[class="modal-header mb-2 mt-2"]');
this.viewMoreDatesLink = this.page.getByText(/View more dates/).first();
}
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(serviceLocation: ServiceLocation) {
if (await this.firstAvailableDate.isVisible()) {
await this.firstAvailableDate.click();
} else {
await this.viewMoreDatesLink.click();
while(await this.firstAvailableDate.isHidden()){
await this.viewMoreDatesLink.click();
}
await this.firstAvailableDate.click();
}
serviceLocation === ServiceLocation.DropOff ? await this.dropOffButton.click() : await this.firstAvailableTime.click();
await this.modalContinueButton.click();
return (`${await this.dateText.allInnerTexts()}`);
}
}

View file

@ -0,0 +1,135 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IAppointmentDetails } from '@business-logic/types/CustomerDetails';
import { ServiceLocation } from '@business-logic/types/Enums';
import { AddressForm } from './forms/AddressForm';
import { faker } from '@faker-js/faker';
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 saveAddressButton: Locator;
url = process.env['BASE_URL']! + '/?issPage=service-location';
constructor(page: Page) {
super(page);
this.page = page;
this.addressForm = new AddressForm(page);
// Initial selection
this.inShopButton = this.page.locator('[buttonlabel="In-shop"]');
this.mobileButton = this.page.locator('[buttonlabel="Mobile"]')
this.dropOffButton = this.page.locator('[buttonlabel="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('div').filter({ hasText: /Appts/}).first();
this.changeZipButton = this.page.locator('[id="serviceZipLinkPromptId"]');
this.updateZipTextBox = this.page.getByRole('textbox', { name: 'Update your service ZIP code'});
this.saveZipButton = this.page.getByRole('button', { name: 'Save ZIP code' });
// 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.saveAddressButton = this.page.getByRole('button', { name: 'Save Address' });
}
async selectLocation(appointmentDetails: IAppointmentDetails){
if (appointmentDetails.alternateServiceZip) {
await this.changeZipButton.click();
await this.fillAndValidate(this.updateZipTextBox, appointmentDetails.alternateServiceZip);
await this.saveZipButton.click();
}
switch(appointmentDetails.serviceLocation) {
case ServiceLocation.Mobile:
await this.scheduleMobile(appointmentDetails);
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) {
await this.selectAShopOptions.locator(`[buttonbodycopy="${appointmentDetails.shopAddress}"]`).check();
} else {
await this.firstAppointmentButton.click();
}
}
async scheduleMobile(appointmentDetails: IAppointmentDetails){
if (appointmentDetails.serviceAddress) {
await this.mobileButton.click();
await this.enterServiceAddressButton.click();
await this.addressForm.populateAddress({ address: appointmentDetails.serviceAddress! });
if (faker.datatype.boolean()) {
await this.vehicleProtectedYesButton.check();
} else {
await this.vehicleProtectedNoButton.check();
}
await this.saveAddressButton.click();
} 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();
}
}
async validateRecalWarning(){
await expect(this.RecalWarningMessage1).toBeVisible();
await expect(this.RecalWarningMessage2).toBeVisible();
}
}

View file

@ -0,0 +1,26 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { ServicePackage } from '@business-logic/types/Enums';
export class ServicePackagesPage extends BasePage {
readonly page: Page;
readonly standardPackageButton: Locator;
readonly premiumPackageButton: Locator;
readonly glassOnlyButton: Locator;
url = process.env['BASE_URL']! + '/?issPage=service-packages';
constructor(page: Page) {
super(page);
this.page = page;
this.standardPackageButton = this.page.locator('li').filter({ hasText: 'Standard' });
this.premiumPackageButton = this.page.locator('li').filter({ hasText: 'Premium' });
this.glassOnlyButton = this.page.locator('li').filter({ hasText: 'Glass service' });
// this.validateURL(this.url);
}
async selectServicePackage(servicePackage: ServicePackage){
await this.page.getByText(servicePackage).click();
return (`${await this.page.locator('li').filter({ hasText: 'Premium' }).locator('[class="pricing-info"]').allInnerTexts()}`);
//return (`${await this.page.getByText(servicePackage).locator('[class="pricing-info"]').allInnerTexts()}`);
}
}

View file

@ -0,0 +1,19 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class TpaConfirmationPage extends BasePage {
readonly page: Page;
readonly successMessage: Locator;
readonly url = process.env['BASE_URL']! + '/?issPage=tpa-confirmation';
constructor(page: Page) {
super(page);
this.page = page;
this.successMessage = page.getByText('Success');
}
async validateSuccessMessage() {
await expect(this.successMessage).toBeVisible();
}
}

View file

@ -0,0 +1,24 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class TpaSearchPage extends BasePage {
readonly page: Page;
readonly firstLocationButton: Locator;
readonly doNotSeeMyShopButton: Locator;
url = process.env['BASE_URL']! + '/?issPage=tpa-search';
constructor(page: Page) {
super(page);
this.page = page;
this.firstLocationButton = page.locator("fieldset[aria-labelledby='chooseShop']").first();
this.doNotSeeMyShopButton = page.getByRole('link', { name: 'I don\'t see my shop' });
}
async selectDoNotSeeMyShop() {
await this.doNotSeeMyShopButton.click()
}
async selectFirstLocation() {
await this.firstLocationButton.click();
}
}

View file

@ -0,0 +1,18 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class TpaSubmitPage extends BasePage {
readonly page: Page;
readonly deductible: Locator;
url = process.env['BASE_URL']! + '/?issPage=tpa-submit';
constructor(page: Page) {
super(page);
this.page = page;
this.deductible = page.getByText('Deductible $');
}
async validateDeductible(expDeductible) {
await expect(this.deductible).toContainText(expDeductible);
}
}

View file

@ -0,0 +1,150 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { SideDoorDamage, VehicleDamage, WindshieldDamage } from '@business-logic/types/Enums';
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 separateApptsWarning: Locator;
readonly editVehicleButton: Locator;
url = process.env['BASE_URL']! + '/?issPage=vehicle-damage';
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.separateApptsWarning = this.page.locator('[class*="widget-name-HasReplacementConflict"]');
this.editVehicleButton = this.page.getByRole('link', { name: 'Edit vehicle' });
}
async checkSeparateApptsWarning() {
// Select conflict
await this.windshieldChkBox.check();
await this.chipButton.check();
await this.rearWindowChkBox.check();
// Expect warning
await expect.soft(this.separateApptsWarning).toBeAttached();
// Undo changes
await this.crackButton.check();
await this.windshieldChkBox.uncheck();
await expect.soft(this.crackButton).not.toBeVisible();
await this.rearWindowChkBox.uncheck();
}
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.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();
break;
}
}
}
async selectCrack(){
await this.crackButton.check();
}
async selectChips(numChips: string){
const numChipsButton = this.page.getByText(numChips, {exact: true});
await this.chipButton.check();
await numChipsButton.check();
}
async selectRearWindowDamage(){
await this.rearWindowChkBox.check();
}
}

View file

@ -0,0 +1,25 @@
import { type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { AddressForm } from './forms/AddressForm';
import { VehicleSelectionForm } from './forms/VehicleSelectionForm';
import { ICustomerDetails, IVehicleDetails } from '@business-logic/types/CustomerDetails';
export class VehicleLookupAddressPage extends BasePage {
readonly page: Page;
readonly addressForm: AddressForm;
readonly vehicleSelectionForm: VehicleSelectionForm;
url = process.env['BASE_URL']! + '/?issPage=address-vehicles';
constructor(page: Page) {
super(page);
this.page = page;
this.addressForm = new AddressForm(page);
this.vehicleSelectionForm = new VehicleSelectionForm(page);
}
async lookupVehicleByAddress(customerDetails: ICustomerDetails, vehicleDetails: IVehicleDetails) {
await this.addressForm.populateAddress(customerDetails);
//await this.nextPage();
//await this.vehicleSelectionForm.selectVehicle(vehicleDetails);
}
}

View file

@ -0,0 +1,22 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IVehicleDetails } from '@business-logic/types/CustomerDetails';
export class VehicleLookupLicensePage extends BasePage {
readonly page: Page;
readonly licensePlateNumTextBox: Locator;
readonly licensePlateStateDrpDwn: Locator;
url = process.env['BASE_URL']! + '/?issPage='; // TODO: Input correct URL
constructor(page: Page) {
super(page);
this.page = page;
this.licensePlateNumTextBox = page.getByRole('textbox', { name: 'License plate number'});
this.licensePlateStateDrpDwn = page.getByRole('combobox', { name: 'License plate state'});
}
async enterPlateDetails(vehicleDetails: IVehicleDetails){
await this.licensePlateNumTextBox.fill(vehicleDetails.licensePlateNumber || '');
await this.licensePlateStateDrpDwn.selectOption(vehicleDetails.licensePlateState!);
}
}

View file

@ -0,0 +1,61 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { VehicleLookupType } from '@business-logic/types/Enums';
import { IVehicleDetails } from '@business-logic/types/CustomerDetails';
import { VinLookupPage } from './VinLookupPage';
import { VehicleLookupAddressPage } from './VehicleLookupAddressPage';
import { VehicleLookupLicensePage } from './VehicleLookupLicensePage';
export class VehicleLookupPage extends BasePage {
readonly page: Page;
readonly vinLookupButton: Locator;
readonly addressLookupButton: Locator;
readonly licenseLookupButton: Locator;
readonly vinLookupPage: VinLookupPage;
readonly vehicleLookupAddressPage: VehicleLookupAddressPage;
readonly vehicleLookupLicensePage: VehicleLookupLicensePage;
url = process.env['BASE_URL']! + '/?issPage=vehicle-lookup';
constructor(page: Page) {
super(page);
this.page = page;
this.vinLookupButton = page.getByLabel('Provide my VIN manually', { exact: true });
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);
// this.validateURL(this.url);
}
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;
default:
console.error('VehicleLookupPage >> DATA ISSUE: VehicleLookupType not provided');
break;
}
}
async selectVinLookup(){
await this.vinLookupButton.click();
}
async selectAddressLookup(){
await this.addressLookupButton.click();
}
async selectLicenseLookup(){
await this.licenseLookupButton.click();
}
}

View file

@ -0,0 +1,10 @@
import { Page } from "@playwright/test";
import { PartQuestionsPage } from "./PartQuestionsPage";
export default class VehiclePartQuestionsPage extends PartQuestionsPage{
url = process.env['BASE_URL']! + '/?issPage=vehicle-parts';
constructor(page: Page) {
super(page);
}
}

View file

@ -0,0 +1,35 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IVehicleDetails } from '@business-logic/types/CustomerDetails';
export class VehicleSelectionPage extends BasePage {
readonly page: Page;
readonly yearDropdown: Locator;
readonly makeDropdown: Locator;
readonly modelDropdown: Locator;
readonly styleDropdown: Locator;
url = process.env['BASE_URL']! + '/?issPage=vehicle-selection';
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.validateURL(this.url);
}
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);
}
}
}

View file

@ -0,0 +1,26 @@
import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class VinLookupPage extends BasePage {
readonly page: Page;
readonly vinLookupTextBox: Locator;
readonly lookupVinForMe: Locator;
url = process.env['BASE_URL']! + '/?issPage=vin-lookup'; // TODO: Input correct URL
constructor(page: Page) {
super(page);
this.page = page;
this.vinLookupTextBox = page.getByRole('textbox', { name: 'Enter your VIN' });
this.lookupVinForMe = page.getByRole('link', { name: 'look up your VIN' });
// this.validateURL(this.url);
}
async enterVin(vin: string) {
await this.vinLookupTextBox.fill(vin);
}
async triggerBailout() {
await this.continueButton.click();
await this.lookupVinForMe.click();
}
}

View file

@ -0,0 +1,105 @@
import test, { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IClaimDetails, ICustomerDetails } from '@business-logic/types/CustomerDetails';
import { getClientAuthByClientTag, getClientSignature } from '@impl/api/AdminServiceApiUtil';
import { ICertificateInfo, IClientSignatureRequest } from '@business-logic/types/Authentication';
import { buildToken, getTimestamp } from '@impl/utils/TokenUtils';
export class WelcomePage extends BasePage {
readonly page: Page;
readonly policyNumber: Locator;
readonly policyZip: Locator;
readonly damageDate: Locator;
readonly damageCause: Locator;
readonly phoneNumber: Locator;
readonly emailAddress: Locator;
readonly city: Locator;
readonly state: Locator;
readonly cookieCloseButton: Locator;
url = process.env['BASE_URL']! + '/?issPage=welcome-page';
constructor(page: Page) {
super(page);
this.page = page;
this.policyNumber = page.getByRole('textbox', { name: 'Policy number' });
this.policyZip = page.getByRole('textbox', { name: 'Policy ZIP' });
this.damageDate = page.getByRole('textbox', { name: 'When did the damage occur?<' });
this.damageCause = page.locator('#damageCauseQuestionField');
this.phoneNumber = page.getByRole('textbox', { name: 'Best number to reach you' });
this.emailAddress = page.getByRole('textbox', { name: 'Email address' });
this.city = page.getByRole('textbox', { name: 'In which city did the damage' });
this.state = page.locator('select[name="\\38 fdf9dc2e13e430eb57529499dceb3eb"]');
this.cookieCloseButton = page.getByRole('button', { name: 'Close' });
}
async goto(clientTag: string) {
await this.page.goto(process.env['BASE_URL']! + `/?issPage=entry-page&ClientTag=${clientTag}`);
await this.validateURL(this.url);
await this.cookieCloseButton.click();
}
async gotoWithAuthentication(clientTag: string) {
const clientAuth = await getClientAuthByClientTag(clientTag);
const certificate = clientAuth.certificateInfo[0] as ICertificateInfo;
let formData: Map<string, string> = new Map<string, string>();
formData.set("Timestamp", getTimestamp())
const request: IClientSignatureRequest = {
clientTag: clientTag,
token: buildToken(clientAuth, formData),
certificateFileName: certificate.name,
certificateKey: certificate.key,
certificateAlgorithm: certificate.algorithm,
certificateType: certificate.type
}
const result = await getClientSignature(request);
await this.page.goto(process.env['BASE_URL']! + `/?issPage=entry-page&ClientTag=${clientTag}&token=${request.token}&signature=${result.signature}`);
await this.validateURL(this.url);
await this.logReferralNumber();
await this.cookieCloseButton.click();
}
async hasCityInfo() {
await expect.soft(this.damageCause).toBeVisible();
return this.city.isVisible();
}
async populatePage(customerDetails: ICustomerDetails, claimDetails: IClaimDetails, isFillCityInfo: boolean) {
await this.policyNumber.fill(claimDetails.policyNumber);
await this.policyZip.fill(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);
// Phone number fixed 12/5. Can't start with 1 or 0
await this.emailAddress.fill(customerDetails.email);
if (isFillCityInfo) {
await this.city.fill(customerDetails.address.city);
await this.state.selectOption(customerDetails.address.state);
}
await this.logReferralNumber();
}
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}`);
});
}
}

View file

@ -0,0 +1,58 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from '../BasePage';
import { ICustomerDetails } from '@business-logic/types/CustomerDetails';
import { faker } from '@faker-js/faker/locale/en';
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;
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.');
}
async forceAddressFormToAppear() {
await expect(async () => {
await this.streetAddressTextBox.click();
await this.streetAddressTextBox.pressSequentially('7400 Safelite Way');
await this.streetAddressTextBox.press('Tab');
await expect(this.zipCodeTextBox).toBeVisible({ timeout: 100 });
}).toPass();
}
async populateAddress(customerDetails: Partial<ICustomerDetails>) {
if (customerDetails.address) {
// Force address form to appear
await this.forceAddressFormToAppear();
// Fill address
await this.fillAndValidate(this.streetAddressTextBox, customerDetails.address.street);
await this.fillAndValidate(this.zipCodeTextBox, customerDetails.address.postalCode)
await this.fillAndValidate(this.cityTextBox, customerDetails.address.city);
await this.stateDrpDwn.selectOption(customerDetails.address.state);
}
if (customerDetails.firstName) {
await this.fillAndValidate(this.firstNameTextBox, customerDetails.firstName);
}
if (customerDetails.lastName) {
await this.fillAndValidate(this.lastNameTextBox, customerDetails.lastName);
}
}
}

View file

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

View file

@ -0,0 +1,112 @@
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv-safe';
import { OrtoniReportConfig } from "ortoni-report";
if (!process.env.CI) {
// Environment variables are present in CI environment, no need to read from file
if (process.env.NODE_ENV == 'undefined' || process.env.NODE_ENV == null) {
dotenv.config({ path: `playwright-tests/.env.dev`, example: 'playwright-tests/.env.example' });
}
else {
dotenv.config({ path: `playwright-tests/.env.${process.env.NODE_ENV}`, example: 'playwright-tests/.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.
*/
const reportConfig: OrtoniReportConfig = {
port: 1994,
open: "never",
folderPath: "test-results",
filename: "index.html",
logo: "../data/logo.png",
title: "Test Report",
showProject: false,
projectName: "ISS-Nextgen-Playwright-Report",
testType: `E2E- Environment: ${process.env.NODE_ENV} `,
preferredTheme: "light",
base64Image: true,
};
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 ? 2 : 5,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
['ortoni-report', reportConfig],
['list']
],
timeout: 120_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",
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View file

@ -0,0 +1,628 @@
import TestCase from "@business-logic/types/TestCase";
import { expect, Page } from "@playwright/test";
import { addSmokeTagToRandomTest, prepareTest, test, TestInfo } from "@business-logic/types/Test";
import { RuleEngine, ValidationOptions } from "@business-logic/types/RuleEngine";
import { ICustomerDetails } from "@business-logic/types/CustomerDetails";
import essentialHpTestCases from "./0016_EssentialRepairInShop";
import essentialReplaceTestCases from "./0013_EssentialReplace";
import { BailoutCode, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import essentialTpaNotEnabledTestCases from "./0003_EssentialTpaNotEnabledBailout";
import essentialDoNotSeeShopTestCases from "./0011_EssentialDoNotSeeShopBailout";
import essentialVehicleNotFoundTestCases from "./0010_EssentialVehicleNotFoundBailout";
import essentialPartsServiceErrorBailout_0009 from "./0009_EssentialPartsServiceErrorBailout";
import essentialHeavyVehicleBailoutTestCases from "./0008_EssentialHeavyVehicleBailout";
import essentialRepairMobileTests from "./0014_EssentialRepairMobile";
import essentialReplacePartsQuestionsDropoff_0015 from "./0015_EssentialReplacePartsQuestionsDropoff";
import essentialUniqueGlassTests from "./0012_EssentialUniqueGlass";
import essentialRepairInShopAcuraTests from "./0002_EssentialRepairInShopAcura";
import essentialTpaEnabled_0004 from "./0004_EssentialTpaEnabled";
import essentialRepairMobileHyundaiTests from "./0007_EssentialRepairHyundaiMobile";
import essentialTpaNotEnabledReplace_0017 from "./0017_EssentialTpaNotEnabledBailoutReplace";
import essentialTpaEnabledReplace_0018 from "./0018_EssentialTpaEnabledReplace";
import essentialTpaEnabledReplaceRecal_0019 from "./0019_EssentialTpaEnabledReplaceRecal";
import advancedScenario0001TestCases from "./advanced/0001a_ReplaceInShopCredit";
import advancedScenario0003TestCases from "./advanced/0003a_MobileAfterpay";
import essentialReplaceDynamicAdasTests from "./0005_EssentialReplaceDynamicAdas";
import essentialReplaceStaticAdasTests from "./0001_EssentialReplaceStatisAdas";
import essentialVehicleLookupBailoutTests from "./0020_EssentialVehicleLookupBailout";
import essentialPriceServiceErrorBailoutTests from "./0021_EssentialPriceServiceErrorBailout";
import { forceAPIError, mockApiResponse } from "@impl/utils/HttpUtils";
import advancedScenario0002TestCases from "./advanced/0002a_ReplaceOemEndorsement";
import ApiResponseInterceptUtil from "@impl/api/ApiResponseInterceptUtil";
import advancedScenario0004aTestCases from "./advanced/0004a_NoDeductibleAdas";
import advancedScenario0006TestCases from "./advanced/0006a_RepairNoDeductibleMobile";
import advancedScenario0007TestCases from "./advanced/0007a_RepairStateLanguage";
import advancedScenario0008TestCases from "./advanced/0008a_RepairTpa";
import advancedScenario0011TestCases from "./advanced/0011a_ItacNoAdas";
import advancedScenario0012TestCases from "./advanced/0012a_ItacDropOff";
import advancedScenario0014TestCases from "./advanced/0014a_NoCompAdas";
import advancedScenario0013TestCases from "./advanced/0013a_ItacMobile";
import advancedScenario0015TestCases from "./advanced/0015a_NoCompPartQuestions";
import advancedScenario0016TestCases from "./advanced/0016a_NoCompAllGlass";
import advancedScenario0017TestCases from "./advanced/0017a_NoCompPremium";
import advancedScenario0018TestCases from "./advanced/0018a_NoCompGlassOnly";
import advancedScenario0019TestCases from "./advanced/0019a_NoCompEditVehicle";
import advancedScenario0020TestCases from "./advanced/0020a_NoCompChangeLoc";
import advancedScenario0005TestCases from "./advanced/0005a_CapabilityQuestions";
import advancedScenario0009TestCases from "./advanced/0009a_NoDeductibleFlorida";
import advancedScenario0010TestCases from "./advanced/0010a_RearGlass";
test.describe.parallel('ISS QA Automation Regression', () => {
const ruleEngine = new RuleEngine<TestCase>();
const options = new ValidationOptions();
addSmokeTagToRandomTest(essentialReplaceStaticAdasTests);
addSmokeTagToRandomTest(essentialRepairInShopAcuraTests);
addSmokeTagToRandomTest(essentialTpaNotEnabledTestCases);
addSmokeTagToRandomTest(essentialTpaEnabled_0004);
addSmokeTagToRandomTest(essentialReplaceDynamicAdasTests);
addSmokeTagToRandomTest(essentialRepairMobileHyundaiTests);
addSmokeTagToRandomTest(essentialVehicleNotFoundTestCases);
addSmokeTagToRandomTest(essentialHeavyVehicleBailoutTestCases);
addSmokeTagToRandomTest(essentialPartsServiceErrorBailout_0009);
addSmokeTagToRandomTest(essentialDoNotSeeShopTestCases);
addSmokeTagToRandomTest(essentialUniqueGlassTests);
addSmokeTagToRandomTest(essentialReplaceTestCases);
addSmokeTagToRandomTest(essentialRepairMobileTests);
addSmokeTagToRandomTest(essentialReplacePartsQuestionsDropoff_0015);
addSmokeTagToRandomTest(essentialHpTestCases);
addSmokeTagToRandomTest(essentialTpaNotEnabledReplace_0017);
addSmokeTagToRandomTest(essentialTpaEnabledReplace_0018);
addSmokeTagToRandomTest(essentialTpaEnabledReplaceRecal_0019);
addSmokeTagToRandomTest(essentialVehicleLookupBailoutTests);
addSmokeTagToRandomTest(essentialPriceServiceErrorBailoutTests);
//Scenario 1
for (const testCase of essentialReplaceStaticAdasTests) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 2
for (const testCase of essentialRepairInShopAcuraTests) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 3
for (const testCase of essentialTpaNotEnabledTestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 4
for (const testCase of essentialTpaEnabled_0004) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 5
for (const testCase of essentialReplaceDynamicAdasTests) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 7
for (const testCase of essentialRepairMobileHyundaiTests) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 8
for (const testCase of essentialHeavyVehicleBailoutTestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 9
for (const testCase of essentialPartsServiceErrorBailout_0009) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 10
for (const testCase of essentialVehicleNotFoundTestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 11
for (const testCase of essentialDoNotSeeShopTestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 12
for (const testCase of essentialUniqueGlassTests) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 13 //defect# SSR-2009
for (const testCase of essentialReplaceTestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 14
for (const testCase of essentialRepairMobileTests) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 15
for (const testCase of essentialReplacePartsQuestionsDropoff_0015) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 16
for (const testCase of essentialHpTestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 17
for (const testCase of essentialTpaNotEnabledReplace_0017) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 18
for (const testCase of essentialTpaEnabledReplace_0018) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 19
for (const testCase of essentialTpaEnabledReplaceRecal_0019) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
//Scenario 20
for (const testCase of essentialVehicleLookupBailoutTests) {
test(...prepareTest(testCase, run, options, ruleEngine)); //skipping this until SSR-2004 is fixed
}
//Scenario 21
for (const testCase of essentialPriceServiceErrorBailoutTests) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Advanced Scenarios
// Scenario 0001a
for (const testCase of advancedScenario0001TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0002a
for (const testCase of advancedScenario0002TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0003a
// Note: payment will fail in dev. Payment (PIA) works fine in SYS
for (const testCase of advancedScenario0003TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0004a
for (const testCase of advancedScenario0004aTestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0005a
for (const testCase of advancedScenario0005TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0006a
for (const testCase of advancedScenario0006TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0007a
for (const testCase of advancedScenario0007TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0008a
for (const testCase of advancedScenario0008TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0009a
for (const testCase of advancedScenario0009TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0010a
for (const testCase of advancedScenario0010TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0011a
for (const testCase of advancedScenario0011TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0012a
for (const testCase of advancedScenario0012TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0013a
for (const testCase of advancedScenario0013TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0014a
for (const testCase of advancedScenario0014TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0015a
for (const testCase of advancedScenario0015TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0016a
// FIXME:
for (const testCase of advancedScenario0016TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0017a
for (const testCase of advancedScenario0017TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0018a
for (const testCase of advancedScenario0018TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0019a
for (const testCase of advancedScenario0019TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
// Scenario 0020a
for (const testCase of advancedScenario0020TestCases) {
test(...prepareTest(testCase, run, options, ruleEngine));
}
});
test.afterEach(async ({ page, testInfo }) => {
await TestCase.afterEachMethod(page, testInfo);
});
async function run(page: Page, testInfo: TestInfo): Promise<void> {
await testInfo.testCase.setup();
testInfo.testCase.setupPages(page);
if (testInfo.testCase.testData.isAuthenticationRequired) {
await testInfo.testCase.pages.welcomePage.gotoWithAuthentication(testInfo.testCase.testData!.clientTag!);
}
else {
await testInfo.testCase.pages.welcomePage.goto(testInfo.testCase.testData!.clientTag!);
}
await runWorkflow(page, testInfo.testCase);
}
async function runWorkflow(page: Page, testCase: TestCase) {
// Intercept API Responses
const apiResponseInterceptUtil = new ApiResponseInterceptUtil(testCase.testData);
page.on('response', apiResponseInterceptUtil.handleInterceptResponse);
// Destructure data for easy access
const { customerDetails, claimDetails, vehicleDetails, vehicleDamage,
appointmentDetails, isSafelite, endorsements,
partQuestions, paymentDetails, isNoComp, isItac, isRecalNotification,
isRecalWarning, servicePackage, hasStateLawPopup, otherVehiclesOnPolicy,
isSeparateApptsWarning, vehiclePartQuestions, editVehicleDetails,
hasMilitaryWarning, capabilityQuestions } = testCase.testData;
let { isPolicyFound } = testCase.testData; // Allow isPolicyFound to be re-assigned
// Destructure pages for easy access
const { welcomePage, duplicateCheckPage, policyHolderDetailsPage,
vehicleSelectionPage, vehicleDamagePage, coverageStatementPage,
providerPreferencePage, serviceLocationPage, servicePackagesPage,
schedulePage, contactDetailsPage, orderConfirmationPage, policyVehiclesPage,
endorsementsPage, vehicleLookupPage, partQuestionsPage, paymentMethodPage,
vehicleLookupAddressPage, vehicleLookupLicensePage, vinLookupPage,
bailoutPage, tpaSearchPage, tpaSubmitPage, tpaConfirmationPage,
vehiclePartQuestionsPage, capabilityQuestionsPage } = testCase.pages;
// Destructure bailout flags
const { isVehicleSelectBailout, isDoNotSeeMyShopBailout, isTpaNotEnabledBailout,
isRequestCallbackBailout, isHeavyTruckVehicleBailout, isPartsServiceErrorBailout,
isSafeliteNotTheProviderBailout, isVehicleLookupBailout, isPriceServiceErrorBailout } = testCase.testData.bailoutFlags || {};
const repairTypes: VehicleDamage[] = [
VehicleDamage.WindshieldOneChip,
VehicleDamage.WindshieldTwoChips,
VehicleDamage.WindshieldThreeChips
]
// Are we replacing or repairing?
const isReplace = !repairTypes.some(damageType => {
return vehicleDamage!.includes(damageType);
});
const hasEndorsements = endorsements && endorsements.length > 0;
await test.step('WelcomePage >> Populate Customer Details', async () => {
await welcomePage.populatePage(customerDetails!, claimDetails!, await welcomePage.hasCityInfo());
// mockApiResponse(page, 'location/api/v1/location/zip/43016', 'common', testCase.testData!.mockTesting || false);
mockApiResponse(page, 'location/api/v1/location/zip/36116', 'common',testCase.testData!.isMockTesting || false);
mockApiResponse(page, 'coverage/api/v1/coverage/policy-information', 'scenario1', testCase.testData!.isMockTesting || false);
await welcomePage.nextPage();
});
if (testCase.testData.isDuplicateClaim) {
// TODO: Scenarios where we want to resume from duplicate
// TODO: Dynamic for when we don't care if duplicate page appears
await test.step('DuplicateCheckPage >> Start New Claim', async () => {
await duplicateCheckPage.startNewClaim();
await duplicateCheckPage.nextPage();
});
}
if (isPolicyFound) {
await test.step('PolicyVehiclesPage >> Select vehicle', async () => {
// Validate other vehicles on policy
if (otherVehiclesOnPolicy && otherVehiclesOnPolicy.length > 0) {
for (const vehicle of otherVehiclesOnPolicy) {
await policyVehiclesPage.validateVehicleIsOnPolicy(vehicle);
}
}
// Select vehicle
await policyVehiclesPage.selectVehicle(vehicleDetails!);
await policyVehiclesPage.nextPage();
});
if (isHeavyTruckVehicleBailout) {
await test.step('BailoutPage >> Heavy Vehicle Bailout', async () => {
await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.HeavyTruckVehicle);
});
return;
}
if (hasEndorsements) {
await test.step('EndorsementsPage >> Select Endorsements', async () => {
await endorsementsPage.verifyEndorsements(endorsements);
await endorsementsPage.selectEndorsements(endorsements);
await endorsementsPage.nextPage();
});
}
} else {
await test.step('PolicyHolderDetailsPage >> Enter customer data', async () => {
await policyHolderDetailsPage.fillCustomerDetails(customerDetails!);
await policyHolderDetailsPage.nextPage();
});
if (isVehicleLookupBailout){
forceAPIError(page, '/vehicle/api/v1/vehicle/lookup')
}
await test.step('VehicleDetailsPage >> Select Vehicle', async () => {
await vehicleSelectionPage.selectVehicle(vehicleDetails!);
await vehicleSelectionPage.nextPage();
});
if (isVehicleLookupBailout) {
await test.step('BailoutPage >> Vehicle Lookup Bailout', async () => {
await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.VehicleLookupError);
});
return;
}
if (isHeavyTruckVehicleBailout) {
await test.step('BailoutPage >> Heavy Vehicle Bailout', async () => {
await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.HeavyTruckVehicle);
});
return;
}
}
if (editVehicleDetails) {
isPolicyFound = false; // Flow proceeds as unverified
testCase.testData.isPolicyFound = false;
await test.step('VehicleDamagePage >> Click Edit Vehicle', async () => {
await vehicleDamagePage.editVehicleButton.click();
});
await test.step('VehicleDetailsPage >> Select edited vehicle', async () => {
await test.step('VehicleDetailsPage >> Select Vehicle', async () => {
await vehicleSelectionPage.selectVehicle(editVehicleDetails);
await vehicleSelectionPage.nextPage();
});
});
}
await test.step('VehicleDamagePage >> Select Damage', async () => {
if (isSeparateApptsWarning) {
await vehicleDamagePage.checkSeparateApptsWarning();
}
await vehicleDamagePage.selectDamage(vehicleDamage!);
await vehicleDamagePage.nextPage();
});
if (isPartsServiceErrorBailout) {
await test.step('VehicleLookupPage >> Select Lookup Type', async () => {
await vehicleLookupPage.vehicleLookup(vehicleDetails!);
});
await test.step('VinLookupPage >> Lookup by VIN', async () => {
await vinLookupPage.enterVin(vehicleDetails!.vin!);
forceAPIError(page, '/parts/api/v1/parts')
await vinLookupPage.nextPage();
});
await test.step('BailoutPage >> Parts Service Error Bailout', async () => {
await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.PartsServiceError);
return;
});
return;
}
if (isReplace && !(isPolicyFound)) {
await test.step('VehicleLookupPage >> Select Lookup Type' + vehicleDetails?.vehicleLookupType, async () => {
await vehicleLookupPage.vehicleLookup(vehicleDetails!);
});
if (isVehicleSelectBailout) {
await test.step('BailoutPage >> Validate Bailout', async () => {
await vinLookupPage.enterVin(vehicleDetails!.vin!);
await vinLookupPage.triggerBailout();
await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.VehicleNotFound);
});
return;
}
switch (vehicleDetails!.vehicleLookupType!) {
case VehicleLookupType.Address:
await test.step('VehicleLookupAddressPage >> Lookup by address: ' + customerDetails!.address.street, async () => {
await vehicleLookupAddressPage.lookupVehicleByAddress(customerDetails!, vehicleDetails!);
await vehicleLookupAddressPage.nextPage();
});
break;
case VehicleLookupType.LicensePlateNumber:
await test.step('VehicleLookupLicensePage >> Lookup by license plate: ' + vehicleDetails!.licensePlateNumber, async () => {
await vehicleLookupLicensePage.enterPlateDetails(vehicleDetails!);
await vehicleLookupLicensePage.nextPage();
});
break;
case VehicleLookupType.Vin:
await test.step('VinLookupPage >> Lookup by VIN: ' + vehicleDetails!.vin!, async () => {
await vinLookupPage.enterVin(vehicleDetails!.vin!);
await vinLookupPage.nextPage();
});
break;
}
}
if (capabilityQuestions && capabilityQuestions.length > 0) {
await capabilityQuestionsPage.validatePartQuestions(capabilityQuestions);
await capabilityQuestionsPage.selectPartQuestionResponses(capabilityQuestions);
await capabilityQuestionsPage.nextPage();
}
if (partQuestions && partQuestions.length > 0) {
await partQuestionsPage.validatePartQuestions(partQuestions);
await partQuestionsPage.selectPartQuestionResponses(partQuestions);
await partQuestionsPage.nextPage();
}
if (vehiclePartQuestions && vehiclePartQuestions.length > 0) {
await vehiclePartQuestionsPage.validatePartQuestions(vehiclePartQuestions);
await vehiclePartQuestionsPage.selectPartQuestionResponses(vehiclePartQuestions);
await vehiclePartQuestionsPage.nextPage();
}
await test.step('CoverageStatementPage >> Next page', async () => {
// Confirm no coverage
if (isPolicyFound && (isItac || isNoComp)) {
await coverageStatementPage.continueToScheduleButton.click();
}
await coverageStatementPage.nextPage();
});
if (hasStateLawPopup) {
await test.step('ProviderPreferencePage >> Dismiss state law popup', async () => {
await providerPreferencePage.validateStateLawModalIsVisible();
await providerPreferencePage.gotItButton.click();
});
}
if (!isPolicyFound || !(isItac || isNoComp)) {
await test.step('ProviderPreferencePage >> Select Provider ' + isSafelite ? "Safelite" : "Other shops(Non-Safelite)", async () => {
await providerPreferencePage.selectProvider(isSafelite);
});
}
// validations for Recal warning mesage
if (isRecalWarning) {
await serviceLocationPage.validateRecalWarning();
}
// if isRecalNotifidation flag true additional step to acknowledge Recal notification.
if (isRecalNotification) {
await providerPreferencePage.acknowledgeRecalNotificaiton();
}
// If No-Comp or ITAC, ProviderPreferencePage does not appear
if (!isPolicyFound || !(isItac || isNoComp)) {
if (!isSafelite) {
if (testCase.testData!.clientTag == '05CC1609-3631-4044-B45A-E78E13343B9A') { // Avoiding TPA flow for Federated Insureance due to Defect# SSR-1984 //Temporary fix until Defect# SSR-1984 is addressed.
await test.step('***** Performing Safelite flow for Federal Insurance due to defec# SSR-1984 *****', async () => { });
await test.step('TpaSearchPage >> TPA Search', async () => {
await tpaSearchPage.selectFirstLocation();
// await tpaSearchPage.nextPage();
});
return;
}
if (isTpaNotEnabledBailout) {
await test.step('validateBailoutDetails >> Bailout code : ' + BailoutCode.TPANotEnabled, async () => {
await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.TPANotEnabled);
return;
});
return;
}
if (isDoNotSeeMyShopBailout) {
await test.step('TpaSearchPage >> Select "Do Not See My Shop"', async () => {
await tpaSearchPage.selectDoNotSeeMyShop();
});
await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.DoNotSeeMyShop);
return;
}
await test.step('TpaSearchPage >> TPA Search', async () => {
await tpaSearchPage.selectFirstLocation();
await tpaSearchPage.nextPage();
});
await test.step('TpaSubmitPage >> TPA Submit', async () => {
// TODO: Validations
await tpaSubmitPage.nextPage();
});
await test.step('TpaConfirmationPage >> TPA Confirmation', async () => {
await tpaConfirmationPage.validateSuccessMessage();
return;
});
return;
}
}
await test.step('ServiceLocationPage >> Select service location', async () => {
await serviceLocationPage.selectLocation(appointmentDetails!);
if (hasMilitaryWarning) {
await expect.soft(serviceLocationPage.militaryWarningMessage).toBeVisible();
}
await serviceLocationPage.nextPage();
});
await test.step('SchedulePage >> Select day and time', async () => {
customerDetails!.apptDate = await schedulePage.scheduleFirstAppointment(appointmentDetails!.serviceLocation);
});
await test.step('ContactDetailsPage >> Validate contact details', async () => {
const expectedContactDetails: Partial<ICustomerDetails> = {
firstName: customerDetails!.firstName,
lastName: customerDetails!.lastName,
email: customerDetails!.email,
phoneNumber: customerDetails!.phoneNumber
};
const actualContactDetails = await contactDetailsPage.getContactDetails();
expect.soft(actualContactDetails).toEqual(expectedContactDetails);
await contactDetailsPage.fillNotes(customerDetails!.notes);
if (isPriceServiceErrorBailout) {
forceAPIError(page, '/price/api/v1/price/combined-quote');
}
await contactDetailsPage.nextPage();
});
if (isPriceServiceErrorBailout) {
await test.step('BailoutPage >> Price Service Error Bailout', async () => {
await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.PricingResponseError);
});
return;
}
await test.step('ServicePackagesPage >> Choose service package', async () => {
customerDetails!.packagePrice = await servicePackagesPage.selectServicePackage(servicePackage!);
await servicePackagesPage.nextPage();
});
if (isPolicyFound && claimDetails!.policyDeductible > 0) {
await test.step('PaymentMethodPage >> Execute Payment', async () => {
await paymentMethodPage.executePayment(paymentDetails!);
await paymentMethodPage.nextPage();
});
} else {
await test.step('PaymentMethodPage >> Skip to Order Confirmation', async () => {
await paymentMethodPage.nextPage();
});
}
await test.step('OrderConfirmationPage >> Validate order', async () => {
await orderConfirmationPage.validateOrderConfirmationPage(testCase.testData);
});
}

View file

@ -0,0 +1,70 @@
import ClientData from "@business-logic/data/ClientData";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import TestCase from "@business-logic/types/TestCase";
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialReplaceStaticAdasData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isSafelite: true,
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Knoxville',
state: 'Tennessee',
postalCode: '37996',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2019',
make: 'Toyota',
model: 'C-HR',
style: '4 door hatchback',
vin: 'NMTKHMBX5KR086519',
vehicleLookupType: VehicleLookupType.Vin,
},
vehicleDamage: [
VehicleDamage.WindshieldCrack,
],
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialReplaceStaticAdasTests: TestCase[] = [];
for (const client of essentialClients) {
const data = { ...essentialReplaceStaticAdasData };
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0001 Essential Replace Statis ADAS Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Essentials','@0001', '@test_report'],
testData: data
}, undefined, '0001');
essentialReplaceStaticAdasTests.push(tc);
}
export default essentialReplaceStaticAdasTests;

View file

@ -0,0 +1,70 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialRepairInShopAcuraData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: true,
servicePackage: ServicePackage.Premium,
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Fort Collins',
state: 'Colorado',
postalCode: '80526',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2020',
make: 'Acura',
model: 'ILX',
style: '4 door sedan',
vin: '19UDE2F38LA001705'
},
vehicleDamage: [
VehicleDamage.WindshieldOneChip,
],
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialRepairInShopAcuraTests: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialRepairInShopAcuraData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0002 Essential Repair Mobile Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`],
testData: data
}, undefined, '0002');
essentialRepairInShopAcuraTests.push(tc);
}
export default essentialRepairInShopAcuraTests;

View file

@ -0,0 +1,81 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialTpaNotEnabledData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: false,
bailoutFlags: {
isTpaNotEnabledBailout: true
},
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Dublin',
state: 'Ohio',
postalCode: '43016',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2022',
make: 'Honda',
model: 'Civic',
style: '4 door hatchback'
},
vehicleDamage: [
VehicleDamage.WindshieldThreeChips,
// VehicleDamage.WindshieldCrack,
// VehicleDamage.DriverFrontDoor,
// VehicleDamage.DriverQuarterPanel,
// VehicleDamage.DriverRearDoor,
// VehicleDamage.PassengerFrontDoor,
// VehicleDamage.PassengerQuarterPanel,
// VehicleDamage.PassengerRearDoor,
// VehicleDamage.RearWindow
],
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '6826 Sawmill Rd, Columbus, OH 43235',
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClientsWithTpaDisabled();
const essentialTpaNotEnabledTestCases: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialTpaNotEnabledData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0003 Essential TPA Not Enabled Bailout Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Bailout', '@TpaNotEnabled', '@Essentials'],
testData: data
}, undefined, '0003');
essentialTpaNotEnabledTestCases.push(tc);
}
export default essentialTpaNotEnabledTestCases;

View file

@ -0,0 +1,83 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialTpaEnabledData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: false,
bailoutFlags: {
isTpaNotEnabledBailout: false
},
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
// phoneNumber: faker.phone.toString(),
notes: 'Automated Test',
address: {
// street: faker.location.streetAddress(),
street: '134 Woodlands Place',
city: 'Dublin',
state: 'Florida',
postalCode: '32040',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2022',
make: 'Honda',
model: 'Civic',
style: '4 door hatchback'
},
vehicleDamage: [
VehicleDamage.WindshieldThreeChips,
// VehicleDamage.WindshieldCrack,
// VehicleDamage.DriverFrontDoor,
// VehicleDamage.DriverQuarterPanel,
// VehicleDamage.DriverRearDoor,
// VehicleDamage.PassengerFrontDoor,
// VehicleDamage.PassengerQuarterPanel,
// VehicleDamage.PassengerRearDoor,
// VehicleDamage.RearWindow
],
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '6826 Sawmill Rd, Columbus, OH 43235',
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClientsWithTpaEnabled();
const essentialTpaEnabled_0004: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialTpaEnabledData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0004 Essential TPA Enabled Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@TpaEnabled', '@Essentials','@0004'],
testData: data
}, undefined, '0004');
essentialTpaEnabled_0004.push(tc);
}
export default essentialTpaEnabled_0004;

View file

@ -0,0 +1,78 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialReplaceDynamicAdasData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: true,
servicePackage: ServicePackage.Premium,
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Chicago',
state: 'Illinois',
postalCode: '60645',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2021',
make: 'BMW',
model: '740',
style: '4 door sedan',
vin: 'WBA7T2C01LGL17632',
vehicleLookupType: VehicleLookupType.Vin,
},
vehicleDamage: [
VehicleDamage.WindshieldCrack,
],
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
serviceAddress: {
street: "2088 Haviland Road, Columbus, OH, USA",
city:"Vermillion",
state: "Ohio",
postalCode: "44089",
country: "undefined"
},
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialReplaceDynamicAdasTests: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialReplaceDynamicAdasData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0005 Essential Replace Dynamic ADAS Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`],
testData: data
}, undefined, '0005');
essentialReplaceDynamicAdasTests.push(tc);
}
export default essentialReplaceDynamicAdasTests;

View file

@ -0,0 +1,77 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialRepairMobileHyundaiData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: true,
servicePackage: ServicePackage.Premium,
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Raleigh',
state: 'North Carolina',
postalCode: '27615',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2013',
make: 'Hyundai',
model: 'Sonata',
style: '4 door sedan',
vin: '5NPEC4AB6DH791034'
},
vehicleDamage: [
VehicleDamage.WindshieldTwoChips,
],
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
serviceAddress: {
street: "2088 Haviland Road, Columbus, OH, USA",
city:"Vermillion",
state: "Ohio",
postalCode: "44089",
country: "undefined"
},
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialRepairMobileHyundaiTests: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialRepairMobileHyundaiData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0007 Essential Repair Hyundai Mobile Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`],
testData: data
}, undefined, '0007');
essentialRepairMobileHyundaiTests.push(tc);
}
export default essentialRepairMobileHyundaiTests;

View file

@ -0,0 +1,81 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialHeavyVehicleBailoutData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: true,
bailoutFlags: {
isHeavyTruckVehicleBailout: true
},
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Dublin',
state: 'Ohio',
postalCode: '43016',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2017',
make: 'Freightliner',
model: '114sd',
style: 'conventional cab' // TODO: Check correctness of vehicle style
},
vehicleDamage: [
// VehicleDamage.WindshieldThreeChips,
VehicleDamage.WindshieldCrack,
// VehicleDamage.DriverFrontDoor,
// VehicleDamage.DriverQuarterPanel,
// VehicleDamage.DriverRearDoor,
// VehicleDamage.PassengerFrontDoor,
// VehicleDamage.PassengerQuarterPanel,
// VehicleDamage.PassengerRearDoor,
// VehicleDamage.RearWindow
],
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '6826 Sawmill Rd, Columbus, OH 43235',
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialHeavyVehicleBailoutTestCases: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialHeavyVehicleBailoutData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0008 Essential Heavy Vehicle Bailout Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Bailout', '@HeavyVehicle', '@Essentials'],
testData: data
}, undefined, '0008');
essentialHeavyVehicleBailoutTestCases.push(tc);
}
export default essentialHeavyVehicleBailoutTestCases;

View file

@ -0,0 +1,83 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialPartsServiceErrorData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: true,
bailoutFlags: {
isPartsServiceErrorBailout: true
},
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Tulare',
state: 'California',
postalCode: '93247',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2012',
make: 'Dodge',
model: 'Charger',
style: '4 door sedan',
vin: '2C3CDXBG6CH260654',
vehicleLookupType: VehicleLookupType.Vin
},
vehicleDamage: [
// VehicleDamage.WindshieldThreeChips,
VehicleDamage.WindshieldCrack,
// VehicleDamage.DriverFrontDoor,
// VehicleDamage.DriverQuarterPanel,
// VehicleDamage.DriverRearDoor,
// VehicleDamage.PassengerFrontDoor,
// VehicleDamage.PassengerQuarterPanel,
// VehicleDamage.PassengerRearDoor,
// VehicleDamage.RearWindow
],
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '6826 Sawmill Rd, Columbus, OH 43235',
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialPartsServiceErrorBailout_0009: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialPartsServiceErrorData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0009 Essential Parts Service Error Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Bailout', '@PartsServiceError', '@Essentials','@0009'],
testData: data
}, undefined, '0009');
essentialPartsServiceErrorBailout_0009.push(tc);
}
export default essentialPartsServiceErrorBailout_0009;

View file

@ -0,0 +1,83 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialVehicleNotFoundData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: true,
bailoutFlags: {
isVehicleSelectBailout: true
},
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Dublin',
state: 'Ohio',
postalCode: '43016',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2022',
make: 'Honda',
model: 'Civic',
style: '4 door hatchback',
vin: '0HGCR2E30FA099831',
vehicleLookupType: VehicleLookupType.Vin
},
vehicleDamage: [
// VehicleDamage.WindshieldThreeChips,
VehicleDamage.WindshieldCrack,
// VehicleDamage.DriverFrontDoor,
// VehicleDamage.DriverQuarterPanel,
// VehicleDamage.DriverRearDoor,
// VehicleDamage.PassengerFrontDoor,
// VehicleDamage.PassengerQuarterPanel,
// VehicleDamage.PassengerRearDoor,
// VehicleDamage.RearWindow
],
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '6826 Sawmill Rd, Columbus, OH 43235',
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialVehicleNotFoundTestCases: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialVehicleNotFoundData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0010 Essential Vehicle Not Found Bailout Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Bailout', '@VehicleNotFound'],
testData: data
}, undefined, '0001');
essentialVehicleNotFoundTestCases.push(tc);
}
export default essentialVehicleNotFoundTestCases;

View file

@ -0,0 +1,81 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialDoNotSeeShopData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: false,
bailoutFlags: {
isDoNotSeeMyShopBailout: true
},
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Dublin',
state: 'Ohio',
postalCode: '43016',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2022',
make: 'Honda',
model: 'Civic',
style: '4 door hatchback'
},
vehicleDamage: [
VehicleDamage.WindshieldThreeChips,
// VehicleDamage.WindshieldCrack,
// VehicleDamage.DriverFrontDoor,
// VehicleDamage.DriverQuarterPanel,
// VehicleDamage.DriverRearDoor,
// VehicleDamage.PassengerFrontDoor,
// VehicleDamage.PassengerQuarterPanel,
// VehicleDamage.PassengerRearDoor,
// VehicleDamage.RearWindow
],
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '6826 Sawmill Rd, Columbus, OH 43235',
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClientsWithTpaEnabled();
const essentialDoNotSeeShopTestCases: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialDoNotSeeShopData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0011 Essential DoNotSeeShop Bailout Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Bailout', '@DoNotSeeShop', '@Essentials'],
testData: data
}, undefined, '0011');
essentialDoNotSeeShopTestCases.push(tc);
}
export default essentialDoNotSeeShopTestCases;

View file

@ -0,0 +1,84 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, PartQuestionType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialUnqiqueGlassData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
isSafelite: true,
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Tacoma',
state: 'Washington',
postalCode: '98409',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2019',
make: 'Ram',
model: 'Promaster',
style: 'cargo van',
vin: '3C6TRVBG8KE566001',
vehicleLookupType: VehicleLookupType.Vin
},
vehicleDamage: [
VehicleDamage.WindshieldCrack,
VehicleDamage.DriverSlidingDoor
],
vehiclePartQuestions: [
{
partQuestionType: PartQuestionType.WindshieldColor,
isOnPage: true,
optionToSelect: 'Green Tint'
},
{
partQuestionType: PartQuestionType.DriverSideColor,
isOnPage: true,
optionToSelect: 'Green Tint',
secondaryQuestionOptionToSelect: 'solar, driver side, rear'
},
],
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialUniqueGlassTests: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialUnqiqueGlassData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0012 Essential Unique Glass Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`],
testData: data
}, undefined, '0012');
essentialUniqueGlassTests.push(tc);
}
export default essentialUniqueGlassTests;

View file

@ -0,0 +1,102 @@
import ClientData from "@business-logic/data/ClientData";
import { DamageType, PartQuestionType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import TestCase from "@business-logic/types/TestCase";
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialReplaceData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
vehiclePartQuestions: [
{
partQuestionType: PartQuestionType.WindshieldColor,
isOnPage: true,
optionToSelect: 'Green Tint'
},
{
partQuestionType: PartQuestionType.DriverFrontColor,
isOnPage: true,
optionToSelect: 'Green Tint'
},
{
partQuestionType: PartQuestionType.DriverRearColor,
isOnPage: true,
optionToSelect: 'Green Tint'
},
{
partQuestionType: PartQuestionType.PassengerFrontColor,
isOnPage: true,
optionToSelect: 'Green Tint'
},
{
partQuestionType: PartQuestionType.PassengerRearColor,
isOnPage: true,
optionToSelect: 'Green Tint'
},
],
isSafelite: true,
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: 'Reed',
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: '10212 JEWEL CT',// DO NOT use fake address here as this scenario search vehicle by address //faker.location.streetAddress(),
city: 'CONROE',
state: 'Texas',
postalCode: '77385',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2015',
make: 'Ford',
model: 'F Series F150',
style: '2 door super cab',
vin: '5N1AN0NW9BC524974',
vehicleLookupType: VehicleLookupType.Address,
},
vehicleDamage: [
VehicleDamage.WindshieldCrack,
VehicleDamage.DriverFrontDoor,
VehicleDamage.DriverRearDoor,
VehicleDamage.PassengerFrontDoor,
VehicleDamage.PassengerRearDoor,
],
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: undefined,
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialReplaceTestCases: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialReplaceData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0013 Essential Replace Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Essentials'],
testData: data
}, undefined, '0013');
essentialReplaceTestCases.push(tc);
}
export default essentialReplaceTestCases;

View file

@ -0,0 +1,77 @@
import ClientData from "@business-logic/data/ClientData";
import TestCase from "@business-logic/types/TestCase";
import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums";
import { ITestData } from "@business-logic/types/ITestData"
import { faker } from "@faker-js/faker";
import { getNextWeekday } from "@impl/utils/DateUtils";
const nextWeekday = getNextWeekday();
const essentialRepairMobileData: Partial<ITestData> = {
clientTag: 'ALL_ESSENTIAL',
isDuplicateClaim: false,
isPolicyFound: false,
endorsements: [],
isReplace: false,
partQuestions: undefined,
isSafelite: true,
servicePackage: faker.helpers.enumValue(ServicePackage),
customerDetails: {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: "itqatest@safelite.com",
phoneNumber: faker.helpers.fromRegExp(/[2-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]/),
notes: 'Automated Test',
address: {
street: faker.location.streetAddress(),
city: 'Dublin',
state: 'Ohio',
postalCode: '43016',
country: 'United States'
}
},
claimDetails: {
policyNumber: faker.string.alphanumeric(5),
policyDeductible: -1, // Not advanced, so we don't care about deductible.
damageDate: '2024-10-10',
damageCause: DamageType.Other
},
vehicleDetails: {
year: '2021',
make: 'Subaru',
model: 'Outback',
style: '4 door station wagon',
vin: '4S4BTAFC7M3163249'
},
vehicleDamage: [
VehicleDamage.WindshieldThreeChips,
],
appointmentDetails: {
serviceLocation: ServiceLocation.Mobile,
serviceAddress: {
street: "2088 Haviland Road, Columbus, OH, USA",
city:"Vermillion",
state: "Ohio",
postalCode: "44089",
country: "undefined"
},
appointmentDate: nextWeekday
}
}
const essentialClients = ClientData.getEssentialClients();
const essentialRepairMobileTests: TestCase[] = [];
for (const client of essentialClients) {
const data = {...essentialRepairMobileData};
data.clientTag = client.clientTag;
data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false
const tc = new TestCase({
name: `0014 Essential Repair Mobile Client: "${client.accountName}"`,
tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Essentials'],
testData: data
}, undefined, '0014');
essentialRepairMobileTests.push(tc);
}
export default essentialRepairMobileTests;

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