Merge pull request #1165 from Safelite/feature/kroell/INSR-8907
INSR-8907: recal acknowledgement modal
This commit is contained in:
commit
910aefcd37
8 changed files with 571 additions and 136 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@
|
|||
linkType="text"
|
||||
href="#!"
|
||||
:text="textForToggle"
|
||||
@click="handleClickToggle" />
|
||||
<div
|
||||
@click-event="handleClickToggle" />
|
||||
<button
|
||||
type="button"
|
||||
class="tpa-toggle d-inline-block"
|
||||
:class="[isDetailVisible ? 'active' : '']"
|
||||
@click="handleClickToggle"></div>
|
||||
@click="handleClickToggle"></button>
|
||||
<Transition>
|
||||
<div
|
||||
v-show="isDetailVisible"
|
||||
|
|
@ -60,6 +61,8 @@ export default {
|
|||
.tpa-recal-toggle {
|
||||
.tpa-toggle {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
|
|
|
|||
151
src/layouts/schedule-page/recal-ack-modal/recal-ack-modal.vue
Normal file
151
src/layouts/schedule-page/recal-ack-modal/recal-ack-modal.vue
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div id="recal-ack-modal-container">
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:modalId="modalName"
|
||||
:footerButtonText="modalCloseButtonText"
|
||||
@footerButtonEvent="footerButtonClick"
|
||||
:displayCloseButton="false"
|
||||
:displayCloseLink="true">
|
||||
<div class="form-test-invalid">
|
||||
<h5 class="modal-headline">{{ modalHeadline }}</h5>
|
||||
<div>
|
||||
<div
|
||||
class="mb-4 modal-body-text"
|
||||
v-html="modalBodyText"></div>
|
||||
<recalAckToggle
|
||||
class="mb-3"
|
||||
cmsWidgetName="RecalAckModalToggle" />
|
||||
<checkBox
|
||||
ref="recalAcknowledgement"
|
||||
v-model="isAcknowledged"
|
||||
class="mb-4 mt-4 acknowledgement-checkbox"
|
||||
:class="showAcknowledgementError && ' has-error'"
|
||||
:validationRules="rules.optionRequired"
|
||||
checkboxName="recalAcknowledgement"
|
||||
buttonID="recalAcknowledgement"
|
||||
:tabIndex="0"
|
||||
:checkboxLabel="modalSubBodyText"
|
||||
:screenReaderOnlyText="modalSubBodyText"
|
||||
isRequired />
|
||||
<div
|
||||
v-if="showAcknowledgementError"
|
||||
class="row form-test-error mt-1">
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import recalAckToggle from '@/layouts/schedule-page/recal-ack-modal/recal-ack-toggle/recal-ack-toggle.vue';
|
||||
import checkBox from '@/ux-components/checkbox/checkbox.vue';
|
||||
import globalRules from '@/constants/global-rules';
|
||||
|
||||
export default {
|
||||
name: "recal-ack-modal",
|
||||
components: {
|
||||
modal,
|
||||
checkBox,
|
||||
recalAckToggle
|
||||
},
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
ackError: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isAcknowledged: false,
|
||||
showAcknowledgementError: false,
|
||||
rules: {
|
||||
optionRequired: globalRules.OPTION_REQUIRED
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modalName() {
|
||||
return this.cmsWidgetName;
|
||||
},
|
||||
modalHeadline() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
|
||||
},
|
||||
modalBodyText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'BodyText');
|
||||
},
|
||||
modalSubBodyText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'BodyText2');
|
||||
},
|
||||
modalCloseButtonText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'FooterText');
|
||||
},
|
||||
errorMessage() {
|
||||
if (this.showAcknowledgementError) {
|
||||
return this.ackError;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isAcknowledged() {
|
||||
this.showAcknowledgementError = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.$refs[this.modalName].openModal();
|
||||
},
|
||||
footerButtonClick() {
|
||||
if (this.isAcknowledged) {
|
||||
this.$emit('recalAcknowledged', this.isAcknowledged);
|
||||
this.$refs[this.modalName]?.closeModal();
|
||||
} else {
|
||||
this.showAcknowledgementError = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.modal-headline {
|
||||
font-size: 1.625rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
.modal-body-text {
|
||||
color: #4d4e53;
|
||||
:deep(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
:deep(p:first-of-type) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
.acknowledgement-checkbox {
|
||||
:deep(p) {
|
||||
font-size: 1rem;
|
||||
color: #4d4e53;
|
||||
}
|
||||
:deep(.form-check-input) {
|
||||
&:checked + label p {
|
||||
font-size: 1rem;
|
||||
color: #4d4e53;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.modal) {
|
||||
.modal-footer {
|
||||
button {
|
||||
background-color: $green;
|
||||
color: $white;
|
||||
font-weight: $font-weight-bold;
|
||||
&:hover, &:focus {
|
||||
background-color: $green;
|
||||
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<div class="recal-ack-toggle">
|
||||
<textLink
|
||||
id="moreDetails"
|
||||
class="d-inline-block"
|
||||
linkType="text"
|
||||
href="#!"
|
||||
:text="textForToggle"
|
||||
:preventDefaultOnClick="true"
|
||||
@click-event="handleClickToggle" />
|
||||
<button
|
||||
type="button"
|
||||
class="recal-ack-toggle d-inline-block"
|
||||
:class="[isDetailVisible ? 'active' : '']"
|
||||
@click="handleClickToggle"></button>
|
||||
<Transition>
|
||||
<div
|
||||
v-show="isDetailVisible"
|
||||
id="recal-ack-detail-wrapper"
|
||||
class="mt-2">
|
||||
<div
|
||||
id="recal-ack-detail-content"
|
||||
class="mb-2"
|
||||
v-html="detailContent" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import textLink from '@/ux-components/text-link/text-link.vue';
|
||||
|
||||
export default {
|
||||
name: 'recal-ack-toggle',
|
||||
components: {
|
||||
textLink
|
||||
},
|
||||
props: {
|
||||
cmsWidgetName: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDetailVisible: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textForToggle() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
|
||||
},
|
||||
detailContent() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'BodyText');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickToggle() {
|
||||
this.isDetailVisible = !this.isDetailVisible;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/ux-variables-svg-strings.scss";
|
||||
.recal-ack-toggle {
|
||||
.recal-ack-toggle {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
transition: all 0.5s ease;
|
||||
background-image: url($svg-tpa-recal-modal-toggle);
|
||||
background-repeat: no-repeat;
|
||||
margin-left: 0.5rem;
|
||||
width: 16px;
|
||||
height: 9px;
|
||||
display: inline-block;
|
||||
}
|
||||
&.active::after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
a {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
#recal-ack-detail-wrapper {
|
||||
// START - Vue Transition
|
||||
&.v-enter-active,
|
||||
&.v-leave-active {
|
||||
transition: opacity 250ms ease-in;
|
||||
}
|
||||
|
||||
&.v-enter-from,
|
||||
&.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
// END - Vue Transition
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
#recal-ack-detail-content {
|
||||
font-size: 1rem;
|
||||
p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
color: #4d4e53;
|
||||
}
|
||||
ol {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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: '<div></div>',
|
||||
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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@
|
|||
:ref="SUGGEST_TIMESLOT_MODAL_REF_NAME"
|
||||
cmsWidgetName="SuggestTimeslotModal"
|
||||
@confirmAppointmentClicked="autoSelectTimeslotConfirmed" />
|
||||
<recalAckModal
|
||||
:ref="RECAL_ACK_MODAL_REF_NAME"
|
||||
cmsWidgetName="RecalAckModalWidget"
|
||||
:ackError="ackError"
|
||||
@recalAcknowledged="updateIsRecalAcknowledged"/>
|
||||
</Form>
|
||||
</template>
|
||||
<script>
|
||||
|
|
@ -75,6 +80,7 @@ import datePicker from '@/digital-components/date-picker/date-picker.vue';
|
|||
import serviceLocation from '@/layouts/schedule-page/service-location/service-location.vue';
|
||||
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
|
||||
import suggestTimeslotModal from '@/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue';
|
||||
import recalAckModal from '@/layouts/schedule-page/recal-ack-modal/recal-ack-modal.vue';
|
||||
|
||||
// Supporting files
|
||||
import { experimentSettings, experimentTest, experimentVariation, experimentUniverses } from '@/constants/experiments';
|
||||
|
|
@ -105,10 +111,13 @@ import {
|
|||
import { Form } from 'vee-validate';
|
||||
import BaseFormMixin from '@/mixins/base-form-mixin.js';
|
||||
import { useMainStore } from '@/store';
|
||||
import { containsRecalParts, anyPartWithRequiresRecalFlag } from '@/helpers/recal-helper';
|
||||
import errorMessages from '@/constants/error-messages';
|
||||
|
||||
// Define constants
|
||||
const SUGGEST_TIMESLOT_MODAL_REF_NAME = 'SuggestTimeslotModal';
|
||||
const TIME_SLOTS_CALL_DAYS_LIMIT = 15;
|
||||
const RECAL_ACK_MODAL_REF_NAME = 'recalAckModal';
|
||||
|
||||
const getAvailableDates = async (
|
||||
startDateString,
|
||||
|
|
@ -230,7 +239,8 @@ export default {
|
|||
serviceLocation,
|
||||
siteFooter,
|
||||
suggestTimeslotModal,
|
||||
Form
|
||||
Form,
|
||||
recalAckModal
|
||||
},
|
||||
mixins: [BaseFormMixin],
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -370,7 +380,9 @@ export default {
|
|||
selectedServiceLocationCity: this.getServiceLocationCity(),
|
||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||
showDatePickerError: false,
|
||||
SUGGEST_TIMESLOT_MODAL_REF_NAME
|
||||
SUGGEST_TIMESLOT_MODAL_REF_NAME,
|
||||
RECAL_ACK_MODAL_REF_NAME,
|
||||
isRecalAcknowledged: this.getIsRecalAcknowledgedForScheduling()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -412,6 +424,21 @@ export default {
|
|||
},
|
||||
supportingItems() {
|
||||
return useMainStore().lineItems.supportingItems;
|
||||
},
|
||||
displayRecalAckModal() {
|
||||
// if any of the glass parts contain an item with the requiresRecalibration flag set to true
|
||||
// and the order does not contain a recalibration part, show the recalibration acknowledgement modal
|
||||
const { glassParts } = useMainStore().order.lineItems;
|
||||
const containsRecalPart = containsRecalParts(glassParts);
|
||||
const hasPartWithRecalRequirement = anyPartWithRequiresRecalFlag(glassParts);
|
||||
|
||||
if (!containsRecalPart && hasPartWithRecalRequirement) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
ackError() {
|
||||
return errorMessages.ACKNOWLEDGEMENT_REQUIRED;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -440,6 +467,8 @@ export default {
|
|||
this.mql = window.matchMedia('(min-width: 1200px)');
|
||||
this.isMobileView = !this.mql.matches;
|
||||
this.mql.addEventListener('change', this.handleMqlChange);
|
||||
this.mainStore.order.isRecalAcknowledgedForScheduling = false;
|
||||
this.isRecalAcknowledged = false;
|
||||
},
|
||||
unmounted() {
|
||||
if (this.mql) {
|
||||
|
|
@ -931,6 +960,12 @@ export default {
|
|||
this.showDatePickerError = false;
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
if (this.displayRecalAckModal && this.isRecalAcknowledged === false) {
|
||||
showIssLoadingModal(false);
|
||||
this.$refs[RECAL_ACK_MODAL_REF_NAME].openModal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isFormValid) {
|
||||
this.showDatePickerError = true;
|
||||
return;
|
||||
|
|
@ -947,7 +982,14 @@ export default {
|
|||
this.navigationScenarios.CLICKED_FORWARD,
|
||||
this.$route
|
||||
);
|
||||
}
|
||||
},
|
||||
updateIsRecalAcknowledged(isAcknowledged) {
|
||||
this.isRecalAcknowledged = isAcknowledged;
|
||||
this.mainStore.order.isRecalAcknowledgedForScheduling = isAcknowledged;
|
||||
},
|
||||
getIsRecalAcknowledgedForScheduling() {
|
||||
return this.mainStore.order.isRecalAcknowledgedForScheduling;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue