diff --git a/src/App.vue b/src/App.vue index 2284217c..554b3117 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,7 +8,9 @@ \ No newline at end of file diff --git a/src/assets/icons/spinner.svg b/src/assets/icons/spinner.svg new file mode 100644 index 00000000..d5b7fe4c --- /dev/null +++ b/src/assets/icons/spinner.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common-components/button-question/button-question.spec.js b/src/common-components/button-question/button-question.spec.js new file mode 100644 index 00000000..ae556efe --- /dev/null +++ b/src/common-components/button-question/button-question.spec.js @@ -0,0 +1,117 @@ +import { shallowMount } from "@vue/test-utils"; +import buttonQuestion from "@/common-components/button-question/button-question"; + +jest.mock("@/store", () => { return {}; }, { virtual: true }); + +describe("buttonQuestion.vue", () => { + it("Should show overflow classes on fieldset if isOverflowScrollable is true", () => { + // Act + const wrapper = shallowMount(buttonQuestion, { + propsData: { + isOverflowScrollable: true, + groupName: "group-name" + } + }); + + // Assert + const fieldSet = wrapper.find('fieldset'); + expect(fieldSet.classes()).toContain("overflow-scroll"); + }); +}); + +describe("buttonQuestion.vue", () => { + it("Fieldset classes should contain row if button type is listCard", () => { + // Act + const wrapper = shallowMount(buttonQuestion, { + propsData: { + buttonType: "listCard", + groupName: "group-name" + } + }); + // Assert + const Div = wrapper.find('fieldset div'); + expect(Div.classes()).toContain("row"); + }); +}); + +describe("buttonQuestion.vue", () => { + it("Fieldset classes should contain d-flex if button type is listButtonHorizontal", () => { + // Act + const wrapper = shallowMount(buttonQuestion, { + propsData: { + buttonType: "listButtonHorizontal", + groupName: "group-name" + } + }); + // Assert + const Div = wrapper.find('fieldset div'); + expect(Div.classes()).toContain("d-flex"); + }); +}); + +describe("buttonQuestion.vue", () => { + it("Fieldset classes should contain ui-radio if button type is radio", () => { + // Act + const wrapper = shallowMount(buttonQuestion, { + propsData: { + buttonType: "radio", + groupName: "group-name" + } + }); + // 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'], + groupName: "group-name" + } + + 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.getValue.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.getValue.call(localThis, answer)).toBe('testName'); + }); +}); + +function setupMocks(mountOptionsMockData = {}) { + const defaultMountOptions = { route: { query: { issPage: 'page-name' } } }; + + return defaultMountOptions; +} diff --git a/src/common-components/button-question/button-question.vue b/src/common-components/button-question/button-question.vue new file mode 100644 index 00000000..10337778 --- /dev/null +++ b/src/common-components/button-question/button-question.vue @@ -0,0 +1,253 @@ + + + + + + \ No newline at end of file diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 19833112..2aeaf26a 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -11,6 +11,10 @@ const endpoints = { url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`, method: "GET", }, + GetVehicleYears: { + url: "/vehicle/api/v1/vehicle/years", + method: "GET", + }, }; export { endpoints }; diff --git a/src/layouts/vehicle-year/vehicle-year.vue b/src/layouts/vehicle-year/vehicle-year.vue index b19526e9..b3253139 100644 --- a/src/layouts/vehicle-year/vehicle-year.vue +++ b/src/layouts/vehicle-year/vehicle-year.vue @@ -1,82 +1,74 @@ - - \ No newline at end of file + ]; + + let resultMap = await settleAllPromises(promiseResultMap); + + // Call the "next" function to complete the transition to this page. + next((vm) => { + vm.setCmsContent(resultMap.cmsContent); + vm.$refs.yearQuestion.initializeComponent( + resultMap.yearQuestionInitialData + ); + }); + }, + + components: { + yearQuestion, + siteHeader, + siteSubHeader, + }, + }; + + \ No newline at end of file diff --git a/src/layouts/vehicle-year/year-question/year-question.vue b/src/layouts/vehicle-year/year-question/year-question.vue new file mode 100644 index 00000000..0cd35425 --- /dev/null +++ b/src/layouts/vehicle-year/year-question/year-question.vue @@ -0,0 +1,56 @@ + + + + \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index 56668172..25e1c8b1 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -74,6 +74,15 @@ export const useMainStore = defineStore({ }); }, + // Vehicle API Actions + getVehicleYears() { + return globalMethods.callHttpClient({ + method: endpoints.GetVehicleYears.method, + endpoint: endpoints.GetVehicleYears.url, + payload: {}, + }); + }, + // Vehicle API Actions updateVehicleYear(year) { diff --git a/src/styles/common-error-styles.scss b/src/styles/common-error-styles.scss new file mode 100644 index 00000000..def634dc --- /dev/null +++ b/src/styles/common-error-styles.scss @@ -0,0 +1,151 @@ +html { + .has-error { + &.list-button, + &.list-card, + &.list-card.list-button { + border: none; + 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: .5rem; + } + label { + border: 1px solid $red; + border-radius: .5rem; + } + label:hover { + box-shadow: 0px 0px 0px 4px $red-200; + border-radius: 10px; + border: 1px solid $red; + } + } + &.list-button-horizontal { + color: $red; + 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; + } + } + &.ui-radio, + &.ui-checkbox { + input[type=checkbox], + input[type=radio], + input[type=radio]+label:before, + input[type=checkbox]+label:before { + border: 1px solid $red; + background-color: initial; + } + input[type=checkbox]:checked + label:before { + border: 1px solid $blue; + } + } + &.textbox-question, + &.dropdown-question { + input:hover { + box-shadow: 0px 0px 0px 4px $red-200; + border-radius: .5rem; + border: 1px solid $red; + } + input:focus { + box-shadow: 0 0 0 2.5px $red; + } + p { + color: $red; + } + input, + select { + border: 1px solid transparent; + box-shadow: 0 0 0 1px $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 { + border: none; + box-shadow: 0 0 0 1px $gray-500; + border-radius: .5rem; + } + } + input:checked:focus { + + label { + border: none; + 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-600; + background: $gray-200; + cursor: pointer; + pointer-events: all; + font-weight: $font-weight-normal; + } + &.btn.btn-primary:hover { + background: $gray-200; + box-shadow: none; + } + &.btn.btn-primary:focus, + &.btn.btn-primary:focus-visible { + box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700; + } + } + } + \ No newline at end of file diff --git a/src/styles/common-styles.scss b/src/styles/common-styles.scss new file mode 100644 index 00000000..a516165f --- /dev/null +++ b/src/styles/common-styles.scss @@ -0,0 +1,60 @@ +// Common/Global Styles +// Use this file for global styles that don't or won't have their own stylesheet +body { + font-size: 16px; + background-color: #fff; + color: #4D5151; + .container-fluid { + max-width: 576px; //Remove once desktop app is complete + &.container-shadow { + box-shadow: 0px 0px 6px 0px rgba(0,0,0,0.15); //Use instead of Bootstrap's helper + } + &.make-tall { + height: 100vh; + display: flex; + flex-direction: column; + } + .prevent-squish{ + overflow-x: unset; + } + } + .pointer { + cursor: pointer; + } + .container, + .container-fluid { + overflow-x: hidden; + } + + .sub-container{ + &.make-tall { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + } + } + + .sr-only { + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; + } + + .page-container-grouped-styles { + @extend .container-fluid, .shadow, .rounded-3, .p-2, .position-relative, .make-tall, .px-5; + } + + //Footer modal backdrop adjustments for positioning + .modal-backdrop { + left: 50%; + transform: translateX(-50%); + max-width: 576px; + height: calc(100% - 72px); + } + + } + \ No newline at end of file diff --git a/src/styles/common-typography-styles.scss b/src/styles/common-typography-styles.scss new file mode 100644 index 00000000..9ee9aa28 --- /dev/null +++ b/src/styles/common-typography-styles.scss @@ -0,0 +1,67 @@ +//Typography +p, +body { + font-size: 1rem; + line-height: 1.625; + font-weight: 400; +} + +//Headings +//add helper fw-bold to any element to get bold style (500) +h1,.h1 { + line-height: 1.325; + font-weight: 300; +} +h2,.h2 { + line-height: 1.325; + font-weight: 300; +} +h3,.h3 { + line-height: 1.375; + font-weight: 300; +} +h4,.h4 { + line-height: 1.6; + font-weight: 300; +} +h5,.h5 { + line-height: 1.325; + font-weight: 400; +} +h6,.h6 { + line-height: 1.7; + letter-spacing: .75px; + text-transform: uppercase; +} + +.small { + font-size: .875rem; + line-height: 1.7; + font-weight: 400; +} + +label, +.label { + font-size: 1rem; + line-height: 1.5; + font-weight: 400; +} + +caption, +.caption { + font-size: .75rem; + line-height: 1.7; + font-weight: 400; +} + + +// Font size +.fs-5 { + line-height: 2; +} + +.fs-6 { + font-size: 1rem !important; + line-height: 1.4; + font-weight: 500; +} \ No newline at end of file diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss new file mode 100644 index 00000000..52cf8916 --- /dev/null +++ b/src/styles/ux-variables.scss @@ -0,0 +1,186 @@ +//Colors + +// White/black +$white: #FFFFFF; +$black: #000000; + +// Blues +$blue-100: #E4F1F7;// Used in theme +$blue-200: #C1DFEE; +$blue-300: #9FCEE6; +$blue-400: #69ADCF;// Used in theme +$blue-500: #3B8FB8; +$blue: #1574A1;// Default Blue +$blue-700: #06577C; +$blue-800: #003D58; +$blue-900: #002433;// Used in theme + +// Reds +$red-100: #FFE6E4; +$red-200: #FCBFBB; +$red-300: #F89892; +$red-400: #E65C53; +$red: #D4281C;// Default Red +$red-600: #AC160B; +$red-700: #840900; +$red-800: #5B0600; +$red-900: #330300; + +// Greens +$green-100: #E3F2EA; +$green-200: #BCEDD4; +$green-300: #94D7B6; +$green-400: #5ABC8C;// Used in theme +$green-500: #2CA168; +$green: #0C7E47;// Default Green +$green-700: #006A36; +$green-800: #004F28; +$green-900: #00331A; + +// Yellows +$yellow-100: #FFF5EB; +$yellow-200: #FFE1C6; +$yellow-300: #FFCAA0; +$yellow-400: #F5975D; +$yellow: #E86421;// Default Yellow +$yellow-600: #BB4B06; +$yellow-700: #8E3C00; +$yellow-800: #602D00; +$yellow-900: #331A00; + +// Grays +$gray-100: #F5F5F5;// Used in theme +$gray-200: #E3E4E4;// Used in theme +$gray-300: #D2D4D4; +$gray: #B0B3B3;// Default Gray +$gray-500: #8E9292;// Used in theme +$gray-550: #727676;// Used in theme +$gray-600: #4D5151;// Used in theme +$gray-700: #303333;// Used in theme +$gray-800: #222424;// Used in theme +$gray-900: #181A1A;// Used in theme + +// Miscellaneous Colors +$indigo: #6610f2; +$purple: #6f42c1; +$pink: #d63384; +$orange: #fd7e14; +$teal: #20c997; +$cyan: #0dcaf0; + +// scss-docs-start colors-map +$colors: ( +"blue": $blue, +"indigo": $indigo, +"purple": $purple, +"pink": $pink, +"red": $red, +"orange": $orange, +"yellow": $yellow, +"green": $green, +"teal": $teal, +"cyan": $cyan, +"white": $white, +"gray": $gray, +"gray-dark": $gray-500 +); + +// scss-docs-start theme-color-variables +$primary: $blue; +$secondary: $red; +$success: $green; +$info: $cyan; +$warning: $yellow; +$danger: $red; +$light: $gray-100; +$dark: $gray-500; + +// scss-docs-start theme-colors-map +$theme-colors: ( +"primary": $primary, +"secondary": $secondary, +"success": $success, +"info": $info, +"warning": $warning, +"danger": $danger, +"light": $light, +"dark": $dark +); + +//Default font color +$body-color: $gray-600; + +//Fonts +$font-family-sans-serif: Roboto, Arial, Helvetica, sans-serif; +$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +// stylelint-enable value-keyword-case +$font-family-base: $font-family-sans-serif; +$font-family-code: $font-family-monospace; +$font-size-base: 1rem; // Assumes the browser default, typically `16px` + +//Custom Font size (extra small) +$font-size-xsm: $font-size-base * .75; +$font-sizes: ( + 7: $font-size-xsm +); + +//Font weight +$font-weight-lighter: lighter; +$font-weight-light: 300; +$font-weight-normal: 400; +$font-weight-bold: 500; +$font-weight-bolder: bolder; + +//Headings +$h1-font-size: $font-size-base * 3; +$h2-font-size: $font-size-base * 2.625; +$h3-font-size: $font-size-base * 2; +$h4-font-size: $font-size-base * 1.625; +$h5-font-size: $font-size-base * 1.25; +$h6-font-size: $font-size-base * .875; + +//Border Radius +// Helper classes are rounded, rounded-1, rounded-2, rounded-3 +$border-radius: .25rem; +$border-radius-sm: .2rem; +$border-radius-lg: .5rem;//Used for buttons. Can be used for other things, of course. +$border-radius-pill: 50rem; + +//Spacing +// 8 spacers available instead of the usual 5 +$spacer: 1rem; +$spacers: ( + 0: 0, + 1: $spacer * .25, /* 4px */ + 2: $spacer * .5, /* 8px */ + 3: $spacer * .75, /* 12px */ + 4: $spacer * 1, /* 16px */ + 5: $spacer * 1.5, /* 24px */ + 6: $spacer * 2, /* 32px */ + 7: $spacer * 2.5, /* 40px */ + 8: $spacer * 3, /* 48px */ +); + +//Grid breakpoints +$grid-breakpoints: ( + xs: 0, + sm: 576px, + md: 838px, + lg: 1074px, + xl: 1416px +); + +//Shadow +$box-shadow: 0 .5rem 1rem rgba($black, .15); +$box-shadow-sm: 0 .125rem .25rem rgba($black, .075); +$box-shadow-lg: 0 1rem 3rem rgba($black, .25);//Safelite default +$box-shadow-inset: inset 0 1px 2px rgba($black, .075); + +//Alerts +$alert-bg-scale: -90%; +$alert-border-scale: -100%; +$alert-color-scale: 40%; + +//Modal animation +$modal-fade-transform: translate(0, 0); +$modal-backdrop-opacity: 0; 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 new file mode 100644 index 00000000..0d51bf57 --- /dev/null +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.spec.js @@ -0,0 +1,264 @@ +import { shallowMount } from "@vue/test-utils"; +import listButtonHorizontal from "./list-button-horizontal"; +import { nextTick } from "vue"; + +describe("list-button-horizontal.vue", () => { + it("Should return input type checkbox if isMultiSelect is true", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + isMultiSelect: true, + }, + }); + + // Assert + const input = wrapper.find("input"); + + expect(input.attributes().type).toEqual("checkbox"); + }); + + it("Should return input type radio if isMultiSelect is false or not specified", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + isMultiSelect: false, + }, + }); + + // Assert + const input = wrapper.find("input"); + + expect(input.attributes().type).toEqual("radio"); + }); + + it("Should return primary label text (buttonID)", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + buttonID: "List Card Checkbox", + }, + }); + + // Assert + const label = wrapper.find("label"); + + expect(label.attributes().for).toEqual("List Card Checkbox"); + }); + + it("Should return screen reader text", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + screenReaderOnlyText: "Screen Reader Only Text", + }, + }); + + // Assert + const paragraph = wrapper.find("span.sr-only"); + + expect(paragraph.text()).toEqual("Screen Reader Only Text"); + }); + + it("Should return text alignment class", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + textPosition: "text-center", + }, + }); + + // Assert + const paragraph = wrapper.find("span.m-0"); + + expect(paragraph.attributes("class")).toContain("text-center"); + }); + + it("Should return aria-required state", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + isRequired: true, + }, + }); + + // Assert + const input = wrapper.find("input"); + + expect(input.attributes()["aria-required"]).toEqual("true"); + }); + + it("Should return loader enabled true", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + propsData: { + selectingInitiatesLoad: true, + }, + }); + + // Assert + + const label = wrapper.find("label"); + + wrapper.vm.handleCheckChange = jest.fn(); + wrapper.vm.triggerButton(); + + await nextTick(); + + const loader = wrapper.find("loader-stub"); + + expect(loader.exists()).toBe(true); + }); + + it("Should return loader color", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + propsData: { + loaderColor: "blue", + selectingInitiatesLoad: true, + }, + }); + + // Assert + + const label = wrapper.find("label"); + + wrapper.vm.handleCheckChange = jest.fn(); + wrapper.vm.triggerButton(); + + await nextTick(); + + const loader = wrapper.find("loader-stub"); + + expect(loader.attributes("class")).toContain("blue"); + }); + + it("Should return loader position", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + propsData: { + loaderPosition: "right", + selectingInitiatesLoad: true, + }, + }); + + // Assert + + const label = wrapper.find("label"); + + wrapper.vm.handleCheckChange = jest.fn(); + wrapper.vm.triggerButton(); + + await nextTick(); + + const loader = wrapper.find("loader-stub"); + + expect(loader.attributes("class")).toContain("right"); + }); + + it("Should emit button value on click", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + value: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: false, + modelValue: ["List Card Checkbox"], + }, + }); + wrapper.vm.handleCheckChange(); + // Assert + expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean, buttonId: undefined, checkValue: false}]); + }); + + it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { + // Act + const wrapper = shallowMount(listButtonHorizontal, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: false, + modelValue: ["List Card Checkbox"], + isMultiSelect: false, + value: "Car-Front", + selectedValues: ["Car-Front"] + }, + }); + // Assert + expect(wrapper.vm.checkValue).toEqual(true); + }); + + 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 new file mode 100644 index 00000000..534448b7 --- /dev/null +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue @@ -0,0 +1,344 @@ + + + + + + \ No newline at end of file diff --git a/src/ux-components/list-button/list-button.spec.js b/src/ux-components/list-button/list-button.spec.js new file mode 100644 index 00000000..e54710ed --- /dev/null +++ b/src/ux-components/list-button/list-button.spec.js @@ -0,0 +1,256 @@ +import { shallowMount } from "@vue/test-utils"; +import listButton from "./list-button"; +import { nextTick } from "vue"; + +describe("list-button.vue", () => { + it("Should return input type checkbox if isMultiSelect is true", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + isMultiSelect: true, + }, + }); + + // Assert + const input = wrapper.find("input"); + + expect(input.attributes().type).toEqual("checkbox"); + }); + + it("Should return input type radio if isMultiSelect is false or not specified", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + isMultiSelect: false, + }, + }); + + // Assert + const input = wrapper.find("input"); + + expect(input.attributes().type).toEqual("radio"); + }); + + it("Should return primary label text (buttonID)", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + buttonID: "List Card Checkbox", + }, + }); + + // Assert + const label = wrapper.find("label"); + + expect(label.attributes().for).toEqual("List Card Checkbox"); + }); + + it("Should return screen reader text", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + screenReaderOnlyText: "Screen Reader Only Text", + }, + }); + + // Assert + const paragraph = wrapper.find("span.sr-only"); + + expect(paragraph.text()).toEqual("Screen Reader Only Text"); + }); + + it("Should return text alignment class", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + textPosition: "text-center", + }, + }); + + // Assert + const paragraph = wrapper.find("span.m-0"); + + expect(paragraph.attributes("class")).toContain("text-center"); + }); + + it("Should return aria-required state", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + isRequired: true, + }, + }); + + // Assert + const input = wrapper.find("input"); + + expect(input.attributes()["aria-required"]).toEqual("true"); + }); + + it("Should return loader enabled true", async () => { + // Act + const wrapper = shallowMount(listButton, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + propsData: { + selectingInitiatesLoad: true, + }, + }); + + // Assert + wrapper.vm.handleCheckChange = jest.fn(); + wrapper.vm.triggerButton(); + + await nextTick(); + + const loader = wrapper.find("loader-stub"); + + expect(loader.exists()).toBe(true); + }); + + it("Should return loader color", async () => { + // Act + const wrapper = shallowMount(listButton, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + propsData: { + loaderColor: "blue", + selectingInitiatesLoad: true, + }, + }); + + // Assert + wrapper.vm.handleCheckChange = jest.fn(); + wrapper.vm.triggerButton(); + await nextTick(); + + const loader = wrapper.find("loader-stub"); + + expect(loader.attributes("class")).toContain("blue"); + }); + + it("Should return loader position", async () => { + // Act + const wrapper = shallowMount(listButton, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + propsData: { + loaderPosition: "right", + selectingInitiatesLoad: true, + }, + }); + + // Assert + wrapper.vm.handleCheckChange = jest.fn(); + wrapper.vm.triggerButton(); + + await nextTick(); + + const loader = wrapper.find("loader-stub"); + + expect(loader.attributes("class")).toContain("right"); + }); + + it("Should emit button value on click", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + value: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: false, + modelValue: ["List Card Checkbox"], + buttonID: 'list-card-id' + }, + }); + + wrapper.vm.handleCheckChange(); + + // Assert + expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: false, buttonId: 'list-card-id'}]); + + }); + + it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { + // Act + const wrapper = shallowMount(listButton, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: false, + modelValue: ["List Card Checkbox"], + selectedValues: "Car-Front" + }, + }); + // Assert + expect(wrapper.componentVM.checkValue).toEqual(false); + }); + + 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 new file mode 100644 index 00000000..c789060e --- /dev/null +++ b/src/ux-components/list-button/list-button.vue @@ -0,0 +1,241 @@ + + + + + + \ No newline at end of file diff --git a/src/ux-components/list-card/list-card.spec.js b/src/ux-components/list-card/list-card.spec.js new file mode 100644 index 00000000..9c9e1986 --- /dev/null +++ b/src/ux-components/list-card/list-card.spec.js @@ -0,0 +1,329 @@ +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 () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isMultiSelect: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "checkbox-demo-1", + groupName: "Checkbox 1", + buttonImage: "windshield-damage.svg", + }, + }); + + // Assert + const input = wrapper.find("input"); + expect(input.attributes().type).toEqual("checkbox"); + }); + + it("Should return primary label text", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + }, + }); + + // Assert + const paragraph = wrapper.find("p"); + expect(paragraph.text()).toEqual("Windshield"); + }); + + it("Should return secondary (sub) label text", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + buttonLabelSubCopy: "Test", + }, + }); + + // Assert + const paragraph = wrapper.find("p:nth-of-type(2)"); + expect(paragraph.text()).toEqual("Test"); + }); + + it("Should return value used for various text settings including the label 'for' and input id", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + buttonLabelSubCopy: "Test", + }, + }); + + // Assert + const label = wrapper.find("label"); + expect(label.attributes().for).toEqual("List Card Checkbox"); + }); + + it("Should return input group name used for radio or checkbox", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + buttonLabelSubCopy: "Test", + }, + }); + + // Assert + const input = wrapper.find("input"); + expect(input.attributes().name).toEqual("radio 1"); + }); + + it("Should return aria-required state", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + }, + }); + + // Assert + const input = wrapper.find("input"); + expect(input.attributes()["aria-required"]).toEqual("true"); + }); + + it("Should return flex row classes if isWide is true", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: true, + buttonLabelSubCopy: "", + }, + }); + + // Assert + const label = wrapper.find("label"); + expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-2", "ps-4", "pe-4"]); + }); + + it("Should return flex row classes if isWide is true and checkboxTop if buttonLabelSubCopy is true", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: true, + buttonLabelSubCopy: "Button Subcopy", + }, + }); + + // Assert + const label = wrapper.find("label"); + expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-2", "ps-4", "pe-4", "checkboxTop"]); + }); + + it("Should return flex column classes if isWide is false", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: false, + }, + }); + + // Assert + const label = wrapper.find("label"); + expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-column", "pt-4", "pb-3"]); + }); + + it("Should emit button value on click", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + value: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: false, + buttonID: 'list-card-id', + selectedValues: "List Card Checkbox" + }, + }); + wrapper.vm.handleCheckChange(); + // Assert + expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: true, buttonId: 'list-card-id'}]); + }); + + it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { + // Act + const wrapper = shallowMount(listCard, { + propsData: { + isRadioHorizontal: true, + buttonLabel: "Windshield", + value: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + isWide: false, + selectedValues: "Car-Front" + }, + }); + // Assert + expect(wrapper.componentVM.checkValue).toEqual(false); + }); + + it("Should set an initial value for validation if selectedValues include the value", async () => { + // Arrange + const wrapper = shallowMount(listCard, { + propsData: { + value: "Windshield", + groupName: "radio 1", + selectedValues: ["Windshield"], + }, + }); + + // Assert + 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, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + 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, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + 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 new file mode 100644 index 00000000..eb2897b2 --- /dev/null +++ b/src/ux-components/list-card/list-card.vue @@ -0,0 +1,399 @@ + + + + + + \ No newline at end of file diff --git a/src/ux-components/loader/loader.vue b/src/ux-components/loader/loader.vue new file mode 100644 index 00000000..25d7e02d --- /dev/null +++ b/src/ux-components/loader/loader.vue @@ -0,0 +1,90 @@ + + + + + + \ No newline at end of file diff --git a/src/ux-components/radio/radio.spec.js b/src/ux-components/radio/radio.spec.js new file mode 100644 index 00000000..bf2594e0 --- /dev/null +++ b/src/ux-components/radio/radio.spec.js @@ -0,0 +1,110 @@ +import { shallowMount } from "@vue/test-utils"; +import radio from "./radio"; + +describe("radio.vue", () => { + it("Should return group name", async () => { + // Act + const wrapper = shallowMount(radio, { + propsData: { + groupName: "radio-button-test", + }, + }); + + // Assert + const input = wrapper.find("input"); + + // Expect + expect(input.attributes().name).toEqual("radio-button-test"); + }); + + it("Should return checkbox id", async () => { + // Act + const wrapper = shallowMount(radio, { + propsData: { + buttonID: "Radio ID", + }, + }); + + // Assert + const input = wrapper.find("input"); + + // Expect + expect(input.attributes().id).toEqual("Radio ID"); + }); + + it("Should return label text", async () => { + // Act + const wrapper = shallowMount(radio, { + propsData: { + buttonLabel: "label text", + }, + }); + + // Assert + const paragraph = wrapper.find("p"); + + expect(paragraph.text()).toEqual("label text"); + }); + + it("Should return label text", async () => { + // Act + const wrapper = shallowMount(radio, { + propsData: { + screenReaderOnlyText: "screenreader text", + }, + }); + + // Assert + const paragraph = wrapper.find("span"); + + expect(paragraph.text()).toEqual("screenreader text"); + }); + + it("Should emit button value on click", async () => { + // Act + const wrapper = shallowMount(radio, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + propsData: { + buttonLabel: "Windshield", + value: "List Card Checkbox", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + isRequired: true, + isWide: false, + modelValue: ["List Card Checkbox"], + }, + }); + wrapper.vm.handleCheckChange(); + // Assert + expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{"buttonID": "List Card Checkbox", value: "List Card Checkbox", checkValue: false}]);; + }); + + it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { + // Act + const wrapper = shallowMount(radio, { + global: { + mocks: { + '$route': { query: { issPage: 'page-name' } }, + } + }, + propsData: { + buttonLabel: "Windshield", + buttonID: "List Card Checkbox", + groupID: "radio-demo-1", + groupName: "radio 1", + buttonImage: "windshield-damage.svg", + isRequired: true, + modelValue: ["List Card Checkbox"], + value: "Car-Front", + selectedValues: "Car-Front" + }, + }); + // Assert + expect(wrapper.componentVM.checkValue).toEqual(true); + }); +}); diff --git a/src/ux-components/radio/radio.vue b/src/ux-components/radio/radio.vue new file mode 100644 index 00000000..1a0d52d7 --- /dev/null +++ b/src/ux-components/radio/radio.vue @@ -0,0 +1,139 @@ + + + + + + \ No newline at end of file diff --git a/vue.config.js b/vue.config.js index ccad75f0..97730661 100644 --- a/vue.config.js +++ b/vue.config.js @@ -10,6 +10,7 @@ module.exports = { // Note: only include functions, variables and mixins here additionalData: ` @import "./node_modules/bootstrap/scss/functions"; + @import "@/styles/ux-variables.scss"; @import "./node_modules/bootstrap/scss/variables"; @import "./node_modules/bootstrap/scss/mixins"; `, diff --git a/vue.release.config.js b/vue.release.config.js index f6b57ccf..49511e62 100644 --- a/vue.release.config.js +++ b/vue.release.config.js @@ -10,6 +10,7 @@ module.exports = { // Note: only include functions, variables and mixins here additionalData: ` @import "./node_modules/bootstrap/scss/functions"; + @import "@/styles/ux-variables.scss"; @import "./node_modules/bootstrap/scss/variables"; @import "./node_modules/bootstrap/scss/mixins"; `,