Fixed test, added documentation to store, broke out processing functions for both documentation purposes and to reduce overall size of store file, sorted store actions methods.

This commit is contained in:
DavidAtSafelite 2023-10-12 15:16:24 -04:00
parent 33420c6077
commit 46cdd41772
10 changed files with 1842 additions and 1160 deletions

View file

@ -12,7 +12,8 @@
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit --coverage --ci",
"test:unit:lite": "vue-cli-service test:unit --ci"
"test:unit:lite": "vue-cli-service test:unit --ci",
"test:pc": "vue-cli-service test:unit site-footer.spec.js"
},
"dependencies": {
"axios": "^1.4.0",

View file

@ -7,6 +7,15 @@
</router-view>
</template>
<script>
// eslint-disable-next-line no-extend-native
Array.prototype.safeFilter = (propertyName) => {
if (!propertyName) return this;
if (!this?.length) return [];
return this.map((x) => x[propertyName])
.filter((x) => x);
};
</script>
<style lang="scss">
@import "./node_modules/bootstrap/scss/bootstrap";
@import "@/styles/common-styles.scss";

View file

@ -462,13 +462,16 @@ export default {
if (direction === 'future') {
// first 0, then 1
for (let i = 0; i <= monthsAfterToLoadOffset; i++) {
months.push(await this.getMonthData(i, options));
months.push(this.getMonthData(i, options));
}
// See https://eslint.org/docs/latest/rules/no-await-in-loop
Promise.all(months);
} else if (direction === 'past') {
// first 0, then -1
for (let i = 0; i >= 0 - monthsBeforeToLoadOffset; i--) {
months.unshift(await this.getMonthData(i, options));
months.unshift(this.getMonthData(i, options));
}
Promise.all(months);
} else {
// TODO - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED
// for (let i = monthsAfterToLoadOffset; i >= monthsBeforeToLoadOffset; i--) {

View file

@ -11,7 +11,7 @@
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="javascript:void(0)"
useLoadingModal
@click-event="navigateWithScenario(getRouterLinkRouteFromCopy(copy))" />
@clickEvent="navigateWithScenario(getRouterLinkRouteFromCopy(copy))" />
</span>
<span v-else-if="doesCopyContainTextLink(copy)">
<textLink
@ -19,7 +19,7 @@
:text="getRouterLinkDisplayTextFromCopy(copy)"
:href="getExternalLink(copy)"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
@click-event="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))" />
@clickEvent="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))" />
</span>
<span
v-else
@ -57,6 +57,7 @@ export default {
cmsWidgetName: String,
marginTopSizeOverride: Number // override mt-2 with a bootstrap size from 0-5 or auto
},
emits: ['textLinkClicked'],
computed: {
pageQueryString() {
return applicationConfig.PAGE_QUERYSTRING;

View file

@ -8,7 +8,7 @@ export function getDamageString() {
const { isRepair } = mainStore.damage;
const damageLocations = mainStore.damage.glassToReplace;
if (isRepair || !damageLocations || !Array.isArray(damageLocations)) {
if (isRepair || !damageLocations?.length) {
return '';
}

View file

@ -1,6 +1,15 @@
import { useMainStore } from '@/store';
import { updateOrCreateISSCookie } from '@/helpers/cookie-helper';
/*
Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing
*/
async function saveSessionHelper(store) {
const savedSessionInfo = await store.saveSession();
store.setSaveSessionInfo(savedSessionInfo.data);
updateOrCreateISSCookie();
}
/*
Will call API to save existing order, or create new one depending where it's called from.
This will also set Referral information in the store after saving, and then
@ -8,8 +17,8 @@ import { updateOrCreateISSCookie } from '@/helpers/cookie-helper';
*/
export async function saveSession({ shouldAwaitSaveSessionQueue = false }) {
const store = useMainStore();
var saveSessionPromise = store.applicationUser.saveSessionPromise
? store.applicationUser.saveSessionPromise.then(() => { return saveSessionHelper(store); })
const saveSessionPromise = store.applicationUser.saveSessionPromise
? store.applicationUser.saveSessionPromise.then(() => saveSessionHelper(store))
: saveSessionHelper(store);
store.setSaveSessionPromise(saveSessionPromise);
@ -18,12 +27,3 @@ export async function saveSession({ shouldAwaitSaveSessionQueue = false }) {
await saveSessionPromise;
}
}
/*
Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing
*/
async function saveSessionHelper(store) {
const savedSessionInfo = await store.saveSession();
store.setSaveSessionInfo(savedSessionInfo.data);
updateOrCreateISSCookie();
}

View file

@ -10,26 +10,24 @@ const mockMixin = {
};
describe('site-footer.vue', () => {
// TODO: fix this so that it works correctly (toHaveBeenCalled() <-- )
it.skip('Should emit ForwardClicked on button click', async () => {
it('Should emit ForwardClicked on button click', async () => {
// Act
const wrapper = mount(siteFooter, {
mixins: [mockMixin]
});
wrapper.vm.buttonClick();
// Assert
expect(wrapper.emitted().forwardClicked[0]).toHaveBeenCalled;
expect(wrapper.emitted().forwardClicked).toBeTruthy(); // This is how to verify emitted custom events.
});
// TODO: fix this so that it works correctly (toHaveBeenCalled() <--
it.skip('Should emit BackClicked on link click', async () => {
it('Should emit BackClicked on link click', async () => {
// Act
const wrapper = mount(siteFooter, {
mixins: [mockMixin]
});
wrapper.vm.linkClick();
// Assert
expect(wrapper.emitted().backClicked[0]).toHaveBeenCalled;
expect(wrapper.emitted().backClicked).toBeTruthy(); // This is how to verify emitted custom events.
});
it('Should change button text when update button text is called', async () => {

View file

@ -0,0 +1,385 @@
/* eslint-disable no-plusplus */
/* eslint-disable no-param-reassign */
import getDateDifferenceInDays from '@/helpers/date-helper';
if (!Array.prototype.safeFilter) {
// eslint-disable-next-line no-extend-native
Array.prototype.safeFilter = (propertyName) => {
if (!propertyName) return this;
if (!this?.length) return [];
return this.map((x) => x[propertyName])
.filter((x) => x);
};
}
/**
* @function addPricesToLineItems
* @memberof StoreProcessors
* @author T-Wrecks Team
* @copyright Safelite
* @summary
* Accepts an array of items and an array of corresponding prices, then
* adds the price data to the line items by collating on partNumber.
* The added properties are "**laborAmount**", "**sellingPrice**" and "**kitPrice**".
* @remarks
* This will almost certainly change when Insurance Pricing is live.
* @param {Array} lineItems
* An array filled with order items (windshield, wipers, raindefense, etc.)
* @param {Array} pricingLineItems
* An array filled with prices for repair items.
* @returns {Array}
* The line items array with price data added.
* @static
*/
const addPricesToLineItems = (lineItems, pricingLineItems) => {
if (!lineItems) throw new Error('Invalid line items specified.');
if (!pricingLineItems) throw new Error('Invalid pricing line items specified.');
lineItems.forEach((lineItem) => {
const lineItemIndex = pricingLineItems
.findIndex((pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber);
if (lineItem.childParts) {
addPricesToLineItems(lineItem.childParts, pricingLineItems);
}
if (lineItemIndex > -1) {
const pricedLineItem = pricingLineItems[lineItemIndex];
lineItem.laborAmount = pricedLineItem.laborAmount;
lineItem.sellingPrice = pricedLineItem.sellingPrice;
lineItem.kitPrice = pricedLineItem.kitPrice;
}
});
return lineItems;
};
/**
* @function buildQueryStringParameterFromArrayOfComplexObjects
* @memberof StoreProcessors
* @author T-Wrecks Team
* @copyright Safelite
* @summary
* Accepts an array of objects and the name of a parameter present on these objects.
* Extracts the value of these properties so as to create a query string snippet containing
* this data in the format "propertyName[index].[key]=[value]&..."
* @param {Array} arrayOfObjects
* Array of items containing data to transform into a query string snippet.
* @param {string} parameterName
* The name of the parameter used to extract data from the "arrayOfObjects".
* @returns {string}
* A string containing the query string snippet.
* @static
*/
const buildQueryStringParameterFromArrayOfComplexObjects = (arrayOfObjects, parameterName) => {
if (!arrayOfObjects) throw new Error('Invalid array of objects specified.');
if (!parameterName) throw new Error('Invalid parameter name specified.');
let queryStringParameter = '';
for (let i = 0; i < arrayOfObjects.length; i++) {
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(arrayOfObjects[i])) {
queryStringParameter += `${parameterName}[${i}].${key}=${value}&`;
}
}
// Remove trailing &
return queryStringParameter.slice(0, -1);
};
/**
* @function convertGlassPieceNamingFromApi
* @memberof StoreProcessors
* @author T-Wrecks Team
* @copyright Safelite
* @summary
* Accepts an array with glass data properties.
* Array is modified such that "glassPiece", "location" and "name" properties are removed (if present) and
* location/name data is moved to properties "**glassLocation**" and "**glassName**".
* @remarks
* This is one of a few functions that deal with glassLocation data.
* Modified to be a bit more fault tolerant/flexible.
* Essentially the reverse of convertGlassPieceToBackEndCompatibleFormat.
* Could potentially refactor all such to be more DRY.
* @param {Array} glassArray
* An array of glass parts with location and name defined.
* @returns {Array}
* A modified glass array with data defined on **glassLocation** and **glassName** properties.
* @static
*/
const convertGlassPieceNamingFromApi = (glassArray) => {
if (!glassArray) return [];
return glassArray.map((glass) => {
glass.glassLocation = glass.glassPiece?.location ?? glass.location ?? glass.glassLocation;
glass.glassName = glass.glassPiece?.name ?? glass.name ?? glass.glassName;
delete glass.glassPiece;
delete glass.location;
delete glass.name;
return glass;
});
};
/**
* @function convertGlassPieceToBackEndCompatibleFormat
* @memberof StoreProcessors
* @author T-Wrecks Team
* @copyright Safelite
* @summary
* Accepts an array with glass data properties.
* Array is modified such that glassLocation/glassName data is moved to properties "**location**" and "**name**".
* @remarks
* This is one of a few functions that deal with glassLocation data.
* Could potentially be combined with very similar convertResultsForApi
* @param {Array} glassPieces
* An array of glass parts with glassLocation and glassName defined.
* @returns {Array}
* A modified glass array with data defined on **location** and **name** properties.
* @static
*/
// There was no meaningful difference between the below and convertGlassPieceNamingForApi
const convertGlassPieceToBackEndCompatibleFormat = (glassPieces) => {
if (!glassPieces?.length) return [];
// check if array already converted. (likely when a session has been saved previously and then reloaded)
if (glassPieces[0].location !== undefined) {
return glassPieces;
}
return glassPieces.map((glassPiece) => ({
location: glassPiece.glassLocation,
name: glassPiece.glassName
}));
};
/**
* @function convertResultsForApi
* @memberof StoreProcessors
* @author T-Wrecks Team
* @copyright Safelite
* @summary
* Accepts a source array and maps to an API friendly format.
* Array with "**glassLocation**" is mapped to "**location**", "**glassName**" is mapped to
* "**name**", and property "result" is simply copied if present.
* @remarks
* This is one of a few functions that deal with glassLocation data.
* This one also copies result data if present.
* Could potentially be refactored with others to be more DRY
* @param {Array} resultsArray
* Array with glassLocation and glassName properties (often result as well).
* @returns {Array}
* Array that only has **location**, **name** and **result** properties
* @static
*/
const convertResultsForApi = (resultsArray) => {
if (!resultsArray) return [];
// check if array already converted.
// (likely when a session has been saved previously and then reloaded)
if (resultsArray[0].location !== undefined) {
return resultsArray;
}
return resultsArray.map((answer) => ({
location: answer.glassLocation,
name: answer.glassName,
result: answer.result
}));
};
/**
* @function getFlattenedArrayOfLineItemsWithChildParts
* @memberof StoreProcessors
* @author T-Wrecks Team
* @copyright Safelite
* @summary
* Accepts a source array of line item data.
* Flattens array in regard to childPart line items.
* @param {Array} lineItems
* Array with line items and potentially childParts to be flattened.
* @returns {Array}
* Flattened line item array.
* @static
*/
const getFlattenedArrayOfLineItemsWithChildParts = (lineItems) => {
let flattenedArray = [];
lineItems?.forEach((lineItem) => {
flattenedArray.push(lineItem);
if (lineItem.childParts) {
flattenedArray = [
...flattenedArray,
...getFlattenedArrayOfLineItemsWithChildParts(lineItem.childParts)
];
}
});
return flattenedArray;
};
/**
* @function getHasRecalibrationPart
* @memberof StoreProcessors
* @author T-Wrecks Team
* @copyright Safelite
* @summary
* Examines the store.order.lineItems state for a requiresRecalibration portion.
* If present, examines the state -> order -> lineItems -> glassParts state for a
* **recalibrationType** section.
* If present, checks that the first element is defined.
* @param {object} state
* The store state having a valid store -> order -> lineItems section.
* @returns {boolean}
* True if recalibration part is present in state, false otherwise.
* @static
*/
const getHasRecalibrationPart = (state) => {
// eslint-disable-next-line max-len
const hasRequiresRecalibration = state.order.lineItems.glassParts.safeFilter('requiresRecalibration')?.length > 0;
// eslint-disable-next-line max-len
const hasRecalibrationType = state.order.lineItems.glassParts.safeFilter('recalibrationType')?.length > 0;
if (hasRequiresRecalibration) {
if (hasRecalibrationType) {
// Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType'
return (
state.order.lineItems.glassParts.safeFilter('recalibrationType')[0].toLowerCase() !== 'unknown'
);
}
// Has 'requiresRecalibration' but no 'recalibrationType' at all
return true;
}
// Does not have 'requiresRecalibration'
return false;
};
/**
* @function getLineItemQueryStringForPricing
* @memberof StoreProcessors
* @summary
* Accepts an array of line items with "partNumber" properties and converts this
* information to a query string snippet acceptable to the pricing endpoint.
* Currently the endpoint requires the inclusion of an index.
* However it appears that the actual index number is irrelevant.
* @remarks
* Please note that if the index number became relevant this method
* would have a BUG in that it potentially calls itself recursively without
* passing the current index. Thus child parts restart the index.
* However, this isn't a huge concern as Insurance Pricing won't be using this
* and it will go away completely.
* @param {Array} lineItems
* An array of line items to be priced.
* @returns {string}
* A query string text snippet containing part number data to be priced.
* @static
*/
const getLineItemQueryStringForPricing = (lineItems) => lineItems
.map((lineItem, index) => {
let queryStringSnippet = `&LineItems[${index}].partNumber=${lineItem.partNumber}`;
if (lineItem.childParts) {
queryStringSnippet += getLineItemQueryStringForPricing(lineItem.childParts);
}
return queryStringSnippet;
})
.join('');
/**
* @function getTimeSlotsAdditionalEventData
* @memberof StoreProcessors
* @summary
* Converts provided parameters into comma delimited key/value pairs specifying data with state magic number keys.
* @param {Array} provisionalTriggers
* Array that is converted to comma delimited data point.
* @param {string} zipCode
* Service location zip code
* @param {string} firstAvailableAppointmentDateString
* The first available appointment date, used to determine
* the number of days until that date.
* @param {string} shopAppointmentType
* Value included in return data points if present.
* @returns {string}
* A string containing comma delimited data points.
* @static
*/
const getTimeSlotsAdditionalEventData = (
provisionalTriggers,
zipCode,
firstAvailableAppointmentDateString,
shopAppointmentType
) => {
let numberOfDays = null;
if (firstAvailableAppointmentDateString) numberOfDays = getDateDifferenceInDays(new Date(), firstAvailableAppointmentDateString);
return (shopAppointmentType)
// eslint-disable-next-line max-len
? `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ShopAppointmentType:${shopAppointmentType},ProvisionalTriggers:${provisionalTriggers.join(',')}`
: `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ProvisionalTriggers:${provisionalTriggers.join(',')}`;
};
/**
* @function providersEqual
* @memberof StoreProcessors
* @summary
* Tests equality of addy/number properties on two provider objects.
* @remarks
* TODO: Change back to deepEqual once zipCodeCtu is added to saveSession.
* @param {Array} providerA
* First Provider object to compare for equality
* @param {string} providerB
* Second Provider object to compare for equality
* @returns {boolean}
* True if provider objects have equal addy/number properties, false otherwise.
* @static
*/
const providersEqual = (providerA, providerB) =>
(
providerA.providerNumber === providerB.providerNumber
&& providerA.address?.city === providerB.address?.city
&& providerA.address?.state === providerB.address?.state
&& providerA.address?.streetAddress === providerB.address?.streetAddress
&& providerA.address?.zipCode === providerB.address?.zipCode
);
/**
* @function provisionalTriggersToString
* @memberof StoreProcessors
* @summary
* Converts provided array to comma delimited string with key prepended.
* @remarks
* @param {Array} provisionalTriggers
* Array of items to include in result.
* @returns {string}
* Returns string with key name and parameter array converted to comma delimited string.
* @static
*/
const provisionalTriggersToString = (provisionalTriggers) => `ProvisionalTriggers:${provisionalTriggers.join(',')}`;
/**
* @module StoreProcessors
* @endpoint
* @property {StoreProcessors.addPricesToLineItems} addPricesToLineItems
* See method for details.
* @property {StoreProcessors.buildQueryStringParameterFromArrayOfComplexObjects} buildQueryStringParameterFromArrayOfComplexObjects
* See method for details.
* @property {StoreProcessors.convertGlassPieceNamingFromApi} convertGlassPieceNamingFromApi
* See method for details.
* @property {StoreProcessors.convertResultsForApi} convertResultsForApi
* See method for details.
* @property {StoreProcessors.getAllPartNumbers} getAllPartNumbers
* See method for details.
* @property {StoreProcessors.getHasRecalibrationPart} getHasRecalibrationPart
* See method for details.
* @property {StoreProcessors.getLineItemQueryStringForPricing} getLineItemQueryStringForPricing
* See method for details.
* @property {StoreProcessors.getTimeSlotsAdditionalEventData} getTimeSlotsAdditionalEventData
* See method for details.
* @static
* @default
*/
const StoreProcessors = {
addPricesToLineItems,
buildQueryStringParameterFromArrayOfComplexObjects,
convertGlassPieceNamingFromApi,
convertGlassPieceToBackEndCompatibleFormat,
convertResultsForApi,
getFlattenedArrayOfLineItemsWithChildParts,
getHasRecalibrationPart,
getLineItemQueryStringForPricing,
getTimeSlotsAdditionalEventData,
providersEqual,
provisionalTriggersToString
};
export default StoreProcessors;

File diff suppressed because it is too large Load diff

View file

@ -898,7 +898,7 @@ describe('Store', () => {
store.damage.glassToReplace = [
{ glassLocation: location1, glassName: name1 },
{ glassLocation: location2, glassName: name2 }
],
];
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
// Act