diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e58f51862..2760d6a1a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -79,4 +79,44 @@ stages: __VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__) __VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__) __VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__) + indexDeployVariables: + __VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__) + __VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__) + cfDistributionId: $(cfDistributionId) + + + # QA Build/Deploy + - stage: Qa + variables: + - group: FixMyGlassQa + jobs: + - deployment: qaBuildDeployment + displayName: Build and Deploy FMG - QA + environment: digitalCloud-qa + container: node + workspace: + clean: all + strategy: + runOnce: + deploy: + steps: + - checkout: self + clean: true + - template: templates/digital/step-build-vue.yml@AzureDevOps + parameters: + buildOutputDir: dist + - template: templates/digital/step-deploy-vue.yml@AzureDevOps + parameters: + artifactName: vueDist + awsProfile: $(qaDeploymentProfile) + outputPath: /fmg/ + deployBuckets: + safelite-qa-fmg-us-east-1: + clearFolder: true + deployFolder: '' + region: us-east-1 + appDeployVariables: + __VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__) + __VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__) + __VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__) cfDistributionId: $(cfDistributionId) \ No newline at end of file diff --git a/public/index.html b/public/index.html index b926ca739..b8a80e384 100644 --- a/public/index.html +++ b/public/index.html @@ -1,6 +1,10 @@ + + @@ -11,9 +15,17 @@ <%= htmlWebpackPlugin.options.title %> + + + + +
diff --git a/src/common-components/button-question/button-question.spec.js b/src/common-components/button-question/button-question.spec.js index d36b73012..591b34317 100644 --- a/src/common-components/button-question/button-question.spec.js +++ b/src/common-components/button-question/button-question.spec.js @@ -1,6 +1,9 @@ import { shallowMount } from "@vue/test-utils"; import buttonQuestion from "@/common-components/button-question/button-question"; import { nextTick } from "vue"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import store from "@/store"; +jest.mock("@/store",()=>{return{};},{virtual:true}); describe("buttonQuestion.vue", () => { it("Should show overflow classes on fieldset if isOverflowScrollable is true", () => { @@ -45,6 +48,65 @@ describe("buttonQuestion.vue", () => { }); }); +describe("buttonQuestion.vue", () => { + it("Fieldset classes should contain ui-radio if button type is radio", () => { + // Act + const wrapper = shallowMount(buttonQuestion, { + propsData: { + buttonType: "radio", + } + }); + // Assert + const Div = wrapper.find('fieldset div'); + expect(Div.classes()).toContain("ui-radio"); + }); +}); + + +// testing a computed property +describe("buttonQuestion.vue", () => { + it("getColLength should return '12' if prop isWide is set to true", () => { + // Act + const localThis = { isWide: true } + + expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("12"); + }); +}); + +describe("buttonQuestion.vue", () => { + it("getColLength should return '' if prop isWide is set to false", () => { + // Act + const localThis = { + isWide: false, + answers: ['a', 'b'] + } + + expect(buttonQuestion.computed.getColLength.call(localThis)).toBe(""); + }); +}); + +describe("buttonQuestion.vue", () => { + it("Should return answer.Text if prop useTextForValue is true", async () => { + // Act + const localThis = { useTextForValue: true }; + const answer = { 'Name': 'testName', 'Text': 'testText' }; + + // Assert + expect(buttonQuestion.methods.getValues.call(localThis, answer)).toBe('testText'); + }); +}); + +describe("buttonQuestion.vue", () => { + it("Should return answer.Name if prop useTextForValue is false and answer.Name exists", async () => { + // Act + const localThis = { useTextForValue: false }; + const answer = { 'Name': 'testName', 'Text': 'testText' }; + + // Assert + expect(buttonQuestion.methods.getValues.call(localThis, answer)).toBe('testName'); + }); +}); + describe("buttonQuestion.vue", () => { it("Should trigger event modelValue change to new value on when radio button selected", async () => { // Act @@ -76,3 +138,54 @@ describe("buttonQuestion.vue", () => { }); }); +describe("buttonQuestion.vue", () => { + it("Should add a value to this.selectedValues if prop isMultiSelect is true, checkValue is true and this.selectedValues already exists", () => { + // Act + const wrapper = shallowMount(buttonQuestion, { + propsData: { + isMultiSelect: true, + modelValue: [ 'a', 'b' ] + } + }); + const val = { checkValue: true, value: "2021", } + wrapper.vm.handleCheckedChanged(val); + + // Assert + expect(wrapper.vm.selectedValues).toEqual(["a", "b", "2021"]); + }); +}); + +describe("buttonQuestion.vue", () => { + it("Should remove a value to this.selectedValues if prop isMultiSelect is true, checkValue is false and this.selectedValues already exists", () => { + // Act + const wrapper = shallowMount(buttonQuestion, { + propsData: { + isMultiSelect: true, + modelValue: [ 'a', 'b' ] + } + }); + const val = { checkValue: false, value: "a", } + wrapper.vm.handleCheckedChanged(val); + + // Assert + expect(wrapper.vm.selectedValues).toEqual(["b"]); + }); +}); + + +describe("buttonQuestion.vue", () => { + it("Should do nothing to this.selectedValues if this.selectedValues is not an array", () => { + // Act + const wrapper = shallowMount(buttonQuestion, { + propsData: { + isMultiSelect: true, + modelValue: 'a', + } + }); + const val = { checkValue: true, value: "c", } + wrapper.vm.handleCheckedChanged(val); + + // Assert + expect(wrapper.vm.selectedValues).toEqual("a"); + }); +}); diff --git a/src/common-components/button-question/button-question.vue b/src/common-components/button-question/button-question.vue index 91c83fc2a..e29f1fd93 100644 --- a/src/common-components/button-question/button-question.vue +++ b/src/common-components/button-question/button-question.vue @@ -35,6 +35,7 @@ :selectedValues="selectedValues" data-test="button" :validationRules="validationRules" + :class="[suppressError ? 'alertError' : '']" /> diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 2325c29cd..322dab574 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -66,6 +66,10 @@ const endpoints = { LogExperimentExposureIfAssigned:{ url: "/analytics/api/v1/analytics/log-experiment-exposure", method: "POST", + }, + LogActivity:{ + url: "/analytics/api/v1/analytics/activity", + method: "POST", } }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 9e87416d4..7587907dc 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -18,6 +18,7 @@ const storeActions = { SET_REFERRAL_INFORMATION: "setReferralInformation", VALIDATE_ZIP: "validateZip", LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure", + LOG_ACTIVITY: "logActivity", // DEPENDENCY MUTATIONS RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies", diff --git a/src/helpers/heritage-integration/cookie-helper.js b/src/helpers/heritage-integration/cookie-helper.js index bf1c56b72..e69c21419 100644 --- a/src/helpers/heritage-integration/cookie-helper.js +++ b/src/helpers/heritage-integration/cookie-helper.js @@ -18,7 +18,7 @@ export function updateOrCreateFunnelCookie() { ReferralNumber: store.getters.order.referralNumber, ReferralDate: store.getters.order.referralDate, ReferralCorrelationId: store.getters.order.referralCorrelationId, - ReferralParentAccountNumber: store.getters.order.parentAccountNumber, + ReferralParentAccountNumber: store.getters.order.accountNumber, }); } @@ -72,7 +72,7 @@ export function getDeviceIdValue(){ return cookieValueMatch[0].split('=')[1]; } - return ''; + return '00000000-0000-0000-0000-000000000000'; } /* @@ -88,6 +88,19 @@ export function getSessionKeyValue(){ return 0; } +/* + Gets value of skey cookie, returns 0 if not found. +*/ +export function getSessionIdValue(){ + const cookieValue = getCookieValueByName(cookieNames.SESSION_ID); + + if(cookieValue){ + return cookieValue; + } + + return '00000000-0000-0000-0000-000000000000'; +} + /* =========================== = PRIVATE FUNCTIONS = diff --git a/src/helpers/heritage-integration/cookie-helper.spec.js b/src/helpers/heritage-integration/cookie-helper.spec.js index b4498f61b..03fca51b5 100644 --- a/src/helpers/heritage-integration/cookie-helper.spec.js +++ b/src/helpers/heritage-integration/cookie-helper.spec.js @@ -1,4 +1,4 @@ -import {getFunnelCookie, getDeviceIdValue, getSessionKeyValue} from "@/helpers/heritage-integration/cookie-helper.js"; +import {getFunnelCookie, getDeviceIdValue, getSessionKeyValue, getSessionIdValue} from "@/helpers/heritage-integration/cookie-helper.js"; import { removeAllTestCookies, setupCookies } from "@/helpers/unit-test-helper"; describe("cookies", () => { @@ -126,5 +126,19 @@ describe("cookies", () => { }); }); + + describe("getSessionIdValue", () => { + test("getSessionIdValue, should return GUID", () => { + // Arrange + setupCookies({}); + + // Act + const result = getSessionIdValue(); + + //Assert + expect(result).toBe('cba0c3d1-3c1b-4305-bb56-31aa50f58e27'); + + }); + }); }) \ No newline at end of file diff --git a/src/helpers/heritage-integration/navigation-helper.js b/src/helpers/heritage-integration/navigation-helper.js index 147be16a9..989dec817 100644 --- a/src/helpers/heritage-integration/navigation-helper.js +++ b/src/helpers/heritage-integration/navigation-helper.js @@ -48,7 +48,7 @@ export async function navigateToHeritageFunnel() { externalUrls.HERITAGE_FUNNEL, { corid: store.getters.order.referralCorrelationId, - src: "concept-funnel" + src: "concept-funnel", } ); } diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index 73ffa0b7c..35c3b8570 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -41,6 +41,7 @@ export async function saveOrder() { referralNumber: savedOrderInfo.data.referralNumber, referralCorrelationId: savedOrderInfo.data.referralCorrelationId, referralDate: savedOrderInfo.data.referralDate, + accountNumber: savedOrderInfo.data.accountNumber }, false); // Update the cookie with the referral information when saved. diff --git a/src/helpers/unit-test-helper.js b/src/helpers/unit-test-helper.js index 20fad7ba2..753465886 100644 --- a/src/helpers/unit-test-helper.js +++ b/src/helpers/unit-test-helper.js @@ -62,6 +62,7 @@ export const cookies = { "anotherCookie": "{}", "someOtherCookie": "{}", "dxdev": "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe", + "sid": "cba0c3d1-3c1b-4305-bb56-31aa50f58e27", "skey": "12345" }; diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index f15b862ac..543a02bef 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -281,7 +281,7 @@ export default { navigateForward(partsData){ // Temporary easter egg to navigate to heritage funnel. - const vehicleYearsToShowHeritageFunnel = [ 2001, 2002, 2010 ]; + const vehicleYearsToShowHeritageFunnel = [ 2001, 2002, 2010, 2016 ]; if (vehicleYearsToShowHeritageFunnel.includes(store.getters.vehicle.year)) { navigateToHeritageFunnel(); return; diff --git a/src/layouts/vehicle-make/make-question/make-question.vue b/src/layouts/vehicle-make/make-question/make-question.vue index 559568236..e1493c536 100644 --- a/src/layouts/vehicle-make/make-question/make-question.vue +++ b/src/layouts/vehicle-make/make-question/make-question.vue @@ -5,7 +5,7 @@ selectingInitiatesLoad :questionText="questionText" :answers="makes" - groupName="Choose Vehicle Make" + groupName="ChooseVehicleMake" textPosition="text-start" v-model="selectedValueAsArray" isRequired=true diff --git a/src/layouts/vehicle-model/model-question/model-question.vue b/src/layouts/vehicle-model/model-question/model-question.vue index c05732352..e3a56e8c7 100644 --- a/src/layouts/vehicle-model/model-question/model-question.vue +++ b/src/layouts/vehicle-model/model-question/model-question.vue @@ -5,7 +5,7 @@ selectingInitiatesLoad :questionText="questionText" :answers="models" - groupName="Choose Vehicle Model" + groupName="ChooseVehicleModel" textPosition="text-start" v-model="selectedValueAsArray" isRequired=true diff --git a/src/layouts/vehicle-style/style-question/style-question.vue b/src/layouts/vehicle-style/style-question/style-question.vue index 0d3dd54ac..e7a289bfd 100644 --- a/src/layouts/vehicle-style/style-question/style-question.vue +++ b/src/layouts/vehicle-style/style-question/style-question.vue @@ -5,7 +5,7 @@ selectingInitiatesLoad :questionText="questionText" :answers="styles" - groupName="Choose Vehicle Style" + groupName="ChooseVehicleStyle" textPosition="text-start" v-model="selectedValueAsArray" isRequired=true diff --git a/src/layouts/vehicle-year/year-question/year-question.vue b/src/layouts/vehicle-year/year-question/year-question.vue index 8a53e83db..16bac5442 100644 --- a/src/layouts/vehicle-year/year-question/year-question.vue +++ b/src/layouts/vehicle-year/year-question/year-question.vue @@ -5,7 +5,7 @@ selectingInitiatesLoad :questionText="questionText" :answers="years" - groupName="Choose Vehicle Year" + groupName="ChooseVehicleYear" textPosition="text-start" v-model="selectedValueAsArray" isRequired=true diff --git a/src/main.js b/src/main.js index fd53bde6d..6a81170db 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import App from "./App.vue"; import router from "./router"; import store from "@/store"; import baseMixin from "@/mixins/base-mixin.js"; +import analyticsMixin from "@/mixins/analytics-mixin.js"; import "../node_modules/bootstrap/dist/js/bootstrap.js"; // Vue App Setup @@ -15,5 +16,6 @@ vueApp.use(store); vueApp.use(LoadScript); vueApp.use(Maska); vueApp.mixin(baseMixin); +vueApp.mixin(analyticsMixin); vueApp.mount("#app"); diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js new file mode 100644 index 000000000..a62a0bd1a --- /dev/null +++ b/src/mixins/analytics-mixin.js @@ -0,0 +1,66 @@ +import { storeActions } from "@/constants/store-actions"; +import baseMixin from "@/mixins/base-mixin"; +import { settleAllPromises } from "@/helpers/layout-helper"; +import { getDeviceIdValue, getSessionIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper"; +import { queryStrings } from "@/constants/query-strings"; + +export default { + methods: { + async logPageEvent(destinationFmgPageValue, pageEvent){ + const logActivityPromise = baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.LOG_ACTIVITY, + { + userId: getDeviceIdValue(), + sessionKey: getSessionKeyValue(), + pageName: destinationFmgPageValue, + sessionId: getSessionIdValue(), + shouldUseSessionId: true, + pageEvent: { + action: '', + event: pageEvent, + } + }, false); + + // Settle promises and get results + const promiseResultMap = [ + { + resultKey: "logActivity", + promise: logActivityPromise, + }, + ]; + + let resultMap = await settleAllPromises(promiseResultMap); + }, + + pushEventToGA(category, action, label, value, pageName) { + const eventToBePushed = { + 'event': 'ga_event', + 'category': category, + 'action': action, + 'label': label, + 'value': value, + 'path': `/fmg/?${queryStrings.FMG_PAGE}=${pageName}` + } + pushToDataLayerIfDefined(eventToBePushed); + }, + + pushPageViewToGA(pageName) { + const pageViewEvent = { + 'event': 'logPageview', + 'pagePath': `/fmg/?${queryStrings.FMG_PAGE}=${pageName}`, + 'pageTitle': pageName + }; + pushToDataLayerIfDefined(pageViewEvent); + } + }, + computed: { + storeActions() { + return storeActions; + }, + }, +}; + +function pushToDataLayerIfDefined(data) { + if (window.dataLayer !== undefined) { + window.dataLayer.push(data); + } +} \ No newline at end of file diff --git a/src/mixins/analytics-mixin.spec.js b/src/mixins/analytics-mixin.spec.js new file mode 100644 index 000000000..c85a637fc --- /dev/null +++ b/src/mixins/analytics-mixin.spec.js @@ -0,0 +1,21 @@ +import analyticsMixin from "@/mixins/analytics-mixin"; +import { setupMocksForJsFiles } from "@/helpers/unit-test-helper.js"; +import { storeActions } from "@/constants/store-actions"; + +describe("analyticsMixin.js", () => { + test("logPageEvent: calls dispatch with type and payload", () => { + const type = ""; + const payload = {}; + + const mockData = { + actionList: [{ + actionName: storeActions.LOG_ACTIVITY + }], + } + var mocks = setupMocksForJsFiles(mockData); + + analyticsMixin.methods.logPageEvent(type, payload); + + expect(mocks.baseMixin.methods.dispatchNonBlockingStoreAction).toBeCalled(); + }); +}); \ No newline at end of file diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index b5a954c43..43ce3f6f0 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -11,10 +11,10 @@ export default { }; }, methods: { - setCmsContent(cmsContent){ + setCmsContent(cmsContent) { this.$root.cmsContentByWidget = cmsContent; }, - getCmsContent(widgetName, fieldName){ + getCmsContent(widgetName, fieldName) { return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName] ? this.$root.cmsContentByWidget[widgetName][fieldName] : ''; }, dispatchNonBlockingStoreAction(type, payload, encodePayload = true) { @@ -25,10 +25,10 @@ export default { return store.dispatch(type, payload); }, - savePageDataToStore(page, data){ + savePageDataToStore(page, data) { store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data }); }, - onSubmit() {}, // DO NOT REMOVE; needed to prevent default form submit behavior + onSubmit() { }, // DO NOT REMOVE; needed to prevent default form submit behavior onInvalidSubmit({ values, errors, results }) { // identify the first error field and put focus on it // get error names array diff --git a/src/router/index.js b/src/router/index.js index ace5b43cc..1d46eec5f 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -4,6 +4,7 @@ import { storeActions } from "@/constants/store-actions"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { routingTable } from "@/router/router-constants/routing-table.js"; import { globalEvents, globalEventTypes } from "@/constants/events"; +import { queryStrings } from "@/constants/query-strings"; // Heritage integration import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper"; @@ -14,10 +15,12 @@ import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel } from "@/helpe import baseMixin from "@/mixins/base-mixin"; import eventBus from "@/helpers/event-bus/event-bus"; import store from "@/store"; +import analyticsMixin from "@/mixins/analytics-mixin"; // Components import ComponentTest from "@/layouts/component-test/component-test.vue"; import FormTest from "@/layouts/form-test/form-test.vue"; +import { analyticsPageEvents } from "./router-constants/analytics-page-events"; const routes = [ { @@ -36,7 +39,6 @@ const routes = [ async beforeEnter(to, from, next) { // If we have no query string, or we don't have the FmgPage query string. try { - // If the saved session has timed out, clear the session, execute 404 logic. if (getFunnelCookie() !== null && !isSavedSessionStillActive()) { await GoToFunnelStartOn404(next); @@ -120,6 +122,11 @@ const router = createRouter({ //---------------------------------------------------------- Router Functions ---------------------------------------------------------- +router.afterEach((to, from) => { + analyticsMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]); + analyticsMixin.methods.logPageEvent(to.query[queryStrings.FMG_PAGE], analyticsPageEvents.ENTRY); +}); + router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => { navigate(scenario, currentRoute, false, optionalQuery, optionalParams, optionalPageData); } @@ -162,7 +169,7 @@ async function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery if (getFunnelCookie()?.ReferralNumber && getFunnelCookie()?.ReferralDate) { await saveOrder(); } - + router.push({ name: "root", query: Object.assign(optionalQuery, { diff --git a/src/router/router-constants/analytics-page-events.js b/src/router/router-constants/analytics-page-events.js new file mode 100644 index 000000000..ab4753b3d --- /dev/null +++ b/src/router/router-constants/analytics-page-events.js @@ -0,0 +1,5 @@ +const analyticsPageEvents = { + ENTRY: "ENTRY", +}; + +export { analyticsPageEvents }; diff --git a/src/store/index.js b/src/store/index.js index c18abfd90..63dbac93d 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -54,7 +54,7 @@ const getDefaultState = () => { referralNumber: null, referralDate: null, referralCorrelationId: null, - parentAccountNumber: 0, + accountNumber: 0, }, applicationUser: { eventBus: [], @@ -124,7 +124,7 @@ export const mutations = { state.order.referralDate = referralDate; }, updateParentAcctNumber(state, parentAcctNumber) { - state.order.parentAccountNumber = parentAcctNumber; + state.order.accountNumber = parentAcctNumber; }, updateIsInsurance(state, isInsurance) { state.order.payment.isInsurance = isInsurance; @@ -234,7 +234,7 @@ export const mutations = { state.order.damage.numberOfChips = orderInformation.numberOfChips; state.order.lineItems.glassParts = orderInformation.parts; - state.order.parentAccountNumber = orderInformation.parentAccountNumber; + state.order.accountNumber = orderInformation.accountNumber; state.order.serviceLocation.zipCode = orderInformation.zipCode; state.order.payment.isInsurance = orderInformation.IsInsuranceOrder; @@ -421,6 +421,42 @@ export const actions = { }); }, + logActivity(context, { userId, sessionKey, pageName, sessionId, pageEvent, customEvent, shouldUseSessionId }) { + var customEventData = {}; + customEvent?.forEach(function(event) + { + var category = event.category; + var action = event.action; + var label = event.label; + var value = event.value; + customEventData[category] = { + category: category, + action: action, + label: label, + value: value + }; + }) + + return globalMethods.callHttpClient({ + method: endpoints.LogActivity.method, + endpoint: endpoints.LogActivity.url, + payload: { + userId: userId, + sessionKey: sessionKey, + sessionId: sessionId, + pageName: pageName, + applicationName: 'SafeliteDotCom', + shouldUseSessionId: shouldUseSessionId, + pageEvent: { + action: pageEvent.action, + event: pageEvent.event, + }, + customEvent: customEventData + } + }); + }, + + // Parts API Actions getPartsOrQuestions(context, { carId, glassArray, zipCode, vin = '' }) { return globalMethods.callHttpClient({ @@ -450,12 +486,14 @@ export const actions = { make: vehicle.make, model: vehicle.model, style: vehicle.style, + vin: vehicle.vin }, numberOfChips: damage.numberOfChips, zipCode: 43215, // TODO CSR-416, should not be hardcoded (state.order.serviceLocation.zipCode) glassToReplace: damage.glassToReplace, referralNumber: context.state.order.referralNumber, - referralDate: context.state.order.referralDate + referralDate: context.state.order.referralDate, + accountNumber: context.state.order.accountNumber }, }); }, diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 7618cc25a..e27569a6f 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -221,7 +221,7 @@ describe("Mutations", () => { isRepair: false, numberOfChips: 0, parts: [], - parentAccountNumber: "123456789", + accountNumber: "123456789", insuranceInfo: {} }); @@ -617,6 +617,32 @@ describe("Actions", () => { expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, "xxx-xxx-xxx"); }); + it("logActivity action, should return nothing", async () => { + + // Arrange + const context = state; + var pageEvent = { + action: "", + event: "ENTRY", + } + + var customEvent = [{ + category: "tstCat", + action: "click", + label: "damage", + value: "psych" + }]; + + // Act + globalMethods.callHttpClient.mockImplementation(() => { + return Promise.resolve({ }); + }); + + // Assert + const response = await actions.logActivity(context, { userId: "userId", sessionKey: "sessionKey", pageName: "pageName", sessionId: "sessionId", pageEvent: pageEvent, customEvent: customEvent, shouldUseSessionId: true }); + expect(response).toEqual({}); + }); + }); describe("Getters", () => { diff --git a/src/styles/common-error-styles.scss b/src/styles/common-error-styles.scss index 1602778d3..67bd9473b 100644 --- a/src/styles/common-error-styles.scss +++ b/src/styles/common-error-styles.scss @@ -1,85 +1,126 @@ -.has-error { - &.list-button, - &.list-card { - border: 1px solid $red; - color: $red; - input[type=checkbox]:focus + label, - input[type=radio]:focus + label { - box-shadow: 0 0 0 2.5px $red; - } - input[type=checkbox]:checked + label { - box-shadow: 0 0 0 1px $red !important; - } - &:hover { - box-shadow: 0px 0px 0px 4px $red-200; - border-radius: 10px !important; - } - } - &.list-button-horizontal { - color: $red; - label { +html { + .has-error { + &.list-button, + &.list-card { border: 1px solid $red; + color: $red; + input[type=checkbox]:focus + label, + input[type=radio]:focus + label { + box-shadow: 0 0 0 2.5px $red; + } + input[type=checkbox]:checked + label { + box-shadow: 0 0 0 1px $red; + } &:hover { box-shadow: 0px 0px 0px 4px $red-200; + border-radius: 10px; } } - input[type=checkbox]:focus + label, - input[type=radio]:focus + label { - box-shadow: 0 0 1px $red !important; - } - } - &.ui-radio, - &.ui-checkbox { - input[type=checkbox], - input[type=radio], - input[type=radio]+label:before, - input[type=checkbox]+label:before { - border: 1px solid $red; - } - input[type=checkbox]:checked + label:before { - border: 1px solid $blue; - } - } - &.textbox-question, - &.dropdown-question { - p { + &.list-button-horizontal { color: $red; - } - input, - select { - border: 1px solid $red; - &:focus { - border: 1px solid transparent; + label { + border: 1px solid $red; + &:hover { + box-shadow: 0px 0px 0px 4px $red-200; + } + } + input[type=checkbox]:focus + label, + input[type=radio]:focus + label { + box-shadow: 0 0 1px $red; } } - select { - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right 0.75rem center; - background-size: 16px 12px; + &.ui-radio, + &.ui-checkbox { + input[type=checkbox], + input[type=radio], + input[type=radio]+label:before, + input[type=checkbox]+label:before { + border: 1px solid $red; + } + input[type=checkbox]:checked + label:before { + border: 1px solid $blue; + } + } + &.textbox-question, + &.dropdown-question { + p { + color: $red; + } + input, + select { + border: 1px solid $red; + &:focus { + border: 1px solid transparent; + } + } + select { + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + } + } + } + //Restore to default style if alert box is present + .alertError { + .has-error { + &.list-button, + &.list-card { + border: 1px solid $gray-500; + input:not(:focus) { + + label { + box-shadow: 0 0 0 1px $gray-500; + border-radius: .5rem; + } + } + input:checked:focus { + + label { + box-shadow: 0 0 0 2.5px $blue; + border-radius: .5rem; + } + } + input:checked:not(:focus) { + + label { + box-shadow: 0 0 0 1px $blue; + border-radius: .5rem; + } + } + input:focus { + border: 1px solid $gray-500; + + label { + box-shadow: 0 0 0 2.5px transparent; + } + } + &:hover { + box-shadow: 0 0 0 4px $blue-300; + + label { + box-shadow: 0 0 0 2.5px transparent; + border: 1px solid $blue; + } + } + } + } + } + + .form-test-error { + color: $red; + font-size: .875rem; + font-weight: 500; + } + + .form-test-invalid { + &.btn.btn-primary { + color: $gray; + background: $gray-200; + cursor: pointer; + pointer-events: all; + } + &.btn.btn-primary:hover, + &.btn.btn-primary:focus, + &.btn.btn-primary:focus-visible { + color: $gray !important; + background: $gray-200; + box-shadow: none; } } } - -.form-test-error { - color: $red; - font-size: .875rem; - font-weight: 500; -} - -.form-test-invalid { - &.btn.btn-primary { - color: $gray; - background: $gray-200; - cursor: pointer; - pointer-events: all; - } - &.btn.btn-primary:hover, - &.btn.btn-primary:focus, - &.btn.btn-primary:focus-visible { - color: $gray !important; - background: $gray-200 !important; - box-shadow: none !important; - } - -} diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.spec.js b/src/ux-components/list-button-horizontal/list-button-horizontal.spec.js index f95b378d7..cbd757a88 100644 --- a/src/ux-components/list-button-horizontal/list-button-horizontal.spec.js +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.spec.js @@ -100,7 +100,7 @@ describe("list-button-horizontal.vue", () => { const label = wrapper.find("label"); wrapper.vm.handleCheckChange = jest.fn(); - wrapper.vm.handleClick(); + wrapper.vm.triggerButton(); await nextTick(); @@ -123,7 +123,7 @@ describe("list-button-horizontal.vue", () => { const label = wrapper.find("label"); wrapper.vm.handleCheckChange = jest.fn(); - wrapper.vm.handleClick(); + wrapper.vm.triggerButton(); await nextTick(); @@ -146,7 +146,7 @@ describe("list-button-horizontal.vue", () => { const label = wrapper.find("label"); wrapper.vm.handleCheckChange = jest.fn(); - wrapper.vm.handleClick(); + wrapper.vm.triggerButton(); await nextTick(); @@ -195,4 +195,54 @@ describe("list-button-horizontal.vue", () => { // Assert expect(wrapper.componentVM.checkValue).toEqual("Car-Front"); }); + + it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + selectingInitiatesLoad: false, + }, + }); + + // Assert + wrapper.vm.handleInputChange(); + + await nextTick(); + + expect(wrapper.vm.handleCheckChange).toBeCalled; + }); + + it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + isMultiSelect: true, + }, + }); + + // Assert + wrapper.vm.handleKeyupArrow(); + + await nextTick(); + + expect(wrapper.vm.handleKeyupArrow).toHaveReturned; + }); + + it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + selectingInitiatesLoad: false, + isMultiSelect: false, + }, + }); + + // Assert + wrapper.vm.handleKeyupArrow(); + + await nextTick(); + + expect(wrapper.vm.handleCheckChange).toBeCalled; + }); + }); diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.vue b/src/ux-components/list-button-horizontal/list-button-horizontal.vue index bd1977ba3..d128982eb 100644 --- a/src/ux-components/list-button-horizontal/list-button-horizontal.vue +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue @@ -2,8 +2,11 @@
@@ -77,27 +84,47 @@ export default { checkValue: Boolean, }; }, - created(){ - if(Array.isArray(this.selectedValues)){ - this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0]; + created() { + if (Array.isArray(this.selectedValues)) { + this.checkValue = this.isMultiSelect + ? this.selectedValues.includes(this.value) + : this.selectedValues[0]; } }, methods: { displayLoader() { this.isLoaderDisplayed = true; }, - handleClick(value) { - if(this.selectingInitiatesLoad) { - this.displayLoader(); + handleInputChange() { + if(!this.selectingInitiatesLoad) { + this.handleCheckChange(); + } + }, + handleKeyupArrow() { + if (this.isMultiSelect) { + return; // Prevent arrow keys from doing anything if element is a checkbox + } + + if(!this.selectingInitiatesLoad) { this.handleCheckChange(); } - this.handleChange(value); + this.handleChange(this.value); }, - handleCheckChange(newValue, oldValue){ - const isInitialization = typeof(oldValue) === 'function'; - if (!isInitialization) { - this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() }); + triggerButton() { + if(this.selectingInitiatesLoad) { + this.displayLoader(); + this.handleCheckChange(); } + this.handleChange(this.value); + }, + handleCheckChange() { + const emitEvent = { + checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question + value: this.value.toString(), + buttonId: this.buttonID && this.buttonID.toString(), + }; + this.$emit("isCheckedChanged", emitEvent); + this.$emit("update:modelValue", emitEvent); } }, components: { @@ -105,6 +132,7 @@ export default { }, setup(props) { const inputType = props.isMultiSelect ? "checkbox" : "radio"; + const fieldOptions = { type: inputType, checkedValue: props.value, @@ -118,13 +146,11 @@ export default { } const { - checked, handleChange, errors, } = useField(props.groupName, props.validationRules, fieldOptions); return { - checked, handleChange, errors, fieldOptions, // only need to expose this for unit test purposes @@ -137,10 +163,11 @@ export default { .list-button-horizontal { input[type="radio"], input[type="checkbox"] { + position: absolute; + height: 0; opacity: 0; width: 0; - height: 0; - position: absolute; + &:focus-visible + label { box-shadow: 0 0 0 2.5px $blue; z-index: 2; diff --git a/src/ux-components/list-button/list-button.spec.js b/src/ux-components/list-button/list-button.spec.js index 7cc885b49..7a244a5bf 100644 --- a/src/ux-components/list-button/list-button.spec.js +++ b/src/ux-components/list-button/list-button.spec.js @@ -96,11 +96,8 @@ describe("list-button.vue", () => { }); // Assert - - const label = wrapper.find("label"); - wrapper.vm.handleCheckChange = jest.fn(); - wrapper.vm.handleClick(); + wrapper.vm.triggerButton(); await nextTick(); @@ -119,10 +116,8 @@ describe("list-button.vue", () => { }); // Assert - - const label = wrapper.find("label"); wrapper.vm.handleCheckChange = jest.fn(); - wrapper.vm.handleClick(); + wrapper.vm.triggerButton(); await nextTick(); const loader = wrapper.find("loader-stub"); @@ -140,11 +135,8 @@ describe("list-button.vue", () => { }); // Assert - - const label = wrapper.find("label"); - wrapper.vm.handleCheckChange = jest.fn(); - wrapper.vm.handleClick(); + wrapper.vm.triggerButton(); await nextTick(); @@ -197,4 +189,53 @@ describe("list-button.vue", () => { expect(wrapper.componentVM.checkValue).toEqual("Car-Front"); }); + it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + selectingInitiatesLoad: false, + }, + }); + + // Assert + wrapper.vm.handleInputChange(); + + await nextTick(); + + expect(wrapper.vm.handleCheckChange).toBeCalled; + }); + + it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + isMultiSelect: true, + }, + }); + + // Assert + wrapper.vm.handleKeyupArrow(); + + await nextTick(); + + expect(wrapper.vm.handleKeyupArrow).toHaveReturned; + }); + + it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + selectingInitiatesLoad: false, + isMultiSelect: false, + }, + }); + + // Assert + wrapper.vm.handleKeyupArrow(); + + await nextTick(); + + expect(wrapper.vm.handleCheckChange).toBeCalled; + }); + }); diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue index d48b2407d..aba30934d 100644 --- a/src/ux-components/list-button/list-button.vue +++ b/src/ux-components/list-button/list-button.vue @@ -2,8 +2,11 @@
@@ -80,33 +84,47 @@ export default { checkValue: Boolean, }; }, - created(){ - if(Array.isArray(this.selectedValues)){ - this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0]; + created() { + if (Array.isArray(this.selectedValues)) { + this.checkValue = this.isMultiSelect + ? this.selectedValues.includes(this.value) + : this.selectedValues[0]; } }, methods: { displayLoader() { this.isLoaderDisplayed = true; }, - handleClick(value) { + handleInputChange() { + if(!this.selectingInitiatesLoad) { + this.handleCheckChange(); + } + }, + handleKeyupArrow() { + if (this.isMultiSelect) { + return; // Prevent arrow keys from doing anything if element is a checkbox + } + + if(!this.selectingInitiatesLoad) { + this.handleCheckChange(); + } + this.handleChange(this.value); + }, + triggerButton() { if(this.selectingInitiatesLoad) { this.displayLoader(); this.handleCheckChange(); } - this.handleChange(value); + this.handleChange(this.value); }, - handleCheckChange(value, oldValue){ - const isInitialization = typeof(oldValue) === 'function'; - if (!isInitialization) { - const emitEvent = { - checkValue: this.checkValue, - value: this.value.toString(), - buttonId: this.buttonID.toString(), - }; - this.$emit('isCheckedChanged', emitEvent); - this.$emit("update:modelValue", emitEvent); - } + handleCheckChange() { + const emitEvent = { + checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question + value: this.value.toString(), + buttonId: this.buttonID && this.buttonID.toString(), + }; + this.$emit("isCheckedChanged", emitEvent); + this.$emit("update:modelValue", emitEvent); }, }, components: { @@ -128,13 +146,11 @@ export default { } const { - checked, handleChange, errors, } = useField(props.groupName, props.validationRules, fieldOptions); return { - checked, handleChange, errors, fieldOptions, // only need to expose this for unit test purposes @@ -154,10 +170,10 @@ export default { opacity: 0; &:focus-visible + label { - box-shadow: 0 0 0 2.5px $blue inset; + box-shadow: 0 0 0 2.5px $blue; } &:focus + label { - box-shadow: 0 0 0 2.5px $blue inset; + box-shadow: 0 0 0 2.5px $blue; } &:checked + label { color: $black; @@ -165,6 +181,9 @@ export default { background: $blue-100; box-shadow: 0 0 0 1px $blue; } + &:checked:focus + label { + box-shadow: 0 0 0 2.5px $blue; + } &:checked + label p, &:checked + label span { font-weight: 500; diff --git a/src/ux-components/list-card/list-card.spec.js b/src/ux-components/list-card/list-card.spec.js index 3aa803cc1..d810909e8 100644 --- a/src/ux-components/list-card/list-card.spec.js +++ b/src/ux-components/list-card/list-card.spec.js @@ -1,5 +1,6 @@ import { shallowMount } from "@vue/test-utils"; import listCard from "./list-card"; +import { nextTick } from "vue"; describe("list-card.vue", () => { it("Should return input type checkbox if isMultiSelect is true", async () => { @@ -18,7 +19,6 @@ describe("list-card.vue", () => { // Assert const input = wrapper.find("input"); - expect(input.attributes().type).toEqual("checkbox"); }); @@ -38,7 +38,6 @@ describe("list-card.vue", () => { // Assert const paragraph = wrapper.find("p"); - expect(paragraph.text()).toEqual("Windshield"); }); @@ -59,7 +58,6 @@ describe("list-card.vue", () => { // Assert const paragraph = wrapper.find("p:nth-of-type(2)"); - expect(paragraph.text()).toEqual("Test"); }); @@ -80,7 +78,6 @@ describe("list-card.vue", () => { // Assert const label = wrapper.find("label"); - expect(label.attributes().for).toEqual("List Card Checkbox"); }); @@ -101,7 +98,6 @@ describe("list-card.vue", () => { // Assert const input = wrapper.find("input"); - expect(input.attributes().name).toEqual("radio 1"); }); @@ -122,7 +118,6 @@ describe("list-card.vue", () => { // Assert const input = wrapper.find("input"); - expect(input.attributes()["aria-required"]).toEqual("true"); }); @@ -247,5 +242,88 @@ describe("list-card.vue", () => { expect(wrapper.vm.fieldOptions.initialValue).toEqual([ 'Windshield' ]); }); + it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + selectingInitiatesLoad: false, + }, + }); + + // Assert + wrapper.vm.handleInputChange(); + + await nextTick(); + + expect(wrapper.vm.handleCheckChange).toBeCalled; + }); + + it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isMultiSelect: true, + }, + }); + + // Assert + wrapper.vm.handleKeyupArrow(); + + await nextTick(); + + expect(wrapper.vm.handleKeyupArrow).toHaveReturned; + }); + + it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + selectingInitiatesLoad: false, + isMultiSelect: false, + }, + }); + + // Assert + wrapper.vm.handleKeyupArrow(); + + await nextTick(); + + expect(wrapper.vm.handleCheckChange).toBeCalled; + }); + + it("Should run handleChange if triggerButton is triggered", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + selectingInitiatesLoad: false, + }, + }); + + // Assert + wrapper.vm.triggerButton(); + + await nextTick(); + + expect(wrapper.vm.handleChange).toBeCalled; + expect(wrapper.vm.handleCheckChange).not.toBeCalled; + expect(wrapper.vm.displayLoader).not.toBeCalled; + }); + + it("Should run handleCheckChange and displayLoader if triggerButton is triggered and seletingInitiatesLoad is true", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + selectingInitiatesLoad: true, + }, + }); + + // Assert + wrapper.vm.triggerButton(); + + await nextTick(); + + expect(wrapper.vm.handleCheckChange).toBeCalled; + expect(wrapper.vm.displayLoader).toBeCalled; + }); }); diff --git a/src/ux-components/list-card/list-card.vue b/src/ux-components/list-card/list-card.vue index a178895fb..742ec4584 100644 --- a/src/ux-components/list-card/list-card.vue +++ b/src/ux-components/list-card/list-card.vue @@ -6,8 +6,11 @@ isWide ? 'horizontal' : '', (errors.length > 0 || hasError) ? 'has-error' : '', ]" - @mouseup="handleChange(value)" - @keyup.space="handleChange(value)" + @keyup.space="triggerButton()" + @keyup.up="handleKeyupArrow()" + @keyup.down="handleKeyupArrow()" + @keyup.left="handleKeyupArrow()" + @keyup.right="handleKeyupArrow()" >