Merge remote-tracking branch 'origin/develop' into feature/kiener/INSR-6047

This commit is contained in:
scottkiener-at-safelite 2026-01-26 16:24:23 -05:00
commit 204fc0a9e2
82 changed files with 7631 additions and 3376 deletions

View file

@ -8,9 +8,18 @@ schedules:
- develop
pool: 'Default'
resources:
repositories:
- repository: AzureDevOps
type: github
name: Safelite/AzureDevOps
endpoint: Safelite
ref: refs/tags/t5.7.40
variables:
# - group: Digital-Infrastructure
# - group: ISS-BuildBranches
- group: SafelitePlaywright
- name: dockerImageName
value: 'playwright-tests'
- name: imageTag
@ -30,158 +39,17 @@ stages:
- stage: TestPr
displayName: Run Playwright Test
jobs:
- 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) or tests don't exist for this shard number!"
exit 0 # Suppress error. It will be visible in report.
fi
displayName: 'Run Playwright Tests - Shard $(shardNumber)'
- 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
dependsOn: playwright_tests
timeoutInMinutes: 8
cancelTimeoutInMinutes: 10
steps:
- task: DownloadPipelineArtifact@2
inputs:
targetPath: '$(System.DefaultWorkingDirectory)/playwright-reports'
- task: Docker@2
displayName: 'Build Docker Image'
inputs:
command: build
dockerfile: Dockerfile.playwright
repository: $(dockerImageName)
tags: $(imageTag)
arguments: '--no-cache --pull'
- bash: |
# Create container for jira writeback
container_id=$(docker create \
--ipc=host \
-e JIRA_SERVER=$(JIRA_SERVER) \
-e JIRA_USERNAME=$(JIRA_USERNAME) \
-e JIRA_API_KEY=$(JIRA_API_KEY) \
-e IS_REGRESSION=$(IS_REGRESSION) \
-e JIRA_BOARD_ID=$(JIRA_BOARD_ID) \
-e JIRA_EPIC_KEY=$(JIRA_EPIC_KEY) \
-e JIRA_PROJECT_KEY=$(JIRA_PROJECT_KEY) \
$(dockerImageName):$(imageTag) \
bash -c "echo \"Moving Playwright reports out of subfolders...\" &&
find ./playwright-reports/ -mindepth 2 -type f -exec mv {} ./playwright-reports/ \; &&
echo \"Merging reports...\" &&
PLAYWRIGHT_JUNIT_OUTPUT_DIR='/app/test-results' PLAYWRIGHT_JUNIT_OUTPUT_NAME='junit_results.xml' npx playwright merge-reports --reporter='playwright-tests/impl/reporter/JiraWritebackReporter.ts',junit ./playwright-reports
echo \"Contents of ortoni-report:\" && ls ./ortoni-report &&
echo 'Current dir: ' && pwd
echo 'Contents of current dir: ' && ls
echo 'Contents of /app/test-results' && ls /app/test-results ")
# Start container and stream logs
echo "Starting merge"
docker start -a $container_id
# Create directory for test results
echo "Creating test results directory..."
mkdir -p $(System.DefaultWorkingDirectory)/ortoni-report
mkdir -p $(System.DefaultWorkingDirectory)/test-results
# Copy test results from container
echo "Copying test results..."
docker cp $container_id:/app/ortoni-report/. $(System.DefaultWorkingDirectory)/ortoni-report
docker cp $container_id:/app/test-results/junit_results.xml $(System.DefaultWorkingDirectory)/test-results
# Remove container
echo "Cleaning up container..."
docker rm $container_id
env:
JIRA_API_KEY: $(JIRA_API_KEY)
displayName: merge_and_publish_results_to_jira
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
searchFolder: 'test-results'
testResultsFormat: 'JUnit'
testResultsFiles: 'junit_results.xml'
mergeTestResults: true
failTaskOnFailedTests: false
testRunTitle: 'Playwright Tests'
condition: succeededOrFailed()
- task: PublishPipelineArtifact@1
displayName: 'Publish Merged Report'
condition: always()
inputs:
targetPath: '$(System.DefaultWorkingDirectory)/ortoni-report'
artifact: 'playwright-merged-report'
publishLocation: 'pipeline'
- script: |
docker rmi $(dockerImageName):$(imageTag) -f
displayName: 'Cleanup Docker Image'
condition: always()
- template: templates/digital/playwright-test.yml@AzureDevOps
parameters:
applicationType: 'vue'
totalShards: ${{ variables.totalShards }}
targetUrl: $(BASE_URL)
dockerFileName: 'Dockerfile.playwright'
isRegression: ${{ variables.IS_REGRESSION }}
filterTags: '@Advanced'
playwrightTestsPath: 'playwright-tests'
npmServePath: '.'
npmrcPath: 'playwright-tests/.npmrc'
secrets:
CCIS_API_AUTH: $(CCIS_API_AUTH)
JIRA_API_KEY: $(JIRA_API_KEY)

199
package-lock.json generated
View file

@ -142,7 +142,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.0.tgz",
"integrity": "sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
@ -2374,7 +2373,8 @@
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
@ -3281,6 +3281,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"@gar/promisify": "^1.0.1",
"semver": "^7.3.5"
@ -3293,6 +3294,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
@ -3308,6 +3310,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"mkdirp": "^1.0.4",
"rimraf": "^3.0.2"
@ -3323,6 +3326,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
@ -3393,7 +3397,6 @@
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright": "1.56.1"
},
@ -3973,7 +3976,6 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.1.tgz",
"integrity": "sha512-P6iIPyYQ+qH8CvGauAqanhVnjrnRe0IZFSYCeGkSRW9q3u8bdVn2NPI+lasFyVsEQn1J/IFmp5Aax41+dAP9wg==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@ -4440,7 +4442,6 @@
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"peer": true,
"dependencies": {
"@types/linkify-it": "*",
"@types/mdurl": "*"
@ -4625,7 +4626,6 @@
"integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.23.0",
"@typescript-eslint/types": "8.23.0",
@ -5419,7 +5419,6 @@
"resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-5.0.8.tgz",
"integrity": "sha512-nV7tYQLe7YsTtzFrfOMIHc5N2hp5lHG2rpYr0aNja9rNljdgcPZLyQRb2YRivTHqTv7lI962UXFURcpStHgyFw==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/helper-compilation-targets": "^7.12.16",
"@soda/friendly-errors-webpack-plugin": "^1.8.0",
@ -5640,7 +5639,6 @@
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
"peer": true,
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
@ -5775,7 +5773,6 @@
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.3.4",
"@vue/shared": "3.3.4"
@ -5848,7 +5845,6 @@
"resolved": "https://registry.npmjs.org/@vue/vue3-jest/-/vue3-jest-27.0.0.tgz",
"integrity": "sha512-VL61CgZBoQqayXfzlZJHHpZuX4lsT8dmdZMJzADhdAJjKu26JBpypHr/2ppevxItljPiuALQW4MKhhCXZRXnLg==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
"chalk": "^2.1.0",
@ -6097,7 +6093,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
"integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -6201,6 +6196,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"humanize-ms": "^1.2.1"
},
@ -6215,6 +6211,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
@ -6228,7 +6225,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -6361,7 +6357,8 @@
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/arch": {
"version": "2.2.0",
@ -6400,6 +6397,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
@ -6599,7 +6597,6 @@
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@ -6663,7 +6660,6 @@
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz",
"integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==",
"dev": true,
"peer": true,
"dependencies": {
"@jest/transform": "^27.5.1",
"@jest/types": "^27.5.1",
@ -6984,6 +6980,7 @@
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
@ -7128,7 +7125,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
@ -7217,6 +7213,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"@npmcli/fs": "^1.0.0",
"@npmcli/move-file": "^1.0.1",
@ -7248,6 +7245,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
@ -7490,6 +7488,7 @@
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"dev": true,
"license": "ISC",
"peer": true,
"engines": {
"node": ">=10"
}
@ -7534,6 +7533,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
@ -7748,6 +7748,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"color-support": "bin.js"
}
@ -8090,7 +8091,8 @@
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/consolidate": {
"version": "0.15.1",
@ -8292,7 +8294,6 @@
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
"integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==",
"dev": true,
"peer": true,
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.7",
@ -8369,7 +8370,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -8642,6 +8642,7 @@
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mimic-response": "^3.1.0"
},
@ -8706,6 +8707,7 @@
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4.0.0"
}
@ -8922,7 +8924,8 @@
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/depd": {
"version": "2.0.0",
@ -8949,6 +8952,7 @@
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8"
}
@ -8969,10 +8973,11 @@
"dev": true
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
@ -9125,7 +9130,6 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
}
@ -9297,6 +9301,33 @@
"node": ">= 0.8"
}
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@ -9335,6 +9366,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
@ -9345,7 +9377,8 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/error-ex": {
"version": "1.3.2",
@ -9604,7 +9637,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@ -9736,7 +9768,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
"integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
"dev": true,
"peer": true,
"dependencies": {
"array-includes": "^3.1.4",
"array.prototype.flat": "^1.2.5",
@ -10363,6 +10394,7 @@
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"dev": true,
"license": "(MIT OR WTFPL)",
"peer": true,
"engines": {
"node": ">=6"
}
@ -10621,7 +10653,8 @@
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fill-range": {
"version": "7.1.1",
@ -10886,7 +10919,8 @@
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fs-extra": {
"version": "9.1.0",
@ -10909,6 +10943,7 @@
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -10986,6 +11021,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.3",
@ -11099,7 +11135,8 @@
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/glob": {
"version": "7.2.3",
@ -11291,7 +11328,8 @@
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/hash-sum": {
"version": "2.0.0",
@ -11476,7 +11514,8 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true
"optional": true,
"peer": true
},
"node_modules/http-deceiver": {
"version": "1.2.7",
@ -11587,6 +11626,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"ms": "^2.0.0"
}
@ -11731,7 +11771,8 @@
"integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/inflight": {
"version": "1.0.6",
@ -11776,6 +11817,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
@ -11790,7 +11832,8 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true
"optional": true,
"peer": true
},
"node_modules/ipaddr.js": {
"version": "2.0.1",
@ -12044,7 +12087,8 @@
"integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/is-map": {
"version": "2.0.2",
@ -12473,7 +12517,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
"dev": true,
"peer": true,
"dependencies": {
"@jest/core": "^27.5.1",
"import-local": "^3.0.2",
@ -15187,7 +15230,8 @@
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/jsdoc": {
"version": "4.0.2",
@ -15513,10 +15557,11 @@
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@ -15860,6 +15905,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"agentkeepalive": "^4.1.3",
"cacache": "^15.2.0",
@ -15889,6 +15935,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 6"
}
@ -15900,6 +15947,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@tootallnate/once": "1",
"agent-base": "6",
@ -16122,6 +16170,7 @@
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@ -16162,7 +16211,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -16258,6 +16306,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -16272,6 +16321,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.1.0",
"minipass-sized": "^1.0.3",
@ -16291,6 +16341,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -16305,6 +16356,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -16319,6 +16371,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"minipass": "^3.0.0"
},
@ -16332,6 +16385,7 @@
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
@ -16357,7 +16411,8 @@
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/module-alias": {
"version": "2.2.2",
@ -16428,7 +16483,8 @@
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/natural-compare": {
"version": "1.4.0",
@ -16473,6 +16529,7 @@
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"semver": "^7.3.5"
},
@ -16486,6 +16543,7 @@
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
@ -16498,7 +16556,8 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/node-fetch": {
"version": "2.6.7",
@ -16559,6 +16618,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"env-paths": "^2.2.0",
"glob": "^7.1.4",
@ -16585,6 +16645,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"abbrev": "1"
},
@ -16602,6 +16663,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
@ -16616,6 +16678,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"isexe": "^2.0.0"
},
@ -16724,6 +16787,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"are-we-there-yet": "^3.0.0",
"console-control-strings": "^1.1.0",
@ -17077,6 +17141,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"aggregate-error": "^3.0.0"
},
@ -17313,7 +17378,6 @@
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.4.tgz",
"integrity": "sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==",
"peer": true,
"dependencies": {
"@vue/devtools-api": "^6.5.0",
"vue-demi": ">=0.14.5"
@ -17468,7 +17532,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -18001,6 +18064,7 @@
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
@ -18116,7 +18180,8 @@
"integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/promise-retry": {
"version": "2.0.1",
@ -18125,6 +18190,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"err-code": "^2.0.2",
"retry": "^0.12.0"
@ -18140,6 +18206,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 4"
}
@ -18319,6 +18386,7 @@
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"peer": true,
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
@ -18335,6 +18403,7 @@
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -18783,7 +18852,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"dev": true,
"peer": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@ -19064,7 +19132,8 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true,
"license": "ISC",
"optional": true
"optional": true,
"peer": true
},
"node_modules/set-function-length": {
"version": "1.2.2",
@ -19252,7 +19321,8 @@
"url": "https://feross.org/support"
}
],
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/simple-get": {
"version": "4.0.1",
@ -19274,6 +19344,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
@ -19316,6 +19387,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
@ -19348,6 +19420,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"ip-address": "^9.0.5",
"smart-buffer": "^4.2.0"
@ -19364,6 +19437,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"agent-base": "^6.0.2",
"debug": "^4.3.3",
@ -19912,6 +19986,7 @@
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@ -19930,6 +20005,7 @@
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
@ -19942,7 +20018,8 @@
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true,
"license": "ISC"
"license": "ISC",
"peer": true
},
"node_modules/tar-fs/node_modules/tar-stream": {
"version": "2.2.0",
@ -19950,6 +20027,7 @@
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
@ -19979,6 +20057,7 @@
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"dev": true,
"license": "ISC",
"peer": true,
"engines": {
"node": ">=8"
}
@ -19989,6 +20068,7 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
@ -20225,7 +20305,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -20503,6 +20582,7 @@
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
@ -20558,7 +20638,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -20698,6 +20777,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"unique-slug": "^2.0.0"
}
@ -20709,6 +20789,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"imurmurhash": "^0.1.4"
}
@ -20872,7 +20953,6 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@ -21004,7 +21084,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -21018,7 +21097,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@ -21171,7 +21249,6 @@
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
@ -21482,7 +21559,6 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dev": true,
"peer": true,
"dependencies": {
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.12.1",
@ -21698,7 +21774,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -21807,7 +21882,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@ -21874,7 +21948,6 @@
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10.13.0"
}
@ -22047,6 +22120,7 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
@ -22182,7 +22256,6 @@
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},

3
playwright-tests/.npmrc Normal file
View file

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

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
{
"name": "safelite-iss-nextgen-playwright-tests",
"version": "1.0.0",
"description": "Playwright tests for ISS Nextgen using Safelite Playwright Core",
"directories": {
"test": "tests"
},
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:debug": "playwright test --debug",
"test:smoke": "playwright test --grep @smoke",
"test:report": "playwright test --grep @test_report",
"report": "playwright show-report",
"install": "playwright install",
"install:deps": "playwright install-deps"
},
"keywords": [],
"license": "ISC",
"dependencies": {
"axios": "^1.9.0",
"axios-retry": "^4.5.0",
"safelite-playwright-core": "^1.0.26"
},
"devDependencies": {
"@faker-js/faker": "^9.8.0",
"@playwright/test": "^1.52.0",
"@types/dotenv-safe": "^8.1.6",
"@types/node": "^22.15.32",
"dotenv-safe": "^9.1.0",
"eslint": "^9.28.0",
"luxon": "^3.6.1",
"ortoni-report": "^3.0.5",
"playwright-jira-reporter": "^1.0.16"
},
"private": true
}

View file

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

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
<path fill="#E86421" fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
</svg>

After

Width:  |  Height:  |  Size: 474 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="9" viewBox="0 0 15 9">
<path fill="#E86421" fill-rule="nonzero" d="M7.5 0a.806.806 0 0 0-.593.265L.246 7.455a.957.957 0 0 0 0 1.28.796.796 0 0 0 1.185 0l6.07-6.55 6.068 6.55a.796.796 0 0 0 1.186 0 .957.957 0 0 0 0-1.28L8.093.265A.806.806 0 0 0 7.5 0"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View file

@ -1,9 +0,0 @@
const buttonVariants = Object.freeze({
primary: 'primary',
secondary: 'secondary',
success: 'success',
link: 'link',
navigation: 'navigation'
});
export default buttonVariants;

View file

@ -0,0 +1,15 @@
export const buttonVariants = Object.freeze({
primary: 'primary',
secondary: 'secondary',
success: 'success',
link: 'link',
navigation: 'navigation'
});
export const dropdownVariants = Object.freeze({
primary: 'primary',
compact: 'compact'
});
export const modalPositions = Object.freeze({
center: 'center',
edge: 'edge'
});

View file

@ -23,13 +23,18 @@ const errorMessages = Object.freeze({
LAST_NAME_REQUIRED: 'Please enter your last name',
EMAIL_ADDRESS_REQUIRED: 'Email is required.',
EMAIL_ADDRESS_FORMAT: 'Please enter a valid email address.',
SERVICE_ZIP_REQUIRED: 'Please enter your service ZIP',
SERVICE_ZIP_FORMAT: 'Please enter a valid service ZIP',
SERVICE_ZIP_REQUIRED: 'ZIP code is required',
SERVICE_ZIP_FORMAT: 'Invalid ZIP, please enter a new ZIP (5 characters)',
MOBILE_SERVICE_ZIP_FORMAT: 'Invalid ZIP code',
NO_SERVICE_IN_AREA: (area) => `We're sorry, but we don't offer service in ${area} at this time.`,
PROVIDER_REQUIRED: 'Please select a provider.',
INVALID_ZIP: 'The ZIP you entered was invalid. Please enter a valid ZIP.',
ZIP_CODE_NOT_SERVICED_FOR_VEHICLE: 'We do not currently offer glass service for your vehicle in this ZIP code. Please try another ZIP code.',
VIN_REQUIRED: 'Please enter your VIN',
VIN_FORMAT:
// eslint-disable-next-line max-len
'Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q',
OPTION_REQUIRED: 'Please choose an option',
OPTION_REQUIRED: 'Please select an option',
VEHICLE_REQUIRED: 'Please select a vehicle',
POLICY_NUMBER_REQUIRED: 'Policy number is required.',
POLICY_NUMBER_FORMAT: 'Please enter an alpha-numeric string',
@ -59,7 +64,8 @@ const errorMessages = Object.freeze({
MODEL_REQUIRED: 'Vehicle model is required.',
STYLE_REQUIRED: 'Vehicle style is required.',
MOBILE_LOCATION_REQUIRED: 'Please enter your service address',
DATE_REQUIRED: 'Please select a date'
DATE_REQUIRED: 'Please select a date',
TIME_REQUIRED: 'Please select an appointment time.'
});
export default errorMessages;

View file

@ -6,10 +6,10 @@ export const pageProgressMapper = {
percent: 15
},
'policy-vehicles': {
percent: 15
percent: 20
},
'policy-endorsements': {
percent: 15
percent: 20
},
'vehicle-selection': {
percent: 20
@ -71,6 +71,9 @@ export const pageProgressMapper = {
'tpa-search': {
percent: 85
},
'tpa-submit': {
percent: 95
},
'tpa-confirmation': {
percent: 100
},

View file

@ -294,6 +294,11 @@ export default {
this.resetField({
value: newValue
});
},
answers() {
this.resetField({
value: this.modelValue
});
}
},
beforeMount() {

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,9 @@
<div
class="dropdown-question"
:class="{
'has-error' : (meta.touched && errors && errors.length && !isDisabled) || hasError,
'has-success': (meta.touched && (!errors || !errors.length) && !isDisabled)
'has-error' : (meta?.touched && errors && errors.length && !isDisabled) || hasError,
'has-success': (meta?.touched && (!errors || !errors.length) && !isDisabled),
[`dropdown-${variant}`]: true
}">
<label
:for="inputId"
@ -34,7 +35,7 @@
</option>
</select>
<div
v-show="meta.touched && errorMessage && !isDisabled"
v-show="meta?.touched && errorMessage && !isDisabled"
class="row mt-1 form-test-error">
<span role="alert">{{ errorMessage }}</span>
</div>
@ -42,6 +43,7 @@
</template>
<script>
import { dropdownVariants } from '@/constants/component-variants';
import { useField } from 'vee-validate';
export default {
@ -59,7 +61,11 @@ export default {
validationRules: String,
cmsWidgetName: String,
hasError: Boolean,
placeHolderText: String
placeHolderText: String,
variant: {
type: String,
default: dropdownVariants.primary
}
},
emits: ['update:modelValue'],
setup(props) {
@ -140,7 +146,7 @@ export default {
background-size: 0.625rem 1rem;
color: $gray-600;
border-radius: $border-radius;
min-height: 3rem;
min-height: 2.8125rem;
letter-spacing: inherit;
box-shadow: $box-shadow-input;
&:focus,
@ -154,5 +160,30 @@ export default {
filter: grayscale(100%);
}
}
&.dropdown-compact {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 1.25rem;
.form-label {
color: $darker-gray;
font-weight: $font-weight-light;
margin: 0;
}
.form-select {
color: $heritage-blue-secondary;
background-position-x: 98%;
padding: 0;
padding-left: .3125rem;
width: 5.625rem;
min-height: auto;
border: none;
box-shadow: none;
&:focus,
&:focus-visible {
border: .125rem solid $black;
}
}
}
}
</style>

View file

@ -13,19 +13,19 @@
}">
<div class="modal-dialog"
:class="{
'modal-dialog-edge': modalPosition === 'edge',
'modal-dialog-centered': modalPosition === 'center'
'modal-dialog-edge': modalPosition === modalPositions.edge,
'modal-dialog-centered': modalPosition === modalPositions.center
}">
<div
class="modal-content"
role="dialog"
aria-modal="true">
<div class="modal-header mb-2 mt-2">
<label
<span
v-if="headerText"
class="modal-title d-flex justify-content-center pb-0 w-100">
{{ headerText }}
</label>
class="modal-title d-flex justify-content-center pb-0 w-100"
:innerHTML="headerText">
</span>
<button
ref="closeButton"
type="button"
@ -60,6 +60,7 @@
import ButtonMain from '@/ux-components/button-main/button-main.vue';
import { Modal } from 'bootstrap';
import { useForm } from 'vee-validate';
import { modalPositions } from '@/constants/component-variants';
export default {
// eslint-disable-next-line vue/multi-word-component-names
@ -84,7 +85,11 @@ export default {
},
modalPosition: {
type: String,
default: 'edge'
default: modalPositions.edge
},
allowInvalidSubmit: {
type: Boolean,
default: false
}
},
emits: ['footer-button-event', 'isModalOpened'],
@ -102,7 +107,8 @@ export default {
modalId,
meta,
validate,
resetForm
resetForm,
modalPositions
};
},
computed: {
@ -121,7 +127,7 @@ export default {
methods: {
async validateAndEmit() {
const validationResult = await this.validate();
if (validationResult.valid) {
if (validationResult.valid || this.allowInvalidSubmit) {
this.$emit('footer-button-event');
}
},
@ -257,6 +263,9 @@ export default {
border-radius: 1.5rem;
overflow: auto;
}
.modal-header {
justify-content: center;
}
}
}
&.show .modal-dialog {

View file

@ -51,7 +51,7 @@ export default {
props: {
customText: String, // used to allow the insert of token values into textblock
justifyText: String, // left, right, center
typeStyle: String, // h1-h6, body, small, label, caption
typeStyle: String, // h1-h6, body, small, label, caption, disclaimer
// (see Figma or Confluence documentation)
fontWeight: String, // bold=500, default is 400
cmsWidgetName: String,
@ -109,5 +109,12 @@ export default {
&.dark {
color: $black;
}
&.disclaimer {
color: $lighter-gray;
font-weight: $font-weight-light;
font-size: .8125rem;
font-style: italic;
margin-top: .625rem;
}
}
</style>

View file

@ -16,7 +16,8 @@
class="input-wrapper"
:class="[
includeSearchIcon ? 'has-search-icon' : '',
includeSelectIcon ? 'has-select-icon' : ''
includeSelectIcon ? 'has-select-icon' : '',
hasTextButton ? 'has-text-button' : ''
]">
<input
:id="inputId"
@ -61,6 +62,13 @@
data-bs-toggle="modal"
:data-bs-target="'#' + cmsWidgetName"
aria-label="Select button" />
<buttonMain
v-if="hasTextButton"
ref="buttonMain"
variant="primary"
:buttonText="buttonText"
@clickEvent="clickedTextButton">
</buttonMain>
</div>
<div
v-show="errorMessage"
@ -76,9 +84,13 @@
<script>
import { useField, validate } from 'vee-validate';
import { extractSpanText } from '@/helpers/text-helper';
import buttonMain from '@/ux-components/button-main/button-main.vue';
export default {
name: 'textbox-question',
components: {
buttonMain
},
props: {
type: {
type: String,
@ -118,7 +130,11 @@ export default {
includeSelectIcon: Boolean,
min: String,
max: String,
disableAutoFill: Boolean
disableAutoFill: Boolean,
buttonCallback: {
type: Function,
default: () => {}
}
},
emits: ['focus', 'update:modelValue', 'textboxQuestionEvent.inputIdAssigned', 'click-event'],
setup(props) {
@ -161,6 +177,12 @@ export default {
questionTextForAria() {
return extractSpanText(this.getCmsContent(this.cmsWidgetName, 'QuestionText'));
},
buttonText() {
return this.getCmsContent(this.cmsWidgetName, 'ButtonText');
},
hasTextButton() {
return this.buttonText && this.buttonText.length > 0;
},
value: {
get() {
return this.modelValue;
@ -227,6 +249,13 @@ export default {
if (this.meta.valid) {
this.$emit('click-event');
}
},
async clickedTextButton() {
const result = await validate(this.value, this.validationRules);
if (result.valid) {
this.handleChange(this.value);
this.$emit('click-event');
}
}
}
};
@ -273,7 +302,7 @@ input::-webkit-date-and-time-value {
color: #4d5151;
}
.form-test-error span {
color: #d4281c;
color: $red;
font-size: 1rem;
font-weight: 400;
}
@ -318,6 +347,19 @@ input::-webkit-date-and-time-value {
display: flex;
}
}
&.has-text-button {
display: flex;
input[type='text'] {
border-radius: 1.5rem;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
min-width: 6.875rem;
}
}
}
input {
&.has-icon {
@ -338,8 +380,8 @@ input::-webkit-date-and-time-value {
.form-control {
border: $border-input;
border-radius: $border-radius;
min-height: 3rem;
max-height: 3rem;
min-height: 2.8125rem;
max-height: 2.8125rem;
padding: 0.75rem 1rem;
letter-spacing: inherit;
box-shadow: $box-shadow-input;

View file

@ -17,6 +17,21 @@ export function convertDateToDateString(date) {
);
}
export function convertDateToTwoDigitDay(date) {
if (date instanceof Date !== true) return null;
return (`0${date.getDate()}`).slice(-2);
}
export function convertDateToTwoDigitMonth(date) {
if (date instanceof Date !== true) return null;
return (`0${date.getMonth() + 1}`).slice(-2);
}
export function convertDateToShortMonth(date) {
if (date instanceof Date !== true) return null;
return date.toLocaleString('en-US', { month: 'short' });
}
export function convertDateStringToDate(dateString) {
// dateString must be YYYY-MM-DD format
if (typeof dateString !== 'string') return null;
@ -51,6 +66,12 @@ export function getDisplayTextForDurationLength(durationMinimum, durationMaximum
return `${durationText} ${unitText}`;
}
export function isAfternoon(timeString) {
if (typeof timeString !== 'string') return false;
const hours = parseInt(timeString.split(':')[0], 10);
return hours >= 12;
}
export function militaryToTwelveHourTime(timeString) {
// Expected input: "HH:MM"
if (typeof timeString !== 'string') return null;
@ -163,9 +184,11 @@ export function combineDateAndTime(date, time) {
// Return the new date object
return newDate;
}
export function addMinutes(date, minutes) {
return new Date(date.getTime() + minutes * 60000);
}
export function shortTimeString(date) {
// Use a ternary operator to check if the input is a valid date object
return date instanceof Date

View file

@ -86,7 +86,7 @@ function defineGlobalPhoneNumberRules() {
function defineGlobalExtensionRules() {
defineRule(
globalRules.EXTENSION_FORMAT,
regex(/^[0-9]{5}$/, errorMessages.EXTENSION_FORMAT)
regex(/^[0-9]{1,5}$/, errorMessages.EXTENSION_FORMAT)
);
}

View file

@ -5,6 +5,7 @@ function processZipCodeResults(request) {
containsMilitaryBase: serviceZipValidationResponse.containsMilitaryBase,
isValid: serviceZipValidationResponse.isValid,
isServiceable: serviceZipValidationResponse.isServiceable,
city: serviceZipValidationResponse.city,
state: serviceZipValidationResponse.state,
zipCodeCtu: serviceZipValidationResponse.zipCodeCtu
})).catch(() => ({

View file

@ -34,11 +34,8 @@ export function createOrderedListFromStringOfParagraphs(stringOfParagraphs) {
* @returns {string}
*/
export function toTitleCase(text) {
const temp = text?.toLowerCase()?.split(' ') ?? [];
for (let i = 0; i < temp.length; i++) {
temp[i] = temp[i].charAt(0).toUpperCase() + temp[i].slice(1);
}
return temp.join(' ');
let temp = text?.toLowerCase() ?? '';
return temp.replace(/(^|\s|-)\S/g, (letter) => letter.toUpperCase());
}
/**

View file

@ -12,8 +12,7 @@
hasIcon
disableAutoFill
validationRules="street-address-required"
@keydown.enter.prevent
class="mb-4" />
@keydown.enter.prevent />
</div>
</div>
<transition
@ -29,24 +28,22 @@
v-model="addressModel.streetAddress2"
cmsWidgetName="StreetAddress2QuestionWidget"
aria-haspopup=""
inputId="streetAddress2Field"
class="mb-4" />
inputId="streetAddress2Field" />
</div>
</div>
<div
class="row"
aria-live="polite">
<div class="col">
<div class="col-lg-6">
<textboxQuestion
ref="city"
v-model="addressModel.city"
cmsWidgetName="CityQuestionWidget"
inputId="cbf28188fdf2436688fd735915f7ee56"
disableAutoFill
validationRules="city-required"
class="mb-4" />
validationRules="city-required" />
</div>
<div class="col">
<div class="col-lg-6">
<dropdownQuestion
ref="state"
v-model="addressModel.state"
@ -55,8 +52,7 @@
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
validationRules="state-required"
class="mb-4" />
validationRules="state-required" />
</div>
</div>
<div
@ -70,8 +66,7 @@
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
validationRules="zip-code-required|zip-code-format"
class="mb-4" />
validationRules="zip-code-required|zip-code-format" />
</div>
</div>
</div>
@ -362,3 +357,12 @@ export default {
}
};
</script>
<style scoped lang="scss">
.textbox-question {
margin-bottom: 1.25rem;
}
.dropdown-question {
margin-bottom: 1.25rem;
}
</style>

View file

@ -38,7 +38,7 @@
<a
href=""
target="_blank"
@click="openCookiePreferences">Cookie Preferences</a>
@click="openCookiePreferences">Cookie preferences</a>
</div>
<div class="footer-menu-item">
<textLink

View file

@ -314,7 +314,6 @@ describe('Google Map', () => {
// Arrange
const wrapper = shallowMount(googleMap, {});
await awaitingSetupTicks(wrapper);
const numberOfExtendCallsDuringMount = 2;
mockGeocode.mockImplementation(() => {
const result = {
@ -329,7 +328,7 @@ describe('Google Map', () => {
const result = wrapper.vm.getBounds(locations);
// Assert
expect(mockExtend).toHaveBeenCalledTimes(numberOfExtendCallsDuringMount + numberOfExtendCallsPostMount);
expect(mockExtend).toHaveBeenCalledTimes(numberOfExtendCallsPostMount);
expect(result).toBe(mockLatLngBound);
}
);

View file

@ -55,7 +55,7 @@ export default {
const { AdvancedMarkerElement, PinElement } = await window.google.maps.importLibrary('marker');
markers?.forEach((marker, index) => {
const pinElement = new PinElement({
glyph: String.fromCharCode('A'.charCodeAt(0) + index),
glyphText: String.fromCharCode('A'.charCodeAt(0) + index),
glyphColor: '#000000',
borderColor: '#000000'
});
@ -107,9 +107,11 @@ export default {
async getBoundsFromAddress(address) {
await this.setGeocoder();
const result = await this.geocoder.geocode({ address });
return result?.results?.length > 0
? result.results[0].geometry?.bounds
: null;
if (result?.results?.length > 0) {
return result.results[0].geometry?.bounds ?? null;
} else {
return null;
}
},
async createMapWithMarkersForAddresses(markers) {
const markerPositions = await this.getLocationsFromAddresses(markers);
@ -117,9 +119,12 @@ export default {
const map = await this.getMap(zipBounds?.getCenter());
await this.addMarkersToMap(map, markerPositions);
let positionsToDisplay = markerPositions.map((marker) => marker.position);
if(zipBounds) {
positionsToDisplay.push(zipBounds.getNorthEast());
positionsToDisplay.push(zipBounds.getSouthWest());
}
const positionsToDisplay = markerPositions.map((marker) => marker.position)
.concat(zipBounds?.getNorthEast(), zipBounds?.getSouthWest());
const bounds = this.getBounds(positionsToDisplay);
map.fitBounds(bounds);
}

View file

@ -2,13 +2,13 @@
exports[`Shop list button should render correctly with all relevant props 1`] = `
<transition-stub name="fade" mode="out-in" appear="false" persisted="false" css="true" selectedvalue="selected value">
<base-input-button-stub modelvalue="value of modal" groupname="name of group" buttonwrapperclasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2" ismultiselect="false" validationrules="" isrequired="true" selectinginitiatesload="false" suppresserror="false" buttonlabel="Label of button" buttonlabelsubcopy="Sub copy of button" buttonbodycopy="button body copy" screenreaderonlytext="screen reader only text" additionalbuttondata="[object Object]" alttext="" iswide="false" value="1234"></base-input-button-stub>
<base-input-button-stub modelvalue="value of modal" groupname="name of group" buttonwrapperclasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover mb-2" ismultiselect="false" validationrules="" isrequired="true" selectinginitiatesload="false" suppresserror="false" buttonlabel="Label of button" buttonlabelsubcopy="Sub copy of button" buttonbodycopy="button body copy" screenreaderonlytext="screen reader only text" additionalbuttondata="[object Object]" alttext="" iswide="false" value="1234"></base-input-button-stub>
</transition-stub>
`;
exports[`Shop list button should render correctly with required props 1`] = `
<transition-stub name="fade" mode="out-in" appear="false" persisted="false" css="true">
<base-input-button-stub modelvalue="value of modal" groupname="name of group" buttonwrapperclasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2" ismultiselect="false" validationrules="" isrequired="true" selectinginitiatesload="false" suppresserror="false" alttext="" iswide="false" value="1234"></base-input-button-stub>
<base-input-button-stub modelvalue="value of modal" groupname="name of group" buttonwrapperclasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover mb-2" ismultiselect="false" validationrules="" isrequired="true" selectinginitiatesload="false" suppresserror="false" alttext="" iswide="false" value="1234"></base-input-button-stub>
</transition-stub>
`;

View file

@ -149,10 +149,9 @@ describe('Shop list button', () => {
// Assert
expect(buttonLabelSubCopy.exists()).toBeTruthy();
expect(buttonLabelSubCopy.classes().length).toBe(4);
expect(buttonLabelSubCopy.classes().length).toBe(3);
expect(buttonLabelSubCopy.classes()).toContain('m-0');
expect(buttonLabelSubCopy.classes()).toContain('caption');
expect(buttonLabelSubCopy.classes()).toContain('ms-2');
expect(buttonLabelSubCopy.classes()).toContain('button-label-sub-copy');
expect(buttonLabelSubCopy.classes()).toContain(textPosition);
});
test('when "textPosition" prop not provided', async () => {
@ -173,10 +172,9 @@ describe('Shop list button', () => {
// Assert
expect(buttonLabelSubCopy.exists()).toBeTruthy();
expect(buttonLabelSubCopy.classes().length).toBe(3);
expect(buttonLabelSubCopy.classes().length).toBe(2);
expect(buttonLabelSubCopy.classes()).toContain('m-0');
expect(buttonLabelSubCopy.classes()).toContain('caption');
expect(buttonLabelSubCopy.classes()).toContain('ms-2');
expect(buttonLabelSubCopy.classes()).toContain('button-label-sub-copy');
});
});
describe('availability indicator block with expected when displayAvailabilityIndicators true', () => {
@ -378,8 +376,7 @@ describe('Shop list button', () => {
// Assert
expect(buttonBodyCopy.exists()).toBeTruthy();
expect(buttonBodyCopy.classes()).toContain('m-0');
expect(buttonBodyCopy.classes()).toContain('button-label-sub-copy');
expect(buttonBodyCopy.classes()).toContain('small');
expect(buttonBodyCopy.classes()).toContain('button-body-copy');
});
test('screen reader only text when screenReaderOnlyText provided', async () => {
// Arrange

View file

@ -5,35 +5,39 @@
<baseInputButton
v-bind="$props"
v-model="selectedValue"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2">
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover mb-2">
<div
:aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
<div class="row-one">
<div class="availability-indicator-spacer"></div>
<span
id="buttonLabelSpan"
class="m-0 button-label-copy"
:class="textPosition">{{ buttonLabel }}
</span>
<span class="separator"></span>
<span
id="buttonLabelSubCopySpan"
class="m-0 caption ms-2"
class="m-0 button-label-sub-copy"
:class="textPosition">{{ buttonLabelSubCopy }}
</span>
<div
v-if="displayAvailabilityIndicators"
id="availabilityIndicatorBlock"
class="availability-indicator rounded-pill"
:class="availabilityRatingClass">
<div class="availability-indicator-container">
<div
id="availabilityIndicator"
class="d-flex align-items-center">
v-if="displayAvailabilityIndicators"
id="availabilityIndicatorBlock"
class="availability-indicator rounded-pill"
:class="availabilityRatingClass">
<div
id="availabilityBadge"
class="availability-badge"
:class="availabilityRating == 'high' ? 'green' : 'orange'">
id="availabilityIndicator"
class="d-flex align-items-center">
<div
id="availabilityBadge"
class="availability-badge"
:class="availabilityRatingClass">
</div>
<span class="m-0 button-auxillary-copy">{{ badgeText }}</span>
</div>
<span class="m-0 button-auxillary-copy">{{ badgeText }}</span>
</div>
</div>
</div>
@ -41,7 +45,7 @@
<span
v-if="buttonBodyCopy"
id="buttonBodyCopy"
class="m-0 button-label-sub-copy small"
class="m-0 button-body-copy"
v-html="buttonBodyCopy"></span>
</div>
<span
@ -103,116 +107,128 @@ export default {
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
$heritage-border-radius: 60px;
$heritage-border-color: #CACBCC;
$heritage-box-shadow: 0 1px 5px rgba($black, 0.2);
$heritage-checked-background-color: #e7f1f6;
$heritage-checked-border-color: #0070d1;
.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:focus-visible + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content p,
&:checked + .list-button-content span {
font-weight: 500;
}
}
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:checked + .list-button-content {
background: $heritage-checked-background-color;
border-color: $heritage-checked-border-color;
box-shadow: 0 0 0 1px $blue;
.button-label-copy {
font-weight: 500;
color: $black;
}
}
&:focus:checked + .list-button-content {
border-width: 2.5px;
}
}
}
.list-button-content {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
outline: none;
span {
&.small {
font-size: 0.75rem;
color: $gray-550;
}
}
color: $darker-gray;
font-weight: $font-weight-normal;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $heritage-border-radius;
border: 1px solid $heritage-border-color;
box-shadow: $heritage-box-shadow;
width: 100%;
outline: none;
}
.button-content {
row-gap: 0.25rem;
row-gap: 0.25rem;
.row-one {
display: flex;
align-items: center;
line-height: 1.5rem;
.row-one {
display: flex;
align-items: center;
justify-content: center;
line-height: 1.5rem;
.button-label-copy {
font-weight: 500;
}
.separator {
height: 1.25rem;
margin: 0rem .3125rem;
border-left: 1px solid $heritage-border-color;
}
.availability-indicator {
background-repeat: no-repeat;
display: flex;
align-items: center;
margin-left: auto;
padding: 0.125rem 0.5rem;
.availability-indicator-spacer {
width: 8rem;
margin-right: auto;
}
.availability-badge {
display: inline-flex;
width: 13px;
height: 12px;
background-position: center;
background-repeat: no-repeat;
margin: 0 0.25rem 0 0;
.availability-indicator-container {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
width: 8rem;
}
&.green {
background-image: url($svg-shop-list-button-green-availability);
}
.availability-indicator {
background-repeat: no-repeat;
display: flex;
align-items: center;
padding: 0.125rem 0.5rem;
&.orange {
background-image: url($svg-shop-list-button-orange-availability);
}
}
.button-auxillary-copy {
box-sizing: border-box;
justify-content: right;
line-height: 1.25rem;
font-weight: 500;
font-size: 0.75rem;
align-items: center;
}
.availability-badge {
display: inline-flex;
width: 13px;
height: 12px;
background-position: center;
background-repeat: no-repeat;
margin: 0 0.25rem 0 0;
&.green {
color: $green-700;
background-color: $green-100;
}
&.green {
background-image: url($svg-shop-list-button-green-availability);
}
&.orange {
color: $orange-600;
background-color: $orange-100;
}
&.orange {
background-image: url($svg-shop-list-button-orange-availability);
}
&.gray {
color: $gray-600;
background-color: $gray-100;
padding-left: 0.125rem;
padding-right: 0.125rem;
}
}
}
&.gray {
background-image: url(~@/assets/img/heritage-loader-blue.gif);
background-size: contain;
}
}
.button-auxillary-copy {
box-sizing: border-box;
justify-content: right;
line-height: 1.25rem;
font-weight: 500;
font-size: 0.75rem;
align-items: center;
}
.row-two {
text-align: left;
}
&.green {
color: $green-700;
background-color: $green-100;
}
&.orange {
color: $orange-600;
background-color: $orange-100;
}
&.gray {
color: $gray-600;
background-color: $gray-100;
padding-left: 0.125rem;
padding-right: 0.125rem;
}
}
}
.row-two {
text-align: center;
}
}
</style>

View file

@ -24,8 +24,6 @@
"
:aria-disabled="disableForwardAction || isForwardActionDisabled"
:isDisabled="disableForwardAction || isForwardActionDisabled"
data-bs-target="#footerModal"
data-bs-dismiss="modal"
data-test-id="site-footer-main-button"
@clickEvent="buttonClick" />
</div>
@ -36,8 +34,6 @@
linkType="navigation"
:text="backLink"
href="javascript:void(0)"
data-bs-target="#footerModal"
data-bs-dismiss="modal"
data-test-id="site-footer-back-button"
@clickEvent="linkClick" />
</div>

View file

@ -1,50 +0,0 @@
import { mount, shallowMount } from '@vue/test-utils';
import menuModal from '@/iss-components/site-header/menu-modal/menu-modal.vue';
describe('menu-modal.vue', () => {
it('Should return text Footer Navigation', async () => {
// Act
const wrapper = shallowMount(menuModal);
// Assert
const footerModalLabel = wrapper.find('h5');
// Expect
expect(footerModalLabel.text()).toContain('Footer Navigation');
});
it('Should return footer text as Safelite Group', async () => {
// Act
const wrapper = shallowMount(menuModal);
// Assert
const modalFooter = wrapper.find('div.modal-footer');
// Expect
expect(modalFooter.text()).toContain('Safelite Group');
});
it('Should return Terms of use text link text', async () => {
// Act
const wrapper = shallowMount(menuModal);
// Expect
expect(wrapper.html()).toContain('Terms of service');
});
it('Should return "Your privacy choices" text link text', async () => {
// Act
const wrapper = mount(menuModal);
// Expect
expect(wrapper.html()).toContain('Your privacy choices');
});
it('Should return Warranty text link text', async () => {
// Act
const wrapper = shallowMount(menuModal);
// Expect
expect(wrapper.html()).toContain('Warranty');
});
});

View file

@ -1,232 +0,0 @@
<template>
<div class="menu-modal-container">
<button
class="menu-button"
type="button"
:class="[isActive ? 'active' : '']"
aria-label="Hamburger Menu (modal window)"
@click="toggleModal">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</button>
</div>
<div class="menu-modal-container">
<button
aria-hidden="true"
tabindex="-1"
class="menu-button"
type="button"
:class="[isActive ? 'active' : '']"
aria-label="Hamburger Menu (modal window)"
@click="toggleModal">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</button>
</div>
<!-- Modal -->
<div
id="footerModal"
class="modal menu-modal fade"
data-bs-backdrop="false"
tabindex="-1"
aria-labelledby="footerModalLabel"
aria-hidden="true"
v-on="{ 'show.bs.modal': show, 'hide.bs.modal': hide }">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header visually-hidden">
<h5 id="footerModalLabel" class="modal-title">
Footer Navigation
</h5>
</div>
<div class="modal-body d-flex flex-column">
<textLink
linkType="newWindowLink"
text="Terms of service"
href="//www.safelite.com/terms-of-use"
target="_blank" />
<textLink
linkType="newWindowLink"
text="Your privacy choices"
href="//www.safelite.com/privacy-center"
target="_blank">
<template #after-text>
<img
class="ccpa-icon"
src="~@/assets/img/icons/ccpa-icon.svg"
alt="Your privacy choices" />
</template>
</textLink>
<textLink
linkType="newWindowLink"
text="Warranty"
href="//www.safelite.com/national-lifetime-warranty"
target="_blank" />
<textLink
linkType="newWindowLink"
text="Notice at collection"
href="https://www.safelite.com/ccpa-privacy-policy"
target="_blank" />
</div>
<div class="modal-footer d-flex justify-content-start">
&copy; {{ new Date().getFullYear() }} Safelite Group
</div>
</div>
</div>
</div>
</template>
<script>
import textLink from '@/ux-components/text-link/text-link';
import { Modal } from 'bootstrap';
import baseMixin from '@/mixins/base-mixin.js';
export default {
name: 'menu-modal',
components: {
textLink,
},
data() {
return {
isActive: false,
};
},
methods: {
toggleModal() {
if (this.isActive) {
this.closeModal();
} else {
this.openModal();
}
},
openModal() {
Modal.getOrCreateInstance(
document.getElementById('footerModal')
).show();
},
closeModal() {
Modal.getInstance(document.getElementById('footerModal')).hide();
},
show() {
this.isActive = true;
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
},
hide() {
const self = this;
self.isActive = false;
},
},
};
</script>
<style lang="scss" scoped>
.menu-modal-container {
position: absolute;
padding: 1.47rem 0.5rem 1.47rem 1.47rem;
right: 0;
button {
border: none;
&.menu-button {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2);
background-color: $white;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0; //Required to prevent 'squish' on iPhone
z-index: 1050;
.bar1,
.bar2,
.bar3 {
width: 14px;
height: 2px;
background-color: $blue;
margin: 1px 0;
transition: 0.25s;
}
&.active .bar1 {
transform: rotate(-45deg) translate(-3px, 3px);
}
&.active .bar2 {
opacity: 0;
}
&.active .bar3 {
transform: rotate(45deg) translate(-3px, -3px);
}
}
}
}
.modal {
&.menu-modal {
left: auto;
height: calc(100% - 56px);
top: 56px;
border-top: 1px solid $gray-300;
overflow-x: visible;
overflow-y: visible;
.modal-body {
padding: 2rem;
.ccpa-icon {
width: 2.0625rem;
height: 1rem;
margin-left: 0.5rem;
}
}
.modal-fullscreen {
width: 100vw;
}
.modal-footer {
border-top: none;
padding: 2rem;
}
.menu-modal-container {
position: absolute;
padding: 1.47rem 0 1.47rem 1.47rem;
right: 0;
top: -5rem;
button {
border: none;
&.menu-button {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
box-shadow: none;
background-color: $white;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0; //Required to prevent 'squish' on iPhone
z-index: 1056;
.bar1,
.bar2,
.bar3 {
width: 14px;
height: 2px;
background-color: $blue;
margin: 1px 0;
transition: 0.25s;
}
&.active .bar1 {
transform: rotate(-45deg) translate(-3px, 3px);
}
&.active .bar2 {
opacity: 0;
}
&.active .bar3 {
transform: rotate(45deg) translate(-3px, -3px);
}
}
}
}
//.modal-backdrop styles are in common-styles.scss
}
}
</style>

View file

@ -186,7 +186,7 @@ export default {
VehiclesForQuestions() {
// Map API result data, to address-vehicles data structure
const mappedData = this.VehiclesFromApi.map((v) => {
const maskSymbol = 'X';
const maskSymbol = '*';
const vinStart = maskSymbol.repeat(v.vin.length - 6);
const vinEnd = v.vin.substring(v.vin.length - 6);
return {

View file

@ -31,7 +31,7 @@
<buttonMain
:variant="buttonVariants.primary"
buttonText="Start a new claim"
class="w-100"
class="mt-5 w-100"
@clickEvent="startNewClaim" />
<siteFooter
ref="siteFooter"
@ -54,7 +54,7 @@ import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import buttonVariants from '@/constants/button-variants';
import { buttonVariants } from '@/constants/component-variants';
// Supporting files
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper.js';
@ -135,7 +135,8 @@ export default {
* @summary Steps to perform when forward button clicked.
*/
async forwardButtonAction() {
if (this.selectedAnswer == null) {
// If user clicked foward without selecting an option or user click on "start a new claim" button.
if (this.selectedAnswer == null || this.selectedAnswer === 'NewClaim') {
this.navigateForward();
return;
}
@ -154,6 +155,7 @@ export default {
}
},
navigateForward() {
this.mainStore.updateDuplicateCheckVisited(true);
if (!this.mainStore.isPolicyLookupSuccessful) {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
@ -221,17 +223,23 @@ export default {
}
.duplicate-check-question {
span {
font-weight:600;
}
.question-text {
justify-content: left;
display: inline-flex !important;
margin-top: map-get($spacers, 4);
margin-bottom: 0.625rem !important;
span {
font-weight: 600;
}
}
.form-test-error {
margin-top: 0 !important;
span {
font-weight: 500;
}
}
.question-text.d-flex {
margin-top: 0;
}
}
@ -241,12 +249,6 @@ export default {
}
}
.subheader-secondary {
p {
margin-bottom: 0.25rem !important;
}
}
.form-group {
margin-bottom: 1.25rem !important;
}

View file

@ -1,10 +1,15 @@
// Components
import { shallowMount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import entryPage from '@/layouts/entry-page/entry-page.vue';
import { shallowMount } from '@vue/test-utils';
// Supporting Files
import baseMixin from '@/mixins/base-mixin';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { useMainStore } from '@/store';
import settleAllPromises from '@/helpers/layout-helper.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import * as clientAuthHelper from '@/helpers/clientauth-helper';
// Mock our module for promises.
jest.mock('@/helpers/layout-helper.js', () => jest.fn());
@ -15,34 +20,192 @@ jest.mock('@/helpers/cms-content-helper', () => ({
setupModalLinks: jest.fn()
}));
/** @ignore */
function setupMocks(queryString) {
function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRunAfterInitializingStore = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
},
route: { queryString }
}
});
const wrapper = shallowMount(
entryPage,
mountOptions
);
const testingPinia = createTestingPinia({
initialState: {
main: mainInitialState
}
});
useMainStore(testingPinia);
methodToRunAfterInitializingStore();
const apiResponses = {};
mountOptions.global.plugins = [testingPinia];
mountOptions.data = () => (initialData);
const apiResponses = { cmsContent: {} };
settleAllPromises.mockImplementation(() => apiResponses);
fetchCmsContentForPage.mockImplementation(() => { });
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
const wrapper = shallowMount(entryPage, mountOptions);
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {});
wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$router.navigateWithSpinner = jest.fn();
wrapper.vm.navigateForward = baseMixin.methods.navigateForward;
return { wrapper };
}
describe('entry-page.vue', () => {
test('should render', () => {
const queryString = 'policynumber="123456"';
const { wrapper } = setupMocks(queryString);
test('shows unauthorized message when not authorized', async () => {
const wrapper = shallowMount(entryPage, getMountOptions());
wrapper.setData({ unauthorized: true });
await wrapper.vm.$nextTick();
expect(wrapper.find('#message').isVisible()).toBe(true);
expect(wrapper.text()).toContain('Unauthorized Access.');
});
describe('validateClientTagOnEntry', () => {
let wrapper;
beforeEach(() => {
const mainInitialState = { issConfig: {} };
wrapper = getMountedComponent(mainInitialState).wrapper;
});
window.console.log(wrapper.vm.$route.query);
expect(wrapper).toBeTruthy();
it('returns unauthorized if no clienttag', async () => {
const result = await wrapper.vm.validateClientTagOnEntry({});
expect(result.isAuthorized).toBe(false);
});
it('returns unauthorized if validateISSClientTag returns falsy', async () => {
jest.spyOn(clientAuthHelper, 'validateISSClientTag').mockResolvedValueOnce(null);
const result = await wrapper.vm.validateClientTagOnEntry({ clienttag: 'abc' });
expect(result.isAuthorized).toBe(false);
});
it('returns authorized and clientData if valid and not RSAToken', async () => {
const resp = {
active: true,
accountName: 'Client',
authentication: '',
parentAccountNumber: 'P',
styleSheet: '',
coverageEnabled: true,
siteType: ''
};
jest.spyOn(clientAuthHelper, 'validateISSClientTag').mockResolvedValueOnce(resp);
const result = await wrapper.vm.validateClientTagOnEntry({ clienttag: 'abc' });
expect(result.isAuthorized).toBe(true);
expect(result.clientData).toEqual(resp);
});
it('handles RSAToken with valid signature and EncParams', async () => {
const resp = {
active: true,
accountName: 'Client',
authentication: 'RSAToken EncParams',
parentAccountNumber: 'P',
styleSheet: '',
coverageEnabled: true,
siteType: ''
};
const decryptedData = 'foo=bar&from=yesterday';
jest.spyOn(clientAuthHelper, 'validateISSClientTag').mockResolvedValueOnce(resp);
jest.spyOn(clientAuthHelper, 'validateISSClientSignature').mockResolvedValueOnce({ valid: true, decryptedData });
const result = await wrapper.vm.validateClientTagOnEntry({ clienttag: 'abc', token: 'tok', signature: 'sig' });
expect(result.isAuthorized).toBe(true);
expect(result.clientData).toEqual(resp);
expect(result.decryptedParams).toEqual({ foo: 'bar', from: 'yesterday' });
});
it('handles RSAToken with invalid signature', async () => {
const resp = {
active: true,
accountName: 'Client',
authentication: 'RSAToken',
parentAccountNumber: 'P',
styleSheet: '',
coverageEnabled: true,
siteType: ''
};
jest.spyOn(clientAuthHelper, 'validateISSClientTag').mockResolvedValueOnce(resp);
jest.spyOn(clientAuthHelper, 'validateISSClientSignature').mockResolvedValueOnce({ valid: false });
const result = await wrapper.vm.validateClientTagOnEntry({ clienttag: 'abc', token: 'tok', signature: 'sig' });
expect(result.isAuthorized).toBe(false);
});
});
test('populateISSConfigValues sets issConfig fields and parses clientFlags', () => {
const mainInitialState = {
issConfig: {}
};
const { wrapper } = getMountedComponent(mainInitialState);
const data = {
accountName: 'TestClient',
parentAccountNumber: '12345',
styleSheet: 'test-style',
coverageEnabled: true,
siteType: 'test-site',
clientFlags: JSON.stringify({
TPAEnabled: true,
ClientFullName: 'Full Name',
ClientDisplayName: 'Display Name',
ClaimRegistrationRequired: true,
EnableNoCompQuote: true
})
};
wrapper.vm.populateISSConfigValues(data);
const { issConfig } = wrapper.vm.mainStore;
expect(issConfig.clientName).toBe('TestClient');
expect(issConfig.clientFullName).toBe('Full Name');
expect(issConfig.clientDisplayName).toBe('Display Name');
expect(issConfig.parentAccountNumber).toBe('12345');
expect(issConfig.styleSheet).toBe('test-style');
expect(issConfig.isCoverageEnabled).toBe(true);
expect(issConfig.siteType).toBe('test-site');
expect(issConfig.enableTPAFlow).toBe(true);
expect(issConfig.isClaimRegistrationRequired).toBe(true);
expect(issConfig.enableNoCompQuote).toBe(true);
});
describe('combineClientParameters', () => {
let wrapper;
beforeEach(() => {
wrapper = getMountedComponent({ issConfig: {} }).wrapper;
});
it('returns correct params from config and query', () => {
const configParams = JSON.stringify(['PolicyNbr', 'DateOfLoss', 'Unused']);
const queryStringParams = { policynbr: '123', dateofloss: '2022-01-01', somethingelse: 'no' };
const result = wrapper.vm.combineClientParameters(configParams, queryStringParams);
expect(result).toEqual({ policynbr: '123', dateofloss: '2022-01-01' });
});
it('returns empty object if configParams is not valid JSON', () => {
const configParams = 'notjson';
const queryStringParams = { policynbr: '123' };
const result = wrapper.vm.combineClientParameters(configParams, queryStringParams);
expect(result).toEqual({});
});
it('ignores params not present in query', () => {
const configParams = JSON.stringify(['PolicyNbr', 'MissingParam']);
const queryStringParams = { policynbr: '123' };
const result = wrapper.vm.combineClientParameters(configParams, queryStringParams);
expect(result).toEqual({ policynbr: '123' });
});
});
test('populates store items from params', async () => {
const mainInitialState = {
issConfig: { disabledFields: {} },
order: { policy: {} }
};
const params = {
policynumber: 'ABC123',
policyzipcode: '90210',
dateofloss: '2022-01-01',
returnurl: 'http://success',
returnurl2: 'http://fail'
};
const { wrapper } = getMountedComponent(mainInitialState);
wrapper.vm.populateStoreItemsFromParams(params);
expect(wrapper.vm.mainStore.order.policy.policyNumber).toBe('ABC123');
expect(wrapper.vm.mainStore.order.policy.policyZipCode).toBe('90210');
expect(wrapper.vm.mainStore.order.policy.dateOfLoss).toBe('2022-01-01');
expect(wrapper.vm.mainStore.issConfig.successReturnURL).toBe('http://success');
expect(wrapper.vm.mainStore.issConfig.failureReturnURL).toBe('http://fail');
});
});

View file

@ -143,6 +143,7 @@ export default {
},
populateISSConfigValues(data) {
this.mainStore.issConfig.clientName = data.accountName;
this.mainStore.issConfig.clientFullName = data.accountName; // Defaults to use the client name.
this.mainStore.issConfig.clientDisplayName = data.accountName; // Defaults to use the client name.
this.mainStore.issConfig.parentAccountNumber = data.parentAccountNumber;
this.mainStore.issConfig.styleSheet = data.styleSheet;
@ -157,6 +158,10 @@ export default {
this.mainStore.issConfig.enableTPAFlow = true;
}
if (clientFlags.ClientFullName != null) {
this.mainStore.issConfig.clientFullName = clientFlags.ClientFullName;
}
if (clientFlags.ClientDisplayName != null) {
this.mainStore.issConfig.clientDisplayName = clientFlags.ClientDisplayName;
}

View file

@ -15,7 +15,7 @@
<siteSubHeader
ref="siteSubHeader"
cmsWidgetName="SiteSubHeaderWidget"
class="mt-4" />
class="site-sub-header-container" />
<buttonQuestion
v-if="educatorEndorsement"
ref="schoolPropertyQuestion"
@ -192,17 +192,147 @@ export default {
min-height: 1px;
padding-left: .9375rem;
padding-right: .9375rem;
.windshield-chip-count-question {
:deep(.col:first-of-type) {
.list-button-horizontal {
border-bottom-left-radius: $border-radius-list-button;
border-top-left-radius: $border-radius-list-button;
&:hover {
box-shadow: none;
}
.list-button-horizontal-content {
border-bottom-left-radius: $border-radius-list-button;
border-top-left-radius: $border-radius-list-button;
border-right-width: 0;
span {
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
color: #525656;
}
}
&.selected {
.list-button-horizontal-content {
span {
color: #000;
font-weight: 600;
}
}
}
}
}
:deep(.col:last-of-type) {
.list-button-horizontal {
border-bottom-right-radius: $border-radius-list-button;
border-top-right-radius: $border-radius-list-button;
&:hover {
box-shadow: none;
}
.list-button-horizontal-content {
border-bottom-right-radius: $border-radius-list-button;
border-top-right-radius: $border-radius-list-button;
span {
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
color: #525656;
}
}
&.selected {
.list-button-horizontal-content {
span {
font-weight: 600;
color: #000;
}
}
}
}
}
:deep(.list-button-horizontal) {
.list-button-horizontal-content {
padding: 0.625rem;
border: 1px solid #b3b4b5;
height: 2.875rem;
}
&.selected {
.list-button-horizontal-content {
border: 1px solid #0070d1;
box-shadow: none;
}
}
input[type="radio"] {
&:focus + .list-button-horizontal-content {
box-shadow: none;
border: 2.5px solid #0070d1;
outline: none;
}
}
&.has-error {
border: none;
.list-button-horizontal-content {
border: 1px solid #db0020;
}
input[type="radio"] {
&:focus + .list-button-horizontal-content {
border: 2.5px solid #db0020;
}
}
}
}
:deep(fieldset) {
box-shadow: 0 1px 5px rgba(0, 0, 0, .2);
border-radius: $border-radius-list-button;
}
}
:deep(.question-text) {
line-height: 1.5rem;
margin-bottom: .3125rem;
& > span {
text-align: left;
line-height: 1.5rem;
font-weight: 600;
}
}
}
}
:deep(.subheader-primary) {
margin-bottom: map-get($spacers, 2);
line-height: 1.5rem;
h5 {
line-height: 1.5rem;
span {
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
}
}
}
:deep(.subheader-secondary) {
line-height: 1.5rem;
margin-top: 0.625rem;
p {
font-size: $h6-font-size;
line-height: map-get($spacers, 5);
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
}
}
@ -213,18 +343,6 @@ export default {
justify-content: left !important;
padding-left: 0;
padding-right: 0;
margin-bottom: map-get($spacers, 5);
}
:deep(.question-text) {
& > span {
text-align: left;
line-height: map-get($spacers, 5);
}
margin-bottom: map-get($spacers, 2);
}
:deep(div.question-text.d-flex) {
margin-top: map-get($spacers, 4);
}
</style>

View file

@ -127,7 +127,7 @@ describe('navigation', () => {
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
test('if the coverage policy verified navigate forward to policy-vehicle page', async () => {
test('if the coverage policy verified navigate forward to vehicle damage page', async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: '1234 Main St',
@ -135,14 +135,6 @@ describe('navigation', () => {
state: 'OH',
zipCode: '43215'
};
const mockvehicles = [
{
vin: 'TEST_VIN'
},
{
vin: 'TEST_VIN2'
}
];
const { wrapper } = setupMocks();
@ -152,13 +144,12 @@ describe('navigation', () => {
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCoverageEnabled: true,
vehiclesCount: 2,
vehiclesFound: mockvehicles
isCoverageEnabled: true
});
useMainStore().order.policy.policyNumber = 'p_0001';
useMainStore().order.policy.dateOfLoss = '2022-01-28';
useMainStore().order.vehicle.policyVehicleId = 0;
// Act
@ -167,10 +158,111 @@ describe('navigation', () => {
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED,
undefined,
{},
{},
mockvehicles
undefined
);
});
test('if the coverage policy unverified navigate forward to vehicle damage page', async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: '1234 Main St',
city: 'Columbus',
state: 'OH',
zipCode: '43215'
};
const { wrapper } = setupMocks();
await wrapper.setData({
firstName: 'KK',
lastName: 'KK',
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCoverageEnabled: true
});
useMainStore().order.policy.policyNumber = 'p_0001';
useMainStore().order.policy.dateOfLoss = '2022-01-28';
useMainStore().order.vehicle.policyVehicleId = null;
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
undefined
);
});
test('if the coverage policy verified navigate back to welcome page', async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: '1234 Main St',
city: 'Columbus',
state: 'OH',
zipCode: '43215'
};
const { wrapper } = setupMocks();
await wrapper.setData({
firstName: 'KK',
lastName: 'KK',
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCoverageEnabled: true
});
useMainStore().order.policy.policyNumber = 'p_0001';
useMainStore().order.policy.dateOfLoss = '2022-01-28';
useMainStore().order.vehicle.policyVehicleId = 0;
// Act
await wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_BACK_POLICY_VERIFIED,
undefined
);
});
test('if the coverage policy unverified navigate back to vehicle-selection page', async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: '1234 Main St',
city: 'Columbus',
state: 'OH',
zipCode: '43215'
};
const { wrapper } = setupMocks();
await wrapper.setData({
firstName: 'KK',
lastName: 'KK',
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCoverageEnabled: true
});
useMainStore().order.policy.policyNumber = 'p_0001';
useMainStore().order.policy.dateOfLoss = '2022-01-28';
useMainStore().order.vehicle.policyVehicleId = null;
// Act
await wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_BACK_POLICY_UNVERIFIED,
undefined
);
});
});

View file

@ -14,7 +14,7 @@
id="sub-header"
cmsWidgetName="SiteSubHeaderWidget"
class="mt-4" />
<div class="row mb-4">
<div class="row">
<div class="col">
<textboxQuestion
ref="policyHolderFirstName"
@ -36,8 +36,8 @@
:validationRules="rules.lastName" />
</div>
</div>
<div class="row mb-4">
<div class="col-md-8">
<div class="row">
<div class="col-lg-8">
<textboxQuestion
ref="phoneNumber"
v-model="customerQuestions.phoneNumber"
@ -48,13 +48,14 @@
:mask="phoneMask"
disableAutoFill />
</div>
<div class="col-md-4">
<div class="col-lg-4">
<textboxQuestion
ref="extension"
v-model="customerQuestions.extension"
inputId="extensionField"
cmsWidgetName="ExtensionQuestion"
:validationRules="rules.extension"
maxLength="5"
disableAutoFill />
</div>
</div>
@ -65,8 +66,7 @@
cmsWidgetName="EmailAddressQuestion"
:validationRules="rules.email"
isRequired
disableAutoFill
class="mb-4" />
disableAutoFill />
<addressQuestions
id="address-questions-wrapper"
ref="addressQuestions"
@ -78,7 +78,7 @@
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
@backClicked="navigateBack" />
@backClicked="backButtonAction" />
</div>
</div>
</div>
@ -138,7 +138,6 @@ export default {
data() {
return {
customerQuestions: this.getPolicyHolderDetailsFromStore(),
vehiclesFound: [],
rules: {
firstName: globalRules.POLICYHOLDER_FIRST_NAME_REQUIRED,
lastName: globalRules.POLICYHOLDER_LAST_NAME_REQUIRED,
@ -152,8 +151,8 @@ export default {
isCoverageEnabled() {
return this.mainStore.issConfig.isCoverageEnabled;
},
vehiclesCount() {
return this.vehiclesFound.length;
isPolicyVehicleSelected() {
return (this.mainStore.order.vehicle.policyVehicleId != null && this.mainStore.order.vehicle.policyVehicleId >= 0);
}
},
methods: {
@ -163,13 +162,10 @@ export default {
},
navigateForward() {
if (this.vehiclesCount > 0) {
if (this.isPolicyVehicleSelected) {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED,
this.$route,
{},
{},
this.vehiclesFound
this.$route
);
} else {
this.$router.navigate(
@ -179,6 +175,20 @@ export default {
}
},
backButtonAction() {
if (this.isPolicyVehicleSelected) {
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK_POLICY_VERIFIED,
this.$route
);
} else {
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK_POLICY_UNVERIFIED,
this.$route
);
}
},
getPolicyHolderDetailsFromStore() {
return {
addressQuestions: {
@ -194,7 +204,7 @@ export default {
lastName: this.mainStore.order.customer.lastName,
phoneNumber: this.mainStore.order.contactInfo.homePhone,
extension: this.mainStore.order.contactInfo.extension,
email: this.mainStore.order.customer.emailAddress,
email: this.mainStore.order.customer.emailAddress
};
}
}
@ -218,4 +228,10 @@ export default {
#address-questions-wrapper .form-test-error {
line-height: 24px;
}
.textbox-question {
margin-bottom: 1.25rem;
}
.dropdown-question {
margin-bottom: 1.25rem;
}
</style>

View file

@ -1,13 +1,13 @@
<template>
<buttonQuestion
ref="policyVehiclesQuestion"
v-model="selectedVehicleVin"
groupName="policyVehiclesQuestionOption"
buttonTypeString="listButton"
isOverflowScrollable
:answers="answers"
isRequired
:validationRules="validationRules" />
ref="policyVehiclesQuestion"
v-model="selectedVehicleVin"
groupName="policyVehiclesQuestionOption"
buttonTypeString="listButton"
isOverflowScrollable
:answers="answers"
isRequired
:validationRules="validationRules" />
</template>
<script>
import buttonQuestion from '@/digital-components/button-question/button-question.vue';

View file

@ -21,7 +21,7 @@
cmsWidgetName="PolicyVehiclesQuestion"
:vehicles="VehiclesForQuestions"
:validationRules="rules.optionRequired"
class="mb-2"/>
class="mb-2" />
<buttonMain
:variant="buttonVariants.primary"
buttonText="Add another vehicle"
@ -70,7 +70,7 @@ import coverageType from '@/constants/coverage-type';
import alert from '@/ux-components/alert/alert.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import buttonVariants from '@/constants/button-variants';
import { buttonVariants } from '@/constants/component-variants';
export default {
name: 'policy-vehicles',
@ -114,7 +114,7 @@ export default {
const vehicles = this.policyVehicles;
const mappedData =
vehicles?.map((v) => {
const maskSymbol = 'X';
const maskSymbol = '*';
const vinStart = maskSymbol.repeat(v.vin.length - 6);
const vinEnd = v.vin.substring(v.vin.length - 6);
return {
@ -122,7 +122,7 @@ export default {
vehicle: v,
Text: `${v.vehicleYear} ${v.vehicleMake} ${v.vehicleModel}`,
Name: v.vin,
SubText: `VIN ${vinStart}${vinEnd}`
SubText: `VIN: ${vinStart}${vinEnd}`
};
}) ?? [];
return mappedData;
@ -297,25 +297,24 @@ export default {
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.iss-heritage-container-width {
.policy-vehicles-container {
position: relative;
min-height: 1px;
padding-left: .9375rem;
padding-right: .9375rem;
}
}
.policy-vehicles {
.button-question {
.question-text {
margin: 1rem 0;
span {
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
}
:deep(.subheader-secondary p) {
text-align: left;
}
:deep(span.small) {
font-size: 1rem;
}
:deep(.form-test-error) {
text-align: left;
}
}
}

View file

@ -6,7 +6,6 @@ import { createTestingPinia } from '@pinia/testing';
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { useMainStore } from '@/store/index.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
// Mock fetchCmsContentForPage
jest.mock('@/helpers/cms-content-helper', () => ({
@ -214,7 +213,7 @@ describe('schedule-page.vue', () => {
// Act
const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod(
'2023-01-01',
'2023-01-31'
'2023-01-15'
);
// Assert
@ -236,7 +235,7 @@ describe('schedule-page.vue', () => {
estimatedServiceMinutesMaximum: 120
});
});
test('Should call API service in days of 34 or less when getAvailableDatesMethod is called with large date ranges', async () => {
test('Should call API service in days of 15 or less when getAvailableDatesMethod is called with large date ranges', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.selectableDatesData = {
@ -264,7 +263,7 @@ describe('schedule-page.vue', () => {
// 2023-01-01 --> 2023-02-05
// 2023-02-06 --> 2023-03-12
// 2023-03-13 --> 2023-03-31
expect(store.getShopTimeSlots).toHaveBeenCalledTimes(3);
expect(store.getShopTimeSlots).toHaveBeenCalledTimes(6);
});
});
describe('Rendering', () => {
@ -290,32 +289,16 @@ describe('schedule-page.vue', () => {
// Assert
expect(testValue).toStrictEqual('01234');
});
test('getDisplayTextForMilitaryTime should return the correctly formatted string', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.selectableDatesData = {
days: []
};
const timeInput1 = '15:00';
const timeInput2 = '15:30';
// Act
const testOutput1 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1);
const testOutput2 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2);
const testOutput3 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1, true);
const testOutput4 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2, true);
// Assert
expect(testOutput1).toBe('3:00 PM');
expect(testOutput2).toBe('3:30 PM');
expect(testOutput3).toBe('3 PM');
expect(testOutput4).toBe('3:30 PM');
});
});
test('forwardButtonAction should call route method navigateWithoutSaving', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.$router.navigate = jest.fn(() => ({}));
wrapper.vm.selectedTimeSlotInfo = {
timeSlot: {
routeCode: 'test-id'
}
};
// Act
await wrapper.vm.forwardButtonAction();

View file

@ -1,7 +1,6 @@
<template>
<Form
ref="theForm"
v-slot="{ meta }"
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit">
<div class="fade-on-route-transition">
@ -16,56 +15,27 @@
cmsWidgetName="ScheduleSubHeaderWidget"
secondaryTextClasses="text-center small sub-text"
class="mt-4" />
<template v-if="ChangeShopLink.length">
<textBlock
cmsWidgetName="ChangeShopLink"
justifyText="center"
class="mb-5 text-link-small change-shop-link"
:marginTopSizeOverride="1" />
</template>
<div class="main-content-container">
<locationAlerts
ref="locationAlerts"
cmsWidgetPrefix="LocationAlert-" />
<datePicker
ref="datePicker"
v-model="selectedDate"
v-model="selectedTimeSlotInfo"
customComponentId="dateQuestion"
selectableDatesSetting="custom"
class="text-link-small"
:showTimeSlotError="showDatePickerError"
:customSelectableDatesCallback="
getAvailableDatesMethod
"
validationRules="date-required"
@dateClicked="openInshopTimeSlotsModal" />
<timeSlotModalQuestion
ref="timeSlotModalQuestion"
v-model="selectedTimeSlotInfo"
customComponentId="timeSlotModalQuestion"
cmsWidgetName="TimeSlotModalQuestion"
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
mobileCmsWidgetName="MobileTimeSlotModal"
dropoffCmsWidgetName="DropOffTimeSlotModal"
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
:selectedDate="selectedDate"
:appointmentType="appointmentType"
:premiumAppointmentFee="mobilePremiumAppointmentFee"
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
:estimatedServiceMinutesMinimum="
selectableDatesData.estimatedServiceMinutesMinimum
"
:estimatedServiceMinutesMaximum="
selectableDatesData.estimatedServiceMinutesMaximum
"
validationRules="time-slot-selection-required"
@timeSlotModalClosed="timeSlotModalClosed"
@timeSlotSelected="forwardButtonAction" />
@dateSelected="dateSelectedFromPicker"
@timeSlotSelected="timeSlotSelectedFromPicker" />
<siteFooter
ref="navbar"
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isForwardButtonNavigationDisabled="!isFormValid"
@backClicked="navigateBack"
@forwardClicked="forwardButtonAction" />
</div>
@ -80,9 +50,7 @@ import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import locationAlerts from '@/layouts/schedule-page/location-alerts/location-alerts.vue';
import datePicker from '@/digital-components/date-picker/date-picker.vue';
import timeSlotModalQuestion from '@/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
// Supporting files
import {
@ -97,27 +65,15 @@ import {
} from '@/helpers/cms-content-helper';
import {
calcDaysBetweenDates,
convertDateStringToDate,
sumDateString
} from '@/helpers/date-helper';
import settleAllPromises from '@/helpers/layout-helper';
import { Form, defineRule } from 'vee-validate';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import errorMessages from '@/constants/error-messages';
import { required } from '@/helpers/validation-rules';
import { useMainStore } from '@/store';
// DEFINE VALIDATION RULES
defineRule('date-required', required(errorMessages.DATE_REQUIRED));
defineRule('time-slot-selection-required', (value) => {
if (value?.timeSlot?.routeCode == null) {
return errorMessages.DATE_REQUIRED;
}
return true;
});
// Define constants
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
const TIME_SLOTS_CALL_DAYS_LIMIT = 15;
const getAvailableDates = async (
startDateString,
@ -127,13 +83,11 @@ const getAvailableDates = async (
) => {
const apiEndDateLimit = sumDateString(
startDateString,
TIME_SLOTS_CALL_DAYS_LIMIT
TIME_SLOTS_CALL_DAYS_LIMIT - 1
);
const difference = calcDaysBetweenDates(startDateString, endDateString);
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
const storeActionConfigs = [];
const timeSlotsData = {};
timeSlotsData.days = [];
let apiStartDate = startDateString;
let apiEndDate = endDateString;
@ -144,7 +98,7 @@ const getAvailableDates = async (
apiStartDate = sumDateString(apiEndDate, 1);
apiEndDate = sumDateString(
apiStartDate,
TIME_SLOTS_CALL_DAYS_LIMIT
TIME_SLOTS_CALL_DAYS_LIMIT - 1
);
if (i === apiCallsCount) {
@ -154,11 +108,7 @@ const getAvailableDates = async (
apiEndDate = apiEndDateLimit;
}
if (
appointmentType === AppointmentTypeStrings.MOBILE
|| appointmentType
=== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
) {
if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
storeActionConfig = {
storeAction: GET_MOBILE_TIME_SLOTS,
payload: {
@ -232,9 +182,7 @@ export default {
siteSubHeader,
locationAlerts,
datePicker,
timeSlotModalQuestion,
siteFooter,
textBlock,
// eslint-disable-next-line vue/no-reserved-component-names
Form
},
@ -300,7 +248,6 @@ export default {
resultMap.datePickerInitialData.initialShopTimeSlotsResponse,
resultMap.premiumFeeWithPrice
);
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
});
},
setup() {
@ -312,6 +259,7 @@ export default {
selectedDate: this.getSelectedDate(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
selectableDatesData: [],
showDatePickerError: false,
mobilePremiumAppointmentFee: null
};
},
@ -325,38 +273,14 @@ export default {
appointmentType() {
return useMainStore().order.serviceLocation.appointmentType;
},
timeSlotsForSelectedDate() {
if (!this.selectedDate) {
return null;
}
return this.selectableDatesData.days?.find((selectableDate) => selectableDate.date === this.selectedDate);
isFormValid() {
const hasTimeSlotSelected = this.selectedTimeSlotInfo?.timeSlot?.routeCode != null;
return hasTimeSlotSelected;
},
supportingItems() {
return useMainStore().lineItems.supportingItems;
}
},
watch: {
selectedDate(newValue, oldValue) {
// Clear time slot selection if date selected changes
if (newValue !== oldValue) {
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null
},
isPremiumAppointment: null
};
}
},
selectedTimeSlotInfo(newValue) {
this.updateFooterButtonText(newValue);
}
},
methods: {
splitCopyOnCMSPlaceHolder,
arePagePrerequisitesValid() {
@ -400,9 +324,6 @@ export default {
getServiceZipCtuCodeFromStore() {
return this.mainStore.order.serviceLocation.zipCodeCtu;
},
openInshopTimeSlotsModal() {
this.$refs.timeSlotModalQuestion.openModal();
},
getSelectedDate() {
return this.mainStore.order.schedule.date;
},
@ -420,64 +341,21 @@ export default {
return selectedTimeSlotInfo;
},
timeSlotModalClosed() {
// Clear the selectedDate if no timeSlot has been selected
if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) {
this.selectedDate = null;
}
dateSelectedFromPicker(date) {
this.selectedDate = date;
this.showDatePickerError = false;
},
updateFooterButtonText(timeSlotInfo) {
let navbarButtonText;
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
navbarButtonText = 'Continue';
} else {
navbarButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(timeSlotInfo.timeSlot.date)}`;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(timeSlotInfo.timeSlot.startTime)}`;
} else if (
this.appointmentType === AppointmentTypeStrings.MOBILE
&& !timeSlotInfo.isPremiumAppointment
) {
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.startTime,
true
)} - ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.endTime,
true
)}`;
}
}
this.$refs.navbar.updateButtonText(navbarButtonText);
},
convertSelectedDateToShortMonthAndDay(selectedDate) {
// This conversion ensures we don't get get GMT induced date changes
const dateObject = convertDateStringToDate(selectedDate);
return dateObject.toLocaleDateString('en-us', {
month: 'short',
day: 'numeric'
});
},
getDisplayTextForMilitaryTime(
militaryTimeInput,
shouldTrimMinutesIfEmpty = false
) {
// Expected input: "HH:MM"
let hours = parseInt(militaryTimeInput.split(':')[0], 10);
const minutes = militaryTimeInput.split(':')[1];
const meridianNotation = hours > 11 ? 'PM' : 'AM';
if (hours > 12) {
hours -= 12;
}
if (shouldTrimMinutesIfEmpty && minutes === '00') {
return `${hours} ${meridianNotation}`;
}
return `${hours}:${minutes} ${meridianNotation}`;
timeSlotSelectedFromPicker(timeSlot) {
this.selectedTimeSlotInfo = timeSlot;
this.showDatePickerError = false;
},
forwardButtonAction() {
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
if (!this.isFormValid) {
this.showDatePickerError = true;
return;
}
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route

View file

@ -1,24 +1,21 @@
<template>
<transition
name="fade"
mode="out-in">
<div
v-if="isDisplayed"
class="appointment-type-question"
aria-live="polite">
<buttonQuestion
ref="buttonQuestion"
v-model="selectedValues"
customButtonQuestionId="appointmentTypeQuestion"
:questionText="questionText"
:answers="answersToDisplay"
:groupName="groupName"
buttonTypeString="listCard"
:suppressError="suppressError"
:validationRules="validationRules"
isRequired />
</div>
</transition>
<transition
name="fade"
mode="out-in">
<div
class="appointment-type-question"
aria-live="polite">
<buttonQuestion
ref="buttonQuestion"
v-model="selectedValues"
customButtonQuestionId="appointmentTypeQuestion"
:answers="answersToDisplay"
:groupName="groupName"
:suppressError="suppressError"
:validationRules="validationRules"
isRequired />
</div>
</transition>
</template>
<script>
@ -38,12 +35,9 @@ export default {
modelValue: String,
groupName: String,
isAvailable: Boolean,
isDisplayed: Boolean,
suppressError: Boolean,
validationRules: String,
cmsWidgetName: String,
isServiceableMobile: Boolean,
isServiceableInshop: Boolean
cmsWidgetName: String
},
emits: ['update:modelValue'],
computed: {
@ -51,23 +45,13 @@ export default {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
},
answersFromCms() {
const retCms = this.getCmsContent(this.cmsWidgetName, 'Answers');
if (retCms && retCms.length > 0 && this.isMobileFeeHidden) {
return retCms.map((x) => (x.Name === AppointmentTypeStrings.MOBILE ? { ...x, SubText: x.SubText.replace('*', '') } : x));
}
return retCms;
return this.getCmsContent(this.cmsWidgetName, 'Answers');
},
answersToDisplay() {
const shouldShowMobile = this.isServiceableMobile && this.isItacOrNoComp;
const shouldShowMobileNotITACNotNoComp = this.isServiceableMobile && !this.isItacOrNoComp;
const shouldShowInshop = this.isServiceableInshop;
const shouldShowDropoff = this.isServiceableInshop && !useMainStore().damage.isRepair;
return this.answersFromCms
? this.answersFromCms.filter((answer) => (
(answer.Name === AppointmentTypeStrings.IN_SHOP && shouldShowInshop)
|| (answer.Name === AppointmentTypeStrings.MOBILE && shouldShowMobile)
|| (answer.Name === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP && shouldShowMobileNotITACNotNoComp)
|| (answer.Name === AppointmentTypeStrings.DROP_OFF && shouldShowDropoff)
(answer.Name === AppointmentTypeStrings.IN_SHOP)
|| (answer.Name === AppointmentTypeStrings.MOBILE)
))
: [];
},
@ -78,58 +62,13 @@ export default {
set(newValue) {
this.$emit('update:modelValue', newValue);
}
},
isITAC() {
return useMainStore().isITAC;
},
isItacOrNoComp() {
return this.isITAC || useMainStore().isNoComp;
},
isMobileFeeHidden() {
return this.getSettingValue(experimentSettings.ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_HIDDEN) === 'true';
},
isMobileOnly() {
return this.isServiceableMobile && !this.isServiceableInshop;
}
},
watch: {
answersToDisplay: {
handler(newValue) {
// If there is only one option to display and that option is 'Mobile' then select it
if (
newValue.length === 1
&& newValue.findIndex((answer) => (answer.Name === AppointmentTypeStrings.MOBILE
|| answer.Name === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP)) !== -1
) {
if (this.isItacOrNoComp) {
this.selectedValues = AppointmentTypeStrings.MOBILE;
} else {
this.selectedValues = AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
}
}
},
immediate: true
},
isMobileOnly: {
handler(newValue) {
if (newValue) {
if (this.isItacOrNoComp) {
this.selectedValues = AppointmentTypeStrings.MOBILE;
} else {
this.selectedValues = AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
}
}
}
}
}
};
</script>
<style lang="scss" scoped>
:deep(.button-question) {
.list-card img {
height: auto;
width: 3.417rem;
}
.appointment-type-question {
margin-bottom: 1.25rem;
}
</style>

View file

@ -1,29 +1,13 @@
/* eslint-env jest */
import baseMixin from '@/mixins/base-mixin';
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { mount, flushPromises } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import navigationScenarios from '@/router/router-constants/navigation-scenarios';
import routerParams from '@/router/router-constants/router-params';
import { useMainStore } from '@/store';
import serviceLocation from '@/layouts/service-location/service-location.vue';
// Define Mocks
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: jest.fn(() => Promise.resolve('content'))
}));
/** @ignore */
function setupMocks() {
const wrapper = shallowMount(
serviceLocation,
getMountOptions({
router: {
navigate: jest.fn()
}
})
);
wrapper.vm.$router.navigateWithSpinner = jest.fn();
wrapper.vm.navigateBack = baseMixin.methods.navigateBack;
return { wrapper };
}
import { getZipCodeData } from '@/helpers/service-location-helper';
const mockGetServiceabilityDetails = () => {
const serviceabilityDetails = {
@ -35,123 +19,199 @@ const mockGetServiceabilityDetails = () => {
return Promise.resolve(serviceabilityDetails);
};
const mockZipcodeData = (zip) => {
if (zip === '43235' || zip === '55555') {
return Promise.resolve({
containsMilitaryBase: false,
isValid: true,
isServiceable: true,
city: 'Columbus',
state: 'OH',
zipCodeCtu: '01820'
});
}
if (zip === '45433') {
return Promise.resolve({
containsMilitaryBase: true,
isValid: true,
isServiceable: true,
city: 'Columbus',
state: 'OH',
zipCodeCtu: '01820'
});
}
return Promise.resolve({
containsMilitaryBase: false,
isValid: false,
isServiceable: false,
city: null,
state: null,
zipCodeCtu: null
});
}
const mockProviders = () => {
return Promise.resolve([
{
"address": {
"city": "COLUMBUS",
"country": "US",
"state": "OH",
"streetAddress": "6826 Sawmill Rd",
"streetAddress2": "",
"zipCode": "43235",
"zipCodeCtu": "03357"
},
"distanceInMiles": 4.136335989015438,
"providerNumber": "003357",
"companyName": "SAFELITE AUTOGLASS - COLUMBUS, OH",
"phoneNumber": "6142336400",
"isSafeliteShop": true
},
{
"address": {
"city": "Lewis Center",
"country": "US",
"state": "OH",
"streetAddress": "1343 Cameron Ave",
"streetAddress2": "",
"zipCode": "43035",
"zipCodeCtu": "03357"
},
"distanceInMiles": 8.193072412262042,
"providerNumber": "003417",
"companyName": "SAFELITE AUTOGLASS - LEWIS CENTER, OH",
"phoneNumber": "6147815433",
"isSafeliteShop": true
}
])
}
jest.mock(
'@/helpers/service-location-helper',
() => ({
getServiceabilityDetails: jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode))
getServiceabilityDetails: jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode)),
getZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode)),
getMobileZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode))
})
);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
// linkWidgetName is not defined. This whole spec file needs review.
if (widgetName === linkWidgetName) {
return mockLinkCmsContent[cmsFieldName];
const mockRoute = {
params: {}
};
const mockRouter = {
navigate: jest.fn()
};
const mountOptions = {
global: {
mixins: [
{
computed: {
navigationScenarios() {
return navigationScenarios;
},
routerParams() {
return routerParams;
}
},
methods: {
getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(() => 80),
getPageNameByQueryString: jest.fn(() => '')
}
}
if (widgetName === modalWidgetName) {
return mockModalCmsContent[cmsFieldName];
}
return null;
}),
getZipCodeData: jest.fn((zip) => {
if (zip === '43235' || zip === '55555') {
return Promise.resolve({
containsMilitaryBase: false,
isValid: true,
state: 'OH',
zipCodeCtu: '01820'
});
}
if (zip === '45433') {
return Promise.resolve({
containsMilitaryBase: true,
isValid: true,
state: 'OH'
});
}
return Promise.resolve({
containsMilitaryBase: false,
isValid: false,
state: null,
zipCodeCtu: null
});
}),
onSubmit: jest.fn(),
onInvalidSubmit: jest.fn()
],
mocks: {
$route: mockRoute,
$router: mockRouter
},
stubs: {
alert: true,
textBlock: true,
appointmentTypeQuestion: true,
contentGroupModal: true,
siteFooter: true,
siteHeader: true,
siteSubHeader: true,
shopAddress: true,
serviceZipQuestion: true
}
}
};
function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
mountOptions.global.plugins = [createTestingPinia({
initialState: {
main: {
order: {
serviceLocation: {
appointmentType: appointmentType
}
}
}
}
})];
useMainStore().getProviders = jest.fn().mockImplementation(() => mockProviders());
const wrapper = mount(serviceLocation, mountOptions);
describe('navigation', () => {
test('if the back button is clicked, navigate back', async () => {
// Arrange
const { wrapper } = setupMocks({
wrapper.vm.$router.navigateWithSpinner = jest.fn();
wrapper.vm.navigateBack = baseMixin.methods.navigateBack;
return { wrapper };
}
beforeEach(() => {
jest.clearAllMocks();
});
describe('service-location.vue', () => {
describe('navigation', () => {
test('if the back button is clicked, navigate back', async () => {
// Arrange
const { wrapper } = setupMocks({
});
// Act
await wrapper.vm.navigateBack();
// Assert
expect(wrapper.vm.$router.navigateWithSpinner).toHaveBeenCalled();
});
});
describe('updating service zip', () => {
test('updates the page model after changing the service zip code', () => {
// Arrange
const { wrapper } = setupMocks({ appointmentType: AppointmentTypeStrings.IN_SHOP });
const newServiceZipCode = '61606';
const shopAddressComponent = wrapper.findComponent({
ref: 'shopAddress'
});
// Act
shopAddressComponent.vm.$emit('zip-updated', newServiceZipCode);
// Assert
expect(wrapper.vm.zipCode).toStrictEqual(newServiceZipCode);
});
// Act
await wrapper.vm.navigateBack();
test('displays military zip message when zip is updated', async () => {
// Arrange
const { wrapper } = setupMocks({ appointmentType: AppointmentTypeStrings.MOBILE });
// Assert
expect(wrapper.vm.$router.navigateWithSpinner).toHaveBeenCalled();
});
});
describe('updating service zip', () => {
test('updates the page model after changing the service zip code', () => {
// Arrange
const { wrapper } = setupMocks({
mixins: [mockMixin]
});
const newServiceZipCodeQuestion = {
zipCode: '61606',
state: 'IL'
};
const serviceZipCodeComponent = wrapper.findComponent({
ref: 'serviceZipCodeQuestion'
});
// Act
serviceZipCodeComponent.vm.$emit('update:modelValue', newServiceZipCodeQuestion);
// Assert
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeQuestion);
});
test('displays military zip message when zip is updated', () => {
// Arrange
const { wrapper } = setupMocks({});
const mobileLocationQuestionsComponent = wrapper.findComponent({
ref: 'mobileLocationQuestions'
});
mobileLocationQuestionsComponent.resetComponent = jest.fn();
const serviceZipCodeComponent = wrapper.findComponent({
ref: 'serviceZipCodeQuestion'
});
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
expect(wrapper.vm.zipContainsMilitaryBase).toBe(false);
// TODO: Use or remove
const newServiceZipCodeQuestion = {
zipCode: '45433',
state: 'OH'
};
// Act
serviceZipCodeComponent.vm.$emit('updated-contains-military-base', true);
// Assert
expect(wrapper.vm.zipContainsMilitaryBase).toBe(true);
const mobileServiceZipCodeQuestion = wrapper.findComponent({
ref: 'mobileServiceZipCodeQuestion'
});
expect(wrapper.vm.zipContainsMilitaryBase).toBe(false);
const newMobileServiceZipCode = '45433'
// Act
mobileServiceZipCodeQuestion.vm.$emit('update:modelValue', newMobileServiceZipCode);
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.zipContainsMilitaryBase).toBe(true);
});
});
});

View file

@ -12,28 +12,7 @@
<div class="service-location-container iss-heritage-content-container-width">
<siteSubHeader
cmsWidgetName="SiteSubHeaderWidget"
class="mt-4" />
<serviceZipModalQuestion
ref="serviceZipCodeQuestion"
v-model="serviceZipCodeQuestion"
modalWidgetName="ServiceZipModalWidget"
:onZipUpdateCallback="reloadShopData"
@updatedServiceability="setServiceabilityDetails"
@updatedContainsMilitaryBase="
setContainsMilitaryBase
" />
<alert
v-if="displayMilitaryZipAlert"
ref="alertMilitaryBaseZip"
class="my-5"
cmsWidgetName="AlertMilitaryBaseZipWidget"
alertClass="alert-warning" />
<alert
v-if="displayServiceableMobileOnly"
ref="alertMobileOnly"
class="my-5"
cmsWidgetName="AlertMobileOnlyWidget"
alertClass="alert-warning" />
class="subheader" />
<alert
v-if="displayRecalibrationWarning"
ref="alertRecalNoMobile"
@ -41,18 +20,6 @@
cmsWidgetName="AlertRecalNoMobileWidget"
alertClass="alert-warning"
@text-link-clicked="openModalAction" />
<alert
v-if="displayServiceableInshopOnly"
ref="alertInshopOnly"
class="my-5"
cmsWidgetName="AlertInshopOnlyWidget"
alertClass="alert-warning" />
<alert
v-if="displayNoShopsAlert"
ref="alertNoShops"
class="my-5"
cmsWidgetName="AlertNoShopsWidget"
alertClass="alert-warning" />
<alert
v-if="displayBigTruckNoShops"
ref="alertBigTruckNoShops"
@ -60,41 +27,94 @@
cmsWidgetName="AlertBigTruckNoShopsWidget"
alertClass="alert-warning" />
<div class="appointment-type">
<div class="appointment-type-question-text d-flex">
<span class="w-100 recal-text" v-if="recalibrationRequired">{{ scheduleRecalText }}</span>
<span class="w-100" v-if="!requiresInshopRecalibration">{{ appointmentQuestionText }}</span>
</div>
<alert
v-if="displayServiceableInshopOnly"
ref="alertInshopOnly"
cmsWidgetName="AlertInshopOnlyWidget"
:manualCopy="inShopOnlyCopy"
:isCollapsible="true"
alertClass="alert-warning" />
<alert
v-if="displayServiceableMobileOnly"
ref="alertMobileOnly"
cmsWidgetName="AlertMobileOnlyWidget"
:manualCopy="mobileOnlyCopy"
:isCollapsible="true"
alertClass="alert-warning" />
<appointmentTypeQuestion
v-show="isAppointmentTypeDisplayed"
v-if="!requiresInshopRecalibration && isServiceableMobile"
ref="appointmentTypeQuestion"
v-model="selectedAppointmentType"
:isServiceableMobile="isServiceableMobile"
:isServiceableInshop="isServiceableInshop"
:isDisplayed="isAppointmentTypeDisplayed"
groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget"
validationRules="option-required" />
</div>
<mobileLocationModalQuestions
v-if="isMobileLocationDisplayed"
ref="mobileLocationQuestions"
v-model="mobileLocationQuestions"
customComponentId="mobileLocationQuestions"
:mobileFeePart="mobileFeePart"
validationRules="mobile-location-required"
linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget"
:onZipUpdateCallback="reloadShopData"
@updated-mobile-fee-part="setMobileFeePart"
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
@updated-mobile-ctu="setCtuForMobile" />
<shopQuestion
v-show="isShopQuestionDisplayed"
:ref="SHOP_QUESTION_REF_NAME"
v-model="selectedProvider"
:selectedAppointmentType="selectedAppointmentType"
:isDisplayed="isShopQuestionDisplayed"
cmsWidgetName="ShopQuestionWidget"
@updatedMobileProviderNumber="
setMobileProviderNumber
" />
<div
v-if="isInshop">
<alert
v-if="displayLowAvailabilityInshop"
ref="alertLowAvailabilityInshop"
cmsWidgetName="AlertLowAvailabilityInshopWidget"
:isCollapsible="true"
alertClass="alert-warning" />
<shopAddress
ref="shopAddress"
v-model="selectedProvider"
:serviceZipcode="zipCode"
:selectedAppointmentType="selectedAppointmentType"
modalWidgetName="ChangeLocationModalWidget"
cmsWidgetName="ShopAddressWidget"
@zip-updated="handleInShopZipUpdated" />
<!-- INTEGRATION TODO: Put this right under the calendar -->
<alert
v-if="selectedProvider === null"
ref="alertNoAvailableShops"
cmsWidgetName="AlertNoAvailableShopsWidget"
:isCollapsible="true"
alertClass="alert-danger" />
</div>
<div
v-if="isMobile">
<div class="expandable-link-container">
<a
v-if="displayMilitaryZipAlert && !militaryBaseWarningExpanded"
href="#"
class="expandable-link"
@click.prevent="militaryBaseWarningExpanded = true">
{{ militaryBaseWarningLinkText }}
</a>
<div
v-if="displayMilitaryZipAlert && militaryBaseWarningExpanded"
class="expandable-link-text">
{{ militaryBaseWarningText }}
</div>
</div>
<serviceZipQuestion
ref="mobileServiceZipCodeQuestion"
v-model="mobileZipCode"
customInputId="mobileServiceZipCode"
class="mobile-service-zip-question"
:placeholderText="mobileZipPlaceholder"
:serviceZipFormatErrorMessage="errorMessages.MOBILE_SERVICE_ZIP_FORMAT"
:hasError="mobileZipError !== ''"
cmsWidgetName="MobileZipWidget" />
<div
v-if="mobileZipError"
ref="errorMessageDiv"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ mobileZipError }}</span>
</div>
<textBlock
v-if="displayMobileFeeDisclaimer"
cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="disclaimer" />
</div>
<contentGroupModal
:ref="RECAL_MODAL_REF_NAME"
cssModalHeadlineClass="text-center"
@ -104,7 +124,7 @@
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="
!meta.valid || displayNoShopsAlert
!meta.valid || displayNoShopsAlert || displayBigTruckNoShops
"
@backClicked="navigateBack(this, navigateBackScenario)"
@forwardClicked="forwardButtonAction" />
@ -118,44 +138,31 @@
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import settleAllPromises from '@/helpers/layout-helper';
import { required } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store';
import {
getPricedMobileFeePart,
getServiceabilityDetails,
getZipCodeData
getZipCodeData,
getMobileZipCodeData
} from '@/helpers/service-location-helper';
import { toTitleCase } from '@/helpers/text-helper.js';
import { getAvailabilityRating } from '@/helpers/service-location-helper';
// Import Component
import alert from '@/ux-components/alert/alert.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import appointmentTypeQuestion from '@/layouts/service-location/appointment-type-question/appointment-type-question.vue';
import baseFormMixin from '@/mixins/base-form-mixin';
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
import mobileLocationModalQuestions from '@/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue';
import { Form, defineRule } from 'vee-validate';
import shopQuestion from '@/layouts/service-location/shop-question/shop-question.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import serviceZipModalQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue';
import shopAddress from '@/layouts/service-location/shop-address/shop-address.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import widgetFields from '@/constants/cms-widget-fields';
// DEFINE VALIDATION RULES
defineRule('mobile-location-required', (value) => {
if (
value.addressQuestions.streetAddress === ''
|| value.addressQuestions.city === ''
|| value.addressQuestions.state === ''
|| value.addressQuestions.zipCode === ''
|| value.isVehicleProtected == null
) {
return errorMessages.MOBILE_LOCATION_REQUIRED;
}
return true;
});
defineRule('selection-required', required(errorMessages.OPTION_REQUIRED));
const SHOP_QUESTION_REF_NAME = 'shopQuestion';
const RECAL_MODAL_REF_NAME = 'RecalModal';
const SITE_FOOTER_REF_NAME = 'siteFooter';
@ -163,16 +170,16 @@ export default {
name: 'service-location',
components: {
alert,
textBlock,
appointmentTypeQuestion,
contentGroupModal,
mobileLocationModalQuestions,
siteFooter,
siteHeader,
siteSubHeader,
// eslint-disable-next-line vue/no-reserved-component-names
Form,
serviceZipModalQuestion,
shopQuestion
shopAddress,
serviceZipQuestion
},
mixins: [baseFormMixin],
async beforeRouteEnter(to, from, next) {
@ -182,8 +189,8 @@ export default {
const zipCodeData = getZipCodeData(serviceZipCode);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData(serviceZipCode);
const getGlassFeesPromise = useMainStore().getGlassFees();
const providersPromise = useMainStore().getProviders(serviceZipCode);
// Settle promises and get results
const promiseResultMap = [
@ -203,13 +210,13 @@ export default {
resultKey: 'serviceabilityDetails',
promise: serviceabilityDetailsPromise
},
{
resultKey: 'shopQuestionInitialData',
promise: shopQuestionInitialDataPromise
},
{
resultKey: 'zipCodeData',
promise: zipCodeData
},
{
resultKey: 'providers',
promise: providersPromise
}
];
@ -221,10 +228,9 @@ export default {
resultMap.zipCodeData,
resultMap.serviceabilityDetails,
resultMap.mobileFeePart,
resultMap.shopQuestionInitialData?.mobileProviderNumber,
resultMap.glassFees
resultMap.glassFees,
resultMap.providers
);
vm.$refs[SHOP_QUESTION_REF_NAME].initializeComponent(resultMap.shopQuestionInitialData);
});
},
setup() {
@ -238,7 +244,6 @@ export default {
city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(),
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
isGlassServiceableInshop: null,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null,
@ -249,9 +254,13 @@ export default {
mobileProviderNumber: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
SHOP_QUESTION_REF_NAME,
mobileZipCode: '',
mobileZipError: '',
militaryBaseWarningExpanded: false,
availabilityRating: null,
RECAL_MODAL_REF_NAME,
SITE_FOOTER_REF_NAME
SITE_FOOTER_REF_NAME,
errorMessages
};
},
computed: {
@ -264,61 +273,6 @@ export default {
answersFromCms() {
return this.getCmsContent('ServiceTypeQuestionWidget', 'Answers');
},
serviceZipCodeQuestion: {
get() {
return {
state: this.state,
zipCode: this.zipCode
};
},
set(newValue) {
if (newValue.zipCode !== this.zipCode) {
this.resetMobileLocation();
this.selectedAppointmentType = null;
this.selectedProvider = null;
}
this.state = newValue.state;
this.zipCode = newValue.zipCode;
// eslint-disable-next-line vue/valid-next-tick
this.$nextTick();
}
},
mobileLocationQuestions: {
get() {
return {
addressQuestions: {
streetAddress: this.streetAddress,
streetAddress2: this.streetAddress2,
city: this.city,
state: this.state,
zipCode: this.zipCode
},
isVehicleProtected: this.isVehicleProtected
};
},
set(newValue) {
this.streetAddress = newValue.addressQuestions.streetAddress;
this.streetAddress2 = newValue.addressQuestions.streetAddress2;
this.city = newValue.addressQuestions.city;
this.state = newValue.addressQuestions.state;
this.zipCode = newValue.addressQuestions.zipCode;
this.isVehicleProtected = newValue.isVehicleProtected;
if (newValue.zipCode !== this.zipCode) {
if (
!(
this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
)
) {
this.selectedAppointmentType = null;
}
this.selectedProvider = null;
}
}
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
return (
@ -338,22 +292,14 @@ export default {
return this.isGlassServiceableInshop;
},
isShopQuestionDisplayed() {
isInshop() {
return (
this.selectedAppointmentType === 'Inshop'
|| this.selectedAppointmentType === 'Dropoff'
);
},
isAppointmentTypeDisplayed() {
return this.zipCode && !this.displayNoShopsAlert;
},
isMobileLocationDisplayed() {
return (
this.selectedAppointmentType
=== AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType
=== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
);
isMobile() {
return this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
},
requiresInshopRecalibration() {
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
@ -375,15 +321,17 @@ export default {
displayRecalibrationWarning() {
return this.requiresInshopRecalibration;
},
displayLowAvailabilityInshop() {
return this.availabilityRating === 'low' && this.isInshop;
},
displayServiceableInshopOnly() {
return (
!this.displayRecalibrationWarning
&& this.isServiceableInshop
&& !this.isServiceableMobile
);
return !this.isServiceableMobile && this.isServiceableInshop && !this.displayRecalibrationWarning;
},
displayServiceableMobileOnly() {
return this.isServiceableMobile && !this.isServiceableInshop;
return this.isServiceableMobile && this.recalibrationRequired && !this.isServiceableInshop;
},
displayMobileFeeDisclaimer() {
return this.mainStore.isNoComp || this.mainStore.isITAC;
},
navigateBackScenario() {
const { isNoComp, isITAC } = useMainStore();
@ -393,6 +341,38 @@ export default {
},
isBigTruck() {
return this.mainStore.order.vehicle.isBigTruck;
},
mobileZipPlaceholder() {
return this.getCmsContent('MobileZipPlaceHolderWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
militaryBaseWarningLinkText() {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.HEADLINE_TEXT);
},
militaryBaseWarningText() {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
},
appointmentQuestionText() {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
},
scheduleRecalText() {
if(this.requiresInshopRecalibration) {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
} else {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
}
},
inShopOnlyCopy() {
let content = this.getCmsContent('AlertInshopOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
mobileOnlyCopy() {
let content = this.getCmsContent('AlertMobileOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
recalibrationRequired() {
return this.mainStore.hasRecalibrationPart;
}
},
methods: {
@ -401,13 +381,10 @@ export default {
useMainStore().order.serviceLocation.zipCode !== null
);
},
async reloadShopData(zipCode) {
await this.$refs[SHOP_QUESTION_REF_NAME].reloadShopData(zipCode);
},
async forwardButtonAction() {
let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null);
if (this.isMobileLocationDisplayed) {
if (this.isMobile) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
this.mainStore.updateMobileFee(this.mobileFeePart);
}
@ -432,7 +409,6 @@ export default {
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: this.selectedAppointmentType,
isVehicleProtected: this.isVehicleProtected,
provider
});
@ -466,9 +442,6 @@ export default {
|| useMainStore().order.customer.address.zipCode
);
},
getIsVehicleProtectedFromStore() {
return useMainStore().order.serviceLocation.isVehicleProtected;
},
getSelectedAppointmentType() {
return useMainStore().order.serviceLocation.appointmentType;
},
@ -482,12 +455,13 @@ export default {
zipCodeData,
serviceabilityDetails,
mobileFeePart,
mobileProviderNumber,
glassFees
glassFees,
providers
) {
if (zipCodeData) {
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
this.zipCodeCtu = zipCodeData.zipCodeCtu;
this.city = zipCodeData.city;
}
if (serviceabilityDetails) {
@ -498,8 +472,24 @@ export default {
this.mobileFeePart = mobileFeePart;
}
if (mobileProviderNumber) {
this.setMobileProviderNumber(mobileProviderNumber);
if (providers) {
this.setMobileProviderNumber(providers.mobileProviderNumber);
let foundMatch = false;
if(this.selectedProvider && this.selectedProvider.providerNumber) {
const matchedProvider = providers.shopProviders.find(
(provider) => provider.providerNumber === this.selectedProvider.providerNumber
);
if (matchedProvider) {
foundMatch = true;
this.selectedProvider = matchedProvider;
}
}
if(providers.shopProviders.length > 0 && !foundMatch) {
this.selectedProvider = providers.shopProviders[0];
}
else if(providers.shopProviders.length === 0) {
this.selectedProvider = null;
}
}
if (glassFees) {
@ -521,13 +511,6 @@ export default {
setMobileProviderNumber(providerNumber) {
this.mobileProviderNumber = providerNumber;
},
resetMobileLocation() {
this.streetAddress = '';
this.streetAddress2 = '';
this.city = '';
this.isVehicleProtected = null;
},
setServiceabilityDetails(serviceabilityDetails) {
this.isGlassServiceableInshop =
serviceabilityDetails.isGlassServiceableInshop;
@ -537,14 +520,94 @@ export default {
serviceabilityDetails.isGlassServiceableMobile;
this.isRecalibrationServiceableMobile =
serviceabilityDetails.isRecalibrationServiceableMobile;
if (!this.isServiceableMobile) {
this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP;
}
},
async updateMobileZip() {
this.mobileZipError = '';
const zipCodeData = await getMobileZipCodeData(this.mobileZipCode);
if (!zipCodeData.isValid) {
this.mobileZipError = errorMessages.INVALID_ZIP;
} else if (!zipCodeData.isServiceable) {
this.mobileZipError = errorMessages.NO_SERVICE_IN_AREA(toTitleCase(zipCodeData.city));
} else {
this.city = zipCodeData.city;
this.zipCode = this.mobileZipCode;
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.mobileZipCode);
serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
this.setServiceabilityDetails(details);
}
});
const providersPromise = useMainStore().getProviders(this.mobileZipCode);
providersPromise.then((result) => {
const providers = result.data;
if (providers) {
this.setMobileProviderNumber(providers.mobileProviderNumber);
if(providers.shopProviders.length > 0) {
this.selectedProvider = providers.shopProviders[0];
}
else {
this.selectedProvider = null;
}
}
});
}
},
async handleInShopZipUpdated(newZip) {
this.zipCode = newZip;
const zipCodeData = await getZipCodeData(this.zipCode);
this.city = zipCodeData.city;
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.zipCode);
serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
this.setServiceabilityDetails(details);
}
});
}
},
watch: {
mobileZipCode(newZip) {
if(newZip !== '') {
this.updateMobileZip();
}
},
selectedAppointmentType() {
this.mobileZipCode = '';
if(this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP && this.selectedProvider) {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
getAvailabilityRating(
formattedStartDate,
formattedEndDate,
AppointmentTypeStrings.IN_SHOP,
this.selectedProvider ? this.selectedProvider.providerNumber : null
).then((rating) => {
this.availabilityRating = rating;
});
}
}
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
$page-side-padding: 1.5rem;
.iss-heritage-container-width {
.service-location-container {
position: relative;
@ -554,58 +617,41 @@ $page-side-padding: 1.5rem;
}
}
.question-text {
span {
line-height: 1.5rem;
text-align: center;
.service-location-container {
.subheader {
margin: 1.25rem 0;
}
}
.list-card-content p {
&:first-of-type {
line-height: 1.5rem;
.form-test-error {
font-weight: $font-weight-bold;
}
&:not(:nth-of-type(1)) {
line-height: 1.25rem;
}
}
.choose-option {
.button-question > div {
&:first-of-type {
margin-bottom: 0.95rem;
line-height: 1.5rem;
.appointment-type-question-text {
color: $black;
font-weight: 600;
margin-bottom: 1rem;
font-size: 1rem;
line-height: 1.625rem;
flex-direction: column;
.recal-text {
margin-bottom: 1.25rem;
font-weight: $font-weight-bold;
}
}
}
.button-question {
.row.form-test-error {
line-height: 1.5rem;
padding-left: 0rem !important;
}
}
.appointment-type-question {
.row {
margin-top: 0;
.col-12 {
margin: 0 0 .5rem 0;
&:last-child {
margin-bottom: 0;
}
.mobile-service-zip-question {
:deep(.form-test-error span) {
font-weight: $font-weight-bold;
}
}
.question-text {
margin: 0;
span {
padding: 1rem 0;
text-align: center;
}
.expandable-link-container {
margin-bottom: 1rem;
}
}
.service-location-button-question {
.question-text {
margin-top: 1.5rem;
span {
text-align: center;
.expandable-link {
font-style: italic;
font-weight: $font-weight-bold;
text-decoration: none;
color: $heritage-blue-primary;
line-height: 1.375rem;
&:hover {
text-decoration: underline;
}
}
}

View file

@ -35,7 +35,7 @@
<script>
import textLink from '@/ux-components/text-link/text-link.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import modal from '@/digital-components/modal/modal.vue';
import alert from '@/ux-components/alert/alert.vue';

View file

@ -1,51 +0,0 @@
<template>
<textboxQuestion
ref="zipInputTextQuestion"
v-model="value"
:cmsWidgetName="cmsWidgetName"
inputId="serviceZipCode"
questionAlignment="center"
cornerStyle="rounded"
mask="#####"
:displayQuestionText="false"
isRequired
validationRules="zip-required|zip-format" />
</template>
<script>
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
// Supporting Files
import { defineRule } from 'vee-validate';
import { required, regex } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages';
// Define Validation Rules
defineRule('zip-required', required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule('zip-format', regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
export default {
name: 'service-zip-question',
components: {
textboxQuestion
},
// TODO: Correct prop def.
props: {
modelValue: {
serviceZipCode: String
},
cmsWidgetName: String
},
emits: ['update:modelValue'],
computed: {
value: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit('update:modelValue', newValue);
}
}
}
};
</script>

View file

@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import serviceZipQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
describe('service-zip-question.vue', () => {
it('Should get the modelValue', async () => {
// Arrange
const text = 'test';
const text = '12345';
const wrapper = shallowMount(serviceZipQuestion, {
props: {
modelValue: text
@ -13,16 +13,16 @@ describe('service-zip-question.vue', () => {
});
// Act
const modelValueText = wrapper.vm.value;
wrapper.vm.value = 'test also';
const modelValueText = wrapper.vm.internalZipcode;
wrapper.vm.internalZipcode = '55555';
// Assert
expect(modelValueText).toEqual('test');
expect(modelValueText).toEqual('12345');
});
it('Should emit to set value', async () => {
// Arrange
const text = 'test';
const text = '12345';
const wrapper = shallowMount(serviceZipQuestion, {
props: {
modelValue: text
@ -31,9 +31,10 @@ describe('service-zip-question.vue', () => {
});
// Act
wrapper.vm.value = 'test also';
wrapper.vm.internalZipcode = '55555';
wrapper.vm.updateModelValue();
// Assert
expect(wrapper.emitted('update:modelValue')).toEqual([['test also']]);
expect(wrapper.emitted('update:modelValue')).toEqual([['55555']]);
});
});

View file

@ -0,0 +1,74 @@
<template>
<textboxQuestion
ref="zipInputTextQuestion"
v-model="internalZipcode"
:cmsWidgetName="cmsWidgetName"
inputId="serviceZipCode"
cornerStyle="rounded"
mask="#####"
isRequired
:hasError="hasError"
@clickEvent="updateModelValue"
:validationRules="`zip-required|${cmsWidgetName}-zip-format`" />
</template>
<script>
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
// Supporting Files
import { defineRule } from 'vee-validate';
import { required, regex } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages';
// Define Validation Rules
defineRule('zip-required', required(errorMessages.SERVICE_ZIP_REQUIRED));
export default {
name: 'service-zip-question',
components: {
textboxQuestion
},
// TODO: Correct prop def.
props: {
modelValue: String,
cmsWidgetName: String,
serviceZipFormatErrorMessage: {
type: String,
default: errorMessages.SERVICE_ZIP_FORMAT
},
hasError: {
type: Boolean,
default: false
}
},
data() {
return {
internalZipcode: this.modelValue
}
},
emits: ['update:modelValue'],
methods: {
updateModelValue() {
this.$emit('update:modelValue', this.internalZipcode);
}
},
watch: {
modelValue(newValue) {
this.internalZipcode = newValue;
}
},
mounted() {
defineRule(`${this.cmsWidgetName}-zip-format`, regex(/^\d{5}$/, this.serviceZipFormatErrorMessage));
}
};
</script>
<style lang="scss" scoped>
.textbox-question.has-success {
:deep(input.form-control) {
border: $border-input;
&:focus {
border: $border-input-focus;
}
}
}
</style>

View file

@ -0,0 +1,411 @@
<template>
<transition
name="fade"
mode="out-in">
<div
class="shop-address"
aria-live="polite">
<div class="address-header">{{ questionText }}</div>
<div
v-if="modelValue"
class="current-address">
<div class="address-line">
<span class="shop-name">{{ displayedShopName }}</span>
<span class="shop-distance">{{ modelValue.distanceInMiles?.toFixed(2) }} mi</span>
</div>
<div class="address-line">{{ getProviderAddress(modelValue) }}</div>
<div class="address-line">
{{ getProviderCityZipState(modelValue) }}
</div>
</div>
<div
v-else
class="no-shops-found">
{{ `No shops found within 100 miles of ${internalZipcode}` }}
</div>
<buttonMain
class="more-shops-button"
:buttonText="showMoreShopsLinkText"
:variant="'primary'"
@clickEvent="openModal" />
<alert
class="alert-shop-distance"
v-if="modelValue?.distanceInMiles > 30"
ref="alertShopDistance"
cmsWidgetName="AlertShopDistanceWidget"
alertClass="alert-warning" />
<modal
:ref="modalName"
:headerText="modalHeaderText"
:onModalOpenedCallback="onModalOpened"
:footerButtonText="modalFooterText"
:modalPosition="modalPositions.center"
:allowInvalidSubmit="true"
@footerButtonEvent="updateSelectedProvider">
<googleMap
id="map"
class="mb-4"
:markers="providerAddresses"
:zipCode="internalZipcode" />
<dropdownQuestion
inputId="searchRadiusDropdown"
ref="searchRadiusQuestion"
v-model="searchRadiusInMiles"
:cmsWidgetName="searchRadiusQuestionWidgetName"
:variant="dropdownVariants.compact"
:options="searchRadiusOptions" />
<buttonQuestion
ref="shopListButtonQuestion"
v-model="internalProviderNumber"
buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton"
class="radioQuestion"
:answers="nearbyShopsData"
groupName="chooseShop"
textPosition="text-start"
isRequired
validationRules="option-required"
:additionalButtonData="additionalButtonData" />
<span class="service-zip-prompt">
{{ serviceZipPrompt }}
</span>
<serviceZipQuestion
v-if="renderServiceZip"
:ref="SERVICE_ZIP_QUESTION_REF_NAME"
v-model="internalZipcode"
customInputId="serviceZipCode"
:cmsWidgetName="textboxQuestionWidgetName" />
<div
v-if="errorMessage"
ref="errorMessageDiv"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ errorMessage }}</span>
</div>
</modal>
</div>
</transition>
</template>
<script>
// Components
import alert from '@/ux-components/alert/alert.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import modal from '@/digital-components/modal/modal.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import shopListButton from '@/iss-components/shop-list-button/shop-list-button.vue';
import dropdownQuestion from '@/digital-components/dropdown-question/dropdown-question.vue';
import googleMap from '@/iss-components/google-map/google-map.vue';
// Supporting files
import baseMixin from '@/mixins/base-mixin.js';
import { toTitleCase } from '@/helpers/text-helper.js';
import { markRaw } from 'vue';
import { getAvailabilityRating } from '@/helpers/service-location-helper';
import { useMainStore } from '@/store';
import widgetFields from '@/constants/cms-widget-fields';
import { dropdownVariants, modalPositions } from '@/constants/component-variants';
import errorMessages from '@/constants/error-messages';
import { defineRule } from 'vee-validate';
import { required } from '@/helpers/validation-rules';
defineRule('option-required', required(errorMessages.PROVIDER_REQUIRED));
const SERVICE_ZIP_QUESTION_REF_NAME = 'serviceZipCodeQuestion';
export default {
name: 'shop-address',
components: {
alert,
buttonMain,
modal,
serviceZipQuestion,
buttonQuestion,
dropdownQuestion,
googleMap
},
mixins: [baseMixin],
props: {
modelValue: {
type: Object,
default: () => null
},
serviceZipcode: String,
cmsWidgetName: String,
validationRules: String,
modalWidgetName: String,
selectedAppointmentType: String,
},
data() {
return {
toTitleCase,
serviceZipCodeTextInputId: '',
internalZipcode: this.serviceZipcode,
internalProviderNumber: this.modelValue?.providerNumber,
searchRadiusQuestionWidgetName: 'SearchRadiusQuestionWidget',
textboxQuestionWidgetName: 'ChangeLocationZipQuestionWidget',
shopListButton: markRaw(shopListButton),
searchRadiusInMiles: "25",
nearbyShops: [],
modalPositions,
dropdownVariants,
renderServiceZip: true,
errorMessage: '',
openingModal: false,
SERVICE_ZIP_QUESTION_REF_NAME
};
},
emits: ['update:modelValue', 'zip-updated'],
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT);
},
showMoreShopsLinkText() {
return this.getCmsContent('ShowMoreShopsLinkWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
displayedShopName() {
if(this.modelValue.isSafeliteShop) {
return this.getCmsContent('SafeliteShopNameWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}
return toTitleCase(this.modelValue.companyName);
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT).replaceAll('{custom:serviceZipcode}', this.internalZipcode);
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT);
},
modalName() {
return this.modalWidgetName;
},
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
return {
displayAvailabilityIndicators: true,
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: "InshopOrDropoff"
};
},
serviceZipPrompt() {
return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
},
nearbyShopsData() {
return this.nearbyShops.map((shopProvider) => {
const city = toTitleCase(shopProvider.address.city);
const distanceInMiles = shopProvider.distanceInMiles.toFixed(2);
return {
buttonLabel: city,
buttonLabelSubCopy: `${distanceInMiles} mi`,
buttonBodyCopy: this.getFullProviderAddress(shopProvider),
value: shopProvider.providerNumber
};
});
},
searchRadiusArray() {
const options = this.getCmsContent(this.searchRadiusQuestionWidgetName, widgetFields.INPUT_QUESTION_WIDGET.ANSWERS);
return options;
},
searchRadiusOptions() {
const optionsObj = {};
if(this.searchRadiusArray) {
this.searchRadiusArray.forEach(option => {
optionsObj[option.Name] = option.Text;
});
}
return optionsObj;
},
providerAddresses() {
return this.nearbyShops?.map((provider) => ({
title: toTitleCase(provider.companyName),
fullAddress: this.getFullProviderAddress(provider),
addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)]
})) ?? [];
},
},
watch: {
async internalZipcode() {
if(this.openingModal) {
return;
}
const shopsUpdated = await this.updateShops();
if (shopsUpdated) {
await this.$nextTick();
this.$refs[SERVICE_ZIP_QUESTION_REF_NAME].internalZipcode = '';
}
},
async searchRadiusInMiles() {
if(this.openingModal) {
return;
}
await this.updateShops();
}
},
methods: {
async updateShops(autoExpand = false) {
this.errorMessage = '';
let result = await this.getNearbyShops(this.searchRadiusInMiles);
let currentSearchIndex = this.searchRadiusArray.findIndex(option => option.Name === this.searchRadiusInMiles);
while (autoExpand && result.length === 0 && currentSearchIndex < this.searchRadiusArray.length - 1) {
const newSearchRadius = this.searchRadiusArray[currentSearchIndex + 1].Name;
result = await this.getNearbyShops(newSearchRadius);
currentSearchIndex++;
}
if (result.length === 0) {
this.errorMessage = errorMessages.ZIP_CODE_NOT_SERVICED_FOR_VEHICLE;
}
else {
if (!result.find(shop => shop.providerNumber === this.internalProviderNumber)) {
this.internalProviderNumber = '';
await this.$nextTick();
}
this.nearbyShops = result;
this.searchRadiusInMiles = this.searchRadiusArray[currentSearchIndex].Name;
return true;
}
},
openModal() {
this.$refs[this.modalName].openModal();
},
closeModal() {
this.$refs[this.modalName].closeModal();
},
onModalOpened() {
this.openingModal = true;
this.internalZipcode = this.serviceZipcode;
this.searchRadiusInMiles = "25";
this.nearbyShops = [];
this.updateShops(true).then(() => {
this.internalProviderNumber = this.nearbyShops[0]?.providerNumber;
}).finally(() => {
this.$nextTick().then(() => {
this.openingModal = false;
});
});
this.forceServiceZipRerender();
},
async updateSelectedProvider() {
const selectedShop = this.nearbyShops.find(shop => shop.providerNumber === this.internalProviderNumber);
if (selectedShop) {
this.$emit('update:modelValue', selectedShop);
this.$emit('zip-updated', this.internalZipcode);
this.closeModal();
}
},
async getNearbyShops(radiusInMiles) {
const result = await useMainStore().getProviders(this.internalZipcode, radiusInMiles);
return result.data.shopProviders;
},
getFullProviderAddress(provider) {
const addressLine1 = this.getProviderAddress(provider);
const addressLine2 = this.getProviderCityZipState(provider);
const joinString =
addressLine1.length > 0 && addressLine2.length > 0 ? ', ' : '';
return [addressLine1, addressLine2].join(joinString);
},
getProviderAddress(provider) {
return toTitleCase(provider?.address?.streetAddress);
},
getProviderCityZipState(provider) {
const city = toTitleCase(provider?.address?.city);
const state = provider?.address?.state ?? '';
const zipCode = provider?.address?.zipCode ?? '';
let addressLine2 = '';
if (city) {
addressLine2 += city;
if (state || zipCode) {
addressLine2 += state ? ', ' : ' ';
}
}
if (state) {
addressLine2 += state;
addressLine2 += zipCode ? ' ' : '';
}
if (zipCode) {
addressLine2 += zipCode;
}
return addressLine2;
},
forceServiceZipRerender() {
this.renderServiceZip = false;
this.$nextTick(() => {
this.renderServiceZip = true;
});
},
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
.shop-address {
.address-header {
color: $black;
font-weight: 600;
margin-bottom: 0.625rem;
}
.address-line {
display: flex;
}
.shop-distance {
margin-left: auto;
}
.current-address {
margin: .625rem 0;
}
.no-shops-found {
font-size: .75rem;
margin: .75rem 0rem;
font-weight: $font-weight-bold;
line-height: 1.375rem;
}
.more-shops-button {
margin-bottom: 1.25rem;
}
.alert-shop-distance {
margin-top: 2.5rem;
}
:deep(.modal-title) {
font-weight: 400;
display: block;
width: fit-content;
}
.radioQuestion {
margin-bottom: .625rem;
}
.service-zip-prompt {
color: $black;
font-weight: 600;
}
:deep(#map) {
height: 27rem;
}
:deep(.textbox-question) {
&.has-error {
input.form-control {
border: $border-input;
&:focus {
border: $border-input-focus;
}
}
}
.form-label {
font-weight: $font-weight-normal;
color: $darker-gray;
}
.btn-primary {
margin-bottom: 1.25rem;
}
}
}
</style>

View file

@ -1,294 +0,0 @@
<template>
<transition
name="fade"
mode="out-in">
<div
v-if="isDisplayed"
class="shop-question"
aria-live="polite">
<alert
v-if="displayDropoffInformation"
ref="alertDropoffInformation"
class="mb-4 drop-off-alert"
cmsWidgetName="AlertDropoffInformationWidget"
alertClass="alert-info"
:isDismissible="false" />
<buttonQuestion
ref="buttonQuestion"
v-model="selectedProviderNumber"
buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton"
class="radioQuestion"
:questionText="questionText"
:answers="answers"
groupName="chooseShop"
textPosition="text-start"
isRequired
validationRules="option-required"
:additionalButtonData="additionalButtonData" />
<textLink
v-show="displaySeeMoreLocationsLink"
id="showMoreShopsId"
ref="showMoreShopsLink"
class="show-more-shops-link"
cmsWidgetName="ShowMoreShopsLinkWidget"
linkType="text"
:text="showMoreShopsLinkText"
href="#!"
:aria-label="showMoreShopsLinkText"
@clickEvent="getNextShopsFromList" />
</div>
</transition>
</template>
<script>
// Components
import alert from '@/ux-components/alert/alert.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
// Supporting files
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import baseMixin from '@/mixins/base-mixin.js';
import { defineRule } from 'vee-validate';
import { required } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store/index.js';
import { markRaw, nextTick } from 'vue';
import { getAvailabilityRating } from '@/helpers/service-location-helper';
import shopListButton from '@/iss-components/shop-list-button/shop-list-button.vue';
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
export default {
name: 'shop-question',
components: {
alert,
buttonQuestion,
textLink
},
mixins: [baseMixin],
props: {
modelValue: {
type: Object,
default: () => null
},
selectedAppointmentType: String,
cmsWidgetName: String,
validationRules: String,
isDisplayed: Boolean
},
emits: ['update:modelValue', 'updatedMobileProviderNumber'],
data() {
return {
shopProviders: [],
shopListButton: markRaw(shopListButton),
answers: [],
shopIndex: 0,
displaySeeMoreLocationsLink: false
};
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
},
selectedValue: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit('update:modelValue', newValue);
}
},
selectedProviderNumber: {
get() {
return this.selectedValue?.providerNumber;
},
set(newValue) {
// Button Question only supports Number, or String data types so we must get the full object to emit
this.selectedValue = this.getSelectedProviderObject(newValue);
}
},
displayDropoffInformation() {
return this.selectedAppointmentType === 'Dropoff';
},
showMoreShopsLinkText() {
return this.getCmsContent('ShowMoreShopsLinkWidget', 'Text');
},
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
return {
displayAvailabilityIndicators: true,
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.selectedAppointmentType
};
}
},
watch: {
selectedAppointmentType: {
async handler(newValue) {
this.resetAnswers();
await nextTick();
this.selectedProviderNumber = null;
await nextTick();
if (newValue !== AppointmentTypeStrings.MOBILE && newValue !== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
this.getNextShopsFromList();
}
}
},
shopProviders: {
async handler(newValue) {
await nextTick();
if (this.selectedAppointmentType) {
const selectedShopIndex = this.getSelectedProviderIndex(
newValue,
this.selectedProviderNumber
);
if (selectedShopIndex >= 3) {
await this.getNextShopsFromList(selectedShopIndex + 1);
} else {
await this.getNextShopsFromList();
await nextTick();
}
}
}
}
},
methods: {
loadInitialData(serviceZipCode) {
return this.loadData(serviceZipCode);
},
loadData(serviceZipCode) {
return useMainStore().getProviders(serviceZipCode);
},
initializeComponent(shopQuestionInitialData) {
this.shopProviders = shopQuestionInitialData.shopProviders;
},
async getNextShopsFromList(numberToGet = 3) {
const shopIterator = (array, n) => {
const l = array.length;
return () => {
const end = this.shopIndex + n;
const part = array.slice(this.shopIndex, end);
this.shopIndex = end < l ? end : this.shopProviders.length;
return part;
};
};
const toTitleCase = (str) => str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
const nextShop = shopIterator(this.shopProviders, numberToGet);
// Map API result data
const mappedData = nextShop().map((shopProvider) => {
const streetAddress = toTitleCase(shopProvider.address.streetAddress);
const city = toTitleCase(shopProvider.address.city);
const { state } = shopProvider.address;
const { zipCode } = shopProvider.address;
const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
return {
buttonLabel: city,
buttonLabelSubCopy: `${distanceInMiles} mi`,
buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`,
value: shopProvider.providerNumber
};
});
if (this.answers.length === 0) {
this.answers = mappedData;
} else {
mappedData.forEach((shop) => {
this.answers.push(shop);
});
}
await nextTick();
if (this.shopIndex === this.shopProviders.length) {
this.displaySeeMoreLocationsLink = false;
} else {
this.displaySeeMoreLocationsLink = true;
}
await nextTick();
this.scrollToPageBottom();
},
resetAnswers() {
this.answers = [];
this.shopIndex = 0;
},
async reloadShopData(serviceZipCode) {
const result = await this.loadData(serviceZipCode);
this.initializeComponent(result.data);
if (result.data?.mobileProviderNumber
&& (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP)) {
this.$emit('updatedMobileProviderNumber', result.data.mobileProviderNumber);
}
this.resetAnswers();
await nextTick();
await this.getNextShopsFromList();
},
getSelectedProviderObject(providerNumber) {
const provider = this.shopProviders?.find((p) => p.providerNumber === providerNumber);
return provider;
},
getSelectedProviderIndex(providers, selectedProviderNumber) {
const index = providers.findIndex((p) => p.providerNumber === selectedProviderNumber);
return index;
}
}
};
</script>
<style lang="scss">
@import "@/styles/ux-variables-svg-strings.scss";
.shop-question {
margin-top: 1rem;
text-align: center;
.button-question {
.question-text {
margin-top: 0.5rem;
}
}
.drop-off-alert {
background-image: url($svg-drop-off-alert);
background-repeat: no-repeat;
background-size: 0.75rem;
background-position: 0.5rem 0.75rem;
border-radius: 0.5rem;
display: flex;
flex-direction: row;
padding: 0.5rem 0.5rem 0.5rem 1.5rem !important;
gap: 0.25rem;
.alert-heading {
text-align: left;
font-size: 0.75rem;
line-height: 1.25rem;
}
}
}
</style>

View file

@ -2,24 +2,20 @@
exports[`tpa-submit returns the initial data 1`] = `
Object {
"companyName": "Frederick Jones",
"customValueMap": Object {
"glassShop": "Frederick Jones",
"modalPositions": Object {
"center": "center",
"edge": "edge",
},
"sections": Array [],
"widget": Object {
"damageLocations": "DamageLocationsWidget",
"alertIncomplete": "AlertIncompleteWidget",
"alertRecalWarning": "AlertRecalWarningWidget",
"contactDetails": "ContactDetailsSectionWidget",
"editShopLinkText": "EditShopLinkTextWidget",
"footer": "SiteFooterWidget",
"orderDetails": "OrderDetailsContent",
"serviceSummary": "ServiceSummaryContent",
"siteHeader": "SiteHeaderWidget",
"siteSubHeader": "SiteSubHeaderWidget",
"subheader": Object {
"contactInfo": "ContactDetailsSubTitle",
"damage": "DamageSubTitle",
"shop": "PreferredShopSubTitle",
"vehicle": "VehicleSubTitle",
},
},
}
`;

View file

@ -3,12 +3,18 @@
exports[`contact-details-drawer snapshot matches returns the initial data 1`] = `
Object {
"emailAddress": "fred.tay@gmail.com",
"extension": null,
"firstName": "Frederick",
"isModalOpened": false,
"lastName": "Taylor",
"modalPositions": Object {
"center": "center",
"edge": "edge",
},
"phoneNumber": "606-009-2943",
"rules": Object {
"emailAddress": "email-required|email-address-format",
"extension": "extension-format",
"firstName": "first-name-required",
"lastName": "last-name-required",
"phoneNumber": "phone-number-required|phone-number-format",
@ -16,6 +22,7 @@ Object {
"widget": Object {
"drawerFooter": "ContactDetailsDrawerFooterWidget",
"emailQuestion": "EmailQuestionWidget",
"extensionQuestion": "ExtensionQuestionWidget",
"firstNameQuestion": "FirstNameQuestionWidget",
"lastNameQuestion": "LastNameQuestionWidget",
"phoneNumberQuestion": "PhoneNumberQuestionWidget",

View file

@ -65,15 +65,16 @@ describe('contact-details-drawer', () => {
describe('method', () => {
describe('saveContactDetails', () => {
test.each([
['Sarah', 'Jones', 's.jones@gmail.com', '724-996-0909'],
[null, 'Jones', 's.jones@gmail.com', '724-996-0909'],
['Sarah', null, 's.jones@gmail.com', '724-996-0909'],
['Sarah', 'Jones', null, '724-996-0909'],
['Sarah', 'Jones', 's.jones@gmail.com', null]
['Sarah', 'Jones', 's.jones@gmail.com', '724-996-0909', '12345'],
[null, 'Jones', 's.jones@gmail.com', '724-996-0909', '12345'],
['Sarah', null, 's.jones@gmail.com', '724-996-0909', '12345'],
['Sarah', 'Jones', null, '724-996-0909', '12345'],
['Sarah', 'Jones', 's.jones@gmail.com', null, '12345'],
['Sarah', 'Jones', 's.jones@gmail.com', '724-996-0909', null],
])(
'when first name data "%p", last name "%p", email "%p", and phone number "%p", updateContactInfo called with expected',
(firstName, lastName, emailAddress, phoneNumber) => {
'when first name data "%p", last name "%p", email "%p", phone number "%p", and extension "%p", updateContactInfo called with expected',
(firstName, lastName, emailAddress, phoneNumber, extension) => {
// Arrange
const mainInitialState = {
order: {
@ -81,12 +82,13 @@ describe('contact-details-drawer', () => {
firstName: 'Frederick',
lastName: 'Taylor',
emailAddress: 'fred.tay@gmail.com',
servicePhone: '606-009-2943'
servicePhone: '606-009-2943',
extension: '11111'
}
}
};
const initialData = {
firstName, lastName, emailAddress, phoneNumber
firstName, lastName, emailAddress, phoneNumber, extension
};
const { wrapper } = getMountedComponent(mainInitialState, initialData);
@ -98,7 +100,7 @@ describe('contact-details-drawer', () => {
expect(useMainStore().updateContactInfo).toBeCalledTimes(1);
expect(useMainStore().updateContactInfo).toBeCalledWith({ firstName, lastName, emailAddress });
expect(useMainStore().updatePhoneNumbers).toBeCalledTimes(1);
expect(useMainStore().updatePhoneNumbers).toBeCalledWith({ home: phoneNumber, service: phoneNumber });
expect(useMainStore().updatePhoneNumbers).toBeCalledWith({ home: phoneNumber, service: phoneNumber, extension: extension });
}
);
});

View file

@ -1,6 +1,7 @@
<template>
<modal
:ref="modalName"
:modalPosition="modalPositions.center"
:headerText="headerText"
:footerButtonText="footerButtonText"
:onModalClosedCallback="resetFormValues"
@ -8,25 +9,27 @@
@isModalOpened="setModalStatus"
@footerButtonEvent="clickFooterButtonEvent">
<template v-if="isModalOpened">
<textboxQuestion
ref="firstNameQuestion"
v-model="firstName"
inputId="firstName"
:cmsWidgetName="widget.firstNameQuestion"
isRequired
:validationRules="rules.firstName" />
<textboxQuestion
ref="lastNameQuestion"
v-model="lastName"
class="mt-4"
inputId="lastName"
:cmsWidgetName="widget.lastNameQuestion"
isRequired
:validationRules="rules.lastName" />
<div class="row">
<textboxQuestion
class="col"
ref="firstNameQuestion"
v-model="firstName"
inputId="firstName"
:cmsWidgetName="widget.firstNameQuestion"
isRequired
:validationRules="rules.firstName" />
<textboxQuestion
class="col"
ref="lastNameQuestion"
v-model="lastName"
inputId="lastName"
:cmsWidgetName="widget.lastNameQuestion"
isRequired
:validationRules="rules.lastName" />
</div>
<textboxQuestion
ref="emailQuestion"
v-model="emailAddress"
class="mt-4"
inputId="emailAddress"
:cmsWidgetName="widget.emailQuestion"
isRequired
@ -34,13 +37,20 @@
<textboxQuestion
ref="phoneNumberQuestion"
v-model="phoneNumber"
class="mt-4"
inputId="phoneNumber"
:cmsWidgetName="widget.phoneNumberQuestion"
isRequired
:mask="phoneMask"
disableAutoFill
:validationRules="rules.phoneNumber" />
<textboxQuestion
ref="extensionQuestion"
v-model="extension"
inputId="extension"
:cmsWidgetName="widget.extensionQuestion"
disableAutoFill
maxLength="5"
:validationRules="rules.extension" />
</template>
</modal>
</template>
@ -51,10 +61,11 @@ import modal from '@/digital-components/modal/modal.vue';
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
// Supporting Files
import { useMainStore } from '@/store/index.js';
import { useMainStore } from '@/store';
import globalRules from '@/constants/global-rules.js';
import MaskaFormattedMasks from '@/constants/maska-masks';
import widgetFields from '@/constants/cms-widget-fields.js';
import { modalPositions } from '@/constants/component-variants';
export default {
name: 'contact-details-drawer',
@ -67,27 +78,32 @@ export default {
const { firstName,
lastName,
emailAddress,
servicePhone } = useMainStore().contactInfo;
servicePhone,
extension } = useMainStore().contactInfo;
return {
isModalOpened: false,
firstName,
lastName,
emailAddress,
phoneNumber: servicePhone,
extension: extension,
widget: {
title: 'ContactDetailsDrawerHeaderWidget',
firstNameQuestion: 'FirstNameQuestionWidget',
lastNameQuestion: 'LastNameQuestionWidget',
emailQuestion: 'EmailQuestionWidget',
phoneNumberQuestion: 'PhoneNumberQuestionWidget',
extensionQuestion: 'ExtensionQuestionWidget',
drawerFooter: 'ContactDetailsDrawerFooterWidget'
},
rules: {
firstName: globalRules.FIRST_NAME_REQUIRED,
lastName: globalRules.LAST_NAME_REQUIRED,
emailAddress: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`,
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`
}
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`,
extension: `${globalRules.EXTENSION_FORMAT}`
},
modalPositions
};
},
computed: {
@ -121,10 +137,11 @@ export default {
lastName: this.lastName,
emailAddress: this.emailAddress
};
useMainStore().updateContactInfo(contactInfo);
useMainStore().updatePhoneNumbers({
this.mainStore.updateContactInfo(contactInfo);
this.mainStore.updatePhoneNumbers({
home: this.phoneNumber,
service: this.phoneNumber
service: this.phoneNumber,
extension: this.extension
});
this.$emit('update-contact-details');
},
@ -132,11 +149,13 @@ export default {
const { firstName,
lastName,
emailAddress,
servicePhone } = useMainStore().contactInfo;
servicePhone,
extension } = this.mainStore.contactInfo;
this.firstName = firstName;
this.lastName = lastName;
this.emailAddress = emailAddress;
this.phoneNumber = servicePhone;
this.extension = extension;
},
openModal() {
this.modal.openModal();
@ -155,5 +174,14 @@ export default {
font-size: $h5-font-size;
font-weight: $font-weight-normal !important;
}
.textbox-question {
margin-bottom: 1.25rem;
:deep(label:has(b)) {
font-weight: $font-weight-normal;
b {
font-weight: 600;
}
}
}
}
</style>

View file

@ -18,7 +18,7 @@ describe('deductible-box', () => {
it('renders the correct deductible label', () => {
const labelElement = wrapper.find('#deductibleLabel');
expect(labelElement.text()).toBe('Deductible');
expect(labelElement.text()).toBe('Deductible:');
});
it('has the correct id', () => {
@ -26,12 +26,8 @@ describe('deductible-box', () => {
});
it('has the correct classes', () => {
expect(wrapper.classes().length).toBe(5);
expect(wrapper.classes().length).toBe(1);
expect(wrapper.classes()).toContain('deductible-box');
expect(wrapper.classes()).toContain('d-flex');
expect(wrapper.classes()).toContain('justify-content-between');
expect(wrapper.classes()).toContain('align-items-center');
expect(wrapper.classes()).toContain('py-2');
});
it.each([
['', ''],

View file

@ -1,17 +1,9 @@
<template>
<div
id="deductibleBox"
class="deductible-box d-flex justify-content-between align-items-center py-2">
<p
id="deductibleLabel"
class="deductible-box__text">
Deductible
</p>
<p
id="deductibleValue"
class="deductible-box__text">
{{ value }}
</p>
class="deductible-box">
<span id="deductibleLabel" class="deductible-label">Deductible:</span>
<span id="deductibleValue" class="deductible-value">{{ value }}</span>
</div>
</template>
@ -28,12 +20,17 @@ export default {
<style lang="scss" scoped>
.deductible-box {
background-color: $green;
display: flex;
align-items: center;
gap: .25rem;
}
.deductible-box__text {
color: white;
.deductible-label {
color: $black;
font-weight: 600;
}
.deductible-value {
color: $green;
font-weight: 500;
margin: 0;
font-size: 1.625rem;
}
</style>

View file

@ -20,12 +20,12 @@ describe('ReviewBlock.vue', () => {
});
it('renders the correct number of lines', () => {
const lineElements = wrapper.findAll('.review-block__body--line');
const lineElements = wrapper.findAll('.review-body p');
expect(lineElements.length).toBe(lines.length);
});
it('renders the correct line text', () => {
const lineElements = wrapper.findAll('.review-block__body--line');
const lineElements = wrapper.findAll('.review-body p');
lines.forEach((line, index) => {
expect(lineElements.at(index).text()).toBe(line);
});

View file

@ -1,29 +1,28 @@
<template>
<div class="review-block">
<div class="d-flex justify-content-between">
<p class="review-block__header small-strong pb-1">
{{ customHeaderText }}
</p>
<textLink
linkType="textSmall"
:text="editLinkText"
useLoadingModal
href="javascript:void(0)"
class="link"
@clickEvent="editClicked">
<template
v-if="editScreenReaderTextCmsWidgetName"
#after-text>
<span class="sr-only"> {{ screenReaderOnlyText }} </span>
</template>
</textLink>
</div>
<p
v-for="line in lines"
:key="line"
class="small review-block__body--line"
v-html="line">
<p class="review-block__header">
{{ customHeaderText }}
</p>
<div class="review-body">
<p
v-for="line in lines"
:key="line"
v-html="line">
</p>
</div>
<textLink
linkType="text"
:text="editLinkText"
useLoadingModal
href="javascript:void(0)"
class="link"
@clickEvent="editClicked">
<template
v-if="editScreenReaderTextCmsWidgetName"
#after-text>
<span class="sr-only"> {{ screenReaderOnlyText }} </span>
</template>
</textLink>
</div>
</template>
@ -36,14 +35,13 @@ export default {
props: {
customHeaderText: String,
editScreenReaderTextCmsWidgetName: String,
lines: Array
lines: Array,
editLinkText: {
type: String,
default: 'Edit'
},
},
emits: ['click-edit'],
data() {
return {
editLinkText: 'Edit'
};
},
methods: {
editClicked() {
this.$emit('click-edit');
@ -59,12 +57,16 @@ export default {
}
.review-block__header {
color: $black;
font-weight: $font-weight-bold;
margin-bottom: .625rem;
}
.review-block__body--line {
color: $gray-600;
.review-body {
margin-bottom: .625rem
}
.link {
text-underline-offset: 1px;
font-weight: $font-weight-normal
}
}
</style>

View file

@ -18,7 +18,15 @@ import coverageStatuses from '@/constants/coverage-statuses';
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: jest.fn(),
doesCopyContainRouterLink: jest.fn(),
getStringWithCustomValues: jest.fn(),
getStringWithCustomValues: jest.fn((str, customValueMap) => {
let newString = str ?? '';
if (customValueMap != null) {
Object.keys(customValueMap).forEach((key) => {
newString = newString.replaceAll(`{custom:${key}}`, customValueMap[key]);
});
}
return newString;
}),
processIfStatements: jest.fn()
}));
@ -129,11 +137,7 @@ describe('tpa-submit', () => {
// Assert
expect(subHeader.exists()).toBeTruthy();
expect(subHeader.props().justifyText).toBe('center');
expect(subHeader.props().marginTopSizeOverride).toBe(4);
expect(subHeader.classes()).toContain('text-color--black');
expect(subHeader.classes()).toContain('fs-5');
expect(subHeader.classes()).toContain('tpa-submit__title--line-height');
expect(subHeader.classes()).toContain('tpa-submit-title');
});
test('sub header body one', () => {
// Arrange
@ -144,34 +148,6 @@ describe('tpa-submit', () => {
// Assert
expect(subHeaderBodyOne.exists()).toBeTruthy();
expect(subHeaderBodyOne.classes()).toContain('small');
expect(subHeaderBodyOne.classes()).toContain('text-color--darker-gray');
});
test('sub header body two', () => {
// Arrange
const wrapper = shallowMount(tpaSubmit, getMountOptions());
// Act
const subHeaderBodyTwo = wrapper.findComponent({ ref: 'tpaSubmitSubHeaderBodyTwo' });
// Assert
expect(subHeaderBodyTwo.exists()).toBeTruthy();
expect(subHeaderBodyTwo.classes()).toContain('mb-4');
expect(subHeaderBodyTwo.classes()).toContain('small');
expect(subHeaderBodyTwo.classes()).toContain('text-color--darker-gray');
});
test('main button one', () => {
// Arrange
const wrapper = shallowMount(tpaSubmit, getMountOptions());
// Act
const mainButton = wrapper.findComponent({ ref: 'buttonMainOne' });
// Assert
expect(mainButton.exists()).toBeTruthy();
expect(mainButton.props().variant).toBe('success');
expect(mainButton.classes()).toContain('w-100');
expect(mainButton.classes()).toContain('mb-5');
});
test('service summary section', () => {
// Arrange
@ -183,21 +159,6 @@ describe('tpa-submit', () => {
// Assert
expect(serviceSummarySection.exists()).toBeTruthy();
});
test('service summary title', () => {
// Arrange
const wrapper = shallowMount(tpaSubmit, getMountOptions());
// Act
const serviceSummaryTitle = wrapper.findComponent({ ref: 'tpaSubmitServiceSummaryTitle' });
// Assert
expect(serviceSummaryTitle.exists()).toBeTruthy();
expect(serviceSummaryTitle.props().marginTopSizeOverride).toBe(4);
expect(serviceSummaryTitle.classes()).toContain('fw-bold');
expect(serviceSummaryTitle.classes()).toContain('fs-1');
expect(serviceSummaryTitle.classes()).toContain('lh-lg');
expect(serviceSummaryTitle.classes()).toContain('text-color--black');
});
describe('review blocks', () => {
const title1 = 'Section 1';
const title2 = 'Another Section';
@ -253,24 +214,8 @@ describe('tpa-submit', () => {
// Assert
expect(submitOrderDetailsTitle.exists()).toBeTruthy();
expect(submitOrderDetailsTitle.props().marginTopSizeOverride).toBe(4);
expect(submitOrderDetailsTitle.classes()).toContain('fw-bold');
expect(submitOrderDetailsTitle.classes()).toContain('text-color--black');
});
test('submit order details body', () => {
// Arrange
const wrapper = shallowMount(tpaSubmit, getMountOptions());
// Act
const submitOrderDetailsTitle = wrapper.findComponent({ ref: 'tpaSubmitOrderDetailsBody' });
// Assert
expect(submitOrderDetailsTitle.exists()).toBeTruthy();
expect(submitOrderDetailsTitle.props().marginTopSizeOverride).toBe(4);
expect(submitOrderDetailsTitle.classes()).toContain('mb-4');
expect(submitOrderDetailsTitle.classes()).toContain('px-4');
expect(submitOrderDetailsTitle.classes()).toContain('small');
expect(submitOrderDetailsTitle.classes()).toContain('text-color--darker-gray');
expect(submitOrderDetailsTitle.classes()).toContain('order-details-title');
});
test('deductible box', () => {
// Arrange
@ -306,7 +251,7 @@ describe('tpa-submit', () => {
});
});
describe('before route enter', () => {
test('produces 4 sections', async () => {
test('produces 2 sections', async () => {
// Arrange
const { wrapper } = getMountedComponent();
expect(wrapper.vm.sections.length).toBe(0);
@ -320,65 +265,13 @@ describe('tpa-submit', () => {
);
// Assert
expect(wrapper.vm.sections.length).toBe(4);
});
test.each([
['2004', 'Honda', 'Civic', '2004 Honda Civic'],
[null, 'Honda', 'Civic', 'Honda Civic'],
['2004', '', 'Civic', '2004 Civic'],
['2004', 'Honda', null, '2004 Honda'],
['2004', null, undefined, '2004'],
[null, null, null, '']
])(
'when store vehicle has year %p, make %p, and model %p, has line %p',
async (year, make, model, line) => {
// Arrange
const initialStore = {
order: {
vehicle: { year, make, model }
}
};
const { wrapper } = getMountedComponent(initialStore);
const vehicleSectionIndex = 0;
const expectedLines = [line];
// Act
await tpaSubmit.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: 'tpa-submit' } },
undefined,
(c) => c(wrapper.vm)
);
// Assert
const vehicleSection = wrapper.vm.sections[vehicleSectionIndex];
expect(vehicleSection.lines).toStrictEqual(expectedLines);
}
);
test('damage section lines equal result from getDamageDisplayContent', async () => {
// Arrange
const { wrapper } = getMountedComponent();
const damageSectionIndex = 1;
const expectedLines = ['hi', 'potato', 'vehicle 3'];
getDamageDisplayContent.mockImplementationOnce(() => expectedLines);
// Act
await tpaSubmit.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: 'tpa-submit' } },
undefined,
(c) => c(wrapper.vm)
);
// Assert
const damageSection = wrapper.vm.sections[damageSectionIndex];
expect(damageSection.lines).toEqual(expectedLines);
expect(wrapper.vm.sections.length).toBe(2);
});
describe('preferred shop section', () => {
test('has three lines', async () => {
test('has two lines', async () => {
// Arrange
const { wrapper } = getMountedComponent();
const preferredShopSectionIndex = 2;
const preferredShopSectionIndex = 0;
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -390,15 +283,15 @@ describe('tpa-submit', () => {
// Assert
const preferredShopSection = wrapper.vm.sections[preferredShopSectionIndex];
expect(preferredShopSection.lines.length).toBe(3);
expect(preferredShopSection.lines.length).toBe(2);
});
test('first line is value returned from toTitleCase method', async () => {
test('title is value returned from toTitleCase method', async () => {
// Arrange
const initialData = { companyName: 'some value' };
const { wrapper } = getMountedComponent({}, initialData);
const expectedName = 'some expected name';
toTitleCase.mockImplementationOnce(() => expectedName);
const preferredShopSectionIndex = 2;
const preferredShopSectionIndex = 0;
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -410,9 +303,9 @@ describe('tpa-submit', () => {
// Assert
const preferredShopSection = wrapper.vm.sections[preferredShopSectionIndex];
expect(preferredShopSection.lines[0]).toBe(expectedName);
expect(preferredShopSection.title).toBe(expectedName);
});
test('second line is expected and formatAddress called', async () => {
test('first line is expected and formatAddress called', async () => {
// Arrange
const address = {
streetAddress: '123 South Ln',
@ -430,7 +323,7 @@ describe('tpa-submit', () => {
const { wrapper } = getMountedComponent(initialStore);
const line = 'some returned line';
formatAddress.mockImplementationOnce(() => line);
const preferredShopSectionIndex = 2;
const preferredShopSectionIndex = 0;
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -442,7 +335,7 @@ describe('tpa-submit', () => {
// Assert
const preferredShopSection = wrapper.vm.sections[preferredShopSectionIndex];
expect(preferredShopSection.lines[1]).toBe(line);
expect(preferredShopSection.lines[0]).toBe(line);
expect(formatAddress).toHaveBeenCalledTimes(1);
expect(formatAddress).toHaveBeenCalledWith(
address.streetAddress,
@ -452,7 +345,7 @@ describe('tpa-submit', () => {
address.zipCode
);
});
test('third line is expected and toDisplayPhoneNumber called', async () => {
test('second line is expected and toDisplayPhoneNumber called', async () => {
// Arrange
const phoneNumber = '9998887777';
const initialStore = {
@ -465,7 +358,7 @@ describe('tpa-submit', () => {
const { wrapper } = getMountedComponent(initialStore);
const expectedLine = 'returned from to display phone num';
toDisplayPhoneNumber.mockImplementationOnce(() => expectedLine);
const preferredShopSectionIndex = 2;
const preferredShopSectionIndex = 0;
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -477,7 +370,7 @@ describe('tpa-submit', () => {
// Assert
const preferredShopSection = wrapper.vm.sections[preferredShopSectionIndex];
expect(preferredShopSection.lines[2]).toBe(expectedLine);
expect(preferredShopSection.lines[1]).toBe(expectedLine);
expect(toDisplayPhoneNumber).toHaveBeenCalledWith(phoneNumber);
});
});
@ -487,8 +380,14 @@ describe('tpa-submit', () => {
const lastName = 'Eddison';
const emailAddress = 'myname@gmail.com';
const servicePhone = '0001112222';
const providerCompanyName = 'Some Provider LLC';
const initialStore = {
order: {
serviceLocation: {
provider: {
companyName: providerCompanyName
}
},
contactInfo: {
firstName,
lastName,
@ -498,11 +397,11 @@ describe('tpa-submit', () => {
}
};
const { wrapper } = getMountedComponent(initialStore);
const expectedLine1 = 'Jones Eddison';
const expectedLine2 = emailAddress;
const expectedLine3 = 'some value returned';
toDisplayPhoneNumber.mockImplementation((number) => (number === servicePhone ? expectedLine3 : ''));
const contactInfoSectionIndex = 3;
const expectedEmail = emailAddress;
const expectedPhone = 'some value returned';
toDisplayPhoneNumber.mockImplementation((number) => (number === servicePhone ? expectedPhone : ''));
const contactInfoSectionIndex = 1;
wrapper.vm.getCmsContent.mockImplementation(() => '{custom:contactPhone} or {custom:contactEmail}');
// Act
await tpaSubmit.beforeRouteEnter.call(
@ -511,12 +410,59 @@ describe('tpa-submit', () => {
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.setSections();
// Assert
const contactInfoSection = wrapper.vm.sections[contactInfoSectionIndex];
expect(contactInfoSection.lines[0]).toBe(expectedLine1);
expect(contactInfoSection.lines[1]).toBe(expectedLine2);
expect(contactInfoSection.lines[2]).toBe(expectedLine3);
expect(contactInfoSection.lines[0]).toContain(expectedEmail);
expect(contactInfoSection.lines[0]).toContain(expectedPhone);
expect(toDisplayPhoneNumber).toHaveBeenCalledWith(servicePhone);
});
test('contact info section has expected content with extension', async () => {
// Arrange
const firstName = 'Jones';
const lastName = 'Eddison';
const emailAddress = 'myname@gmail.com';
const servicePhone = '0001112222';
const extension = '12345';
const providerCompanyName = 'Some Provider LLC';
const initialStore = {
order: {
serviceLocation: {
provider: {
companyName: providerCompanyName
}
},
contactInfo: {
firstName,
lastName,
emailAddress,
servicePhone,
extension
}
}
};
const { wrapper } = getMountedComponent(initialStore);
const expectedEmail = emailAddress;
const expectedPhone = 'some value returned';
toDisplayPhoneNumber.mockImplementation((number) => (number === servicePhone ? expectedPhone : ''));
const contactInfoSectionIndex = 1;
wrapper.vm.getCmsContent.mockImplementation(() => '{custom:contactPhone} or {custom:contactEmail}');
// Act
await tpaSubmit.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: 'tpa-submit' } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.setSections();
// Assert
const contactInfoSection = wrapper.vm.sections[contactInfoSectionIndex];
expect(contactInfoSection.lines[0]).toContain(expectedEmail);
expect(contactInfoSection.lines[0]).toContain(expectedPhone);
expect(contactInfoSection.lines[0]).toContain(`Ext. ${extension}`);
expect(toDisplayPhoneNumber).toHaveBeenCalledWith(servicePhone);
});
});
@ -524,7 +470,6 @@ describe('tpa-submit', () => {
test.each([
['subHeaderTitle', 'SiteSubHeaderWidget', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT, 'site sub header'],
['subHeaderBodyOne', 'SiteSubHeaderWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT, 'sub header body one'],
['serviceSummaryText', 'ServiceSummaryContent', widgetFields.TEXT_BLOCK_WIDGET.TEXT, 'service summary text'],
['orderDetailsTitle', 'OrderDetailsContent', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT, 'order details title'],
['forwardButtonText', 'SiteFooterWidget', widgetFields.FOOTER_WIDGET.FORWARD_BUTTON_TEXT, 'forward button text']
])('computed %p returns expected value', (computedName, widgetLabel, fieldLabel, expected) => {

View file

@ -13,53 +13,52 @@
</div>
<div class="iss-heritage-container-width">
<div class="tpa-submit-container iss-heritage-content-container-width">
<!-- TODO fix styling -->
<alert
ref="alertIncomplete"
alertClass="alert-warning"
:cmsWidgetName="widget.alertIncomplete"
/>
<textBlock
ref="subHeaderTitle"
:customText="subHeaderTitle"
justifyText="center"
:marginTopSizeOverride="4"
class="text-color--black fs-5 tpa-submit__title--line-height" />
class="tpa-submit-title"/>
<textBlock
id="tpaSubmitSubHeaderBodyOne"
ref="tpaSubmitSubHeaderBodyOne"
:customText="subHeaderBodyOne"
class="small text-color--darker-gray" />
<textBlock
id="tpaSubmitSubHeaderBodyTwo"
ref="tpaSubmitSubHeaderBodyTwo"
:customText="subHeaderBodyTwo"
class="mb-4 small text-color--darker-gray" />
<buttonMain
ref="buttonMainOne"
variant="success"
:buttonText="forwardButtonText"
class="w-100 mb-5"
@clickEvent="forwardButtonAction" />
<hr class="mb-0" />
:customText="subHeaderBodyOne" />
<alert
v-if="recalibrationRequired"
ref="alertRecalWarning"
alertClass="alert-warning recal-alert"
:isCollapsible="true"
:cmsWidgetName="widget.alertRecalWarning"
@textLinkClicked="openRecalModal" />
<modal
ref="recalModal"
class="recal-modal"
:modalPosition="modalPositions.center"
:headerText="recalModalContent.headerText"
:footerButtonText="recalModalContent.footerButtonText"
@footerButtonEvent="closeRecalModal">
<div class="recal-modal-subheader">{{ recalModalContent.subHeaderText }}</div>
<img class="recal-modal-image" :src="recalModalContent.image" />
<div class="recal-modal-body" v-html="recalModalContent.bodyText"></div>
</modal>
<hr />
<div id="serviceSummarySection">
<textBlock
id="tpaSubmitServiceSummaryTitle"
ref="tpaSubmitServiceSummaryTitle"
:customText="serviceSummaryText"
:marginTopSizeOverride="4"
class="fw-bold fs-1 lh-lg text-color--black" />
<div class="px-4 my-3">
<div
v-for="(section, index) in sections"
:key="section.title">
<hr
v-if="index !== 0"
class="my-3" />
<reviewBlock
:id="'review-block-' + index"
:customHeaderText="section.title"
:lines="section.lines"
@clickEdit="() => section.onClickEdit()" />
</div>
<div
v-for="(section, index) in sections"
:key="section.title">
<hr v-if="index !== 0" />
<reviewBlock
:id="'review-block-' + index"
:customHeaderText="section.title"
:lines="section.lines"
:editLinkText="section.editLinkText"
@clickEdit="() => section.onClickEdit()" />
</div>
</div>
<hr class="mb-0" />
<hr />
<div
id="submitOrderDetailsSection"
ref="submitOrderDetailsSection">
@ -67,17 +66,10 @@
id="tpaSubmitOrderDetailsTitle"
ref="tpaSubmitOrderDetailsTitle"
:customText="orderDetailsTitle"
:marginTopSizeOverride="4"
class="fw-bold text-color--black" />
<textBlock
id="tpaSubmitOrderDetailsBody"
ref="tpaSubmitOrderDetailsBody"
:customText="orderDetailsBody"
:marginTopSizeOverride="4"
class="mb-4 px-4 small text-color--darker-gray" />
:marginTopSizeOverride="0"
class="fw-bold order-details-title" />
<deductibleBox
ref="deductibleBox"
class="px-3"
:value="deductibleBoxValue" />
</div>
<siteFooter
@ -104,6 +96,8 @@ import reviewBlock from '@/layouts/tpa-submit/review-block/review-block.vue';
import deductibleBox from '@/layouts/tpa-submit/deductible-box/deductible-box.vue';
import contactDetailsDrawer from '@/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import alert from '@/ux-components/alert/alert.vue';
import modal from '@/digital-components/modal/modal.vue';
// Supporting files
import {
@ -114,21 +108,16 @@ import {
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import widgetFields from '@/constants/cms-widget-fields.js';
import { useMainStore } from '@/store';
import {
toTitleCase,
toDisplayPhoneNumber,
formatAddress,
formatAmountInDollars
} from '@/helpers/text-helper.js';
import {
getDamageDisplayContent,
getLocationAnswer
} from '@/helpers/damage-review-content-generator.js';
import damageLocationsSelected from '@/constants/damage-locations-selected.js';
import { submitWorkOrder } from '@/helpers/order-helper.js';
import bailoutMessage from '@/constants/bailoutMessage';
import submitType from '@/constants/submit-type';
import { modalPositions } from '@/constants/component-variants';
const VERIFYING_COVERAGE = 'Verifying coverage';
@ -143,7 +132,9 @@ export default {
siteFooter,
contactDetailsDrawer,
// eslint-disable-next-line vue/no-reserved-component-names
Form
Form,
alert,
modal
},
mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) {
@ -154,27 +145,19 @@ export default {
});
},
data() {
const { companyName } = useMainStore().order.serviceLocation.provider;
return {
sections: [],
widget: {
siteHeader: 'SiteHeaderWidget',
siteSubHeader: 'SiteSubHeaderWidget',
serviceSummary: 'ServiceSummaryContent',
subheader: {
vehicle: 'VehicleSubTitle',
damage: 'DamageSubTitle',
shop: 'PreferredShopSubTitle',
contactInfo: 'ContactDetailsSubTitle'
},
damageLocations: 'DamageLocationsWidget',
orderDetails: 'OrderDetailsContent',
footer: 'SiteFooterWidget'
footer: 'SiteFooterWidget',
alertIncomplete: 'AlertIncompleteWidget',
alertRecalWarning: 'AlertRecalWarningWidget',
editShopLinkText: 'EditShopLinkTextWidget',
contactDetails: 'ContactDetailsSectionWidget'
},
companyName,
customValueMap: {
glassShop: companyName
}
modalPositions
};
},
computed: {
@ -197,12 +180,6 @@ export default {
);
return getStringWithCustomValues(cmsContent, this.customValueMap);
},
serviceSummaryText() {
return this.getCmsContent(
this.widget.serviceSummary,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
orderDetailsTitle() {
return this.getCmsContent(
this.widget.orderDetails,
@ -227,53 +204,21 @@ export default {
);
},
isVerified() {
return useMainStore().isVerified;
return this.mainStore.isVerified;
},
currentDeductible() {
return useMainStore().order.currentDeductible;
return this.mainStore.order.currentDeductible;
},
deductibleBoxValue() {
return this.isVerified
? formatAmountInDollars(this.currentDeductible)
: VERIFYING_COVERAGE;
},
getVehicleLines() {
const { year, make, model } = useMainStore().order.vehicle;
const line = [year, make, model]
.filter((v) => v != null && v !== '')
.join(' ');
return [line];
},
locationAnswers() {
return this.getInputQuestionWidgetAnswersNullSafe(this.widget.damageLocations);
},
driverSideDamageAnswers() {
const answerContent = getLocationAnswer(
damageLocationsSelected.DRIVER,
this.locationAnswers
);
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
},
passengerSideDamageAnswers() {
const answerContent = getLocationAnswer(
damageLocationsSelected.PASSENGER,
this.locationAnswers
);
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
},
getDamageLines() {
const { glassToReplace, isRepair } = useMainStore().order.damage;
return getDamageDisplayContent(
this.locationAnswers,
this.driverSideDamageAnswers,
this.passengerSideDamageAnswers,
glassToReplace,
isRepair
);
getPreferredShopTitle() {
return toTitleCase(this.mainStore.order.serviceLocation.provider.companyName ?? '');
},
getPreferredShopLines() {
const { phoneNumber, address } =
useMainStore().order.serviceLocation.provider;
const { phoneNumber, address } = this.mainStore.order.serviceLocation.provider;
const { streetAddress, city, state, zipCode } = address;
const displayAddress = formatAddress(
streetAddress,
@ -284,58 +229,86 @@ export default {
);
const displayPhoneNumber = toDisplayPhoneNumber(phoneNumber);
return [
toTitleCase(this.companyName ?? ''),
displayAddress,
displayPhoneNumber
];
},
getEditShopLinkText() {
return this.getCmsContent(
this.widget.editShopLinkText,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
getContactInfoTitle() {
const cmsContent = this.getCmsContent(
this.widget.contactDetails,
widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT
);
return getStringWithCustomValues(cmsContent, this.customValueMap);
},
getContactInfoLines() {
const { firstName, lastName, emailAddress, servicePhone } =
useMainStore().contactInfo;
const cmsContent = this.getCmsContent(
this.widget.contactDetails,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
return [
`${firstName} ${lastName}`,
emailAddress ?? '',
toDisplayPhoneNumber(servicePhone)
getStringWithCustomValues(cmsContent, this.customValueMap)
];
},
getEditContactInfoLinkText() {
return this.getCmsContent(
this.widget.contactDetails,
widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT
);
},
recalModalContent() {
const widgetName = 'RecalModal';
return {
headerText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT),
subHeaderText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.SUBHEADER_TEXT),
bodyText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT),
footerButtonText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT),
image: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.IMAGE)
}
},
recalibrationRequired() {
return this.mainStore.hasRecalibrationPart;
},
customValueMap() {
let contactPhone = toDisplayPhoneNumber(this.mainStore.contactInfo.servicePhone);
if (this.mainStore.contactInfo.extension) {
contactPhone += ` Ext. ${this.mainStore.contactInfo.extension}`;
}
return {
glassShop: this.getPreferredShopTitle,
contactPhone: contactPhone,
contactEmail: this.mainStore.contactInfo.emailAddress ?? ''
}
}
},
methods: {
setSections() {
const vehicleScenario = useMainStore().isPolicyVehicle
? this.navigationScenarios.EDIT_POLICY_VEHICLE
: this.navigationScenarios.EDIT_VEHICLE;
this.sections = [
this.getSection(
this.widget.subheader.vehicle,
this.getVehicleLines,
() => this.navigate(vehicleScenario)
),
// eslint-disable-next-line max-len
this.getSection(
this.widget.subheader.damage,
this.getDamageLines,
() => this.navigate(this.navigationScenarios.EDIT_DAMAGE)
),
this.getSection(
this.widget.subheader.shop,
this.getPreferredShopTitle,
this.getPreferredShopLines,
() => this.navigate(this.navigationScenarios.EDIT_PREFERRED_SHOP)
this.getEditShopLinkText,
() => this.navigate(this.navigationScenarios.EDIT_PREFERRED_SHOP),
),
// eslint-disable-next-line max-len
this.getSection(
this.widget.subheader.contactInfo,
this.getContactInfoTitle,
this.getContactInfoLines,
this.getEditContactInfoLinkText,
this.openContactDetailsModal
)
];
},
getSection(widgetName, lines, onClick) {
getSection(title, lines, editLinkText, onClick) {
return {
title: this.getCmsContent(
widgetName,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
),
title: title,
lines,
editLinkText,
onClickEdit: onClick
};
},
@ -344,7 +317,7 @@ export default {
await submitWorkOrder({ submitType: submitType.TPA }).then(() => {
this.navigate(this.navigationScenarios.CLICKED_FORWARD);
}).catch((submitError) => {
useMainStore().setBailout(bailoutMessage.saveSessionError(submitError.data));
this.mainStore.setBailout(bailoutMessage.saveSessionError(submitError.data));
this.navigate(
this.navigationScenarios.SAVE_SESSION_FAILED,
this.$route,
@ -372,6 +345,12 @@ export default {
},
openContactDetailsModal() {
this.$refs.contactDetailsDrawer.openModal();
},
openRecalModal() {
this.$refs.recalModal.openModal();
},
closeRecalModal() {
this.$refs.recalModal.closeModal();
}
}
};
@ -387,26 +366,69 @@ export default {
}
.tpa-submit {
#tpaSubmitOrderDetailsBody {
p {
font-size: .875rem;
}
}
.tpa-submit__title--line-height {
line-height: map-get($spacers, 6);
.alert-warning {
margin-top: 1.25rem;
margin-bottom: 0rem;
}
.text-color--darker-gray {
color: $darker-gray;
.tpa-submit-title {
font-size: 1.25rem;
color: $black;
font-weight: 400;
margin-top: 1.25rem;
margin-bottom: .625rem;
}
.text-color--black {
.recal-alert {
margin-bottom: 4rem;
}
b {
font-weight: $font-weight-bold;
color: $black;
}
hr {
opacity: 1;
color: $gray-350;
margin: 1.875rem 0rem;
}
.fw-bold {
color: $black;
}
.order-details-title {
margin-bottom: .625rem;
}
.modal.recal-modal .modal-dialog {
.modal-header {
justify-content: start;
margin: 0rem;
padding: 1.25rem 1.25rem .625rem 1.25rem;
.modal-title {
font-weight: $font-weight-bolder;
font-size: .875rem;
line-height: 1.25rem;
text-align: start;
justify-content: start;
}
.btn-close {
display: none;
}
}
.modal-body {
padding: 0rem 1.25rem 1.25rem 1.25rem;
b {
font-weight: 600;
}
}
.recal-modal-subheader {
font-weight: $font-weight-bold;
color: $black;
}
.recal-modal-image {
margin: 1.25rem 0rem
}
}
}
</style>

View file

@ -136,7 +136,6 @@ describe('welcome-page.vue', () => {
const state = wrapper.findComponent({ ref: 'state' });
const glassOnlyDamage = wrapper.findComponent({ ref: 'glassOnlyDamage' });
const phoneNumber = wrapper.findComponent({ ref: 'phoneNumber' });
const email = wrapper.findComponent({ ref: 'email' });
// Assert
expect(policyNumber.exists()).toBe(true);
@ -146,7 +145,6 @@ describe('welcome-page.vue', () => {
expect(state.exists()).toBe(false);
expect(glassOnlyDamage.exists()).toBe(false);
expect(phoneNumber.exists()).toBe(true);
expect(email.exists()).toBe(true);
});
test('Policy zip field should be visible at all times', async () => {
// Arrange

View file

@ -12,7 +12,7 @@
<div class="welcome-page-container iss-heritage-content-container-width">
<siteSubHeader
cmsWidgetName="SiteSubHeaderWidget"
class="mt-4" />
class="form-group" />
<textboxQuestion
ref="policyNumber"
v-model="welcomePageModel.policyNumber"
@ -22,16 +22,6 @@
disableAutoFill
:isDisabled="isPolicyHolderDisabled"
:validationRules="rules.policyNumber" />
<textboxQuestion
ref="policyZip"
v-model="welcomePageModel.policyZipCode"
inputId="policyZipCode"
cmsWidgetName="PolicyZipQuestion"
isRequired
mask="#####"
:isDisabled="isPolicyZipDisabled"
:validationRules="rules.policyZip"
class="mt-3" />
<textboxQuestion
ref="phoneNumber"
v-model="welcomePageModel.phoneNumber"
@ -41,15 +31,17 @@
isRequired
:mask="phoneMask"
disableAutoFill
class="mt-3" />
placeholderText="###-###-####"
class="form-group" />
<textboxQuestion
ref="extension"
v-model="welcomePageModel.extension"
inputId="extensionField"
cmsWidgetName="ExtensionQuestion"
:validationRules="rules.extension"
maxLength="5"
disableAutoFill
class="mt-3" />
class="form-group" />
<textboxQuestion
ref="dateOfLoss"
v-model="welcomePageModel.dateOfLoss"
@ -62,7 +54,7 @@
:max="new Date().toJSON().slice(0, 10)"
:min="'1972-12-01'"
:validationRules="rules.lossDate"
class="mt-3" />
class="form-group" />
<textBlock
cmsWidgetName="DamageDateEstimateWidget"
typeStyle="small"
@ -77,44 +69,45 @@
disableAutoFill
:validationRules="rules.damageOption"
placeHolderText="Select an option"
class="mt-3" />
class="form-group" />
<textboxQuestion
ref="policyZip"
v-model="welcomePageModel.policyZipCode"
inputId="policyZipCode"
cmsWidgetName="PolicyZipQuestion"
isRequired
mask="#####"
:isDisabled="isPolicyZipDisabled"
:validationRules="rules.policyZip"
class="form-group" />
<dropdownQuestion
v-if="displayDamageStateQuestion"
id="welcomeDropdown"
ref="state"
v-model="welcomePageModel.damageState"
class="mt-3"
class="form-group"
cmsWidgetName="DamageStateQuestion"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="getStates"
:validationRules="rules.lossState"
isRequired
disableAutoFill
placeHolderText="Select an option" />
placeHolderText="Select State" />
<textboxQuestion
v-if="displayDamageCityQuestion"
ref="damageCity"
v-model="welcomePageModel.damageCity"
class="mt-3"
class="form-group"
inputId="damageCityField"
cmsWidgetName="DamageCityQuestion"
isRequired
disableAutoFill
:validationRules="rules.lossCity" />
<textboxQuestion
ref="email"
v-model="welcomePageModel.email"
inputId="emailField"
cmsWidgetName="EmailAddressQuestion"
:validationRules="rules.email"
isRequired
disableAutoFill
class="mt-3" />
<buttonQuestion
v-if="displayGlassOnlyQuestion"
ref="glassOnlyDamage"
v-model="welcomePageModel.isDamageGlassOnly"
class="px-0 mt-3"
class="px-0 form-group"
cmsWidgetName="GlassOnlyQuestion"
inputId="isDamageGlassOnly"
:answers="DamageGlassOnlyOptions"
@ -142,17 +135,9 @@
:isDismissible="false" />
<siteFooter
ref="siteFooter"
class="mt-3"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction" />
<textBlock
id="requestCallbackLink"
cmsWidgetName="HelpTextWidget"
linkType="navigation"
href="javascript:void(0)"
class="mb-5 text-left"
@clickEvent="handleHelpLinkClick" />
</div>
</div>
</div>
@ -257,7 +242,6 @@ export default {
duplicates: [],
rules: {
damageOption: 'damage-option-required',
email: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`,
extension: `${globalRules.EXTENSION_FORMAT}`,
lossCity: `${globalRules.DATE_OF_LOSS_CITY_REQUIRED}|${globalRules.DATE_OF_LOSS_CITY_FORMAT}`,
// eslint-disable-next-line max-len
@ -302,7 +286,11 @@ export default {
return !!this.getCmsContent('GlassOnlyQuestion', 'QuestionText');
},
getStates() {
return states;
return Object.keys(states).reduce((acc, key) => {
// eslint-disable-next-line no-param-reassign
acc[key] = states[key].toUpperCase();
return acc;
}, {});
},
isPolicyHolderDisabled() {
return !!this.mainStore.issConfig.disabledFields.policyNumber;
@ -323,7 +311,14 @@ export default {
this.mainStore.updatePolicyData(this.welcomePageModel);
const promises = [];
promises.push(this.configureZip().then(async () => await this.mainStore.getBillToInfo()));
if (!this.mainStore.order.loadedFromCookie) {
// Clear duplicate orders if navigating away from the welcome page after visiting duplicate check page.
if (this.mainStore.order.visitedDuplicateCheckPage) {
this.mainStore.clearDuplicateOrders();
}
// Skip duplicate check if loaded from cookie or already visited duplicate check page.
if (!this.mainStore.order.loadedFromCookie && !this.mainStore.order.visitedDuplicateCheckPage) {
promises.push(this.mainStore.getDuplicateReferrals());
}
promises.push(this.mainStore.getCoveragePolicyInfo());
@ -408,7 +403,6 @@ export default {
isDamageGlassOnly: this.mainStore.order.policy.isDamageGlassOnly,
phoneNumber: this.mainStore.order.contactInfo.homePhone,
extension: this.mainStore.order.contactInfo.extension,
email: this.mainStore.order.customer.emailAddress,
isPolicyNumberDisabled: this.mainStore.order.policy.isPolicyNumberDisabled
};
},
@ -425,7 +419,6 @@ export default {
this.welcomePageModel.isDamageGlassOnly = response.policy.isDamageGlassOnly;
this.welcomePageModel.phoneNumber = response.customer.homePhone;
this.welcomePageModel.extension = response.customer.extension;
this.welcomePageModel.email = response.customer.emailAddress;
},
findDamageCause(damageCause) {
const damageCauseOptions = this.DamageCauseOptions;
@ -481,19 +474,13 @@ export default {
this.mainStore.order.loadedFromCookie = true;
this.answeredContinueModal = true;
this.$refs.continueModal.closeModal();
},
handleHelpLinkClick() {
useMainStore().setBailout(bailoutMessage.RequestCallback());
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
}
}
};
</script>
<style lang="scss" scoped>
@import '@/styles/ux-variables-svg-strings.scss';
form {
.container-fluid {
display: flex;
@ -508,6 +495,26 @@ form {
min-height: 1px;
padding-left: .9375rem;
padding-right: .9375rem;
.form-group {
margin-top: 1.25rem;
:deep(.input-wrapper) {
input[type="date"] {
max-height: 3rem;
}
}
&.has-error {
:deep(.input-wrapper) {
input[type="date"]::-webkit-calendar-picker-indicator {
background-color: $svg-calendar-error-fill-color;
// eslint-disable-next-line max-len
background-image: url($svg-error-calendar-graphic);
}
}
}
}
}
}

View file

@ -27,6 +27,8 @@ const navigationScenarios = Object.freeze({
// Policy Holder Details
CLICKED_FORWARD_POLICY_VERIFIED: 'CLICKED_FORWARD_POLICY_VERIFIED',
CLICKED_BACK_POLICY_UNVERIFIED: 'CLICKED_BACK_POLICY_UNVERIFIED',
CLICKED_BACK_POLICY_VERIFIED: 'CLICKED_BACK_POLICY_VERIFIED',
// Policy Vehicle
CLICKED_FORWARD_LISTED_VEHICLE: 'CLICKED_FORWARD_LISTED_VEHICLE',

View file

@ -9,11 +9,11 @@ const routingTable = () => [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
destinationIssPageValue: issPageValues.WELCOME_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
@ -26,7 +26,7 @@ const routingTable = () => [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_REPAIR,
@ -386,7 +386,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_DUPLICATES,
@ -394,7 +394,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED_NO_VEHICLES,
@ -423,7 +423,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED_NO_VEHICLES,
@ -435,7 +435,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_LOADED_DUPLICATE_WITH_POLICY_VEHICLE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_LOADED_DUPLICATE_WITH_NON_POLICY_VEHICLE,
@ -451,16 +451,20 @@ const routingTable = () => [
issPageValue: issPageValues.POLICY_HOLDER_DETAILS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
scenario: navigationScenarios.CLICKED_BACK_POLICY_UNVERIFIED,
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
},
{
scenario: navigationScenarios.CLICKED_BACK_POLICY_VERIFIED,
destinationIssPageValue: issPageValues.WELCOME_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_UNVERIFIED,
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED,
destinationIssPageValue: issPageValues.POLICY_VEHICLES
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
}
]
},
@ -473,7 +477,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_LISTED_VEHICLE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_NON_LISTED_VEHICLE,
@ -776,11 +780,11 @@ const routingTable = () => [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.POLICY_VEHICLES
destinationIssPageValue: issPageValues.WELCOME_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAR_ID_NOT_FOUND,

View file

@ -19,7 +19,7 @@ describe('Router', () => {
router.push = jest.fn();
router.navigate(scenario, currentRoute);
expect(router.push.mock.calls[0][0].query.issPage).toBe(issPageValues.POLICY_HOLDER_DETAILS);
expect(router.push.mock.calls[0][0].query.issPage).toBe(issPageValues.VEHICLE_SELECTION);
});
it('Should set state as expected', () => {

View file

@ -197,6 +197,7 @@ export const getDefaultState = () => ({
homePhone: null,
alternativePhone: null,
servicePhone: null,
extension: null,
requestTextUpdates: false,
notesForTechnician: ''
},
@ -225,7 +226,8 @@ export const getDefaultState = () => ({
loadedFromCookie: false,
loadedFromDupeCheck: null,
loadedSessionClearedPreviousData: null,
availableVaps: null
availableVaps: null,
visitedDuplicateCheckPage: false
},
applicationUser: {
experiments: [],
@ -243,6 +245,7 @@ export const getDefaultState = () => ({
},
issConfig: {
clientName: 'Generic Insurance', // this is the default and will be overriden by the client's name
clientFullName: 'Generic Insurance', // this is the default and will be overriden by the client's name or client's full name.
clientDisplayName: 'Generic Insurance', // this is the default and will be overridden by the client's name or client display name.
clientHeader: {},
styleSheet: '', // Stylesheet used by the client.
@ -353,6 +356,7 @@ export const useMainStore = defineStore({
homePhone: s.order.contactInfo.homePhone,
alternativePhone: s.order.contactInfo.alternativePhone,
servicePhone: s.order.contactInfo.servicePhone,
extension: s.order.contactInfo.extension,
requestTextUpdates: s.order.contactInfo.requestTextUpdates ?? false,
notesForTechnician: s.order.contactInfo.notesForTechnician
}),
@ -594,6 +598,12 @@ export const useMainStore = defineStore({
return Promise.reject(e);
}
},
clearDuplicateOrders() {
this.applicationUser.duplicateOrders = [];
},
updateDuplicateCheckVisited(visited) {
this.order.visitedDuplicateCheckPage = visited;
},
updateCoverageStatus(status) {
this.order.insuranceCoverage.coverageStatus = status;
},
@ -1011,14 +1021,13 @@ export const useMainStore = defineStore({
});
},
getProviders(serviceZipCode) {
getProviders(serviceZipCode, shopRadiusInMiles = 100) {
const { carId, isBigTruck } = this.order.vehicle;
const damageType = this.damage.isRepair ? 'Repair' : 'Replace';
const { parentAccountNumber } = this.order;
const partsWithRecal = getTopLevelGlassPartsWithRecal(this.order.lineItems.glassParts);
const windshieldPartWithRecal = partsWithRecal?.length > 0 ? partsWithRecal[0] : null;
const safeliteOnly = true;
const shopRadiusInMiles = 100;
let url = `${endpoints.GetProviders.url}/${serviceZipCode}/${damageType}/${shopRadiusInMiles}/${parentAccountNumber}/${safeliteOnly}/${carId}`;
if (windshieldPartWithRecal) {
@ -1455,7 +1464,7 @@ export const useMainStore = defineStore({
emailAddress: contactInfo.emailAddress || customer.emailAddress,
firstName: contactInfo.firstName || customer.firstName,
lastName: contactInfo.lastName || customer.lastName,
homePhone: contactInfo.homePhone,
homePhone: contactInfo.extension && contactInfo.homePhone ? contactInfo.homePhone + contactInfo.extension : contactInfo.homePhone,
servicePhone: contactInfo.servicePhone,
alternativePhone: contactInfo.alternativePhone,
isSmsOptIn: contactInfo.requestTextUpdates ?? false
@ -1589,7 +1598,8 @@ export const useMainStore = defineStore({
order.contactInfo.firstName = data?.customer?.firstName;
order.contactInfo.lastName = data?.customer?.lastName;
order.contactInfo.emailAddress = data?.customer?.emailAddress;
order.contactInfo.homePhone = data?.customer?.homePhone;
order.contactInfo.extension = data?.customer?.homePhone?.slice(10);
order.contactInfo.homePhone = data?.customer?.homePhone?.slice(0, 10);
order.contactInfo.servicePhone = data?.customer?.servicePhone;
order.contactInfo.alternativePhone = data?.customer?.alternativePhone;
order.contactInfo.requestTextUpdates = data?.customer?.isSmsOptIn;
@ -1657,7 +1667,8 @@ export const useMainStore = defineStore({
order.contactInfo.firstName = data?.customer?.firstName;
order.contactInfo.lastName = data?.customer?.lastName;
order.contactInfo.emailAddress = data?.customer?.emailAddress;
order.contactInfo.homePhone = data?.customer?.homePhone;
order.contactInfo.extension = data?.customer?.homePhone?.slice(10);
order.contactInfo.homePhone = data?.customer?.homePhone?.slice(0, 10);
order.contactInfo.servicePhone = data?.customer?.servicePhone;
order.contactInfo.alternativePhone = data?.customer?.alternativePhone;
order.contactInfo.requestTextUpdates = data?.customer?.isSmsOptIn;
@ -1719,6 +1730,7 @@ export const useMainStore = defineStore({
order.eon = data?.eon;
order.workOrderId = data?.workOrderId;
order.loadedFromCookie = true;
order.visitedDuplicateCheckPage = true;
order.loadedSessionClearedPreviousData = false;
issConfig.enableContinueFromCookie = false;
this.updateIsSafeliteProvider(data?.provider?.isSafeliteProvider);
@ -2041,6 +2053,7 @@ export const useMainStore = defineStore({
this.order.carrierPhoneNumber = null;
this.order.customerPortalLoginToken = null;
this.order.loadedFromCookie = false;
this.order.visitedDuplicateCheckPage = false;
},
resetPolicy() {
@ -2095,6 +2108,7 @@ export const useMainStore = defineStore({
this.order.contactInfo.homePhone = null;
this.order.contactInfo.alternativePhone = null;
this.order.contactInfo.servicePhone = null;
this.order.contactInfo.extension = null;
this.order.contactInfo.requestTextUpdates = false;
this.order.contactInfo.notesForTechnician = '';
},
@ -2156,6 +2170,7 @@ export const useMainStore = defineStore({
resetISSConfigState() {
this.issConfig.clientName = 'Generic Insurance';
this.issConfig.clientFullName = 'Generic Insurance';
this.issConfig.clientDisplayName = 'Generic Insurance';
this.issConfig.clientHeader = {};
this.issConfig.parentAccountNumber = 0;
@ -2275,11 +2290,11 @@ export const useMainStore = defineStore({
this.order.policy.damageState = welcomePageModel?.damageState;
this.order.policy.damageCity = welcomePageModel?.damageCity;
this.order.policy.isDamageGlassOnly = welcomePageModel?.isDamageGlassOnly;
this.order.customer.emailAddress = welcomePageModel?.email;
this.order.serviceLocation.zipCode = welcomePageModel?.policyZipCode;
this.updatePhoneNumbers({
home: welcomePageModel?.phoneNumber,
service: welcomePageModel?.phoneNumber
service: welcomePageModel?.phoneNumber,
extension: welcomePageModel?.extension
});
},
updatePolicyHolderDetails(customerQuestions) {
@ -2290,6 +2305,12 @@ export const useMainStore = defineStore({
this.order.customer.address.zipCode = customerQuestions.addressQuestions.zipCode;
this.order.customer.firstName = customerQuestions.firstName;
this.order.customer.lastName = customerQuestions.lastName;
this.order.customer.emailAddress = customerQuestions.email;
this.updatePhoneNumbers({
home: customerQuestions.phoneNumber,
service: customerQuestions.phoneNumber,
extension: customerQuestions.extension
});
this.order.serviceLocation.zipCode = customerQuestions.addressQuestions.zipCode;
},
updateIsSafeliteProvider(isSafelite) {
@ -2369,23 +2390,23 @@ export const useMainStore = defineStore({
const isMobileApt = appointmentType === AppointmentTypeStrings.MOBILE
|| appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const retPricedLineItems = await globalMethods.callHttpClient({
method: endpoints.TaxOrderItems.method,
endpoint: endpoints.TaxOrderItems.url,
payload: {
ParentAccountNumber: this.order.parentAccountNumber,
BillToAccountNumber: this.billToAccountNumber,
ProviderNumber: this.providerNumber,
AppointmentType: appointmentType,
PricedLineItems: getLineItemsFlattened(pricedLineItems),
ServiceLocation: {
City: isMobileApt ? serviceLocationCity : null,
State: isMobileApt ? serviceLocationState : null,
ZipCode: isMobileApt ? serviceLocationZipCode : null,
},
ServerData: lineItemServerData ? lineItemServerData : "",
},
ParentAccountNumber: this.order.parentAccountNumber,
BillToAccountNumber: this.billToAccountNumber,
ProviderNumber: this.providerNumber,
AppointmentType: appointmentType,
PricedLineItems: getLineItemsFlattened(pricedLineItems),
ServiceLocation: {
City: isMobileApt ? serviceLocationCity : null,
State: isMobileApt ? serviceLocationState : null,
ZipCode: isMobileApt ? serviceLocationZipCode : null
},
ServerData: lineItemServerData || ''
}
}).then((response) => {
this.order.lineItems.serverData = response.data.serverData;
return addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems);
@ -2575,6 +2596,7 @@ export const useMainStore = defineStore({
contact.homePhone = phoneNumbers.home !== undefined ? phoneNumbers.home : contact.homePhone;
contact.alternativePhone = phoneNumbers.alternative !== undefined ? phoneNumbers.alternative : contact.alternativePhone;
contact.servicePhone = phoneNumbers.service !== undefined ? phoneNumbers.service : contact.servicePhone;
contact.extension = phoneNumbers.extension !== undefined ? phoneNumbers.extension : contact.extension;
},
GetExperimentsByUser(userId) {

View file

@ -494,18 +494,21 @@ describe('Store', () => {
const homePhone = getRandomInt(1000000000, 9999999999);
const servicePhone = getRandomInt(1000000000, 9999999999);
const altPhone = getRandomInt(1000000000, 9999999999);
const extension = getRandomInt(10000, 99999);
// Act
store.updatePhoneNumbers({
home: homePhone,
service: servicePhone,
alternative: altPhone
alternative: altPhone,
extension: extension
});
// Assert
expect(store.contactInfo.homePhone).toEqual(homePhone);
expect(store.contactInfo.servicePhone).toEqual(servicePhone);
expect(store.contactInfo.alternativePhone).toEqual(altPhone);
expect(store.contactInfo.extension).toEqual(extension);
});
it('All null values => contact info set in store to all nulls', () => {
// Act

View file

@ -22,44 +22,6 @@ body {
flex-direction: column;
padding: 0 1.5rem;
}
// Set max-width on columns to prevent overly-wide
// components on extra wide screens.
.col-md-6 {
max-width: 472px;
@include media-breakpoint-up(xl) {
max-width: 708px;
}
.col {
max-width: 236px;
&.one-list-card-width {
max-width: 472px;
@include media-breakpoint-up(xl) {
max-width: 66.6666666%;
}
}
}
.shop-question {
.col {
max-width: 472px;
}
}
}
.col-xl-4 {
max-width: 472px;
.col {
max-width: 100%;
}
}
//END set max-width on columns
}
.pointer {
@ -156,6 +118,10 @@ body {
width: 46.25rem; // 740px
}
@include media-breakpoint-up(lg) {
width: 60rem; // 960px
}
@include media-breakpoint-up(xl) {
width: 63.75rem; // 1020px
}
@ -166,7 +132,7 @@ body {
width: 100%;
}
@include media-breakpoint-up(md) {
@include media-breakpoint-up(lg) {
width: 58.33333333%;
}
}
@ -174,10 +140,15 @@ body {
div.textbox-question {
label {
span.sub-caption {
color: #525656;
font-weight: 400;
}
}
}
.site-sub-header-container {
margin-top: 1.25rem;
}
}
:root {
@ -203,10 +174,10 @@ $heritage-btn-width: 9.0625rem; // 145px
@include heritage-btn-variant('secondary', $white, $heritage-blue-secondary, transparent, true);
@include heritage-btn-link('link', $blue, transparent, $heritage-blue-secondary);
@include heritage-btn-size('lg', 0.625rem, 1.25rem);
@include heritage-btn-size('lg', 0.625rem, 1.25rem);
@include heritage-btn-size('md', 7px, 15px);
@include heritage-btn-size('sm', 3px, 6px);
@include heritage-btn-size('xs', 1px, 5px);
@include heritage-btn-size('sm', 3px, 6px);
@include heritage-btn-size('xs', 1px, 5px);
&.btn-link {
--bs-btn-padding-x: 0px;

View file

@ -115,21 +115,3 @@ caption,
font-weight: 800;
font-style: normal;
}
@font-face {
font-family: "Urbanist";
src:
url("@/assets/fonts/Urbanist-MediumItalic.woff2") format("woff2"),
url("@/assets/fonts/Urbanist-MediumItalic.woff") format("woff");
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: "Urbanist";
src:
url("@/assets/fonts/Urbanist-BoldItalic.woff2") format("woff2"),
url("@/assets/fonts/Urbanist-BoldItalic.woff") format("woff");
font-weight: 700;
font-style: italic;
}

View file

@ -11,23 +11,25 @@ html {
border: 1px solid $red;
position: relative;
z-index: 4;
border-radius: 60px;
.button-content {
border: none;
}
&:hover {
@include box-shadow-hover($red-200);
z-index: 5;
border: 1px solid $red;
}
input[type="radio"]:focus+.list-button-content {
box-shadow: 0 0 0 2.5px $red;
}
&:not(.selected):not(.no-hover):hover {
position: relative;
@include box-shadow-hover($red-200);
z-index: 5;
}
}
&.list-card {
border: 1px solid $red;
border-radius: 0.5rem;
}
&.ui-radio {
@ -48,7 +50,23 @@ html {
// END HOVER
&.list-button,
&.list-button {
color: $red;
input[type="checkbox"]:focus+label,
input[type="radio"]:focus+label {
box-shadow: 0 0 0 2.5px $red;
}
input[type="checkbox"]:checked+label {
box-shadow: 0 0 0 1px $red;
}
&:hover {
border-radius: 60px;
}
}
&.list-card {
color: $red;
@ -141,6 +159,7 @@ html {
border: 1px solid $success;
position: relative;
z-index: 4;
border-radius: 60px;
.button-content {
border: none;
@ -158,6 +177,7 @@ html {
&.list-card {
border: 1px solid $success;
border-radius: 0.5rem;
}
&.ui-radio {
@ -178,7 +198,23 @@ html {
// END HOVER
&.list-button,
&.list-button {
color: $success;
input[type="checkbox"]:focus+label,
input[type="radio"]:focus+label {
box-shadow: 0 0 0 2.5px $success;
}
input[type="checkbox"]:checked+label {
box-shadow: 0 0 0 1px $success;
}
&:hover {
border-radius: 60px;
}
}
&.list-card {
color: $success;

View file

@ -1,7 +1,9 @@
.base-input-button {
&:not(.has-error):hover {
&:not(.disabled) {
cursor: pointer;
}
&:not(.has-error):hover {
&.list-button,
&.list-button-horizontal,
&.list-card {

View file

@ -1,4 +1,5 @@
$svg-calendar-picker: "data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.1762 1.52764H13.7656V1.50352C13.7656 1.10476 13.6076 0.722334 13.3263 0.44037C13.0451 0.158406 12.6636 0 12.2659 0C11.8681 0 11.4866 0.158406 11.2054 0.44037C10.9241 0.722334 10.7661 1.10476 10.7661 1.50352V1.52764H5.42476V1.50352C5.42476 1.10476 5.26675 0.722334 4.9855 0.44037C4.70424 0.158406 4.32277 0 3.92501 0C3.52725 0 3.14579 0.158406 2.86453 0.44037C2.58327 0.722334 2.42526 1.10476 2.42526 1.50352V1.52764H1.82376C1.34046 1.52891 0.877316 1.72195 0.53557 2.06455C0.193824 2.40716 0.00127018 2.87146 0 3.35598V14.1717C0.0016909 14.656 0.194379 15.1201 0.536035 15.4626C0.87769 15.8051 1.34059 15.9983 1.82376 16H14.1746C14.6581 15.9987 15.1214 15.8057 15.4634 15.4632C15.8054 15.1206 15.9983 14.6563 16 14.1717V3.35598C15.9987 2.87146 15.8062 2.40716 15.4644 2.06455C15.1227 1.72195 14.6595 1.52891 14.1762 1.52764ZM11.8889 1.50352C11.8889 1.4033 11.9286 1.30718 11.9993 1.23631C12.07 1.16544 12.1659 1.12563 12.2659 1.12563C12.3658 1.12563 12.4617 1.16544 12.5324 1.23631C12.6031 1.30718 12.6428 1.4033 12.6428 1.50352V2.99899C12.6428 3.09922 12.6031 3.19534 12.5324 3.2662C12.4617 3.33707 12.3658 3.37688 12.2659 3.37688C12.1659 3.37688 12.07 3.33707 11.9993 3.2662C11.9286 3.19534 11.8889 3.09922 11.8889 2.99899V1.50352ZM3.54807 1.50352C3.54807 1.4033 3.58778 1.30718 3.65847 1.23631C3.72916 1.16544 3.82504 1.12563 3.92501 1.12563C4.02498 1.12563 4.12086 1.16544 4.19155 1.23631C4.26224 1.30718 4.30195 1.4033 4.30195 1.50352V2.99899C4.30195 3.09922 4.26224 3.19534 4.19155 3.2662C4.12086 3.33707 4.02498 3.37688 3.92501 3.37688C3.82504 3.37688 3.72916 3.33707 3.65847 3.2662C3.58778 3.19534 3.54807 3.09922 3.54807 2.99899V1.50352ZM14.8772 14.1717C14.8747 14.3573 14.8001 14.5345 14.6691 14.6658C14.5382 14.797 14.3614 14.8719 14.1762 14.8744H1.82536C1.63995 14.8723 1.4627 14.7976 1.33144 14.6663C1.20018 14.5351 1.12531 14.3575 1.12281 14.1717V6.59296H14.8772V14.1717Z' fill='%23167CAC'/%3E%3Cpath d='M2.33063 11.282H3.93464V12.6006C3.93464 12.7499 3.99379 12.8931 4.09907 12.9986C4.20435 13.1041 4.34715 13.1634 4.49604 13.1634C4.64494 13.1634 4.78773 13.1041 4.89301 12.9986C4.9983 12.8931 5.05745 12.7499 5.05745 12.6006V11.282H7.46346V12.6006C7.46346 12.7499 7.52261 12.8931 7.62789 12.9986C7.73318 13.1041 7.87597 13.1634 8.02486 13.1634C8.17376 13.1634 8.31655 13.1041 8.42184 12.9986C8.52712 12.8931 8.58627 12.7499 8.58627 12.6006V11.282H10.9923V12.6006C10.9923 12.7499 11.0514 12.8931 11.1567 12.9986C11.262 13.1041 11.4048 13.1634 11.5537 13.1634C11.7026 13.1634 11.8454 13.1041 11.9507 12.9986C12.0559 12.8931 12.1151 12.7499 12.1151 12.6006V11.282H13.7191C13.868 11.282 14.0108 11.2227 14.1161 11.1172C14.2214 11.0116 14.2805 10.8685 14.2805 10.7192C14.2805 10.57 14.2214 10.4268 14.1161 10.3213C14.0108 10.2157 13.868 10.1564 13.7191 10.1564H12.1151V8.84425C12.1151 8.69498 12.0559 8.55183 11.9507 8.44628C11.8454 8.34073 11.7026 8.28143 11.5537 8.28143C11.4048 8.28143 11.262 8.34073 11.1567 8.44628C11.0514 8.55183 10.9923 8.69498 10.9923 8.84425V10.1628H8.58627V8.84425C8.58627 8.69498 8.52712 8.55183 8.42184 8.44628C8.31655 8.34073 8.17376 8.28143 8.02486 8.28143C7.87597 8.28143 7.73318 8.34073 7.62789 8.44628C7.52261 8.55183 7.46346 8.69498 7.46346 8.84425V10.1628H5.05745V8.84425C5.05745 8.69498 4.9983 8.55183 4.89301 8.44628C4.78773 8.34073 4.64494 8.28143 4.49604 8.28143C4.34715 8.28143 4.20435 8.34073 4.09907 8.44628C3.99379 8.55183 3.93464 8.69498 3.93464 8.84425V10.1628H2.33063C2.18174 10.1628 2.03894 10.2221 1.93366 10.3277C1.82837 10.4332 1.76923 10.5764 1.76923 10.7257C1.76923 10.8749 1.82837 11.0181 1.93366 11.1236C2.03894 11.2292 2.18174 11.2885 2.33063 11.2885V11.282Z' fill='%23167CAC'/%3E%3C/svg%3E";
$svg-error-calendar-graphic: "data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.1762 1.52764H13.7656V1.50352C13.7656 1.10476 13.6076 0.722334 13.3263 0.44037C13.0451 0.158406 12.6636 0 12.2659 0C11.8681 0 11.4866 0.158406 11.2054 0.44037C10.9241 0.722334 10.7661 1.10476 10.7661 1.50352V1.52764H5.42476V1.50352C5.42476 1.10476 5.26675 0.722334 4.9855 0.44037C4.70424 0.158406 4.32277 0 3.92501 0C3.52725 0 3.14579 0.158406 2.86453 0.44037C2.58327 0.722334 2.42526 1.10476 2.42526 1.50352V1.52764H1.82376C1.34046 1.52891 0.877316 1.72195 0.53557 2.06455C0.193824 2.40716 0.00127018 2.87146 0 3.35598V14.1717C0.0016909 14.656 0.194379 15.1201 0.536035 15.4626C0.87769 15.8051 1.34059 15.9983 1.82376 16H14.1746C14.6581 15.9987 15.1214 15.8057 15.4634 15.4632C15.8054 15.1206 15.9983 14.6563 16 14.1717V3.35598C15.9987 2.87146 15.8062 2.40716 15.4644 2.06455C15.1227 1.72195 14.6595 1.52891 14.1762 1.52764ZM11.8889 1.50352C11.8889 1.4033 11.9286 1.30718 11.9993 1.23631C12.07 1.16544 12.1659 1.12563 12.2659 1.12563C12.3658 1.12563 12.4617 1.16544 12.5324 1.23631C12.6031 1.30718 12.6428 1.4033 12.6428 1.50352V2.99899C12.6428 3.09922 12.6031 3.19534 12.5324 3.2662C12.4617 3.33707 12.3658 3.37688 12.2659 3.37688C12.1659 3.37688 12.07 3.33707 11.9993 3.2662C11.9286 3.19534 11.8889 3.09922 11.8889 2.99899V1.50352ZM3.54807 1.50352C3.54807 1.4033 3.58778 1.30718 3.65847 1.23631C3.72916 1.16544 3.82504 1.12563 3.92501 1.12563C4.02498 1.12563 4.12086 1.16544 4.19155 1.23631C4.26224 1.30718 4.30195 1.4033 4.30195 1.50352V2.99899C4.30195 3.09922 4.26224 3.19534 4.19155 3.2662C4.12086 3.33707 4.02498 3.37688 3.92501 3.37688C3.82504 3.37688 3.72916 3.33707 3.65847 3.2662C3.58778 3.19534 3.54807 3.09922 3.54807 2.99899V1.50352ZM14.8772 14.1717C14.8747 14.3573 14.8001 14.5345 14.6691 14.6658C14.5382 14.797 14.3614 14.8719 14.1762 14.8744H1.82536C1.63995 14.8723 1.4627 14.7976 1.33144 14.6663C1.20018 14.5351 1.12531 14.3575 1.12281 14.1717V6.59296H14.8772V14.1717Z' fill='" + $svg-calendar-error-stroke-color + "'/%3E%3Cpath d='M2.33063 11.282H3.93464V12.6006C3.93464 12.7499 3.99379 12.8931 4.09907 12.9986C4.20435 13.1041 4.34715 13.1634 4.49604 13.1634C4.64494 13.1634 4.78773 13.1041 4.89301 12.9986C4.9983 12.8931 5.05745 12.7499 5.05745 12.6006V11.282H7.46346V12.6006C7.46346 12.7499 7.52261 12.8931 7.62789 12.9986C7.73318 13.1041 7.87597 13.1634 8.02486 13.1634C8.17376 13.1634 8.31655 13.1041 8.42184 12.9986C8.52712 12.8931 8.58627 12.7499 8.58627 12.6006V11.282H10.9923V12.6006C10.9923 12.7499 11.0514 12.8931 11.1567 12.9986C11.262 13.1041 11.4048 13.1634 11.5537 13.1634C11.7026 13.1634 11.8454 13.1041 11.9507 12.9986C12.0559 12.8931 12.1151 12.7499 12.1151 12.6006V11.282H13.7191C13.868 11.282 14.0108 11.2227 14.1161 11.1172C14.2214 11.0116 14.2805 10.8685 14.2805 10.7192C14.2805 10.57 14.2214 10.4268 14.1161 10.3213C14.0108 10.2157 13.868 10.1564 13.7191 10.1564H12.1151V8.84425C12.1151 8.69498 12.0559 8.55183 11.9507 8.44628C11.8454 8.34073 11.7026 8.28143 11.5537 8.28143C11.4048 8.28143 11.262 8.34073 11.1567 8.44628C11.0514 8.55183 10.9923 8.69498 10.9923 8.84425V10.1628H8.58627V8.84425C8.58627 8.69498 8.52712 8.55183 8.42184 8.44628C8.31655 8.34073 8.17376 8.28143 8.02486 8.28143C7.87597 8.28143 7.73318 8.34073 7.62789 8.44628C7.52261 8.55183 7.46346 8.69498 7.46346 8.84425V10.1628H5.05745V8.84425C5.05745 8.69498 4.9983 8.55183 4.89301 8.44628C4.78773 8.34073 4.64494 8.28143 4.49604 8.28143C4.34715 8.28143 4.20435 8.34073 4.09907 8.44628C3.99379 8.55183 3.93464 8.69498 3.93464 8.84425V10.1628H2.33063C2.18174 10.1628 2.03894 10.2221 1.93366 10.3277C1.82837 10.4332 1.76923 10.5764 1.76923 10.7257C1.76923 10.8749 1.82837 11.0181 1.93366 11.1236C2.03894 11.2292 2.18174 11.2885 2.33063 11.2885V11.282Z' fill='" + $svg-calendar-error-stroke-color + "'/%3E%3C/svg%3E";
$svg-date-picker-nav-back-button: "data:image/svg+xml,%3Csvg width='7' height='12' viewBox='0 0 7 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.331685 6.00121C0.330445 5.82446 0.399256 5.65442 0.523053 5.52832L5.84499 0.198256C5.97188 0.0713149 6.14397 -8.63821e-08 6.32341 -6.72174e-08C6.50285 -4.80527e-08 6.67495 0.071315 6.80183 0.198256C6.92872 0.325198 7 0.497368 7 0.67689C7 0.856412 6.92872 1.02858 6.80183 1.15552L1.94874 6.00121L6.80183 10.8526C6.92745 10.9796 6.99751 11.1512 6.99662 11.3299C6.99572 11.5085 6.92393 11.6794 6.79705 11.8051C6.67016 11.9308 6.49857 12.0009 6.32003 12C6.14148 11.9991 5.97061 11.9273 5.84499 11.8003L0.526881 6.47601C0.401568 6.34983 0.331375 6.17908 0.331685 6.00121Z' fill='%231574A1'/%3E%3C/svg%3E%0A";
$svg-date-picker-forward-button: "data:image/svg+xml,%3Csvg width='7' height='12' viewBox='0 0 7 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.66831 5.99879C6.66955 6.17554 6.60074 6.34558 6.47695 6.47168L1.15501 11.8017C1.02812 11.9287 0.85603 12 0.676587 12C0.497145 12 0.325053 11.9287 0.198168 11.8017C0.0712831 11.6748 7.268e-09 11.5026 8.07183e-09 11.3231C8.87567e-09 11.1436 0.0712831 10.9714 0.198168 10.8445L5.05126 5.99879L0.198168 1.14736C0.0725521 1.02042 0.00248585 0.848755 0.00338306 0.670131C0.00428028 0.491506 0.0760674 0.320554 0.202952 0.194881C0.329837 0.0692091 0.501426 -0.000888818 0.679971 9.54485e-06C0.858515 0.000906955 1.02939 0.0727263 1.15501 0.199668L6.47312 5.52399C6.59843 5.65017 6.66862 5.82092 6.66831 5.99879Z' fill='%231574A1'/%3E%3C/svg%3E%0A";
$svg-drop-off-alert: "data:image/svg+xml,%3Csvg viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_13957_112512)'%3E%3Cpath d='M5.99865 0C4.81147 4.82643e-07 3.65095 0.352111 2.66392 1.01179C1.67688 1.67146 0.907678 2.60907 0.45361 3.70599C-0.000459241 4.80291 -0.11899 6.00986 0.113013 7.17415C0.345015 8.33845 0.917126 9.40778 1.75697 10.2469C2.59682 11.086 3.66666 11.6571 4.83117 11.8881C5.99567 12.119 7.20251 11.9994 8.29902 11.5443C9.39553 11.0893 10.3324 10.3192 10.9912 9.33159C11.65 8.34396 12.0011 7.18313 12 5.99594C11.9971 4.40566 11.3638 2.88142 10.2388 1.75742C9.11375 0.633431 7.58894 0.00143011 5.99865 0V0ZM5.99865 11.2478C4.96135 11.2473 3.94748 10.9392 3.08518 10.3627C2.22288 9.7861 1.55085 8.96685 1.15401 8.00846C0.75718 7.05006 0.653353 5.99554 0.855656 4.97815C1.05796 3.96077 1.55731 3.02618 2.29061 2.29251C3.0239 1.55884 3.95823 1.059 4.97551 0.856176C5.99279 0.653349 7.04737 0.756633 8.00597 1.15297C8.96457 1.54931 9.78416 2.22092 10.3612 3.08293C10.9382 3.94493 11.2467 4.95864 11.2478 5.99594C11.2478 7.38835 10.6949 8.72377 9.71053 9.70861C8.7262 10.6934 7.39106 11.2471 5.99865 11.2478V11.2478Z' fill='%2306577C'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.22736 8.84695C6.30613 8.76818 6.35038 8.66135 6.35038 8.54996V5.30996C6.35038 5.19857 6.30613 5.09174 6.22736 5.01298C6.1486 4.93421 6.04177 4.88996 5.93038 4.88996C5.81899 4.88996 5.71216 4.93421 5.63339 5.01298C5.55463 5.09174 5.51038 5.19857 5.51038 5.30996V8.54996C5.51038 8.66135 5.55463 8.76818 5.63339 8.84695C5.71216 8.92571 5.81899 8.96996 5.93038 8.96996C6.04177 8.96996 6.1486 8.92571 6.22736 8.84695ZM5.69704 3.97918C5.76611 4.02533 5.84731 4.04996 5.93038 4.04996C5.98558 4.05012 6.04026 4.03936 6.09129 4.01831C6.14232 3.99726 6.18868 3.96633 6.22771 3.9273C6.26675 3.88827 6.29768 3.8419 6.31873 3.79088C6.33978 3.73985 6.35053 3.68516 6.35038 3.62996C6.35038 3.54689 6.32574 3.46569 6.27959 3.39662C6.23344 3.32755 6.16785 3.27372 6.0911 3.24193C6.01436 3.21014 5.92991 3.20183 5.84844 3.21803C5.76697 3.23424 5.69213 3.27424 5.63339 3.33298C5.57465 3.39171 5.53465 3.46655 5.51845 3.54802C5.50224 3.6295 5.51056 3.71394 5.54235 3.79069C5.57414 3.86743 5.62797 3.93303 5.69704 3.97918Z' fill='%2306577C'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_13957_112512'%3E%3Crect width='12' height='12' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A";

View file

@ -20,7 +20,7 @@ $red-100: #ffe6e4;
$red-200: #fcbfbb;
$red-300: #f89892;
$red-400: #e65c53;
$red: #d4281c; // Default Red
$red: #db0020; // Default Red
$red-600: #ac160b;
$red-700: #840900;
$red-800: #5b0600;
@ -70,8 +70,13 @@ $orange: #fd7e14;
$teal: #20c997;
$cyan: #0dcaf0;
//Heritage
$heritage-blue-primary: #0070D1;
$heritage-blue-secondary: #167cac;
// Darker gray
$darker-gray: #525656;
$lighter-gray: #9aa1a3;
// scss-docs-start colors-map
$colors: (
@ -153,7 +158,9 @@ $font-weight-bolder: bolder;
$border-radius: 0.25rem;
$border-radius-sm: 0.2rem;
$border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course.
$border-radius-xl: 1.375rem;
$border-radius-pill: 50rem;
$border-radius-list-button: 1.375rem; // Used for list buttons
//Progress Bar Styling
$progress-bar-success-color: $green;
@ -187,7 +194,9 @@ $spacers: (
//Bootstrap Grid Breakpoints - Use these breakpoints only
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
xxl: 1440px,
);
@ -204,6 +213,11 @@ $alert-bg-scale: -90%;
$alert-border-scale: -100%;
$alert-color-scale: 40%;
$alert-yellow-bg: #fef6e8;
$alert-yellow-color: #e86421;
$alert-red-bg: #fbe9e8;
$alert-red-color: $red;
//Modal animation
// This affects all [Bootstrap] modals
$modal-fade-transform: translate(0, 100%);
@ -215,9 +229,8 @@ $enable-important-utilities: false;
// Letter Spacing
$letter-spacing-primary: 0.03em;
// Borders
$border-input: 1px solid rgba(179, 180, 181);
$border-input-focus: 2.5px solid $blue;
$border-input: 1px solid $gray-350;
$border-input-focus: 2.5px solid $heritage-blue-primary;
//Heritage
$heritage-blue-primary: #0070D1;
$heritage-blue-secondary: #167cac;
$svg-calendar-error-fill-color: $alert-red-bg;
$svg-calendar-error-stroke-color: '%23db0020'; // URL encoded #db0020

View file

@ -1,64 +1,85 @@
<template>
<div
class="alert fade show text-center mb-0 py-2 px-4"
class="alert fade show"
role="alert"
:class="[
isDismissible ? 'alert-dismissible' : '',
alertClass,
cssClassNameForCmsWidget,
]">
<p class="m-0 fw-bold alert-heading">
{{ alertHeadline }}
</p>
<template
v-for="paragraph in splitAlertCopyForParagraphTag"
:key="paragraph">
<p
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
class="m-0 text-body small"
v-html="paragraph"></p>
<p
v-else
class="m-0 text-body small">
<template v-if="doesCopyContainRouterLink(paragraph)">
<textBlock
:customText="paragraph"
class="mb-1"
marginTopSizeOverride="0" />
</template>
<template
v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)"
v-else
:key="copy">
<span v-if="doesCopyContainTextLink(copy)">
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
@click-event="
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
" />
</span>
<span
v-else
v-html="copy"></span>
</template>
</p>
</template>
<button
type="button"
class="btn-close p-2"
data-bs-dismiss="alert"
aria-label="Close">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 23.7 23.7"
xml:space="preserve">
<path
d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581 1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21 .46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z" />
<div class="alert-header">
<svg class="alert-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
<path fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
</svg>
</button>
<span class="m-0 fw-bold alert-header-text">
{{ alertHeadline }}
</span>
<button
v-if="isCollapsible"
class="btn-collapse"
type="button"
@click="toggleCollapse">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="9" viewBox="0 0 15 9"
class="btn-collapse-icon"
:class="{ 'rotated': !collapsed }">
<path fill-rule="nonzero" d="M7.5 0a.806.806 0 0 0-.593.265L.246 7.455a.957.957 0 0 0 0 1.28.796.796 0 0 0 1.185 0l6.07-6.55 6.068 6.55a.796.796 0 0 0 1.186 0 .957.957 0 0 0 0-1.28L8.093.265A.806.806 0 0 0 7.5 0"/>
</svg>
</button>
<button
v-if="isDismissible"
type="button"
class="btn-close p-2"
data-bs-dismiss="alert"
aria-label="Close">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 23.7 23.7"
xml:space="preserve">
<path
d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581 1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21 .46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z" />
</svg>
</button>
</div>
<div class="alert-body"
v-show="alertCopy"
:id="collapseId">
<template
v-for="paragraph in splitAlertCopyForParagraphTag"
:key="paragraph">
<p
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
class="m-0 text-body"
v-html="paragraph"></p>
<p
v-else
class="m-0 text-body">
<template v-if="doesCopyContainRouterLink(paragraph)">
<textBlock
:customText="paragraph"
class="mb-1"
marginTopSizeOverride="0" />
</template>
<template
v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)"
v-else
:key="copy">
<span v-if="doesCopyContainTextLink(copy)">
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
@click-event="
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
" />
</span>
<span
v-else
v-html="copy"></span>
</template>
</p>
</template>
</div>
</div>
</template>
@ -74,6 +95,7 @@ import {
import applicationConfig from '@/constants/application-config';
import textBlock from '@/digital-components/text-block/text-block.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
import { Collapse } from 'bootstrap';
export default {
// eslint-disable-next-line vue/multi-word-component-names
@ -84,13 +106,14 @@ export default {
},
props: {
isDismissible: Boolean,
isCollapsible: Boolean,
/*
alertClass class names:
alert-success (green)
alert-danger (red)
alert-warning (yellow)
alert-info (blue)
*/
alertClass class names:
alert-success (green)
alert-danger (red)
alert-warning (yellow)
alert-info (blue)
*/
alertClass: String,
cmsWidgetName: {
type: String,
@ -106,7 +129,14 @@ export default {
shouldScrollToOnMount: {
type: Boolean,
default: true
}
},
startCollapsed: Boolean
},
data() {
return {
collapsed: this.startCollapsed || false,
collapseElement: null,
};
},
computed: {
pageQueryString() {
@ -124,10 +154,18 @@ export default {
},
splitAlertCopyForParagraphTag() {
return splitCMSCopyOnParagraphTag(this.alertCopy);
}
},
collapseId() {
return this.isCollapsible
? `${this.cmsWidgetName}-collapse`
: null;
},
},
mounted() {
this.ensureAlertIsInViewPort();
if (this.isCollapsible) {
this.collapseElement = Collapse.getOrCreateInstance(document.getElementById(this.collapseId));
}
},
methods: {
doesCopyContainRouterLink,
@ -152,37 +190,83 @@ export default {
<= (window.innerHeight - footerHeight
|| document.documentElement.clientHeight - footerHeight)
);
}
},
toggleCollapse() {
this.collapsed = !this.collapsed;
if (this.collapsed) {
this.collapseElement.hide();
} else {
this.collapseElement.show();
}
},
}
};
</script>
<style lang="scss" scoped>
.alert {
border: 1px solid;
padding: 0rem;
margin-bottom: 2rem;
.alert-header {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
padding: .75rem 1rem .75rem 1rem;
font-weight: 600;
color: $black;
}
.alert-icon {
min-width: 1rem;
max-height: 1rem;
}
.alert-header-text {
flex-grow: 1;
padding: 0rem .625rem;
}
.btn-collapse {
display: flex;
border: none;
background: none;
min-width: 1rem;
max-height: 1rem;
padding: 0;
}
.btn-collapse-icon {
height: 100%;
width: 100%;
transition: transform 0.3s;
transform: rotate(180deg);
&.rotated {
transform: none;
}
}
a {
text-decoration: none;
font-weight: $font-weight-normal;
}
border-color: transparent;
button {
display: none;
}
.btn-close {
background: none;
opacity: 1;
width: 0.75rem;
height: 0.75rem;
top: 2px;
right: 2px;
}
&.alert-dismissible {
button {
display: flex;
top: 2px;
right: 2px;
}
.alert-body {
margin: 0 1rem;
padding: .5rem 1rem .75rem 1rem;
border-top: 1px solid;
color: $darker-gray;
line-height: 26px;
font-weight: 400;
}
&.alert-info {
background-color: $blue-100;
.alert-heading {
color: $blue-700;
border-color: $blue-200;
.alert-body {
border-top-color: $blue-200;
}
svg {
fill: $blue-700;
@ -191,9 +275,10 @@ export default {
}
}
&.alert-danger {
background-color: $red-100;
.alert-heading {
color: $red-600;
background-color: $alert-red-bg;
border-color: $alert-red-color;
.alert-body {
border-top-color: $alert-red-color;
}
svg {
fill: $red-600;
@ -202,20 +287,20 @@ export default {
}
}
&.alert-warning {
background-color: $yellow-100;
.alert-heading {
color: $yellow-600;
background-color: $alert-yellow-bg;
border-color: $alert-yellow-color;
.alert-body {
border-top-color: $alert-yellow-color;
}
svg {
fill: $yellow-600;
width: 1rem;
height: 1rem;
fill: $alert-yellow-color;
}
}
&.alert-success {
background-color: $green-100;
.alert-heading {
color: $green-700;
border-color: $green-200;
.alert-body {
border-top-color: $green-200;
}
svg {
fill: $green-700;
@ -223,9 +308,5 @@ export default {
height: 1rem;
}
}
& p {
font-size: 0.875rem;
margin-bottom: 0.25rem !important;
}
}
</style>

View file

@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import buttonVariants from '@/constants/button-variants';
import { buttonVariants } from '@/constants/component-variants';
import buttonSizes from '@/constants/button-sizes';
/** @ignore */

View file

@ -9,7 +9,7 @@
</template>
<script>
import buttonVariants from '@/constants/button-variants';
import { buttonVariants } from '@/constants/component-variants';
import buttonSizes from '@/constants/button-sizes';
export default {

View file

@ -5,7 +5,10 @@
buttonWrapperClasses="list-group base-input-button list-button no-hover d-flex flex-column w-100 mb-2">
<div
:aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4"
:class="{
'has-sub-copy': buttonLabelSubCopy,
}">
<span
class="m-0"
:class="textPosition">
@ -85,6 +88,10 @@ $heritage-checked-border-color: #0070d1;
background: $heritage-checked-background-color;
border-color: $heritage-checked-border-color;
box-shadow: 0 0 0 1px $blue;
&:not(.has-sub-copy) {
font-weight: 500;
color: $black;
}
}
&:checked + .list-button-content span:nth-child(2) {
font-weight: 400;
@ -104,10 +111,15 @@ $heritage-checked-border-color: #0070d1;
width: 100%;
outline: none;
&:not(.has-sub-copy) {
font-weight: 400;
color: $darker-gray;
}
span {
&.small {
color: $darker-gray;
}
}
}
</style>
</style>

View file

@ -62,7 +62,7 @@ export default {
<style lang="scss" scoped>
a {
color: #0070d1;
color: $heritage-blue-primary;
text-decoration: none;
line-height: 1.56;
padding: 0;