Merge pull request #545 from Safelite/feature/digital/SSR-1064
Feature/digital/ssr 1064
This commit is contained in:
commit
3d1b73c171
16 changed files with 653 additions and 33 deletions
9
src/constants/payment-method-constants.js
Normal file
9
src/constants/payment-method-constants.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const paymentMethods = Object.freeze({
|
||||
NONE: null,
|
||||
PAY_AT_TIME_OF_SERVICE: 'PayAtTimeOfService',
|
||||
CREDIT_CARD: 'CreditCard',
|
||||
PAYPAL: 'Paypal',
|
||||
AFTERPAY: 'Afterpay'
|
||||
});
|
||||
|
||||
export default paymentMethods;
|
||||
|
|
@ -3,7 +3,13 @@
|
|||
<footer
|
||||
id="infoBox"
|
||||
class="footer container-fluid g-5 my-5 px-0">
|
||||
<div class="row d-flex flex-row-reverse align-items-center vw-100 mx-0">
|
||||
<div
|
||||
class="row d-flex vw-100 mx-0"
|
||||
:class="[
|
||||
isStackedVertically
|
||||
? 'flex-column align-items-stretch'
|
||||
: 'flex-row-reverse align-items-center'
|
||||
]">
|
||||
<div
|
||||
id="stacked"
|
||||
class="col button-col d-flex px-0">
|
||||
|
|
@ -61,7 +67,8 @@ export default {
|
|||
isForwardActionDisabled: Boolean,
|
||||
isBackButtonHidden: { type: Boolean, default: false },
|
||||
isForwardButtonHidden: { type: Boolean, default: false },
|
||||
cmsWidgetName: String
|
||||
cmsWidgetName: String,
|
||||
isStackedVertically: { type: Boolean, default: false }
|
||||
},
|
||||
emits: ['backClicked', 'forwardClicked'],
|
||||
data() {
|
||||
|
|
@ -161,6 +168,17 @@ export default {
|
|||
& > .container-fluid {
|
||||
overflow-x: visible; // needed to fix hidden footer on some iphones
|
||||
}
|
||||
div.flex-column {
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
div.link-col {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.footerImage
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { getMountOptions } from '@/helpers/unit-test-helper.js';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
import paymentMethodListButton from '@/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button.vue';
|
||||
|
||||
const testConstants = {
|
||||
images: {
|
||||
A: 'imageA',
|
||||
B: 'imageB',
|
||||
NONE: null
|
||||
},
|
||||
names: {
|
||||
A: 'nameA',
|
||||
B: 'nameB'
|
||||
},
|
||||
content: {
|
||||
withInline: 'Button text with {custom:inlineImage} inline.',
|
||||
noInline: 'Button text with no inline'
|
||||
}
|
||||
};
|
||||
|
||||
let cmsContent;
|
||||
|
||||
function generateDefaultProps() {
|
||||
return {
|
||||
buttonLabel: testConstants.content.noInline,
|
||||
altText: testConstants.content.noInline,
|
||||
groupName: 'payment-method',
|
||||
value: testConstants.names.A
|
||||
};
|
||||
}
|
||||
|
||||
function setupMocks(initialData = {}) {
|
||||
const mountOptions = getMountOptions({
|
||||
router: {
|
||||
navigate: jest.fn()
|
||||
}
|
||||
});
|
||||
|
||||
mountOptions.data = () => (
|
||||
initialData
|
||||
);
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
|
||||
}
|
||||
};
|
||||
|
||||
mountOptions.global.mixins = [mockMixin];
|
||||
|
||||
const wrapper = shallowMount(paymentMethodListButton, mountOptions);
|
||||
wrapper.vm.setCmsContent = jest.fn();
|
||||
return { wrapper };
|
||||
}
|
||||
|
||||
describe('Payment Method Question', () => {
|
||||
beforeEach(() => {
|
||||
cmsContent = {};
|
||||
});
|
||||
|
||||
describe('Side image', () => {
|
||||
it('Renders side image if image is present and not inline', () => {
|
||||
// Arrange
|
||||
const props = generateDefaultProps();
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
hasImage: true
|
||||
});
|
||||
|
||||
// Act
|
||||
const showSideImage = wrapper.vm.shouldDisplaySideImage;
|
||||
|
||||
expect(showSideImage).toBe(true);
|
||||
});
|
||||
|
||||
it('Does not render side image if no image is present', () => {
|
||||
// Arrange
|
||||
const props = generateDefaultProps();
|
||||
|
||||
props.buttonImage = testConstants.images.NONE;
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props
|
||||
});
|
||||
|
||||
// Act
|
||||
const showSideImage = wrapper.vm.shouldDisplaySideImage;
|
||||
|
||||
expect(showSideImage).toBe(false);
|
||||
});
|
||||
|
||||
it('Does not render side image if inline', () => {
|
||||
// Arrange
|
||||
const props = generateDefaultProps();
|
||||
|
||||
props.buttonLabel = testConstants.content.withInline;
|
||||
props.altText = testConstants.content.withInline;
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props
|
||||
});
|
||||
|
||||
// Act
|
||||
const showSideImage = wrapper.vm.shouldDisplaySideImage;
|
||||
|
||||
expect(showSideImage).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
v-model="selectedValue"
|
||||
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2">
|
||||
<div class="button-content list-button-content d-flex flex-row py-3 px-4">
|
||||
<img
|
||||
v-if="shouldDisplaySideImage"
|
||||
:id="buttonImageId"
|
||||
class="ms-auto order-3"
|
||||
:src="buttonImage"
|
||||
:alt="altText" />
|
||||
<div class="order-2">
|
||||
<p class="m-0">
|
||||
<span
|
||||
v-for="token in tokens"
|
||||
:key="token">
|
||||
<span v-if="isInlineImageToken(token)">
|
||||
<img
|
||||
v-if="hasImage"
|
||||
:src="buttonImage"
|
||||
:alt="getInlineAltText(token)" />
|
||||
<span v-else> {{ getInlineAltText(token) }}</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ token }}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from '@/digital-components/base-input-button/base-input-button.vue';
|
||||
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin.js';
|
||||
import { splitCopyOnCMSPlaceHolder } from '@/helpers/cms-content-helper.js';
|
||||
|
||||
const INLINE_IMAGE_TOKEN = 'custom:inlineImage';
|
||||
|
||||
function isInlineImageToken(token) {
|
||||
return token.includes(INLINE_IMAGE_TOKEN);
|
||||
}
|
||||
|
||||
function getInlineAltText(token) {
|
||||
const innerTokens = token.split(',');
|
||||
|
||||
return innerTokens[1] ?? '';
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'payment-method-list-button',
|
||||
components: {
|
||||
baseInputButton
|
||||
},
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
computed: {
|
||||
tokens() {
|
||||
return splitCopyOnCMSPlaceHolder(this.buttonLabel ?? '');
|
||||
},
|
||||
hasImage() {
|
||||
return !!this.buttonImage;
|
||||
},
|
||||
shouldDisplaySideImage() {
|
||||
return this.hasImage && this.tokens.every((token) => !isInlineImageToken(token));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isInlineImageToken,
|
||||
getInlineAltText
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: static; //override bootstrap
|
||||
|
||||
&:focus-visible + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
}
|
||||
&:checked:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content p,
|
||||
&:checked + .list-button-content span {
|
||||
font-weight: 500;
|
||||
}
|
||||
&:checked + .list-button-content span:nth-child(2) {
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-button-content {
|
||||
color: $gray-600;
|
||||
position: relative;
|
||||
background: $white;
|
||||
transition: all 150ms linear;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 1px solid $gray-500;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
span {
|
||||
&.small {
|
||||
font-size: $font-size-xsm;
|
||||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { getMountOptions } from '@/helpers/unit-test-helper.js';
|
||||
import widgetFields from '@/constants/cms-widget-fields.js';
|
||||
import paymentMethodQuestion from '@/layouts/payment-method/payment-method-question/payment-method-question.vue';
|
||||
import baseMixin from '@/mixins/base-mixin';
|
||||
|
||||
let cmsContent;
|
||||
|
||||
const mockCmsContent = {
|
||||
[widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT]: 'Choose a payment option',
|
||||
[widgetFields.INPUT_QUESTION_WIDGET.ANSWERS]: [
|
||||
{
|
||||
Name: 'NameA',
|
||||
Text: 'TextA',
|
||||
SubText: 'SubTextA',
|
||||
ImageId: 'idA',
|
||||
AnswerImageUrl: 'imageA',
|
||||
SubWidgetName: 'subWidgetA'
|
||||
},
|
||||
{
|
||||
Name: 'NameB',
|
||||
Text: 'TextB',
|
||||
SubText: 'SubTextB',
|
||||
ImageId: 'idB',
|
||||
AnswerImageUrl: 'imageB',
|
||||
SubWidgetName: 'subWidgetB'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function generateDefaultProps() {
|
||||
return {
|
||||
modelValue: 'modelValue',
|
||||
cmsWidgetName: 'PaymentMethodWidget'
|
||||
};
|
||||
}
|
||||
|
||||
function setupMocks(customMountOptions) {
|
||||
const mountOptions = getMountOptions(customMountOptions);
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
...baseMixin.methods,
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent[cmsFieldName])
|
||||
}
|
||||
};
|
||||
|
||||
mountOptions.global.mixins = [mockMixin];
|
||||
|
||||
const wrapper = shallowMount(paymentMethodQuestion, mountOptions);
|
||||
wrapper.vm.setCmsContent = jest.fn();
|
||||
return { wrapper };
|
||||
}
|
||||
|
||||
describe('Payment Method Question', () => {
|
||||
beforeEach(() => {
|
||||
cmsContent = mockCmsContent;
|
||||
});
|
||||
it('Should map cms content correctly', () => {
|
||||
// Arrange
|
||||
const props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props
|
||||
});
|
||||
|
||||
// Act
|
||||
const mappedContent = wrapper.vm.paymentMethodAnswerData;
|
||||
|
||||
// Assert
|
||||
expect(mappedContent).toEqual([
|
||||
{
|
||||
buttonLabel: cmsContent.Answers[0].Text,
|
||||
altText: cmsContent.Answers[0].Text,
|
||||
groupName: 'payment-method',
|
||||
value: cmsContent.Answers[0].Name,
|
||||
buttonImage: cmsContent.Answers[0].AnswerImageUrl
|
||||
},
|
||||
{
|
||||
buttonLabel: cmsContent.Answers[1].Text,
|
||||
altText: cmsContent.Answers[1].Text,
|
||||
groupName: 'payment-method',
|
||||
value: cmsContent.Answers[1].Name,
|
||||
buttonImage: cmsContent.Answers[1].AnswerImageUrl
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('Handles null cms content gracefully', () => {
|
||||
// Arrange
|
||||
cmsContent = {};
|
||||
const props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props
|
||||
});
|
||||
|
||||
// Act
|
||||
const mappedContent = wrapper.vm.paymentMethodAnswerData;
|
||||
|
||||
// Assert
|
||||
expect(mappedContent).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div class="payment-method-question">
|
||||
<buttonQuestion
|
||||
v-model="selectedMethod"
|
||||
groupName="payment-method"
|
||||
buttonTypeString="payment-method-list-button"
|
||||
:buttonTypeObject="paymentMethodListButton"
|
||||
isWide
|
||||
:questionText="paymentMethodQuestionText"
|
||||
:answers="paymentMethodAnswerData"
|
||||
isRequired
|
||||
:validationRules="validationRules"
|
||||
textPosition="text-start" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
|
||||
import widgetFields from '@/constants/cms-widget-fields.js';
|
||||
import paymentMethodListButton from './payment-method-list-button/payment-method-list-button.vue';
|
||||
|
||||
export default {
|
||||
name: 'payment-method-question',
|
||||
components: {
|
||||
buttonQuestion
|
||||
},
|
||||
props: {
|
||||
modelValue: String,
|
||||
validationRules: String,
|
||||
cmsWidgetName: String
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
paymentMethodListButton
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedMethod: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(selectedMethod) {
|
||||
this.$emit('update:modelValue', selectedMethod);
|
||||
}
|
||||
},
|
||||
paymentMethodQuestionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT);
|
||||
},
|
||||
paymentMethodAnswerData() {
|
||||
const cmsData = this.getInputQuestionWidgetAnswersNullSafe(this.cmsWidgetName);
|
||||
|
||||
return cmsData.map((answer) => ({
|
||||
buttonLabel: answer.Text,
|
||||
altText: answer.Text,
|
||||
groupName: 'payment-method',
|
||||
value: answer.Name,
|
||||
buttonImage: answer.AnswerImageUrl
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.payment-method-question {
|
||||
:deep(.question-text span) {
|
||||
text-align: left;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
src/layouts/payment-method/payment-method.spec.js
Normal file
43
src/layouts/payment-method/payment-method.spec.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Components
|
||||
import paymentMethod from '@/layouts/payment-method/payment-method.vue';
|
||||
|
||||
// Supporting Files
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { getMountOptions } from '@/helpers/unit-test-helper.js';
|
||||
import { useMainStore } from '@/store';
|
||||
import paymentMethods from '@/constants/payment-method-constants';
|
||||
|
||||
function setupMocks() {
|
||||
const mountOptions = getMountOptions();
|
||||
|
||||
const wrapper = shallowMount(paymentMethod, mountOptions);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
||||
describe('payment-method.vue', () => {
|
||||
test('getting payment method when method is pay later', async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks();
|
||||
const payLaterPaymentMethod = paymentMethods.PAY_AT_TIME_OF_SERVICE;
|
||||
useMainStore().savePaymentMethodChoice(payLaterPaymentMethod);
|
||||
|
||||
// Act
|
||||
const paymethod = wrapper.vm.getPaymentMethodFromStore();
|
||||
|
||||
// Assert
|
||||
expect(paymethod).toBe(payLaterPaymentMethod);
|
||||
});
|
||||
test('getting payment method when method is pay in advance', async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks();
|
||||
const payInAdvancePaymentMethod = paymentMethods.CREDIT_CARD;
|
||||
useMainStore().savePaymentMethodChoice(payInAdvancePaymentMethod);
|
||||
|
||||
// Act
|
||||
const paymethod = wrapper.vm.getPaymentMethodFromStore();
|
||||
|
||||
// Assert
|
||||
expect(paymethod).not.toBe(payInAdvancePaymentMethod);
|
||||
});
|
||||
});
|
||||
|
|
@ -21,14 +21,17 @@
|
|||
<div>Cart Placeholder</div>
|
||||
<hr class="my-5" />
|
||||
<div>Pia Alert Placeholder</div>
|
||||
<div>Payment Method Question</div>
|
||||
<paymentMethodQuestion
|
||||
v-model="paymentMethodInternalModel"
|
||||
cmsWidgetName="PaymentMethodWidget"
|
||||
:validationRules="rules.optionRequired" />
|
||||
<div>Pia Disabled Placeholder</div>
|
||||
<siteFooter
|
||||
ref="siteFooter"
|
||||
cmsWidgetName="SiteFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
:isBackButtonHidden="shouldHideBackButton"
|
||||
buttonSize
|
||||
:isStackedVertically="true"
|
||||
@backClicked="navigateBack"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
|
|
@ -42,20 +45,18 @@ import siteHeader from '@/iss-components/site-header/site-header.vue';
|
|||
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
|
||||
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
|
||||
import reviewDropdown from '@/layouts/payment-method/review-dropdown/review-dropdown.vue';
|
||||
import paymentMethodQuestion from '@/layouts/payment-method/payment-method-question/payment-method-question.vue';
|
||||
|
||||
// Supporting Items
|
||||
import settleAllPromises from '@/helpers/layout-helper';
|
||||
import { useMainStore } from '@/store';
|
||||
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
|
||||
import paymentMethods from '@/constants/payment-method-constants';
|
||||
import globalRules from '@/constants/global-rules';
|
||||
import { Form } from 'vee-validate';
|
||||
|
||||
import { Form, defineRule } from 'vee-validate';
|
||||
|
||||
import { required } from '@/helpers/validation-rules';
|
||||
import errorMessages from '@/constants/error-messages';
|
||||
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
|
||||
|
||||
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: 'payment-method',
|
||||
components: {
|
||||
|
|
@ -64,7 +65,8 @@ export default {
|
|||
siteHeader,
|
||||
siteSubHeader,
|
||||
siteFooter,
|
||||
reviewDropdown
|
||||
reviewDropdown,
|
||||
paymentMethodQuestion
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
|
|
@ -87,15 +89,42 @@ export default {
|
|||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
|
||||
vm.updateFooterButtonText(vm.customCallToActionButtonCopy);
|
||||
|
||||
vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData);
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return { };
|
||||
return {
|
||||
paymentMethodInternalModel: this.getPaymentMethodFromStore(),
|
||||
rules: {
|
||||
optionRequired: globalRules.OPTION_REQUIRED
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
damageInfo() {
|
||||
return useMainStore().order.damage;
|
||||
},
|
||||
customCallToActionButtonCopy() {
|
||||
switch (this.paymentMethod) {
|
||||
case paymentMethods.CREDIT_CARD:
|
||||
return 'Continue to checkout';
|
||||
case paymentMethods.PAYPAL:
|
||||
return 'Continue to Paypal';
|
||||
case paymentMethods.AFTERPAY:
|
||||
return 'Continue to Afterpay';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
paymentMethod() {
|
||||
return this.paymentMethodInternalModel;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
customCallToActionButtonCopy(newValue) {
|
||||
this.updateFooterButtonText(newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -167,9 +196,17 @@ export default {
|
|||
&& customerReqs
|
||||
);
|
||||
},
|
||||
getPaymentMethodFromStore() {
|
||||
const { payInAdvanceType } = useMainStore().order.payment;
|
||||
const isPayInAdvance = useMainStore().order.payment.isPayInAdvance && !!payInAdvanceType;
|
||||
|
||||
return isPayInAdvance ? payInAdvanceType : paymentMethods.PAY_AT_TIME_OF_SERVICE;
|
||||
},
|
||||
updateFooterButtonText(newValue) {
|
||||
this.$refs.siteFooter.updateButtonText(newValue);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
// Multiple paths based on payment
|
||||
console.log('forward button hit...');
|
||||
await useMainStore().savePaymentMethodChoice(this.paymentMethod);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useMainStore } from '@/store';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { getMountOptions } from '@/helpers/unit-test-helper.js';
|
||||
import { getDamageDisplayContent } from '@/helpers/damage-review-content-generator.js';
|
||||
import baseMixin from '@/mixins/base-mixin';
|
||||
|
||||
jest.mock('@/helpers/damage-review-content-generator.js', () => ({
|
||||
getDamageDisplayContent: jest.fn(),
|
||||
|
|
@ -16,6 +17,7 @@ jest.mock('@/helpers/damage-review-content-generator.js', () => ({
|
|||
let cmsContent;
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
...baseMixin.methods,
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,20 +33,14 @@ export default {
|
|||
},
|
||||
driverSideDamageAnswers() {
|
||||
const answerContent = getLocationAnswer(damageLocationsSelected.DRIVER, this.locationAnswers);
|
||||
return this.getAnswersNullSafe(answerContent?.SubWidgetName);
|
||||
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
|
||||
},
|
||||
passengerSideDamageAnswers() {
|
||||
const answerContent = getLocationAnswer(damageLocationsSelected.PASSENGER, this.locationAnswers);
|
||||
return this.getAnswersNullSafe(answerContent?.SubWidgetName);
|
||||
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
|
||||
},
|
||||
locationAnswers() {
|
||||
return this.getAnswersNullSafe(this.damageLocationsWidgetName);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAnswersNullSafe(widgetName) {
|
||||
const rawAnswers = this.getCmsContent(widgetName, 'Answers');
|
||||
return rawAnswers || [];
|
||||
return this.getInputQuestionWidgetAnswersNullSafe(this.damageLocationsWidgetName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import settleAllPromises from '@/helpers/layout-helper.js';
|
|||
import widgetFields from '@/constants/cms-widget-fields.js';
|
||||
import { toTitleCase, formatAddress, toDisplayPhoneNumber } from '@/helpers/text-helper.js';
|
||||
import { getDamageDisplayContent } from '@/helpers/damage-review-content-generator.js';
|
||||
import baseMixin from '@/mixins/base-mixin';
|
||||
|
||||
// Mock fetchCmsContentForPage
|
||||
jest.mock('@/helpers/cms-content-helper', () => ({
|
||||
|
|
@ -35,6 +36,12 @@ jest.mock('@/helpers/damage-review-content-generator.js', () => ({
|
|||
// Mock our module for promises.
|
||||
jest.mock('@/helpers/layout-helper.js', () => jest.fn());
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
...baseMixin.methods
|
||||
}
|
||||
};
|
||||
|
||||
function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRunAfterInitializingStore = () => {}) {
|
||||
const mountOptions = getMountOptions({
|
||||
router: {
|
||||
|
|
@ -52,6 +59,7 @@ function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRu
|
|||
|
||||
mountOptions.global.plugins = [testingPinia];
|
||||
mountOptions.data = () => (initialData);
|
||||
mountOptions.mixins = [mockMixin];
|
||||
|
||||
const apiResponses = { cmsContent: {} };
|
||||
|
||||
|
|
@ -775,14 +783,14 @@ describe('tpa-submit', () => {
|
|||
[[], []],
|
||||
[undefined, []],
|
||||
[null, []]
|
||||
])('getAnswersNullSafe', (rawAnswers, expected) => {
|
||||
])('getInputQuestionWidgetAnswersNullSafe', (rawAnswers, expected) => {
|
||||
// Arrange
|
||||
const widgetName = 'WidgetName';
|
||||
const { wrapper } = getMountedComponent();
|
||||
wrapper.vm.getCmsContent = jest.fn().mockImplementationOnce(() => rawAnswers);
|
||||
|
||||
// Act
|
||||
const result = wrapper.vm.getAnswersNullSafe(widgetName);
|
||||
const result = wrapper.vm.getInputQuestionWidgetAnswersNullSafe(widgetName);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expected);
|
||||
|
|
|
|||
|
|
@ -208,15 +208,15 @@ export default {
|
|||
return [line];
|
||||
},
|
||||
locationAnswers() {
|
||||
return this.getAnswersNullSafe(this.widget.damageLocations);
|
||||
return this.getInputQuestionWidgetAnswersNullSafe(this.widget.damageLocations);
|
||||
},
|
||||
driverSideDamageAnswers() {
|
||||
const answerContent = getLocationAnswer(damageLocationsSelected.DRIVER, this.locationAnswers);
|
||||
return this.getAnswersNullSafe(answerContent?.SubWidgetName);
|
||||
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
|
||||
},
|
||||
passengerSideDamageAnswers() {
|
||||
const answerContent = getLocationAnswer(damageLocationsSelected.PASSENGER, this.locationAnswers);
|
||||
return this.getAnswersNullSafe(answerContent?.SubWidgetName);
|
||||
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
|
||||
},
|
||||
getDamageLines() {
|
||||
const { glassToReplace, isRepair } = useMainStore().order.damage;
|
||||
|
|
@ -285,10 +285,6 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
getAnswersNullSafe(widgetName) {
|
||||
const rawAnswers = this.getCmsContent(widgetName, 'Answers');
|
||||
return rawAnswers || [];
|
||||
},
|
||||
openContactDetailsModal() {
|
||||
this.$refs.contactDetailsDrawer.openModal();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import vehicleCategories from '@/constants/vehicle-categories.js';
|
|||
import queryStrings from '@/constants/query-strings';
|
||||
import dynamicStrings from '@/constants/dynamic-strings';
|
||||
import routerParams from '@/router/router-constants/router-params';
|
||||
import widgetFields from '@/constants/cms-widget-fields.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -19,6 +20,10 @@ export default {
|
|||
getCmsContent(widgetName, fieldName) {
|
||||
return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName] ? this.$root.cmsContentByWidget[widgetName][fieldName] : '';
|
||||
},
|
||||
getInputQuestionWidgetAnswersNullSafe(widgetName) {
|
||||
const rawAnswers = this.getCmsContent(widgetName, widgetFields.INPUT_QUESTION_WIDGET.ANSWERS);
|
||||
return rawAnswers || [];
|
||||
},
|
||||
getFooterInfoBoxHeight() {
|
||||
const footerInfoBox = document.querySelector('.footer#infoBox');
|
||||
return footerInfoBox ? footerInfoBox.offsetHeight : 0;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import baseMixin from '@/mixins/base-mixin';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import widgetFields from '@/constants/cms-widget-fields.js';
|
||||
|
||||
describe('base-mixin', () => {
|
||||
it('should set and get cms content', () => {
|
||||
|
|
@ -20,4 +21,63 @@ describe('base-mixin', () => {
|
|||
const localThis = { cmsWidgetName: 'testWidget' };
|
||||
expect(baseMixin.computed.cssClassNameForCmsWidget.call(localThis)).toBe('widget-name-testWidget');
|
||||
});
|
||||
|
||||
it('getInputQuestionWidgetAnswersNullSafe returns an empty array when the input question widget has no answers', () => {
|
||||
// Arrange
|
||||
const widgetName = 'inputQuestionWidget';
|
||||
const wrapper = shallowMount(baseMixin, {
|
||||
propsData: {
|
||||
cmsContentByWidget: {}
|
||||
}
|
||||
});
|
||||
const cmsContent = {
|
||||
[widgetName]: {
|
||||
[widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT]: 'Choose an option',
|
||||
[widgetFields.INPUT_QUESTION_WIDGET.ANSWERS]: []
|
||||
}
|
||||
};
|
||||
wrapper.componentVM.setCmsContent(cmsContent);
|
||||
|
||||
// Act
|
||||
const answers = wrapper.componentVM.getInputQuestionWidgetAnswersNullSafe(widgetName);
|
||||
|
||||
// Assert
|
||||
expect(answers).not.toBeNull(); // Check that answers is not null
|
||||
expect(Array.isArray(answers)).toBeTruthy(); // Check that answers is an array
|
||||
expect(answers).toHaveLength(0); // Check that answers is empty
|
||||
});
|
||||
|
||||
it('getInputQuestionWidgetAnswersNullSafe returns a non-empty array when the input question widget has answers', () => {
|
||||
// Arrange
|
||||
const widgetName = 'inputQuestionWidget';
|
||||
const wrapper = shallowMount(baseMixin, {
|
||||
propsData: {
|
||||
cmsContentByWidget: {}
|
||||
}
|
||||
});
|
||||
const cmsContent = {
|
||||
[widgetName]: {
|
||||
[widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT]: 'Choose an option',
|
||||
[widgetFields.INPUT_QUESTION_WIDGET.ANSWERS]: [
|
||||
{
|
||||
Name: 'NameA',
|
||||
Text: 'TextA'
|
||||
},
|
||||
{
|
||||
Name: 'NameB',
|
||||
Text: 'TextB'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
wrapper.componentVM.setCmsContent(cmsContent);
|
||||
|
||||
// Act
|
||||
const answers = wrapper.componentVM.getInputQuestionWidgetAnswersNullSafe(widgetName);
|
||||
|
||||
// Assert
|
||||
expect(answers).not.toBeNull(); // Check that answers is not null
|
||||
expect(Array.isArray(answers)).toBeTruthy(); // Check that answers is an array
|
||||
expect(answers).not.toHaveLength(0); // Check that answers is not empty
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import damageLocationsSelected from '@/constants/damage-locations-selected';
|
|||
import coverageStatuses from '@/constants/coverage-statuses';
|
||||
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from '@/constants/schedule-constants';
|
||||
import { convertDateStringToDate, getDateDifferenceInDays, militaryToTwelveHourTime } from '@/helpers/date-helper';
|
||||
import paymentMethods from '@/constants/payment-method-constants';
|
||||
|
||||
const storeId = 'main';
|
||||
|
||||
|
|
@ -147,7 +148,9 @@ const getDefaultState = () => ({
|
|||
coverageStatus: coverageStatuses.PENDING,
|
||||
claimNumber: null
|
||||
},
|
||||
parentAccountNumber: 0
|
||||
parentAccountNumber: 0,
|
||||
isPayInAdvance: null,
|
||||
payInAdvanceType: null
|
||||
},
|
||||
contactInfo: {
|
||||
firstName: null,
|
||||
|
|
@ -2135,6 +2138,12 @@ export const useMainStore = defineStore({
|
|||
this.resetVehicleState();
|
||||
this.resetDamageState();
|
||||
this.resetBailout();
|
||||
},
|
||||
|
||||
savePaymentMethodChoice(paymentMethod) {
|
||||
const isPayInAdvance = paymentMethod !== paymentMethods.PAY_AT_TIME_OF_SERVICE;
|
||||
this.order.payment.isPayInAdvance = isPayInAdvance;
|
||||
this.order.payment.payInAdvanceType = isPayInAdvance ? paymentMethod : null;
|
||||
}
|
||||
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useMainStore } from '@/store/index.js';
|
|||
import globalMethods from '@/global-methods.js';
|
||||
import { getRandomString, getRandomGuid, getRandomInt, getRandomBoolean } from '@/helpers/data-generation.js';
|
||||
import coverageStatuses from '@/constants/coverage-statuses.js';
|
||||
import paymentMethods from '@/constants/payment-method-constants';
|
||||
import { endpoints } from '@/constants/endpoints';
|
||||
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
|
||||
|
||||
|
|
@ -2083,4 +2084,29 @@ describe('Store', () => {
|
|||
expect(store.order.policy.policyLookupSuccessful).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('savePaymentMethodChoice method', () => {
|
||||
it('isPayInAdvance saved as false when choice is to pay at time of servce', () => {
|
||||
// Arrange
|
||||
const paymentMethod = paymentMethods.PAY_AT_TIME_OF_SERVICE;
|
||||
|
||||
// Act
|
||||
store.savePaymentMethodChoice(paymentMethod);
|
||||
|
||||
// Assert
|
||||
expect(store.order.payment.isPayInAdvance).toEqual(false);
|
||||
expect(store.order.payment.payInAdvanceType).toEqual(null);
|
||||
});
|
||||
it('isPayInAdvance saved as true when choice is other than to pay at time of servce', () => {
|
||||
// Arrange
|
||||
const paymentMethod = paymentMethods.CREDIT_CARD;
|
||||
|
||||
// Act
|
||||
store.savePaymentMethodChoice(paymentMethod);
|
||||
|
||||
// Assert
|
||||
expect(store.order.payment.isPayInAdvance).toEqual(true);
|
||||
expect(store.order.payment.payInAdvanceType).toEqual(paymentMethod);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue