diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2db5a1ef..caca6e33 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -75,4 +75,8 @@ stages: clearFolder: true deployFolder: '' region: us-east-2 - cfDistributionId: $(cfDistributionId) \ No newline at end of file + appDeployVariables: + __VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__) + __VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__) + cfDistributionId: $(cfDistributionId) + \ No newline at end of file diff --git a/src/constants/application-config.js b/src/constants/application-config.js index ff2229e3..9c3a3d05 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -2,7 +2,8 @@ const applicationConfig = { CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod" CONSUMER_CF_DISTRO: process.env.VUE_APP_CONSUMER_CF_DISTRO, APPLICATION_NAME: "SelfService", - SITE_ENTRY_TRIGGER_VALUE: "SelfService" + SITE_ENTRY_TRIGGER_VALUE: "SelfService", + APPLICATION_ABBREVIATION: "iss", }; export { applicationConfig }; \ No newline at end of file diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 7581f022..19833112 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -1,17 +1,17 @@ const endpoints = { - GetRouteInfo: { - url: "/content/api/v1/content/RouteInfo", - method: "POST", - }, - GetHomepageInfo: { - url: "/content/api/v1/content/HomepageInfo", - method: "GET", - }, - GetPageData: { - url: "/content/api/v1/content", - method: "GET", - }, - }; + GetRouteInfo: { + url: (applicationAbbreviation) => `/content/api/v1/content/${applicationAbbreviation}/RouteInfo`, + method: "POST", + }, + GetHomepageInfo: { + url: (applicationAbbreviation) => `/content/api/v1/content/${applicationAbbreviation}/HomepageInfo`, + method: "GET", + }, + GetPageData: { + url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`, + method: "GET", + }, +}; - export { endpoints }; +export { endpoints }; \ No newline at end of file diff --git a/src/global-methods.spec.js b/src/global-methods.spec.js new file mode 100644 index 00000000..286fe976 --- /dev/null +++ b/src/global-methods.spec.js @@ -0,0 +1,77 @@ +import axios from 'axios'; +import globalMethods from "@/global-methods"; + +//Mock external dependencies +jest.mock("axios"); + +it("Global Methods - Call Http Client - Should Resolve Promise", () => { + //Arrange + const endpoint = "https://mock.safelite.com"; + const httpArgs = setupMocksForHttpClient({ endpoint: endpoint }); + + //Act + globalMethods.callHttpClient(httpArgs).then((response) => { + //Assert + expect(axios.mock.calls[0][0].url).toContain(endpoint); + expect(response.data.message).toContain("Success"); + expect(response.status).toEqual(200); + }); + }); + + it("Global Methods - Call Http Client - Should Reject Promise", () => { + //Arrange + const endpoint = "https://mock.safelite.com"; + const httpArgs = setupMocksForHttpClient({ + endpoint: endpoint, + isError: true, + }); + + //Act + globalMethods.callHttpClient(httpArgs).catch((err) => { + //Assert + expect(axios.mock.calls[0][0].url).toContain(endpoint); + expect(err.data.message).toContain("Error"); + expect(err.status).toEqual(500); + }); + }); + + function setupMocksForHttpClient({ + endpoint = null, + isError = false, + additionalData = null, + }) { + //Clear node module + axios.mockClear(); + + // Success Response + const response = { + status: 200, + data: { + message: "Success", + additionalData: additionalData, + }, + }; + + // Error Response + const error = { + response: { + status: 500, + data: { + message: "Error", + additionalData: additionalData, + }, + }, + }; + + // Error interceptor on Axios returns a different object, so we need to mimic that. + if (isError) { + axios.mockRejectedValue(error); + } else { + axios.mockResolvedValue(response); + } + + return { + endpoint: endpoint, + logApiCall: true + }; + } \ No newline at end of file diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js new file mode 100644 index 00000000..5dd0c87f --- /dev/null +++ b/src/helpers/cms-content-helper.js @@ -0,0 +1,121 @@ +import { dynamicStrings } from "@/constants/dynamic-strings"; +import { useMainStore } from '@/store'; + +export function fetchCmsContentForPage(issPage) { + return useMainStore().getPageData(issPage) + .then((response) => { + const pageDataFromCms = {}; + + response.data.Result.forEach((widget) => { + let widgetWithReplacements = findAndReplaceGlobalStateValues( + widget.Model, + widget.Name + ); + + // If we already have this widget, push it on the collection + if (widgetWithReplacements.Name in pageDataFromCms) { + pageDataFromCms[widgetWithReplacements.Name].push( + widgetWithReplacements.Model + ); + return; + } + + pageDataFromCms[widgetWithReplacements.Name] = [ + widgetWithReplacements.Model, + ]; + }); + + Object.keys(pageDataFromCms).forEach((key) => { + if (pageDataFromCms[key].length === 1) { + pageDataFromCms[key] = pageDataFromCms[key][0]; + } + }); + + 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: {}, + }; + + Object.keys(widgetModel).forEach((key) => { + let modelWithReplacements = processWidgetItemForReplacement( + widgetModel, + key + ); + + 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(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]; + } + + // Function to convert a string, into a matching global state item. +function mapStringToState(str) { + // Pull all matches out of the string. + const regexExp = new RegExp("{(.*?):(.*?)}", "g"); + const regexMatches = [...str.matchAll(regexExp)]; + const globalStateMatches = regexMatches.filter(match => { + return match[1] === dynamicStrings.GLOBAL_STATE; + }) + + // Our final string value that will be built from the matches. + let stringBuilder = ""; + + for (const match of globalStateMatches) { + // Reset store state for each match. + let storeState = useMainStore().state; + + for (const s of match[2].split(".")) { + if (storeState[s] != undefined) { + storeState = storeState[s]; + } else { + return ""; // if we can't map our string to state data, return an empty string. + } + } + + const stringWithReplacement = str.replace(match[0], storeState); + + // If we still have values we need to substitute, call this function again. + if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) { + return mapStringToState(stringWithReplacement); + } + + // Concatenate the string. + stringBuilder = `${stringBuilder} ${stringWithReplacement}`; + } + + return stringBuilder.trimStart(); + } \ No newline at end of file diff --git a/src/helpers/layout-helper.js b/src/helpers/layout-helper.js new file mode 100644 index 00000000..a7a31119 --- /dev/null +++ b/src/helpers/layout-helper.js @@ -0,0 +1,28 @@ +export function settleAllPromises(promiseResultMap) { + // Pull our keys out of the promise 'table' + const promiseNames = Object.entries(promiseResultMap); + + return Promise.allSettled( + promiseNames.map((e) => e[1]).map((n) => n.promise) + ).then((results) => { + const resultMap = {}; + + // Build a map of the results + for (let i = 0; i < results.length; ++i) { + const promiseName = promiseNames[i][1].resultKey; + + // Some Promises like the cms content call don't have a 'data' field + // when returned, so other promises do. Map the results to the object + // so that the object is the return data. + + if (results[i]?.value?.data === undefined) { + resultMap[promiseName] = results[i]?.value; + } else { + resultMap[promiseName] = results[i]?.value?.data; + } + } + + return resultMap; + }); + } + \ No newline at end of file diff --git a/src/helpers/layout-helper.spec.js b/src/helpers/layout-helper.spec.js new file mode 100644 index 00000000..d7181075 --- /dev/null +++ b/src/helpers/layout-helper.spec.js @@ -0,0 +1,25 @@ +import { settleAllPromises } from "@/helpers/layout-helper"; + +it("layout-helper: Should settle all promises and return mapped promise results", () => { + // Arrange + const mockPromiseOne = Promise.resolve({ data: "test-data" }); + const mockPromiseTwo = Promise.resolve({ data: "test-data-two" }); + + const promiseResultMap = [ + { + resultKey: "MockResultOne", + promise: mockPromiseOne, + }, + { + resultKey: "MockResultTwo", + promise: mockPromiseTwo, + }, + ]; + + // Act + settleAllPromises(promiseResultMap).then((results) => { + // Assert + expect(results.MockResultOne).toEqual("test-data"); + expect(results.MockResultTwo).toEqual("test-data-two"); + }); +}); diff --git a/src/layouts/vehicle-year/vehicle-year.vue b/src/layouts/vehicle-year/vehicle-year.vue index 29d5a40b..630d7ddd 100644 --- a/src/layouts/vehicle-year/vehicle-year.vue +++ b/src/layouts/vehicle-year/vehicle-year.vue @@ -3,15 +3,54 @@
Vehicle Year Placeholder
+ + {{returnVehicleYear}} +