CASH-399 - Convert Technician Notes to Dropdown/Accordion
This commit is contained in:
parent
001a3e0e86
commit
6a7ffac6d8
5 changed files with 251 additions and 65 deletions
|
|
@ -1,11 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="textarea-question">
|
<div class="textarea-question">
|
||||||
<div class="label-wrapper d-flex flex-column mb-1" :aria-label="questionText">
|
<div class="label-wrapper d-flex flex-column mb-1 mt-1" :aria-label="questionText">
|
||||||
<!-- Wrap label and span because v-html prevents v-if from displaying if v-if <span> is inside <label>-->
|
|
||||||
<div class="d-flex mb-1">
|
|
||||||
<label :for="textAreaLabelCopy" class="fw-bold" v-html="questionText"></label>
|
|
||||||
<span v-if="!isRequired" class="fw-normal ms-1">(Optional)</span>
|
|
||||||
</div>
|
|
||||||
<textarea
|
<textarea
|
||||||
:id="textAreaLabelCopy"
|
:id="textAreaLabelCopy"
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,82 @@
|
||||||
// Components
|
import { shallowMount, mount } from "@vue/test-utils";
|
||||||
import customerDetails from "@/layouts/customer-details/customer-details.vue";
|
import CustomerDetails from "./customer-details.vue";
|
||||||
|
import TechNotes from "./tech-notes/tech-notes.vue";
|
||||||
|
import TextboxQuestion from "@/digital-components/textbox-question/textbox-question.vue";
|
||||||
|
import PhoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question.vue";
|
||||||
|
import CheckboxQuestion from "@/digital-components/checkbox-question/checkbox-question.vue";
|
||||||
|
import TextBlock from "@/digital-components/text-block/text-block.vue";
|
||||||
|
import FunnelHeader from "@/fmg-components/funnel-header/funnel-header.vue";
|
||||||
|
import FunnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header.vue";
|
||||||
|
import Navbar from "@/fmg-components/nav-bar/nav-bar.vue";
|
||||||
|
|
||||||
// Supporting Files
|
const mockMixin = {
|
||||||
import { shallowMount } from "@vue/test-utils";
|
methods: {
|
||||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
getCmsContent() {
|
||||||
|
return "Mocked CMS Content";
|
||||||
// Mock our module for promises.
|
|
||||||
jest.mock("@/helpers/layout-helper.js", () => ({
|
|
||||||
settleAllPromises: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("customer-details.vue", () => {
|
|
||||||
describe("navigation", () => {
|
|
||||||
test("if the back button is clicked, navigate back", async () => {
|
|
||||||
// Arrange
|
|
||||||
const { wrapper } = setupMocks();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await wrapper.vm.backButtonAction();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("if the continue button is clicked, navigate forward", async () => {
|
|
||||||
// Arrange
|
|
||||||
const { wrapper } = setupMocks();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await wrapper.vm.forwardButtonAction();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupMocks() {
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
customerDetails,
|
|
||||||
getMountOptions({
|
|
||||||
router: {
|
|
||||||
navigate: jest.fn(),
|
|
||||||
navigate: jest.fn(),
|
|
||||||
navigateWithSaving: jest.fn(),
|
|
||||||
navigateWithoutSaving: jest.fn(),
|
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
);
|
};
|
||||||
|
|
||||||
return { wrapper };
|
const mockStoreActions = {
|
||||||
|
SAVE_CUSTOMER_DETAILS: "SAVE_CUSTOMER_DETAILS",
|
||||||
|
SAVE_SERVICE_LOCATION_TECH_NOTES: "SAVE_SERVICE_LOCATION_TECH_NOTES",
|
||||||
|
SAVE_PAYMENT_METHOD_CHOICE: "SAVE_PAYMENT_METHOD_CHOICE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("CustomerDetails.vue", () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = mount(CustomerDetails, {
|
||||||
|
global: {
|
||||||
|
components: {
|
||||||
|
TechNotes,
|
||||||
|
TextboxQuestion,
|
||||||
|
PhoneNumberQuestion,
|
||||||
|
CheckboxQuestion,
|
||||||
|
TextBlock,
|
||||||
|
FunnelHeader,
|
||||||
|
FunnelSubHeader,
|
||||||
|
Navbar,
|
||||||
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
|
mocks: {
|
||||||
|
storeActions: mockStoreActions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
firstName: "John",
|
||||||
|
lastName: "Doe",
|
||||||
|
emailAddress: "john.doe@example.com",
|
||||||
|
phoneNumber: "1234567890",
|
||||||
|
isSmsOptIn: true,
|
||||||
|
techNotes: "Initial Tech Notes",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should render the CustomerDetails component", () => {
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should render all child components", () => {
|
||||||
|
expect(wrapper.findComponent(TechNotes).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(TextboxQuestion).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(PhoneNumberQuestion).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(CheckboxQuestion).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(TextBlock).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(FunnelHeader).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(FunnelSubHeader).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(Navbar).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should pass the correct props to TechNotes", () => {
|
||||||
|
const techNotes = wrapper.findComponent(TechNotes);
|
||||||
|
expect(techNotes.props("modelValue")).toBe("Initial Tech Notes");
|
||||||
|
expect(techNotes.props("textAreaLabelCopy")).toBe("Mocked CMS Content");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,7 @@
|
||||||
cmsWidgetName="TextMeQuestionWidget"
|
cmsWidgetName="TextMeQuestionWidget"
|
||||||
v-model="isSmsOptIn" />
|
v-model="isSmsOptIn" />
|
||||||
|
|
||||||
<textareaQuestion
|
<techNotes v-model="techNotes" :textAreaLabelCopy="textAreaLabelCopy" />
|
||||||
class="mb-4"
|
|
||||||
v-model="techNotes"
|
|
||||||
cmsWidgetName="TextAreaContentWidget"
|
|
||||||
textAreaLabelCopy="Notes for your technician"
|
|
||||||
maxLength="150" />
|
|
||||||
|
|
||||||
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
|
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
|
||||||
|
|
||||||
<navbar
|
<navbar
|
||||||
|
|
@ -72,20 +66,18 @@
|
||||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||||
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
||||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||||
import textareaQuestion from "@/digital-components/textarea-question/textarea-question";
|
|
||||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||||
import phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
|
import phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
|
||||||
import textBlock from "@/digital-components/text-block/text-block";
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||||
|
import techNotes from "@/layouts/customer-details/tech-notes/tech-notes";
|
||||||
//Supporting files
|
//Supporting files
|
||||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import { errorMessages } from "@/constants/error-messages";
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
import { routerParams } from "@/router/router-constants/router-params";
|
|
||||||
import { required, regex } from "@/helpers/validation-rules";
|
import { required, regex } from "@/helpers/validation-rules";
|
||||||
import { Form, defineRule } from "vee-validate";
|
import { Form, defineRule } from "vee-validate";
|
||||||
import { useField, validate } from "vee-validate";
|
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||||
|
|
||||||
|
|
@ -199,13 +191,13 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
funnelHeader,
|
funnelHeader,
|
||||||
funnelSubHeader,
|
funnelSubHeader,
|
||||||
textareaQuestion,
|
|
||||||
textboxQuestion,
|
textboxQuestion,
|
||||||
navbar,
|
navbar,
|
||||||
Form,
|
Form,
|
||||||
textBlock,
|
textBlock,
|
||||||
phoneNumberQuestion,
|
phoneNumberQuestion,
|
||||||
checkboxQuestion,
|
checkboxQuestion,
|
||||||
|
techNotes,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
76
src/layouts/customer-details/tech-notes/tech-notes.spec.js
Normal file
76
src/layouts/customer-details/tech-notes/tech-notes.spec.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { shallowMount, mount } from "@vue/test-utils";
|
||||||
|
import TechNotes from "./tech-notes.vue";
|
||||||
|
import TextareaQuestion from "@/digital-components/textarea-question/textarea-question.vue";
|
||||||
|
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent() {
|
||||||
|
return "Mocked CMS Content";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("TechNotes.vue", () => {
|
||||||
|
it("Should render TechNotes", async () => {
|
||||||
|
// Arrange
|
||||||
|
const wrapper = shallowMount(TechNotes, {
|
||||||
|
props: {
|
||||||
|
modelValue: "Initial Tech Notes",
|
||||||
|
textAreaLabelCopy: "Notes for your technician",
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mixins: [mockMixin],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const techNotes = wrapper.findComponent({ ref: "techNotes" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(techNotes.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should populate the techNotes prop and render it correctly", async () => {
|
||||||
|
// Arrange
|
||||||
|
const wrapper = mount(TechNotes, {
|
||||||
|
props: {
|
||||||
|
modelValue: "Populated Tech Notes",
|
||||||
|
textAreaLabelCopy: "Notes for your technician",
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
components: {
|
||||||
|
TextareaQuestion,
|
||||||
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const textarea = wrapper.find("textarea");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(textarea.element.value).toBe("Populated Tech Notes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should toggle the active class when clicked", async () => {
|
||||||
|
// Arrange
|
||||||
|
const wrapper = shallowMount(TechNotes, {
|
||||||
|
props: {
|
||||||
|
modelValue: "Initial Tech Notes",
|
||||||
|
textAreaLabelCopy: "Notes for your technician",
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mixins: [mockMixin],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleElement = wrapper.find(".tech-notes-toggle");
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(wrapper.vm.isActive).toBe(false); // inactive
|
||||||
|
await toggleElement.trigger("click");
|
||||||
|
expect(wrapper.vm.isActive).toBe(true); // Active after first click
|
||||||
|
await toggleElement.trigger("click");
|
||||||
|
expect(wrapper.vm.isActive).toBe(false); // Inactive after second click
|
||||||
|
});
|
||||||
|
});
|
||||||
94
src/layouts/customer-details/tech-notes/tech-notes.vue
Normal file
94
src/layouts/customer-details/tech-notes/tech-notes.vue
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<div class="tech-notes">
|
||||||
|
<div class="tech-notes-toggle" :class="{ active: isActive }" @click="toggleClass">
|
||||||
|
<textLink linkType="text" href="#!" :text="textAreaLabelCopy" />
|
||||||
|
</div>
|
||||||
|
<div class="tech-notes-content">
|
||||||
|
<textareaQuestion
|
||||||
|
ref="techNotes"
|
||||||
|
class="mb-4"
|
||||||
|
v-model="localTechNotes"
|
||||||
|
cmsWidgetName="TextAreaContentWidget"
|
||||||
|
:textAreaLabelCopy="textAreaLabelCopy"
|
||||||
|
maxLength="150" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import textLink from "@/ux-components/text-link/text-link";
|
||||||
|
import textareaQuestion from "@/digital-components/textarea-question/textarea-question";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "techNotes",
|
||||||
|
components: {
|
||||||
|
textLink,
|
||||||
|
textareaQuestion,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
textAreaLabelCopy: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
localTechNotes: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isActive: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleClass() {
|
||||||
|
this.isActive = !this.isActive;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tech-notes {
|
||||||
|
.tech-notes-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.9' xml:space='preserve'%3e%3cpath d='M8 8.9c-.2 0-.5-.1-.6-.3L.3 1.5C.1 1.4 0 1.1 0 .9 0 .7.1.4.3.3.4.1.7 0 .9 0c.2 0 .5.1.6.3L8 6.7 14.5.2c.1-.1.4-.2.6-.2.2 0 .5.1.6.3s.3.4.3.6c0 .2-.1.5-.3.6L8.6 8.6c-.1.2-.4.3-.6.3z' fill='%231474a2'/%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right center;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
width: 16px;
|
||||||
|
height: 9px;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
&.active:after {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
&.active + .tech-notes-content {
|
||||||
|
max-height: 500px;
|
||||||
|
transition: all 250ms ease-in;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tech-notes-content {
|
||||||
|
max-height: 0;
|
||||||
|
transition: all 250ms ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue