diff --git a/src/helpers/clientauth-helper.js b/src/helpers/clientauth-helper.js index 27aec8dc..2466fbc9 100644 --- a/src/helpers/clientauth-helper.js +++ b/src/helpers/clientauth-helper.js @@ -1,13 +1,15 @@ import { useMainStore } from '@/store'; -export function validateISSClientTag(clientTag) { +const validateISSClientTag = (clientTag) => { const store = useMainStore(); return store.validateClientTag(clientTag) .then((response) => - // Success + // Success response, (error) => // Error null); -} +}; + +export default validateISSClientTag; diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 96a6c375..12bd7043 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -1,42 +1,78 @@ import dynamicStrings from '@/constants/dynamic-strings'; import { useMainStore } from '@/store'; -export function fetchCmsContentForPage(issPage) { - const store = useMainStore(); - const { clientName } = store.issConfig; - const { accountNumber } = store.issConfig; - const clientOverride = clientName.length > 0 && accountNumber > 0; +// This function will process the widget item and replace any global state variables with their values. +// This is a recursive function, it will call itself until it runs out of items to iterate on given the object. +/** + * @function processWidgetItemForReplacement + * @param widgetModel + * @param key + */ +function processWidgetItemForReplacement(widgetModel, key) { + // If we have a string, and it needs to be replaced. + if (typeof widgetModel[key] === 'string') { + if (widgetModel[key].includes('{if:')) { + widgetModel[key] = processIfStatements(widgetModel[key], + dynamicStrings.GLOBAL_STATE, + getStoreValueFromString); + } - return ( - store - .getPageData(issPage) - // Get the base/default page first. - .then((baseResponse) => { - if (!clientOverride) { - // Return the base page if there are no client override. - return processPageData(baseResponse, null); - } - // Else get the client override page. - const pageName = `${issPage}_${clientName.toLowerCase().replace(/ /g, '')}`; + if (widgetModel[key].includes(dynamicStrings.MODAL_LINK)) { + widgetModel[key] = mapStringToModal(widgetModel[key]); + } + if (widgetModel[key].includes(dynamicStrings.EXTERNAL_LINK)) { + widgetModel[key] = mapStringToLink(widgetModel[key]); + } + if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) { + widgetModel[key] = mapStringToState(widgetModel[key]); + } + return widgetModel[key]; + } - return store.getPageData(pageName).then( - (clientResponse) => - // Process the client override if it exists. - processPageData(baseResponse, clientResponse), - (error) => { - console.error(error); - // Process the just the base if no client override exists. - return processPageData(baseResponse, null); - } - ); - }) - ); + // If we have an object. array, etc + if (typeof widgetModel[key] === 'object' && Object.keys(widgetModel[key]).length) { + Object.keys(widgetModel[key]).forEach((item) => { + processWidgetItemForReplacement(widgetModel[key], item); + }); + + return widgetModel[key]; + } + + // If we have something else like a number, boolean, etc. just return it + return widgetModel[key]; +} + +// Parent function for processWidgetItemForReplacement. This will loop through the parent +// object and pass any objects that need additional processing to the processWidgetItemForReplacement function. +/** + * @function processWidgetItemForfindAndReplaceGlobalStateValuesReplacement + * @param widgetModel + * @param widgetName + */ +function findAndReplaceGlobalStateValues(widgetModel, widgetName) { + const objWithReplacements = { + Name: widgetName, + Model: {} + }; + + Object.keys(widgetModel).forEach((key) => { + const modelWithReplacements = processWidgetItemForReplacement(widgetModel, key); + + objWithReplacements.Model[key] = modelWithReplacements; + }); + + return objWithReplacements; } // Support method for processing the page data from the CMS call. // widgets = current widget collection used by page. // baseResponse = contains the widgets from the base page. // clientResponse = contains the widgets from the client override page. (null if none) +/** + * @function processPageData + * @param baseResponse + * @param clientResponse + */ function processPageData(baseResponse, clientResponse) { const pageDataFromCms = {}; let widgets = []; @@ -100,70 +136,51 @@ function processPageData(baseResponse, clientResponse) { return pageDataFromCms; } -// Parent function for processWidgetItemForReplacement. This will loop through the parent -// object and pass any objects that need additional processing to the processWidgetItemForReplacement function. -function findAndReplaceGlobalStateValues(widgetModel, widgetName) { - const objWithReplacements = { - Name: widgetName, - Model: {} - }; +/** + * + * @param issPage + */ +export function fetchCmsContentForPage(issPage) { + const store = useMainStore(); + const { clientName } = store.issConfig; + const { accountNumber } = store.issConfig; + const clientOverride = clientName.length > 0 && accountNumber > 0; - Object.keys(widgetModel).forEach((key) => { - const modelWithReplacements = processWidgetItemForReplacement(widgetModel, key); + return ( + store + .getPageData(issPage) + // Get the base/default page first. + .then((baseResponse) => { + if (!clientOverride) { + // Return the base page if there are no client override. + return processPageData(baseResponse, null); + } + // Else get the client override page. + const pageName = `${issPage}_${clientName.toLowerCase().replace(/ /g, '')}`; - objWithReplacements.Model[key] = modelWithReplacements; - }); - - return objWithReplacements; -} - -// This function will process the widget item and replace any global state variables with their values. -// This is a recursive function, it will call itself until it runs out of items to iterate on given the object. -function processWidgetItemForReplacement(widgetModel, key) { - // If we have a string, and it needs to be replaced. - if (typeof widgetModel[key] === 'string') { - if (widgetModel[key].includes('{if:')) { - widgetModel[key] = processIfStatements( - widgetModel[key], - dynamicStrings.GLOBAL_STATE, - getStoreValueFromString - ); - } - - if (widgetModel[key].includes(dynamicStrings.MODAL_LINK)) { - widgetModel[key] = mapStringToModal(widgetModel[key]); - } - if (widgetModel[key].includes(dynamicStrings.EXTERNAL_LINK)) { - widgetModel[key] = mapStringToLink(widgetModel[key]); - } - if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) { - widgetModel[key] = mapStringToState(widgetModel[key]); - } - return widgetModel[key]; - } - - // If we have an object. array, etc - if (typeof widgetModel[key] === 'object' && Object.keys(widgetModel[key]).length) { - Object.keys(widgetModel[key]).forEach((item) => { - processWidgetItemForReplacement(widgetModel[key], item); - }); - - return widgetModel[key]; - } - - // If we have something else like a number, boolean, etc. just return it - return widgetModel[key]; + return store.getPageData(pageName).then((clientResponse) => + // Process the client override if it exists. + processPageData(baseResponse, clientResponse), + (error) => { + console.error(error); + // Process the just the base if no client override exists. + return processPageData(baseResponse, null); + }); + }) + ); } +/** + * @function mapStringToModal + * @param str + */ function mapStringToModal(str) { const startIndex = str.indexOf(`{${dynamicStrings.MODAL_LINK}`); let linkToReplace = str.substring(startIndex, str.length); linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf('}') + 1); - const params = linkToReplace.substring( - dynamicStrings.MODAL_LINK.length + 2, - linkToReplace.length - 1 - ); + const params = linkToReplace.substring(dynamicStrings.MODAL_LINK.length + 2, + linkToReplace.length - 1); const splitParams = params.split(','); const bodyText = `${splitParams[1]}`; @@ -176,15 +193,17 @@ function mapStringToModal(str) { return returnVal; } +/** + * @function mapStringToLink + * @param str + */ function mapStringToLink(str) { const startIndex = str.indexOf(`{${dynamicStrings.EXTERNAL_LINK}`); let linkToReplace = str.substring(startIndex, str.length); linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf('}') + 1); - const params = linkToReplace.substring( - dynamicStrings.EXTERNAL_LINK.length + 2, - linkToReplace.length - 1 - ); + const params = linkToReplace.substring(dynamicStrings.EXTERNAL_LINK.length + 2, + linkToReplace.length - 1); const splitParams = params.split(','); const bodyText = `${splitParams[1]}`; @@ -198,17 +217,20 @@ function mapStringToLink(str) { } // Function to convert a string, into a matching global state item. +/** + * @function mapStringToState + * @param str + */ function mapStringToState(str) { // Pull all matches out of the string. const regexExp = /{([^{}]*?):([^{}]*?)}/g; const regexMatches = [...str.matchAll(regexExp)]; - const globalStateMatches = regexMatches.filter( - (match) => match[1] === dynamicStrings.GLOBAL_STATE - ); + const globalStateMatches = regexMatches.filter((match) => match[1] === dynamicStrings.GLOBAL_STATE); // Our final string value that will be built from the matches. const stringBuilder = ''; + // eslint-disable-next-line no-restricted-syntax for (const match of globalStateMatches) { // Reset store state for each match. const valueFromStore = getStoreValueFromString(match[2]); @@ -230,13 +252,18 @@ function mapStringToState(str) { return str.trimStart(); } +/** + * @function getStoreValueFromString + * @param str + */ function getStoreValueFromString(str) { if (!str) return ''; let storeOrStateObject = useMainStore(); + // eslint-disable-next-line no-restricted-syntax for (const s of str.split('.')) { if (s === 'getters') continue; // For backward compatibility - if (storeOrStateObject[s] != undefined) { + if (typeof storeOrStateObject[s] !== 'undefined') { storeOrStateObject = storeOrStateObject[s]; } else { break; @@ -259,32 +286,34 @@ function getStoreValueFromString(str) { export function processIfStatements(str, ifConditionKeyword, replacePlaceholderCallback) { const containsRelevantIfStatement = new RegExp(`{if:${ifConditionKeyword}:.+?}`, 'g').test(str); - const hasEmbeddedCrLf = /\r?\n|\r/g.test(str); if (!containsRelevantIfStatement) { return str; } const ifStatementRegexExpression = getIfStatementRegexExpression(); const ifStatementRegexMatches = [...str.matchAll(ifStatementRegexExpression)]; - const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword( - ifStatementRegexMatches, - ifConditionKeyword - ); + const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(ifStatementRegexMatches, + ifConditionKeyword); executeIfStatementAndSetProcessedStrings(completeIfStatementArray, replacePlaceholderCallback); const reconstructedPostProcessedString = joinProcessedRegexArray(ifStatementRegexMatches); - return processIfStatements( - reconstructedPostProcessedString, + return processIfStatements(reconstructedPostProcessedString, ifConditionKeyword, - replacePlaceholderCallback - ); + replacePlaceholderCallback); } +/** + * + * @param matches + * @param ifConditionKeyword + */ function getAndFlagFirstNonNestedIfStatementWithKeyword(matches, ifConditionKeyword) { let index = 0; + // eslint-disable-next-line no-restricted-syntax for (const match of matches) { if (match.groups.isIfStatement && match.groups.ifConditionType === ifConditionKeyword) { let interiorIndex = 0; let nestedLevel = 0; let elseStatementIndex = null; + // eslint-disable-next-line no-restricted-syntax for (const interiorMatch of matches.slice(index + 1)) { if (interiorMatch.groups.isIfStatement) { if (interiorMatch.groups.ifConditionType === ifConditionKeyword) { @@ -314,6 +343,11 @@ function getAndFlagFirstNonNestedIfStatementWithKeyword(matches, ifConditionKeyw return matches; } +/** + * + * @param matches + * @param elseStatementIndex + */ function flagMatchesForProcessing(matches, elseStatementIndex) { matches[0].isFlaggedForProcessing = true; matches[matches.length - 1].isFlaggedForProcessing = true; @@ -322,6 +356,10 @@ function flagMatchesForProcessing(matches, elseStatementIndex) { } } +/** + * + * @param regexMatches + */ function joinProcessedRegexArray(regexMatches) { let processedString = ''; regexMatches.forEach((match) => { @@ -331,6 +369,11 @@ function joinProcessedRegexArray(regexMatches) { return processedString; } +/** + * + * @param ifStatementArray + * @param replacePlaceholderCallback + */ function executeIfStatementAndSetProcessedStrings(ifStatementArray, replacePlaceholderCallback) { const ifCondition = replacePlaceholderCallback(ifStatementArray[0].groups.ifCondition); let isInsideDesiredBlock = ifCondition; @@ -345,6 +388,11 @@ function executeIfStatementAndSetProcessedStrings(ifStatementArray, replacePlace }); } +/** + * + * @param entry + * @param isInsideDesiredBlock + */ function setProcessedStringOnEntry(entry, isInsideDesiredBlock) { if (!isInsideDesiredBlock) { entry.groups.processedString = ''; @@ -363,43 +411,52 @@ function setProcessedStringOnEntry(entry, isInsideDesiredBlock) { } } +/** + * + */ function getIfStatementRegexExpression() { // Matches but does not capture: // {if:...} or {else} or {end} const anyLogicOperatorNonCapture = '(?:{(?:end|else|if:.*?)})'; // NOTE: ? syntax stores the captured match like so: match.groups.variableName - const matchStartOfString = - '(?^.+?)' + // Match and Capture all characters (lazy), cannot be empty - '(?=(?:{if))'; // Looks ahead but does not capture {if - const matchIfOperator = - '(?{if:)' + // Match & Capture {if: - '(?.*?):' + // Match all chars up to and including next ':' - Capture all chars up to ':' - '(?.*?)}' + // Match all chars up to and including next '}' - Capture all chars up to '}' - '(?.*?)' + // Match and Capture all characters (lazy), can be empty - `(?=${anyLogicOperatorNonCapture})`; // Looks ahead but does not capture the next logic operator - const matchElseOperator = - '(?{else})' + // Match & Capture {else} - '(?.*?)' + // Match & Capture all characters (lazy), can be empty - `(?=${anyLogicOperatorNonCapture})`; // Looks ahead but does not capture the next logic operator - const matchEndOperator = - '(?{end})' + // Match & Capture {end} - '(?.*?)' + // Match & Capture all characters (lazy), can be empty - `(?=${anyLogicOperatorNonCapture}|$)`; // Looks ahead but does not capture the next logic operator + const matchStartOfString + = '(?^.+?)' // Match and Capture all characters (lazy), cannot be empty + + '(?=(?:{if))'; // Looks ahead but does not capture {if + const matchIfOperator + = '(?{if:)' // Match & Capture {if: + + '(?.*?):' // Match all chars up to and including next ':' - Capture all chars up to ':' + + '(?.*?)}' // Match all chars up to and including next '}' - Capture all chars up to '}' + + '(?.*?)' // Match and Capture all characters (lazy), can be empty + + `(?=${anyLogicOperatorNonCapture})`; // Looks ahead but does not capture the next logic operator + const matchElseOperator + = '(?{else})' // Match & Capture {else} + + '(?.*?)' // Match & Capture all characters (lazy), can be empty + + `(?=${anyLogicOperatorNonCapture})`; // Looks ahead but does not capture the next logic operator + const matchEndOperator + = '(?{end})' // Match & Capture {end} + + '(?.*?)' // Match & Capture all characters (lazy), can be empty + + `(?=${anyLogicOperatorNonCapture}|$)`; // Looks ahead but does not capture the next logic operator // Combine all matching patterns, separated by 'or' pipes - return new RegExp( - `${matchStartOfString}|${matchIfOperator}|${matchElseOperator}|${matchEndOperator}`, - 'g' - ); + return new RegExp(`${matchStartOfString}|${matchIfOperator}|${matchElseOperator}|${matchEndOperator}`, + 'g'); } /// /////////////////////////////////////// // End of If Statement Processing Logic // /// /////////////////////////////////////// +/** + * + * @param copy + */ export function doesCopyContainTextLink(copy) { return copy.includes(dynamicStrings.TEXT_LINK); } +/** + * + * @param context + */ export function setupModalLinks(context) { context.$nextTick(() => { const elements = document.getElementsByClassName('modal-text'); @@ -412,12 +469,17 @@ export function setupModalLinks(context) { }); } +/** + * + * @param copy + */ export function doesCopyContainRouterLink(copy) { return copy.includes(this.dynamicStrings.ROUTER_LINK); } /** * splits copy on { ... } such as {routerlink: ...} + * @param copy * @returns array of strings */ export function splitCopyOnCMSPlaceHolder(copy) { @@ -427,6 +489,7 @@ export function splitCopyOnCMSPlaceHolder(copy) { /** * Returns string2 of input following this pattern: {string1:string2,string3} + * @param copy * @returns string */ export function getRouterLinkRouteFromCopy(copy) { @@ -438,6 +501,7 @@ export function getRouterLinkRouteFromCopy(copy) { /** * Returns string3 of input following this pattern: { string1: string2, string3 } + * @param copy * @returns string */ export function getRouterLinkDisplayTextFromCopy(copy) { @@ -449,18 +513,21 @@ export function getRouterLinkDisplayTextFromCopy(copy) { /** * Returns a router link as an 'a' tag element + * @param copy * @returns string */ export function getRouterLinkHtmlStringFromCopy(copy) { - return `${getRouterLinkDisplayTextFromCopy(copy)}`; + return `${getRouterLinkDisplayTextFromCopy(copy)}`; } // Copy returned from the CMS that has newlines will return blocks wrapped in //

...

// This function returns an array of each paragraph, works with or without html // attributes present +/** + * + * @param copy + */ export function splitCMSCopyOnParagraphTag(copy) { // filter removes empty strings that are a result of string.split with regex return copy.split(/(?:)|(?:<\/p>)/g).filter((paragraph) => paragraph !== ''); diff --git a/src/helpers/global-rule-definer.js b/src/helpers/global-rule-definer.js index 1a844fb2..650fb97e 100644 --- a/src/helpers/global-rule-definer.js +++ b/src/helpers/global-rule-definer.js @@ -48,6 +48,7 @@ function defineGlobalPhoneNumberRules() { } /** + * @function defineGlobalRules * @summary Define all global rules */ export default function defineGlobalRules() { diff --git a/src/helpers/text-helper.js b/src/helpers/text-helper.js index e1cd64dd..01d18309 100644 --- a/src/helpers/text-helper.js +++ b/src/helpers/text-helper.js @@ -1,8 +1,11 @@ /** - * Returns string with inline style tag stripped out - * @returns string + * @function stripRteStyle + * @summary + * Returns string with inline style tag stripped out + * @param {string} stringWithStyleTag + * @returns {string} */ -export function stripRteStyle(stringWithStyleTag) { +export default function stripRteStyle(stringWithStyleTag) { const regexExp = /[\s*]style="(.*?)"/g; return stringWithStyleTag.replace(regexExp, ''); } diff --git a/src/helpers/validation-rules.spec.js b/src/helpers/validation-rules.spec.js index d8db23ab..df0c7ce6 100644 --- a/src/helpers/validation-rules.spec.js +++ b/src/helpers/validation-rules.spec.js @@ -1,5 +1,4 @@ -import { required } from '@/helpers/validation-rules'; -import { regex } from '@/helpers/validation-rules'; +import { regex, required } from '@/helpers/validation-rules'; describe('validation-rules.vue', () => { test('required rules should return error if value missing', () => { diff --git a/src/iss-components/address-questions/address-questions.spec.js b/src/iss-components/address-questions/address-questions.spec.js index 27274bbf..60f8d692 100644 --- a/src/iss-components/address-questions/address-questions.spec.js +++ b/src/iss-components/address-questions/address-questions.spec.js @@ -1,11 +1,78 @@ // Components -import addressQuestions from '@/iss-components/address-questions/address-questions'; +import addressQuestions from '@/iss-components/address-questions/address-questions.vue'; // Supporting Files import { mount, shallowMount } from '@vue/test-utils'; import { getMountOptions } from '@/helpers/unit-test-helper.js'; let autocompleteElement; + +/** @ignore */ +function setupMocks({ + mountOptions, + props, + isShallowMount = true, + querySelectorFunction, + geocoderResult = ['1234 Test Street'] +}) { + const resultingMountOptions = getMountOptions({ + ...mountOptions, + router: { + navigate: jest.fn() + }, + loadScript: jest.fn().mockResolvedValue() + }); + + window.google = { + maps: { + event: { + addListener: jest + .fn() + .mockImplementation((element, eventName, callbackFunction) => { + /** @ignore */ + function interceptedCallbackFunction(e) { + callbackFunction(e.detail); + } + // selectedPlace = "Woogly"; + element.addEventListener(eventName, interceptedCallbackFunction); + }), + removeListener: jest.fn(), + clearInstanceListeners: jest.fn() + }, + places: { + Autocomplete: jest.fn().mockImplementation((el) => el) + }, + Geocoder: class Geocoder { + // constructor(); + + geocode(request, callback) { + callback([geocoderResult], true); + } + }, + GeocoderStatus: { + OK: true + } + } + }; + + if (props) resultingMountOptions.propsData = props; + + const wrapper = isShallowMount + ? shallowMount(addressQuestions, resultingMountOptions) + : mount(addressQuestions, resultingMountOptions); + document.querySelector = jest.fn().mockImplementation((query) => { + let result = null; + if (query === '.pac-container') result = document.createElement('div'); + else if (querySelectorFunction) { + result = querySelectorFunction(query); + } + + return result ?? null; + }); + + return { wrapper }; +} + describe('address-questions.vue', () => { beforeEach(() => { // Create the `addressField1` element (autocomplete's input) @@ -456,80 +523,3 @@ 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, - isShallowMount = true, - querySelectorFunction, - geocoderResult = ['1234 Test Street'] -}) { - const resultingMountOptions = getMountOptions({ - ...mountOptions, - router: { - navigate: jest.fn() - }, - loadScript: jest.fn().mockResolvedValue() - }); - - window.google = { - maps: { - event: { - addListener: jest - .fn() - .mockImplementation((element, eventName, callbackFunction) => { - /** - * - * @param e - */ - function interceptedCallbackFunction(e) { - callbackFunction(e.detail); - } - // selectedPlace = "Woogly"; - element.addEventListener(eventName, interceptedCallbackFunction); - }), - removeListener: jest.fn(), - clearInstanceListeners: jest.fn() - }, - places: { - Autocomplete: jest.fn().mockImplementation((el) => el) - }, - Geocoder: class Geocoder { - // constructor(); - - geocode(request, callback) { - callback([geocoderResult], true); - } - }, - GeocoderStatus: { - OK: true - } - } - }; - - if (props) resultingMountOptions.propsData = props; - - const wrapper = isShallowMount - ? shallowMount(addressQuestions, resultingMountOptions) - : mount(addressQuestions, resultingMountOptions); - document.querySelector = jest.fn().mockImplementation((query) => { - let result = null; - if (query === '.pac-container') result = document.createElement('div'); - else if (querySelectorFunction) { - result = querySelectorFunction(query); - } - - return result ?? null; - }); - - return { wrapper }; -} diff --git a/src/iss-components/address-questions/address-questions.vue b/src/iss-components/address-questions/address-questions.vue index 2c0f9501..78f6d640 100644 --- a/src/iss-components/address-questions/address-questions.vue +++ b/src/iss-components/address-questions/address-questions.vue @@ -89,9 +89,9 @@