Merge pull request #238 from Safelite/feature/digital/SSR-346

Feature/digital/ssr 346
This commit is contained in:
Jason Wheeler 2023-04-12 10:08:18 -04:00 committed by GitHub
commit 5efbb2e5af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1048 additions and 523 deletions

View file

@ -1,68 +1,255 @@
jest.mock("vee-validate", () => ({
useForm: jest.fn(),
}));
const mockValidate = (returnValue) => jest.fn(async () => Promise.resolve({ valid: returnValue }));
const mockMeta = (returnValue) => jest.fn(async () => Promise.resolve(returnValue));
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import Modal from "./modal"; import modal from "./modal";
import crypto from "crypto";
import { useForm } from "vee-validate";
import { Modal } from "bootstrap";
const footerButtonText = "Sample footer text here.";
const headerText = "Sample header text here.";
global.crypto = crypto;
describe("modal.vue", () => { describe("modal.vue", () => {
it("Should display header text when HeaderText is defined in the CMS", async () => { it("Should display modal header text when headerText is defined", async () => {
// Act // Arrange / Act
const wrapper = shallowMount(Modal, { const fakeMeta = {
mixins: [mockMixin], touched: true,
dirty: true,
valid: true,
validated: true,
};
useForm.mockReturnValue({
meta: mockMeta(fakeMeta),
validate: mockValidate(true),
});
const wrapper = shallowMount(modal, {
props: { props: {
cmsWidgetName: "test", headerText: headerText,
footerButtonText: footerButtonText,
}, },
attachTo: document.body, attachTo: document.body,
}); });
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["HeaderText"]));
// Assert
expect(wrapper.html()).toEqual(expect.stringContaining(headerText));
}); });
it("Should display subheader text when SubheaderText is defined in the CMS", async () => { it("Should display footer button text when footerButtonText is defined", async () => {
// Act // Arrange / Act
const wrapper = shallowMount(Modal, { const fakeMeta = {
mixins: [mockMixin], touched: true,
dirty: true,
valid: true,
validated: true,
};
useForm.mockReturnValue({
meta: mockMeta(fakeMeta),
validate: mockValidate(true),
});
const wrapper = shallowMount(modal, {
props: { props: {
cmsWidgetName: "test", footerButtonText: footerButtonText,
}, },
attachTo: document.body, attachTo: document.body,
}); });
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["SubheaderText"]));
// Assert
expect(wrapper.html()).toEqual(expect.stringContaining(footerButtonText));
}); });
it("Should insert image url when Image is defined in the CMS", async () => { it("Should emit 'footer-button-event' if the form is valid", async () => {
// Act // Arrange
const wrapper = shallowMount(Modal, { const fakeMeta = {
mixins: [mockMixin], touched: true,
dirty: true,
valid: true,
validated: true,
};
useForm.mockReturnValue({
meta: mockMeta(fakeMeta),
validate: mockValidate(true),
});
const resetButtonStyle = jest.fn();
const wrapper = shallowMount(modal, {
props: { props: {
cmsWidgetName: "test", footerButtonText: footerButtonText,
headerText: headerText,
}, },
attachTo: document.body, attachTo: document.body,
}); });
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["Image"])); wrapper.vm.resetButtonStyle = resetButtonStyle;
// Act
const buttonMain = wrapper.findComponent({ ref: "modalButtonMain" });
await buttonMain.trigger("click-event");
// Assert
expect(wrapper.emitted("footer-button-event")).toBeTruthy();
}); });
it("Should display body text when BodyText is defined in the CMS", async () => { it("Should not emit 'footer-button-event' if the form is invalid", async () => {
// Act // Arrange
const wrapper = shallowMount(Modal, { const fakeMeta = {
mixins: [mockMixin], touched: true,
dirty: true,
valid: false,
validated: true,
};
useForm.mockReturnValue({
meta: mockMeta(fakeMeta),
validate: mockValidate(false),
});
const resetButtonStyle = jest.fn();
const wrapper = shallowMount(modal, {
props: { props: {
cmsWidgetName: "test", footerButtonText: footerButtonText,
headerText: headerText,
}, },
attachTo: document.body, attachTo: document.body,
}); });
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["BodyText"])); wrapper.vm.resetButtonStyle = resetButtonStyle;
// Act
const buttonMain = wrapper.findComponent({ ref: "modalButtonMain" });
await buttonMain.trigger("click-event");
// Assert
expect(wrapper.emitted("footer-button-event")).toBeFalsy();
});
it("Should have a disabled footer button when the form has not been touched", async () => {
// Arrange / Act
const fakeMeta = {
touched: true,
dirty: true,
valid: true,
validated: true,
};
useForm.mockReturnValue({
meta: mockMeta(fakeMeta),
validate: mockValidate(true),
});
const resetButtonStyle = jest.fn();
const wrapper = shallowMount(modal, {
props: {
footerButtonText: footerButtonText,
headerText: headerText,
},
attachTo: document.body,
});
wrapper.vm.resetButtonStyle = resetButtonStyle;
// Assert
expect(wrapper.vm.isFooterButtonDisabled).toBe(true);
});
it("Should have a disabled footer button when the form is invalid", async () => {
// Arrange / Act
const fakeMeta = {
touched: true,
dirty: true,
valid: true,
validated: true,
};
useForm.mockReturnValue({
meta: mockMeta(fakeMeta),
validate: mockValidate(true),
});
const resetButtonStyle = jest.fn();
const wrapper = shallowMount(modal, {
props: {
footerButtonText: footerButtonText,
headerText: headerText,
},
attachTo: document.body,
});
wrapper.vm.resetButtonStyle = resetButtonStyle;
// Assert
expect(wrapper.vm.isFooterButtonDisabled).toBe(true);
});
it("Should call bootstrap Modal method 'show' when calling 'openModal'", async () => {
// Arrange
const fakeMeta = {
touched: true,
dirty: true,
valid: true,
validated: true,
};
useForm.mockReturnValue({
meta: mockMeta(fakeMeta),
validate: mockValidate(true),
});
const showMock = jest.spyOn(Modal.prototype, "show");
const resetButtonStyle = jest.fn();
const wrapper = shallowMount(modal, {
props: {
footerButtonText: footerButtonText,
headerText: headerText,
},
attachTo: document.body,
});
wrapper.vm.resetButtonStyle = resetButtonStyle;
// Act
wrapper.vm.openModal();
// Assert
expect(showMock).toHaveBeenCalled();
});
it("Should call bootstrap Modal method 'hide' when calling 'closeModal'", async () => {
// Arrange
const fakeMeta = {
touched: true,
dirty: true,
valid: true,
validated: true,
};
useForm.mockReturnValue({
meta: mockMeta(fakeMeta),
validate: mockValidate(true),
});
const hideMock = jest.spyOn(Modal.prototype, "hide");
const resetButtonStyle = jest.fn();
const wrapper = shallowMount(modal, {
props: {
footerButtonText: footerButtonText,
headerText: headerText,
},
attachTo: document.body,
});
wrapper.vm.resetButtonStyle = resetButtonStyle;
// Act
wrapper.vm.openModal();
wrapper.vm.closeModal();
// Assert
expect(hideMock).toHaveBeenCalled();
}); });
}); });
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return mockCmsContent[cmsFieldName];
}),
},
};
const mockCmsContent = {
HeaderText: "Sample header text here.",
SubheaderText: "Sample subheader text here.",
Image: "https://www.sampleImage.sample",
BodyText: "Sample body text here.",
FooterText: "Sample footer text here.",
};

View file

@ -2,39 +2,38 @@
<!-- Modal --> <!-- Modal -->
<div <div
class="modal fade modal-component" class="modal fade modal-component"
v-on="{ 'hidden.bs.modal': resetButtonStyle }" v-on="{ 'hidden.bs.modal': onModalClosed, 'shown.bs.modal': onModalOpened }"
:id="cmsWidgetName" :id="modalId"
tabindex="-1" tabindex="-1"
aria-labelledby="ModalComponentLabel" aria-labelledby="ModalComponentLabel"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header mb-2 mt-2">
<label
v-if="headerText"
class="modal-title d-flex justify-content-center pb-0 w-100">
{{ headerText }}
</label>
<button <button
ref="closeButton"
type="button" type="button"
class="btn-close" class="btn-close"
data-bs-dismiss="modal" @mousedown="closeModal"
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<div class="modal-body ps-4 pe-4 pt-5 pb-4"> <div class="modal-body ps-5 pe-5 pb-4 pt-0">
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" /> <slot></slot>
<h5 class="mb-4" v-html="ModalHeadline"></h5>
<p class="fw-bold mb-2 subheader-text" v-html="ModalSubheadertext"></p>
<p class="mb-0" v-html="ModalBodyText"></p>
<p
class="my-4 caption modal-sub-body"
v-if="ModalSubBodyText"
v-html="ModalSubBodyText"></p>
</div> </div>
<div class="modal-footer px-5 py-4"> <div class="modal-footer px-5 py-4">
<buttonMain <modalButtonMain
isPrimary isPrimary
class="w-100" class="w-100"
ref="buttonMain" ref="modalButtonMain"
suppressLoader loaderColor="white"
:buttonText="ModalCloseButtonText" :buttonText="footerButtonText"
@click-event="buttonClick" @click-event="validateAndEmit"
data-bs-dismiss="modal" /> :class="isFooterButtonDisabled && 'form-test-invalid'" />
</div> </div>
</div> </div>
</div> </div>
@ -42,46 +41,80 @@
</template> </template>
<script> <script>
import buttonMain from "@/ux-components/button-main/button-main"; import modalButtonMain from "@/ux-components/modal-button-main/modal-button-main";
import { Modal } from "bootstrap";
import { useForm } from "vee-validate";
export default { export default {
name: "modal", name: "modal",
props: { props: {
cmsWidgetName: String, modalId: String,
headerText: String,
footerButtonText: String,
onModalOpenedCallback: {
type: Function,
},
onModalClosedCallback: {
type: Function,
},
}, },
computed: { setup(props) {
ModalHeadline() { const modalId = props.modalId ? props.modalId : `modal-${crypto.randomUUID()}`;
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
}, const { meta, validate, resetForm } = useForm();
ModalSubheadertext() {
return this.getCmsContent(this.cmsWidgetName, "SubheaderText"); return {
}, modalId,
ModalBodyText() { meta,
return this.getCmsContent(this.cmsWidgetName, "BodyText"); validate,
}, resetForm,
ModalSubBodyText() { };
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
},
ModalImage() {
return this.getCmsContent(this.cmsWidgetName, "Image");
},
ModalCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
}, },
methods: { methods: {
async validateAndEmit() {
const validationResult = await this.validate();
if (validationResult.valid) {
this.$emit("footer-button-event");
} else {
this.resetButtonStyle();
}
},
resetButtonStyle() { resetButtonStyle() {
this.$refs.buttonMain.resetButtonStyle(); this.$refs.modalButtonMain.resetButtonStyle();
},
onModalOpened() {
this.onModalOpenedCallback?.();
},
onModalClosed() {
this.resetButtonStyle();
this.onModalClosedCallback?.();
},
openModal() {
const modal = Modal.getOrCreateInstance(document.getElementById(this.modalId));
modal?.show();
},
closeModal() {
const modal = Modal.getInstance(document.getElementById(this.modalId));
modal?.hide();
},
},
computed: {
isFooterButtonDisabled() {
if (!this.meta.touched) {
return !this.meta.valid;
}
return !this.meta.dirty || !this.meta.valid;
}, },
}, },
components: { components: {
buttonMain, modalButtonMain,
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.modal { .modal {
overflow: hidden;
top: auto; top: auto;
bottom: 0; bottom: 0;
h5, h5,
@ -92,9 +125,17 @@ export default {
.modal-header { .modal-header {
border-bottom: none; border-bottom: none;
.btn-close { .btn-close {
position: absolute;
right: 1rem;
top: 1rem;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.6874 1.82179L9.50889 8L15.6874 14.1782C16.1042 14.595 16.1042 15.2707 15.6874 15.6875C15.4843 15.8907 15.2146 16 14.9328 16C14.6514 16 14.3818 15.8906 14.1787 15.6875L8.00017 9.50934L1.82171 15.6875C1.6182 15.8907 1.34876 16 1.06708 16C0.785733 16 0.516056 15.8906 0.312965 15.6875C-0.10429 15.2706 -0.10429 14.5952 0.312797 14.1784L6.03276 8.45867L6.49146 8L0.312945 1.82177C-0.10429 1.40483 -0.10429 0.729418 0.312797 0.31262C0.728936 -0.104145 1.40489 -0.104051 1.82185 0.31262L7.54152 6.03202L8.00017 6.49065L14.1786 0.312472C14.5955 -0.104107 15.2707 -0.104201 15.6874 0.312452C16.1042 0.729289 16.1042 1.40496 15.6874 1.82179Z' fill='%231574A1'/%3E%3C/svg%3E%0A"); background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.6874 1.82179L9.50889 8L15.6874 14.1782C16.1042 14.595 16.1042 15.2707 15.6874 15.6875C15.4843 15.8907 15.2146 16 14.9328 16C14.6514 16 14.3818 15.8906 14.1787 15.6875L8.00017 9.50934L1.82171 15.6875C1.6182 15.8907 1.34876 16 1.06708 16C0.785733 16 0.516056 15.8906 0.312965 15.6875C-0.10429 15.2706 -0.10429 14.5952 0.312797 14.1784L6.03276 8.45867L6.49146 8L0.312945 1.82177C-0.10429 1.40483 -0.10429 0.729418 0.312797 0.31262C0.728936 -0.104145 1.40489 -0.104051 1.82185 0.31262L7.54152 6.03202L8.00017 6.49065L14.1786 0.312472C14.5955 -0.104107 15.2707 -0.104201 15.6874 0.312452C16.1042 0.729289 16.1042 1.40496 15.6874 1.82179Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
opacity: 1; opacity: 1;
} }
.modal-title {
font-weight: 500;
color: $black;
}
} }
&.modal-component { &.modal-component {
.modal-dialog { .modal-dialog {

View file

@ -173,9 +173,14 @@ function mapStringToModal(str) {
let params = linkToReplace.substring((dynamicStrings.MODAL_LINK).length + 2, linkToReplace.length -1) let params = linkToReplace.substring((dynamicStrings.MODAL_LINK).length + 2, linkToReplace.length -1)
let splitParams = params.split(","); let splitParams = params.split(",");
let bodyText = '<a href="#!" data-bs-toggle="modal" data-bs-target="#' + splitParams[0] + '" aria-label="Modal window">' + splitParams[1] +'</a>'
return str.replace(linkToReplace, bodyText); let bodyText = '<a modalTarget="' + splitParams[0] + '" class="modal-text" aria-label="Modal window">' + splitParams[1] + '</a>'
let returnVal = str.replace(linkToReplace, bodyText)
if (returnVal.includes(dynamicStrings.MODAL_LINK)) {
returnVal = mapStringToModal(returnVal);
}
return returnVal;
} }
// Function to convert a string, into a matching global state item. // Function to convert a string, into a matching global state item.
@ -214,7 +219,20 @@ function mapStringToState(str) {
} }
return stringBuilder.trimStart(); return stringBuilder.trimStart();
} }
export function setupModalLinks(context) {
context.$nextTick(() => {
const elements = document.getElementsByClassName("modal-text")
for(let element of elements){
const target = element.getAttribute("modalTarget");
if(target)
{
element.addEventListener("click", () => context.$refs[target].openModal() );
}
};
});
}
export function doesCopyContainRouterLink(copy) { export function doesCopyContainRouterLink(copy) {
return copy.includes(this.dynamicStrings.ROUTER_LINK); return copy.includes(this.dynamicStrings.ROUTER_LINK);

View file

@ -48,6 +48,8 @@ export function getMountOptions(mockData) {
mocks.queryStrings = queryStrings; mocks.queryStrings = queryStrings;
mocks.$router = mockData?.router; mocks.$router = mockData?.router;
mocks.$route = mockData?.route; mocks.$route = mockData?.route;
mocks.GaActions = GaActions;
mocks.pushEventToGA = jest.fn();
mocks.$loadScript = mockData?.loadScript; mocks.$loadScript = mockData?.loadScript;
mocks.prependActionToMethod = jest.fn(); mocks.prependActionToMethod = jest.fn();

View file

@ -0,0 +1,67 @@
import { mount } from "@vue/test-utils";
import contentGroupModal from "./content-group-modal";
import crypto from "crypto";
global.crypto = crypto;
describe("content-group-modal.vue", () => {
it("Should display header text when HeaderText is defined in the CMS", async () => {
const wrapper = mount(contentGroupModal, {
mixins: [mockMixin],
props: {
cmsWidgetName: "test",
},
attachTo: document.body,
});
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["HeaderText"]));
});
it("Should display subheader text when SubheaderText is defined in the CMS", async () => {
const wrapper = mount(contentGroupModal, {
mixins: [mockMixin],
props: {
cmsWidgetName: "test",
},
attachTo: document.body,
});
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["SubheaderText"]));
});
it("Should insert image url when Image is defined in the CMS", async () => {
const wrapper = mount(contentGroupModal, {
mixins: [mockMixin],
props: {
cmsWidgetName: "test",
},
attachTo: document.body,
});
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["Image"]));
});
it("Should display body text when BodyText is defined in the CMS", async () => {
const wrapper = mount(contentGroupModal, {
mixins: [mockMixin],
props: {
cmsWidgetName: "test",
},
attachTo: document.body,
});
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["BodyText"]));
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return mockCmsContent[cmsFieldName];
}),
},
};
const mockCmsContent = {
HeaderText: "Sample header text here.",
SubheaderText: "Sample subheader text here.",
Image: "https://www.sampleImage.sample",
BodyText: "Sample body text here.",
FooterText: "Sample footer text here.",
};

View file

@ -0,0 +1,86 @@
<template>
<modal
:ref="ModalName"
:modalId="ModalName"
:footerButtonText="ModalCloseButtonText"
@footer-button-event="footerButtonClick">
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
<h5 class="mb-4" v-html="ModalHeadline"></h5>
<p class="fw-bold mb-2 subheader-text" v-html="ModalSubheadertext"></p>
<p class="mb-0" v-html="ModalBodyText"></p>
<p
class="my-4 caption modal-sub-body"
v-if="ModalSubBodyText"
v-html="ModalSubBodyText"></p>
</modal>
</template>
<script>
import modal from "@/digital-components/modal/modal";
export default {
name: "content-group-modal",
props: {
cmsWidgetName: String,
},
computed: {
ModalName() {
return this.cmsWidgetName;
},
ModalHeadline() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
ModalSubheadertext() {
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
},
ModalBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText");
},
ModalSubBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
},
ModalImage() {
return this.getCmsContent(this.cmsWidgetName, "Image");
},
ModalCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
},
methods: {
openModal() {
this.$refs[this.ModalName].openModal();
},
footerButtonClick() {
this.$refs[this.ModalName].closeModal();
},
},
components: {
modal,
},
};
</script>
<style lang="scss">
.modal {
&.modal-component {
.modal-dialog {
.modal-content {
.modal-body {
.modal-sub-body {
color: $gray-600;
}
ul {
margin-bottom: 0;
}
p {
&:last-child {
margin-bottom: 0;
}
}
}
}
}
}
}
</style>

View file

@ -8,7 +8,7 @@ import { useMainStore } from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { applicationConfig } from "@/constants/application-config"; import { applicationConfig } from "@/constants/application-config";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage, setupModalLinks } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper.js";
jest.mock("@/helpers/damage-helper", () => ({ jest.mock("@/helpers/damage-helper", () => ({
@ -20,9 +20,10 @@ jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(), settleAllPromises: jest.fn(),
})); }));
// Mock fetchCmsContentForPage // Mock fetchCmsContentForPage, setupModalLinks
jest.mock("@/helpers/cms-content-helper", () => ({ jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(), fetchCmsContentForPage: jest.fn(),
setupModalLinks: jest.fn(),
})); }));
describe("coverage-statement.vue...", () => { describe("coverage-statement.vue...", () => {

View file

@ -1,48 +1,49 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }" > <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }" >
<div class="page-container-grouped-styles"> <div class="page-container-grouped-styles">
<div class="fade-on-route-transition position-relative"> <div class="fade-on-route-transition position-relative">
<siteHeader cmsWidgetName="SiteHeaderWidget" /> <siteHeader cmsWidgetName="SiteHeaderWidget" />
<div class="select-car"> <div class="select-car">
<div class="container-fluid pb-2"> <div class="container-fluid pb-2">
<div class="row px-3"> <div class="row px-3">
<div class="col"> <div class="col">
<div class="select-car-form rounded"> <div class="select-car-form rounded">
<textBlock <textBlock
cmsWidgetName="verifyingCoverageStatement" cmsWidgetName="verifyingCoverageStatement"
typeStyle="h5" typeStyle="h5"
justifyText="center" justifyText="center"
class="mt-0 mb-4" class="mt-0 mb-4"
id="coverage-statement-text-block" id="coverage-statement-text-block"
/> />
<div> <div>
<p v-html="continueWithSchedulingBodyText" class="mt-0 small" ></p> <p v-html="continueWithSchedulingBodyText" class="mt-0 small" ></p>
</div> </div>
<textBlock <textBlock
cmsWidgetName="whatHappensNextCopy" cmsWidgetName="whatHappensNextCopy"
class="mt-4 mb-2 fw-bold" class="mt-4 mb-2 fw-bold"
id="coverage-statement-text-block" id="coverage-statement-text-block"
/> />
<div> <div>
<p class="small" v-html="bodyText" ref="coverageStatementBodyText"></p> <p class="small" v-html="bodyText" ref="coverageStatementBodyText"></p>
</div> </div>
<recalModal cmsWidgetName="RecalModal" />
<steeringText cmsWidgetName="MASteeringText" ></steeringText> <steeringText cmsWidgetName="MASteeringText" ></steeringText>
<siteFooter <siteFooter
cmsWidgetName="SiteFooterWidget" cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
@backClicked="navigateBack" @backClicked="navigateBack"
@forwardClicked="forwardButtonAction" @forwardClicked="forwardButtonAction"
ref="siteFooter" ref="siteFooter"
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Form> <recalModal ref="RecalModal" cmsWidgetName="RecalModal" />
</Form>
</template> </template>
<script> <script>
@ -56,116 +57,119 @@ import textBlock from "@/digital-components/text-block/text-block";
import recalModal from '@/layouts/coverage-statement/recal-modal/recal-modal.vue'; import recalModal from '@/layouts/coverage-statement/recal-modal/recal-modal.vue';
import steeringText from "@/iss-components/steering-text/steering-text.vue"; import steeringText from "@/iss-components/steering-text/steering-text.vue";
// Import Supporting Files // Import Supporting Files
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; import { fetchCmsContentForPage, setupModalLinks } from '@/helpers/cms-content-helper';
import { settleAllPromises } from '@/helpers/layout-helper'; import { settleAllPromises } from '@/helpers/layout-helper';
import { getDamageString } from '@/helpers/damage-helper.js'; import { getDamageString } from '@/helpers/damage-helper.js';
import { useMainStore } from "@/store"; import { useMainStore } from "@/store";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
export default { export default {
name: 'coverage-statement', name: 'coverage-statement',
mixins: [baseFormMixin, vehicleQuestionsMixin], mixins: [baseFormMixin, vehicleQuestionsMixin],
components: { components: {
siteFooter, siteFooter,
siteHeader, siteHeader,
siteSubHeader, siteSubHeader,
Form, Form,
textBlock, textBlock,
recalModal, recalModal,
steeringText steeringText
}, },
computed: { mounted() {
bodyText() { setupModalLinks(this);
if (useMainStore().order.damage.isRepair) { },
return this.unverifiedNonADASRepairBodyText; computed: {
} bodyText() {
else { if (useMainStore().order.damage.isRepair) {
let parts = useMainStore().order.lineItems.glassParts; return this.unverifiedNonADASRepairBodyText;
}
else {
let parts = useMainStore().order.lineItems.glassParts;
// if ADAS, display ADASNextSteps // if ADAS, display ADASNextSteps
if (parts.filter(part => part.requiresRecalibration).length > 0) { if (parts.filter(part => part.requiresRecalibration).length > 0) {
return this.unverifiedADASNextStepsBodyText; return this.unverifiedADASNextStepsBodyText;
} }
// if non-ADAS, display NonADASNextSteps // if non-ADAS, display NonADASNextSteps
else { else {
return this.unverifiedNonADASNextStepsBodyText; return this.unverifiedNonADASNextStepsBodyText;
} }
} }
}, },
continueWithSchedulingBodyText() { continueWithSchedulingBodyText() {
return this.getCmsContent("continueWithSchedulingCopy", "BodyText"); return this.getCmsContent("continueWithSchedulingCopy", "BodyText");
}, },
unverifiedADASNextStepsBodyText() { unverifiedADASNextStepsBodyText() {
return this.getCmsContent("UnverifiedADASNextStepsWidget", "BodyText").replaceAll("{custom:damage}", this.damageText); return this.getCmsContent("UnverifiedADASNextStepsWidget", "BodyText").replaceAll("{custom:damage}", this.damageText);
}, },
unverifiedNonADASNextStepsBodyText() { unverifiedNonADASNextStepsBodyText() {
return this.getCmsContent("UnverifiedNonADASNextStepsWidget", "BodyText").replaceAll("{custom:damage}", this.damageText); return this.getCmsContent("UnverifiedNonADASNextStepsWidget", "BodyText").replaceAll("{custom:damage}", this.damageText);
}, },
unverifiedNonADASRepairBodyText() { unverifiedNonADASRepairBodyText() {
return this.getCmsContent("UnverifiedNonADASRepairWidget", "BodyText"); return this.getCmsContent("UnverifiedNonADASRepairWidget", "BodyText");
}, },
damageText() { damageText() {
var damageString = getDamageString(); var damageString = getDamageString();
return damageString == "match" ? "" : damageString; return damageString == "match" ? "" : damageString;
}, },
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage); const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
{ {
resultKey: 'cmsContent', resultKey: 'cmsContent',
promise: cmsContentPromise, promise: cmsContentPromise,
}, },
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
}); });
}, },
methods: { methods: {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
if (useMainStore().order.vehicle.vin) { if (useMainStore().order.vehicle.vin) {
return true; return true;
} }
return false; return false;
}, },
async forwardButtonAction() { async forwardButtonAction() {
return this.navigateForward(); return this.navigateForward();
}, },
navigateForward() { navigateForward() {
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_COVERAGE_STATEMENT, this.navigationScenarios.CLICKED_FORWARD_COVERAGE_STATEMENT,
this.$route this.$route
); );
}, },
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
ol { ol {
margin-left: -1rem; margin-left: -1rem;
li { li {
margin-bottom: .5rem; margin-bottom: .5rem;
line-height: 1.5rem; line-height: 1.5rem;
a { a {
line-height: 1.5rem; line-height: 1.5rem;
padding: 0; padding: 0;
} }
} }
} }
#coverage-statement-text-block { #coverage-statement-text-block {
color: #000000; color: $black;
} }
p strong { p strong {
color: #000000; color: $black;
font-weight: 500; font-weight: 500;
} }
</style> </style>

View file

@ -1,10 +1,10 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount, mount } from "@vue/test-utils";
import recalModal from "@/layouts/coverage-statement/recal-modal/recal-modal.vue"; import recalModal from "@/layouts/coverage-statement/recal-modal/recal-modal.vue";
describe("modal.vue", () => { describe("modal.vue", () => {
it("Should display header text when HeaderText is defined in the CMS", async () => { it("Should display header text when HeaderText is defined in the CMS", async () => {
// Act // Act
const wrapper = shallowMount(recalModal, { const wrapper = mount(recalModal, {
mixins: [mockMixin], mixins: [mockMixin],
props: { props: {
cmsWidgetName: "test", cmsWidgetName: "test",
@ -16,7 +16,7 @@ describe("modal.vue", () => {
it("Should display subheader text when SubheaderText is defined in the CMS", async () => { it("Should display subheader text when SubheaderText is defined in the CMS", async () => {
// Act // Act
const wrapper = shallowMount(recalModal, { const wrapper = mount(recalModal, {
mixins: [mockMixin], mixins: [mockMixin],
props: { props: {
cmsWidgetName: "test", cmsWidgetName: "test",
@ -28,7 +28,7 @@ describe("modal.vue", () => {
it("Should insert image url when Image is defined in the CMS", async () => { it("Should insert image url when Image is defined in the CMS", async () => {
// Act // Act
const wrapper = shallowMount(recalModal, { const wrapper = mount(recalModal, {
mixins: [mockMixin], mixins: [mockMixin],
props: { props: {
cmsWidgetName: "test", cmsWidgetName: "test",
@ -40,7 +40,7 @@ describe("modal.vue", () => {
it("Should display body text when BodyText is defined in the CMS", async () => { it("Should display body text when BodyText is defined in the CMS", async () => {
// Act // Act
const wrapper = shallowMount(recalModal, { const wrapper = mount(recalModal, {
mixins: [mockMixin], mixins: [mockMixin],
props: { props: {
cmsWidgetName: "test", cmsWidgetName: "test",

View file

@ -1,55 +1,45 @@
<template> <template>
<!-- Modal --> <modal
<div :ref="ModalName"
class="modal fade modal-component" :modalId="ModalName"
v-on="{ 'hidden.bs.modal': resetButtonStyle }" :footerButtonText="ModalCloseButtonText"
:id="cmsWidgetName" @footer-button-event="closeModal"
tabindex="-1" >
aria-labelledby="ModalComponentLabel" <div class="recal-modal-body ps-4 pe-4 pt-0 pb-5">
aria-hidden="true"> <h5 class="mb-4 text-center" v-html="ModalHeadline"></h5>
<div class="modal-dialog modal-dialog-centered"> <img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
<div class="modal-content"> <p class="fw-bold mb-2 subheader-text" v-html="ModalSubheadertext"></p>
<div class="modal-header"> <p class="mb-0 small" v-html="ModalBodyText"></p>
<button <p
type="button" class="my-4 caption modal-sub-body"
class="btn-close" v-if="ModalSubBodyText"
data-bs-dismiss="modal" v-html="ModalSubBodyText">
aria-label="Close"></button> </p>
</div>
<div class="modal-body ps-4 pe-4 pt-0 pb-5">
<h5 class="mb-4 text-center" v-html="ModalHeadline"></h5>
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
<p class="fw-bold mb-2 subheader-text" v-html="ModalSubheadertext"></p>
<p class="mb-0 small" v-html="ModalBodyText"></p>
<p
class="my-4 caption modal-sub-body"
v-if="ModalSubBodyText"
v-html="ModalSubBodyText"></p>
</div>
<div class="modal-footer px-5 py-4">
<buttonMain
isPrimary
class="w-100"
ref="buttonMain"
suppressLoader
:buttonText="ModalCloseButtonText"
@click-event="buttonClick"
data-bs-dismiss="modal" />
</div>
</div>
</div> </div>
</div> </modal>
</template> </template>
<script> <script>
import buttonMain from "@/ux-components/button-main/button-main"; import modal from "@/digital-components/modal/modal";
export default { export default {
name: "modal", name: "recal-modal",
props: { props: {
cmsWidgetName: String, cmsWidgetName: String,
}, },
methods: {
openModal() {
this.$refs[this.ModalName].openModal();
},
closeModal() {
this.$refs[this.ModalName].closeModal();
},
},
computed: { computed: {
ModalName() {
return this.cmsWidgetName;
},
ModalHeadline() { ModalHeadline() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText"); return this.getCmsContent(this.cmsWidgetName, "HeaderText");
}, },
@ -69,77 +59,30 @@ export default {
return this.getCmsContent(this.cmsWidgetName, "FooterText"); return this.getCmsContent(this.cmsWidgetName, "FooterText");
}, },
}, },
methods: {
resetButtonStyle() {
this.$refs.buttonMain.resetButtonStyle();
},
},
components: { components: {
buttonMain, modal,
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.modal {
top: auto; .recal-modal-body {
bottom: 0; .modal-sub-body {
overflow: hidden; color: $gray-600;
h5,
strong,
.subheader-text {
color: $black;
}
.modal-header {
border-bottom: none;
.btn-close {
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.6874 1.82179L9.50889 8L15.6874 14.1782C16.1042 14.595 16.1042 15.2707 15.6874 15.6875C15.4843 15.8907 15.2146 16 14.9328 16C14.6514 16 14.3818 15.8906 14.1787 15.6875L8.00017 9.50934L1.82171 15.6875C1.6182 15.8907 1.34876 16 1.06708 16C0.785733 16 0.516056 15.8906 0.312965 15.6875C-0.10429 15.2706 -0.10429 14.5952 0.312797 14.1784L6.03276 8.45867L6.49146 8L0.312945 1.82177C-0.10429 1.40483 -0.10429 0.729418 0.312797 0.31262C0.728936 -0.104145 1.40489 -0.104051 1.82185 0.31262L7.54152 6.03202L8.00017 6.49065L14.1786 0.312472C14.5955 -0.104107 15.2707 -0.104201 15.6874 0.312452C16.1042 0.729289 16.1042 1.40496 15.6874 1.82179Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
opacity: 1;
} }
} ul {
&.modal-component { margin-bottom: 0;
.modal-dialog { }
max-width: 576px; p {
margin: 0 auto; &:last-child {
.modal-content { margin-bottom: 0;
margin: 0 auto;
box-shadow: 0px 16px 48px -16px rgba(0, 0, 0, 0.25);
border-radius: 1.5rem 1.5rem 0 0;
.modal-body {
.modal-sub-body {
color: $gray-600;
}
ul {
margin-bottom: 0;
}
p {
&:last-child {
margin-bottom: 0;
}
}
}
}
&.modal-dialog-centered {
align-items: flex-end;
min-height: 100%;
} }
} }
.modal-footer { .subheader-text {
border-top: none; color: $black;
background-color: $gray-100;
box-shadow: 0px -1px 0px rgba(179, 180, 181, 0.3);
button {
margin: 0;
}
} }
} }
}
body {
.modal-backdrop {
height: 100%;
&.show {
opacity: 0.4;
}
}
}
</style> </style>

View file

@ -146,11 +146,11 @@
break; break;
case "returnurl": case "returnurl":
this.mainStore.iisConfig.returnURL = value; this.mainStore.issConfig.returnURL = value;
break; break;
case "returnurl2": case "returnurl2":
this.mainStore.iisConfig.returnURL2 = value; this.mainStore.issConfig.returnURL2 = value;
break; break;
// Not stored // Not stored

View file

@ -350,7 +350,7 @@ function setupMocks({
], ],
route: { route: {
query: { query: {
fmgPage: "part-questions", issPage: "part-questions",
}, },
}, },
data() { data() {

View file

@ -14,7 +14,7 @@
v-html="ProviderPreferenceBodyText" v-html="ProviderPreferenceBodyText"
class="mt-0 body-text" class="mt-0 body-text"
></div> ></div>
<shoppreferenceModal cmsWidgetName="ShopPreferenceDrawer" />
<buttonQuestion <buttonQuestion
cmsWidgetName="ServiceLocationQuestion" cmsWidgetName="ServiceLocationQuestion"
:questionText="questionText" :questionText="questionText"
@ -38,15 +38,19 @@
</div> </div>
</div> </div>
</div> </div>
<contentGroupModal cmsWidgetName="ShopPreferenceDrawer"
ref="ShopPreferenceDrawer"
/>
</Form> </Form>
</template> </template>
<script> <script>
// Import Supporting Files // Import Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage, setupModalLinks } from '@/helpers/cms-content-helper';
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import buttonQuestion from "@/digital-components/button-question/button-question"; import buttonQuestion from "@/digital-components/button-question/button-question";
import contentGroupModal from "@/iss-components/content-group-modal/content-group-modal"
// Import Component // Import Component
import baseFormMixin from "@/mixins/base-form-mixin"; import baseFormMixin from "@/mixins/base-form-mixin";
@ -54,7 +58,6 @@ import { Form,defineRule } from "vee-validate";
import siteFooter from "@/iss-components/site-footer/site-footer.vue"; import siteFooter from "@/iss-components/site-footer/site-footer.vue";
import siteHeader from "@/iss-components/site-header/site-header.vue"; import siteHeader from "@/iss-components/site-header/site-header.vue";
import siteSubHeader from "@/iss-components/site-sub-header/site-sub-header.vue"; import siteSubHeader from "@/iss-components/site-sub-header/site-sub-header.vue";
import shoppreferenceModal from '@/layouts/provider-preference/shoppreference-modal/shoppreference-modal.vue';
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED)); defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -67,8 +70,8 @@ export default {
siteHeader, siteHeader,
siteSubHeader, siteSubHeader,
Form, Form,
shoppreferenceModal, buttonQuestion,
buttonQuestion contentGroupModal
}, },
data() { data() {
return { return {
@ -124,6 +127,9 @@ export default {
}, },
resetDependentState() {}, resetDependentState() {},
}, },
mounted() {
setupModalLinks(this);
}
}; };
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -1,41 +0,0 @@
import { shallowMount } from "@vue/test-utils";
import shoppreferenceModal from '@/layouts/provider-preference/shoppreference-modal/shoppreference-modal.vue';
describe("modal.vue", () => {
it("Should display header text when HeaderText is defined in the CMS", async () => {
// Act
const wrapper = shallowMount(shoppreferenceModal, {
mixins: [mockMixin],
props: {
cmsWidgetName: "test",
},
attachTo: document.body,
});
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["HeaderText"]));
});
it("Should display body text when BodyText is defined in the CMS", async () => {
// Act
const wrapper = shallowMount(shoppreferenceModal, {
mixins: [mockMixin],
props: {
cmsWidgetName: "test",
},
attachTo: document.body,
});
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["BodyText"]));
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return mockCmsContent[cmsFieldName];
}),
},
};
const mockCmsContent = {
HeaderText: "Sample header text here.",
BodyText: "Sample body text here.",
FooterText: "Sample footer text here.",
};

View file

@ -1,137 +0,0 @@
<template>
<!-- Modal -->
<div
class="modal fade modal-component"
v-on="{ 'hidden.bs.modal': resetButtonStyle }"
:id="cmsWidgetName"
tabindex="-1"
aria-labelledby="ModalComponentLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body ps-4 pe-4 pt-4 pb-4">
<h5 class="mb-2 text-center header-text" v-html="ModalHeadline"></h5>
<p class="mb-0 small body-text" v-html="ModalBodyText"></p>
</div>
<div class="modal-footer px-5 py-4">
<buttonMain
isPrimary
class="w-100"
ref="buttonMain"
suppressLoader
:buttonText="ModalCloseButtonText"
@click-event="buttonClick"
data-bs-dismiss="modal" />
</div>
</div>
</div>
</div>
</template>
<script>
import buttonMain from "@/ux-components/button-main/button-main";
export default {
name: "modal",
props: {
cmsWidgetName: String,
},
computed: {
ModalHeadline() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
ModalBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText");
},
ModalCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
},
methods: {
resetButtonStyle() {
this.$refs.buttonMain.resetButtonStyle();
},
},
components: {
buttonMain,
},
};
</script>
<style lang="scss">
.modal {
top: auto;
bottom: 0;
h5,
strong,
.header-text{
color: #000000;
font-weight: 500;
font-size: 16px;
line-height: 26px;
}
.body-text{
color:#525656;
font-size: 16px;
line-height: 26px;
}
.modal-header {
border-bottom: none;
.btn-close {
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.6874 1.82179L9.50889 8L15.6874 14.1782C16.1042 14.595 16.1042 15.2707 15.6874 15.6875C15.4843 15.8907 15.2146 16 14.9328 16C14.6514 16 14.3818 15.8906 14.1787 15.6875L8.00017 9.50934L1.82171 15.6875C1.6182 15.8907 1.34876 16 1.06708 16C0.785733 16 0.516056 15.8906 0.312965 15.6875C-0.10429 15.2706 -0.10429 14.5952 0.312797 14.1784L6.03276 8.45867L6.49146 8L0.312945 1.82177C-0.10429 1.40483 -0.10429 0.729418 0.312797 0.31262C0.728936 -0.104145 1.40489 -0.104051 1.82185 0.31262L7.54152 6.03202L8.00017 6.49065L14.1786 0.312472C14.5955 -0.104107 15.2707 -0.104201 15.6874 0.312452C16.1042 0.729289 16.1042 1.40496 15.6874 1.82179Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
opacity: 1;
}
}
&.modal-component {
.modal-dialog {
max-width: 576px;
margin: 0 auto;
.modal-content {
margin: 0 auto;
box-shadow: 0px 16px 48px -16px rgba(0, 0, 0, 0.25);
border-radius: 1.5rem 1.5rem 0 0;
.modal-body {
.modal-sub-body {
color: $gray-600;
}
ul {
margin-bottom: 0;
}
p {
&:last-child {
margin-bottom: 0;
}
}
}
}
&.modal-dialog-centered {
align-items: flex-end;
min-height: 100%;
}
}
.modal-footer {
border-top: none;
background-color: $gray-100;
box-shadow: 0px -1px 0px rgba(179, 180, 181, 0.3);
button {
margin: 0;
}
}
}
}
body {
.modal-backdrop {
height: 100%;
&.show {
opacity: 0.4;
}
}
}
</style>

View file

@ -63,4 +63,11 @@ body {
overflow: hidden; overflow: hidden;
} }
} }
.modal-text {
display: inline;
color: $blue;
cursor: pointer;
text-decoration: underline;
}
} }

View file

@ -11,7 +11,7 @@ describe("list-button.vue", () => {
mockData: { mockData: {
global: { global: {
mocks: { mocks: {
$route: { query: { fmgPage: "page-name" } }, $route: { query: { issPage: "page-name" } },
GaActions: GaActions, GaActions: GaActions,
pushEventToGA: jest.fn(), pushEventToGA: jest.fn(),
}, },
@ -37,7 +37,7 @@ describe("list-button.vue", () => {
mockData: { mockData: {
global: { global: {
mocks: { mocks: {
$route: { query: { fmgPage: "page-name" } }, $route: { query: { issPage: "page-name" } },
GaActions: GaActions, GaActions: GaActions,
pushEventToGA: jest.fn(), pushEventToGA: jest.fn(),
}, },
@ -64,7 +64,7 @@ describe("list-button.vue", () => {
mockData: { mockData: {
global: { global: {
mocks: { mocks: {
$route: { query: { fmgPage: "page-name" } }, $route: { query: { issPage: "page-name" } },
GaActions: GaActions, GaActions: GaActions,
pushEventToGA: jest.fn(), pushEventToGA: jest.fn(),
}, },

View file

@ -0,0 +1,188 @@
import { shallowMount } from "@vue/test-utils";
import modalButtonMain from "./modal-button-main";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
describe("modal-button-main.vue", () => {
it("Should return btn-primary class", () => {
// Arrange/Act
const wrapper = shallowMount(
modalButtonMain,
setupMocks({
propsData: {
isPrimary: true,
},
})
);
const button = wrapper.find("button");
// Assert
expect(button.attributes("class")).toContain("btn-primary");
});
it("Should return aria-disabled state", () => {
// Arrange/Act
const wrapper = shallowMount(
modalButtonMain,
setupMocks({
propsData: {
isDisabled: true,
},
})
);
const button = wrapper.find("button");
// Assert
expect(button.attributes()["aria-disabled"]).toEqual("true");
});
it("Should return loader color", async () => {
// Arrange
const wrapper = shallowMount(
modalButtonMain,
setupMocks({
propsData: {
loaderColor: "blue",
loaderEnabled: true,
},
})
);
// Act
wrapper.vm.clicked();
await nextTick();
// Assert
const loader = wrapper.find("loader-stub");
expect(loader.attributes("class")).toContain("blue");
});
it("Should return loader position", async () => {
// Arrange
const wrapper = shallowMount(
modalButtonMain,
setupMocks({
propsData: {
loaderPosition: "right",
loaderEnabled: true,
},
})
);
// Act
wrapper.vm.clicked();
await nextTick();
// Assert
const loader = wrapper.find("loader-stub");
expect(loader.attributes("class")).toContain("right");
});
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
// Arrange
const wrapper = shallowMount(
modalButtonMain,
setupMocks({
propsData: {
loaderPosition: "right",
loaderEnabled: true,
},
})
);
wrapper.setData({
isLoaderDisplayed: true,
});
// Act
wrapper.vm.removeLoader();
await nextTick();
// Assert
const loader = wrapper.find("loader-stub");
expect(wrapper.vm.isLoaderDisplayed).toBe(false);
});
it("Should set 'isLoaderDisplayed' to false when calling 'resetButtonStyle'", async () => {
// Arrange
const wrapper = shallowMount(
modalButtonMain,
setupMocks({
propsData: {
loaderPosition: "right",
loaderEnabled: true,
},
})
);
wrapper.setData({
isLoaderDisplayed: true,
});
// Act
wrapper.vm.resetButtonStyle();
await nextTick();
// Assert
expect(wrapper.vm.isLoaderDisplayed).toBe(false);
});
it("Should emit 'click-event' event when clicking if the button is enabled", async () => {
// Arrange
const wrapper = shallowMount(
modalButtonMain,
setupMocks({
propsData: {
loaderPosition: "right",
loaderEnabled: true,
isDisabled: false,
},
})
);
const buttonElement = wrapper.find("button");
// Act
buttonElement.trigger("click");
await nextTick();
// Assert
expect(wrapper.emitted("click-event")).toBeTruthy();
});
it("Should not emit 'click-event' event when clicking if the button is disabled", async () => {
// Arrange
const wrapper = shallowMount(
modalButtonMain,
setupMocks({
propsData: {
loaderPosition: "right",
loaderEnabled: true,
isDisabled: true,
},
})
);
const buttonElement = wrapper.find("button");
// Act
buttonElement.trigger("click");
await nextTick();
// Assert
expect(wrapper.emitted("click-event")).toBeFalsy();
});
});
function setupMocks(mountOptionsMockData = {}) {
const defaultMountOptions = { route: { query: { issPage: "page-name" } } };
const baseMountOptions = getMountOptions(
Object.assign(defaultMountOptions, mountOptionsMockData)
);
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
return allMountOptions;
}

View file

@ -0,0 +1,153 @@
<template>
<button
type="button"
:aria-disabled="isDisabled"
class="btn d-flex align-items-center justify-content-center py-3 px-4 delay"
:class="[
isPrimary ? 'btn-primary' : 'btn-secondary',
isFloat ? 'float-end' : '',
isLoaderDisplayed ? 'has-loader' : '',
]"
@click="clicked">
<span class="m-0">{{ this.buttonText }}</span>
<loader
class="ms-2"
v-if="isLoaderDisplayed && !suppressLoader"
v-bind:class="[this.loaderColor, this.loaderPosition]" />
</button>
</template>
<script>
import loader from "@/ux-components/loader/loader";
export default {
name: "modalButtonMain",
props: {
isPrimary: Boolean,
buttonText: String,
isDisabled: Boolean,
loaderColor: String,
loaderPosition: String,
isFloat: Boolean,
suppressLoader: Boolean,
},
data() {
return {
isLoaderDisplayed: false,
};
},
methods: {
removeLoader() {
this.isLoaderDisplayed = false;
},
clicked() {
this.pushEventToGA(
this.$route.query[this.queryStrings.ISS_PAGE],
this.GaActions.CLICKED,
this.buttonText,
true
);
if (!this.isDisabled) {
this.isLoaderDisplayed = true;
this.$emit("click-event");
}
},
resetButtonStyle() {
this.isLoaderDisplayed = false;
},
},
components: {
loader,
},
};
</script>
<style lang="scss">
.btn {
&.btn-primary {
position: relative;
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
border: none;
border-radius: $border-radius-lg;
color: $white;
justify-content: center;
font-weight: 500;
@media (hover: hover) {
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
}
&:focus {
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
color: $white;
background: linear-gradient(270deg, rgba(6, 87, 124, 1) 0%, rgba(6, 87, 124, 1) 100%);
}
&:disabled {
background: $gray-200 !important;
background: linear-gradient(270deg, $gray-200 0%, $gray-200 100%) !important;
color: $gray-600 !important;
font-weight: 400;
height: 48px;
border: none;
border-radius: $border-radius-lg;
cursor: pointer;
pointer-events: all;
}
&.has-loader {
color: $white;
background: $blue-700;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
pointer-events: none;
}
&.delay {
// fixes flicker while transitioning between states
transition: background 0s 0s ease-in-out;
}
}
&.btn-secondary {
position: relative;
background: transparent;
border: 1px solid $blue;
border-radius: $border-radius-lg;
color: $blue;
font-weight: 500;
transition: all 150ms linear;
height: 3rem;
&:hover {
color: $white;
@include blue-gradient;
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
color: $white;
@include blue-gradient;
}
&:disabled {
background: transparent;
color: $gray-550 !important;
font-weight: 400;
height: 48px;
border: 1px solid $gray-550;
border-radius: $border-radius-lg;
cursor: pointer;
pointer-events: all;
}
&.has-loader {
color: $white;
@include blue-gradient;
pointer-events: none;
}
&.delay {
// fixes flicker while transitioning between states
transition: background 0s 0s ease-in-out;
}
}
}
</style>