CASH-399 - Convert Technician Notes to Dropdown/Accordion

This commit is contained in:
Minojhini Valaiyapathi 2025-03-19 11:51:34 -04:00
parent 001a3e0e86
commit 6a7ffac6d8
5 changed files with 251 additions and 65 deletions

View file

@ -1,11 +1,6 @@
<template>
<div class="textarea-question">
<div class="label-wrapper d-flex flex-column mb-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>
<div class="label-wrapper d-flex flex-column mb-1 mt-1" :aria-label="questionText">
<textarea
:id="textAreaLabelCopy"
ref="textarea"
@ -44,8 +39,8 @@ export default {
default: "text area",
},
},
// TODO: At some point in the future we should probably add the tie in to validation here in case the field must be populated for some other use cases
setup() {},
// TODO: At some point in the future we should probably add the tie in to validation here in case the field must be populated for some other use cases
setup() {},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");

View file

@ -1,53 +1,82 @@
// Components
import customerDetails from "@/layouts/customer-details/customer-details.vue";
import { shallowMount, mount } from "@vue/test-utils";
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
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
const mockMixin = {
methods: {
getCmsContent() {
return "Mocked CMS Content";
},
},
};
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
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("customer-details.vue", () => {
describe("navigation", () => {
test("if the back button is clicked, navigate back", async () => {
// Arrange
const { wrapper } = setupMocks();
describe("CustomerDetails.vue", () => {
let wrapper;
// 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();
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");
});
});
function setupMocks() {
const wrapper = shallowMount(
customerDetails,
getMountOptions({
router: {
navigate: jest.fn(),
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
})
);
return { wrapper };
}

View file

@ -47,13 +47,7 @@
cmsWidgetName="TextMeQuestionWidget"
v-model="isSmsOptIn" />
<textareaQuestion
class="mb-4"
v-model="techNotes"
cmsWidgetName="TextAreaContentWidget"
textAreaLabelCopy="Notes for your technician"
maxLength="150" />
<techNotes v-model="techNotes" :textAreaLabelCopy="textAreaLabelCopy" />
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
<navbar
@ -72,20 +66,18 @@
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
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 phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
import textBlock from "@/digital-components/text-block/text-block";
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
import techNotes from "@/layouts/customer-details/tech-notes/tech-notes";
//Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import { errorMessages } from "@/constants/error-messages";
import { routerParams } from "@/router/router-constants/router-params";
import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { useField, validate } from "vee-validate";
import store from "@/store";
import { paymentMethods } from "@/constants/payment-method-constants";
@ -199,13 +191,13 @@ export default {
components: {
funnelHeader,
funnelSubHeader,
textareaQuestion,
textboxQuestion,
navbar,
Form,
textBlock,
phoneNumberQuestion,
checkboxQuestion,
techNotes,
},
};
</script>

View 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
});
});

View 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>