Merge branch 'release/2026.08.13' into feature/CASH-2820
This commit is contained in:
commit
2efdf1db50
59 changed files with 1690 additions and 754 deletions
|
|
@ -32,6 +32,7 @@ const errorMessages = {
|
||||||
DATE_REQUIRED: "Please select a date",
|
DATE_REQUIRED: "Please select a date",
|
||||||
PHONE_REQUIRED: "Please enter your phone number",
|
PHONE_REQUIRED: "Please enter your phone number",
|
||||||
PHONE_FORMAT: "Phone number must be 10 digits",
|
PHONE_FORMAT: "Phone number must be 10 digits",
|
||||||
|
PHONE_EXTENSION_FORMAT: "The specified extension is invalid",
|
||||||
SMS_CONSENT_REQUIRED_1: "Please select checkbox to receive text messages",
|
SMS_CONSENT_REQUIRED_1: "Please select checkbox to receive text messages",
|
||||||
SMS_CONSENT_REQUIRED_2: "Please select at least one consent option",
|
SMS_CONSENT_REQUIRED_2: "Please select at least one consent option",
|
||||||
YEAR_REQUIRED: "Please select your vehicle year",
|
YEAR_REQUIRED: "Please select your vehicle year",
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,17 @@ const experimentSettings = {
|
||||||
TIER_ONE: "TierOne",
|
TIER_ONE: "TierOne",
|
||||||
TIER_TWO: "TierTwo",
|
TIER_TWO: "TierTwo",
|
||||||
TIER_THREE: "TierThree",
|
TIER_THREE: "TierThree",
|
||||||
|
TIER_TWO_DESCRIPTION: "TierTwo_Description",
|
||||||
|
TIER_THREE_DESCRIPTION: "TierThree_Description",
|
||||||
|
|
||||||
// Consent Management
|
// Consent Management
|
||||||
SHOW_CONSENT_MANAGEMENT: "ShowConsentManagement",
|
SHOW_CONSENT_MANAGEMENT: "ShowConsentManagement",
|
||||||
|
|
||||||
// YMM
|
// YMM
|
||||||
BAILOUT_VIN_REQUIRED_VEHICLES: "BailoutVINRequiredVehicles",
|
BAILOUT_VIN_REQUIRED_VEHICLES: "BailoutVINRequiredVehicles",
|
||||||
|
|
||||||
|
// Promo Banner
|
||||||
|
SHOW_PROMO_BANNER: "ShowQuotePageRegionalPromoBanner",
|
||||||
};
|
};
|
||||||
|
|
||||||
const experimentTriggers = {
|
const experimentTriggers = {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ const queryStrings = {
|
||||||
FROM_HERITAGE: "fromheritage",
|
FROM_HERITAGE: "fromheritage",
|
||||||
PHONE_NUMBER: "phonenumber",
|
PHONE_NUMBER: "phonenumber",
|
||||||
OFFER_QUOTE: "offerquote",
|
OFFER_QUOTE: "offerquote",
|
||||||
|
PIA_AUTH_AMOUNT_ADJUST: "piaauthamountadjust",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { queryStrings };
|
export { queryStrings };
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ const storeActions = {
|
||||||
SAVE_CCTOKEN: "saveCCToken",
|
SAVE_CCTOKEN: "saveCCToken",
|
||||||
SAVE_PAYPAL_TOKEN: "savePaypalToken",
|
SAVE_PAYPAL_TOKEN: "savePaypalToken",
|
||||||
SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount",
|
SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount",
|
||||||
|
SAVE_AUTHORIZATION_AMOUNT: "saveAuthorizationAmount",
|
||||||
SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn",
|
SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn",
|
||||||
SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling",
|
SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling",
|
||||||
SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected",
|
SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const storeMutations = {
|
||||||
UPDATE_CCTOKEN: "updateCCToken",
|
UPDATE_CCTOKEN: "updateCCToken",
|
||||||
UPDATE_PAYPAL_TOKEN: "updatePaypalToken",
|
UPDATE_PAYPAL_TOKEN: "updatePaypalToken",
|
||||||
UPDATE_NEXTGEN_SETTLED_AMOUNT: "updateNextGenSettledAmount",
|
UPDATE_NEXTGEN_SETTLED_AMOUNT: "updateNextGenSettledAmount",
|
||||||
|
UPDATE_AUTHORIZATION_AMOUNT: "updateAuthorizationAmount",
|
||||||
|
|
||||||
// VEHICLE MUTATIONS
|
// VEHICLE MUTATIONS
|
||||||
UPDATE_YEAR: "updateYear",
|
UPDATE_YEAR: "updateYear",
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,9 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<loader
|
<div v-if="loadingStatus !== 'none'" class="date-picker-loader">
|
||||||
class="date-picker-loader"
|
<loader loaderColor="blue" loaderPosition="center" />
|
||||||
:class="[loadingStatus !== 'none' ? 'show-loader' : '']"
|
</div>
|
||||||
loaderColor="blue"
|
|
||||||
loaderPosition="center" />
|
|
||||||
<div class="row form-test-error" id="date-of-month-error">
|
<div class="row form-test-error" id="date-of-month-error">
|
||||||
<ErrorMessage :name="customComponentId" class="small mt-1"></ErrorMessage>
|
<ErrorMessage :name="customComponentId" class="small mt-1"></ErrorMessage>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -742,15 +740,6 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.date-picker-loader {
|
|
||||||
opacity: 0;
|
|
||||||
max-height: 0;
|
|
||||||
|
|
||||||
&.show-loader {
|
|
||||||
opacity: 1;
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.date-picker {
|
.date-picker {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|
@ -769,13 +758,20 @@ export default {
|
||||||
font-family: UrbanistSemiBold;
|
font-family: UrbanistSemiBold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.loader {
|
.date-picker-loader {
|
||||||
height: 2rem;
|
display: flex;
|
||||||
width: calc(100% - 1.5rem);
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&::after {
|
:deep(.loader) {
|
||||||
width: 1.5rem;
|
height: 2rem;
|
||||||
height: 1.5rem;
|
width: calc(100% - 1.5rem);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.month {
|
.month {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
:id="modalId + '-modalbtn'"
|
:id="modalId + '-modalbtn'"
|
||||||
ref="modalButtonMain"
|
ref="modalButtonMain"
|
||||||
loaderColor="white"
|
loaderColor="white"
|
||||||
|
loaderPosition="center"
|
||||||
:isDisabled="footerButtonDisabled"
|
:isDisabled="footerButtonDisabled"
|
||||||
:buttonText="footerButtonText"
|
:buttonText="footerButtonText"
|
||||||
@click-event="validateAndEmit"
|
@click-event="validateAndEmit"
|
||||||
|
|
|
||||||
|
|
@ -36,48 +36,6 @@ describe("modal-button-main.vue", () => {
|
||||||
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return loader color", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
modalButtonMain,
|
|
||||||
setupMocks({
|
|
||||||
propsData: {
|
|
||||||
loaderColor: "blue",
|
|
||||||
loaderEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.clicked();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
expect(loader.attributes("class")).toContain("blue");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader position", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
modalButtonMain,
|
|
||||||
setupMocks({
|
|
||||||
propsData: {
|
|
||||||
loaderPosition: "right",
|
|
||||||
loaderEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.clicked();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
expect(loader.attributes("class")).toContain("right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const wrapper = shallowMount(
|
const wrapper = shallowMount(
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@
|
||||||
@click="clicked">
|
@click="clicked">
|
||||||
<span class="m-0">{{ this.buttonText }}</span>
|
<span class="m-0">{{ this.buttonText }}</span>
|
||||||
<loader
|
<loader
|
||||||
class="ms-2"
|
|
||||||
v-if="isLoaderDisplayed && !suppressLoader"
|
v-if="isLoaderDisplayed && !suppressLoader"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
:loaderColor="loaderColor"
|
||||||
|
:loaderPosition="loaderPosition" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,11 @@ describe("Question Chain component", () => {
|
||||||
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
|
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should NOT emit update:modelValue if returnedAnswer contains 'nextQuestion'", () => {
|
test("should not emit update:modelValue for nextQuestion during normal forward progress", () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
const { wrapper } = setupMocks({});
|
const { wrapper } = setupMocks({});
|
||||||
const testQuestion = {};
|
const testQuestion = {};
|
||||||
const testReturnedAnswer = "1|nextQuestion|DB10840|No";
|
const testReturnedAnswer = "1|nextQuestion|3|No";
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer);
|
wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer);
|
||||||
|
|
@ -121,6 +121,24 @@ describe("Question Chain component", () => {
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
|
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should emit update:modelValue for nextQuestion when revising a saved answer", async () => {
|
||||||
|
//Arrange
|
||||||
|
const { wrapper } = setupMocks({});
|
||||||
|
await wrapper.setProps({ hasSavedAnswer: true });
|
||||||
|
const testQuestion = {};
|
||||||
|
const testReturnedAnswer = "1|nextQuestion|3|No";
|
||||||
|
|
||||||
|
//Act
|
||||||
|
wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(wrapper.emitted()).toHaveProperty("update:modelValue");
|
||||||
|
expect(wrapper.emitted()["update:modelValue"][0][0]).toMatchObject({
|
||||||
|
incomplete: true,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("method getQuestionChainAnswerIfComplete...", () => {
|
describe("method getQuestionChainAnswerIfComplete...", () => {
|
||||||
|
|
@ -213,25 +231,103 @@ describe("Question Chain component", () => {
|
||||||
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.vm.questions[1].answerSelected).toBeUndefined;
|
expect(wrapper.vm.questions[1].answerSelected).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return false if returnedAnswer is a nextQuestion (not a final answer)", async () => {
|
test("should follow the No path and show the next question when changing Q1 from Yes to No", async () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
questionDataProp: [
|
||||||
|
{
|
||||||
|
questionSequence: 1,
|
||||||
|
questionText: "Is this the Overland edition?",
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
answerResult: null,
|
||||||
|
answerText: "Yes",
|
||||||
|
nextQuestionSequence: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
answerResult: null,
|
||||||
|
answerText: "No",
|
||||||
|
nextQuestionSequence: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 2,
|
||||||
|
questionText: "Rain sensing wipers?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "PART-A", answerText: "Yes", nextQuestionSequence: 3 },
|
||||||
|
{ answerResult: "PART-B", answerText: "No", nextQuestionSequence: 3 },
|
||||||
|
],
|
||||||
|
answerSelected: "2|answer|PART-A|Yes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 3,
|
||||||
|
questionText: "Laminated door glass?",
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
answerResult: "PART-C",
|
||||||
|
answerText: "Yes",
|
||||||
|
nextQuestionSequence: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
answerResult: "PART-D",
|
||||||
|
answerText: "No",
|
||||||
|
nextQuestionSequence: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
answerSelected: "3|answer|PART-C|Yes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 5,
|
||||||
|
questionText: "Factory installed antenna?",
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
answerResult: "PART-E",
|
||||||
|
answerText: "Yes",
|
||||||
|
nextQuestionSequence: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
answerResult: "PART-F",
|
||||||
|
answerText: "No",
|
||||||
|
nextQuestionSequence: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await wrapper.setProps({ index: 0 });
|
||||||
|
|
||||||
|
const result = wrapper.vm.getQuestionChainAnswerIfComplete("1|nextQuestion|5|No");
|
||||||
|
|
||||||
|
expect(result.incomplete).toBe(true);
|
||||||
|
expect(wrapper.vm.questions[1].answerSelected).toBeUndefined();
|
||||||
|
expect(wrapper.vm.questions[2].answerSelected).toBeUndefined();
|
||||||
|
expect(wrapper.vm.currentQuestionNum).toBe(5);
|
||||||
|
expect(result.answeredQuestions).toHaveLength(1);
|
||||||
|
expect(result.answeredQuestions[0].selectedAnswerText).toBe("No");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return incomplete answer if returnedAnswer is a nextQuestion (not a final answer)", async () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
const { wrapper } = setupMocks({});
|
const { wrapper } = setupMocks({});
|
||||||
const testReturnedAnswer = "1|nextQuestion|DB10840|No";
|
const testReturnedAnswer = "1|nextQuestion|3|No";
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
const result = wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
const result = wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
expect(result).toBeFalsy();
|
expect(result).toMatchObject({
|
||||||
|
incomplete: true,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should trigger scroll to .current-question if returnedAnswer is a nextQuestion (not a final answer)", async () => {
|
test("should trigger scroll to .current-question if returnedAnswer is a nextQuestion (not a final answer)", async () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
const { wrapper } = setupMocks({});
|
const { wrapper } = setupMocks({});
|
||||||
const testReturnedAnswer = "1|nextQuestion|DB10840|No";
|
const testReturnedAnswer = "1|nextQuestion|3|No";
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
||||||
|
|
@ -323,9 +419,206 @@ describe("Question Chain component", () => {
|
||||||
expect(wrapper.vm.questions[0].answers[0].problemQuestionId).toBe(9531);
|
expect(wrapper.vm.questions[0].answers[0].problemQuestionId).toBe(9531);
|
||||||
expect(wrapper.vm.questions[0].answers[1].problemQuestionId).toBe(9529);
|
expect(wrapper.vm.questions[0].answers[1].problemQuestionId).toBe(9529);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should normalize uppercase answerSelected to match button value on created", async () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
questionDataProp: [
|
||||||
|
{
|
||||||
|
questionSequence: 1,
|
||||||
|
questionText: "Is your vehicle equipped with rain sensing wipers?",
|
||||||
|
answerSelected: "1|ANSWER|DW01705|YES",
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
answerResult: "DW01705",
|
||||||
|
answerText: "Yes",
|
||||||
|
nextQuestionSequence: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
answerResult: "DW01591",
|
||||||
|
answerText: "No",
|
||||||
|
nextQuestionSequence: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.questions[0].answerSelected).toBe("1|answer|DW01705|Yes");
|
||||||
|
});
|
||||||
|
test("should show question 3 after answering No on Cherokee Overland question", async () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
questionDataProp: getCherokeeWindshieldQuestions(),
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const q1 = wrapper.vm.questions[0];
|
||||||
|
wrapper.vm.handleAnswer(q1, "1|nextQuestion|3|No");
|
||||||
|
|
||||||
|
expect(wrapper.vm.currentQuestionNum).toBe(3);
|
||||||
|
expect(
|
||||||
|
wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question))
|
||||||
|
).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ questionSequence: 1 }),
|
||||||
|
expect.objectContaining({ questionSequence: 3 }),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question))
|
||||||
|
).not.toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining({ questionSequence: 2 })])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should resume at question 3 after remount when Q1 No was already saved", async () => {
|
||||||
|
const questionDataProp = getCherokeeWindshieldQuestions();
|
||||||
|
questionDataProp[0].answerSelected = "1|nextQuestion|3|No";
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({ questionDataProp });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.currentQuestionNum).toBe(3);
|
||||||
|
expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should resolve on Q1 Yes and not show Q2 for Honda HUD question", async () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
questionDataProp: getHondaAccordWindshieldQuestions(),
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const q1 = wrapper.vm.questions[0];
|
||||||
|
wrapper.vm.handleAnswer(q1, "1|answer|FW04796|Yes");
|
||||||
|
|
||||||
|
expect(wrapper.emitted()["update:modelValue"][0][0]).toMatchObject({
|
||||||
|
answerResult: "FW04796",
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
expect(wrapper.vm.currentQuestionNum).toBe(0);
|
||||||
|
expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should resolve on Q3 No and not continue after remount", async () => {
|
||||||
|
const questionDataProp = getHondaAccordWindshieldQuestions();
|
||||||
|
questionDataProp[0].answerSelected = "1|nextQuestion|2|No";
|
||||||
|
questionDataProp[1].answerSelected = "2|nextQuestion|3|No";
|
||||||
|
questionDataProp[2].answerSelected = "3|answer|FW04793|No";
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({ questionDataProp });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.currentQuestionNum).toBe(0);
|
||||||
|
expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true);
|
||||||
|
expect(
|
||||||
|
wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question))
|
||||||
|
).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show Q2 after answering No on Honda HUD question", async () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
questionDataProp: getHondaAccordWindshieldQuestions(),
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const q1 = wrapper.vm.questions[0];
|
||||||
|
wrapper.vm.handleAnswer(q1, "1|nextQuestion|2|No");
|
||||||
|
|
||||||
|
expect(wrapper.vm.currentQuestionNum).toBe(2);
|
||||||
|
expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getHondaAccordWindshieldQuestions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
questionSequence: 1,
|
||||||
|
questionText:
|
||||||
|
"Is your vehicle equipped with a Heads-up Display which projects vehicle information, such as speed, onto the windshield?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "FW04796", answerText: "Yes", nextQuestionSequence: null },
|
||||||
|
{ answerResult: "", answerText: "No", nextQuestionSequence: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 2,
|
||||||
|
questionText:
|
||||||
|
"Is your vehicle equipped with an auto-dimming rearview mirror which will darken automatically when a vehicles approaches from the rear at night?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "FW04795", answerText: "Yes", nextQuestionSequence: null },
|
||||||
|
{ answerResult: "", answerText: "No", nextQuestionSequence: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 3,
|
||||||
|
questionText: "Is your vehicle equipped with a power moonroof?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "FW04794", answerText: "Yes", nextQuestionSequence: null },
|
||||||
|
{ answerResult: "FW04793", answerText: "No", nextQuestionSequence: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCherokeeWindshieldQuestions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
questionSequence: 1,
|
||||||
|
questionText:
|
||||||
|
"Is your Cherokee the Overland edition which can be identified by having a wood and leather wrapped steering wheel?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "", answerText: "Yes", nextQuestionSequence: 2 },
|
||||||
|
{ answerResult: "", answerText: "No", nextQuestionSequence: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 2,
|
||||||
|
questionText:
|
||||||
|
"Is your vehicle equipped with rain sensing wipers that adjust their speed automatically when it rains?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "DW02270", answerText: "Yes", nextQuestionSequence: null },
|
||||||
|
{ answerResult: "DW02264", answerText: "No", nextQuestionSequence: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 3,
|
||||||
|
questionText:
|
||||||
|
"Is your vehicle equipped with rain sensing wipers that adjust their speed automatically when it rains?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "DW02268", answerText: "Yes", nextQuestionSequence: null },
|
||||||
|
{ answerResult: "", answerText: "No", nextQuestionSequence: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 4,
|
||||||
|
questionText:
|
||||||
|
"Is your vehicle equipped with automatic climate control which will change the fan speed automatically in order to maintain a set temperature?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "", answerText: "Yes", nextQuestionSequence: 5 },
|
||||||
|
{ answerResult: "", answerText: "No", nextQuestionSequence: 6 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 5,
|
||||||
|
questionText: "Is your vehicle equipped with heated seats?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "DW02104", answerText: "Yes", nextQuestionSequence: null },
|
||||||
|
{ answerResult: "DW02103", answerText: "No", nextQuestionSequence: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionSequence: 6,
|
||||||
|
questionText: "Is your vehicle equipped with heated seats?",
|
||||||
|
answers: [
|
||||||
|
{ answerResult: "DW02102", answerText: "Yes", nextQuestionSequence: null },
|
||||||
|
{ answerResult: "DW02101", answerText: "No", nextQuestionSequence: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function setupMocks({
|
function setupMocks({
|
||||||
questionDataProp = [
|
questionDataProp = [
|
||||||
{
|
{
|
||||||
|
|
@ -351,6 +644,7 @@ function setupMocks({
|
||||||
const mountOptions = getMountOptions({});
|
const mountOptions = getMountOptions({});
|
||||||
mountOptions.propsData = {
|
mountOptions.propsData = {
|
||||||
questionData: questionDataProp,
|
questionData: questionDataProp,
|
||||||
|
index: 0,
|
||||||
};
|
};
|
||||||
mountOptions["attachTo"] = document.body;
|
mountOptions["attachTo"] = document.body;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<div v-for="(q, i) in questions" :key="i">
|
<div v-for="(q, i) in questions" :key="i">
|
||||||
<transition appear name="fade" mode="out-in">
|
<transition appear name="fade" mode="out-in">
|
||||||
<buttonQuestion
|
<buttonQuestion
|
||||||
v-if="q.answerSelected || q.questionSequence === currentQuestionNum"
|
v-if="isQuestionVisible(q)"
|
||||||
class="radioQuestion"
|
class="radioQuestion"
|
||||||
:class="q.questionSequence === currentQuestionNum && 'current-question'"
|
:class="isCurrentQuestion(q) && 'current-question'"
|
||||||
:questionText="q.questionText"
|
:questionText="q.questionText"
|
||||||
:answers="q.answers"
|
:answers="q.answers"
|
||||||
:groupName="`question-${index}-${q.questionSequence}`"
|
:groupName="`question-${index}-${q.questionSequence}`"
|
||||||
|
|
@ -19,6 +19,11 @@
|
||||||
<script>
|
<script>
|
||||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||||
import { useValidateForm } from "vee-validate";
|
import { useValidateForm } from "vee-validate";
|
||||||
|
import {
|
||||||
|
isTerminalQuestionChainAnswer,
|
||||||
|
mapQuestionAnswersForChain,
|
||||||
|
normalizeAnswerSelectedValue,
|
||||||
|
} from "@/helpers/question-chain-helper";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "questionChain",
|
name: "questionChain",
|
||||||
|
|
@ -34,54 +39,86 @@ export default {
|
||||||
modelValue: Object,
|
modelValue: Object,
|
||||||
index: Number,
|
index: Number,
|
||||||
answerKey: String,
|
answerKey: String,
|
||||||
|
hasSavedAnswer: Boolean,
|
||||||
|
hasDownstreamSavedAnswers: Boolean,
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
// do a test validation check upon create to prevent out of sync / incorrect valid states
|
// do a test validation check upon create to prevent out of sync / incorrect valid states
|
||||||
await useValidateForm(); // NOTE: needs to have async/await here; tested and won't work without it
|
await useValidateForm(); // NOTE: needs to have async/await here; tested and won't work without it
|
||||||
|
|
||||||
this.questionData.map((q, i) => {
|
this.questionData.map((q, i) => {
|
||||||
|
const answers = mapQuestionAnswersForChain(q);
|
||||||
const question = {
|
const question = {
|
||||||
questionText: q.questionText,
|
questionText: q.questionText,
|
||||||
questionSequence: q.questionSequence,
|
questionSequence: q.questionSequence,
|
||||||
answers: q.answers.map((a) => {
|
answers,
|
||||||
return {
|
answerSelected: normalizeAnswerSelectedValue(q.answerSelected, answers),
|
||||||
buttonLabel: a.answerText,
|
|
||||||
// Name will either be nextQuestionSequence or answerResult
|
|
||||||
// Name will be used by list-button as the input value.
|
|
||||||
// It must be a single string or number, so concatenating together a string with
|
|
||||||
// 4 pieces of data separated by pipe characters:
|
|
||||||
// question number|type of answer|answer value|answer text
|
|
||||||
value: a.nextQuestionSequence
|
|
||||||
? q.questionSequence +
|
|
||||||
"|nextQuestion|" +
|
|
||||||
a.nextQuestionSequence +
|
|
||||||
"|" +
|
|
||||||
a.answerText
|
|
||||||
: q.questionSequence + "|answer|" + a.answerResult + "|" + a.answerText,
|
|
||||||
nextQuestionSequence: a.nextQuestionSequence,
|
|
||||||
answerResult: a.answerResult,
|
|
||||||
problemQuestionId: a.problemQuestionId,
|
|
||||||
questionSequence: q.questionSequence,
|
|
||||||
questionType: a.nextQuestionSequence ? "nextQuestion" : "answer",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
answerSelected: q.answerSelected || "",
|
|
||||||
};
|
};
|
||||||
if (!q.suppressThisQuestion) {
|
if (!q.suppressThisQuestion) {
|
||||||
this.questions.push(question);
|
this.questions.push(question);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.modelValue?.length > 0 && this.questions.length > 0) {
|
if (this.questions.length > 0) {
|
||||||
// set this.currentQuestionNum to first valid question
|
this.initializeCurrentQuestionNum();
|
||||||
this.currentQuestionNum = this.questions[0].questionSequence;
|
this.scrollCurrentQuestionIntoView();
|
||||||
// scroll the next question into view
|
|
||||||
this.$nextTick(() => {
|
|
||||||
document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getQuestionSequence(question) {
|
||||||
|
return Number(question?.questionSequence);
|
||||||
|
},
|
||||||
|
isCurrentQuestion(question) {
|
||||||
|
return this.getQuestionSequence(question) === Number(this.currentQuestionNum);
|
||||||
|
},
|
||||||
|
isQuestionVisible(question) {
|
||||||
|
return !!question.answerSelected || this.isCurrentQuestion(question);
|
||||||
|
},
|
||||||
|
initializeCurrentQuestionNum() {
|
||||||
|
const lastAnsweredQuestion = [...this.questions]
|
||||||
|
.reverse()
|
||||||
|
.find((question) => question.answerSelected);
|
||||||
|
|
||||||
|
if (isTerminalQuestionChainAnswer(lastAnsweredQuestion?.answerSelected)) {
|
||||||
|
this.currentQuestionNum = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastAnsweredQuestion?.answerSelected?.includes("|nextQuestion|")) {
|
||||||
|
const nextQuestionSequence = Number(
|
||||||
|
lastAnsweredQuestion.answerSelected.split("|")[2]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.questions.some(
|
||||||
|
(question) => this.getQuestionSequence(question) === nextQuestionSequence
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.currentQuestionNum = nextQuestionSequence;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstUnanswered = this.questions.find((question) => !question.answerSelected);
|
||||||
|
|
||||||
|
if (firstUnanswered) {
|
||||||
|
this.currentQuestionNum = this.getQuestionSequence(firstUnanswered);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollCurrentQuestionIntoView() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
syncAnswerSelectedToQuestionData(questionSequence, answerSelected) {
|
||||||
|
const sourceQuestion = this.questionData?.find(
|
||||||
|
(q) => this.getQuestionSequence(q) === Number(questionSequence)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sourceQuestion) {
|
||||||
|
sourceQuestion.answerSelected = answerSelected;
|
||||||
|
}
|
||||||
|
},
|
||||||
getProblemQuestionIdFromSelectedAnswer(question, selectedAnswer) {
|
getProblemQuestionIdFromSelectedAnswer(question, selectedAnswer) {
|
||||||
if (!question?.answers || !selectedAnswer) {
|
if (!question?.answers || !selectedAnswer) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -100,10 +137,20 @@ export default {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
question.answerSelected = returnedAnswer;
|
question.answerSelected = returnedAnswer;
|
||||||
const isQuestionChainComplete = this.getQuestionChainAnswerIfComplete(returnedAnswer);
|
this.syncAnswerSelectedToQuestionData(question.questionSequence, returnedAnswer);
|
||||||
|
const questionChainAnswer = this.getQuestionChainAnswerIfComplete(returnedAnswer);
|
||||||
|
|
||||||
if (isQuestionChainComplete) {
|
if (!questionChainAnswer) {
|
||||||
this.$emit("update:modelValue", isQuestionChainComplete);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldNotifyParent =
|
||||||
|
!questionChainAnswer.incomplete ||
|
||||||
|
this.hasSavedAnswer ||
|
||||||
|
this.hasDownstreamSavedAnswers;
|
||||||
|
|
||||||
|
if (shouldNotifyParent) {
|
||||||
|
this.$emit("update:modelValue", questionChainAnswer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getQuestionChainAnswerIfComplete(returnedAnswer) {
|
getQuestionChainAnswerIfComplete(returnedAnswer) {
|
||||||
|
|
@ -117,20 +164,24 @@ export default {
|
||||||
// "5|answer|DW02104|Yes"
|
// "5|answer|DW02104|Yes"
|
||||||
|
|
||||||
const returnedAnswerArray = returnedAnswer.split("|");
|
const returnedAnswerArray = returnedAnswer.split("|");
|
||||||
const questionNum = parseInt(returnedAnswerArray[0]);
|
const questionNum = Number(returnedAnswerArray[0]);
|
||||||
const questionType = returnedAnswerArray[1];
|
const questionType = returnedAnswerArray[1];
|
||||||
const questionAnswer = returnedAnswerArray[2];
|
const questionAnswer = returnedAnswerArray[2];
|
||||||
const answeredQuestions = [];
|
const answeredQuestions = [];
|
||||||
|
|
||||||
this.questions.forEach((q) => {
|
this.questions.forEach((q) => {
|
||||||
|
const questionSequence = this.getQuestionSequence(q);
|
||||||
|
|
||||||
// find this question and mark it as "answered" by populating answerSelected
|
// find this question and mark it as "answered" by populating answerSelected
|
||||||
if (q.questionSequence === questionNum) {
|
if (questionSequence === questionNum) {
|
||||||
q.answerSelected = returnedAnswer;
|
q.answerSelected = returnedAnswer;
|
||||||
|
this.syncAnswerSelectedToQuestionData(questionSequence, returnedAnswer);
|
||||||
}
|
}
|
||||||
// remove all answers AFTER this question...
|
// remove all answers AFTER this question...
|
||||||
// (needed in case user is changing previously answered questions)
|
// (needed in case user is changing previously answered questions)
|
||||||
if (q.questionSequence > questionNum) {
|
if (questionSequence > questionNum) {
|
||||||
delete q.answerSelected;
|
delete q.answerSelected;
|
||||||
|
this.syncAnswerSelectedToQuestionData(questionSequence, "");
|
||||||
}
|
}
|
||||||
if (q.answerSelected) {
|
if (q.answerSelected) {
|
||||||
answeredQuestions.push({
|
answeredQuestions.push({
|
||||||
|
|
@ -146,22 +197,21 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// return false if there's a nextQuestion... or return an object with final answers (truthy)
|
// return incomplete answer if there's a nextQuestion... or return final answers (truthy)
|
||||||
if (questionType === "nextQuestion") {
|
if (questionType === "nextQuestion") {
|
||||||
// update to next question index
|
// update to next question index
|
||||||
this.currentQuestionNum = parseInt(questionAnswer); // update count to display next question
|
this.currentQuestionNum = Number(questionAnswer);
|
||||||
// scroll the next question into view
|
this.scrollCurrentQuestionIntoView();
|
||||||
this.$nextTick(() => {
|
return {
|
||||||
document
|
incomplete: true,
|
||||||
.querySelector(".current-question")
|
answeredQuestions,
|
||||||
.scrollIntoView({ behavior: "smooth" });
|
index: this.index,
|
||||||
});
|
};
|
||||||
return false;
|
|
||||||
} else {
|
} else {
|
||||||
// reset current question index (removes .current-question class)
|
// reset current question index (removes .current-question class)
|
||||||
this.currentQuestionNum = 0; // reset count
|
this.currentQuestionNum = 0; // reset count
|
||||||
const answeredQuestion = this.questions.find(
|
const answeredQuestion = this.questions.find(
|
||||||
(q) => q.questionSequence === questionNum
|
(q) => this.getQuestionSequence(q) === questionNum
|
||||||
);
|
);
|
||||||
|
|
||||||
// return an object with the part answer, all the answered questions, and the part index
|
// return an object with the part answer, all the answered questions, and the part index
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,9 @@
|
||||||
data-test="image-upload"
|
data-test="image-upload"
|
||||||
@change="imageChanged" />
|
@change="imageChanged" />
|
||||||
</label>
|
</label>
|
||||||
<loader
|
<span v-if="isImageProcessing" class="loading-icon">
|
||||||
v-show="isImageProcessing"
|
<loader loaderColor="blue" />
|
||||||
loaderColor="blue"
|
</span>
|
||||||
class="loading-icon"></loader>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
ref="buttonMain"
|
ref="buttonMain"
|
||||||
:buttonText="DonationAddButtonCopy"
|
:buttonText="DonationAddButtonCopy"
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
|
loaderPosition="center"
|
||||||
class="w-100 mb-2 custom-secondary"
|
class="w-100 mb-2 custom-secondary"
|
||||||
@click-event="handleDonationAction" />
|
@click-event="handleDonationAction" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
class="package-label"
|
class="package-label"
|
||||||
:class="[
|
:class="[
|
||||||
this.buttonLabelSubCopy ? 'has-subheader' : '',
|
this.buttonLabelSubCopy ? 'has-subheader' : '',
|
||||||
|
this.additionalButtonData?.packageDescription ? 'has-description' : '',
|
||||||
this.additionalButtonData.isInsuranceSelected ? 'is-insurance' : '',
|
this.additionalButtonData.isInsuranceSelected ? 'is-insurance' : '',
|
||||||
this.hasPackageDiscount() ? 'has-package-discount' : '',
|
this.hasPackageDiscount() ? 'has-package-discount' : '',
|
||||||
]"
|
]"
|
||||||
|
|
@ -13,6 +14,10 @@
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<span v-html="this.buttonLabel"></span>
|
<span v-html="this.buttonLabel"></span>
|
||||||
</p>
|
</p>
|
||||||
|
<p
|
||||||
|
class="package-description m-0"
|
||||||
|
v-if="this.additionalButtonData?.packageDescription"
|
||||||
|
v-html="this.additionalButtonData.packageDescription"></p>
|
||||||
<p
|
<p
|
||||||
class="sub-label m-0"
|
class="sub-label m-0"
|
||||||
v-if="this.buttonLabelSubCopy"
|
v-if="this.buttonLabelSubCopy"
|
||||||
|
|
@ -202,7 +207,7 @@ export default {
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
max-height: 126px;
|
max-height: 126px;
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
max-height: 500px;
|
max-height: 1000px;
|
||||||
padding-bottom: 4.75rem;
|
padding-bottom: 4.75rem;
|
||||||
}
|
}
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
|
|
@ -242,7 +247,8 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-subheader {
|
&.has-subheader,
|
||||||
|
&.has-description {
|
||||||
@media (max-width: 320px) {
|
@media (max-width: 320px) {
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
max-height: 160px;
|
max-height: 160px;
|
||||||
|
|
@ -255,16 +261,21 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-subheader {
|
&.has-subheader,
|
||||||
|
&.has-description {
|
||||||
& > .package-specs {
|
& > .package-specs {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Package container height with package discount AND "most popular" label
|
//Package container height with package discount AND "most popular" label
|
||||||
&.has-package-discount.has-subheader {
|
&.has-package-discount.has-subheader,
|
||||||
|
&.has-package-discount.has-description {
|
||||||
min-height: 170px;
|
min-height: 170px;
|
||||||
}
|
}
|
||||||
|
&.has-package-discount.has-subheader.has-description {
|
||||||
|
min-height: 190px;
|
||||||
|
}
|
||||||
//Package container height with package discount only
|
//Package container height with package discount only
|
||||||
&.has-package-discount {
|
&.has-package-discount {
|
||||||
min-height: 136px;
|
min-height: 136px;
|
||||||
|
|
@ -307,30 +318,28 @@ export default {
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
+ .package-label {
|
+ .package-label {
|
||||||
|
max-height: 1000px;
|
||||||
|
border: 1px solid $blue;
|
||||||
|
|
||||||
.package-specs {
|
.package-specs {
|
||||||
max-height: 1000px;
|
max-height: none;
|
||||||
}
|
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
.package-specs {
|
|
||||||
.hide-when-closed {
|
.hide-when-closed {
|
||||||
display: block;
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s ease 0.15s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
border: 1px solid $blue;
|
|
||||||
max-height: 500px;
|
|
||||||
.special-save-box {
|
.special-save-box {
|
||||||
background-color: $green-100;
|
background-color: $green-100;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
&:before {
|
&:before {
|
||||||
box-shadow: 0px 0px 0px 1px $blue;
|
box-shadow: 0px 0px 0px 1px $blue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
&:after {
|
&:after {
|
||||||
background: $blue;
|
background: $blue;
|
||||||
}
|
}
|
||||||
|
|
@ -356,8 +365,9 @@ export default {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: all 0.5s ease;
|
transition: all 0.5s ease;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
max-height: 500px;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
|
@ -378,6 +388,13 @@ export default {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
&.package-description {
|
||||||
|
color: $gray-550;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
&.service-package-discount {
|
&.service-package-discount {
|
||||||
|
|
@ -441,10 +458,17 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-when-closed {
|
.hide-when-closed {
|
||||||
display: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
display: flex;
|
max-height: none;
|
||||||
flex-direction: column;
|
opacity: 1;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
class="package-label pricing-by-day-pkg-lbl"
|
class="package-label pricing-by-day-pkg-lbl"
|
||||||
:class="[
|
:class="[
|
||||||
this.buttonLabelSubCopy ? 'has-subheader' : '',
|
this.buttonLabelSubCopy ? 'has-subheader' : '',
|
||||||
|
this.additionalButtonData?.packageDescription ? 'has-description' : '',
|
||||||
!this.additionalButtonData.hasDiscount ? 'adjust-top' : '',
|
!this.additionalButtonData.hasDiscount ? 'adjust-top' : '',
|
||||||
]"
|
]"
|
||||||
for="testradio">
|
for="testradio">
|
||||||
|
|
@ -13,6 +14,10 @@
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<span v-html="this.buttonLabel"></span>
|
<span v-html="this.buttonLabel"></span>
|
||||||
</p>
|
</p>
|
||||||
|
<p
|
||||||
|
class="package-description m-0"
|
||||||
|
v-if="this.additionalButtonData?.packageDescription"
|
||||||
|
v-html="this.additionalButtonData.packageDescription"></p>
|
||||||
<p
|
<p
|
||||||
class="sub-label m-0"
|
class="sub-label m-0"
|
||||||
v-if="this.buttonLabelSubCopy"
|
v-if="this.buttonLabelSubCopy"
|
||||||
|
|
@ -41,6 +46,10 @@
|
||||||
<span v-html="getDisplayPrice"></span>
|
<span v-html="getDisplayPrice"></span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p
|
||||||
|
class="package-description m-0"
|
||||||
|
v-if="this.additionalButtonData?.packageDescription"
|
||||||
|
v-html="this.additionalButtonData.packageDescription"></p>
|
||||||
<p
|
<p
|
||||||
class="sub-label m-0"
|
class="sub-label m-0"
|
||||||
v-if="this.buttonLabelSubCopy"
|
v-if="this.buttonLabelSubCopy"
|
||||||
|
|
@ -259,13 +268,18 @@ export default {
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
max-height: 126px;
|
max-height: 126px;
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
max-height: 500px;
|
max-height: 1000px;
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
}
|
}
|
||||||
transition: max-height 0.25s ease-in;
|
transition: max-height 0.25s ease-in;
|
||||||
|
|
||||||
&.has-subheader {
|
&.has-subheader,
|
||||||
|
&.has-description {
|
||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
|
|
||||||
|
& > .package-specs {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
|
@ -288,12 +302,6 @@ export default {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
min-width: 10px;
|
min-width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-subheader {
|
|
||||||
& > .package-specs {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
@ -309,30 +317,28 @@ export default {
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
+ .package-label {
|
+ .package-label {
|
||||||
|
max-height: 1000px;
|
||||||
|
border: 1px solid $blue;
|
||||||
|
|
||||||
.package-specs {
|
.package-specs {
|
||||||
max-height: 1000px;
|
max-height: none;
|
||||||
}
|
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
.package-specs {
|
|
||||||
.hide-when-closed {
|
.hide-when-closed {
|
||||||
display: block;
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s ease 0.15s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
border: 1px solid $blue;
|
|
||||||
max-height: 500px;
|
|
||||||
.special-save-box {
|
.special-save-box {
|
||||||
background-color: $green-200;
|
background-color: $green-200;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
&:before {
|
&:before {
|
||||||
box-shadow: 0px 0px 0px 1px $blue;
|
box-shadow: 0px 0px 0px 1px $blue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
&:after {
|
&:after {
|
||||||
background: $blue;
|
background: $blue;
|
||||||
}
|
}
|
||||||
|
|
@ -358,8 +364,9 @@ export default {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: all 0.5s ease;
|
transition: all 0.5s ease;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
max-height: 500px;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.pricing-by-day {
|
span.pricing-by-day {
|
||||||
|
|
@ -398,6 +405,13 @@ export default {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
&.package-description {
|
||||||
|
color: $gray-550;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
&.service-package-discount-pricing-by-day {
|
&.service-package-discount-pricing-by-day {
|
||||||
|
|
@ -458,10 +472,17 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-when-closed {
|
.hide-when-closed {
|
||||||
display: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
display: flex;
|
max-height: none;
|
||||||
flex-direction: column;
|
opacity: 1;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
isPrimary
|
isPrimary
|
||||||
:buttonText="overrideButtonText || buttonText"
|
:buttonText="overrideButtonText || buttonText"
|
||||||
loaderColor="white"
|
loaderColor="white"
|
||||||
|
loaderPosition="center"
|
||||||
:class="isForwardActionDisabled && 'form-test-invalid'"
|
:class="isForwardActionDisabled && 'form-test-invalid'"
|
||||||
:aria-disabled="isForwardActionDisabled"
|
:aria-disabled="isForwardActionDisabled"
|
||||||
:isDisabled="isForwardActionDisabled"
|
:isDisabled="isForwardActionDisabled"
|
||||||
|
|
@ -68,10 +69,6 @@ export default {
|
||||||
customButtontext: "",
|
customButtontext: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
unmounted() {
|
|
||||||
document.onkeydown = null;
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
backLink() {
|
backLink() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -91,15 +88,8 @@ export default {
|
||||||
},
|
},
|
||||||
removeLoader() {
|
removeLoader() {
|
||||||
this.$refs.buttonMain.removeLoader();
|
this.$refs.buttonMain.removeLoader();
|
||||||
document.onkeydown = function (e) {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
buttonClick() {
|
buttonClick() {
|
||||||
//prevent keyboard input after button click
|
|
||||||
document.onkeydown = function (e) {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
// check session expired and initSession to recreate cookies
|
// check session expired and initSession to recreate cookies
|
||||||
if (analyticsMixin.methods.sessionExpired()) {
|
if (analyticsMixin.methods.sessionExpired()) {
|
||||||
this.routeReturnUser();
|
this.routeReturnUser();
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,43 @@ describe("questionsPageLayout.vue", () => {
|
||||||
|
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Should return false when answerData was cleared even if answerSelected remains", async () => {
|
||||||
|
const { wrapper } = setupMocks({});
|
||||||
|
await wrapper.setProps({ index: 1 });
|
||||||
|
|
||||||
|
const testGlassPiece = {
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
questionSequence: 1,
|
||||||
|
questionText: "Is your Grand Cherokee the Laredo model?",
|
||||||
|
answers: [],
|
||||||
|
answerSelected: "1|answer|DD11132|Yes",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
answerData: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(wrapper.vm.showThisQuestionChain(testGlassPiece, 0)).toBe(false);
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should return false for reset chains ahead of the current index", async () => {
|
||||||
|
const { wrapper } = setupMocks({});
|
||||||
|
await wrapper.setProps({ index: 1 });
|
||||||
|
|
||||||
|
const testGlassPiece = {
|
||||||
|
questions: [
|
||||||
|
{ questionSequence: 1, questionText: "Rain sensing wipers?", answers: [] },
|
||||||
|
],
|
||||||
|
answerData: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(wrapper.vm.showThisQuestionChain(testGlassPiece, 2)).toBe(false);
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("method handleForwardButtonAction...", () => {
|
describe("method handleForwardButtonAction...", () => {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7 question-chain-wrapper">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7 question-chain-wrapper">
|
||||||
<div v-for="(questionsDatum, i) in questionsData" :key="questionsDatum.key">
|
<div
|
||||||
|
v-for="(questionsDatum, i) in questionsData"
|
||||||
|
:key="questionsDatum.key ?? questionsDatum.answerKey ?? i">
|
||||||
<questionChain
|
<questionChain
|
||||||
ref="questionChain"
|
ref="questionChain"
|
||||||
v-model="selectedAnswers[questionsDatum.answerKey]"
|
v-model="selectedAnswers[questionsDatum.answerKey]"
|
||||||
|
|
@ -18,10 +20,12 @@
|
||||||
:index="i"
|
:index="i"
|
||||||
v-if="showThisQuestionChain(questionsDatum, i)"
|
v-if="showThisQuestionChain(questionsDatum, i)"
|
||||||
:answerKey="questionsDatum.answerKey"
|
:answerKey="questionsDatum.answerKey"
|
||||||
|
:hasSavedAnswer="!!questionsDatum.answerData?.answerResult"
|
||||||
|
:hasDownstreamSavedAnswers="hasDownstreamSavedAnswers(i)"
|
||||||
:validationRules="validationRules" />
|
:validationRules="validationRules" />
|
||||||
<!-- Save your progress slot -->
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Save your progress slot (once per page, not per glass piece) -->
|
||||||
|
<slot></slot>
|
||||||
|
|
||||||
<navbar
|
<navbar
|
||||||
ref="navbar"
|
ref="navbar"
|
||||||
|
|
@ -60,12 +64,22 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
hasDownstreamSavedAnswers(glassIndex) {
|
||||||
|
return this.questionsData
|
||||||
|
?.slice(glassIndex + 1)
|
||||||
|
.some((glass) => glass.answerData?.answerResult);
|
||||||
|
},
|
||||||
showThisQuestionChain(glass, i) {
|
showThisQuestionChain(glass, i) {
|
||||||
// return false if no questions or if suppressed
|
|
||||||
if (!glass.questions || glass.questions?.length < 1 || glass.isSuppressedPart) {
|
if (!glass.questions || glass.questions?.length < 1 || glass.isSuppressedPart) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.index === i || glass.answerData?.answerResult?.length > 0;
|
|
||||||
|
if (this.index === i) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show completed chains that still have a saved answer
|
||||||
|
return !!glass.answerData?.answerResult;
|
||||||
},
|
},
|
||||||
handleForwardButtonAction() {
|
handleForwardButtonAction() {
|
||||||
this.$emit("forwardButtonAction");
|
this.$emit("forwardButtonAction");
|
||||||
|
|
|
||||||
|
|
@ -33,21 +33,15 @@ describe("nav-bar.vue", () => {
|
||||||
expect(wrapper.componentVM.customButtontext).toBe("newText");
|
expect(wrapper.componentVM.customButtontext).toBe("newText");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should run removeLoader fn on buttonMain and return false for onkeydown fn", async () => {
|
test("should run removeLoader on buttonMain", async () => {
|
||||||
// Arrange
|
|
||||||
const wrapper = mount(navbar, {
|
const wrapper = mount(navbar, {
|
||||||
mixins: [mockMixin],
|
mixins: [mockMixin],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.$refs.buttonMain.removeLoader = jest.fn();
|
wrapper.vm.$refs.buttonMain.removeLoader = jest.fn();
|
||||||
wrapper.vm.removeLoader();
|
wrapper.vm.removeLoader();
|
||||||
const spy = jest.spyOn(document, "onkeydown");
|
|
||||||
document.onkeydown();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.vm.$refs.buttonMain.removeLoader).toHaveBeenCalled();
|
expect(wrapper.vm.$refs.buttonMain.removeLoader).toHaveBeenCalled();
|
||||||
expect(spy).toReturnWith(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
isPrimary
|
isPrimary
|
||||||
:buttonText="overrideButtonText || buttonText"
|
:buttonText="overrideButtonText || buttonText"
|
||||||
loaderColor="white"
|
loaderColor="white"
|
||||||
|
loaderPosition="center"
|
||||||
:class="isForwardActionDisabled && 'form-test-invalid'"
|
:class="isForwardActionDisabled && 'form-test-invalid'"
|
||||||
:aria-disabled="isForwardActionDisabled"
|
:aria-disabled="isForwardActionDisabled"
|
||||||
:isDisabled="isForwardActionDisabled"
|
:isDisabled="isForwardActionDisabled"
|
||||||
|
|
@ -69,9 +70,6 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
unmounted() {
|
|
||||||
document.onkeydown = null;
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
backLink() {
|
backLink() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -91,15 +89,8 @@ export default {
|
||||||
},
|
},
|
||||||
removeLoader() {
|
removeLoader() {
|
||||||
this.$refs.buttonMain.removeLoader();
|
this.$refs.buttonMain.removeLoader();
|
||||||
document.onkeydown = function (e) {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
buttonClick() {
|
buttonClick() {
|
||||||
//prevent keyboard input after button click
|
|
||||||
document.onkeydown = function (e) {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
// check session expired and initSession to recreate cookies
|
// check session expired and initSession to recreate cookies
|
||||||
if (analyticsMixin.methods.sessionExpired()) {
|
if (analyticsMixin.methods.sessionExpired()) {
|
||||||
this.routeReturnUser();
|
this.routeReturnUser();
|
||||||
|
|
|
||||||
|
|
@ -159,13 +159,37 @@ describe("save-progress-popup-question ", () => {
|
||||||
wrapper.vm.selectContactMethod("PhoneAnswer");
|
wrapper.vm.selectContactMethod("PhoneAnswer");
|
||||||
wrapper.vm.modal.validate = jest.fn().mockResolvedValue({ valid: true });
|
wrapper.vm.modal.validate = jest.fn().mockResolvedValue({ valid: true });
|
||||||
const resetSpy = jest.spyOn(wrapper.vm, "resetPhoneSendButtonStyle");
|
const resetSpy = jest.spyOn(wrapper.vm, "resetPhoneSendButtonStyle");
|
||||||
|
const scrollSpy = jest
|
||||||
|
.spyOn(wrapper.vm, "scrollConsentIntoView")
|
||||||
|
.mockResolvedValue(undefined);
|
||||||
|
|
||||||
await wrapper.vm.validatePhoneAndSave();
|
await wrapper.vm.validatePhoneAndSave();
|
||||||
|
|
||||||
expect(wrapper.vm.showConsentErrors).toBe(true);
|
expect(wrapper.vm.showConsentErrors).toBe(true);
|
||||||
|
expect(scrollSpy).toHaveBeenCalled();
|
||||||
expect(resetSpy).toHaveBeenCalled();
|
expect(resetSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should scroll consent checkboxes into view", async () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mockScrollIntoView = jest.fn();
|
||||||
|
const consentEl = document.createElement("fieldset");
|
||||||
|
consentEl.className = "save-progress-popup-sms-consent";
|
||||||
|
consentEl.scrollIntoView = mockScrollIntoView;
|
||||||
|
wrapper.element.appendChild(consentEl);
|
||||||
|
|
||||||
|
await wrapper.vm.scrollConsentIntoView();
|
||||||
|
|
||||||
|
expect(mockScrollIntoView).toHaveBeenCalledWith({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("should reset the send button loader when phone validation fails", async () => {
|
test("should reset the send button loader when phone validation fails", async () => {
|
||||||
const { wrapper } = setupMocks({
|
const { wrapper } = setupMocks({
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -209,6 +233,66 @@ describe("save-progress-popup-question ", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("footer button suppression", () => {
|
||||||
|
test("should suppress the modal footer button on the phone tab", async () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.vm.selectContactMethod("PhoneAnswer");
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.isFooterButtonSuppressed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show the modal footer button on the email tab before save", () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.vm.isFooterButtonSuppressed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should suppress the modal footer button after a successful email save", async () => {
|
||||||
|
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
pageName: "quote",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
|
||||||
|
wrapper.vm.userInput = "test@example.com";
|
||||||
|
|
||||||
|
await wrapper.vm.saveProgress();
|
||||||
|
|
||||||
|
expect(wrapper.vm.isFooterButtonSuppressed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reset the modal footer button loader before suppressing it after save", async () => {
|
||||||
|
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
pageName: "quote",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
|
||||||
|
wrapper.vm.userInput = "test@example.com";
|
||||||
|
wrapper.vm.modal.resetButtonStyle = jest.fn();
|
||||||
|
|
||||||
|
await wrapper.vm.saveProgress();
|
||||||
|
|
||||||
|
expect(wrapper.vm.modal.resetButtonStyle).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("saveProgress", () => {
|
describe("saveProgress", () => {
|
||||||
test("should save phone number and sms consent when phone tab is selected", async () => {
|
test("should save phone number and sms consent when phone tab is selected", async () => {
|
||||||
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
:onModalClosedCallback="onModalClosed"
|
:onModalClosedCallback="onModalClosed"
|
||||||
:footerButtonText="modalButtonText"
|
:footerButtonText="modalButtonText"
|
||||||
:isFooterButtonPrimary="true"
|
:isFooterButtonPrimary="true"
|
||||||
:isFooterButtonSuppressed="isPhoneTabSelected && !isProgressSaved"
|
:isFooterButtonSuppressed="isFooterButtonSuppressed"
|
||||||
@footer-button-event="saveProgress">
|
@footer-button-event="saveProgress">
|
||||||
<p class="modal-body-inner">{{ modalBodyText }}</p>
|
<p class="modal-body-inner">{{ modalBodyText }}</p>
|
||||||
<fieldset class="save-progress-popup-question__tabs">
|
<fieldset class="save-progress-popup-question__tabs">
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
isRequired
|
isRequired
|
||||||
validationRules="phone-number-required" />
|
validationRules="phone-number-required" />
|
||||||
<saveProgressPopupSmsConsentQuestion
|
<saveProgressPopupSmsConsentQuestion
|
||||||
|
ref="smsConsentQuestion"
|
||||||
v-model="smsConsent"
|
v-model="smsConsent"
|
||||||
:consentCopy="smsConsentCopy"
|
:consentCopy="smsConsentCopy"
|
||||||
:isDisabled="isProgressSaved"
|
:isDisabled="isProgressSaved"
|
||||||
|
|
@ -70,9 +71,14 @@
|
||||||
ref="phoneSendButton"
|
ref="phoneSendButton"
|
||||||
:isPrimary="true"
|
:isPrimary="true"
|
||||||
class="w-100 modal-footer-button"
|
class="w-100 modal-footer-button"
|
||||||
|
loaderColor="white"
|
||||||
|
loaderPosition="center"
|
||||||
:buttonText="modalButtonText"
|
:buttonText="modalButtonText"
|
||||||
@click-event="validatePhoneAndSave" />
|
@click-event="validatePhoneAndSave" />
|
||||||
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
|
<p
|
||||||
|
v-if="!isProgressSaved"
|
||||||
|
class="modal-disclaimer"
|
||||||
|
v-html="modalDisclaimerText"></p>
|
||||||
</template>
|
</template>
|
||||||
<alert
|
<alert
|
||||||
class="my-4"
|
class="my-4"
|
||||||
|
|
@ -192,6 +198,9 @@ export default {
|
||||||
isPhoneTabSelected() {
|
isPhoneTabSelected() {
|
||||||
return this.contactMethod === saveProgressPopupContactMethods.PHONE;
|
return this.contactMethod === saveProgressPopupContactMethods.PHONE;
|
||||||
},
|
},
|
||||||
|
isFooterButtonSuppressed() {
|
||||||
|
return this.isProgressSaved || this.isPhoneTabSelected;
|
||||||
|
},
|
||||||
phoneQuestionWidgetName() {
|
phoneQuestionWidgetName() {
|
||||||
return PHONE_QUESTION_WIDGET;
|
return PHONE_QUESTION_WIDGET;
|
||||||
},
|
},
|
||||||
|
|
@ -240,6 +249,7 @@ export default {
|
||||||
!hasSaveProgressSmsConsentSelection(this.smsConsent, this.showConsentManagement)
|
!hasSaveProgressSmsConsentSelection(this.smsConsent, this.showConsentManagement)
|
||||||
) {
|
) {
|
||||||
this.showConsentErrors = true;
|
this.showConsentErrors = true;
|
||||||
|
await this.scrollConsentIntoView();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,6 +262,14 @@ export default {
|
||||||
resetPhoneSendButtonStyle() {
|
resetPhoneSendButtonStyle() {
|
||||||
this.$refs.phoneSendButton?.resetButtonStyle();
|
this.$refs.phoneSendButton?.resetButtonStyle();
|
||||||
},
|
},
|
||||||
|
async scrollConsentIntoView() {
|
||||||
|
await this.$nextTick();
|
||||||
|
const consentEl =
|
||||||
|
this.$refs.smsConsentQuestion?.$el ??
|
||||||
|
this.$el?.querySelector(".save-progress-popup-sms-consent");
|
||||||
|
|
||||||
|
consentEl?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||||
|
},
|
||||||
async saveProgress() {
|
async saveProgress() {
|
||||||
await saveProgressPopupContactToStore(this.dispatchStoreAction, {
|
await saveProgressPopupContactToStore(this.dispatchStoreAction, {
|
||||||
contactMethod: this.contactMethod,
|
contactMethod: this.contactMethod,
|
||||||
|
|
@ -262,6 +280,9 @@ export default {
|
||||||
// send store call to send to new API (that triggers an email/SMS send)
|
// send store call to send to new API (that triggers an email/SMS send)
|
||||||
await saveQuote({ pageNameToLog: this.pageName });
|
await saveQuote({ pageNameToLog: this.pageName });
|
||||||
|
|
||||||
|
this.modal?.resetButtonStyle();
|
||||||
|
this.resetPhoneSendButtonStyle();
|
||||||
|
|
||||||
// hide save progress button; show success message alert
|
// hide save progress button; show success message alert
|
||||||
this.savedContactMethod = this.contactMethod;
|
this.savedContactMethod = this.contactMethod;
|
||||||
this.isProgressSaved = true;
|
this.isProgressSaved = true;
|
||||||
|
|
@ -372,6 +393,11 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modal.modal-component .modal-dialog .modal-content {
|
.modal.modal-component .modal-dialog .modal-content {
|
||||||
|
max-height: calc(100dvh - 1rem);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
p.modal-body {
|
p.modal-body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -379,6 +405,9 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
.textbox-question {
|
.textbox-question {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -499,6 +528,8 @@ export default {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
order: 3;
|
order: 3;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
max-height: calc(100dvh - 30rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
@ -508,10 +539,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.progress-saved {
|
&.progress-saved {
|
||||||
.modal-footer-button,
|
|
||||||
.modal-disclaimer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.skip-button {
|
.skip-button {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { storeActions } from "@/constants/store-actions";
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
import { deepClone } from "@/helpers/object-helper";
|
import { deepClone } from "@/helpers/object-helper";
|
||||||
import { partNumberStrings } from "@/constants/part-number-strings";
|
import { partNumberStrings } from "@/constants/part-number-strings";
|
||||||
|
import { applicationConfig } from "@/constants/application-config";
|
||||||
|
import { queryStrings } from "@/constants/query-strings";
|
||||||
|
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||||
|
import { peekQueryFromStash } from "@/router/methods/helpers/querystring-stash";
|
||||||
|
|
||||||
export function getDisplayAmountDue(lineItemsObject, includeTax = true) {
|
export function getDisplayAmountDue(lineItemsObject, includeTax = true) {
|
||||||
return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", {
|
return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", {
|
||||||
|
|
@ -56,6 +60,35 @@ export function getAmountDue(lineItemsObject, includeTax = true) {
|
||||||
return ((amountDue * 100) / 100).toFixed(2);
|
return ((amountDue * 100) / 100).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPiaAuthAmountAdjustment() {
|
||||||
|
if (applicationConfig.CURRENT_ENVIRONMENT === "Prod") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustParam =
|
||||||
|
peekQueryFromStash(queryStrings.PIA_AUTH_AMOUNT_ADJUST) ??
|
||||||
|
getQuerystringParameter(queryStrings.PIA_AUTH_AMOUNT_ADJUST);
|
||||||
|
|
||||||
|
if (adjustParam == null || adjustParam === "") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustment = parseFloat(adjustParam);
|
||||||
|
return Number.isFinite(adjustment) ? adjustment : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPiaAuthorizationAmount(lineItemsObject) {
|
||||||
|
const amountDue = parseFloat(getAmountDue(lineItemsObject));
|
||||||
|
const adjustment = getPiaAuthAmountAdjustment();
|
||||||
|
|
||||||
|
if (adjustment === 0) {
|
||||||
|
return amountDue.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustedAmount = Math.max(0.01, amountDue + adjustment);
|
||||||
|
return adjustedAmount.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
export function getSubTotal(lineItemsObject) {
|
export function getSubTotal(lineItemsObject) {
|
||||||
// this is amountDue without sales tax
|
// this is amountDue without sales tax
|
||||||
return getAmountDue(lineItemsObject, false);
|
return getAmountDue(lineItemsObject, false);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
import {
|
import {
|
||||||
getDisplayAmountDue,
|
getDisplayAmountDue,
|
||||||
getAmountDue,
|
getAmountDue,
|
||||||
|
getPiaAuthorizationAmount,
|
||||||
getSubTotal,
|
getSubTotal,
|
||||||
getSalesTax,
|
getSalesTax,
|
||||||
} from "@/helpers/pricing-helper.js";
|
} from "@/helpers/pricing-helper.js";
|
||||||
|
import { applicationConfig } from "@/constants/application-config";
|
||||||
|
|
||||||
|
jest.mock("@/helpers/querystring-helper", () => ({
|
||||||
|
getQuerystringParameter: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/router/methods/helpers/querystring-stash", () => ({
|
||||||
|
peekQueryFromStash: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||||
|
import { peekQueryFromStash } from "@/router/methods/helpers/querystring-stash";
|
||||||
|
|
||||||
const lineItems = {
|
const lineItems = {
|
||||||
glassParts: [],
|
glassParts: [],
|
||||||
|
|
@ -61,4 +74,30 @@ describe("pricing-helper", () => {
|
||||||
expect(result).toBe("3.00");
|
expect(result).toBe("3.00");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getPiaAuthorizationAmount", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
peekQueryFromStash.mockReturnValue(null);
|
||||||
|
getQuerystringParameter.mockReturnValue(null);
|
||||||
|
applicationConfig.CURRENT_ENVIRONMENT = "QA";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns amount due when no adjustment is configured", () => {
|
||||||
|
expect(getPiaAuthorizationAmount(lineItems)).toBe("53.00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores adjustment in production", () => {
|
||||||
|
applicationConfig.CURRENT_ENVIRONMENT = "Prod";
|
||||||
|
peekQueryFromStash.mockReturnValue("-5");
|
||||||
|
|
||||||
|
expect(getPiaAuthorizationAmount(lineItems)).toBe("53.00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies non-production query string adjustment", () => {
|
||||||
|
peekQueryFromStash.mockReturnValue("-5");
|
||||||
|
|
||||||
|
expect(getPiaAuthorizationAmount(lineItems)).toBe("48.00");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
59
src/helpers/question-chain-helper.js
Normal file
59
src/helpers/question-chain-helper.js
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* Builds the pipe-delimited value used by question-chain button options.
|
||||||
|
* Format: questionNumber|type|answer value|answer text
|
||||||
|
*/
|
||||||
|
export function buildQuestionChainAnswerValue({
|
||||||
|
questionSequence,
|
||||||
|
nextQuestionSequence,
|
||||||
|
answerResult,
|
||||||
|
answerText,
|
||||||
|
}) {
|
||||||
|
if (nextQuestionSequence) {
|
||||||
|
return `${questionSequence}|nextQuestion|${nextQuestionSequence}|${answerText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${questionSequence}|answer|${answerResult}|${answerText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapQuestionAnswersForChain(question) {
|
||||||
|
return question.answers.map((answer) => ({
|
||||||
|
buttonLabel: answer.answerText,
|
||||||
|
value: buildQuestionChainAnswerValue({
|
||||||
|
questionSequence: question.questionSequence,
|
||||||
|
nextQuestionSequence: answer.nextQuestionSequence,
|
||||||
|
answerResult: answer.answerResult,
|
||||||
|
answerText: answer.answerText,
|
||||||
|
}),
|
||||||
|
nextQuestionSequence: answer.nextQuestionSequence,
|
||||||
|
answerResult: answer.answerResult,
|
||||||
|
problemQuestionId: answer.problemQuestionId,
|
||||||
|
questionSequence: question.questionSequence,
|
||||||
|
questionType: answer.nextQuestionSequence ? "nextQuestion" : "answer",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTerminalQuestionChainAnswer(answerSelected) {
|
||||||
|
if (!answerSelected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, questionType, answerResult] = answerSelected.split("|");
|
||||||
|
|
||||||
|
return questionType?.toLowerCase() === "answer" && !!answerResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAnswerSelectedValue(answerSelected, answers) {
|
||||||
|
if (!answerSelected) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (answers.some((answer) => answer.value === answerSelected)) {
|
||||||
|
return answerSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedAnswer = answers.find(
|
||||||
|
(answer) => answer.value.toUpperCase() === String(answerSelected).toUpperCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
return matchedAnswer?.value ?? answerSelected;
|
||||||
|
}
|
||||||
90
src/helpers/question-chain-helper.spec.js
Normal file
90
src/helpers/question-chain-helper.spec.js
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import {
|
||||||
|
buildQuestionChainAnswerValue,
|
||||||
|
isTerminalQuestionChainAnswer,
|
||||||
|
mapQuestionAnswersForChain,
|
||||||
|
normalizeAnswerSelectedValue,
|
||||||
|
} from "./question-chain-helper";
|
||||||
|
|
||||||
|
describe("question-chain-helper", () => {
|
||||||
|
describe("buildQuestionChainAnswerValue", () => {
|
||||||
|
test("returns nextQuestion format when nextQuestionSequence is set", () => {
|
||||||
|
expect(
|
||||||
|
buildQuestionChainAnswerValue({
|
||||||
|
questionSequence: 1,
|
||||||
|
nextQuestionSequence: 3,
|
||||||
|
answerResult: "ignored",
|
||||||
|
answerText: "Yes",
|
||||||
|
})
|
||||||
|
).toBe("1|nextQuestion|3|Yes");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns answer format for final answers", () => {
|
||||||
|
expect(
|
||||||
|
buildQuestionChainAnswerValue({
|
||||||
|
questionSequence: 1,
|
||||||
|
nextQuestionSequence: null,
|
||||||
|
answerResult: "DW01705",
|
||||||
|
answerText: "Yes",
|
||||||
|
})
|
||||||
|
).toBe("1|answer|DW01705|Yes");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isTerminalQuestionChainAnswer", () => {
|
||||||
|
test("returns true when answer has a part number and no next question", () => {
|
||||||
|
expect(isTerminalQuestionChainAnswer("1|answer|FW04796|Yes")).toBe(true);
|
||||||
|
expect(isTerminalQuestionChainAnswer("3|answer|FW04793|No")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false when answer continues to the next question", () => {
|
||||||
|
expect(isTerminalQuestionChainAnswer("1|nextQuestion|2|No")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false when answer type is answer but part number is missing", () => {
|
||||||
|
expect(isTerminalQuestionChainAnswer("1|answer||No")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("normalizeAnswerSelectedValue", () => {
|
||||||
|
const answers = [{ value: "1|answer|DW01705|Yes" }, { value: "1|answer|DW01591|No" }];
|
||||||
|
|
||||||
|
test("returns empty string when answerSelected is empty", () => {
|
||||||
|
expect(normalizeAnswerSelectedValue("", answers)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns exact match when casing already matches", () => {
|
||||||
|
expect(normalizeAnswerSelectedValue("1|answer|DW01705|Yes", answers)).toBe(
|
||||||
|
"1|answer|DW01705|Yes"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizes uppercase legacy values to button value casing", () => {
|
||||||
|
expect(normalizeAnswerSelectedValue("1|ANSWER|DW01705|YES", answers)).toBe(
|
||||||
|
"1|answer|DW01705|Yes"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mapQuestionAnswersForChain", () => {
|
||||||
|
test("maps API answers to question-chain button answers", () => {
|
||||||
|
const result = mapQuestionAnswersForChain({
|
||||||
|
questionSequence: 1,
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
answerResult: "DW01705",
|
||||||
|
answerText: "Yes",
|
||||||
|
nextQuestionSequence: null,
|
||||||
|
problemQuestionId: 123,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
buttonLabel: "Yes",
|
||||||
|
value: "1|answer|DW01705|Yes",
|
||||||
|
problemQuestionId: 123,
|
||||||
|
questionType: "answer",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -16,6 +16,11 @@ const tierExperimentSettingKeys = {
|
||||||
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE,
|
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tierDescriptionExperimentSettingKeys = {
|
||||||
|
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO_DESCRIPTION,
|
||||||
|
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE_DESCRIPTION,
|
||||||
|
};
|
||||||
|
|
||||||
export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
|
export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
|
||||||
const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch);
|
const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch);
|
||||||
return !!partTypeMatches?.length;
|
return !!partTypeMatches?.length;
|
||||||
|
|
@ -284,16 +289,24 @@ export function isServicePackageNameTestActive(experimentSettingsMap) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExperimentPackageDisplayName(tierName, experimentSettingsMap) {
|
export function getExperimentPackageDisplayName(tierName, experimentSettingsMap) {
|
||||||
|
return getExperimentPackageSetting(tierName, tierExperimentSettingKeys, experimentSettingsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExperimentPackageDescription(tierName, experimentSettingsMap) {
|
||||||
|
return getExperimentPackageSetting(
|
||||||
|
tierName,
|
||||||
|
tierDescriptionExperimentSettingKeys,
|
||||||
|
experimentSettingsMap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExperimentPackageSetting(tierName, settingKeys, experimentSettingsMap) {
|
||||||
if (!isServicePackageNameTestActive(experimentSettingsMap)) {
|
if (!isServicePackageNameTestActive(experimentSettingsMap)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingKey = tierExperimentSettingKeys[tierName];
|
const settingKey = settingKeys[tierName];
|
||||||
if (!settingKey) {
|
return settingKey ? experimentHelper.getSettingValue(settingKey, experimentSettingsMap) : null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return experimentHelper.getSettingValue(settingKey, experimentSettingsMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maxTier(tierA, tierB) {
|
function maxTier(tierA, tierB) {
|
||||||
|
|
|
||||||
|
|
@ -969,34 +969,29 @@ describe("service-package-helper.js", () => {
|
||||||
|
|
||||||
describe("getExperimentPackageDisplayName", () => {
|
describe("getExperimentPackageDisplayName", () => {
|
||||||
const { experimentSettings } = require("@/constants/experiments");
|
const { experimentSettings } = require("@/constants/experiments");
|
||||||
|
const activeExperimentSettingsMap = {
|
||||||
|
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "true",
|
||||||
|
[experimentSettings.TIER_ONE]: "Essential",
|
||||||
|
[experimentSettings.TIER_TWO]: "Plus",
|
||||||
|
[experimentSettings.TIER_THREE]: "Full Service",
|
||||||
|
[experimentSettings.TIER_TWO_DESCRIPTION]: "Most popular choice",
|
||||||
|
[experimentSettings.TIER_THREE_DESCRIPTION]: "Best value",
|
||||||
|
};
|
||||||
|
|
||||||
it("Returns null when the experiment is not active", () => {
|
it("Returns null when the experiment is not active", () => {
|
||||||
const result = servicePackageHelper.getExperimentPackageDisplayName(
|
expect(
|
||||||
packageNames.TIER_ONE,
|
servicePackageHelper.getExperimentPackageDisplayName(packageNames.TIER_ONE, {})
|
||||||
{}
|
).toBeNull();
|
||||||
);
|
expect(
|
||||||
|
servicePackageHelper.getExperimentPackageDescription(packageNames.TIER_TWO, {})
|
||||||
expect(result).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Returns null when the test flag is not true", () => {
|
it("Returns null when the test flag is not true", () => {
|
||||||
const result = servicePackageHelper.getExperimentPackageDisplayName(
|
|
||||||
packageNames.TIER_ONE,
|
|
||||||
{
|
|
||||||
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "false",
|
|
||||||
[experimentSettings.TIER_ONE]: "Essential",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns the experiment display name for each tier when active", () => {
|
|
||||||
const experimentSettingsMap = {
|
const experimentSettingsMap = {
|
||||||
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "true",
|
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "false",
|
||||||
[experimentSettings.TIER_ONE]: "Essential",
|
[experimentSettings.TIER_ONE]: "Essential",
|
||||||
[experimentSettings.TIER_TWO]: "Plus",
|
[experimentSettings.TIER_TWO_DESCRIPTION]: "Most popular choice",
|
||||||
[experimentSettings.TIER_THREE]: "Full Service",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -1004,28 +999,70 @@ describe("service-package-helper.js", () => {
|
||||||
packageNames.TIER_ONE,
|
packageNames.TIER_ONE,
|
||||||
experimentSettingsMap
|
experimentSettingsMap
|
||||||
)
|
)
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
servicePackageHelper.getExperimentPackageDescription(
|
||||||
|
packageNames.TIER_TWO,
|
||||||
|
experimentSettingsMap
|
||||||
|
)
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns the experiment display name for each tier when active", () => {
|
||||||
|
expect(
|
||||||
|
servicePackageHelper.getExperimentPackageDisplayName(
|
||||||
|
packageNames.TIER_ONE,
|
||||||
|
activeExperimentSettingsMap
|
||||||
|
)
|
||||||
).toBe("Essential");
|
).toBe("Essential");
|
||||||
expect(
|
expect(
|
||||||
servicePackageHelper.getExperimentPackageDisplayName(
|
servicePackageHelper.getExperimentPackageDisplayName(
|
||||||
packageNames.TIER_TWO,
|
packageNames.TIER_TWO,
|
||||||
experimentSettingsMap
|
activeExperimentSettingsMap
|
||||||
)
|
)
|
||||||
).toBe("Plus");
|
).toBe("Plus");
|
||||||
expect(
|
expect(
|
||||||
servicePackageHelper.getExperimentPackageDisplayName(
|
servicePackageHelper.getExperimentPackageDisplayName(
|
||||||
packageNames.TIER_THREE,
|
packageNames.TIER_THREE,
|
||||||
experimentSettingsMap
|
activeExperimentSettingsMap
|
||||||
)
|
)
|
||||||
).toBe("Full Service");
|
).toBe("Full Service");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Returns null for an unknown tier", () => {
|
it("Returns the experiment description for tier two and three when active", () => {
|
||||||
const result = servicePackageHelper.getExperimentPackageDisplayName("UnknownTier", {
|
expect(
|
||||||
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "true",
|
servicePackageHelper.getExperimentPackageDescription(
|
||||||
[experimentSettings.TIER_ONE]: "Essential",
|
packageNames.TIER_TWO,
|
||||||
});
|
activeExperimentSettingsMap
|
||||||
|
)
|
||||||
|
).toBe("Most popular choice");
|
||||||
|
expect(
|
||||||
|
servicePackageHelper.getExperimentPackageDescription(
|
||||||
|
packageNames.TIER_THREE,
|
||||||
|
activeExperimentSettingsMap
|
||||||
|
)
|
||||||
|
).toBe("Best value");
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toBeNull();
|
it("Returns null for unknown tiers and tiers without configured settings", () => {
|
||||||
|
expect(
|
||||||
|
servicePackageHelper.getExperimentPackageDisplayName("UnknownTier", {
|
||||||
|
[experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST]: "true",
|
||||||
|
[experimentSettings.TIER_ONE]: "Essential",
|
||||||
|
})
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
servicePackageHelper.getExperimentPackageDescription(
|
||||||
|
packageNames.TIER_ONE,
|
||||||
|
activeExperimentSettingsMap
|
||||||
|
)
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
servicePackageHelper.getExperimentPackageDescription(
|
||||||
|
"UnknownTier",
|
||||||
|
activeExperimentSettingsMap
|
||||||
|
)
|
||||||
|
).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ describe("capabilityQuestions.vue", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("forwardButtonAction", () => {
|
describe("forwardButtonAction", () => {
|
||||||
test("Should clear out answerData", () => {
|
test("Should clear out answerData", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const { wrapper } = setupMocks({});
|
const { wrapper } = setupMocks({});
|
||||||
|
|
||||||
|
|
@ -245,7 +245,7 @@ describe("capabilityQuestions.vue", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
wrapper.vm.forwardButtonAction();
|
await wrapper.vm.forwardButtonAction();
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
|
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
|
||||||
|
|
|
||||||
|
|
@ -180,11 +180,6 @@ export default {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// clear out answerData for future page loads; must occur prior to store save
|
|
||||||
this.questionsData.forEach((glass) => {
|
|
||||||
glass.answerData = {};
|
|
||||||
});
|
|
||||||
|
|
||||||
// save to vuex store as order.damage.capabilityQuestionAnswers (array)
|
// save to vuex store as order.damage.capabilityQuestionAnswers (array)
|
||||||
// used in GET_PART_FROM_CAPABILITY_QUESTION_ANSWER call following this one
|
// used in GET_PART_FROM_CAPABILITY_QUESTION_ANSWER call following this one
|
||||||
await this.dispatchStoreAction(
|
await this.dispatchStoreAction(
|
||||||
|
|
@ -211,6 +206,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.navigateForward(partsOrQuestions);
|
this.navigateForward(partsOrQuestions);
|
||||||
|
|
||||||
|
this.clearQuestionsAnswerData();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@ describe("moldingQuestions.vue", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("forwardButtonAction", () => {
|
describe("forwardButtonAction", () => {
|
||||||
test("Should clear out answerData", () => {
|
test("Should clear out answerData", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const { wrapper } = setupMocks({});
|
const { wrapper } = setupMocks({});
|
||||||
|
|
||||||
|
|
@ -240,7 +240,7 @@ describe("moldingQuestions.vue", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
wrapper.vm.forwardButtonAction();
|
await wrapper.vm.forwardButtonAction();
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
|
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
|
||||||
|
|
|
||||||
|
|
@ -181,11 +181,6 @@ export default {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// clear out answerData for future page loads; must occur prior to store save
|
|
||||||
this.questionsData.forEach((glass) => {
|
|
||||||
glass.answerData = {};
|
|
||||||
});
|
|
||||||
|
|
||||||
// save to vuex store as order.damage.moldingQuestionArrays (array)
|
// save to vuex store as order.damage.moldingQuestionArrays (array)
|
||||||
await this.dispatchStoreAction(
|
await this.dispatchStoreAction(
|
||||||
this.storeActions.SAVE_MOLDING_QUESTION_ANSWERS,
|
this.storeActions.SAVE_MOLDING_QUESTION_ANSWERS,
|
||||||
|
|
@ -209,6 +204,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.navigateForward(partsOrQuestions);
|
this.navigateForward(partsOrQuestions);
|
||||||
|
|
||||||
|
this.clearQuestionsAnswerData();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -258,9 +258,7 @@ describe("partQuestions.vue...", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
wrapper.vm.forwardButtonAction();
|
await wrapper.vm.forwardButtonAction();
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
|
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
|
||||||
|
|
|
||||||
|
|
@ -182,13 +182,6 @@ export default {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// clear out answerData for future page loads; must occur prior to store save
|
|
||||||
this.questionsData.forEach((glass) => {
|
|
||||||
if (glass.answerData) {
|
|
||||||
glass.answerData = {};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
|
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
|
||||||
|
|
||||||
// save to vuex store as order.damage.partQuestionAnswers (array)
|
// save to vuex store as order.damage.partQuestionAnswers (array)
|
||||||
|
|
@ -209,6 +202,8 @@ export default {
|
||||||
const glassPartsForStore = partsLookup.data.glassPieceParts;
|
const glassPartsForStore = partsLookup.data.glassPieceParts;
|
||||||
|
|
||||||
this.navigateForward(glassPartsForStore);
|
this.navigateForward(glassPartsForStore);
|
||||||
|
|
||||||
|
this.clearQuestionsAnswerData();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<cart
|
<cart
|
||||||
ref="cart"
|
ref="cart"
|
||||||
:damage="damageInfo"
|
:damage="damageInfo"
|
||||||
:availableVaps="[]"
|
:availableVaps="availableVaps"
|
||||||
:allowItemRemoval="false"
|
:allowItemRemoval="false"
|
||||||
v-model="lineItems"
|
v-model="lineItems"
|
||||||
servicePackageOptionsCmsName="ServicePackageTitle"
|
servicePackageOptionsCmsName="ServicePackageTitle"
|
||||||
|
|
@ -72,7 +72,7 @@ import { endpoints } from "../../constants/endpoints";
|
||||||
import { AppointmentTypeStrings } from "../../constants/schedule-constants";
|
import { AppointmentTypeStrings } from "../../constants/schedule-constants";
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import { getAmountDue } from "@/helpers/pricing-helper.js";
|
import { getAmountDue, getPiaAuthorizationAmount } from "@/helpers/pricing-helper.js";
|
||||||
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
|
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
|
||||||
import { createAdyenCheckout } from "@/helpers/adyen-helper";
|
import { createAdyenCheckout } from "@/helpers/adyen-helper";
|
||||||
import { Dropin } from "@adyen/adyen-web/auto";
|
import { Dropin } from "@adyen/adyen-web/auto";
|
||||||
|
|
@ -364,14 +364,22 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await baseMixin.methods.dispatchStoreAction(
|
await this.savePiaPaymentAmounts();
|
||||||
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
|
||||||
this.amountDue,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.saveAndSubmitWorkOrder();
|
await this.saveAndSubmitWorkOrder();
|
||||||
},
|
},
|
||||||
|
async savePiaPaymentAmounts() {
|
||||||
|
await baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
||||||
|
this.piaAuthorizationAmount,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.SAVE_AUTHORIZATION_AMOUNT,
|
||||||
|
this.piaAuthorizationAmount,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
},
|
||||||
async handleFailedPayment(result) {
|
async handleFailedPayment(result) {
|
||||||
console.log(`Result =`);
|
console.log(`Result =`);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
|
@ -555,8 +563,12 @@ export default {
|
||||||
return getAmountDue(this.$store.getters.order.lineItems);
|
return getAmountDue(this.$store.getters.order.lineItems);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
piaAuthorizationAmount() {
|
||||||
|
return getPiaAuthorizationAmount(this.$store.getters.order.lineItems);
|
||||||
|
},
|
||||||
|
|
||||||
adyenPriceTotal() {
|
adyenPriceTotal() {
|
||||||
return Math.round(this.amountDue * 100);
|
return Math.round(parseFloat(this.piaAuthorizationAmount) * 100);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Cart info
|
// Cart info
|
||||||
|
|
@ -598,6 +610,9 @@ export default {
|
||||||
lineItems() {
|
lineItems() {
|
||||||
return deepClone(this.$store.getters.lineItems);
|
return deepClone(this.$store.getters.lineItems);
|
||||||
},
|
},
|
||||||
|
availableVaps() {
|
||||||
|
return this.$store.getters.order.lineItems?.vaps ?? [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,7 @@ import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||||
import { Form } from "vee-validate";
|
import { Form } from "vee-validate";
|
||||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
import {
|
import { getPiaAuthorizationAmount } from "@/helpers/pricing-helper.js";
|
||||||
getDisplayAmountDue,
|
|
||||||
getAmountDue,
|
|
||||||
getSubTotal,
|
|
||||||
getSalesTax,
|
|
||||||
} from "@/helpers/pricing-helper.js";
|
|
||||||
// iframeResizer IS loaded into the page and necessary for the package to
|
// iframeResizer IS loaded into the page and necessary for the package to
|
||||||
// to auto scale the iFrame this page is loaded in
|
// to auto scale the iFrame this page is loaded in
|
||||||
// Do not remove despite showing as "unused" CASH-309
|
// Do not remove despite showing as "unused" CASH-309
|
||||||
|
|
@ -103,11 +98,7 @@ export default {
|
||||||
const payerId = getQuerystringParameter(queryStrings.PAYERID);
|
const payerId = getQuerystringParameter(queryStrings.PAYERID);
|
||||||
|
|
||||||
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PAYPAL_TOKEN, token, false);
|
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PAYPAL_TOKEN, token, false);
|
||||||
baseMixin.methods.dispatchStoreAction(
|
this.savePiaPaymentAmounts();
|
||||||
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
|
||||||
this.getAmountDue(),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.saveAndSubmitWorkOrder();
|
await this.saveAndSubmitWorkOrder();
|
||||||
},
|
},
|
||||||
|
|
@ -160,16 +151,22 @@ export default {
|
||||||
console.log(ccToken);
|
console.log(ccToken);
|
||||||
|
|
||||||
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false);
|
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false);
|
||||||
baseMixin.methods.dispatchStoreAction(
|
this.savePiaPaymentAmounts();
|
||||||
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
|
||||||
this.getAmountDue(),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await this.saveAndSubmitWorkOrder();
|
await this.saveAndSubmitWorkOrder();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getAmountDue() {
|
savePiaPaymentAmounts() {
|
||||||
return getAmountDue(store.getters.order.lineItems);
|
const authorizationAmount = getPiaAuthorizationAmount(store.getters.order.lineItems);
|
||||||
|
baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
||||||
|
authorizationAmount,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.SAVE_AUTHORIZATION_AMOUNT,
|
||||||
|
authorizationAmount,
|
||||||
|
false
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async saveAndSubmitWorkOrder() {
|
async saveAndSubmitWorkOrder() {
|
||||||
// Final work order submit after returning from PIA.
|
// Final work order submit after returning from PIA.
|
||||||
|
|
|
||||||
|
|
@ -87,12 +87,12 @@ describe("policy-info.vue", () => {
|
||||||
expect(wrapper.vm.displayClaimNumber).toBe(true);
|
expect(wrapper.vm.displayClaimNumber).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("is false when only unrelated flags (e.g. otherResponsibleParty, thirdPartyVehicle, injuries) are checked", async () => {
|
test("is false when only unrelated flags (e.g. thirdPartyVehicle, injuries) are checked", async () => {
|
||||||
const { wrapper } = setupMocks();
|
const { wrapper } = setupMocks();
|
||||||
wrapper.vm.morePolicyQuestions = {
|
wrapper.vm.morePolicyQuestions = {
|
||||||
additionalDamage: false,
|
additionalDamage: false,
|
||||||
rental: false,
|
rental: false,
|
||||||
otherResponsibleParty: true,
|
otherResponsibleParty: false,
|
||||||
thirdPartyVehicle: true,
|
thirdPartyVehicle: true,
|
||||||
injuries: true,
|
injuries: true,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,9 @@
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
cmsWidgetName="PhoneExtensionWidget"
|
cmsWidgetName="PhoneExtensionWidget"
|
||||||
v-model="phoneExtension"
|
v-model="phoneExtension"
|
||||||
inputId="phoneExtension" />
|
inputId="phoneExtension"
|
||||||
|
maxLength="5"
|
||||||
|
validationRules="phone-extension-format" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -321,6 +323,7 @@ defineRule("policy-number-format", (value) => {
|
||||||
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
|
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
|
||||||
defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT));
|
defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT));
|
||||||
defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED));
|
defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED));
|
||||||
|
defineRule("phone-extension-format", regex(/^\d{1,5}$/, errorMessages.PHONE_EXTENSION_FORMAT));
|
||||||
defineRule("date-of-loss-required", required(errorMessages.DATE_OF_LOSS_REQUIRED));
|
defineRule("date-of-loss-required", required(errorMessages.DATE_OF_LOSS_REQUIRED));
|
||||||
defineRule("damage-types-required", required(errorMessages.DAMAGE_TYPES_REQUIRED));
|
defineRule("damage-types-required", required(errorMessages.DAMAGE_TYPES_REQUIRED));
|
||||||
defineRule("state-required", required(errorMessages.STATE_REQUIRED));
|
defineRule("state-required", required(errorMessages.STATE_REQUIRED));
|
||||||
|
|
@ -493,9 +496,12 @@ export default {
|
||||||
const additionalDamageFlag =
|
const additionalDamageFlag =
|
||||||
morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.ADDITIONAL_DAMAGE];
|
morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.ADDITIONAL_DAMAGE];
|
||||||
const rentalFlag = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.RENTAL];
|
const rentalFlag = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.RENTAL];
|
||||||
|
const otherResponsibleParty =
|
||||||
|
morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.OTHER_RESPONSIBLE_PARTY];
|
||||||
return Boolean(
|
return Boolean(
|
||||||
this.morePolicyQuestions?.[additionalDamageFlag] ||
|
this.morePolicyQuestions?.[additionalDamageFlag] ||
|
||||||
this.morePolicyQuestions?.[rentalFlag]
|
this.morePolicyQuestions?.[rentalFlag] ||
|
||||||
|
this.morePolicyQuestions?.[otherResponsibleParty]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -566,10 +572,10 @@ export default {
|
||||||
return match ?? value;
|
return match ?? value;
|
||||||
},
|
},
|
||||||
getPolicyStateFromStore() {
|
getPolicyStateFromStore() {
|
||||||
return store.getters.order.customer.address.state;
|
return store.getters.order.damage.damageState;
|
||||||
},
|
},
|
||||||
getCityFromStore() {
|
getCityFromStore() {
|
||||||
return store.getters.order.customer.address.city;
|
return store.getters.order.damage.damageCity;
|
||||||
},
|
},
|
||||||
getDateOfBirthFromStore() {
|
getDateOfBirthFromStore() {
|
||||||
return store.getters.order.policy.dateOfBirth;
|
return store.getters.order.policy.dateOfBirth;
|
||||||
|
|
@ -620,8 +626,8 @@ export default {
|
||||||
zipCode: this.policyZip,
|
zipCode: this.policyZip,
|
||||||
phoneNumber: this.phoneNumber,
|
phoneNumber: this.phoneNumber,
|
||||||
phoneExtension: this.phoneExtension,
|
phoneExtension: this.phoneExtension,
|
||||||
state: this.policyState,
|
state: null,
|
||||||
city: this.city,
|
city: null,
|
||||||
morePolicyQuestions: this.morePolicyQuestions,
|
morePolicyQuestions: this.morePolicyQuestions,
|
||||||
lossLocation: this.lossLocation,
|
lossLocation: this.lossLocation,
|
||||||
lossTime: this.lossTime,
|
lossTime: this.lossTime,
|
||||||
|
|
@ -639,6 +645,8 @@ export default {
|
||||||
{
|
{
|
||||||
dateOfLoss: this.dateOfLoss,
|
dateOfLoss: this.dateOfLoss,
|
||||||
damageCause: this.damageCause,
|
damageCause: this.damageCause,
|
||||||
|
damageState: this.policyState,
|
||||||
|
damageCity: this.city,
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="afterpay-modal-banner" role="alert">
|
<div class="afterpay-modal-banner" role="alert">
|
||||||
|
<component
|
||||||
|
:is="'script'"
|
||||||
|
src="https://js.squarecdn.com/square-marketplace.js"
|
||||||
|
async></component>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
class="callout"
|
class="callout"
|
||||||
|
|
@ -17,52 +21,19 @@
|
||||||
<span v-else v-html="token"></span>
|
<span v-else v-html="token"></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="nbsp"> </span>
|
<span class="nbsp"> </span>
|
||||||
<textLink
|
<a
|
||||||
linkType="text"
|
href="#"
|
||||||
:text="bannerCta"
|
class="text-link"
|
||||||
href="javascript:void(0)"
|
data-afterpay-modal="en_US"
|
||||||
@click-event="openModal" />
|
@click.prevent="onLearnMoreClick">
|
||||||
|
{{ bannerCta }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<modal
|
|
||||||
:ref="modalName"
|
|
||||||
class="afterpay-modal"
|
|
||||||
:headerText="modalHeaderText"
|
|
||||||
:isFooterButtonSuppressed="true">
|
|
||||||
<template v-slot:modal-header-slot>
|
|
||||||
<span>{{ modalSubHeaderText }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-slot>
|
|
||||||
<span class="afterpay-sections">
|
|
||||||
<span
|
|
||||||
v-for="section in afterpaySectionsContent"
|
|
||||||
:key="section"
|
|
||||||
class="afterpay-section">
|
|
||||||
<span class="section-header">
|
|
||||||
<img v-if="section.AnswerImageUrl" :src="section.AnswerImageUrl" />
|
|
||||||
<h3 v-if="section.Text" v-html="section.Text"></h3>
|
|
||||||
</span>
|
|
||||||
<span v-html="section.content"></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-slot:modal-footer-slot>
|
|
||||||
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
|
|
||||||
</template>
|
|
||||||
</modal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper";
|
||||||
doesCopyContainTextLink,
|
|
||||||
splitCopyOnCMSPlaceHolder,
|
|
||||||
getRouterLinkRouteFromCopy,
|
|
||||||
getRouterLinkDisplayTextFromCopy,
|
|
||||||
getExternalLink,
|
|
||||||
splitCMSCopyOnBR,
|
|
||||||
} from "@/helpers/cms-content-helper";
|
|
||||||
import textLink from "@/ux-components/text-link/text-link";
|
|
||||||
import modal from "@/digital-components/modal/modal";
|
|
||||||
|
|
||||||
const INLINE_IMAGE_TOKEN = "custom:inlineImage";
|
const INLINE_IMAGE_TOKEN = "custom:inlineImage";
|
||||||
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
|
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
|
||||||
|
|
@ -82,25 +53,18 @@ export default {
|
||||||
name: "afterpay-banner",
|
name: "afterpay-banner",
|
||||||
props: {
|
props: {
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
modalWidgetName: String,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isInlineImageToken,
|
isInlineImageToken,
|
||||||
isAfterpayPriceToken,
|
isAfterpayPriceToken,
|
||||||
getInlineAltText,
|
getInlineAltText,
|
||||||
doesCopyContainTextLink,
|
onLearnMoreClick() {
|
||||||
splitCopyOnCMSPlaceHolder,
|
this.pushEventToGA(
|
||||||
getRouterLinkRouteFromCopy,
|
this.$route.query[this.queryStrings.FMG_PAGE],
|
||||||
getRouterLinkDisplayTextFromCopy,
|
this.GaActions.CLICKED,
|
||||||
getExternalLink,
|
this.bannerCta,
|
||||||
openModal() {
|
true
|
||||||
this.modal?.openModal();
|
);
|
||||||
},
|
|
||||||
closeModal() {
|
|
||||||
this.modal?.closeModal();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -119,31 +83,6 @@ export default {
|
||||||
bannerCta() {
|
bannerCta() {
|
||||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||||
},
|
},
|
||||||
modalHeaderText() {
|
|
||||||
return this.getCmsContent(this.modalWidgetName, "HeaderText");
|
|
||||||
},
|
|
||||||
modalDisclaimerText() {
|
|
||||||
return this.getCmsContent(this.modalWidgetName, "BodyText2");
|
|
||||||
},
|
|
||||||
modalName() {
|
|
||||||
return this.modalWidgetName;
|
|
||||||
},
|
|
||||||
modal() {
|
|
||||||
return this.$refs[this.modalName];
|
|
||||||
},
|
|
||||||
afterpaySectionsContent() {
|
|
||||||
const sections = this.getCmsContent("AfterpayModalSectionsWidget", "Answers");
|
|
||||||
if (!sections) return [];
|
|
||||||
sections.forEach((section) => {
|
|
||||||
const sectionContent = this.getCmsContent(section.Name, "BodyText");
|
|
||||||
section["content"] = sectionContent;
|
|
||||||
});
|
|
||||||
return sections;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
textLink,
|
|
||||||
modal,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -198,117 +137,9 @@ export default {
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.afterpay-modal {
|
img {
|
||||||
&.modal.modal-component {
|
transform: translateY(-0.1rem);
|
||||||
:deep(.modal-dialog) {
|
|
||||||
margin: 0 1rem;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 840px;
|
|
||||||
top: 3.4rem;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
transform: none;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
.modal-content {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
|
|
||||||
.modal-header.mt-6 {
|
|
||||||
background-color: $blue-100;
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 1.5rem 2rem 1.5rem 1.5rem;
|
|
||||||
font-family: UrbanistSemibold;
|
|
||||||
|
|
||||||
.modal-title.justify-content-center {
|
|
||||||
// OVERRIDE
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
.btn-close {
|
|
||||||
top: 1.25rem;
|
|
||||||
right: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.afterpay-section {
|
|
||||||
display: block;
|
|
||||||
border: 1px solid $gray-200;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
font-family: UrbanistBold;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0 0 0 0.5rem;
|
|
||||||
color: $black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
strong {
|
|
||||||
font-family: UrbanistSemibold;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 1.5rem;
|
|
||||||
|
|
||||||
.afterpay-sections {
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
display: flex;
|
|
||||||
column-gap: 1rem;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.afterpay-section {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
flex: 0 1 33%;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.modal-dialog-centered {
|
|
||||||
height: auto;
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.modal-footer) {
|
|
||||||
position: relative;
|
|
||||||
background: $gray-100;
|
|
||||||
|
|
||||||
.modal-disclaimer {
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
a {
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1.625;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
59
src/layouts/quote/promo-banner/promo-banner.vue
Normal file
59
src/layouts/quote/promo-banner/promo-banner.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div class="promo-banner" role="note">
|
||||||
|
<img class="promo-banner__icon" :src="promoTagIcon" alt="" width="31" height="36" />
|
||||||
|
<p class="promo-banner__copy mb-0" v-html="promoBannerBodyText"></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "promo-banner",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
promoTagIcon() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "Image");
|
||||||
|
},
|
||||||
|
promoBannerBodyText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.promo-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
background-color: $gray-100;
|
||||||
|
border-radius: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 31px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__copy {
|
||||||
|
color: $gray-650;
|
||||||
|
font-size: $font-size-14;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
:deep(strong) {
|
||||||
|
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,6 +5,14 @@
|
||||||
<div
|
<div
|
||||||
class="container quote position-relative"
|
class="container quote position-relative"
|
||||||
:class="[showSaveProgressPopup ? 'initial-popup-open' : '']">
|
:class="[showSaveProgressPopup ? 'initial-popup-open' : '']">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12">
|
||||||
|
<promoBanner
|
||||||
|
v-if="showPromoBanner"
|
||||||
|
cmsWidgetName="PromoBannerWidget"
|
||||||
|
class="mb-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
|
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
|
||||||
|
|
@ -47,8 +55,7 @@
|
||||||
<!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner -->
|
<!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner -->
|
||||||
<afterpayModalBanner
|
<afterpayModalBanner
|
||||||
v-if="showAfterpayBanner"
|
v-if="showAfterpayBanner"
|
||||||
cmsWidgetName="AfterpayBannerWidget"
|
cmsWidgetName="AfterpayBannerWidget" />
|
||||||
modalWidgetName="AfterpayModalWidget" />
|
|
||||||
|
|
||||||
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
|
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
|
||||||
<recalDisclaimer
|
<recalDisclaimer
|
||||||
|
|
@ -102,7 +109,12 @@
|
||||||
<textBlock
|
<textBlock
|
||||||
cmsWidgetName="quoteDisclaimer"
|
cmsWidgetName="quoteDisclaimer"
|
||||||
typeStyle="caption"
|
typeStyle="caption"
|
||||||
class="mb-5 quote-disclaimer text-left text-md-center" />
|
class="quote-disclaimer text-left text-md-center" />
|
||||||
|
<textBlock
|
||||||
|
v-if="showPromoBanner"
|
||||||
|
cmsWidgetName="PromoBannerDisclaimerWidget"
|
||||||
|
typeStyle="caption"
|
||||||
|
class="quote-disclaimer text-left text-md-center" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -134,6 +146,7 @@ import textBlock from "@/digital-components/text-block/text-block";
|
||||||
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||||
import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner";
|
import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner";
|
||||||
|
import promoBanner from "@/layouts/quote/promo-banner/promo-banner";
|
||||||
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
|
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
|
||||||
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
|
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
|
||||||
import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question";
|
import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question";
|
||||||
|
|
@ -684,6 +697,12 @@ export default {
|
||||||
showAfterpayBanner() {
|
showAfterpayBanner() {
|
||||||
return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration;
|
return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration;
|
||||||
},
|
},
|
||||||
|
showPromoBanner() {
|
||||||
|
return experimentMixin.methods.hasSettingEqualTo(
|
||||||
|
experimentSettings.SHOW_PROMO_BANNER,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
},
|
||||||
isRecalPriceRemove() {
|
isRecalPriceRemove() {
|
||||||
return (
|
return (
|
||||||
experimentMixin.methods
|
experimentMixin.methods
|
||||||
|
|
@ -1041,6 +1060,7 @@ export default {
|
||||||
contentGroupModal,
|
contentGroupModal,
|
||||||
loadingModal,
|
loadingModal,
|
||||||
afterpayModalBanner,
|
afterpayModalBanner,
|
||||||
|
promoBanner,
|
||||||
promoModalQuestion,
|
promoModalQuestion,
|
||||||
recalDisclaimer,
|
recalDisclaimer,
|
||||||
saveProgressModalQuestion,
|
saveProgressModalQuestion,
|
||||||
|
|
@ -1166,4 +1186,7 @@ export default {
|
||||||
:deep(.promo-modal-question a) {
|
:deep(.promo-modal-question a) {
|
||||||
@include responsive-font-size-md(0.875rem, 1rem);
|
@include responsive-font-size-md(0.875rem, 1rem);
|
||||||
}
|
}
|
||||||
|
.quote-disclaimer:last-child {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -856,6 +856,52 @@ describe("service-package-question.vue, matching business rules for package disp
|
||||||
|
|
||||||
getExperimentPackageLabelSpy.mockRestore();
|
getExperimentPackageLabelSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
it("should use experiment package descriptions when ServicePackageNameTest is active", () => {
|
||||||
|
const getExperimentPackageDescriptionSpy = jest
|
||||||
|
.spyOn(servicePackageQuestion.methods, "getExperimentPackageDescription")
|
||||||
|
.mockImplementation((tierName) => {
|
||||||
|
const experimentPackageDescriptions = {
|
||||||
|
[packageNames.TIER_TWO]: "Most popular choice",
|
||||||
|
[packageNames.TIER_THREE]: "Best value",
|
||||||
|
};
|
||||||
|
|
||||||
|
return experimentPackageDescriptions[tierName] ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
|
||||||
|
Object.assign(processedCmsContent, mockProcessedCmsContent[packageNameKey]);
|
||||||
|
mockProps.availableLineItems.push(driverFrontWiperLineItem);
|
||||||
|
mockProps.availableLineItems.push(passengerFrontWiperLineItem);
|
||||||
|
const wrapper = setupMocks({
|
||||||
|
mountOptionsMockData: {
|
||||||
|
store: {
|
||||||
|
getters: {
|
||||||
|
order: {
|
||||||
|
damage: {
|
||||||
|
isRepair: true,
|
||||||
|
glassToReplace: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hasAnyNonWindshieldGlassParts: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.vm.servicePackageAnswers[0].additionalButtonData.packageDescription
|
||||||
|
).toBeNull();
|
||||||
|
expect(wrapper.vm.servicePackageAnswers[1].additionalButtonData.packageDescription).toBe(
|
||||||
|
"Most popular choice"
|
||||||
|
);
|
||||||
|
expect(wrapper.vm.servicePackageAnswers[2].additionalButtonData.packageDescription).toBe(
|
||||||
|
"Best value"
|
||||||
|
);
|
||||||
|
expect(wrapper.vm.servicePackageAnswers[1].buttonLabelSubCopy).toBe("MOST POPULAR");
|
||||||
|
expect(wrapper.vm.servicePackageAnswers[2].buttonLabelSubCopy).toBe("");
|
||||||
|
|
||||||
|
getExperimentPackageDescriptionSpy.mockRestore();
|
||||||
|
});
|
||||||
it("should use CMS package names when ServicePackageNameTest is not active", () => {
|
it("should use CMS package names when ServicePackageNameTest is not active", () => {
|
||||||
const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
|
const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
|
||||||
Object.assign(processedCmsContent, mockProcessedCmsContent[packageNameKey]);
|
Object.assign(processedCmsContent, mockProcessedCmsContent[packageNameKey]);
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ export default {
|
||||||
? false
|
? false
|
||||||
: "Special: Save $" + this.getDiscountPrice(answer.Name),
|
: "Special: Save $" + this.getDiscountPrice(answer.Name),
|
||||||
isInsuranceSelected: this.isInsuranceSelected,
|
isInsuranceSelected: this.isInsuranceSelected,
|
||||||
|
packageDescription: this.getExperimentPackageDescription(answer.Name),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -265,6 +266,19 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getExperimentPackageLabel(tierName) {
|
getExperimentPackageLabel(tierName) {
|
||||||
|
return this.getExperimentPackageSetting(tierName, {
|
||||||
|
[packageNames.TIER_ONE]: experimentSettings.TIER_ONE,
|
||||||
|
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO,
|
||||||
|
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getExperimentPackageDescription(tierName) {
|
||||||
|
return this.getExperimentPackageSetting(tierName, {
|
||||||
|
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO_DESCRIPTION,
|
||||||
|
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE_DESCRIPTION,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getExperimentPackageSetting(tierName, tierSettingKeys) {
|
||||||
if (
|
if (
|
||||||
!experimentMixin.methods.hasSettingEqualTo(
|
!experimentMixin.methods.hasSettingEqualTo(
|
||||||
experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST,
|
experimentSettings.SHOW_SERVICE_PACKAGE_NAME_TEST,
|
||||||
|
|
@ -274,13 +288,8 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tierSettingKey = {
|
const settingKey = tierSettingKeys[tierName];
|
||||||
[packageNames.TIER_ONE]: experimentSettings.TIER_ONE,
|
return settingKey ? experimentMixin.methods.getSettingValue(settingKey) : null;
|
||||||
[packageNames.TIER_TWO]: experimentSettings.TIER_TWO,
|
|
||||||
[packageNames.TIER_THREE]: experimentSettings.TIER_THREE,
|
|
||||||
}[tierName];
|
|
||||||
|
|
||||||
return tierSettingKey ? experimentMixin.methods.getSettingValue(tierSettingKey) : null;
|
|
||||||
},
|
},
|
||||||
getHeaderTextFromCms(cmsWidgetName) {
|
getHeaderTextFromCms(cmsWidgetName) {
|
||||||
return this.getCmsContent(cmsWidgetName, "HeaderText");
|
return this.getCmsContent(cmsWidgetName, "HeaderText");
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
|
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
|
||||||
<div
|
<div
|
||||||
class="package-label"
|
class="package-label"
|
||||||
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
|
:class="[
|
||||||
|
this.buttonLabelSubCopy ? 'has-subheader' : '',
|
||||||
|
this.additionalButtonData?.packageDescription ? 'has-description' : '',
|
||||||
|
]"
|
||||||
for="testradio">
|
for="testradio">
|
||||||
<div class="package-specs">
|
<div class="package-specs">
|
||||||
<div v-if="this.additionalButtonData.hasDiscount" class="row">
|
<div v-if="this.additionalButtonData.hasDiscount" class="row">
|
||||||
|
|
@ -10,6 +13,10 @@
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<span v-html="this.buttonLabel"></span>
|
<span v-html="this.buttonLabel"></span>
|
||||||
</p>
|
</p>
|
||||||
|
<p
|
||||||
|
class="package-description m-0"
|
||||||
|
v-if="this.additionalButtonData?.packageDescription"
|
||||||
|
v-html="this.additionalButtonData.packageDescription"></p>
|
||||||
<p
|
<p
|
||||||
class="sub-label m-0"
|
class="sub-label m-0"
|
||||||
v-if="this.buttonLabelSubCopy"
|
v-if="this.buttonLabelSubCopy"
|
||||||
|
|
@ -34,6 +41,10 @@
|
||||||
<span v-html="this.buttonAuxillaryCopy"></span>
|
<span v-html="this.buttonAuxillaryCopy"></span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p
|
||||||
|
class="package-description m-0"
|
||||||
|
v-if="this.additionalButtonData?.packageDescription"
|
||||||
|
v-html="this.additionalButtonData.packageDescription"></p>
|
||||||
<p
|
<p
|
||||||
class="sub-label m-0"
|
class="sub-label m-0"
|
||||||
v-if="this.buttonLabelSubCopy"
|
v-if="this.buttonLabelSubCopy"
|
||||||
|
|
@ -185,12 +196,13 @@ export default {
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
max-height: 126px;
|
max-height: 126px;
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
max-height: 500px;
|
max-height: 1000px;
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
}
|
}
|
||||||
transition: max-height 0.25s ease-in;
|
transition: max-height 0.25s ease-in;
|
||||||
|
|
||||||
&.has-subheader {
|
&.has-subheader,
|
||||||
|
&.has-description {
|
||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,34 +243,33 @@ export default {
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
+ .package-label {
|
+ .package-label {
|
||||||
|
max-height: 1000px;
|
||||||
|
border: 1px solid $blue;
|
||||||
|
background-color: $blue-100;
|
||||||
|
|
||||||
.package-specs {
|
.package-specs {
|
||||||
max-height: 1000px;
|
max-height: none;
|
||||||
}
|
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
.package-specs {
|
|
||||||
.hide-when-closed {
|
.hide-when-closed {
|
||||||
display: block;
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s ease 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
p:first-child {
|
p:first-child {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
border: 1px solid $blue;
|
|
||||||
max-height: 500px;
|
|
||||||
background-color: $blue-100;
|
|
||||||
.special-save-box {
|
.special-save-box {
|
||||||
background-color: $green-200;
|
background-color: $green-200;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
&:before {
|
&:before {
|
||||||
box-shadow: 0px 0px 0px 1px $blue;
|
box-shadow: 0px 0px 0px 1px $blue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
+ .package-label {
|
|
||||||
&:after {
|
&:after {
|
||||||
background: $blue;
|
background: $blue;
|
||||||
}
|
}
|
||||||
|
|
@ -284,8 +295,9 @@ export default {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: all 0.5s ease;
|
transition: all 0.5s ease;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
max-height: 500px;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
|
@ -332,6 +344,13 @@ export default {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
&.package-description {
|
||||||
|
color: $gray-550;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
&.service-package-discount {
|
&.service-package-discount {
|
||||||
|
|
@ -393,10 +412,17 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-when-closed {
|
.hide-when-closed {
|
||||||
display: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
display: flex;
|
max-height: none;
|
||||||
flex-direction: column;
|
opacity: 1;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
</span>
|
</span>
|
||||||
<loader
|
<loader
|
||||||
v-if="isLoaderDisplayed"
|
v-if="isLoaderDisplayed"
|
||||||
:loaderPosition="left"
|
loaderPosition="left"
|
||||||
:allowPageInteraction="true" />
|
:allowPageInteraction="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,10 @@
|
||||||
:isRequired="false"
|
:isRequired="false"
|
||||||
:validationRules="''" />
|
:validationRules="''" />
|
||||||
</form>
|
</form>
|
||||||
<div v-show="isLoading" class="loader-wrapper">
|
<div v-if="isLoading" class="loader-wrapper">
|
||||||
<loader loaderColor="blue" loaderPosition="center" />
|
<loader loaderColor="blue" loaderPosition="center" />
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!isLoading">
|
<div v-if="!isLoading">
|
||||||
<alert
|
<alert
|
||||||
ref="alertInvalidZip"
|
ref="alertInvalidZip"
|
||||||
v-if="displayInvalidZipAlert"
|
v-if="displayInvalidZipAlert"
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,17 @@ import baseMixin from "@/mixins/base-mixin.js";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { getIsWindshieldOnly } from "@/helpers/damage-helper";
|
import { getIsWindshieldOnly } from "@/helpers/damage-helper";
|
||||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||||
|
import { buildQuestionChainAnswerValue } from "@/helpers/question-chain-helper";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
|
clearQuestionsAnswerData() {
|
||||||
|
this.questionsData?.forEach((glass) => {
|
||||||
|
if (glass.answerData) {
|
||||||
|
glass.answerData = {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
hasPartQuestions(partsOrQuestions) {
|
hasPartQuestions(partsOrQuestions) {
|
||||||
return partsOrQuestions?.some((pq) => pq.partQuestions?.length > 0);
|
return partsOrQuestions?.some((pq) => pq.partQuestions?.length > 0);
|
||||||
},
|
},
|
||||||
|
|
@ -114,11 +122,12 @@ export default {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
// set the answerString to use for answerSelected
|
// set the answerString to use for answerSelected
|
||||||
if (chosenAns.nextQuestionSequence) {
|
answerString = buildQuestionChainAnswerValue({
|
||||||
answerString = `${answeredQuestion.questionNum}|nextQuestion|${chosenAns.nextQuestionSequence}|${chosenAns.answerText}`;
|
questionSequence: answeredQuestion.questionNum,
|
||||||
} else {
|
nextQuestionSequence: chosenAns.nextQuestionSequence,
|
||||||
answerString = `${answeredQuestion.questionNum}|answer|${chosenAns.answerResult}|${chosenAns.answerText}`;
|
answerResult: chosenAns.answerResult,
|
||||||
}
|
answerText: chosenAns.answerText,
|
||||||
|
});
|
||||||
|
|
||||||
// mark this question as answered (question-chain will read this)
|
// mark this question as answered (question-chain will read this)
|
||||||
glass.questions[answeredQuestion.questionNum - 1].answerSelected =
|
glass.questions[answeredQuestion.questionNum - 1].answerSelected =
|
||||||
|
|
@ -177,6 +186,8 @@ export default {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const self = vm ?? this;
|
const self = vm ?? this;
|
||||||
|
const isIncomplete = !!answer.incomplete;
|
||||||
|
const hadSavedAnswer = !!self.questionsData[answer.index]?.answerData?.answerResult;
|
||||||
|
|
||||||
// clear out any preloaded answers
|
// clear out any preloaded answers
|
||||||
self.selectedAnswers = {};
|
self.selectedAnswers = {};
|
||||||
|
|
@ -193,7 +204,6 @@ export default {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const answeredQuestionText = answeredQuestion.questionText.toUpperCase();
|
const answeredQuestionText = answeredQuestion.questionText.toUpperCase();
|
||||||
const answeredQuestionAnswer = answeredQuestion.selectedAnswer.toUpperCase();
|
|
||||||
const answeredQuestionAnswerText =
|
const answeredQuestionAnswerText =
|
||||||
answeredQuestion.selectedAnswerText.toUpperCase();
|
answeredQuestion.selectedAnswerText.toUpperCase();
|
||||||
const answeredQuestionNum = answeredQuestion.questionNum;
|
const answeredQuestionNum = answeredQuestion.questionNum;
|
||||||
|
|
@ -208,8 +218,7 @@ export default {
|
||||||
if (answeredQuestionIndex === 0) question.answerSelected = null;
|
if (answeredQuestionIndex === 0) question.answerSelected = null;
|
||||||
|
|
||||||
if (answeredQuestionNum - 1 === questionIndex) {
|
if (answeredQuestionNum - 1 === questionIndex) {
|
||||||
// on the right question
|
question.answerSelected = answeredQuestion.selectedAnswer;
|
||||||
question.answerSelected = answeredQuestionAnswer;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -346,6 +355,27 @@ export default {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isIncomplete) {
|
||||||
|
// User changed an earlier answer; downstream questions were cleared in the chain
|
||||||
|
self.questionsData[answer.index].answerData = null;
|
||||||
|
self.currentGlassIndex = answer.index;
|
||||||
|
|
||||||
|
if (hadSavedAnswer) {
|
||||||
|
self.questionsData[answer.index].key =
|
||||||
|
(self.questionsData[answer.index].key ??
|
||||||
|
self.questionsData[answer.index].answerKey ??
|
||||||
|
answer.index) + Date.now().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force re-render so the updated selection displays when changing a prior answer
|
||||||
|
self.questionsData[answer.index].key =
|
||||||
|
(self.questionsData[answer.index].key ??
|
||||||
|
self.questionsData[answer.index].answerKey ??
|
||||||
|
answer.index) + Date.now().toString();
|
||||||
|
|
||||||
// set final answer data for the current answered glass part
|
// set final answer data for the current answered glass part
|
||||||
self.questionsData[answer.index].answerData = {
|
self.questionsData[answer.index].answerData = {
|
||||||
answerResult: answer.answerResult,
|
answerResult: answer.answerResult,
|
||||||
|
|
|
||||||
|
|
@ -499,6 +499,88 @@ describe("vehicle-questions-mixin", () => {
|
||||||
// Assert
|
// Assert
|
||||||
expect(wrapper.vm.selectedAnswers).toMatchObject({});
|
expect(wrapper.vm.selectedAnswers).toMatchObject({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should clear answerData and keep currentGlassIndex when answer is incomplete", () => {
|
||||||
|
const answer = {
|
||||||
|
incomplete: true,
|
||||||
|
answeredQuestions: [
|
||||||
|
{
|
||||||
|
questionText: "Is this the Overland edition?",
|
||||||
|
selectedAnswer: "1|nextQuestion|5|No",
|
||||||
|
selectedAnswerText: "No",
|
||||||
|
questionNum: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
const { wrapper } = setupMocks({});
|
||||||
|
|
||||||
|
wrapper.vm.currentGlassIndex = 1;
|
||||||
|
wrapper.vm.questionsData = [
|
||||||
|
{
|
||||||
|
glassLocation: "Windshield",
|
||||||
|
glassName: "Single",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
questionSequence: 1,
|
||||||
|
questionText: "Is this the Overland edition?",
|
||||||
|
answers: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
answerData: { answerResult: "OLD-PART" },
|
||||||
|
answerKey: "Windshield-Single",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
glassLocation: "Driver",
|
||||||
|
glassName: "Front",
|
||||||
|
questions: [],
|
||||||
|
answerData: { answerResult: "OTHER-PART" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
wrapper.vm.handleCompletedQuestionChainAnswers(answer, "", wrapper.vm);
|
||||||
|
|
||||||
|
expect(wrapper.vm.questionsData[0].answerData).toBeNull();
|
||||||
|
expect(wrapper.vm.questionsData[1].answerData).toBeNull();
|
||||||
|
expect(wrapper.vm.currentGlassIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not bump key when incomplete answer had no saved answer", () => {
|
||||||
|
const answer = {
|
||||||
|
incomplete: true,
|
||||||
|
answeredQuestions: [
|
||||||
|
{
|
||||||
|
questionText: "Is this the Overland edition?",
|
||||||
|
selectedAnswer: "1|nextQuestion|2|Yes",
|
||||||
|
selectedAnswerText: "Yes",
|
||||||
|
questionNum: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
const { wrapper } = setupMocks({});
|
||||||
|
|
||||||
|
wrapper.vm.questionsData = [
|
||||||
|
{
|
||||||
|
glassLocation: "Windshield",
|
||||||
|
glassName: "Single",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
questionSequence: 1,
|
||||||
|
questionText: "Is this the Overland edition?",
|
||||||
|
answers: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
answerData: null,
|
||||||
|
answerKey: "Windshield-Single",
|
||||||
|
key: "stable-key",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
wrapper.vm.handleCompletedQuestionChainAnswers(answer, "", wrapper.vm);
|
||||||
|
|
||||||
|
expect(wrapper.vm.questionsData[0].key).toBe("stable-key");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("questions in glass parts that are after the answered glass", () => {
|
describe("questions in glass parts that are after the answered glass", () => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { routeData } from "@/router/constants/routes";
|
||||||
import { createAdyenCheckout, getSessionInfo } from "@/helpers/adyen-helper";
|
import { createAdyenCheckout, getSessionInfo } from "@/helpers/adyen-helper";
|
||||||
import { mapAdyenToFmgPaymentMethod, generateCcToken } from "@/helpers/adyen-helper";
|
import { mapAdyenToFmgPaymentMethod, generateCcToken } from "@/helpers/adyen-helper";
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
import { getAmountDue } from "@/helpers/pricing-helper";
|
import { getPiaAuthorizationAmount } from "@/helpers/pricing-helper";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
|
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
|
||||||
|
|
@ -49,7 +49,7 @@ export async function adyenReturnBeforeEnter(to, from) {
|
||||||
|
|
||||||
// TODO once afterpay info is returned, create cc token and submit order.
|
// TODO once afterpay info is returned, create cc token and submit order.
|
||||||
const paymentMethod = mapAdyenToFmgPaymentMethod(sessionInfo?.paymentMethod);
|
const paymentMethod = mapAdyenToFmgPaymentMethod(sessionInfo?.paymentMethod);
|
||||||
const amountDue = getAmountDue(store.getters.order.lineItems);
|
const authorizationAmount = getPiaAuthorizationAmount(store.getters.order.lineItems);
|
||||||
const ccToken = generateCcToken(sessionInfo);
|
const ccToken = generateCcToken(sessionInfo);
|
||||||
|
|
||||||
ccToken.authCode = "831001";
|
ccToken.authCode = "831001";
|
||||||
|
|
@ -68,7 +68,13 @@ export async function adyenReturnBeforeEnter(to, from) {
|
||||||
|
|
||||||
await baseMixin.methods.dispatchStoreAction(
|
await baseMixin.methods.dispatchStoreAction(
|
||||||
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
||||||
amountDue,
|
authorizationAmount,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.SAVE_AUTHORIZATION_AMOUNT,
|
||||||
|
authorizationAmount,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,8 @@ const getDefaultState = () => {
|
||||||
dateOfLoss: null,
|
dateOfLoss: null,
|
||||||
damageCause: null,
|
damageCause: null,
|
||||||
installOemGlass: null,
|
installOemGlass: null,
|
||||||
|
damageCity: null,
|
||||||
|
damageState: null,
|
||||||
},
|
},
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: null,
|
glassParts: null,
|
||||||
|
|
@ -162,6 +164,7 @@ const getDefaultState = () => {
|
||||||
inactivePromos: null,
|
inactivePromos: null,
|
||||||
paypalToken: null,
|
paypalToken: null,
|
||||||
nextGenSettledAmount: 0,
|
nextGenSettledAmount: 0,
|
||||||
|
authorizationAmount: null,
|
||||||
ccToken: {
|
ccToken: {
|
||||||
subscriptionId: null,
|
subscriptionId: null,
|
||||||
expMonth: null,
|
expMonth: null,
|
||||||
|
|
@ -488,6 +491,9 @@ export const mutations = {
|
||||||
updateNextGenSettledAmount(state, nextGenSettledAmount) {
|
updateNextGenSettledAmount(state, nextGenSettledAmount) {
|
||||||
state.order.payment.nextGenSettledAmount = nextGenSettledAmount;
|
state.order.payment.nextGenSettledAmount = nextGenSettledAmount;
|
||||||
},
|
},
|
||||||
|
updateAuthorizationAmount(state, authorizationAmount) {
|
||||||
|
state.order.payment.authorizationAmount = authorizationAmount;
|
||||||
|
},
|
||||||
updateInsuranceVerifiedStatus(state, isVerified) {
|
updateInsuranceVerifiedStatus(state, isVerified) {
|
||||||
state.order.payment.insuranceCoverage.isVerified = isVerified;
|
state.order.payment.insuranceCoverage.isVerified = isVerified;
|
||||||
},
|
},
|
||||||
|
|
@ -614,6 +620,8 @@ export const mutations = {
|
||||||
if (damageDetails) {
|
if (damageDetails) {
|
||||||
state.order.damage.dateOfLoss = damageDetails.dateOfLoss;
|
state.order.damage.dateOfLoss = damageDetails.dateOfLoss;
|
||||||
state.order.damage.damageCause = damageDetails.damageCause;
|
state.order.damage.damageCause = damageDetails.damageCause;
|
||||||
|
state.order.damage.damageState = damageDetails.damageState;
|
||||||
|
state.order.damage.damageCity = damageDetails.damageCity;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -887,7 +895,8 @@ export const mutations = {
|
||||||
sessionInformation.order.damage?.dateOfLoss
|
sessionInformation.order.damage?.dateOfLoss
|
||||||
);
|
);
|
||||||
state.order.damage.damageCause = sessionInformation.order.damage?.damageCause;
|
state.order.damage.damageCause = sessionInformation.order.damage?.damageCause;
|
||||||
|
state.order.damage.damageState = sessionInformation.order.damage?.damageState;
|
||||||
|
state.order.damage.damageCity = sessionInformation.order.damage?.damageCity;
|
||||||
state.order.damage.partQuestionAnswers =
|
state.order.damage.partQuestionAnswers =
|
||||||
sessionInformation.order.damage.partQuestionAnswers;
|
sessionInformation.order.damage.partQuestionAnswers;
|
||||||
state.order.damage.moldingQuestionAnswers =
|
state.order.damage.moldingQuestionAnswers =
|
||||||
|
|
@ -2008,35 +2017,30 @@ export const actions = {
|
||||||
const vehicle = context.getters.vehicle;
|
const vehicle = context.getters.vehicle;
|
||||||
const damage = context.getters.damage;
|
const damage = context.getters.damage;
|
||||||
const order = context.state.order;
|
const order = context.state.order;
|
||||||
|
const payment = context.getters.payment;
|
||||||
|
|
||||||
const carId = vehicle.carId;
|
const glassArrayForPayload = convertGlassPieceNamingForApi(damage.glassToReplace);
|
||||||
const glassArray = damage.glassToReplace;
|
|
||||||
const zipCode = order.serviceLocation.zipCode;
|
|
||||||
const vin = vehicle.vin;
|
|
||||||
const serviceType = damage.isRepair ? "Repair" : "Install";
|
|
||||||
const parentAccountNumber = context.getters.payment.parentAccountNumber;
|
|
||||||
const referralSeqNumber = order.referralSequenceNumber;
|
|
||||||
|
|
||||||
// create a new array to avoid mutating state
|
|
||||||
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
|
|
||||||
|
|
||||||
const partsOrQuestionsEndpoint = vehicle.vinRequired
|
const partsOrQuestionsEndpoint = vehicle.vinRequired
|
||||||
? endpoints.GetPartsOrQuestionsV2
|
? endpoints.GetPartsOrQuestionsV2
|
||||||
: endpoints.GetPartsOrQuestions;
|
: endpoints.GetPartsOrQuestions;
|
||||||
|
|
||||||
|
const requestPayload = {
|
||||||
|
carId: vehicle.carId,
|
||||||
|
glassPieces: glassArrayForPayload,
|
||||||
|
zip: order.serviceLocation.zipCode,
|
||||||
|
vin: vehicle.vin,
|
||||||
|
serviceType: damage.isRepair ? "Repair" : "Install",
|
||||||
|
referralSeqNumber: order.referralSequenceNumber,
|
||||||
|
parentAccountNumber: payment.parentAccountNumber,
|
||||||
|
billToAccountNumber: payment.billToAccountNumber,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await globalMethods
|
const response = await globalMethods
|
||||||
.callHttpClient({
|
.callHttpClient({
|
||||||
method: partsOrQuestionsEndpoint.method,
|
method: partsOrQuestionsEndpoint.method,
|
||||||
endpoint: partsOrQuestionsEndpoint.url,
|
endpoint: partsOrQuestionsEndpoint.url,
|
||||||
payload: {
|
payload: requestPayload,
|
||||||
carId: carId,
|
|
||||||
glassPieces: glassArrayForPayload,
|
|
||||||
zip: zipCode,
|
|
||||||
vin: vin,
|
|
||||||
serviceType: serviceType,
|
|
||||||
referralSeqNumber: referralSeqNumber,
|
|
||||||
parentAccountNumber: parentAccountNumber,
|
|
||||||
},
|
|
||||||
logApiCall: true,
|
logApiCall: true,
|
||||||
pageNameToLog: pageNameToLog,
|
pageNameToLog: pageNameToLog,
|
||||||
})
|
})
|
||||||
|
|
@ -2072,32 +2076,25 @@ export const actions = {
|
||||||
const order = context.state.order;
|
const order = context.state.order;
|
||||||
const payment = context.getters.payment;
|
const payment = context.getters.payment;
|
||||||
|
|
||||||
const carId = vehicle.carId;
|
const glassArrayForPayload = convertGlassPieceNamingForApi(damage.glassToReplace);
|
||||||
const glassArray = damage.glassToReplace;
|
const resultsArrayForPayload = convertResultsForApi(damage.partQuestionAnswers);
|
||||||
const resultsArray = damage.partQuestionAnswers;
|
|
||||||
const zipCode = order.serviceLocation.zipCode;
|
|
||||||
const vin = vehicle.vin;
|
|
||||||
const serviceType = order.serviceLocation?.appointmentType;
|
|
||||||
const referralSeqNumber = order.referralSequenceNumber;
|
|
||||||
const parentAccountNumber = payment.parentAccountNumber;
|
|
||||||
|
|
||||||
// create a new array to avoid mutating state
|
const requestPayload = {
|
||||||
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
|
carId: vehicle.carId,
|
||||||
const resultsArrayForPayload = convertResultsForApi(resultsArray);
|
glassPieces: glassArrayForPayload,
|
||||||
|
answerResults: resultsArrayForPayload,
|
||||||
|
zip: order.serviceLocation.zipCode,
|
||||||
|
vin: vehicle.vin,
|
||||||
|
serviceType: order.serviceLocation?.appointmentType,
|
||||||
|
referralSeqNumber: order.referralSequenceNumber,
|
||||||
|
parentAccountNumber: payment.parentAccountNumber,
|
||||||
|
billToAccountNumber: payment.billToAccountNumber,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await globalMethods.callHttpClient({
|
const response = await globalMethods.callHttpClient({
|
||||||
method: endpoints.GetParts.method,
|
method: endpoints.GetParts.method,
|
||||||
endpoint: endpoints.GetParts.url,
|
endpoint: endpoints.GetParts.url,
|
||||||
payload: {
|
payload: requestPayload,
|
||||||
carId: carId,
|
|
||||||
glassPieces: glassArrayForPayload,
|
|
||||||
answerResults: resultsArrayForPayload,
|
|
||||||
zip: zipCode,
|
|
||||||
vin: vin,
|
|
||||||
serviceType: serviceType,
|
|
||||||
referralSeqNumber: referralSeqNumber,
|
|
||||||
parentAccountNumber: parentAccountNumber,
|
|
||||||
},
|
|
||||||
logApiCall: true,
|
logApiCall: true,
|
||||||
pageNameToLog: pageNameToLog,
|
pageNameToLog: pageNameToLog,
|
||||||
});
|
});
|
||||||
|
|
@ -2860,6 +2857,8 @@ export const actions = {
|
||||||
installOemGlass: order.damage.installOemGlass,
|
installOemGlass: order.damage.installOemGlass,
|
||||||
dateOfLoss: order.damage.dateOfLoss,
|
dateOfLoss: order.damage.dateOfLoss,
|
||||||
damageCause: order.damage.damageCause,
|
damageCause: order.damage.damageCause,
|
||||||
|
damageState: order.damage.damageState,
|
||||||
|
damageCity: order.damage.damageCity,
|
||||||
},
|
},
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: lineItems.glassParts,
|
glassParts: lineItems.glassParts,
|
||||||
|
|
@ -2893,6 +2892,9 @@ export const actions = {
|
||||||
order.payment.piaType == paymentMethods.APPLE_PAY ? true : false,
|
order.payment.piaType == paymentMethods.APPLE_PAY ? true : false,
|
||||||
paypalToken: order.payment.paypalToken,
|
paypalToken: order.payment.paypalToken,
|
||||||
nextGenSettledAmount: order.payment.nextGenSettledAmount,
|
nextGenSettledAmount: order.payment.nextGenSettledAmount,
|
||||||
|
...(order.payment.authorizationAmount > 0
|
||||||
|
? { authorizationAmount: order.payment.authorizationAmount }
|
||||||
|
: {}),
|
||||||
ccToken: {
|
ccToken: {
|
||||||
subscriptionId: order.payment.ccToken.subscriptionId,
|
subscriptionId: order.payment.ccToken.subscriptionId,
|
||||||
expMonth: order.payment.ccToken.expMonth,
|
expMonth: order.payment.ccToken.expMonth,
|
||||||
|
|
@ -3047,6 +3049,10 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_NEXTGEN_SETTLED_AMOUNT, nextGenSettledAmount);
|
context.commit(storeMutations.UPDATE_NEXTGEN_SETTLED_AMOUNT, nextGenSettledAmount);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveAuthorizationAmount(context, authorizationAmount) {
|
||||||
|
context.commit(storeMutations.UPDATE_AUTHORIZATION_AMOUNT, authorizationAmount);
|
||||||
|
},
|
||||||
|
|
||||||
savePaypalToken(context, ppToken) {
|
savePaypalToken(context, ppToken) {
|
||||||
context.commit(storeMutations.UPDATE_PAYPAL_TOKEN, ppToken);
|
context.commit(storeMutations.UPDATE_PAYPAL_TOKEN, ppToken);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -920,6 +920,105 @@ describe("Actions", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("saveSession action, includes authorizationAmount when PIA was authorized", async () => {
|
||||||
|
const context = createAuthorizationAmountSaveSessionContext(312.45);
|
||||||
|
|
||||||
|
globalMethods.callHttpClient.mockImplementation(() => {
|
||||||
|
return Promise.resolve({ data: { referralNumber: 123 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
await actions.saveSession(context, { pageNameToLog: "test" });
|
||||||
|
|
||||||
|
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
payload: expect.objectContaining({
|
||||||
|
order: expect.objectContaining({
|
||||||
|
payment: expect.objectContaining({
|
||||||
|
authorizationAmount: 312.45,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saveSession action, omits authorizationAmount when PIA was not authorized", async () => {
|
||||||
|
const context = createAuthorizationAmountSaveSessionContext(null);
|
||||||
|
|
||||||
|
globalMethods.callHttpClient.mockImplementation(() => {
|
||||||
|
return Promise.resolve({ data: { referralNumber: 123 } });
|
||||||
|
});
|
||||||
|
globalMethods.callHttpClient.mockClear();
|
||||||
|
|
||||||
|
await actions.saveSession(context, { pageNameToLog: "test" });
|
||||||
|
|
||||||
|
const callPayload = globalMethods.callHttpClient.mock.calls[0][0].payload;
|
||||||
|
expect(callPayload.order.payment.authorizationAmount).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createAuthorizationAmountSaveSessionContext(authorizationAmount) {
|
||||||
|
const context = state;
|
||||||
|
const damage = {
|
||||||
|
numberOfChips: "2",
|
||||||
|
partQuestionAnswers: {},
|
||||||
|
moldingQuestionAnswers: {},
|
||||||
|
capabilityQuestionAnswers: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
context.getters = {
|
||||||
|
vehicle: { registration: {} },
|
||||||
|
order: { damage },
|
||||||
|
damage: {},
|
||||||
|
applicationUser: {
|
||||||
|
lastPageVisited: "test-page",
|
||||||
|
crmCustomerId: "xxx-xxx-xxx",
|
||||||
|
savedSessionId: "xxx-xxx-xxx",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
context.state = {
|
||||||
|
order: {
|
||||||
|
damage,
|
||||||
|
payment: {
|
||||||
|
insuranceCoverage: { isVerified: false },
|
||||||
|
isInsurance: false,
|
||||||
|
authorizationAmount,
|
||||||
|
ccToken: {
|
||||||
|
subscriptionId: authorizationAmount ? "sub-123" : null,
|
||||||
|
expMonth: null,
|
||||||
|
expYear: null,
|
||||||
|
cardType: null,
|
||||||
|
billToPostalCode: null,
|
||||||
|
billToFirstName: null,
|
||||||
|
billToLastName: null,
|
||||||
|
referenceNumber: null,
|
||||||
|
authCode: authorizationAmount ? "831001" : null,
|
||||||
|
transactionId: null,
|
||||||
|
transReferenceNumber: null,
|
||||||
|
lastFour: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceLocation: {},
|
||||||
|
customer: {
|
||||||
|
emailAddress: "test@safelite.com",
|
||||||
|
firstName: "John",
|
||||||
|
lastName: "Doe",
|
||||||
|
isSmsOptIn: true,
|
||||||
|
phoneNumber: "1234567890",
|
||||||
|
address: {
|
||||||
|
streetAddress: "123 Main St",
|
||||||
|
streetAddress2: "Apt 1",
|
||||||
|
city: "Anytown",
|
||||||
|
state: "OH",
|
||||||
|
zipCode: "12345",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lineItems: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
it("loadSession action, returns order information, calls mutation", async () => {
|
it("loadSession action, returns order information, calls mutation", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const context = state;
|
const context = state;
|
||||||
|
|
|
||||||
|
|
@ -36,48 +36,6 @@ describe("buttonMain.vue", () => {
|
||||||
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return loader color", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
buttonMain,
|
|
||||||
setupMocks({
|
|
||||||
propsData: {
|
|
||||||
loaderColor: "blue",
|
|
||||||
loaderEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.clicked();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
expect(loader.attributes("class")).toContain("blue");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader position", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
buttonMain,
|
|
||||||
setupMocks({
|
|
||||||
propsData: {
|
|
||||||
loaderPosition: "right",
|
|
||||||
loaderEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.clicked();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
expect(loader.attributes("class")).toContain("right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const wrapper = shallowMount(
|
const wrapper = shallowMount(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
<loader
|
<loader
|
||||||
class="ms-2"
|
class="ms-2"
|
||||||
v-if="isLoaderDisplayed && !suppressLoader"
|
v-if="isLoaderDisplayed && !suppressLoader"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
:loaderColor="loaderColor"
|
||||||
|
:loaderPosition="loaderPosition" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@ export default {
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.btn {
|
.btn {
|
||||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||||
|
|
||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $red;
|
background: $red;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { mount, shallowMount } from "@vue/test-utils";
|
||||||
import interceptOverlay from "./intercept-overlay";
|
import interceptOverlay from "./intercept-overlay";
|
||||||
|
|
||||||
describe("intercept-overlay.vue", () => {
|
describe("intercept-overlay.vue", () => {
|
||||||
|
|
@ -43,4 +43,30 @@ describe("intercept-overlay.vue", () => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("click interception", () => {
|
||||||
|
test("captureClick prevents default and stops propagation", () => {
|
||||||
|
const wrapper = shallowMount(interceptOverlay);
|
||||||
|
const event = { preventDefault: jest.fn(), stopPropagation: jest.fn() };
|
||||||
|
wrapper.vm.captureClick(event);
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(event.stopPropagation).toHaveBeenCalled();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicking the overlay does not propagate to document listeners", async () => {
|
||||||
|
const documentClickFn = jest.fn();
|
||||||
|
document.addEventListener("click", documentClickFn);
|
||||||
|
|
||||||
|
const wrapper = mount(interceptOverlay, { attachTo: document.body });
|
||||||
|
|
||||||
|
const overlay = wrapper.find(".intercept-overlay");
|
||||||
|
await overlay.trigger("click");
|
||||||
|
|
||||||
|
expect(documentClickFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.removeEventListener("click", documentClickFn);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="intercept-overlay" aria-hidden="true" @keydown.capture="blockKey"></div>
|
<div
|
||||||
|
class="intercept-overlay"
|
||||||
|
aria-hidden="true"
|
||||||
|
@keydown.capture="blockKey"
|
||||||
|
@click="captureClick"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -16,6 +20,10 @@ export default {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
},
|
},
|
||||||
|
captureClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -30,60 +30,6 @@ describe("list-button.vue", () => {
|
||||||
const loader = wrapper.findComponent({ name: "loader" });
|
const loader = wrapper.findComponent({ name: "loader" });
|
||||||
expect(loader.exists()).toBe(true);
|
expect(loader.exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return loader color", async () => {
|
|
||||||
// Arrange
|
|
||||||
const { wrapper } = setupMocks({
|
|
||||||
mockData: {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
$route: { query: { fmgPage: "page-name" } },
|
|
||||||
GaActions: GaActions,
|
|
||||||
pushEventToGA: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
loaderColor: "blue",
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.selectedValue = "something";
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.findComponent({ name: "loader" });
|
|
||||||
expect(loader.attributes("class")).toContain("blue");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader position", async () => {
|
|
||||||
// Act
|
|
||||||
const { wrapper } = setupMocks({
|
|
||||||
mockData: {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
$route: { query: { fmgPage: "page-name" } },
|
|
||||||
GaActions: GaActions,
|
|
||||||
pushEventToGA: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
loaderPosition: "right",
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.selectedValue = "something";
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.findComponent({ name: "loader" });
|
|
||||||
expect(loader.attributes("class")).toContain("right");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("baseInputButton checks", () => {
|
describe("baseInputButton checks", () => {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
</span>
|
</span>
|
||||||
<loader
|
<loader
|
||||||
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||||
:class="[this.loaderColor, this.loaderPosition]" />
|
:loaderColor="loaderColor"
|
||||||
|
:loaderPosition="loaderPosition" />
|
||||||
</div>
|
</div>
|
||||||
</baseInputButton>
|
</baseInputButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,16 @@ import loader from "./loader";
|
||||||
|
|
||||||
describe("loader.vue", () => {
|
describe("loader.vue", () => {
|
||||||
test("if it rendered the HTML element with class", () => {
|
test("if it rendered the HTML element with class", () => {
|
||||||
// Arrange/Act
|
|
||||||
const wrapper = shallowMount(loader, {
|
const wrapper = shallowMount(loader, {
|
||||||
props: {},
|
props: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.find("div").exists()).toBeTruthy();
|
expect(wrapper.find("div").exists()).toBeTruthy();
|
||||||
expect(wrapper.find(".loader").exists()).toBeTruthy();
|
expect(wrapper.find(".loader").exists()).toBeTruthy();
|
||||||
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("if it correctly passed props", () => {
|
test("applies loaderColor and loaderPosition as CSS classes on the loader element", () => {
|
||||||
// Arrange/Act
|
|
||||||
const wrapper = shallowMount(loader, {
|
const wrapper = shallowMount(loader, {
|
||||||
props: {
|
props: {
|
||||||
loaderColor: "blue",
|
loaderColor: "blue",
|
||||||
|
|
@ -22,64 +20,95 @@ describe("loader.vue", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
const loaderElement = wrapper.find(".loader");
|
||||||
expect(wrapper.props()).toMatchObject({
|
expect(loaderElement.classes()).toContain("blue");
|
||||||
loaderColor: "blue",
|
expect(loaderElement.classes()).toContain("left");
|
||||||
loaderPosition: "left",
|
wrapper.unmount();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Blocking interaction on page", () => {
|
describe("Blocking interaction on page", () => {
|
||||||
test("Does capture clicks if enabled (default)", async () => {
|
let parent;
|
||||||
// Arrange
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.id = "parent";
|
|
||||||
document.body.appendChild(div);
|
|
||||||
|
|
||||||
const parentClickFn = jest.fn();
|
beforeEach(() => {
|
||||||
|
parent = document.createElement("div");
|
||||||
div.addEventListener("click", parentClickFn);
|
parent.id = "loader-test-parent";
|
||||||
|
document.body.appendChild(parent);
|
||||||
const wrapper = shallowMount(loader, {
|
|
||||||
props: {},
|
|
||||||
attachTo: "#parent",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await wrapper.trigger("click");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(parentClickFn).not.toBeCalled();
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
document.body.removeChild(div);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Does not capture clicks if disabled", async () => {
|
afterEach(() => {
|
||||||
// Arrange
|
document.body.querySelectorAll(".intercept-overlay").forEach((el) => el.remove());
|
||||||
const div = document.createElement("div");
|
if (parent?.parentNode) {
|
||||||
div.id = "parent";
|
parent.parentNode.removeChild(parent);
|
||||||
document.body.appendChild(div);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const parentClickFn = jest.fn();
|
test("teleports intercept overlay to document.body when blocking is enabled", () => {
|
||||||
|
const wrapper = mount(loader, {
|
||||||
|
props: {},
|
||||||
|
attachTo: "#loader-test-parent",
|
||||||
|
});
|
||||||
|
|
||||||
div.addEventListener("click", parentClickFn);
|
const overlayInBody = document.body.querySelector(".intercept-overlay");
|
||||||
|
const overlayInParent = parent.querySelector(".intercept-overlay");
|
||||||
|
|
||||||
const wrapper = shallowMount(loader, {
|
expect(overlayInBody).toBeTruthy();
|
||||||
|
expect(overlayInParent).toBeNull();
|
||||||
|
expect(wrapper.find(".intercept-overlay").exists()).toBe(false);
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not render intercept overlay when allowPageInteraction is true", () => {
|
||||||
|
const wrapper = mount(loader, {
|
||||||
props: {
|
props: {
|
||||||
allowPageInteraction: true,
|
allowPageInteraction: true,
|
||||||
},
|
},
|
||||||
attachTo: "#parent",
|
attachTo: "#loader-test-parent",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
expect(document.body.querySelector(".intercept-overlay")).toBeNull();
|
||||||
await wrapper.trigger("click");
|
expect(wrapper.find(".loader").exists()).toBe(true);
|
||||||
|
|
||||||
// Assert
|
wrapper.unmount();
|
||||||
expect(parentClickFn).toBeCalled();
|
});
|
||||||
|
|
||||||
// Cleanup
|
test("clicking the teleported overlay does not propagate to document listeners", async () => {
|
||||||
document.body.removeChild(div);
|
const documentClickFn = jest.fn();
|
||||||
|
document.addEventListener("click", documentClickFn);
|
||||||
|
|
||||||
|
const wrapper = mount(loader, {
|
||||||
|
props: {},
|
||||||
|
attachTo: "#loader-test-parent",
|
||||||
|
});
|
||||||
|
|
||||||
|
const overlay = document.body.querySelector(".intercept-overlay");
|
||||||
|
expect(overlay).toBeTruthy();
|
||||||
|
|
||||||
|
overlay.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
||||||
|
|
||||||
|
expect(documentClickFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.removeEventListener("click", documentClickFn);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("teleports overlay to body even when loader is mounted inside a button", async () => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
parent.appendChild(button);
|
||||||
|
|
||||||
|
const wrapper = mount(loader, {
|
||||||
|
props: {},
|
||||||
|
attachTo: button,
|
||||||
|
});
|
||||||
|
|
||||||
|
const overlayInBody = document.body.querySelector(".intercept-overlay");
|
||||||
|
const overlayInButton = button.querySelector(".intercept-overlay");
|
||||||
|
|
||||||
|
expect(overlayInBody).toBeTruthy();
|
||||||
|
expect(overlayInButton).toBeNull();
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<interceptOverlay v-if="!allowPageInteraction" />
|
||||||
|
</Teleport>
|
||||||
<div
|
<div
|
||||||
class="loader"
|
class="loader"
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-label="Loading new page"
|
aria-label="Loading new page"
|
||||||
@click="captureClick"
|
v-bind:class="[this.loaderColor, this.loaderPosition]"></div>
|
||||||
v-bind:class="[
|
|
||||||
this.loaderColor,
|
|
||||||
this.loaderPosition,
|
|
||||||
this.allowPageInteraction ? 'allow-ui-interaction' : '',
|
|
||||||
]"></div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import interceptOverlay from "../intercept-overlay/intercept-overlay.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "loader",
|
name: "loader",
|
||||||
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
||||||
|
|
@ -31,12 +31,9 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {},
|
||||||
captureClick(event) {
|
components: {
|
||||||
if (!this.allowPageInteraction) {
|
interceptOverlay,
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -46,18 +43,6 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
//Open an overlay to prevent page interaction
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
z-index: 9999;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
//Spinner basics
|
//Spinner basics
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -101,9 +86,5 @@ export default {
|
||||||
&.black:after {
|
&.black:after {
|
||||||
background-color: $black;
|
background-color: $black;
|
||||||
}
|
}
|
||||||
&.allow-ui-interaction::before {
|
|
||||||
//no-block to enable clicking on certain buttons with loaders while the loader is actives
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue