Merge branch 'develop' into feature/humphries/INSR-7768

This commit is contained in:
Alex Humphries 2026-01-16 12:46:19 -05:00
commit 67faf453ea
37 changed files with 5451 additions and 1898 deletions

View file

@ -8,9 +8,18 @@ schedules:
- develop - develop
pool: 'Default' pool: 'Default'
resources:
repositories:
- repository: AzureDevOps
type: github
name: Safelite/AzureDevOps
endpoint: Safelite
ref: refs/tags/t5.7.40
variables: variables:
# - group: Digital-Infrastructure # - group: Digital-Infrastructure
# - group: ISS-BuildBranches # - group: ISS-BuildBranches
- group: SafelitePlaywright
- name: dockerImageName - name: dockerImageName
value: 'playwright-tests' value: 'playwright-tests'
- name: imageTag - name: imageTag
@ -30,158 +39,17 @@ stages:
- stage: TestPr - stage: TestPr
displayName: Run Playwright Test displayName: Run Playwright Test
jobs: jobs:
- job: playwright_tests - template: templates/digital/playwright-test.yml@AzureDevOps
continueOnError: true parameters:
strategy: applicationType: 'vue'
matrix: totalShards: ${{ variables.totalShards }}
shard1: targetUrl: $(BASE_URL)
shardNumber: 1 dockerFileName: 'Dockerfile.playwright'
shard2: isRegression: ${{ variables.IS_REGRESSION }}
shardNumber: 2 filterTags: '@Advanced'
shard3: playwrightTestsPath: 'playwright-tests'
shardNumber: 3 npmServePath: '.'
shard4: npmrcPath: 'playwright-tests/.npmrc'
shardNumber: 4 secrets:
CCIS_API_AUTH: $(CCIS_API_AUTH)
steps: JIRA_API_KEY: $(JIRA_API_KEY)
- 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()

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 { defineConfig, devices } from '@playwright/test';
import { JiraReporterConfig } from 'playwright-jira-reporter'
import dotenv from 'dotenv-safe'; import dotenv from 'dotenv-safe';
import { OrtoniReportConfig } from "ortoni-report"; import { OrtoniReportConfig } from "ortoni-report";
import * as path from 'path'; import path from 'path';
if (!process.env.CI) { if (!process.env.CI) {
// Environment variables are present in CI environment, no need to read from file // Environment variables are present in CI environment, no need to read from file
if (process.env.NODE_ENV == 'undefined' || process.env.NODE_ENV == null) { const basePath = __dirname; // This gets the directory where the config file is located
dotenv.config({ path: `playwright-tests/.env.dev`, example: 'playwright-tests/.env.example' });
} if (process.env.PLAYWRIGHT_ENV == undefined || process.env.PLAYWRIGHT_ENV == null) {
else { dotenv.config({
dotenv.config({ path: `playwright-tests/.env.${process.env.NODE_ENV}`, example: 'playwright-tests/.env.example' }); 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,
}
/** // Jira Report config
* Read environment variables from file. const jiraReportConfig: JiraReporterConfig = {
* https://github.com/motdotla/dotenv // Jira Reporter Config
*/ isRegressionRun: process.env.IS_REGRESSION === 'true',
// import dotenv from 'dotenv'; jiraProjectKey: process.env.JIRA_PROJECT_KEY || '',
// import path from 'path'; jiraEpicKey: process.env.JIRA_EPIC_KEY || '',
// dotenv.config({ path: path.resolve(__dirname, '.env') }); jiraCardNumber: process.env.JIRA_CARD_NUMBER || '',
applicationName: 'ISS NextGen',
/** jiraApiUtilConfig: {
* See https://playwright.dev/docs/test-configuration. jiraUrl: process.env.JIRA_SERVER || '',
*/ jiraUsername: process.env.JIRA_USERNAME || '',
const reportConfig: OrtoniReportConfig = { jiraApiKey: process.env.JIRA_API_KEY || '',
port: 1994, jiraBoardId: process.env.JIRA_BOARD_ID || ''
open: "never", },
folderPath: "test-results", jiraCreationPermissions: {
filename: "index.html", isCreateTestSubtasks: process.env.IS_REGRESSION !== 'true',
logo: "../business-logic/data/logo.png", isCreateBugs: process.env.IS_REGRESSION !== 'true'
title: "Test Report", },
showProject: false, // Wrapped ortoni config
projectName: "ISS-Nextgen-Playwright-Report", ...ortoniReportConfig
testType: `E2E- Environment: ${process.env.NODE_ENV} `,
preferredTheme: "light",
base64Image: true,
}; };
export const reportFilePath = path.resolve(__dirname, './../test-results/accessibility-report.html'); 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. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 4 : 5, workers: process.env.CI ? 4 : 5,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [ reporter: process.env.CI? [
['ortoni-report', reportConfig], ['junit'],
['playwright-jira-reporter', jiraReportConfig],
['ortoni-report', ortoniReportConfig]
]: [
['ortoni-report', ortoniReportConfig],
['junit'], ['junit'],
['list'] ['list']
], ],
timeout: 120_000, timeout: 240_000,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
@ -68,52 +91,15 @@ export default defineConfig({
trace: 'on-first-retry', trace: 'on-first-retry',
headless: process.env.CI ? true : false, headless: process.env.CI ? true : false,
screenshot: "only-on-failure", screenshot: "only-on-failure",
actionTimeout: 5_000, actionTimeout: 60_000,
navigationTimeout: 20_000 navigationTimeout: 60_000
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */
projects: [ projects: [
{ {
name: 'chromium', name: 'chromium',
use: { ...devices['Desktop Chrome'] }, 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,
// },
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View file

@ -139,7 +139,7 @@ const endpoints = Object.freeze({
}, },
TaxOrderItems: { TaxOrderItems: {
url: `${PRICE_BASE_URL}/taxed-order-items`, url: `${PRICE_BASE_URL}/taxed-order-items`,
method: 'GET' method: 'POST'
}, },
LogExperimentExposureIfAssigned: { LogExperimentExposureIfAssigned: {
url: `${EXPERIMENTS_BASE_URL}/log-exposure`, url: `${EXPERIMENTS_BASE_URL}/log-exposure`,
@ -217,7 +217,6 @@ const endpoints = Object.freeze({
method: 'POST' method: 'POST'
}, },
DuplicateSearch: { DuplicateSearch: {
// eslint-disable-next-line max-len
url: `${ORDER_BASE_URL}/duplicate-check`, url: `${ORDER_BASE_URL}/duplicate-check`,
method: 'GET' method: 'GET'
}, },
@ -231,4 +230,4 @@ const endpoints = Object.freeze({
} }
}); });
export { endpoints }; export default endpoints;

View file

@ -34,7 +34,7 @@ const errorMessages = Object.freeze({
VIN_FORMAT: VIN_FORMAT:
// eslint-disable-next-line max-len // 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', '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 select an option', OPTION_REQUIRED: 'Please choose an option',
VEHICLE_REQUIRED: 'Please select a vehicle', VEHICLE_REQUIRED: 'Please select a vehicle',
POLICY_NUMBER_REQUIRED: 'Policy number is required.', POLICY_NUMBER_REQUIRED: 'Policy number is required.',
POLICY_NUMBER_FORMAT: 'Please enter an alpha-numeric string', POLICY_NUMBER_FORMAT: 'Please enter an alpha-numeric string',
@ -57,13 +57,15 @@ const errorMessages = Object.freeze({
POLICYHOLDER_FIRST_NAME_REQUIRED: 'First name is required.', POLICYHOLDER_FIRST_NAME_REQUIRED: 'First name is required.',
POLICYHOLDER_LAST_NAME_REQUIRED: 'Last name is required.', POLICYHOLDER_LAST_NAME_REQUIRED: 'Last name is required.',
ACKNOWLEDGEMENT_REQUIRED: 'You must agree to the terms to continue', ACKNOWLEDGEMENT_REQUIRED: 'You must agree to the terms to continue',
NO_SELECTION_REQUIRED: 'Please make a selection to continue',
YEAR_REQUIRED: 'Vehicle year is required.', YEAR_REQUIRED: 'Vehicle year is required.',
MAKE_REQUIRED: 'Vehicle make is required.', MAKE_REQUIRED: 'Vehicle make is required.',
MODEL_REQUIRED: 'Vehicle model is required.', MODEL_REQUIRED: 'Vehicle model is required.',
STYLE_REQUIRED: 'Vehicle style is required.', STYLE_REQUIRED: 'Vehicle style is required.',
MOBILE_LOCATION_REQUIRED: 'Please enter your service address', 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; export default errorMessages;

View file

@ -0,0 +1,6 @@
const PROVIDER_PREFERENCE_OPTIONS = Object.freeze({
SAFELITE: 'SafeliteOption',
TPA: 'TPAOption'
});
export default PROVIDER_PREFERENCE_OPTIONS;

File diff suppressed because it is too large Load diff

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) { export function convertDateStringToDate(dateString) {
// dateString must be YYYY-MM-DD format // dateString must be YYYY-MM-DD format
if (typeof dateString !== 'string') return null; if (typeof dateString !== 'string') return null;
@ -51,6 +66,12 @@ export function getDisplayTextForDurationLength(durationMinimum, durationMaximum
return `${durationText} ${unitText}`; 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) { export function militaryToTwelveHourTime(timeString) {
// Expected input: "HH:MM" // Expected input: "HH:MM"
if (typeof timeString !== 'string') return null; if (typeof timeString !== 'string') return null;
@ -163,9 +184,11 @@ export function combineDateAndTime(date, time) {
// Return the new date object // Return the new date object
return newDate; return newDate;
} }
export function addMinutes(date, minutes) { export function addMinutes(date, minutes) {
return new Date(date.getTime() + minutes * 60000); return new Date(date.getTime() + minutes * 60000);
} }
export function shortTimeString(date) { export function shortTimeString(date) {
// Use a ternary operator to check if the input is a valid date object // Use a ternary operator to check if the input is a valid date object
return date instanceof Date return date instanceof Date

View file

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

View file

@ -5,7 +5,7 @@
<baseInputButton <baseInputButton
v-bind="$props" v-bind="$props"
v-model="selectedValue" v-model="selectedValue"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover"> buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover mb-2">
<div <div
:aria-label="buttonLabel" :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">
@ -121,6 +121,7 @@ $heritage-checked-border-color: #0070d1;
&:checked + .list-button-content { &:checked + .list-button-content {
background: $heritage-checked-background-color; background: $heritage-checked-background-color;
border-color: $heritage-checked-border-color; border-color: $heritage-checked-border-color;
box-shadow: 0 0 0 1px $blue;
.button-label-copy { .button-label-copy {
font-weight: 500; font-weight: 500;
color: $black; color: $black;
@ -142,8 +143,6 @@ $heritage-checked-border-color: #0070d1;
box-shadow: $heritage-box-shadow; box-shadow: $heritage-box-shadow;
width: 100%; width: 100%;
outline: none; outline: none;
margin-bottom: .625rem;
height: 4.375rem;
} }
.button-content { .button-content {
row-gap: 0.25rem; row-gap: 0.25rem;

View file

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

View file

@ -55,7 +55,7 @@ function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRu
return { wrapper }; return { wrapper };
} }
const duplicateOrderText = 'Finish Existing Claim'; const duplicateOrderText = 'Finish existing claim';
describe('duplicateCheck.vue', () => { describe('duplicateCheck.vue', () => {
describe('Rendering', () => { describe('Rendering', () => {
@ -259,81 +259,6 @@ describe('duplicateCheck.vue', () => {
}); });
}); });
}); });
describe('getNewOrderSelectionName', () => {
test('answersFromCms null => returns empty string', () => {
// Arrange
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
});
const answersFromCmsValue = null;
mountOptions.mixins = [{
methods: {
getCmsContent: jest.fn().mockImplementation((_, label) => (label === 'Answers' ? answersFromCmsValue : ''))
}
}];
const wrapper = shallowMount(duplicateCheck, mountOptions);
// Assert
expect(wrapper.vm.getNewOrderSelectionName).toBe('');
});
test('answersFromCms empty list => returns empty string', () => {
// Arrange
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
});
const answersFromCmsValue = [];
mountOptions.mixins = [{
methods: {
getCmsContent: jest.fn().mockImplementation((_, label) => (label === 'Answers' ? answersFromCmsValue : ''))
}
}];
const wrapper = shallowMount(duplicateCheck, mountOptions);
// Assert
expect(wrapper.vm.getNewOrderSelectionName).toBe('');
});
test('answersFromCms non-empty list whose first item has no Name property => returns empty string', () => {
// Arrange
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
});
const answersFromCmsValue = [{ test: getRandomString(6, 6) }];
mountOptions.mixins = [{
methods: {
getCmsContent: jest.fn().mockImplementation((_, label) => (label === 'Answers'
? answersFromCmsValue
: ''))
}
}];
const wrapper = shallowMount(duplicateCheck, mountOptions);
// Assert
expect(wrapper.vm.getNewOrderSelectionName).toBe('');
});
test('answersFromCms first item has Name property => returns expected', () => {
// Arrange
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
});
const expectedName = getRandomString(6, 6);
const answersFromCmsValue = [{ Name: expectedName }];
mountOptions.mixins = [{
methods: {
getCmsContent: jest.fn().mockImplementation((_, label) => (label === 'Answers'
? answersFromCmsValue
: ''))
}
}];
const wrapper = shallowMount(duplicateCheck, mountOptions);
// Assert
expect(wrapper.vm.getNewOrderSelectionName).toBe(expectedName);
});
});
}); });
describe('Navigation', () => { describe('Navigation', () => {

View file

@ -23,11 +23,16 @@
class="duplicate-check-question" class="duplicate-check-question"
:cmsWidgetName="widget.existingOrNewQuestion" :cmsWidgetName="widget.existingOrNewQuestion"
:questionText="questionText" :questionText="questionText"
:answers="answers" :answers="duplicateOrders"
groupName="existingOrNewQuestionOption" groupName="existingOrNewQuestionOption"
buttonTypeString="listButton" buttonTypeString="listButton"
isRequired isRequired
:validationRules="rules.selectionRequired" /> :validationRules="rules.selectionRequired" />
<buttonMain
:variant="buttonVariants.primary"
buttonText="Start a new claim"
class="mt-5 w-100"
@clickEvent="startNewClaim" />
<siteFooter <siteFooter
ref="siteFooter" ref="siteFooter"
class="my-5" class="my-5"
@ -48,6 +53,8 @@ import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue'; import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue'; import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.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';
// Supporting files // Supporting files
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper.js'; import { fetchCmsContentForPage } from '@/helpers/cms-content-helper.js';
@ -66,7 +73,8 @@ export default {
buttonQuestion, buttonQuestion,
siteFooter, siteFooter,
// eslint-disable-next-line vue/no-reserved-component-names // eslint-disable-next-line vue/no-reserved-component-names
Form Form,
buttonMain
}, },
mixins: [BaseFormMixin], mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -91,23 +99,16 @@ export default {
}, },
rules: { rules: {
selectionRequired: globalRules.OPTION_REQUIRED selectionRequired: globalRules.OPTION_REQUIRED
} },
buttonVariants
}; };
}, },
computed: { computed: {
questionText() { questionText() {
return this.getCmsContent(this.widget.existingOrNewQuestion, 'QuestionText'); return this.getCmsContent(this.widget.existingOrNewQuestion, 'QuestionText');
}, },
answersFromCms() {
return (
this.getCmsContent(this.widget.existingOrNewQuestion, 'Answers') ?? []
);
},
getNewOrderSelectionName() {
return this.answersFromCms?.[0]?.Name ?? '';
},
duplicateOrders() { duplicateOrders() {
const duplicateOrderText = 'Finish Existing Claim'; const duplicateOrderText = 'Finish existing claim';
const orders = useMainStore().applicationUser.duplicateOrders; const orders = useMainStore().applicationUser.duplicateOrders;
return ( return (
orders?.map((o) => { orders?.map((o) => {
@ -127,9 +128,6 @@ export default {
}; };
}) ?? [] }) ?? []
); );
},
answers() {
return [...this.duplicateOrders, ...this.answersFromCms];
} }
}, },
methods: { methods: {
@ -197,6 +195,10 @@ export default {
this.$route this.$route
); );
} }
},
async startNewClaim() {
this.selectedAnswer = 'NewClaim';
await this.forwardButtonAction();
} }
} }
}; };
@ -216,21 +218,36 @@ export default {
.subheader-secondary { .subheader-secondary {
margin-top: map-get($spacers, 2); margin-top: map-get($spacers, 2);
} }
p {
span {
font-size: $h6-font-size;
}
}
} }
.duplicate-check-question { .duplicate-check-question {
.question-text { .question-text {
justify-content: left; justify-content: left;
display: inline-flex !important; display: inline-flex !important;
margin-top: map-get($spacers, 4); margin-top: map-get($spacers, 4);
margin-bottom: map-get($spacers, 2); margin-bottom: 0.625rem !important;
span {
font-weight: 600;
}
} }
.form-test-error { .form-test-error {
margin-top: 0 !important; margin-top: 0 !important;
span {
font-weight: 500;
}
}
.question-text.d-flex {
margin-top: 0;
} }
} }
.list-button-content {
span.small {
font-weight: 400;
}
}
.form-group {
margin-bottom: 1.25rem !important;
}
</style> </style>

View file

@ -1,10 +1,15 @@
// Components // Components
import { shallowMount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import entryPage from '@/layouts/entry-page/entry-page.vue'; 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 settleAllPromises from '@/helpers/layout-helper.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; 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. // Mock our module for promises.
jest.mock('@/helpers/layout-helper.js', () => jest.fn()); jest.mock('@/helpers/layout-helper.js', () => jest.fn());
@ -15,34 +20,192 @@ jest.mock('@/helpers/cms-content-helper', () => ({
setupModalLinks: jest.fn() setupModalLinks: jest.fn()
})); }));
/** @ignore */ function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRunAfterInitializingStore = () => {}) {
function setupMocks(queryString) {
const mountOptions = getMountOptions({ const mountOptions = getMountOptions({
router: { router: {
navigate: jest.fn() navigate: jest.fn()
}, }
route: { queryString }
}); });
const wrapper = shallowMount( const testingPinia = createTestingPinia({
entryPage, initialState: {
mountOptions main: mainInitialState
); }
});
useMainStore(testingPinia);
methodToRunAfterInitializingStore();
const apiResponses = {}; mountOptions.global.plugins = [testingPinia];
mountOptions.data = () => (initialData);
const apiResponses = { cmsContent: {} };
settleAllPromises.mockImplementation(() => apiResponses); 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 }; return { wrapper };
} }
describe('entry-page.vue', () => { describe('entry-page.vue', () => {
test('should render', () => { test('shows unauthorized message when not authorized', async () => {
const queryString = 'policynumber="123456"'; const wrapper = shallowMount(entryPage, getMountOptions());
const { wrapper } = setupMocks(queryString); 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); it('returns unauthorized if no clienttag', async () => {
expect(wrapper).toBeTruthy(); 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) { populateISSConfigValues(data) {
this.mainStore.issConfig.clientName = data.accountName; 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.clientDisplayName = data.accountName; // Defaults to use the client name.
this.mainStore.issConfig.parentAccountNumber = data.parentAccountNumber; this.mainStore.issConfig.parentAccountNumber = data.parentAccountNumber;
this.mainStore.issConfig.styleSheet = data.styleSheet; this.mainStore.issConfig.styleSheet = data.styleSheet;
@ -157,6 +158,10 @@ export default {
this.mainStore.issConfig.enableTPAFlow = true; this.mainStore.issConfig.enableTPAFlow = true;
} }
if (clientFlags.ClientFullName != null) {
this.mainStore.issConfig.clientFullName = clientFlags.ClientFullName;
}
if (clientFlags.ClientDisplayName != null) { if (clientFlags.ClientDisplayName != null) {
this.mainStore.issConfig.clientDisplayName = clientFlags.ClientDisplayName; this.mainStore.issConfig.clientDisplayName = clientFlags.ClientDisplayName;
} }

View file

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

View file

@ -21,7 +21,7 @@
cmsWidgetName="PolicyVehiclesQuestion" cmsWidgetName="PolicyVehiclesQuestion"
:vehicles="VehiclesForQuestions" :vehicles="VehiclesForQuestions"
:validationRules="rules.optionRequired" :validationRules="rules.optionRequired"
class="mb-2"/> class="mb-2" />
<buttonMain <buttonMain
:variant="buttonVariants.primary" :variant="buttonVariants.primary"
buttonText="Add another vehicle" buttonText="Add another vehicle"
@ -114,7 +114,7 @@ export default {
const vehicles = this.policyVehicles; const vehicles = this.policyVehicles;
const mappedData = const mappedData =
vehicles?.map((v) => { vehicles?.map((v) => {
const maskSymbol = 'X'; const maskSymbol = '*';
const vinStart = maskSymbol.repeat(v.vin.length - 6); const vinStart = maskSymbol.repeat(v.vin.length - 6);
const vinEnd = v.vin.substring(v.vin.length - 6); const vinEnd = v.vin.substring(v.vin.length - 6);
return { return {
@ -122,7 +122,7 @@ export default {
vehicle: v, vehicle: v,
Text: `${v.vehicleYear} ${v.vehicleMake} ${v.vehicleModel}`, Text: `${v.vehicleYear} ${v.vehicleMake} ${v.vehicleModel}`,
Name: v.vin, Name: v.vin,
SubText: `VIN ${vinStart}${vinEnd}` SubText: `VIN: ${vinStart}${vinEnd}`
}; };
}) ?? []; }) ?? [];
return mappedData; return mappedData;
@ -304,6 +304,10 @@ export default {
min-height: 1px; min-height: 1px;
padding-left: .9375rem; padding-left: .9375rem;
padding-right: .9375rem; padding-right: .9375rem;
.subheader-secondary p {
text-align: left;
}
} }
} }
@ -319,4 +323,10 @@ export default {
} }
} }
} }
.list-button-content {
span.small {
font-size: 1rem;
}
}
</style> </style>

View file

@ -43,41 +43,37 @@ function setupMocks(mockApiResponses) {
} }
describe('provider-preference.vue', () => { describe('provider-preference.vue', () => {
test('Should navigate to safelite flow when safelite selected', () => {
// Arrange
const { wrapper } = setupMocks();
// Act
wrapper.vm.selectedProvider = 'SafeliteOption';
wrapper.vm.forwardButtonAction();
// Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE, 'provider-preference');
});
test('Should navigate to tpa flow when TPAOption selected and TPA Flow Enabled', () => {
// Arrange
const { wrapper } = setupMocks();
// Act
wrapper.vm.selectedProvider = 'TPAOption';
useMainStore().issConfig.enableTPAFlow = true;
wrapper.vm.forwardButtonAction();
// Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED, 'provider-preference');
});
test('Should navigate to tpa disabled route page when TPAOption selected and TPA Flow disabled', () => { test('Should navigate to tpa disabled route page when TPAOption selected and TPA Flow disabled', () => {
// Arrange // Arrange
const { wrapper } = setupMocks(); const { wrapper } = setupMocks();
// Act // Act
wrapper.vm.selectedProvider = 'TPAOption';
useMainStore().issConfig.enableTPAFlow = false; useMainStore().issConfig.enableTPAFlow = false;
wrapper.vm.forwardButtonAction(); wrapper.vm.findAnotherShopClicked();
// Test // Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED, 'provider-preference'); expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED, 'provider-preference');
}); });
test('Should navigate to safelite flow when navigateWithTPARecalAnswer is called with SafeliteOption', () => {
// Arrange
const { wrapper } = setupMocks();
// Act
wrapper.vm.navigateWithTPARecalAnswer('SafeliteOption');
// Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE, 'provider-preference');
});
test('Should navigate to TPA flow when navigateWithTPARecalAnswer is called with TPAOption', () => {
// Arrange
const { wrapper } = setupMocks();
// Act
wrapper.vm.navigateWithTPARecalAnswer('TPAOption');
// Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED, 'provider-preference');
});
}); });

View file

@ -15,76 +15,70 @@
id="sub-header" id="sub-header"
cmsWidgetName="SiteSubHeader" cmsWidgetName="SiteSubHeader"
class="mb-0 mt-4 text-center" /> class="mb-0 mt-4 text-center" />
<buttonQuestion <div
ref="buttonQuestion" class="mt-4 mb-5"
v-model="selectedProvider" v-html="scheduleWithSafeliteText"></div>
class="provider-preference" <buttonMain
questionText="Select an option:" ref="buttonMain"
:answers="prefAnswers" class="full-width-button"
groupName="prefQuestions" variant="navigation"
buttonTypeString="providerPrefRadio" buttonText="Schedule now"
:validationRules="rules.optionRequired" @clickEvent="scheduleWithSafelite" />
isRequired /> <div
class="mt-5 mb-5"
v-html="scheduleWithOtherText"></div>
<textLink
class="underlined-text"
linkType="navigation"
text="Find another shop"
href="#"
@clickEvent="findAnotherShopClicked" />
<siteFooter <siteFooter
:ref="SITE_FOOTER_REF_NAME" :ref="SITE_FOOTER_REF_NAME"
class="mt-5" class="mt-6"
cmsWidgetName="SiteFooterWidget" cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="isForwardActionDisabled" :isForwardButtonHidden="true"
:isForwardButtonNavigationDisabled="isForwardNavigationDisabled" @backClicked="navigateBack" />
@backClicked="navigateBack"
@forwardClicked="forwardButtonAction" />
</div> </div>
</div> </div>
</div> </div>
<contentGroupModal
:ref="RECAL_MODAL_REF_NAME"
cssModalHeadlineClass="text-center"
cmsWidgetName="RecalModal" />
<steeringModal <steeringModal
:ref="STEERING_MODAL_REF_NAME" :ref="STEERING_MODAL_REF_NAME"
cmsWidgetName="StateSteeringModal" /> cmsWidgetName="StateSteeringModal" />
<shopPreferenceModal
:ref="SHOP_PREFERENCE_MODAL_REF_NAME"
cmsWidgetName="ShopPreferenceDrawer"
:showSteeringLink="showSteeringLink"
@openSteering="openStateSteeringModal" />
<tpaRecalModal <tpaRecalModal
:ref="TPA_RECAL_MODAL_REF_NAME" :ref="TPA_RECAL_MODAL_REF_NAME"
cmsWidgetName="TPARecalModal" cmsWidgetName="TPARecalModal"
buttonCmsWidgetName="TPARecalQuestion"
:ackError="ackError" :ackError="ackError"
@buttonClick="navigateWithTPAAck" /> :noSelectionError="noSelectionError"
@buttonClick="navigateWithTPARecalAnswer"/>
</Form> </Form>
</template> </template>
<script> <script>
// Import Supporting Files // Import Supporting Files
import { import {
fetchCmsContentForPage, fetchCmsContentForPage,
setupModalLink,
setupModalLinks setupModalLinks
} from '@/helpers/cms-content-helper'; } from '@/helpers/cms-content-helper';
import settleAllPromises from '@/helpers/layout-helper'; import settleAllPromises from '@/helpers/layout-helper';
import errorMessages from '@/constants/error-messages'; import errorMessages from '@/constants/error-messages';
import buttonQuestion from '@/digital-components/button-question/button-question.vue'; import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import issPageValues from '@/router/router-constants/issPage-values'; import showIssLoadingModal from '@/helpers/loading-modal-helper';
import bailoutMessage from '@/constants/bailoutMessage';
import baseFormMixin from '@/mixins/base-form-mixin';
import PROVIDER_PREFERENCE_OPTIONS from '@/constants/provider-preference';
// Import Component // Import Component
import baseFormMixin from '@/mixins/base-form-mixin';
import { Form } from 'vee-validate'; import { Form } from 'vee-validate';
import siteFooter from '@/iss-components/site-footer/site-footer.vue'; import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteHeader from '@/iss-components/site-header/site-header.vue'; import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue'; import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import steeringModal from '@/layouts/provider-preference/steering-modal/steering-modal.vue'; import steeringModal from '@/layouts/provider-preference/steering-modal/steering-modal.vue';
import shopPreferenceModal from '@/layouts/provider-preference/shop-preference-modal/shop-preference-modal.vue';
import tpaRecalModal from '@/layouts/provider-preference/tpa-recal-modal/tpa-recal-modal.vue'; import tpaRecalModal from '@/layouts/provider-preference/tpa-recal-modal/tpa-recal-modal.vue';
import globalRules from '@/constants/global-rules'; import textLink from '@/ux-components/text-link/text-link.vue';
import bailoutMessage from '@/constants/bailoutMessage'; import buttonMain from '@/ux-components/button-main/button-main.vue';
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
import showIssLoadingModal from '@/helpers/loading-modal-helper';
const options = { SAFELITE: 'SafeliteOption', TPA: 'TPAOption' };
const RECAL_MODAL_REF_NAME = 'RecalModal';
const STEERING_MODAL_REF_NAME = 'StateSteeringModal'; const STEERING_MODAL_REF_NAME = 'StateSteeringModal';
const SHOP_PREFERENCE_MODAL_REF_NAME = 'ShopPreferenceDrawer';
const TPA_RECAL_MODAL_REF_NAME = 'TPARecalModal'; const TPA_RECAL_MODAL_REF_NAME = 'TPARecalModal';
const SITE_FOOTER_REF_NAME = 'siteFooter'; const SITE_FOOTER_REF_NAME = 'siteFooter';
@ -97,10 +91,10 @@ export default {
// eslint-disable-next-line vue/no-reserved-component-names // eslint-disable-next-line vue/no-reserved-component-names
Form, Form,
buttonQuestion, buttonQuestion,
buttonMain,
steeringModal, steeringModal,
shopPreferenceModal,
tpaRecalModal, tpaRecalModal,
contentGroupModal textLink
}, },
mixins: [baseFormMixin], mixins: [baseFormMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -115,85 +109,37 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
// open steering modal if it has body text, the state is defined in the CMS content and doesn't
vm.showSteeringLink = !!vm.$refs[STEERING_MODAL_REF_NAME].ModalBodyText; // populate if the state is not listed in the CMS content
if (vm.showSteeringLink) { if (!!vm.$refs[STEERING_MODAL_REF_NAME].ModalBodyText) {
vm.openStateSteeringModal(); vm.openStateSteeringModal();
} }
}); });
}, },
data() { data() {
return { return {
selectedProvider: null, providerPreferenceOptions: PROVIDER_PREFERENCE_OPTIONS,
showSteeringLink: false,
tpaAcknowledgement: false,
rules: {
optionRequired: globalRules.OPTION_REQUIRED
},
RECAL_MODAL_REF_NAME,
STEERING_MODAL_REF_NAME, STEERING_MODAL_REF_NAME,
SHOP_PREFERENCE_MODAL_REF_NAME,
TPA_RECAL_MODAL_REF_NAME, TPA_RECAL_MODAL_REF_NAME,
SITE_FOOTER_REF_NAME SITE_FOOTER_REF_NAME
}; };
}, },
computed: { computed: {
isForwardActionDisabled() {
return this.selectedProvider === null;
},
isForwardNavigationDisabled() {
return this.selectedProvider === options.TPA && !this.tpaAcknowledgement;
},
prefAnswers() {
const cmsAnswersContent = [
{
cmsWidgetName: options.SAFELITE
},
{
cmsWidgetName: options.TPA
}
];
// if cms content has not yet loaded, skip
if (
!this.getCmsContent(
cmsAnswersContent[0].cmsWidgetName,
'HeaderText'
)
|| this.getCmsContent(
cmsAnswersContent[0].cmsWidgetName,
'HeaderText'
) === ''
) {
return {};
}
const modifiedAnswers = cmsAnswersContent.map((answer) => ({
value: answer.cmsWidgetName,
buttonLabel: this.getHeaderTextFromCms(answer.cmsWidgetName),
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.cmsWidgetName),
buttonBodyCopy: this.getBodyTextFromCms(answer.cmsWidgetName)
}));
return modifiedAnswers;
},
ackError() { ackError() {
return errorMessages.ACKNOWLEDGEMENT_REQUIRED; return errorMessages.ACKNOWLEDGEMENT_REQUIRED;
} },
}, noSelectionError() {
watch: { return errorMessages.NO_SELECTION_REQUIRED;
prefAnswers(newValue, oldValue) { },
if (newValue !== oldValue) { scheduleWithSafeliteText() {
setupModalLink(this, RECAL_MODAL_REF_NAME); return this.getCmsContent('ScheduleWithSafeliteText', 'BodyText');
} },
scheduleWithOtherText() {
return this.getCmsContent('ScheduleWithOtherText', 'BodyText');
} }
}, },
mounted() { mounted() {
setupModalLinks(this); setupModalLinks(this);
const pageData = this.mainStore.pageData(issPageValues.PROVIDER_PREFERENCE);
this.selectedProvider = pageData?.selectedProvider
? pageData.selectedProvider
: null;
this.tpaAcknowledgement = pageData?.tpaAcknowledgement
? pageData.tpaAcknowledgement
: false;
}, },
methods: { methods: {
getHeaderTextFromCms(cmsWidgetName) { getHeaderTextFromCms(cmsWidgetName) {
@ -213,50 +159,38 @@ export default {
return true; return true;
}, },
navigateForward(scenario) { navigateForward(scenario) {
showIssLoadingModal(true);
this.$router.navigate(scenario, this.$route); this.$router.navigate(scenario, this.$route);
}, },
navigateWithTPAAck() { navigateWithTPARecalAnswer(answer) {
this.mainStore.saveProviderPreferenceData({ if (answer === this.providerPreferenceOptions.TPA) {
selectedProvider: this.selectedProvider, this.scheduleWithTPA();
tpaAcknowledgement: this.tpaAcknowledgement } else {
}); this.scheduleWithSafelite();
showIssLoadingModal(true); }
this.navigateForward(this.navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED);
}, },
forwardButtonAction() { scheduleWithSafelite() {
if (this.selectedProvider) { this.mainStore.updateIsSafeliteProvider(true);
let scenario = null; const scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE;
switch (this.selectedProvider) { this.navigateForward(scenario);
case options.SAFELITE: },
this.mainStore.updateIsSafeliteProvider(true); scheduleWithTPA() {
scenario = this.mainStore.updateIsSafeliteProvider(false);
this.navigationScenarios const scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED;
.CLICKED_FORWARD_WITH_SAFELITE; this.navigateForward(scenario);
break; },
case options.TPA: findAnotherShopClicked() {
this.mainStore.updateIsSafeliteProvider(false); if (this.mainStore.issConfig.enableTPAFlow) {
if (this.mainStore.issConfig.enableTPAFlow) { if (this.mainStore.hasRecalibrationPart) {
if (this.mainStore.hasRecalibrationPart) { this.$refs[TPA_RECAL_MODAL_REF_NAME].openModal();
this.$refs[TPA_RECAL_MODAL_REF_NAME].openModal(); return;
return; } else {
} this.scheduleWithTPA();
scenario =
this.navigationScenarios
.CLICKED_FORWARD_WITH_TPA_ENABLED;
} else {
this.mainStore.setBailout(bailoutMessage.TPANotEnabled());
scenario =
this.navigationScenarios
.CLICKED_FORWARD_WITH_TPA_DISABLED;
}
break;
default:
} }
} else {
this.mainStore.setBailout(bailoutMessage.TPANotEnabled());
const scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED;
this.navigateForward(scenario); this.navigateForward(scenario);
this.mainStore.saveProviderPreferenceData({
selectedProvider: this.selectedProvider,
tpaAcknowledgement: this.tpaAcknowledgement
});
} }
}, },
openStateSteeringModal() { openStateSteeringModal() {
@ -279,12 +213,11 @@ export default {
#sub-header span { #sub-header span {
color: $black; color: $black;
} }
.question-text { .full-width-button {
margin-top: 0; width: 100%;
margin-bottom: 0.5rem; }
& > span { .underlined-text {
text-align: left; text-decoration: underline;
}
} }
:deep(.safeliteLogo) { :deep(.safeliteLogo) {
background-image: url(~@/assets/img/icons/logo.svg); background-image: url(~@/assets/img/icons/logo.svg);

View file

@ -1,59 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import settleAllPromises from '@/helpers/layout-helper.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import shopPreferenceModal from '@/layouts/provider-preference/shop-preference-modal/shop-preference-modal.vue';
// Mock our module for promises.
jest.mock('@/helpers/layout-helper.js', () => jest.fn());
// Mock fetchCmsContentForPage
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: jest.fn(),
setupModalLinks: jest.fn()
}));
/** @ignore */
function setupMocks(propsData) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
mountOptions.propsData = propsData;
const wrapper = shallowMount(
shopPreferenceModal,
mountOptions
);
const apiResponses = {};
settleAllPromises.mockImplementation(() => apiResponses);
fetchCmsContentForPage.mockImplementation(() => { });
return { wrapper };
}
describe('provider-preference.vue', () => {
test('Should display state specific steering modal link when in state with steering language', async () => {
// Arrange
const { wrapper } = setupMocks({ showSteeringLink: true });
// Act
// Test
expect(wrapper.vm.showSteeringLink).toBeTruthy();
});
test('Should not display state specific steering modal link when not in state with steering language', async () => {
// Arrange
const { wrapper } = setupMocks({ showSteeringLink: false });
// Act
// Test
expect(wrapper.vm.showSteeringLink).toBeFalsy();
});
});

View file

@ -1,111 +0,0 @@
<template>
<modal
:ref="ModalName"
:modalId="ModalName"
:onModalClosedCallback="onModalClosed"
:footerButtonText="ModalCloseButtonText"
@footerButtonEvent="footerButtonClick">
<h5 class="text-center mb-4">
{{ ModalHeadline }}
</h5>
<div>
<div
class="mb-4"
v-html="ModalBodyText"></div>
<a
v-if="showSteeringLink"
class="modal-text"
@click="openSteeringModal"
v-html="ModalSubBodyText">
</a>
</div>
</modal>
</template>
<script>
import modal from '@/digital-components/modal/modal.vue';
import states from '@/constants/states';
export default {
name: 'content-group-modal',
components: {
modal
},
props: {
cmsWidgetName: String,
showSteeringLink: Boolean
},
emits: ['openSteering'],
data() {
return {
clickedSteeringLink: false
};
},
computed: {
ModalName() {
return this.cmsWidgetName;
},
ModalHeadline() {
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
},
ModalSubheadertext() {
return this.getCmsContent(this.cmsWidgetName, 'SubheaderText');
},
ModalBodyText() {
return this.getCmsContent(this.cmsWidgetName, 'BodyText');
},
ModalSubBodyText() {
const header = this.getCmsContent(this.cmsWidgetName, 'BodyText2');
return header.replace('{custom:state}', states[this.mainStore.order.customer.address.state]);
},
ModalImage() {
return this.getCmsContent(this.cmsWidgetName, 'Image');
},
ModalCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, 'FooterText');
}
},
methods: {
openModal() {
this.$refs[this.ModalName].openModal();
},
onModalClosed() {
if (this.clickedSteeringLink) {
this.$emit('openSteering');
this.clickedSteeringLink = false;
}
},
footerButtonClick() {
this.$refs[this.ModalName].closeModal();
},
openSteeringModal() {
this.clickedSteeringLink = true;
this.$refs[this.ModalName].closeModal();
}
}
};
</script>
<style lang="scss" scoped>
.modal {
&.modal-component {
.modal-dialog {
.modal-content {
.modal-body {
.modal-sub-body {
color: $gray-600;
}
ul {
margin-bottom: 0;
}
p {
&:last-child {
margin-bottom: 0;
}
}
}
}
}
}
}
</style>

View file

@ -38,16 +38,26 @@ describe('tpa-Recal-Modal.vue', () => {
test('should show error if not acknowledged', () => { test('should show error if not acknowledged', () => {
const { wrapper } = setupMocks(); const { wrapper } = setupMocks();
wrapper.vm.acknowledged = false; wrapper.vm.acknowledged = false;
wrapper.vm.tpaRecalAnswer = 'TPAOption';
wrapper.vm.footerButtonClick(); wrapper.vm.footerButtonClick();
expect(wrapper.vm.showError).toBeTruthy(); expect(wrapper.vm.showAcknowledgementError).toBeTruthy();
}); });
test('should not show error if acknowledged', () => { test('should not show error if acknowledged', () => {
const { wrapper } = setupMocks(); const { wrapper } = setupMocks();
wrapper.vm.acknowledged = true; wrapper.vm.acknowledged = true;
wrapper.vm.tpaRecalAnswer = 'TPAOption';
wrapper.vm.footerButtonClick(); wrapper.vm.footerButtonClick();
expect(wrapper.vm.showError).toBeFalsy(); expect(wrapper.vm.showAcknowledgementError).toBeFalsy();
});
test('should show error if no answer selected', () => {
const { wrapper } = setupMocks();
wrapper.vm.tpaRecalAnswer = '';
wrapper.vm.tpaRecalAnswer = undefined;
wrapper.vm.footerButtonClick();
expect(wrapper.vm.showNoSelectionError).toBeTruthy();
}); });
}); });

View file

@ -16,11 +16,23 @@
<tpaRecalToggle <tpaRecalToggle
class="mb-5" class="mb-5"
cmsWidgetName="TPARecalModalToggle" /> cmsWidgetName="TPARecalModalToggle" />
<buttonQuestion
ref="tpaRecalQuestion"
v-model="tpaRecalAnswer"
class="radioQuestion safelite-or-tpa-question windshield-chip-count-question mb-4"
:cmsWidgetName="buttonCmsWidgetName"
:questionText="tpaRecalQuestionText"
:answers="tpaRecalAnswersFromCms"
groupName="tpaRecalQuestion"
buttonID="tpaRecalQuestion"
buttonTypeString="listButtonHorizontal"
isRequired></buttonQuestion>
<checkBox <checkBox
v-if="showAcknowledgementCheckbox"
ref="tpaAcknowledgement" ref="tpaAcknowledgement"
v-model="acknowledged" v-model="acknowledged"
class="mb-2" class="mb-2"
:class="showError && ' has-error'" :class="showAcknowledgementError && ' has-error'"
:validationRules="rules.optionRequired" :validationRules="rules.optionRequired"
checkboxName="tpaAcknowledgement" checkboxName="tpaAcknowledgement"
buttonID="tpaAcknowledgement" buttonID="tpaAcknowledgement"
@ -29,9 +41,9 @@
:screenReaderOnlyText="ModalSubBodyText" :screenReaderOnlyText="ModalSubBodyText"
isRequired /> isRequired />
<div <div
v-if="showError" v-if="showAcknowledgementError || showNoSelectionError"
class="row form-test-error mt-1"> class="row form-test-error mt-1">
<p>{{ ackError }}</p> <p>{{ errorMessage }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -42,24 +54,31 @@
import modal from '@/digital-components/modal/modal.vue'; import modal from '@/digital-components/modal/modal.vue';
import tpaRecalToggle from '@/layouts/provider-preference/tpa-recal-modal/tpa-recal-toggle/tpa-recal-toggle.vue'; import tpaRecalToggle from '@/layouts/provider-preference/tpa-recal-modal/tpa-recal-toggle/tpa-recal-toggle.vue';
import checkBox from '@/ux-components/checkbox/checkbox.vue'; import checkBox from '@/ux-components/checkbox/checkbox.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import globalRules from '@/constants/global-rules'; import globalRules from '@/constants/global-rules';
import PROVIDER_PREFERENCE_OPTIONS from '@/constants/provider-preference';
export default { export default {
name: 'content-group-modal', name: 'content-group-modal',
components: { components: {
modal, modal,
tpaRecalToggle, tpaRecalToggle,
checkBox checkBox,
buttonQuestion
}, },
props: { props: {
cmsWidgetName: String, cmsWidgetName: String,
ackError: String buttonCmsWidgetName: String,
ackError: String,
noSelectionError: String
}, },
emits: ['buttonClick'], emits: ['buttonClick'],
data() { data() {
return { return {
acknowledged: false, acknowledged: false,
showError: false, showAcknowledgementError: false,
showNoSelectionError: false,
tpaRecalAnswer: '',
rules: { rules: {
optionRequired: globalRules.OPTION_REQUIRED optionRequired: globalRules.OPTION_REQUIRED
} }
@ -81,13 +100,34 @@ export default {
ModalCloseButtonText() { ModalCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, 'FooterText'); return this.getCmsContent(this.cmsWidgetName, 'FooterText');
}, },
tpaRecalQuestionText() {
return this.getCmsContent(this.buttonCmsWidgetName, 'QuestionText');
},
tpaRecalAnswersFromCms() {
return this.getCmsContent(this.buttonCmsWidgetName, 'Answers');
},
showAcknowledgementCheckbox() {
return this.tpaRecalAnswer === PROVIDER_PREFERENCE_OPTIONS.TPA;
},
isButtonDisabled() { isButtonDisabled() {
return !this.acknowledged; return !this.acknowledged;
},
errorMessage() {
if (this.showNoSelectionError) {
return this.noSelectionError;
} else if (this.showAcknowledgementError) {
return this.ackError;
}
return '';
} }
}, },
watch: { watch: {
acknowledged() { acknowledged() {
this.showError = false; this.showAcknowledgementError = false;
},
tpaRecalAnswer() {
this.showNoSelectionError = false;
this.showAcknowledgementError = false;
} }
}, },
methods: { methods: {
@ -96,12 +136,15 @@ export default {
}, },
footerButtonClick() { footerButtonClick() {
// check if acked, if not show error if (this.tpaRecalAnswer && (!this.showAcknowledgementCheckbox || this.acknowledged )) {
if (this.acknowledged) {
this.$refs[this.ModalName]?.closeModal(); this.$refs[this.ModalName]?.closeModal();
this.$emit('buttonClick'); this.$emit('buttonClick', this.tpaRecalAnswer);
} else if (!this.tpaRecalAnswer) {
this.showNoSelectionError = true;
this.showAcknowledgementError = false;
} else { } else {
this.showError = true; this.showAcknowledgementError = true;
this.showNoSelectionError = false;
} }
} }
} }
@ -130,29 +173,16 @@ export default {
} }
} }
} }
} .safelite-or-tpa-question {
:deep(.question-text) {
.form-check { span {
.form-check-input { font-weight: 500;
&:checked {
+ label {
p {
font-weight: 400 !important;
font-size: 1rem !important;
color: $gray-600 !important;
line-height: 1.5rem;
}
} }
} }
} }
p {
font-weight: 400 !important;
font-size: 1rem !important;
color: $gray-600 !important;
line-height: 1.5rem;
}
} }
.form-test-error { .form-test-error {
p { p {
font-weight: 500; font-weight: 500;

View file

@ -6,7 +6,6 @@ import { createTestingPinia } from '@pinia/testing';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js'; import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { useMainStore } from '@/store/index.js'; import { useMainStore } from '@/store/index.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
// Mock fetchCmsContentForPage // Mock fetchCmsContentForPage
jest.mock('@/helpers/cms-content-helper', () => ({ jest.mock('@/helpers/cms-content-helper', () => ({
@ -214,7 +213,7 @@ describe('schedule-page.vue', () => {
// Act // Act
const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod( const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod(
'2023-01-01', '2023-01-01',
'2023-01-31' '2023-01-15'
); );
// Assert // Assert
@ -236,7 +235,7 @@ describe('schedule-page.vue', () => {
estimatedServiceMinutesMaximum: 120 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 // Arrange
const { wrapper } = getShallowMountedComponent(); const { wrapper } = getShallowMountedComponent();
wrapper.vm.selectableDatesData = { wrapper.vm.selectableDatesData = {
@ -264,7 +263,7 @@ describe('schedule-page.vue', () => {
// 2023-01-01 --> 2023-02-05 // 2023-01-01 --> 2023-02-05
// 2023-02-06 --> 2023-03-12 // 2023-02-06 --> 2023-03-12
// 2023-03-13 --> 2023-03-31 // 2023-03-13 --> 2023-03-31
expect(store.getShopTimeSlots).toHaveBeenCalledTimes(3); expect(store.getShopTimeSlots).toHaveBeenCalledTimes(6);
}); });
}); });
describe('Rendering', () => { describe('Rendering', () => {
@ -290,32 +289,16 @@ describe('schedule-page.vue', () => {
// Assert // Assert
expect(testValue).toStrictEqual('01234'); 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 () => { test('forwardButtonAction should call route method navigateWithoutSaving', async () => {
// Arrange // Arrange
const { wrapper } = getShallowMountedComponent(); const { wrapper } = getShallowMountedComponent();
wrapper.vm.$router.navigate = jest.fn(() => ({})); wrapper.vm.$router.navigate = jest.fn(() => ({}));
wrapper.vm.selectedTimeSlotInfo = {
timeSlot: {
routeCode: 'test-id'
}
};
// Act // Act
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();

View file

@ -1,7 +1,6 @@
<template> <template>
<Form <Form
ref="theForm" ref="theForm"
v-slot="{ meta }"
@submit="onSubmit" @submit="onSubmit"
@invalidSubmit="onInvalidSubmit"> @invalidSubmit="onInvalidSubmit">
<div class="fade-on-route-transition"> <div class="fade-on-route-transition">
@ -16,56 +15,27 @@
cmsWidgetName="ScheduleSubHeaderWidget" cmsWidgetName="ScheduleSubHeaderWidget"
secondaryTextClasses="text-center small sub-text" secondaryTextClasses="text-center small sub-text"
class="mt-4" /> 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"> <div class="main-content-container">
<locationAlerts <locationAlerts
ref="locationAlerts" ref="locationAlerts"
cmsWidgetPrefix="LocationAlert-" /> cmsWidgetPrefix="LocationAlert-" />
<datePicker <datePicker
ref="datePicker" ref="datePicker"
v-model="selectedDate" v-model="selectedTimeSlotInfo"
customComponentId="dateQuestion" customComponentId="dateQuestion"
selectableDatesSetting="custom" selectableDatesSetting="custom"
class="text-link-small" class="text-link-small"
:showTimeSlotError="showDatePickerError"
:customSelectableDatesCallback=" :customSelectableDatesCallback="
getAvailableDatesMethod getAvailableDatesMethod
" "
validationRules="date-required" @dateSelected="dateSelectedFromPicker"
@dateClicked="openInshopTimeSlotsModal" /> @timeSlotSelected="timeSlotSelectedFromPicker" />
<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" />
<siteFooter <siteFooter
ref="navbar" ref="navbar"
class="mt-5" class="mt-5"
cmsWidgetName="SiteFooterWidget" cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid" :isForwardButtonNavigationDisabled="!isFormValid"
@backClicked="navigateBack" @backClicked="navigateBack"
@forwardClicked="forwardButtonAction" /> @forwardClicked="forwardButtonAction" />
</div> </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 siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import locationAlerts from '@/layouts/schedule-page/location-alerts/location-alerts.vue'; import locationAlerts from '@/layouts/schedule-page/location-alerts/location-alerts.vue';
import datePicker from '@/digital-components/date-picker/date-picker.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 siteFooter from '@/iss-components/site-footer/site-footer.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
// Supporting files // Supporting files
import { import {
@ -97,27 +65,15 @@ import {
} from '@/helpers/cms-content-helper'; } from '@/helpers/cms-content-helper';
import { import {
calcDaysBetweenDates, calcDaysBetweenDates,
convertDateStringToDate,
sumDateString sumDateString
} from '@/helpers/date-helper'; } from '@/helpers/date-helper';
import settleAllPromises from '@/helpers/layout-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 BaseFormMixin from '@/mixins/base-form-mixin.js';
import errorMessages from '@/constants/error-messages';
import { required } from '@/helpers/validation-rules';
import { useMainStore } from '@/store'; 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 // 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 ( const getAvailableDates = async (
startDateString, startDateString,
@ -127,13 +83,11 @@ const getAvailableDates = async (
) => { ) => {
const apiEndDateLimit = sumDateString( const apiEndDateLimit = sumDateString(
startDateString, startDateString,
TIME_SLOTS_CALL_DAYS_LIMIT TIME_SLOTS_CALL_DAYS_LIMIT - 1
); );
const difference = calcDaysBetweenDates(startDateString, endDateString); const difference = calcDaysBetweenDates(startDateString, endDateString);
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT); const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
const storeActionConfigs = []; const storeActionConfigs = [];
const timeSlotsData = {};
timeSlotsData.days = [];
let apiStartDate = startDateString; let apiStartDate = startDateString;
let apiEndDate = endDateString; let apiEndDate = endDateString;
@ -144,7 +98,7 @@ const getAvailableDates = async (
apiStartDate = sumDateString(apiEndDate, 1); apiStartDate = sumDateString(apiEndDate, 1);
apiEndDate = sumDateString( apiEndDate = sumDateString(
apiStartDate, apiStartDate,
TIME_SLOTS_CALL_DAYS_LIMIT TIME_SLOTS_CALL_DAYS_LIMIT - 1
); );
if (i === apiCallsCount) { if (i === apiCallsCount) {
@ -154,11 +108,7 @@ const getAvailableDates = async (
apiEndDate = apiEndDateLimit; apiEndDate = apiEndDateLimit;
} }
if ( if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
appointmentType === AppointmentTypeStrings.MOBILE
|| appointmentType
=== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
) {
storeActionConfig = { storeActionConfig = {
storeAction: GET_MOBILE_TIME_SLOTS, storeAction: GET_MOBILE_TIME_SLOTS,
payload: { payload: {
@ -232,9 +182,7 @@ export default {
siteSubHeader, siteSubHeader,
locationAlerts, locationAlerts,
datePicker, datePicker,
timeSlotModalQuestion,
siteFooter, siteFooter,
textBlock,
// eslint-disable-next-line vue/no-reserved-component-names // eslint-disable-next-line vue/no-reserved-component-names
Form Form
}, },
@ -300,7 +248,6 @@ export default {
resultMap.datePickerInitialData.initialShopTimeSlotsResponse, resultMap.datePickerInitialData.initialShopTimeSlotsResponse,
resultMap.premiumFeeWithPrice resultMap.premiumFeeWithPrice
); );
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
}); });
}, },
setup() { setup() {
@ -312,6 +259,7 @@ export default {
selectedDate: this.getSelectedDate(), selectedDate: this.getSelectedDate(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(), selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
selectableDatesData: [], selectableDatesData: [],
showDatePickerError: false,
mobilePremiumAppointmentFee: null mobilePremiumAppointmentFee: null
}; };
}, },
@ -325,38 +273,14 @@ export default {
appointmentType() { appointmentType() {
return useMainStore().order.serviceLocation.appointmentType; return useMainStore().order.serviceLocation.appointmentType;
}, },
timeSlotsForSelectedDate() { isFormValid() {
if (!this.selectedDate) { const hasTimeSlotSelected = this.selectedTimeSlotInfo?.timeSlot?.routeCode != null;
return null; return hasTimeSlotSelected;
}
return this.selectableDatesData.days?.find((selectableDate) => selectableDate.date === this.selectedDate);
}, },
supportingItems() { supportingItems() {
return useMainStore().lineItems.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: { methods: {
splitCopyOnCMSPlaceHolder, splitCopyOnCMSPlaceHolder,
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
@ -400,9 +324,6 @@ export default {
getServiceZipCtuCodeFromStore() { getServiceZipCtuCodeFromStore() {
return this.mainStore.order.serviceLocation.zipCodeCtu; return this.mainStore.order.serviceLocation.zipCodeCtu;
}, },
openInshopTimeSlotsModal() {
this.$refs.timeSlotModalQuestion.openModal();
},
getSelectedDate() { getSelectedDate() {
return this.mainStore.order.schedule.date; return this.mainStore.order.schedule.date;
}, },
@ -420,64 +341,21 @@ export default {
return selectedTimeSlotInfo; return selectedTimeSlotInfo;
}, },
timeSlotModalClosed() { dateSelectedFromPicker(date) {
// Clear the selectedDate if no timeSlot has been selected this.selectedDate = date;
if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) { this.showDatePickerError = false;
this.selectedDate = null;
}
}, },
updateFooterButtonText(timeSlotInfo) { timeSlotSelectedFromPicker(timeSlot) {
let navbarButtonText; this.selectedTimeSlotInfo = timeSlot;
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) { this.showDatePickerError = false;
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}`;
}, },
forwardButtonAction() { forwardButtonAction() {
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot); if (!this.isFormValid) {
this.showDatePickerError = true;
return;
}
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD, this.navigationScenarios.CLICKED_FORWARD,
this.$route this.$route

View file

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

View file

@ -12,7 +12,7 @@
<div class="welcome-page-container iss-heritage-content-container-width"> <div class="welcome-page-container iss-heritage-content-container-width">
<siteSubHeader <siteSubHeader
cmsWidgetName="SiteSubHeaderWidget" cmsWidgetName="SiteSubHeaderWidget"
class="mt-4" /> class="form-group" />
<textboxQuestion <textboxQuestion
ref="policyNumber" ref="policyNumber"
v-model="welcomePageModel.policyNumber" v-model="welcomePageModel.policyNumber"
@ -22,16 +22,6 @@
disableAutoFill disableAutoFill
:isDisabled="isPolicyHolderDisabled" :isDisabled="isPolicyHolderDisabled"
:validationRules="rules.policyNumber" /> :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 <textboxQuestion
ref="phoneNumber" ref="phoneNumber"
v-model="welcomePageModel.phoneNumber" v-model="welcomePageModel.phoneNumber"
@ -41,7 +31,8 @@
isRequired isRequired
:mask="phoneMask" :mask="phoneMask"
disableAutoFill disableAutoFill
class="mt-3" /> placeholderText="###-###-####"
class="form-group" />
<textboxQuestion <textboxQuestion
ref="extension" ref="extension"
v-model="welcomePageModel.extension" v-model="welcomePageModel.extension"
@ -49,7 +40,7 @@
cmsWidgetName="ExtensionQuestion" cmsWidgetName="ExtensionQuestion"
:validationRules="rules.extension" :validationRules="rules.extension"
disableAutoFill disableAutoFill
class="mt-3" /> class="form-group" />
<textboxQuestion <textboxQuestion
ref="dateOfLoss" ref="dateOfLoss"
v-model="welcomePageModel.dateOfLoss" v-model="welcomePageModel.dateOfLoss"
@ -62,7 +53,7 @@
:max="new Date().toJSON().slice(0, 10)" :max="new Date().toJSON().slice(0, 10)"
:min="'1972-12-01'" :min="'1972-12-01'"
:validationRules="rules.lossDate" :validationRules="rules.lossDate"
class="mt-3" /> class="form-group" />
<textBlock <textBlock
cmsWidgetName="DamageDateEstimateWidget" cmsWidgetName="DamageDateEstimateWidget"
typeStyle="small" typeStyle="small"
@ -77,44 +68,45 @@
disableAutoFill disableAutoFill
:validationRules="rules.damageOption" :validationRules="rules.damageOption"
placeHolderText="Select an option" 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 <dropdownQuestion
v-if="displayDamageStateQuestion" v-if="displayDamageStateQuestion"
id="welcomeDropdown" id="welcomeDropdown"
ref="state" ref="state"
v-model="welcomePageModel.damageState" v-model="welcomePageModel.damageState"
class="mt-3" class="form-group"
cmsWidgetName="DamageStateQuestion" cmsWidgetName="DamageStateQuestion"
inputId="8fdf9dc2e13e430eb57529499dceb3eb" inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="getStates" :options="getStates"
:validationRules="rules.lossState" :validationRules="rules.lossState"
isRequired isRequired
disableAutoFill disableAutoFill
placeHolderText="Select an option" /> placeHolderText="Select State" />
<textboxQuestion <textboxQuestion
v-if="displayDamageCityQuestion" v-if="displayDamageCityQuestion"
ref="damageCity" ref="damageCity"
v-model="welcomePageModel.damageCity" v-model="welcomePageModel.damageCity"
class="mt-3" class="form-group"
inputId="damageCityField" inputId="damageCityField"
cmsWidgetName="DamageCityQuestion" cmsWidgetName="DamageCityQuestion"
isRequired isRequired
disableAutoFill disableAutoFill
:validationRules="rules.lossCity" /> :validationRules="rules.lossCity" />
<textboxQuestion
ref="email"
v-model="welcomePageModel.email"
inputId="emailField"
cmsWidgetName="EmailAddressQuestion"
:validationRules="rules.email"
isRequired
disableAutoFill
class="mt-3" />
<buttonQuestion <buttonQuestion
v-if="displayGlassOnlyQuestion" v-if="displayGlassOnlyQuestion"
ref="glassOnlyDamage" ref="glassOnlyDamage"
v-model="welcomePageModel.isDamageGlassOnly" v-model="welcomePageModel.isDamageGlassOnly"
class="px-0 mt-3" class="px-0 form-group"
cmsWidgetName="GlassOnlyQuestion" cmsWidgetName="GlassOnlyQuestion"
inputId="isDamageGlassOnly" inputId="isDamageGlassOnly"
:answers="DamageGlassOnlyOptions" :answers="DamageGlassOnlyOptions"
@ -142,17 +134,9 @@
:isDismissible="false" /> :isDismissible="false" />
<siteFooter <siteFooter
ref="siteFooter" ref="siteFooter"
class="mt-3"
cmsWidgetName="SiteFooterWidget" cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction" />
<textBlock
id="requestCallbackLink"
cmsWidgetName="HelpTextWidget"
linkType="navigation"
href="javascript:void(0)"
class="mb-5 text-left"
@clickEvent="handleHelpLinkClick" />
</div> </div>
</div> </div>
</div> </div>
@ -257,7 +241,6 @@ export default {
duplicates: [], duplicates: [],
rules: { rules: {
damageOption: 'damage-option-required', damageOption: 'damage-option-required',
email: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`,
extension: `${globalRules.EXTENSION_FORMAT}`, extension: `${globalRules.EXTENSION_FORMAT}`,
lossCity: `${globalRules.DATE_OF_LOSS_CITY_REQUIRED}|${globalRules.DATE_OF_LOSS_CITY_FORMAT}`, lossCity: `${globalRules.DATE_OF_LOSS_CITY_REQUIRED}|${globalRules.DATE_OF_LOSS_CITY_FORMAT}`,
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@ -302,7 +285,11 @@ export default {
return !!this.getCmsContent('GlassOnlyQuestion', 'QuestionText'); return !!this.getCmsContent('GlassOnlyQuestion', 'QuestionText');
}, },
getStates() { 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() { isPolicyHolderDisabled() {
return !!this.mainStore.issConfig.disabledFields.policyNumber; return !!this.mainStore.issConfig.disabledFields.policyNumber;
@ -408,7 +395,6 @@ export default {
isDamageGlassOnly: this.mainStore.order.policy.isDamageGlassOnly, isDamageGlassOnly: this.mainStore.order.policy.isDamageGlassOnly,
phoneNumber: this.mainStore.order.contactInfo.homePhone, phoneNumber: this.mainStore.order.contactInfo.homePhone,
extension: this.mainStore.order.contactInfo.extension, extension: this.mainStore.order.contactInfo.extension,
email: this.mainStore.order.customer.emailAddress,
isPolicyNumberDisabled: this.mainStore.order.policy.isPolicyNumberDisabled isPolicyNumberDisabled: this.mainStore.order.policy.isPolicyNumberDisabled
}; };
}, },
@ -425,7 +411,6 @@ export default {
this.welcomePageModel.isDamageGlassOnly = response.policy.isDamageGlassOnly; this.welcomePageModel.isDamageGlassOnly = response.policy.isDamageGlassOnly;
this.welcomePageModel.phoneNumber = response.customer.homePhone; this.welcomePageModel.phoneNumber = response.customer.homePhone;
this.welcomePageModel.extension = response.customer.extension; this.welcomePageModel.extension = response.customer.extension;
this.welcomePageModel.email = response.customer.emailAddress;
}, },
findDamageCause(damageCause) { findDamageCause(damageCause) {
const damageCauseOptions = this.DamageCauseOptions; const damageCauseOptions = this.DamageCauseOptions;
@ -481,13 +466,6 @@ export default {
this.mainStore.order.loadedFromCookie = true; this.mainStore.order.loadedFromCookie = true;
this.answeredContinueModal = true; this.answeredContinueModal = true;
this.$refs.continueModal.closeModal(); this.$refs.continueModal.closeModal();
},
handleHelpLinkClick() {
useMainStore().setBailout(bailoutMessage.RequestCallback());
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
} }
} }
}; };
@ -508,6 +486,10 @@ form {
min-height: 1px; min-height: 1px;
padding-left: .9375rem; padding-left: .9375rem;
padding-right: .9375rem; padding-right: .9375rem;
.form-group {
margin-top: 1.25rem;
}
} }
} }

View file

@ -7,7 +7,7 @@ import bailoutMessage from '@/constants/bailoutMessage';
import coverageStatuses from '@/constants/coverage-statuses'; import coverageStatuses from '@/constants/coverage-statuses';
import coverageType from '@/constants/coverage-type'; import coverageType from '@/constants/coverage-type';
import damageLocationsSelected from '@/constants/damage-locations-selected'; import damageLocationsSelected from '@/constants/damage-locations-selected';
import { endpoints } from '@/constants/endpoints'; import endpoints from '@/constants/endpoints';
import { experimentSettings, experimentTriggers, experimentUniverses } from '@/constants/experiments'; import { experimentSettings, experimentTriggers, experimentUniverses } from '@/constants/experiments';
import partNumberStrings from '@/constants/part-number-strings'; import partNumberStrings from '@/constants/part-number-strings';
import partTypeStrings from '@/constants/part-type-strings'; import partTypeStrings from '@/constants/part-type-strings';
@ -243,6 +243,7 @@ export const getDefaultState = () => ({
}, },
issConfig: { issConfig: {
clientName: 'Generic Insurance', // this is the default and will be overriden by the client's name 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. clientDisplayName: 'Generic Insurance', // this is the default and will be overridden by the client's name or client display name.
clientHeader: {}, clientHeader: {},
styleSheet: '', // Stylesheet used by the client. styleSheet: '', // Stylesheet used by the client.
@ -1351,10 +1352,11 @@ export const useMainStore = defineStore({
async setVehicle() { async setVehicle() {
const encodedMake = encodeURIComponent(this.order.vehicle.make); const encodedMake = encodeURIComponent(this.order.vehicle.make);
const encodedModel = encodeURIComponent(this.order.vehicle.model); const encodedModel = encodeURIComponent(this.order.vehicle.model);
const encodedStyle = encodeURIComponent(this.order.vehicle.style);
const response = await globalMethods const response = await globalMethods
.callHttpClient({ .callHttpClient({
method: endpoints.GetVehicle.method, method: endpoints.GetVehicle.method,
endpoint: `${endpoints.GetVehicle.url}/${this.order.vehicle.year}/${encodedMake}/${encodedModel}/${this.order.vehicle.style}`, endpoint: `${endpoints.GetVehicle.url}/${this.order.vehicle.year}/${encodedMake}/${encodedModel}/${encodedStyle}`,
payload: {} payload: {}
}); });
this.updateVehicle(response.data); this.updateVehicle(response.data);
@ -2154,6 +2156,7 @@ export const useMainStore = defineStore({
resetISSConfigState() { resetISSConfigState() {
this.issConfig.clientName = 'Generic Insurance'; this.issConfig.clientName = 'Generic Insurance';
this.issConfig.clientFullName = 'Generic Insurance';
this.issConfig.clientDisplayName = 'Generic Insurance'; this.issConfig.clientDisplayName = 'Generic Insurance';
this.issConfig.clientHeader = {}; this.issConfig.clientHeader = {};
this.issConfig.parentAccountNumber = 0; this.issConfig.parentAccountNumber = 0;
@ -2273,7 +2276,6 @@ export const useMainStore = defineStore({
this.order.policy.damageState = welcomePageModel?.damageState; this.order.policy.damageState = welcomePageModel?.damageState;
this.order.policy.damageCity = welcomePageModel?.damageCity; this.order.policy.damageCity = welcomePageModel?.damageCity;
this.order.policy.isDamageGlassOnly = welcomePageModel?.isDamageGlassOnly; this.order.policy.isDamageGlassOnly = welcomePageModel?.isDamageGlassOnly;
this.order.customer.emailAddress = welcomePageModel?.email;
this.order.serviceLocation.zipCode = welcomePageModel?.policyZipCode; this.order.serviceLocation.zipCode = welcomePageModel?.policyZipCode;
this.updatePhoneNumbers({ this.updatePhoneNumbers({
home: welcomePageModel?.phoneNumber, home: welcomePageModel?.phoneNumber,
@ -2363,38 +2365,27 @@ export const useMainStore = defineStore({
const serviceLocationCity = serviceLocation.city; const serviceLocationCity = serviceLocation.city;
const serviceLocationState = serviceLocation.state; const serviceLocationState = serviceLocation.state;
const serviceLocationZipCode = serviceLocation.zipCode; const serviceLocationZipCode = serviceLocation.zipCode;
const lineItemsQueryString = getTaxLineItemQueryString(pricedLineItems, 'lineItems');
let queryString = '';
if (appointmentType === AppointmentTypeStrings.MOBILE
|| appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
queryString =
`ParentAccountNumber=${this.order.parentAccountNumber}`
+ `&BillToAccountNumber=${this.billToAccountNumber}`
+ `&ProviderNumber=${this.providerNumber}`
+ `&AppointmentType=${appointmentType}`
+ `&ServiceLocation.City=${serviceLocationCity}`
+ `&ServiceLocation.State=${serviceLocationState}`
+ `&ServiceLocation.ZipCode=${serviceLocationZipCode}`
+ `${lineItemsQueryString}`;
} else {
queryString =
`ParentAccountNumber=${this.order.parentAccountNumber}`
+ `&BillToAccountNumber=${this.billToAccountNumber}`
+ `&ProviderNumber=${this.providerNumber}`
+ `&AppointmentType=${appointmentType}`
+ `${lineItemsQueryString}`;
}
const lineItemServerData = this.order.lineItems.serverData; const lineItemServerData = this.order.lineItems.serverData;
if (lineItemServerData) {
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`; const isMobileApt = appointmentType === AppointmentTypeStrings.MOBILE
} || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const retPricedLineItems = await globalMethods.callHttpClient({ const retPricedLineItems = await globalMethods.callHttpClient({
method: endpoints.TaxOrderItems.method, method: endpoints.TaxOrderItems.method,
endpoint: `${endpoints.TaxOrderItems.url}?${queryString}` 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 || ''
}
}).then((response) => { }).then((response) => {
this.order.lineItems.serverData = response.data.serverData; this.order.lineItems.serverData = response.data.serverData;
return addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems); return addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems);
@ -2402,9 +2393,6 @@ export const useMainStore = defineStore({
return retPricedLineItems; return retPricedLineItems;
}, },
saveProviderPreferenceData(data) {
this.updatePageData({ page: issPageValues.PROVIDER_PREFERENCE, data });
},
addEventToBus(event) { addEventToBus(event) {
this.applicationUser.eventBus.push(event); this.applicationUser.eventBus.push(event);
}, },

View file

@ -10,7 +10,7 @@ import {
} from '@/helpers/data-generation.js'; } from '@/helpers/data-generation.js';
import coverageStatuses from '@/constants/coverage-statuses.js'; import coverageStatuses from '@/constants/coverage-statuses.js';
import { paymentMethods } from '@/constants/payment-method-constants'; import { paymentMethods } from '@/constants/payment-method-constants';
import { endpoints } from '@/constants/endpoints'; import endpoints from '@/constants/endpoints';
import { AppointmentTypeStrings } from '@/constants/schedule-constants'; import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import bailoutCode from '@/constants/bailoutCode'; import bailoutCode from '@/constants/bailoutCode';
import coverageType from '@/constants/coverage-type'; import coverageType from '@/constants/coverage-type';

View file

@ -174,6 +174,7 @@ body {
div.textbox-question { div.textbox-question {
label { label {
span.sub-caption { span.sub-caption {
color: #525656;
font-weight: 400; font-weight: 400;
} }
} }
@ -203,10 +204,10 @@ $heritage-btn-width: 9.0625rem; // 145px
@include heritage-btn-variant('secondary', $white, $heritage-blue-secondary, transparent, true); @include heritage-btn-variant('secondary', $white, $heritage-blue-secondary, transparent, true);
@include heritage-btn-link('link', $blue, transparent, $heritage-blue-secondary); @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('md', 7px, 15px);
@include heritage-btn-size('sm', 3px, 6px); @include heritage-btn-size('sm', 3px, 6px);
@include heritage-btn-size('xs', 1px, 5px); @include heritage-btn-size('xs', 1px, 5px);
&.btn-link { &.btn-link {
--bs-btn-padding-x: 0px; --bs-btn-padding-x: 0px;

View file

@ -8,6 +8,10 @@ html {
&.list-button, &.list-button,
&.list-button.list-group, &.list-button.list-group,
&.list-card &.option-label { &.list-card &.option-label {
border: 1px solid $red;
position: relative;
z-index: 4;
border-radius: 60px;
.button-content { .button-content {
border: 1px solid $red; border: 1px solid $red;
@ -25,6 +29,7 @@ html {
&.list-card { &.list-card {
border: 1px solid $red; border: 1px solid $red;
border-radius: 0.5rem;
} }
&.ui-radio { &.ui-radio {
@ -45,7 +50,23 @@ html {
// END HOVER // 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 { &.list-card {
color: $red; color: $red;
@ -138,6 +159,7 @@ html {
border: 1px solid $success; border: 1px solid $success;
position: relative; position: relative;
z-index: 4; z-index: 4;
border-radius: 60px;
.button-content { .button-content {
border: none; border: none;
@ -155,6 +177,7 @@ html {
&.list-card { &.list-card {
border: 1px solid $success; border: 1px solid $success;
border-radius: 0.5rem;
} }
&.ui-radio { &.ui-radio {
@ -175,7 +198,23 @@ html {
// END HOVER // 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 { &.list-card {
color: $success; color: $success;

View file

@ -20,7 +20,7 @@ $red-100: #ffe6e4;
$red-200: #fcbfbb; $red-200: #fcbfbb;
$red-300: #f89892; $red-300: #f89892;
$red-400: #e65c53; $red-400: #e65c53;
$red: #d4281c; // Default Red $red: #db0020; // Default Red
$red-600: #ac160b; $red-600: #ac160b;
$red-700: #840900; $red-700: #840900;
$red-800: #5b0600; $red-800: #5b0600;
@ -160,6 +160,7 @@ $border-radius-sm: 0.2rem;
$border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course. $border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course.
$border-radius-xl: 1.375rem; $border-radius-xl: 1.375rem;
$border-radius-pill: 50rem; $border-radius-pill: 50rem;
$border-radius-list-button: 1.375rem; // Used for list buttons
//Progress Bar Styling //Progress Bar Styling
$progress-bar-success-color: $green; $progress-bar-success-color: $green;

View file

@ -2,7 +2,7 @@
<baseInputButton <baseInputButton
v-bind="$props" v-bind="$props"
v-model="selectedValue" v-model="selectedValue"
buttonWrapperClasses="list-group base-input-button list-button no-hover d-flex flex-column w-100"> buttonWrapperClasses="list-group base-input-button list-button no-hover d-flex flex-column w-100 mb-2">
<div <div
:aria-label="buttonLabel" :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"
@ -87,6 +87,7 @@ $heritage-checked-border-color: #0070d1;
&:checked + .list-button-content { &:checked + .list-button-content {
background: $heritage-checked-background-color; background: $heritage-checked-background-color;
border-color: $heritage-checked-border-color; border-color: $heritage-checked-border-color;
box-shadow: 0 0 0 1px $blue;
&:not(.has-sub-copy) { &:not(.has-sub-copy) {
font-weight: 500; font-weight: 500;
color: $black; color: $black;
@ -96,9 +97,6 @@ $heritage-checked-border-color: #0070d1;
font-weight: 400; font-weight: 400;
color: $gray-600; color: $gray-600;
} }
&:focus:checked + .list-button-content {
border-width: 2.5px;
}
} }
} }
.list-button-content { .list-button-content {
@ -112,12 +110,9 @@ $heritage-checked-border-color: #0070d1;
box-shadow: $heritage-box-shadow; box-shadow: $heritage-box-shadow;
width: 100%; width: 100%;
outline: none; outline: none;
margin-bottom: .625rem;
height: 4.375rem;
&:not(.has-sub-copy) { &:not(.has-sub-copy) {
font-weight: 400; font-weight: 400;
height: 2.8125rem;
color: $darker-gray; color: $darker-gray;
} }
@ -127,4 +122,4 @@ $heritage-checked-border-color: #0070d1;
} }
} }
} }
</style> </style>