diff --git a/src/helpers/recal-helper.js b/src/helpers/recal-helper.js index c6cce97e..f364bf68 100644 --- a/src/helpers/recal-helper.js +++ b/src/helpers/recal-helper.js @@ -73,3 +73,22 @@ export function containsRecalParts(lineItems) { } } +export function anyPartWithRequiresRecalFlag(lineItems) { + if (!lineItems) { + return false; + } + + if (Array.isArray(lineItems)) { + return lineItems.some((li) => li.requiresRecalibration === true); + } + + const flattened = [ + ...(lineItems.glassParts ?? []), + ...(lineItems.supportingItems ?? []), + ...(lineItems.otherParts ?? []), + ...(lineItems.feeItems ?? []), + ...(lineItems.vaps ?? []), + ]; + + return flattened.some((li) => li.requiresRecalibration === true); +} diff --git a/src/layouts/coverage-statement/coverage-statement.spec.js b/src/layouts/coverage-statement/coverage-statement.spec.js index 2e33cf20..5c056587 100644 --- a/src/layouts/coverage-statement/coverage-statement.spec.js +++ b/src/layouts/coverage-statement/coverage-statement.spec.js @@ -14,6 +14,7 @@ import { useMainStore } from '@/store'; import { getPriceOfLineItems } from '@/helpers/price-calculator.js'; import coverageStatuses from '@/constants/coverage-statuses'; import coverageType from '@/constants/coverage-type'; +import { containsRecalParts } from '@/helpers/recal-helper'; jest.mock('@/helpers/layout-helper.js', () => jest.fn()); jest.mock('@/helpers/price-calculator.js', () => ({ @@ -36,8 +37,6 @@ jest.mock('@/helpers/recal-helper.js', () => ({ containsRecalParts: jest.fn() })); -import { containsRecalParts } from '@/helpers/recal-helper.js'; - const SAFELITE_PROVIDER = 'Safelite'; const CANCEL_CLAIM_REF_NAME = 'CancelClaimModal'; @@ -344,7 +343,7 @@ describe('coverageStatement.vue', () => { // Assert expect(result).toBeFalsy(); }); - test('returns true when a part in glassParts require recalibration and recalibration has been added to order', () => { + test('returns true when a part in glassParts requires recalibration and recalibration has been added to order', () => { // Arrange containsRecalParts.mockReturnValue(true); const mainInitialState = { diff --git a/src/layouts/provider-preference/tpa-recal-modal/tpa-recal-toggle/tpa-recal-toggle.vue b/src/layouts/provider-preference/tpa-recal-modal/tpa-recal-toggle/tpa-recal-toggle.vue index 53b2d204..2c64ba69 100644 --- a/src/layouts/provider-preference/tpa-recal-modal/tpa-recal-toggle/tpa-recal-toggle.vue +++ b/src/layouts/provider-preference/tpa-recal-modal/tpa-recal-toggle/tpa-recal-toggle.vue @@ -6,11 +6,12 @@ linkType="text" href="#!" :text="textForToggle" - @click="handleClickToggle" /> -
+
+ @click="handleClickToggle">
+
+ +
+ +
+ + + +
+

{{ errorMessage }}

+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/layouts/schedule-page/recal-ack-modal/recal-ack-toggle/recal-ack-toggle.vue b/src/layouts/schedule-page/recal-ack-modal/recal-ack-toggle/recal-ack-toggle.vue new file mode 100644 index 00000000..c4525695 --- /dev/null +++ b/src/layouts/schedule-page/recal-ack-modal/recal-ack-toggle/recal-ack-toggle.vue @@ -0,0 +1,115 @@ + + + diff --git a/src/layouts/schedule-page/schedule-page.spec.js b/src/layouts/schedule-page/schedule-page.spec.js index a418f35e..fefde032 100644 --- a/src/layouts/schedule-page/schedule-page.spec.js +++ b/src/layouts/schedule-page/schedule-page.spec.js @@ -12,6 +12,8 @@ jest.mock('@/helpers/cms-content-helper', () => ({ fetchCmsContentForPage: jest.fn() })); +const RECAL_ACK_MODAL_REF_NAME = 'recalAckModal'; + const mockMixin = { methods: { getCmsContent: jest.fn().mockImplementation(() => ''), @@ -55,6 +57,22 @@ const loadingModalStub = { } }; +const serviceLocationStub = { + render: () => {}, + methods: { + forwardButtonAction: jest.fn(), + initializeComponent: jest.fn() + } +}; + +const recalAckModalStub = { + template: '
', + methods: { + openModal: jest.fn(), + footerButtonClick: jest.fn() + } +}; + function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { const mountOptions = getMountOptions({ router: { @@ -64,7 +82,9 @@ function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { mountOptions.global.stubs = { siteFooter: footerStub, - loadingModal: loadingModalStub + loadingModal: loadingModalStub, + serviceLocation: serviceLocationStub, + recalAckModal: recalAckModalStub }; methodToRun(); @@ -75,6 +95,27 @@ function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { ); const wrapper = shallowMount(schedule, mountOptions); + + // Manually create the ref for recalAckModal since shallowMount doesn't populate it + Object.defineProperty(wrapper.vm.$refs, RECAL_ACK_MODAL_REF_NAME, { + value: { + openModal: jest.fn(), + footerButtonClick: jest.fn() + }, + writable: false, + configurable: true + }); + + // Manually create the ref for serviceLocation since shallowMount doesn't populate it + Object.defineProperty(wrapper.vm.$refs, 'serviceLocation', { + value: { + forwardButtonAction: jest.fn(), + initializeComponent: jest.fn() + }, + writable: false, + configurable: true + }); + return { wrapper }; } @@ -126,152 +167,163 @@ afterEach(() => { jest.clearAllMocks(); }); -describe('schedule-page.vue', () => { - beforeAll(() => { - Object.defineProperty(window, 'matchMedia', { value: jest.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn() - })), - writable: true }); +beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn() + })), + writable: true }); +}); +describe('Initial Load', () => { + test('Should pass arePagePrerequisitesValid with a serviceLocation zipcode [in beforeEach]', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + + // Act + const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(arePagePrerequisitesValid).toBe(true); }); - describe('Initial Load', () => { - test('Should pass arePagePrerequisitesValid with a serviceLocation zipcode [in beforeEach]', () => { - // Arrange - const { wrapper } = getShallowMountedComponent(); + test('Should fail arePagePrerequisitesValid with no serviceLocation zipcode', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.order.serviceLocation.zipCode = null; - // Act - const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + // Act + const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); - // Assert - expect(arePagePrerequisitesValid).toBe(true); - }); - test('Should fail arePagePrerequisitesValid with no serviceLocation zipcode', () => { - // Arrange - const { wrapper } = getShallowMountedComponent(); - wrapper.vm.mainStore.order.serviceLocation.zipCode = null; - - // Act - const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); - - // Assert - expect(arePagePrerequisitesValid).toBeFalsy(); - }); - test('should return newShopTimeSlots when getAvailableDatesMethod is called', async () => { - // Arrange - const { wrapper } = getShallowMountedComponent(); - wrapper.vm.selectableDatesData = { - days: [] - }; - const store = useMainStore(); - store.getShopTimeSlots.mockImplementation(() => Promise.resolve({ - data: { - estimatedServiceMinutesMinimum: 90, - estimatedServiceMinutesMaximum: 120, - days: [ - { - date: '2023-12-01', - timeSlots: [ - { - id: '06747-01820-S-B*20424*7 AM', - startTime: '07:00', - endTime: '08:00', - offerPremium: false - } - ] - } - ] - } - })); - - // Act - const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod( - '2023-01-01', - '2023-01-15' - ); - - // Assert - expect(newShopTimeSlots).toStrictEqual({ + // Assert + expect(arePagePrerequisitesValid).toBeFalsy(); + }); + test('should return newShopTimeSlots when getAvailableDatesMethod is called', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.selectableDatesData = { + days: [] + }; + const store = useMainStore(); + store.getShopTimeSlots.mockImplementation(() => Promise.resolve({ + data: { + estimatedServiceMinutesMinimum: 90, + estimatedServiceMinutesMaximum: 120, days: [ { date: '2023-12-01', timeSlots: [ { - endTime: '08:00', id: '06747-01820-S-B*20424*7 AM', - offerPremium: false, - startTime: '07:00' + startTime: '07:00', + endTime: '08:00', + offerPremium: false } ] } - ], - estimatedServiceMinutesMinimum: 90, - estimatedServiceMinutesMaximum: 120 - }); - }); - test('Should call API service in days of 15 or less when getAvailableDatesMethod is called with large date ranges', async () => { - // Arrange - const { wrapper } = getShallowMountedComponent(); - wrapper.vm.selectableDatesData = { - days: [] - }; - const store = useMainStore(); - store.getShopTimeSlots.mockImplementation(() => Promise.resolve({ - data: { - estimatedServiceMinutesMinimum: 90, - estimatedServiceMinutesMaximum: 120, - days: [] + ] + } + })); + + // Act + const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod( + '2023-01-01', + '2023-01-15' + ); + + // Assert + expect(newShopTimeSlots).toStrictEqual({ + days: [ + { + date: '2023-12-01', + timeSlots: [ + { + endTime: '08:00', + id: '06747-01820-S-B*20424*7 AM', + offerPremium: false, + startTime: '07:00' + } + ] } - })); - - // Act - await wrapper.vm.getAvailableDates.call( - wrapper.vm, - '2023-01-01', - '2023-03-31', - 'Inshop', - '123', - '43228' - ); - - // Assert - // 2023-01-01 --> 2023-02-05 - // 2023-02-06 --> 2023-03-12 - // 2023-03-13 --> 2023-03-31 - expect(store.getShopTimeSlots).toHaveBeenCalledTimes(6); + ], + estimatedServiceMinutesMinimum: 90, + estimatedServiceMinutesMaximum: 120 }); }); - describe('Rendering', () => { - test('Schedule page loads', () => { - // Arrange - const { wrapper } = getShallowMountedComponent(); - - // Assert - expect(wrapper).toBeTruthy(); - }); - }); - describe('schedule page methods...', () => { - test('getServiceZipCtuCodeFromStore should return zipCodeCtu', () => { - // Arrange - const { wrapper } = getShallowMountedComponent(); - wrapper.vm.selectableDatesData = { + test('Should call API service in days of 15 or less when getAvailableDatesMethod is called with large date ranges', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.selectableDatesData = { + days: [] + }; + const store = useMainStore(); + store.getShopTimeSlots.mockImplementation(() => Promise.resolve({ + data: { + estimatedServiceMinutesMinimum: 90, + estimatedServiceMinutesMaximum: 120, days: [] - }; + } + })); - // Act - const testValue = wrapper.vm.getServiceZipCtuCodeFromStore(); + // Act + await wrapper.vm.getAvailableDates.call( + wrapper.vm, + '2023-01-01', + '2023-03-31', + 'Inshop', + '123', + '43228' + ); - // Assert - expect(testValue).toStrictEqual('01234'); - }); + // Assert + // 2023-01-01 --> 2023-02-05 + // 2023-02-06 --> 2023-03-12 + // 2023-03-13 --> 2023-03-31 + expect(store.getShopTimeSlots).toHaveBeenCalledTimes(6); }); - test.skip('forwardButtonAction should call route method navigateWithoutSaving', async () => { +}); +describe('Rendering', () => { + test('Schedule page loads', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + + // Assert + expect(wrapper).toBeTruthy(); + }); +}); +describe('schedule page methods...', () => { + test('getServiceZipCtuCodeFromStore should return zipCodeCtu', () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.selectableDatesData = { + days: [] + }; + + // Act + const testValue = wrapper.vm.getServiceZipCtuCodeFromStore(); + + // Assert + expect(testValue).toStrictEqual('01234'); + }); + test('forwardButtonAction should call route method navigate', async () => { // Arrange const { wrapper } = getShallowMountedComponent(); wrapper.vm.$router.navigate = jest.fn(() => ({})); + wrapper.vm.mainStore.saveSchedule = jest.fn(); + wrapper.vm.navigationScenarios = { + CLICKED_FORWARD: 'CLICKED_FORWARD' + }; + + // Mock the serviceLocation ref using Object.defineProperty to bypass readonly + Object.defineProperty(wrapper.vm.$refs, 'serviceLocation', { + value: { + forwardButtonAction: jest.fn() + }, + configurable: true + }); + wrapper.vm.selectedTimeSlotInfo = { timeSlot: { routeCode: 'test-id' @@ -281,6 +333,59 @@ describe('schedule-page.vue', () => { // Act await wrapper.vm.forwardButtonAction(); + // Assert + expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); + }); + test('recalAckModal should open when part requires recal, but recal not added to order', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.lineItems.glassParts = [ + { + partNumber: 'TEST123', + requiresRecalibration: true + } + ]; + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.vm.$refs[RECAL_ACK_MODAL_REF_NAME].openModal).toHaveBeenCalled(); + }); + test('navigation forward is blocked when recalAckModal is displayed, but checkbox has not been acknowledged', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.lineItems.glassParts = [ + { + partNumber: 'TEST123', + requiresRecalibration: true + } + ]; + + // Act + await wrapper.vm.forwardButtonAction(); + await wrapper.vm.$refs[RECAL_ACK_MODAL_REF_NAME].openModal(); + await wrapper.vm.$refs[RECAL_ACK_MODAL_REF_NAME].footerButtonClick(); + + // Assert + expect(wrapper.vm.$router.navigate).not.toHaveBeenCalled(); + }); + test('navigation forward is allowed when checkbox has been acknowledged on recalAckModal', async () => { + // Arrange + const { wrapper } = getShallowMountedComponent(); + wrapper.vm.mainStore.lineItems.glassParts = [ + { + partNumber: 'TEST123', + requiresRecalibration: true + } + ]; + + // Act + await wrapper.vm.$refs[RECAL_ACK_MODAL_REF_NAME].openModal(); + wrapper.vm.updateIsRecalAcknowledged(true); + await wrapper.vm.$refs[RECAL_ACK_MODAL_REF_NAME].footerButtonClick(); + await wrapper.vm.forwardButtonAction(); + // Assert expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); }); diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index 2e65643b..904329f3 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -64,6 +64,11 @@ :ref="SUGGEST_TIMESLOT_MODAL_REF_NAME" cmsWidgetName="SuggestTimeslotModal" @confirmAppointmentClicked="autoSelectTimeslotConfirmed" /> + diff --git a/src/store/index.js b/src/store/index.js index 12dc746f..2aa8248a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -225,7 +225,8 @@ export const getDefaultState = () => ({ loadedFromDupeCheck: null, loadedSessionClearedPreviousData: null, availableVaps: null, - visitedDuplicateCheckPage: false + visitedDuplicateCheckPage: false, + isRecalAcknowledgedForScheduling: false }, applicationUser: { experiments: [], @@ -3163,7 +3164,7 @@ export const useMainStore = defineStore({ resetSubmittedOrder() { // clear from sessionStorage window.sessionStorage.removeItem(webStorageConstants.SUBMITTED_ORDER); - } + }, }, persist: { // Default is localStorage, but sessionStorage is used here to ensure state is cleared when the browser tab is closed, preventing potential issues with stale data on return visits.