diff --git a/src/constants/package-names.js b/src/constants/package-names.js new file mode 100644 index 00000000..fb250451 --- /dev/null +++ b/src/constants/package-names.js @@ -0,0 +1,7 @@ +const packageNames = Object.freeze({ + TIER_ONE: 'TierOne', + TIER_TWO: 'TierTwo', + TIER_THREE: 'TierThree' +}); + +export default packageNames; diff --git a/src/constants/part-type-strings.js b/src/constants/part-type-strings.js index 90c6b5dc..52a3a2c7 100644 --- a/src/constants/part-type-strings.js +++ b/src/constants/part-type-strings.js @@ -2,7 +2,11 @@ FRONT_WIPER: 'FRONT WIPER', REAR_WIPER: 'REAR WIPER', RAIN_DEFENSE: 'RAIN DEFENSE', - RECALIBRATION: 'RECALIBRATION' + RECALIBRATION: 'RECALIBRATION', + REPLACE_FEE: 'REPLACE FEE', + RECYCLE_FEE: 'RECYCLE FEE', + MOBILE_FEE: 'MOBILE FEE', + REPAIR_FEE: 'REPAIR FEE' }); export default partTypeStrings; diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index feaa430d..712919e2 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -106,10 +106,7 @@ import loader from '@/ux-components/loader/loader.vue'; import { useMainStore } from '@/store'; import { useField, ErrorMessage } from 'vee-validate'; import { deepClone } from '@/helpers/object-helper'; -import { - convertDateToDateString, - convertDateStringToDate -} from '@/layouts/schedule-page/helpers/schedule-helper'; +import { convertDateStringToDate, convertDateToDateString } from '@/helpers/date-helper'; import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from './mixins/constants'; import { selectableDaysOptions, requiredParameter } from './mixins/helpers'; diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index 1ea62829..bd7d5796 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -34,7 +34,6 @@ loaderColor="white" :buttonText="footerButtonText" :class="(isButtonDisabled || isFooterButtonDisabled) && 'form-test-invalid'" - :isDisabled="(isButtonDisabled || isFooterButtonDisabled)" @clickEvent="validateAndEmit" /> diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 7f5a7546..432c03d7 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -1,3 +1,8 @@ +/* eslint-disable no-use-before-define */ +/* eslint-disable no-param-reassign */ +/* eslint-disable jsdoc/require-param-type */ +/* eslint-disable jsdoc/require-param-description */ +/* eslint-disable jsdoc/require-returns */ import dynamicStrings from '@/constants/dynamic-strings'; import { useMainStore } from '@/store'; @@ -191,6 +196,45 @@ export function fetchCmsContentForPage(issPage) { ); } +export function fetchGlobalCmsContent(issComponent) { + const store = useMainStore(); + + return ( + store.getPageData(issComponent) + .then((baseResponse) => processPageData(baseResponse, null))); +} + +export function updateCmsSiteHeader(cmsResponse) { + const clientCmsSiteHeader = getCmsSiteHeader(cmsResponse); + useMainStore().issConfig.clientHeader = clientCmsSiteHeader; +} + +function getCmsSiteHeader(cmsResponse) { + if (cmsResponse?.SiteHeaderWidget) { + const siteHeader = cmsResponse?.SiteHeaderWidget; + if (siteHeader.Answers) { + const { parentAccountNumber } = useMainStore().issConfig; + let headerAnswersToUse = siteHeader.Answers + .find((answer) => answer.AccountNumber === parentAccountNumber.toString()); + if (headerAnswersToUse == null) { + headerAnswersToUse = siteHeader.Answers + .find((answer) => answer.AccountNumber === '0'); + } + + if (headerAnswersToUse) { + return { + altText: headerAnswersToUse.AltText, + clientName: headerAnswersToUse.ClientName, + hexCode: headerAnswersToUse.HexCode, + imageId: headerAnswersToUse.ImageId, + imageUrl: headerAnswersToUse.AnswerImageUrl + }; + } + } + } + return {}; +} + /** * @function mapStringToModal * @param str @@ -493,6 +537,7 @@ export function doesCopyContainTextLink(copy) { export function setupModalLinks(context) { context.$nextTick(() => { const elements = document.getElementsByClassName('modal-text'); + // eslint-disable-next-line no-restricted-syntax for (const element of elements) { const target = element.getAttribute('modalTarget'); if (target) { diff --git a/src/helpers/date-helper.js b/src/helpers/date-helper.js index ed943174..1b062c85 100644 --- a/src/helpers/date-helper.js +++ b/src/helpers/date-helper.js @@ -1,4 +1,30 @@ -const getDateDifferenceInDays = (startDate, endDate) => { +export function calcDaysBetweenDates(dateString1, dateString2) { + const date1 = new Date(dateString1); + const date2 = new Date(dateString2); + const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds + return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days +} + +export function convertDateToDateString(date) { + // returns YYYY-MM-DD format + if (date instanceof Date !== true) return null; + return ( + `${date.getFullYear() + }-${ + (`0${date.getMonth() + 1}`).slice(-2) + }-${ + (`0${date.getDate()}`).slice(-2)}` + ); +} + +export function convertDateStringToDate(dateString) { + // dateString must be YYYY-MM-DD format + if (typeof dateString !== 'string') return null; + const dateParts = dateString.split('-'); + return new Date(dateParts[0], parseInt(dateParts[1], 10) - 1, dateParts[2]); +} + +export function getDateDifferenceInDays(startDate, endDate) { const date1 = new Date(endDate); date1.setHours(0, 0, 0, 0); const date2 = new Date(startDate); @@ -7,6 +33,42 @@ const getDateDifferenceInDays = (startDate, endDate) => { const DifferenceInTime = date1.getTime() - date2.getTime(); // To calculate the no. of days between two dates return DifferenceInTime / (1000 * 3600 * 24); -}; +} -export default getDateDifferenceInDays; +export function getDisplayTextForDurationLength(durationMinimum, durationMaximum) { + const isLongAppointment = durationMaximum >= 120; + const isDurationRange = durationMinimum !== durationMaximum; + + const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum; + const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum; + + const durationText = isDurationRange + ? `${adjustedMinimum} - ${adjustedMaximum}` + : adjustedMinimum; + + const unitText = isLongAppointment ? 'hours' : 'minutes'; + + return `${durationText} ${unitText}`; +} + +export function militaryToTwelveHourTime(timeString) { + // Expected input: "HH:MM" + if (typeof timeString !== 'string') return null; + let hours = parseInt(timeString.split(':')[0], 10); + const minutes = timeString.split(':')[1]; + const meridianNotation = hours > 11 ? 'PM' : 'AM'; + + if (hours > 12) { + hours -= 12; + } + + return `${hours}:${minutes} ${meridianNotation}`; +} + +export function sumDateString(dateString, daysToAdd) { + // dateString must be YYYY-MM-DD format + if (typeof dateString !== 'string') return null; + const date = convertDateStringToDate(dateString); + date.setDate(date.getDate() + daysToAdd); + return convertDateToDateString(date); +} diff --git a/src/helpers/service-package-helper.js b/src/helpers/service-package-helper.js new file mode 100644 index 00000000..539bdeb3 --- /dev/null +++ b/src/helpers/service-package-helper.js @@ -0,0 +1,238 @@ +import partTypeStrings from '@/constants/part-type-strings'; +import damageLocationsSelected from '@/constants/damage-locations-selected'; +import packageNames from '@/constants/package-names'; + +export function findLineItemsWithPartType(typeToFind, itemsToSearch) { + const partTypeMatches = itemsToSearch?.filter((lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase()); + return partTypeMatches; +} + +export function containsLineItemWithPartType(typeToFind, itemsToSearch) { + const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch); + return !!partTypeMatches?.length; +} + +export function getVapsLineItems(availableLineItems, vapTypes) { + const lineItems = []; + + for (let i = 0; i < vapTypes.length; i++) { + lineItems.push(...findLineItemsWithPartType(vapTypes[i], availableLineItems)); + } + + return lineItems; +} + +export function containsGlassPieceWithLocation(locationToFind, glassPiecesToSearch) { + const glassLocationMatches = glassPiecesToSearch?.filter((glassPiece) => + glassPiece.glassLocation.toUpperCase() === locationToFind.toUpperCase()); + + return !!glassLocationMatches?.length; +} + +export function shouldFrontWipersBeAvailable( + glassToReplace, + availableLineItems, + isRepair, + targetTier +) { + const frontWiperIsAvailable = containsLineItemWithPartType( + partTypeStrings.FRONT_WIPER, + availableLineItems + ); + + const isFrontWindshieldTask = + isRepair || containsGlassPieceWithLocation(damageLocationsSelected.WINDSHIELD, glassToReplace); + switch (targetTier) { + case packageNames.TIER_TWO: + return frontWiperIsAvailable && isFrontWindshieldTask; + case packageNames.TIER_THREE: + return frontWiperIsAvailable; + default: + return false; + } +} + +export function shouldRearWipersBeAvailable( + glassToReplace, + availableLineItems, + isRepair, + targetTier +) { + const rearWiperIsAvailable = containsLineItemWithPartType( + partTypeStrings.REAR_WIPER, + availableLineItems + ); + const frontWiperIsAvailable = containsLineItemWithPartType( + partTypeStrings.FRONT_WIPER, + availableLineItems + ); + const isRearWindshieldTask = containsGlassPieceWithLocation( + damageLocationsSelected.REAR, + glassToReplace + ); + + switch (targetTier) { + case packageNames.TIER_TWO: + return rearWiperIsAvailable && isRearWindshieldTask; + case packageNames.TIER_THREE: + return rearWiperIsAvailable && (isRearWindshieldTask || !frontWiperIsAvailable); + default: + return false; + } +} + +export function shouldRainDefenseBeAvailable( + glassToReplace, + availableLineItems, + isRepair, + targetTier +) { + const isTierThree = targetTier === packageNames.TIER_THREE; + const frontWipersInTierTwo = shouldFrontWipersBeAvailable( + glassToReplace, + availableLineItems, + isRepair, + packageNames.TIER_TWO + ); + const frontWipersInTierThree = shouldFrontWipersBeAvailable( + glassToReplace, + availableLineItems, + isRepair, + packageNames.TIER_THREE + ); + const rearWipersInTierTwo = shouldRearWipersBeAvailable( + glassToReplace, + availableLineItems, + isRepair, + packageNames.TIER_TWO + ); + + return isTierThree && !(rearWipersInTierTwo && !frontWipersInTierTwo && frontWipersInTierThree); +} + +export function getPackageContents(glassToReplace, availableLineItems, isRepair, targetTier) { + const vaps = []; + + if (shouldFrontWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) { + vaps.push(partTypeStrings.FRONT_WIPER); + } + + if (shouldRearWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) { + vaps.push(partTypeStrings.REAR_WIPER); + } + + if (shouldRainDefenseBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) { + vaps.push(partTypeStrings.RAIN_DEFENSE); + } + + return vaps; +} + +export function getAvailablePackages(glassToReplace, availableLineItems, isRepair) { + const tierOneVaps = getPackageContents( + glassToReplace, + availableLineItems, + isRepair, + packageNames.TIER_ONE + ); + + const tierTwoVaps = getPackageContents( + glassToReplace, + availableLineItems, + isRepair, + packageNames.TIER_TWO + ); + + const tierThreeVaps = getPackageContents( + glassToReplace, + availableLineItems, + isRepair, + packageNames.TIER_THREE + ); + + if (tierTwoVaps.length === 0) { + return [ + { + packageName: packageNames.TIER_ONE, + vapTypes: tierOneVaps + }, + { + packageName: packageNames.TIER_THREE, + vapTypes: tierThreeVaps + } + ]; + } + return [ + { + packageName: packageNames.TIER_ONE, + vapTypes: tierOneVaps + }, + { + packageName: packageNames.TIER_TWO, + vapTypes: tierTwoVaps + }, + { + packageName: packageNames.TIER_THREE, + vapTypes: tierThreeVaps + } + ]; +} + +export function getLowestTierForType(glassToReplace, availableLineItems, isRepair, partType) { + const packages = getAvailablePackages(glassToReplace, availableLineItems, isRepair); + for (let i = 0; i < packages.length; i++) { + for (let j = 0; j < packages[i].vapTypes.length; j++) { + if (packages[i].vapTypes[j].toUpperCase() === partType.toUpperCase()) { + return packages[i].packageName; + } + } + } + + return packageNames.TIER_ONE; +} + +function maxTier(tierA, tierB) { + if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) { + return packageNames.TIER_THREE; + } + + if (tierA === packageNames.TIER_TWO || tierB === packageNames.TIER_TWO) { + return packageNames.TIER_TWO; + } + + return packageNames.TIER_ONE; +} + +export function getHighestRequiredTier(glassToReplace, availableLineItems, isRepair, vaps) { + let currentHighestTier = packageNames.TIER_ONE; + for (let i = 0; i < vaps.length; i++) { + const lowestTierForItem = getLowestTierForType( + glassToReplace, + availableLineItems, + isRepair, + vaps[i].partType + ); + + currentHighestTier = maxTier(currentHighestTier, lowestTierForItem); + } + + return currentHighestTier; +} + +export function getHighestFullySatisfiedTier(glassToReplace, availableLineItems, isRepair, vaps) { + const packages = getAvailablePackages(glassToReplace, availableLineItems, isRepair); + let highestSatisfiedPackage = packageNames.TIER_ONE; + + for (let i = 0; i < packages.length; i++) { + let isPackageSatisfied = true; + for (let j = 0; j < packages[i].vapTypes.length; j++) { + isPackageSatisfied = + isPackageSatisfied && containsLineItemWithPartType(packages[i].vapTypes[j], vaps); + } + if (isPackageSatisfied) { + highestSatisfiedPackage = packages[i].packageName; + } + } + + return highestSatisfiedPackage; +} diff --git a/src/iss-components/site-header/site-header.vue b/src/iss-components/site-header/site-header.vue index 1d8a9c54..d1622a93 100644 --- a/src/iss-components/site-header/site-header.vue +++ b/src/iss-components/site-header/site-header.vue @@ -24,6 +24,7 @@ + + diff --git a/src/layouts/review-page/review-page.spec.js b/src/layouts/review-page/review-page.spec.js new file mode 100644 index 00000000..a9739cef --- /dev/null +++ b/src/layouts/review-page/review-page.spec.js @@ -0,0 +1,386 @@ +// Components +import review from '@/layouts/review-page/review-page.vue'; + +// Supporting Files +import { createTestingPinia } from '@pinia/testing'; +import { shallowMount } from '@vue/test-utils'; +import { getMountOptions } from '@/helpers/unit-test-helper.js'; +import { useMainStore } from '@/store/index.js'; + +// Mock fetchCmsContentForPage +jest.mock('@/helpers/cms-content-helper', () => ({ + fetchCmsContentForPage: jest.fn() +})); + +const mockMixin = { + methods: { + getCmsContent: jest.fn().mockImplementation(() => ''), + setCmsContent: jest.fn() + } +}; + +const footerStub = { + render: () => {}, + methods: { + updateButtonText: jest.fn() + } +}; + +const loadingModalStub = { + render: () => {}, + methods: { + showModal: jest.fn(), + hideModal: jest.fn() + } +}; + +function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn() + } + }); + + // mountOptions.global.mocks["$store"] = store; + + mountOptions.global.stubs = { + siteFooter: footerStub, + loadingModal: loadingModalStub + }; + + methodToRun(); + + mountOptions.mixins = [mockMixin]; + mountOptions.data = () => ( + initialData + ); + + const wrapper = shallowMount(review, mountOptions); + return { wrapper }; +} + +beforeEach(() => { + const testingPinia = createTestingPinia({ + initialState: { + main: { + order: { + vehicle: { + year: '2020', + make: 'Acura', + model: 'MDX', + style: '4 door sedan' + }, + damage: { + isRepair: false, + numberOfChips: 2, + glassToReplace: ['dummy location value'] + }, + lineItems: { + glassParts: ['dummy part value'], + supportingItems: ['dummy supporting item'], + vaps: ['dummy vap'] + }, + serviceLocation: { + address: 'address 1', + address2: 'address 2', + city: 'city', + state: 'state', + zipCode: 'zip code', + appointmentType: 'Mobile', + provider: { + providerNumber: 1, + address: { + streetAddress: 'provider address 1', + city: 'provider city', + state: 'provider state', + zipCode: 'provider zip code' + } + } + }, + schedule: { + date: 'date', + startTime: 'start', + endTime: 'end', + jobMinMinutes: '30', + jobMaxMinutes: '45' + }, + customer: { + firstName: 'first name', + lastName: 'last name', + emailAddress: 'builddigitaltest@safelite.com', + phoneNumber: '555-555-5555', + isSmsOptIn: true + } + } + } + } + }); + useMainStore(testingPinia); +}); +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('Review Page', () => { + describe('arePagePrerequisitesValid', () => { + test('Returns true for baseline valid state', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(true); + }); + test('Returns false for empty state', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order = { + vehicle: { + year: null, + make: null, + model: null, + style: null, + carId: null, + category: null, + vin: null, + imageUrl: null, + imageVifNumber: null, + imageColor: null, + registration: { + licensePlate: null + } + }, + serviceLocation: { + address: null, + address2: null, + city: null, + state: null, + zipCode: null, + zipCodeCtu: null, + appointmentType: null, + isVehicleProtected: null, + provider: { + providerNumber: null, + address: { + streetAddress: null, + city: null, + state: null, + zipCode: null, + zipCodeCtu: null + } + }, + techNotes: null + }, + customer: { + firstName: null, + lastName: null, + emailAddress: null, + phoneNumber: null, + isSmsOptIn: null + }, + damage: { + isRepair: null, + numberOfChips: null, + glassToReplace: null, + partQuestionAnswers: null, + moldingQuestionAnswers: null, + capabilityQuestionAnswers: null + }, + lineItems: { + glassParts: null, + supportingItems: null, + vaps: null, + serverData: null + }, + payment: { + isInsurance: null, + insuranceCoverage: { + isVerified: null, + coverageStatus: null + }, + parentAccountNumber: 0 + }, + schedule: { + date: null, + startTime: null, + endTime: null, + routeCode: null, + jobMaxMinutes: null, + jobMinMinutes: null + }, + referralNumber: null, + referralSequenceNumber: null, + referralDate: null, + referralCorrelationId: null, + eon: null + }; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(false); + }); + describe('Damage requirements', () => { + test('Accepts null glassToReplace when is repair', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.damage.isRepair = true; + wrapper.vm.mainStore.order.damage.glassToReplace = null; + wrapper.vm.mainStore.order.damage.numberOfChips = 1; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(true); + }); + test('Rejects 0 chips when repair', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.damage.isRepair = true; + wrapper.vm.mainStore.order.damage.numberOfChips = 0; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(false); + }); + test('Rejects null chips when repair', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.damage.isRepair = true; + wrapper.vm.mainStore.order.damage.numberOfChips = null; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(false); + }); + test('Accepts null chips when not repair', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.damage.isRepair = false; + wrapper.vm.mainStore.order.damage.numberOfChips = null; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(true); + }); + test('Rejects empty glassToReplace when not repair', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.damage.isRepair = false; + wrapper.vm.mainStore.order.damage.glassToReplace = null; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(false); + }); + test('Rejects null glassToReplace when not repair', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.damage.isRepair = false; + wrapper.vm.mainStore.order.damage.glassToReplace = []; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(false); + }); + }); + describe('Package requirements', () => { + test('Accepts null glassParts when is repair', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.damage.isRepair = true; + wrapper.vm.mainStore.order.lineItems.glassParts = null; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(true); + }); + test('Rejects null glassParts when not repair', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.damage.isRepair = false; + wrapper.vm.mainStore.order.lineItems.glassParts = null; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(false); + }); + }); + describe('Service Location requirements', () => { + test('Accepts null provider address when mobile appointment', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Mobile'; + wrapper.vm.mainStore.order.serviceLocation.provider.address = {}; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(true); + }); + test('Reject null provider address when non-mobile appointment', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Inshop'; + wrapper.vm.mainStore.order.serviceLocation.provider.address = {}; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(false); + }); + test('Accepts null service location address when non-mobile appointment', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Inshop'; + wrapper.vm.mainStore.order.serviceLocation.address = null; + wrapper.vm.mainStore.order.serviceLocation.address2 = null; + wrapper.vm.mainStore.order.serviceLocation.zipCode = null; + wrapper.vm.mainStore.order.serviceLocation.city = null; + wrapper.vm.mainStore.order.serviceLocation.state = null; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(true); + }); + test('Rejects null service location address when mobile appointment', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Mobile'; + wrapper.vm.mainStore.order.serviceLocation.address = null; + wrapper.vm.mainStore.order.serviceLocation.address2 = null; + wrapper.vm.mainStore.order.serviceLocation.zipCode = null; + wrapper.vm.mainStore.order.serviceLocation.city = null; + wrapper.vm.mainStore.order.serviceLocation.state = null; + + // Act + const isValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(isValid).toBe(false); + }); + }); + }); +}); diff --git a/src/layouts/review-page/review-page.vue b/src/layouts/review-page/review-page.vue index 53832ab7..632b4949 100644 --- a/src/layouts/review-page/review-page.vue +++ b/src/layouts/review-page/review-page.vue @@ -1,41 +1,134 @@ + + diff --git a/src/layouts/review-page/review-sections/customer-review/customer-review.spec.js b/src/layouts/review-page/review-sections/customer-review/customer-review.spec.js new file mode 100644 index 00000000..1e0f612d --- /dev/null +++ b/src/layouts/review-page/review-sections/customer-review/customer-review.spec.js @@ -0,0 +1,129 @@ +// Components +import customerReview from '@/layouts/review-page/review-sections/customer-review/customer-review.vue'; + +// Supporting Files +import { shallowMount } from '@vue/test-utils'; +import { getMountOptions } from '@/helpers/unit-test-helper.js'; + +const testConstants = { + cms: { + header: { + text: 'Header' + }, + sms: { + text: 'Sms' + } + }, + customer: { + firstName: 'First', + lastName: 'Last', + phoneNumber: '111-111-1111', + emailAddress: 'builddigitaltest@safelite.com', + isSmsOptIn: false + }, + displayContent: { + fullName: 'First Last', + phoneNumber: '111-111-1111', + emailAddress: 'builddigitaltest@safelite.com', + smsOptIn: 'Sms' + } +}; + +function generateDefaultProps() { + return { + cmsWidgetName: 'CustomerWidget', + customer: { + firstName: testConstants.customer.firstName, + lastName: testConstants.customer.lastName, + phoneNumber: testConstants.customer.phoneNumber, + emailAddress: testConstants.customer.emailAddress, + isSmsOptIn: testConstants.customer.isSmsOptIn + } + }; +} + +let cmsContent; +const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '') + } +}; + +function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn() + } + }); + + methodToRun(); + + mountOptions.data = () => ( + initialData + ); + + mountOptions.mixins = [mockMixin]; + + const wrapper = shallowMount(customerReview, mountOptions); + return { wrapper }; +} + +beforeEach(() => { + cmsContent = { + CustomerWidget: { + HeaderText: testConstants.cms.header.text, + SubheaderText: testConstants.cms.sms.text + } + }; +}); + +describe('Customer Review Block', () => { + test('Should display header text from cms', async () => { + // Arrange + const props = generateDefaultProps(); + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.header).toEqual(testConstants.cms.header.text); + }); + + test('Should display sms text from cms', async () => { + // Arrange + const props = generateDefaultProps(); + + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text); + }); + + test('Should render correct display content', async () => { + // Arrange + const props = generateDefaultProps(); + + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toEqual([ + testConstants.displayContent.fullName, + testConstants.displayContent.emailAddress, + testConstants.displayContent.phoneNumber, + testConstants.displayContent.smsOptIn + ]); + }); +}); diff --git a/src/layouts/review-page/review-sections/customer-review/customer-review.vue b/src/layouts/review-page/review-sections/customer-review/customer-review.vue new file mode 100644 index 00000000..5f442531 --- /dev/null +++ b/src/layouts/review-page/review-sections/customer-review/customer-review.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/layouts/review-page/review-sections/damage-review/damage-review.spec.js b/src/layouts/review-page/review-sections/damage-review/damage-review.spec.js new file mode 100644 index 00000000..b7a36442 --- /dev/null +++ b/src/layouts/review-page/review-sections/damage-review/damage-review.spec.js @@ -0,0 +1,436 @@ +// Components +import damageReview from '@/layouts/review-page/review-sections/damage-review/damage-review.vue'; + +// Supporting Files +import { shallowMount } from '@vue/test-utils'; +import { getMountOptions } from '@/helpers/unit-test-helper.js'; +import damageLocationsSelected from '@/constants/damage-locations-selected'; + +const testConstants = { + cmsConstants: { + widgetNames: { + header: 'DamageReviewWidget', + locations: 'DamageLocationsWidget', + driverDamages: 'DriverDamagesWidget', + passengerDamages: 'PassengerDamagesWidget' + }, + header: { + text: 'Damage' + }, + damageLocations: { + windshield: damageLocationsSelected.WINDSHIELD, + driver: damageLocationsSelected.DRIVER, + passenger: damageLocationsSelected.PASSENGER, + rear: damageLocationsSelected.REAR + }, + damageNames: { + vent: damageLocationsSelected.VENT, + front: damageLocationsSelected.FRONT, + back: damageLocationsSelected.BACK, + quarter: damageLocationsSelected.QUARTER, + side: damageLocationsSelected.SIDEDOOR + }, + locationCopy: { + windshield: 'Windshield copy', + driver: 'Driver copy', + passenger: 'Passenger copy', + rear: 'Rear copy' + }, + damageCopy: { + vent: 'Vent copy', + front: 'Front copy', + back: 'Back copy', + quarter: 'Quarter copy', + side: 'Side copy' + }, + imageId: '00000000-0000-0000-0000-000000000000' + }, + makeBulletedList: (items) => { + let list = ''; + + return list; + }, + glassItems: { + windshield: { + glassLocation: damageLocationsSelected.WINDSHIELD, + glassName: damageLocationsSelected.SINGLE + }, + rear: { + glassLocation: damageLocationsSelected.REAR, + glassName: damageLocationsSelected.STATIONARY + }, + passengerItems: { + vent: { + glassLocation: damageLocationsSelected.PASSENGER, + glassName: damageLocationsSelected.VENT + }, + front: { + glassLocation: damageLocationsSelected.PASSENGER, + glassName: damageLocationsSelected.FRONT + }, + back: { + glassLocation: damageLocationsSelected.PASSENGER, + glassName: damageLocationsSelected.BACK + }, + quarter: { + glassLocation: damageLocationsSelected.PASSENGER, + glassName: damageLocationsSelected.QUARTER + }, + side: { + glassLocation: damageLocationsSelected.PASSENGER, + glassName: damageLocationsSelected.SIDEDOOR + } + }, + driverItems: { + vent: { + glassLocation: damageLocationsSelected.DRIVER, + glassName: damageLocationsSelected.VENT + }, + front: { + glassLocation: damageLocationsSelected.DRIVER, + glassName: damageLocationsSelected.FRONT + }, + back: { + glassLocation: damageLocationsSelected.DRIVER, + glassName: damageLocationsSelected.BACK + }, + quarter: { + glassLocation: damageLocationsSelected.DRIVER, + glassName: damageLocationsSelected.QUARTER + }, + side: { + glassLocation: damageLocationsSelected.DRIVER, + glassName: damageLocationsSelected.SIDEDOOR + } + } + } +}; + +let cmsContent; +const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '') + } +}; + +function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn() + } + }); + + methodToRun(); + + mountOptions.data = () => ( + initialData + ); + + mountOptions.mixins = [mockMixin]; + + const wrapper = shallowMount(damageReview, mountOptions); + return { wrapper }; +} + +beforeEach(() => { + cmsContent = { + DamageReviewWidget: { + Text: testConstants.cmsConstants.header.text + }, + DamageLocationsWidget: { + Answers: [ + { + Name: testConstants.cmsConstants.damageLocations.windshield, + Text: testConstants.cmsConstants.locationCopy.windshield, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageLocations.driver, + Text: testConstants.cmsConstants.locationCopy.driver, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: testConstants.cmsConstants.widgetNames.driverDamages + }, + { + Name: testConstants.cmsConstants.damageLocations.passenger, + Text: testConstants.cmsConstants.locationCopy.passenger, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: testConstants.cmsConstants.widgetNames.passengerDamages + }, + { + Name: testConstants.cmsConstants.damageLocations.rear, + Text: testConstants.cmsConstants.locationCopy.rear, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + } + ] + }, + DriverDamagesWidget: { + Answers: [ + { + Name: testConstants.cmsConstants.damageNames.vent, + Text: testConstants.cmsConstants.damageCopy.vent, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageNames.front, + Text: testConstants.cmsConstants.damageCopy.front, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageNames.back, + Text: testConstants.cmsConstants.damageCopy.back, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageNames.quarter, + Text: testConstants.cmsConstants.damageCopy.quarter, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageNames.side, + Text: testConstants.cmsConstants.damageCopy.side, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + } + ] + }, + PassengerDamagesWidget: { + Answers: [ + { + Name: testConstants.cmsConstants.damageNames.vent, + Text: testConstants.cmsConstants.damageCopy.vent, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageNames.front, + Text: testConstants.cmsConstants.damageCopy.front, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageNames.back, + Text: testConstants.cmsConstants.damageCopy.back, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageNames.quarter, + Text: testConstants.cmsConstants.damageCopy.quarter, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: testConstants.cmsConstants.damageNames.side, + Text: testConstants.cmsConstants.damageCopy.side, + SubText: '', + ImageId: testConstants.cmsConstants.imageId, + Image: '', + SubWidgetName: '' + } + ] + } + }; +}); + +describe('Damage Review Block', () => { + describe('Correctly assembles damage info into a display string', () => { + test('Shows windshield copy when windshield damage is included', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent({ + cmsWidgetName: testConstants.cmsConstants.widgetNames.header, + damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations, + damage: { + isRepair: false, + glassToReplace: [testConstants.glassItems.windshield] + } + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toStrictEqual([ + testConstants.cmsConstants.locationCopy.windshield + ]); + }); + + test('Windshield copy is shown when order is a repair', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent({ + cmsWidgetName: testConstants.cmsConstants.widgetNames.header, + damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations, + damage: { + isRepair: true, + numberOfChips: 2, + glassToReplace: [] + } + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toStrictEqual([ + testConstants.cmsConstants.locationCopy.windshield + ]); + }); + + test('Rear windshield copy shows when rear damage is present', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent({ + cmsWidgetName: testConstants.cmsConstants.widgetNames.header, + damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations, + damage: { + isRepair: false, + numberOfChips: null, + glassToReplace: [testConstants.glassItems.rear] + } + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toStrictEqual([ + testConstants.cmsConstants.locationCopy.rear + ]); + }); + + test('Driver side copy and items are shown when driver side damage is present', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent({ + cmsWidgetName: testConstants.cmsConstants.widgetNames.header, + damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations, + damage: { + isRepair: false, + numberOfChips: null, + glassToReplace: [ + testConstants.glassItems.driverItems.back, + testConstants.glassItems.driverItems.front + ] + } + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toStrictEqual([ + testConstants.cmsConstants.locationCopy.driver, + testConstants.makeBulletedList([ + testConstants.cmsConstants.damageCopy.front, + testConstants.cmsConstants.damageCopy.back + ]) + ]); + }); + + test('Passenger side copy and items are shown when passenger side damage is present', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent({ + cmsWidgetName: testConstants.cmsConstants.widgetNames.header, + damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations, + damage: { + isRepair: false, + numberOfChips: null, + glassToReplace: [ + testConstants.glassItems.passengerItems.quarter, + testConstants.glassItems.passengerItems.vent, + testConstants.glassItems.passengerItems.side + ] + } + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toStrictEqual([ + testConstants.cmsConstants.locationCopy.passenger, + testConstants.makeBulletedList([ + testConstants.cmsConstants.damageCopy.vent, + testConstants.cmsConstants.damageCopy.quarter, + testConstants.cmsConstants.damageCopy.side + ]) + ]); + }); + + test('All relevant sections are shown in order in multiglass scenario', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent({ + cmsWidgetName: testConstants.cmsConstants.widgetNames.header, + damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations, + damage: { + isRepair: false, + numberOfChips: null, + glassToReplace: [ + testConstants.glassItems.windshield, + testConstants.glassItems.rear, + testConstants.glassItems.driverItems.vent, + testConstants.glassItems.driverItems.front, + testConstants.glassItems.driverItems.back, + testConstants.glassItems.passengerItems.quarter, + testConstants.glassItems.passengerItems.back, + testConstants.glassItems.passengerItems.side + ] + } + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toStrictEqual([ + testConstants.cmsConstants.locationCopy.windshield, + testConstants.cmsConstants.locationCopy.driver, + testConstants.makeBulletedList([ + testConstants.cmsConstants.damageCopy.vent, + testConstants.cmsConstants.damageCopy.front, + testConstants.cmsConstants.damageCopy.back + ]), + testConstants.cmsConstants.locationCopy.passenger, + testConstants.makeBulletedList([ + testConstants.cmsConstants.damageCopy.back, + testConstants.cmsConstants.damageCopy.quarter, + testConstants.cmsConstants.damageCopy.side + ]), + testConstants.cmsConstants.locationCopy.rear + ]); + }); + }); +}); diff --git a/src/layouts/review-page/review-sections/damage-review/damage-review.vue b/src/layouts/review-page/review-sections/damage-review/damage-review.vue new file mode 100644 index 00000000..4e0b0878 --- /dev/null +++ b/src/layouts/review-page/review-sections/damage-review/damage-review.vue @@ -0,0 +1,134 @@ + + + diff --git a/src/layouts/review-page/review-sections/schedule-review/schedule-review.vue b/src/layouts/review-page/review-sections/schedule-review/schedule-review.vue new file mode 100644 index 00000000..d34e33a5 --- /dev/null +++ b/src/layouts/review-page/review-sections/schedule-review/schedule-review.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/layouts/review-page/review-sections/service-location-review/service-location-review.spec.js b/src/layouts/review-page/review-sections/service-location-review/service-location-review.spec.js new file mode 100644 index 00000000..834714aa --- /dev/null +++ b/src/layouts/review-page/review-sections/service-location-review/service-location-review.spec.js @@ -0,0 +1,134 @@ +// Components +import serviceLocationReview from '@/layouts/review-page/review-sections/service-location-review/service-location-review.vue'; + +// Supporting Files +import { shallowMount } from '@vue/test-utils'; +import { getMountOptions } from '@/helpers/unit-test-helper.js'; +import { AppointmentTypeStrings } from '@/constants/schedule-constants'; + +const cmsContent = { + ServiceLocationTitleWidget: { + Text: 'Title Text' + } +}; + +const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '') + } +}; + +function generateDefaultProps() { + return { + cmsWidgetName: 'ServiceLocationTitleWidget', + serviceLocation: { + address: 'Mobile Address 1', + address2: 'Mobile Address 2', + city: 'Mobile City', + state: 'MO', + zipCode: '11111', + zipCodeCtu: '', + appointmentType: AppointmentTypeStrings.MOBILE, + isVehicleProtected: false, + provider: { + providerNumber: '', + address: { + streetAddress: 'Service Location Address', + city: 'Service Location City', + state: 'SL', + zipCode: '22222', + zipCodeCtu: '' + } + } + } + }; +} + +function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn() + } + }); + + methodToRun(); + + mountOptions.data = () => ( + initialData + ); + + mountOptions.mixins = [mockMixin]; + + const wrapper = shallowMount(serviceLocationReview, mountOptions); + return { wrapper }; +} + +describe('Service Location Review Block', () => { + test('Should render mobile address if mobile appointment', async () => { + // Arrange + const props = generateDefaultProps(); + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toEqual([ + 'Mobile Address 1, Mobile Address 2, Mobile City, MO 11111' + ]); + }); + + test('Should render service location address if inshop appointment.', async () => { + // Arrange + const props = generateDefaultProps(); + props.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP; + + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toEqual([ + 'Service Location Address, Service Location City, SL 22222' + ]); + }); + + test('Should render service location address if drop-off appointment', async () => { + // Arrange + const props = generateDefaultProps(); + props.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF; + + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toEqual([ + 'Service Location Address, Service Location City, SL 22222' + ]); + }); + + test('Should not add comma or any text if address2 is null', async () => { + // Arrange + const props = generateDefaultProps(); + props.serviceLocation.address2 = null; + + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toEqual(['Mobile Address 1, Mobile City, MO 11111']); + }); +}); diff --git a/src/layouts/review-page/review-sections/service-location-review/service-location-review.vue b/src/layouts/review-page/review-sections/service-location-review/service-location-review.vue new file mode 100644 index 00000000..59241b15 --- /dev/null +++ b/src/layouts/review-page/review-sections/service-location-review/service-location-review.vue @@ -0,0 +1,59 @@ + + + diff --git a/src/layouts/review-page/review-sections/service-package-review/service-package-review.spec.js b/src/layouts/review-page/review-sections/service-package-review/service-package-review.spec.js new file mode 100644 index 00000000..6dfd6f4e --- /dev/null +++ b/src/layouts/review-page/review-sections/service-package-review/service-package-review.spec.js @@ -0,0 +1,969 @@ +// Components +import servicePackageReview from '@/layouts/review-page/review-sections/service-package-review/service-package-review.vue'; + +// Supporting Files +import { shallowMount } from '@vue/test-utils'; +import { getMountOptions } from '@/helpers/unit-test-helper.js'; +import packageNames from '@/constants/package-names'; +import partTypeStrings from '@/constants/part-type-strings'; +import damageLocationsSelected from '@/constants/damage-locations-selected'; + +const testConstants = { + cmsPropValues: { + servicePackageOptionsCmsName: 'ServicePackageTitle', + defaultPackageItemsCmsName: 'DefaultPackageItemDescriptions', + vapsItemsCmsName: 'VapsItemDescriptions' + }, + widgetNames: { + tierOneTitle: 'EconomyServiceTitle', + tierTwoTitle: 'StandardServiceTitle', + tierThreeTitle: 'PremiumServiceTitle' + }, + defaultItemCopy: { + itemOne: 'Item Description 1', + itemTwo: 'Item Description 2', + itemThree: 'Item Description 3', + itemFour: 'Item Description 4', + defaultItemCopyArray: ['Item Description 1', 'Item Description 2', 'Item Description 3'] + }, + vapsCopy: { + frontWiperCopy: 'Front Wiper copy', + rearWiperCopy: 'Rear Wiper copy', + rainDefenseCopy: 'Rain defense copy' + }, + parts: { + frontWiperPart: { + partNumber: 'SBB16', + description: 'SAFELITE BEAM BLADE 16', + partType: 'FRONT WIPER', + price: 32.64 + }, + rearWiperPart: { + partNumber: 'SBBR12A', + description: 'SAFELITE REAR BLADE 12A', + partType: 'REAR WIPER', + price: 24.48 + }, + rainDefensePart: { + partNumber: 'RAIN DEFENSE', + description: null, + partType: 'RAIN DEFENSE', + price: 35.5 + }, + recalPart: { + partNumber: 'RECAL STATIC', + Description: 'Recalibration', + partType: 'recalibration', + Quantity: '1', + price: 150.0 + } + }, + damages: { + frontWindshield: { + glassLocation: damageLocationsSelected.WINDSHIELD, + glassName: damageLocationsSelected.SINGLE + }, + rearWindshield: { + glassLocation: damageLocationsSelected.REAR, + glassName: damageLocationsSelected.STATIONARY + }, + sideGlass: { + glassLocation: damageLocationsSelected.PASSENGER, + glassName: damageLocationsSelected.QUARTER + } + }, + imageId: '00000000-0000-0000-0000-000000000000' +}; + +const figmaScenarios = [ + { + name: '05_01_CSR_Quote_Cash', + params: { + wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: false, + glassToReplace: [testConstants.damages.frontWindshield] + }, + glassParts: [], + supportingItems: [] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Standard', + vapsCombo: [testConstants.parts.frontWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy + ] + } + }, + { + name: 'Premium', + vapsCombo: [ + testConstants.parts.rainDefensePart, + testConstants.parts.frontWiperPart + ], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Standard+RearWiper', + vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rearWiperCopy + ] + } + } + ] + }, + { + name: '05_01_CSR_Quote_Standard_Repair', + params: { + wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: true, + glassToReplace: [] + }, + glassParts: [], + supportingItems: [] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Standard', + vapsCombo: [testConstants.parts.frontWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy + ] + } + }, + { + name: 'Premium', + vapsCombo: [ + testConstants.parts.rainDefensePart, + testConstants.parts.frontWiperPart + ], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Standard+RearWiper', + vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rearWiperCopy + ] + } + } + ] + }, + { + name: '05_01_CSR_Quote_Recal', + params: { + wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: false, + glassToReplace: [testConstants.damages.frontWindshield] + }, + glassParts: [], + supportingItems: [testConstants.parts.recalPart] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Standard', + vapsCombo: [testConstants.parts.frontWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy + ] + } + }, + { + name: 'Premium', + vapsCombo: [ + testConstants.parts.rainDefensePart, + testConstants.parts.frontWiperPart + ], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Standard+RearWiper', + vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rearWiperCopy + ] + } + } + ] + }, + // 05_01_CSR_Quote_RearGlass omitted as a duplicate of below. + { + name: '05_01_CSR_Quote_RearGlass+NonWindshield', + params: { + wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: false, + glassToReplace: [testConstants.damages.rearWindshield] + }, + glassParts: [], + supportingItems: [] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Economy+Frontwiper', + vapsCombo: [testConstants.parts.frontWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy + ] + } + }, + { + name: 'Standard', + vapsCombo: [testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rearWiperCopy + ] + } + }, + { + name: 'Standard+RainDefense', + vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rearWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Premium', + vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.frontWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rearWiperCopy + ] + } + } + ] + }, + { + name: '05_01_CSR_Quote_RearGlass+Windshield', + params: { + wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: false, + glassToReplace: [ + testConstants.damages.frontWindshield, + testConstants.damages.rearWindshield + ] + }, + glassParts: [], + supportingItems: [] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Economy+Frontwiper', + vapsCombo: [testConstants.parts.frontWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy + ] + } + }, + { + name: 'Economy+Rearwiper', + vapsCombo: [testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rearWiperCopy + ] + } + }, + { + name: 'Economy+RainDefense', + vapsCombo: [testConstants.parts.rainDefensePart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Economy+Frontwiper+RainDefense', + vapsCombo: [ + testConstants.parts.frontWiperPart, + testConstants.parts.rainDefensePart + ], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Economy+Rearwiper+RainDefense', + vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rearWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Standard', + vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rearWiperCopy + ] + } + }, + { + name: 'Premium', + vapsCombo: [ + testConstants.parts.rearWiperPart, + testConstants.parts.frontWiperPart, + testConstants.parts.rainDefensePart + ], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rearWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + } + ] + }, + { + name: '05_01_CSR_Quote_RearGlassNoFrontFit', + params: { + wiperResponse: [testConstants.parts.rearWiperPart], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: false, + glassToReplace: [testConstants.damages.rearWindshield] + }, + glassParts: [], + supportingItems: [] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Economy+Raindefense', + vapsCombo: [testConstants.parts.rainDefensePart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Standard', + vapsCombo: [testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rearWiperCopy + ] + } + }, + { + name: 'Premium', + vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rearWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + } + ] + }, + // 05_01_CSR_Quote_Windshield+SideGlass has identical outcomes to 05_01_CSR_Quote_Cash, but included in case that changes in the future. + { + name: '05_01_CSR_Quote_Windshield+SideGlass', + params: { + wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: false, + glassToReplace: [ + testConstants.damages.frontWindshield, + testConstants.damages.sideGlass + ] + }, + glassParts: [], + supportingItems: [] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Standard', + vapsCombo: [testConstants.parts.frontWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy + ] + } + }, + { + name: 'Premium', + vapsCombo: [ + testConstants.parts.rainDefensePart, + testConstants.parts.frontWiperPart + ], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Standard+RearWiper', + vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierTwoTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rearWiperCopy + ] + } + } + ] + }, + // Has no standard package + { + name: '05_01_CSR_Quote_SideGlass', + params: { + wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: false, + glassToReplace: [testConstants.damages.sideGlass] + }, + glassParts: [], + supportingItems: [] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Economy+Frontwiper', + vapsCombo: [testConstants.parts.frontWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy + ] + } + }, + { + name: 'Economy+RainDefense', + vapsCombo: [testConstants.parts.rainDefensePart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Economy+Rearwiper', + vapsCombo: [testConstants.parts.rearWiperPart], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rearWiperCopy + ] + } + }, + { + name: 'Premium', + vapsCombo: [ + testConstants.parts.rainDefensePart, + testConstants.parts.frontWiperPart + ], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + }, + { + name: 'Premium+Rearwiper', + vapsCombo: [ + testConstants.parts.rainDefensePart, + testConstants.parts.frontWiperPart, + testConstants.parts.rearWiperPart + ], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.frontWiperCopy, + testConstants.vapsCopy.rearWiperCopy, + testConstants.vapsCopy.rainDefenseCopy + ] + } + } + ] + }, + // Has no standard package + { + name: '05_01_CSR_Quote_NoWiperFit', + params: { + wiperResponse: [], + rainDefenseResponse: testConstants.parts.rainDefensePart, + damage: { + isRepair: false, + glassToReplace: [testConstants.damages.frontWindshield] + }, + glassParts: [], + supportingItems: [] + }, + iterations: [ + { + name: 'Economy', + vapsCombo: [], + expected: { + packageNameWidget: testConstants.widgetNames.tierOneTitle, + displayContent: testConstants.defaultItemCopy.defaultItemCopyArray + } + }, + { + name: 'Premium', + vapsCombo: [testConstants.parts.rainDefensePart], + expected: { + packageNameWidget: testConstants.widgetNames.tierThreeTitle, + displayContent: [ + ...testConstants.defaultItemCopy.defaultItemCopyArray, + testConstants.vapsCopy.rainDefenseCopy + ] + } + } + ] + } +]; + +function generateDefaultProps() { + return { + servicePackageOptionsCmsName: testConstants.cmsPropValues.servicePackageOptionsCmsName, + defaultPackageItemsCmsName: testConstants.cmsPropValues.defaultPackageItemsCmsName, + vapsItemsCmsName: testConstants.cmsPropValues.vapsItemsCmsName, + lineItems: { + glassParts: [], + supportingItems: [], + vaps: [testConstants.parts.frontWiperPart] + }, + damage: { + isRepair: false, + glassToReplace: [testConstants.damages.frontWindshield] + } + }; +} + +let cmsContent; +const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '') + } +}; + +function initializeWithDefault(wrapper) { + wrapper.vm.initializeComponent({ + wipers: [testConstants.parts.frontWiperPart], + rainDefense: testConstants.parts.rainDefensePart + }); +} + +function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn() + } + }); + + methodToRun(); + + mountOptions.data = () => ( + initialData + ); + + mountOptions.mixins = [mockMixin]; + + const wrapper = shallowMount(servicePackageReview, mountOptions); + wrapper.vm.setCmsContent = jest.fn(); + return { wrapper }; +} + +beforeEach(() => { + cmsContent = { + ServicePackageTitle: { + Answers: [ + { + Name: packageNames.TIER_ONE, + Text: '', + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: testConstants.widgetNames.tierOneTitle + }, + { + Name: packageNames.TIER_TWO, + Text: '', + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: testConstants.widgetNames.tierTwoTitle + }, + { + Name: packageNames.TIER_THREE, + Text: '', + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: testConstants.widgetNames.tierThreeTitle + } + ] + }, + DefaultPackageItemDescriptions: { + Answers: [ + { + Name: 'Item1', + Text: testConstants.defaultItemCopy.itemOne, + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: 'Item2', + Text: testConstants.defaultItemCopy.itemTwo, + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: 'Item3', + Text: testConstants.defaultItemCopy.itemThree, + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: '' + } + ] + }, + VapsItemDescriptions: { + Answers: [ + { + Name: partTypeStrings.FRONT_WIPER, + Text: testConstants.vapsCopy.frontWiperCopy, + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: partTypeStrings.REAR_WIPER, + Text: testConstants.vapsCopy.rearWiperCopy, + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: '' + }, + { + Name: partTypeStrings.RAIN_DEFENSE, + Text: testConstants.vapsCopy.rainDefenseCopy, + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: '' + } + ] + } + }; +}); + +describe('Service Package Review Block', () => { + describe('General functionality', () => { + test('Should properly "Round Down" package tier', async () => { + // Slightly longer explanation: + // Should only return the highest tier where *every* offered VAP is part of the order. + // However, there may be vaps not offered in the qualifying tier. Hence rounding *down*. + // + // I.e. Economy=[], Standard=[front wipers], Premium=[front wipers, rain defense]. + // Current vaps=[rain defense]. Though rain defense is in Premium, we don't satisfy it or standard. + // So our tier should still be Economy. + // Should still display extra vaps. + + // Arrange + const props = generateDefaultProps(); + props.lineItems.vaps = [testConstants.parts.rainDefensePart]; + + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + initializeWithDefault(wrapper); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.packageNameWidget).toEqual(testConstants.widgetNames.tierOneTitle); + + const containsRainDefenseCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.rainDefenseCopy); + expect(containsRainDefenseCopy).toBe(true); + }); + + test('Should display all default items from cms', async () => { + // Arrange + cmsContent.DefaultPackageItemDescriptions.Answers.push({ + Name: 'Item4', + Text: testConstants.defaultItemCopy.itemFour, + SubText: '', + ImageId: testConstants.imageId, + Image: '', + SubWidgetName: '' + }); + + const props = generateDefaultProps(); + props.lineItems.vaps = []; + + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + initializeWithDefault(wrapper); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + const expectedResult = [ + testConstants.defaultItemCopy.itemOne, + testConstants.defaultItemCopy.itemTwo, + testConstants.defaultItemCopy.itemThree, + testConstants.defaultItemCopy.itemFour + ]; + + expect(wrapper.vm.displayContent).toEqual(expectedResult); + }); + + test('Should display vaps if and only if they are added', async () => { + // Arrange + const props = generateDefaultProps(); + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + // Act + await wrapper.vm.$nextTick(); + + initializeWithDefault(wrapper); + + // Assert + const includesFrontWiperCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.frontWiperCopy); + const includesRainDefenseCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.rainDefenseCopy); + expect(includesFrontWiperCopy).toBe(true); + expect(includesRainDefenseCopy).toBe(false); + }); + + test('Should not error if cms content is missing (though may display poorly).', async () => { + // Arrange + cmsContent = {}; + const props = generateDefaultProps(); + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + initializeWithDefault(wrapper); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.packageNameWidget).toEqual(''); + expect(wrapper.vm.displayContent).toEqual([]); + }); + }); + + describe('Match Figma Scenarios', () => { + figmaScenarios.forEach((scenario) => { + scenario.iterations.forEach((iteration) => { + it(`Should match figma scenario "${scenario.name}", iteration "${iteration.name}"`, async () => { + // Arrange + const props = generateDefaultProps(); + props.damage = scenario.params.damage; + props.glassParts = scenario.params.glassParts; + props.lineItems.supportingItems = scenario.params.supportingItems; + props.lineItems.vaps = iteration.vapsCombo; + + const { wrapper } = getShallowMountedComponent({ + ...props + }); + + wrapper.vm.initializeComponent({ + wipers: scenario.params.wiperResponse, + rainDefense: scenario.params.rainDefenseResponse + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.packageNameWidget).toEqual(iteration.expected.packageNameWidget); + expect(wrapper.vm.displayContent).toEqual(iteration.expected.displayContent); + }); + }); + }); + }); +}); diff --git a/src/layouts/review-page/review-sections/service-package-review/service-package-review.vue b/src/layouts/review-page/review-sections/service-package-review/service-package-review.vue new file mode 100644 index 00000000..56655945 --- /dev/null +++ b/src/layouts/review-page/review-sections/service-package-review/service-package-review.vue @@ -0,0 +1,141 @@ + + + diff --git a/src/layouts/review-page/review-sections/vehicle-review/vehicle-review.spec.js b/src/layouts/review-page/review-sections/vehicle-review/vehicle-review.spec.js new file mode 100644 index 00000000..fd4a3d64 --- /dev/null +++ b/src/layouts/review-page/review-sections/vehicle-review/vehicle-review.spec.js @@ -0,0 +1,47 @@ +// Components +import vehicleReview from '@/layouts/review-page/review-sections/vehicle-review/vehicle-review.vue'; + +// Supporting Files +import { shallowMount } from '@vue/test-utils'; +import { getMountOptions } from '@/helpers/unit-test-helper.js'; + +jest.mock('@/helpers/cms-content-helper', () => ({ + fetchCmsContentForPage: () => Promise.resolve('content') +})); + +function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn() + } + }); + + methodToRun(); + + mountOptions.data = () => ( + initialData + ); + + const wrapper = shallowMount(vehicleReview, mountOptions); + wrapper.vm.setCmsContent = jest.fn(); + return { wrapper }; +} + +describe('Vehicle Review Block', () => { + test('Correctly assembles vehicle info into a display string', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent({ + vehicle: { + year: '2019', + make: 'Honda', + model: 'Odyssey' + } + }); + + // Act + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayContent).toStrictEqual(['2019 Honda Odyssey']); + }); +}); diff --git a/src/layouts/review-page/review-sections/vehicle-review/vehicle-review.vue b/src/layouts/review-page/review-sections/vehicle-review/vehicle-review.vue new file mode 100644 index 00000000..cfc9ee09 --- /dev/null +++ b/src/layouts/review-page/review-sections/vehicle-review/vehicle-review.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/layouts/schedule-page/helpers/schedule-helper.js b/src/layouts/schedule-page/helpers/schedule-helper.js index eb5657f8..a2e1fae9 100644 --- a/src/layouts/schedule-page/helpers/schedule-helper.js +++ b/src/layouts/schedule-page/helpers/schedule-helper.js @@ -1,72 +1,8 @@ import { useMainStore } from '@/store'; -export async function getAlertReasons(ctu) { +export default async function getAlertReasons(ctu) { const store = useMainStore(); const alertReasons = await store.getAlertReasonsByCtu(ctu); return Promise.resolve(alertReasons); } - -export function calcDaysBetweenDates(dateString1, dateString2) { - const date1 = new Date(dateString1); - const date2 = new Date(dateString2); - const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds - return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days -} - -export function convertDateToDateString(date) { - // returns YYYY-MM-DD format - if (date instanceof Date !== true) return null; - return ( - `${date.getFullYear() - }-${ - (`0${date.getMonth() + 1}`).slice(-2) - }-${ - (`0${date.getDate()}`).slice(-2)}` - ); -} - -export function convertDateStringToDate(dateString) { - // dateString must be YYYY-MM-DD format - if (typeof dateString !== 'string') return null; - const dateParts = dateString.split('-'); - return new Date(dateParts[0], parseInt(dateParts[1], 10) - 1, dateParts[2]); -} - -export function sumDateString(dateString, daysToAdd) { - // dateString must be YYYY-MM-DD format - if (typeof dateString !== 'string') return null; - const date = convertDateStringToDate(dateString); - date.setDate(date.getDate() + daysToAdd); - return convertDateToDateString(date); -} - -export function militaryToTwelveHourTime(timeString) { - // Expected input: "HH:MM" - if (typeof timeString !== 'string') return null; - let hours = parseInt(timeString.split(':')[0], 10); - const minutes = timeString.split(':')[1]; - const meridianNotation = hours > 11 ? 'PM' : 'AM'; - - if (hours > 12) { - hours -= 12; - } - - return `${hours}:${minutes} ${meridianNotation}`; -} - -export function getDisplayTextForDurationLength(durationMinimum, durationMaximum) { - const isLongAppointment = durationMaximum >= 120; - const isDurationRange = durationMinimum !== durationMaximum; - - const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum; - const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum; - - const durationText = isDurationRange - ? `${adjustedMinimum} - ${adjustedMaximum}` - : adjustedMinimum; - - const unitText = isLongAppointment ? 'hours' : 'minutes'; - - return `${durationText} ${unitText}`; -} diff --git a/src/layouts/schedule-page/location-alerts/location-alerts.vue b/src/layouts/schedule-page/location-alerts/location-alerts.vue index 1730320e..f47b3704 100644 --- a/src/layouts/schedule-page/location-alerts/location-alerts.vue +++ b/src/layouts/schedule-page/location-alerts/location-alerts.vue @@ -9,7 +9,7 @@