Merge pull request #379 from Safelite/refactor/linting4

Linting of the iss-components
This commit is contained in:
DavidAtSafelite 2023-07-25 09:52:13 -04:00 committed by GitHub
commit c4b06b6a69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 421 additions and 351 deletions

View file

@ -143,13 +143,11 @@ describe('address-questions.vue', () => {
};
// Act
autocompleteElement.dispatchEvent(
new CustomEvent('place_changed', { detail: selectedPlace })
);
autocompleteElement.dispatchEvent(new CustomEvent('place_changed', { detail: selectedPlace }));
// Assert
wrapper.vm.$nextTick(function () {
const addressModel = wrapper.vm.addressModel;
wrapper.vm.$nextTick(() => {
const { addressModel } = wrapper.vm;
expect(addressModel.streetAddress).toEqual('1234 Test Road');
expect(addressModel.city).toEqual('Columbus');
expect(addressModel.state).toEqual('OH');
@ -169,9 +167,9 @@ describe('address-questions.vue', () => {
});
const { wrapper } = setupMocks({
querySelectorFunction: function (query) {
querySelectorFunction(query) {
if (query == '.pac-container .pac-item') {
let element = document.createElement('div');
const element = document.createElement('div');
element.textContent = '123 Test Street';
return element;
}
@ -222,8 +220,8 @@ describe('address-questions.vue', () => {
}
});
let noMatchAlert = wrapper.findComponent({ ref: 'alertNoMatchWarning' });
let verificationAlert = wrapper.findComponent({ ref: 'alertVerificationWarning' });
const noMatchAlert = wrapper.findComponent({ ref: 'alertNoMatchWarning' });
const verificationAlert = wrapper.findComponent({ ref: 'alertVerificationWarning' });
expect(noMatchAlert.exists()).toBeFalsy();
expect(verificationAlert.exists()).toBeFalsy();
@ -235,7 +233,7 @@ describe('address-questions.vue', () => {
await wrapper.vm.$nextTick();
// Assert
const addressModel = wrapper.vm.addressModel;
const { addressModel } = wrapper.vm;
expect(addressModel.streetAddress).toEqual('1234 Test Road');
expect(addressModel.city).toEqual('Columbus');
expect(addressModel.state).toEqual('OH');
@ -245,8 +243,7 @@ describe('address-questions.vue', () => {
describe('alerts', () => {
const places = [null, { address_components: null }, undefined, {}];
test.each(places)(
'selected place/place properties is null => display verification alert',
test.each(places)('selected place/place properties is null => display verification alert',
async (place) => {
// Arrange
const { wrapper } = setupMocks({});
@ -259,9 +256,7 @@ describe('address-questions.vue', () => {
const selectedPlace = place;
// Act
autocompleteElement.dispatchEvent(
new CustomEvent('place_changed', { detail: selectedPlace })
);
autocompleteElement.dispatchEvent(new CustomEvent('place_changed', { detail: selectedPlace }));
await wrapper.vm.$nextTick();
// Assert
@ -273,8 +268,7 @@ describe('address-questions.vue', () => {
const noMatchAlert = wrapper.findComponent({ ref: 'alertNoMatchWarning' });
expect(noMatchAlert.exists()).toBe(false);
}
);
});
test('user enters address that yields no autocomplete results => show noMatch alert', async () => {
// Arrange
@ -318,9 +312,9 @@ describe('address-questions.vue', () => {
});
const { wrapper } = setupMocks({
querySelectorFunction: function (query) {
querySelectorFunction(query) {
if (query == '.pac-container .pac-item') {
let element = document.createElement('div');
const element = document.createElement('div');
element.textContent = '123 Test Street';
return element;
}
@ -369,7 +363,7 @@ describe('address-questions.vue', () => {
});
// Assert
wrapper.vm.$nextTick(function () {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: 'alertNoMatchWarning' });
@ -396,7 +390,7 @@ describe('address-questions.vue', () => {
});
// Assert
wrapper.vm.$nextTick(function () {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: 'alertNoMatchWarning' });
@ -423,7 +417,7 @@ describe('address-questions.vue', () => {
});
// Assert
wrapper.vm.$nextTick(function () {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: 'alertNoMatchWarning' });
@ -450,7 +444,7 @@ describe('address-questions.vue', () => {
});
// Assert
wrapper.vm.$nextTick(function () {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: 'alertNoMatchWarning' });
@ -461,6 +455,15 @@ describe('address-questions.vue', () => {
});
});
/**
*
* @param root0
* @param root0.mountOptions
* @param root0.props
* @param root0.isShallowMount
* @param root0.querySelectorFunction
* @param root0.geocoderResult
*/
function setupMocks({
mountOptions,
props,
@ -483,6 +486,10 @@ function setupMocks({
addListener: jest
.fn()
.mockImplementation((element, eventName, callbackFunction) => {
/**
*
* @param e
*/
function interceptedCallbackFunction(e) {
callbackFunction(e.detail);
}

View file

@ -1,26 +1,26 @@
<template>
<div role="application">
<alert
ref="alertVerificationWarning"
v-if="displayVerificationWarning"
ref="alertVerificationWarning"
class="mb-4"
cmsWidgetName="AlertVerificationWarningWidget"
alertClass="alert-warning"
v-bind:isDismissible="false" />
:isDismissible="false" />
<alert
ref="alertNoMatchWarning"
v-if="displayNoMatchWarning"
ref="alertNoMatchWarning"
class="mb-4 mt-4"
cmsWidgetName="AlertNoMatchWarningWidget"
alertClass="alert-warning"
v-bind:isDismissible="false" />
:isDismissible="false" />
<div class="row mt-2 mb-4">
<div class="col">
<textboxQuestion
id="streetAddressField"
cmsWidgetName="StreetAddressQuestionWidget"
v-model="addressModel.streetAddress"
ref="autocomplete"
v-model="addressModel.streetAddress"
cmsWidgetName="StreetAddressQuestionWidget"
inputId="autocomplete"
placeholderText="Search"
aria-haspopup=""
@ -30,36 +30,43 @@
@keydown.enter.prevent />
</div>
</div>
<transition name="fade" mode="out-in">
<transition
name="fade"
mode="out-in">
<div v-if="showAllFields">
<div class="row mt-2 mb-4" v-if="includeStreetAddress2">
<div
v-if="includeStreetAddress2"
class="row mt-2 mb-4">
<div class="col">
<textboxQuestion
ref="streetAddress2"
cmsWidgetName="StreetAddress2QuestionWidget"
v-model="addressModel.streetAddress2"
cmsWidgetName="StreetAddress2QuestionWidget"
aria-haspopup=""
inputId="streetAddress2Field"
/>
inputId="streetAddress2Field" />
</div>
</div>
<div class="row mb-4" aria-live="polite">
<div
class="row mb-4"
aria-live="polite">
<div class="col">
<textboxQuestion
cmsWidgetName="CityQuestionWidget"
v-model="addressModel.city"
ref="city"
v-model="addressModel.city"
cmsWidgetName="CityQuestionWidget"
inputId="cbf28188fdf2436688fd735915f7ee56"
disableAutoFill
validationRules="city-required" />
</div>
</div>
<div class="row mb-4" aria-live="polite">
<div
class="row mb-4"
aria-live="polite">
<div class="col">
<dropdownQuestion
cmsWidgetName="StateQuestionWidget"
v-model="addressModel.state"
ref="state"
v-model="addressModel.state"
cmsWidgetName="StateQuestionWidget"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
@ -67,9 +74,9 @@
</div>
<div class="col">
<textboxQuestion
cmsWidgetName="ZipQuestionWidget"
v-model="addressModel.zipCode"
ref="zipCode"
v-model="addressModel.zipCode"
cmsWidgetName="ZipQuestionWidget"
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
@ -101,7 +108,11 @@ defineRule('zip-code-format', regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.Z
export default {
name: 'address-questions',
emits: ['update:modelValue'], // The component emits an event
components: {
textboxQuestion,
dropdownQuestion,
alert
}, // The component emits an event
props: {
modelValue: {
type: Object,
@ -116,6 +127,7 @@ export default {
validationRules: String,
includeStreetAddress2: false
},
emits: ['update:modelValue'],
data() {
return {
displayVerificationWarning: false,
@ -133,29 +145,59 @@ export default {
},
computed: {
stateOptions: {
get: function () {
get() {
return states;
}
},
addressModel: {
get: function () {
get() {
return this.modelValue;
},
set: function (newValue) {
set(newValue) {
this.$emit('update:modelValue', newValue);
}
}
},
watch: {
matchFound: {
handler(newValue) {
if (!newValue) {
this.displayNoMatchWarning = true;
this.addressModel.city = '';
this.addressModel.state = '';
this.addressModel.zipCode = '';
this.displayVerificationWarning = false;
this.$nextTick(() => {
// Only deep watch the Address Model after a failed match
this.isAddressWatchActive = true;
});
}
}
},
addressModel: {
handler() {
if (this.isAddressWatchActive) {
this.displayNoMatchWarning = false;
this.isAddressWatchActive = false;
}
},
deep: true
}
},
mounted() {
this.setupAddressLookup();
this.showAllFields = !!this.addressModel.streetAddress;
},
methods: {
setupAddressLookup() {
const addressField1 = document.getElementById('autocomplete');
const self = this;
const url = endpoints.GooglePlaces.url.replace('{apiKey}', applicationConfig.GOOGLE_PLACES_API_KEY)
const url = endpoints.GooglePlaces.url.replace('{apiKey}', applicationConfig.GOOGLE_PLACES_API_KEY);
this.$loadScript(
url
)
this.$loadScript(url)
.then(() => {
// Script is loaded, initialize the autocomplete textbox
const autocomplete = new window.google.maps.places.Autocomplete(addressField1, {
@ -165,11 +207,9 @@ export default {
});
// Standard place_changed event handling
const autocompleteListener = window.google.maps.event.addListener(
autocomplete,
const autocompleteListener = window.google.maps.event.addListener(autocomplete,
'place_changed',
fillInAddress
);
fillInAddress);
addressField1.addEventListener('focus', () => {
// Wrapping the addressField1 element in the Google Address Autocomplete object
@ -196,13 +236,12 @@ export default {
}
addressField1.blur();
} else {
return;
}
});
addressField1.addEventListener('change', () => {
// NOTE: The "place_changed" event of the autocomplete fires after this and will use either the address the user had chosen
// NOTE: The "place_changed" event of the autocomplete fires
// after this and will use either the address the user had chosen
// using either the down / up arrows or the address the user was hovering over when they pressed "Enter."
// If a match has been previously found then do nothing
@ -215,9 +254,7 @@ export default {
}
// Get the address that the user clicked on (if any)
const clickedAddress = document.querySelector(
'.pac-container .pac-item:hover'
);
const clickedAddress = document.querySelector('.pac-container .pac-item:hover');
// If the Street Address field changed without clicking (i.e. by pressing Tab, or clicking outside the field)
if (clickedAddress === null) {
@ -228,16 +265,14 @@ export default {
const firstResult = item.textContent;
const geocoder = new window.google.maps.Geocoder();
geocoder.geocode(
{
address: firstResult
},
function (results, status) {
if (status === window.google.maps.GeocoderStatus.OK) {
fillInAddress(results[0]);
}
geocoder.geocode({
address: firstResult
},
(results, status) => {
if (status === window.google.maps.GeocoderStatus.OK) {
fillInAddress(results[0]);
}
);
});
} else {
// No addresses found for the input
self.matchFound = false;
@ -245,6 +280,10 @@ export default {
}
});
/**
*
* @param place
*/
function fillInAddress(place) {
if (!place) {
place = autocomplete.getPlace();
@ -253,7 +292,7 @@ export default {
if (place && place.address_components) {
self.matchFound = true;
self.addressModel.streetAddress = '';
self.$nextTick(function () {
self.$nextTick(() => {
for (const component of place.address_components) {
const componentType = component.types[0];
@ -264,7 +303,7 @@ export default {
}
case 'route': {
self.addressModel.streetAddress
+= ' ' + component.short_name;
+= ` ${component.short_name}`;
break;
}
case 'locality': {
@ -279,6 +318,7 @@ export default {
self.addressModel.zipCode = component.long_name;
break;
}
default:
}
}
@ -303,48 +343,11 @@ export default {
console.log('Unable to load Google Places API script');
});
}
},
mounted() {
this.setupAddressLookup();
this.showAllFields = !!this.addressModel.streetAddress;
},
watch: {
matchFound: {
handler(newValue) {
if (!newValue) {
this.displayNoMatchWarning = true;
this.addressModel.city = '';
this.addressModel.state = '';
this.addressModel.zipCode = '';
this.displayVerificationWarning = false;
this.$nextTick(function () {
// Only deep watch the Address Model after a failed match
this.isAddressWatchActive = true;
});
}
}
},
addressModel: {
handler() {
if (this.isAddressWatchActive) {
this.displayNoMatchWarning = false;
this.isAddressWatchActive = false;
}
},
deep: true
}
},
components: {
textboxQuestion,
dropdownQuestion,
alert
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
#streetAddressField {
position: relative;

View file

@ -1,12 +1,12 @@
<template>
<!-- Modal -->
<div
:id="cmsWidgetName"
class="modal fade modal-component"
v-on="{ 'hidden.bs.modal': resetButtonStyle }"
:id="this.cmsWidgetName"
tabindex="-1"
aria-labelledby="ModalComponentLabel"
aria-hidden="true">
aria-hidden="true"
v-on="{ 'hidden.bs.modal': resetButtonStyle }">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
@ -18,27 +18,27 @@
</div>
<div class="modal-body">
<label
:aria-label="this.ModalQuestionText"
:aria-label="ModalQuestionText"
class="form-label text-center w-100 mb-5 fw-bold"
v-html="this.ModalQuestionText"></label>
v-html="ModalQuestionText"></label>
</div>
<buttonQuestion
v-model="selectedValue"
class="radioQuestion"
:questionText="questionText"
:answers="ModalSelectOptionAnswers"
groupName="ChooseVehicleYear"
v-model="selectedValue"
:validationRules="validationRules"
isRequired />
<div class="modal-footer px-5 py-4">
<buttonMain
ref="buttonMain"
class="w-100"
isPrimary
ref="buttonMain"
suppressLoader
:buttonText="ModalSelectButtonText"
@click-event="buttonClick"
data-bs-dismiss="modal" />
data-bs-dismiss="modal"
@click-event="buttonClick" />
</div>
</div>
</div>
@ -50,7 +50,11 @@ import buttonMain from '@/ux-components/button-main/button-main';
import buttonQuestion from '@/digital-components/button-question/button-question';
export default {
name: 'buttonQuestionModal',
name: 'button-question-modal',
components: {
buttonMain,
buttonQuestion
},
props: {
cmsWidgetName: String
},
@ -69,15 +73,11 @@ export default {
resetButtonStyle() {
this.$refs.buttonMain.resetButtonStyle();
}
},
components: {
buttonMain,
buttonQuestion
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.modal {
top: auto;
bottom: 0;

View file

@ -4,13 +4,22 @@
: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>
<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"
class="my-4 caption modal-sub-body"
v-html="ModalSubBodyText"></p>
</modal>
</template>
@ -20,6 +29,9 @@ import modal from '@/digital-components/modal/modal';
export default {
name: 'content-group-modal',
components: {
modal
},
props: {
cmsWidgetName: String
},
@ -54,14 +66,11 @@ export default {
footerButtonClick() {
this.$refs[this.ModalName].closeModal();
}
},
components: {
modal
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.modal {
&.modal-component {
.modal-dialog {

View file

@ -1,10 +1,14 @@
<template>
<div v-if="isModalVisible" class="container-fluid modal-loader">
<div
v-if="isModalVisible"
class="container-fluid modal-loader">
<div class="row g-2 h-100 d-flex align-items-center">
<div class="container-fluid overflow-hidden">
<div class="row h-100">
<div class="col text-center tagbg mb-4">
<div class="spinner-border text-danger" role="status">
<div
class="spinner-border text-danger"
role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
@ -52,7 +56,7 @@
<script>
export default {
name: 'Modal',
name: 'modal',
data() {
return {
isModalVisible: false
@ -63,23 +67,21 @@ export default {
// Display modal
this.isModalVisible = true;
// Force page reload on back button
window.addEventListener(
'pageshow',
function (evt) {
window.addEventListener('pageshow',
(evt) => {
if (evt.persisted) {
setTimeout(function () {
setTimeout(() => {
window.location.reload();
}, 10);
}
},
false
);
false);
}
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.modal-loader {
position: fixed;
top: 0;

View file

@ -1,23 +1,34 @@
<template>
<input v-if="type == 'button'" type="button" class="nav-button" v-bind:value="text" v-on:click="navTo"/>
<p v-else type="" class="nav-text" v-on:click="navTo">{{text}}</p>
<input
v-if="type == 'button'"
type="button"
class="nav-button"
:value="text"
@click="navTo" />
<p
v-else
type=""
class="nav-text"
@click="navTo">
{{ text }}
</p>
</template>
<script>
export default ({
name: 'navButton',
name: 'nav-button',
props: {
text: String,
scenario: String,
type: String
},
methods: {
navTo(){
this.$router.navigate(this.scenario, this.$route)
navTo() {
this.$router.navigate(this.scenario, this.$route);
}
}
})
});
</script>
<style lang="scss" scoped>

View file

@ -202,15 +202,15 @@ function setupMocks() {
answerResult: '',
answerText: 'No',
nextQuestionSequence: 3
},
}
]
},
}
],
glassLocation: 'Windshield',
glassName: 'Single',
answerKey: 'Windshield-Single',
answerData: null
},
}
]
};
};
@ -233,16 +233,16 @@ function setupMocks() {
'Is your vehicle equipped with a heated windshield that melts snow and ice from underneath the windshield wiper blades?',
selectedAnswerText: 'Yes',
questionNum: 2
},
}
]
},
}
]
};
};
const mountOptions = getMountOptions({
mixins: [baseMixin, vehicleQuestionsMixin]
});
mountOptions['attachTo'] = document.body;
mountOptions.attachTo = document.body;
const wrapper = shallowMount(questionsPageLayout, mountOptions);
return { wrapper };

View file

@ -8,7 +8,10 @@
<div class="row px-3">
<div class="col">
<div class="select-car-form rounded">
<vehicleBanner class="mt-2 mb-4" cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
<vehicleBanner
class="mt-2 mb-4"
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<siteSubHeader cmsWidgetName="SiteSubHeaderWidget" />
<alert
ref="alertFewMoreQuestions"
@ -16,25 +19,27 @@
alertClass="alert-warning"
:manualHeadline="alertFewMoreQuestionsHeader"
:manualCopy="alertFewMoreQuestionsCopy"
v-bind:isDismissible="false"
class="mt-5"/>
<div v-for="(questionsDatum, i) in questionsData" :key="questionsDatum.key">
:isDismissible="false"
class="mt-5" />
<div
v-for="(questionsDatum, i) in questionsData"
:key="questionsDatum.key">
<questionChain
ref="questionChain"
v-model="selectedAnswers[questionsDatum.answerKey]"
:questionData="questionsDatum.questions"
:index="i"
v-if="showThisQuestionChain(questionsDatum, i)"
:answerKey="questionsDatum.answerKey"
:validationRules="validationRules" />
v-if="showThisQuestionChain(questionsDatum, i)"
ref="questionChain"
v-model="selectedAnswers[questionsDatum.answerKey]"
:questionData="questionsDatum.questions"
:index="i"
:answerKey="questionsDatum.answerKey"
:validationRules="validationRules" />
</div>
<siteFooter
class="mt-5"
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!isMetaValid"
@back-clicked="handleBackButtonAction"
@ForwardClicked="handleForwardButtonAction" />
ref="siteFooter"
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!isMetaValid"
@back-clicked="handleBackButtonAction"
@ForwardClicked="handleForwardButtonAction" />
</div>
</div>
</div>
@ -56,6 +61,15 @@ import loadingModal from '@/iss-components/loading-modal/loading-modal.vue';
export default {
name: 'questions-page',
components: {
siteHeader,
vehicleBanner,
alert,
questionChain,
siteSubHeader,
siteFooter,
loadingModal
},
props: {
isMetaValid: Boolean,
alertFewMoreQuestionsHeader: String,
@ -65,6 +79,7 @@ export default {
modelValue: Object,
index: Number
},
emits: ['update:modelValue', 'forwardButtonAction', 'back-click'],
computed: {
selectedAnswers: {
get() {
@ -92,20 +107,11 @@ export default {
showLoadingModal() {
this.$refs.loadingModal.showModal();
}
},
components: {
siteHeader,
vehicleBanner,
alert,
questionChain,
siteSubHeader,
siteFooter,
loadingModal
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.questions-page {
.question-text {
margin-bottom: 0.5rem;

View file

@ -1,36 +1,47 @@
<template>
<div>
<footer class="footer container-fluid g-5 px-0" id="infoBox">
<footer
id="infoBox"
class="footer container-fluid g-5 px-0">
<div class="row d-flex flex-row-reverse align-items-center vw-100 mx-0">
<div class="col button-col d-flex px-0" id="stacked" >
<div
id="stacked"
class="col button-col d-flex px-0">
<buttonMain
v-if="!isForwardButtonHidden"
ref="buttonMain"
isPrimary
:buttonText="buttonText"
loaderColor="white"
:class="(disableForwardAction || isForwardActionDisabled) && 'form-test-invalid'"
:aria-disabled="isForwardActionDisabled"
:isDisabled="isForwardActionDisabled"
@click-event="buttonClick"
data-bs-target="#footerModal"
data-bs-dismiss="modal"
data-test-id="site-footer-main-button" />
v-if="!isForwardButtonHidden"
ref="buttonMain"
isPrimary
:buttonText="buttonText"
loaderColor="white"
:class="(disableForwardAction || isForwardActionDisabled) && 'form-test-invalid'"
:aria-disabled="isForwardActionDisabled"
:isDisabled="isForwardActionDisabled"
data-bs-target="#footerModal"
data-bs-dismiss="modal"
data-test-id="site-footer-main-button"
@click-event="buttonClick" />
</div>
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 px-0 text-break">
<div
v-if="!isBackButtonHidden"
class="col-auto link-col py-1 px-0 text-break">
<textLink
linkType="navigation"
:text="backLink"
@click-event="linkClick"
href="javascript:void(0)"
data-bs-target="#footerModal"
data-bs-dismiss="modal"
data-test-id="site-footer-back-button" />
linkType="navigation"
:text="backLink"
href="javascript:void(0)"
data-bs-target="#footerModal"
data-bs-dismiss="modal"
data-test-id="site-footer-back-button"
@click-event="linkClick" />
</div>
</div>
</footer>
<div class="footerImage" id="imgCity" v-if="footerImageURL" >
<img id="siteFooterImage" :src="footerImageURL" />
<div
v-if="footerImageURL"
id="imgCity"
class="footerImage">
<img
id="siteFooterImage"
:src="footerImageURL" />
</div>
</div>
</template>
@ -38,21 +49,21 @@
<script>
import textLink from '@/ux-components/text-link/text-link';
import buttonMain from '@/ux-components/button-main/button-main';
import { issPageValues } from '@/router/router-constants/issPage-values'
import { issPageValues } from '@/router/router-constants/issPage-values';
export default {
name: 'siteFooter',
emits: ['backClicked', 'forwardClicked'],
name: 'site-footer',
components: {
textLink,
buttonMain
},
props: {
isForwardActionDisabled: Boolean,
isBackButtonHidden: { type: Boolean, default: false },
isForwardButtonHidden: { type: Boolean, default: false },
cmsWidgetName: String
},
components: {
textLink,
buttonMain
},
emits: ['backClicked', 'forwardClicked'],
data() {
return {
paddingHeight: 0,
@ -60,18 +71,6 @@ export default {
disableForwardAction: false
};
},
mounted() {
this.paddingHeight = this.includeFooterImage ? this.getFooterInfoBoxHeight() + 16 : this.getFooterInfoBoxHeight() + 24;
this.$nextTick(() => {
window.addEventListener('resize', this.onResize);
});
},
beforeUnmount() {
window.removeEventListener('resize', this.onResize);
},
unmounted() {
document.onkeydown = null;
},
computed: {
backLink() {
return this.getCmsContent(this.cmsWidgetName, 'BackButtonText');
@ -86,9 +85,21 @@ export default {
},
includeFooterImage() {
const currentPageName = this.getPageNameByQueryString();
return (currentPageName.toLowerCase() == issPageValues.WELCOME_PAGE);
return (currentPageName.toLowerCase() === issPageValues.WELCOME_PAGE);
}
},
mounted() {
this.paddingHeight = this.includeFooterImage ? this.getFooterInfoBoxHeight() + 16 : this.getFooterInfoBoxHeight() + 24;
this.$nextTick(() => {
window.addEventListener('resize', this.onResize);
});
},
beforeUnmount() {
window.removeEventListener('resize', this.onResize);
},
unmounted() {
document.onkeydown = null;
},
methods: {
onResize() {
this.paddingHeight = this.getFooterInfoBoxHeight();
@ -98,15 +109,11 @@ export default {
},
removeLoader() {
this.$refs.buttonMain.removeLoader();
document.onkeydown = function (e) {
return true;
};
document.onkeydown = () => true;
},
buttonClick() {
// prevent keyboard input after button click
document.onkeydown = function (e) {
return false;
};
document.onkeydown = () => false;
this.$emit('forwardClicked');
},
linkClick() {

View file

@ -1,15 +1,31 @@
<template>
<div class="menu-modal-container">
<button class="menu-button" type="button" :class="[isActive ? 'active' : '']" aria-label="Hamburger Menu (modal window)" @click="toggle()">
<button
class="menu-button"
type="button"
:class="[isActive ? 'active' : '']"
aria-label="Hamburger Menu (modal window)"
@click="toggle()">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</button>
</div>
<!-- Modal -->
<div class="modal menu-modal fade" data-bs-backdrop="false" id="footerModal" tabindex="-1" aria-labelledby="footerModalLabel" aria-hidden="true">
<div
id="footerModal"
class="modal menu-modal fade"
data-bs-backdrop="false"
tabindex="-1"
aria-labelledby="footerModalLabel"
aria-hidden="true">
<div class="menu-modal-container">
<button class="menu-button" type="button" :class="[isActive ? 'active' : '']" aria-label="Hamburger Menu (modal window)" @click="toggle()">
<button
class="menu-button"
type="button"
:class="[isActive ? 'active' : '']"
aria-label="Hamburger Menu (modal window)"
@click="toggle()">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
@ -18,40 +34,44 @@
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header visually-hidden">
<h5 class="modal-title" id="footerModalLabel">Footer Navigation</h5>
<h5
id="footerModalLabel"
class="modal-title">
Footer Navigation
</h5>
</div>
<div class="modal-body d-flex flex-column">
<textLink
linkType="navigation"
text="Terms of service"
href="//www.safelite.com/terms-of-use"
target="_blank" />
linkType="navigation"
text="Terms of service"
href="//www.safelite.com/terms-of-use"
target="_blank" />
<textLink
linkType="navigation"
text="Your privacy choices"
href="//www.safelite.com/privacy-center"
target="_blank">
<template v-slot:after-text>
linkType="navigation"
text="Your privacy choices"
href="//www.safelite.com/privacy-center"
target="_blank">
<template #after-text>
<img
class="ccpa-icon"
data-id="ccpa-icon"
src="~@/assets/img/icons/ccpa-icon.svg"
alt="Your privacy choices" />
class="ccpa-icon"
data-id="ccpa-icon"
src="~@/assets/img/icons/ccpa-icon.svg"
alt="Your privacy choices" />
</template>
</textLink>
<textLink
linkType="navigation"
text="Warranty"
href="//www.safelite.com/national-lifetime-warranty"
target="_blank" />
linkType="navigation"
text="Warranty"
href="//www.safelite.com/national-lifetime-warranty"
target="_blank" />
<textLink
linkType="navigation"
text="Notice at collection"
href="https://www.safelite.com/ccpa-privacy-policy"
target="_blank" />
linkType="navigation"
text="Notice at collection"
href="https://www.safelite.com/ccpa-privacy-policy"
target="_blank" />
</div>
<div class="modal-footer d-flex justify-content-start">
&copy; {{new Date().getFullYear()}} Safelite Group
&copy; {{ new Date().getFullYear() }} Safelite Group
</div>
</div>
</div>
@ -63,7 +83,10 @@ import textLink from '@/ux-components/text-link/text-link';
import { Modal } from 'bootstrap';
export default {
name: 'menuModal',
name: 'menu-modal',
components: {
textLink
},
data() {
return {
isActive: false,
@ -88,11 +111,8 @@ export default {
});
}
}
},
components: {
textLink
}
}
};
</script>
<style lang="scss" scoped>

View file

@ -1,7 +1,6 @@
import siteHeader from '@/iss-components/site-header/site-header';
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { useMainStore } from '@/store/';
describe('site-header', () => {
test('renders the logo image', () => {

View file

@ -1,17 +1,23 @@
<template>
<div>
<div class="site-header d-flex justify-content-center align-items-center flex-column py-2" :style="logoBackgroundStyle">
<img id="siteHeaderImage" class="img-fluid" :src="imageSrc" :alt="altText" />
<menuModal/>
<div
class="site-header d-flex justify-content-center align-items-center flex-column py-2"
:style="logoBackgroundStyle">
<img
id="siteHeaderImage"
class="img-fluid"
:src="imageSrc"
:alt="altText" />
<menuModal />
</div>
<alert
v-if="displayGlobalAlert"
class="position-absolute rounded-0 w-100 border-0 shadow-sm start-0"
cmsWidgetName="GlobalAlert"
:manualHeadline="globalAlertMessage.messageHeadline"
:manualCopy="globalAlertMessage.messageCopy"
:alertClass="globalAlertMessage.type"
v-bind:isDismissible="globalAlertMessage.isDismissible" />
v-if="displayGlobalAlert"
class="position-absolute rounded-0 w-100 border-0 shadow-sm start-0"
cmsWidgetName="GlobalAlert"
:manualHeadline="globalAlertMessage.messageHeadline"
:manualCopy="globalAlertMessage.messageCopy"
:alertClass="globalAlertMessage.type"
:isDismissible="globalAlertMessage.isDismissible" />
</div>
</template>
@ -22,7 +28,14 @@ import eventBus from '@/helpers/event-bus/event-bus';
import { globalEvents } from '@/constants/events';
export default ({
name: 'siteHeader',
name: 'site-header',
components: {
menuModal,
alert
},
props: {
cmsWidgetName: String
},
data() {
return {
displayGlobalAlert: false,
@ -35,9 +48,6 @@ export default ({
headerAnswers: null
};
},
props: {
cmsWidgetName: String
},
computed: {
imageSrc() {
return this.headerAnswers ? this.headerAnswers.AnswerImageUrl : '';
@ -51,23 +61,12 @@ export default ({
};
}
},
methods: {
setupHeader() {
const answers = this.getCmsContent(this.cmsWidgetName, 'Answers');
if(Array.isArray(answers)) {
this.headerAnswers = answers.filter(x => x.AccountNumber == this.mainStore.issConfig.accountNumber)[0]
this.headerAnswers = !this.headerAnswers ? answers.filter(x => x.AccountNumber === '0')[0] : this.headerAnswers;
}
}
},
mounted() {
this.$nextTick(this.setupHeader);
// Check if alert event is on the bus
const alertEvent = eventBus.readAndPopEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
const alertEvent = eventBus.readAndPopEventFromBus(globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND);
// If alert event is on the bus, then display the alert
if (alertEvent !== undefined) {
this.displayGlobalAlert = true;
@ -77,17 +76,21 @@ export default ({
// load client customization overrides
const clientOverrideClass = this.mainStore.issConfig.styleSheet.trim();
if (clientOverrideClass
&& clientOverrideClass != ''
&& !document.getElementsByTagName('body')[0].className.split(' ').includes(clientOverrideClass))
{
document.getElementsByTagName('body')[0].className += ' ' + clientOverrideClass;
&& clientOverrideClass !== ''
&& !document.getElementsByTagName('body')[0].className.split(' ').includes(clientOverrideClass)) {
document.getElementsByTagName('body')[0].className += ` ${clientOverrideClass}`;
}
},
components: {
menuModal,
alert
methods: {
setupHeader() {
const answers = this.getCmsContent(this.cmsWidgetName, 'Answers');
if (Array.isArray(answers)) {
this.headerAnswers = answers.filter((x) => x.AccountNumber == this.mainStore.issConfig.accountNumber)[0];
this.headerAnswers = !this.headerAnswers ? answers.filter((x) => x.AccountNumber === '0')[0] : this.headerAnswers;
}
}
}
})
});
</script>
<style lang="scss" scoped>

View file

@ -1,23 +1,20 @@
<template>
<button
@click="handleClick"
class="back-button-wrapper"
:aria-label="backButtonAccessibleText"
>
@click="handleClick">
<svg
xmlns="http://www.w3.org/2000/svg"
width="34px"
height="30px"
viewBox="-2.2 -6 30 30"
>
viewBox="-2.2 -6 30 30">
<g id="Layer_3">
<path
class="outer"
fill="#FFFFFF"
d="M19.934-1.434h-8.486c-1.683,0-2.999,0.528-4.021,1.614L1.685,6.146C0.78,7.079,0.338,8.027,0.338,9.041
c0,1.028,0.444,1.979,1.359,2.908l5.753,5.938c1.054,1.093,2.403,1.647,4.007,1.647l8.475,0.01c2.861,0,4.568-1.692,4.568-4.528
V3.094C24.5,0.259,22.793-1.434,19.934-1.434L19.934-1.434z"
/>
V3.094C24.5,0.259,22.793-1.434,19.934-1.434L19.934-1.434z" />
</g>
<g id="Layer_2">
<path
@ -29,8 +26,7 @@
c0-0.215,0.078-0.42,0.235-0.566l2.861-2.861l-2.861-2.862c-0.156-0.156-0.234-0.352-0.234-0.566c0-0.458,0.362-0.801,0.801-0.801
c0.224,0,0.41,0.069,0.556,0.225l2.881,2.872l2.891-2.881c0.166-0.166,0.353-0.234,0.566-0.234c0.439,0,0.801,0.352,0.801,0.801
c0,0.225-0.078,0.4-0.234,0.576l-2.871,2.871l2.861,2.852c0.146,0.156,0.234,0.352,0.234,0.576c0,0.449-0.361,0.82-0.811,0.82
c-0.225,0-0.43-0.098-0.586-0.244l-2.852-2.871l-2.852,2.871C11.203,13.165,10.999,13.253,10.773,13.253z"
/>
c-0.225,0-0.43-0.098-0.586-0.244l-2.852-2.871l-2.852,2.871C11.203,13.165,10.999,13.253,10.773,13.253z" />
</g>
</svg>
</button>
@ -38,7 +34,7 @@
<script>
export default {
name: 'buttonBack',
name: 'button-back',
props: {
backButtonAccessibleText: {
type: String,
@ -46,6 +42,7 @@ export default {
default: ''
}
},
emits: ['click-event'],
methods: {
handleClick() {
this.$emit('click-event');
@ -54,7 +51,7 @@ export default {
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.back-button-wrapper {
padding: 0;
position: relative;

View file

@ -1,19 +1,24 @@
<template>
<div>
<div class="d-flex align-items-center justify-content-center container-fluid overflow-hidden">
<h5 class="text-center fw-normal mb-0 subheader-primary" :class="headerColor">
<h5
class="text-center fw-normal mb-0 subheader-primary"
:class="headerColor">
<span>
{{ content }}
</span>
<buttonBack
v-if="hasBackButton"
:backButtonAccessibleText="backButtonAccessibleText"
@click-event="clickEvent"
/>
@click-event="clickEvent" />
</h5>
</div>
<div class="subheader-secondary d-flex align-items-center container-fluid overflow-hidden" :class="justifySubheader">
<p class="fw-normal mb-0" :class="alternateFormatting">
<div
class="subheader-secondary d-flex align-items-center container-fluid overflow-hidden"
:class="justifySubheader">
<p
class="fw-normal mb-0"
:class="alternateFormatting">
<span v-html="subText">
</span>
</p>
@ -25,7 +30,8 @@
import buttonBack from '@/iss-components/site-sub-header/button-back/button-back';
export default ({
name: 'siteSubHeader',
name: 'site-sub-header',
components: { buttonBack },
props: {
cmsWidgetName: String,
hasBackButton: Boolean,
@ -33,41 +39,40 @@ export default ({
issContainingPage: String,
darkGraySubText: Boolean
},
emits: ['click-event'],
computed: {
content() {
return this.getCmsContent(this.cmsWidgetName, 'SubHeaderText')
return this.getCmsContent(this.cmsWidgetName, 'SubHeaderText');
},
subText() {
const subText = this.getCmsContent(this.cmsWidgetName, 'SecondaryText');
return subText ?? '';
},
backButtonAccessibleText() {
return this.getCmsContent(this.cmsWidgetName, 'BackButtonAccessibleText')
return this.getCmsContent(this.cmsWidgetName, 'BackButtonAccessibleText');
},
headerColor() {
return this.subText ? 'dark-header' : 'light-header';
},
justifySubheader() {
return (this.justification?.toLowerCase() === 'left') ?
'justify-content-left' :
'justify-content-center';
return (this.justification?.toLowerCase() === 'left')
? 'justify-content-left'
: 'justify-content-center';
},
alternateFormatting() {
// override for service-packages unique style
if (this.issContainingPage?.toLowerCase() === 'service-packages') {
return 'service-packages-subtext mt-4 mb-2 px-5';
} else {
return this.darkGraySubText ? 'dark-gray' : 'light-gray';
}
return this.darkGraySubText ? 'dark-gray' : 'light-gray';
}
},
methods: {
clickEvent() {
this.$emit('click-event');
}
},
components: { buttonBack }
})
}
});
</script>
<style lang="scss" scoped>

View file

@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import steeringTextModal from './steering-text';
import { createApp } from 'vue';
import App from '@/App';
import { createPinia } from 'pinia';
import App from '@/App.vue';
import { createApp } from 'vue';
import steeringTextModal from './steering-text';
describe('steering-text.vue', () => {
test('Should display steering text from CMS for state MA', async () => {

View file

@ -1,11 +1,13 @@
<template>
<div>
<textBlock
cmsWidgetName="MASteeringText"
id="SteeringTextContent"
/>
cmsWidgetName="MASteeringText" />
<div>
<p class="small" v-if="isStateMA" v-html="MASteeringText" ></p>
<p
v-if="isStateMA"
class="small"
v-html="MASteeringText"></p>
</div>
</div>
</template>
@ -14,20 +16,21 @@
import { useMainStore } from '@/store';
export default {
name: 'steeringTextModal',
name: 'steering-text-modal',
setup() {
const mainStore = useMainStore();
return { mainStore };
},
computed: {
MASteeringText() {
return this.getCmsContent('MASteeringText', 'BodyText');
},
isStateMA() {
if (this.mainStore.order.customer.address.state=='MA') {
return true
if (this.mainStore.order.customer.address.state === 'MA') {
return true;
}
return false;
}
},
setup() {
const mainStore = useMainStore();
return { mainStore };
}
}
};
</script>

View file

@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { vehicleCategories } from '@/constants/vehicle-categories.js';
import vehicleBanner from './vehicle-banner';
import { useMainStore } from '@/store';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { mapStores } from 'pinia';
import { createPinia, mapStores } from 'pinia';
import App from '@/App.vue';
import vehicleBanner from './vehicle-banner';
const cmsData = { VehicleBannerWidget:
{
@ -16,8 +16,7 @@ const cmsData = { VehicleBannerWidget:
VanUnmatchedVehicleIcon: 'van-placeholder.jpg',
CommercialVanUnmatchedVehicleIcon: 'commercial-placeholder.jpg',
SuvUnmatchedVehicleIcon: 'suv-placeholder.jpg'
}
}
} };
const vueApp = createApp(App);
const pinia = createPinia();
@ -37,7 +36,7 @@ const mockMixin = {
return vehicleCategories;
}
}
}
};
describe('vehicleBanner', () => {
test('renders the blurrycar image when displayGenericVehicleImage is true', async () => {
@ -81,7 +80,7 @@ describe('vehicleBanner', () => {
mixins: [mockMixin]
});
var iconUrl = wrapper.vm.vehicleImageToDisplay;
const iconUrl = wrapper.vm.vehicleImageToDisplay;
expect(iconUrl).toEqual('car-placeholder.jpg');
wrapper.unmount();
@ -106,7 +105,7 @@ describe('vehicleBanner', () => {
mixins: [mockMixin]
});
var vehicleIcon = wrapper.vm.getUnmatchedVehicleIcon();
const vehicleIcon = wrapper.vm.getUnmatchedVehicleIcon();
expect(vehicleIcon).toEqual(expectedIcon);
expect(wrapper.find('img').attributes('class')).toContain('vehicle-image');

View file

@ -3,8 +3,7 @@
<img
class="vehicle-image img-fluid"
:src="vehicleImageToDisplay"
alt=""
/>
alt="" />
</div>
</template>
@ -24,7 +23,7 @@ export default {
return this.genericVehicleImage;
}
const imageUrl = this.mainStore.order.vehicle.imageUrl;
const { imageUrl } = this.mainStore.order.vehicle;
if (!imageUrl || imageUrl === 'NULL') {
return this.getUnmatchedVehicleIcon();
}
@ -52,7 +51,7 @@ export default {
},
methods: {
getUnmatchedVehicleIcon() {
switch (this.mainStore.order.vehicle.category){
switch (this.mainStore.order.vehicle.category) {
case this.vehicleCategories.CAR:
return this.carUnmatchedVehicleIcon;
case this.vehicleCategories.SUV: