question-chain component
This commit is contained in:
parent
f968245193
commit
2acb133e4e
2 changed files with 477 additions and 0 deletions
|
|
@ -0,0 +1,314 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import questionChain from "@/common-components/question-chain/question-chain.vue";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import { nextTick } from "vue";
|
||||
|
||||
describe("Question Chain component", () => {
|
||||
describe("on create...", () => {
|
||||
test("Should populate questions data array", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const expectedFirstQuestionAnswer = {
|
||||
answerResult: "DB09410",
|
||||
buttonLabel: "Yes",
|
||||
nextQuestionSequence: null,
|
||||
questionSequence: 1,
|
||||
questionType: "answer",
|
||||
value: "1|answer|DB09410|Yes",
|
||||
};
|
||||
|
||||
//Act
|
||||
await nextTick();
|
||||
|
||||
//Assert
|
||||
expect(wrapper.vm.questions[0].answers[0]).toMatchObject(expectedFirstQuestionAnswer);
|
||||
});
|
||||
|
||||
test("Should not include suppressed questions on questions data array", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
await wrapper.setData({
|
||||
questionData: [
|
||||
{
|
||||
questionSequence: 1,
|
||||
questionText: "Question here?",
|
||||
answers: [
|
||||
{
|
||||
answerResult: "DB09410",
|
||||
answerText: "Yes",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
{
|
||||
answerResult: "DB10840",
|
||||
answerText: "No",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
],
|
||||
suppressThisQuestion: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
//Act
|
||||
await nextTick();
|
||||
|
||||
//Assert
|
||||
expect(wrapper.vm.questions[0]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("method handleAnswer...", () => {
|
||||
test("should run getQuestionChainAnswerIfComplete", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testQuestion = {};
|
||||
const testReturnedAnswer = "aReturnedAnswer";
|
||||
wrapper.vm.getQuestionChainAnswerIfComplete = jest.fn();
|
||||
|
||||
//Act
|
||||
wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer);
|
||||
|
||||
//Assert
|
||||
expect(wrapper.vm.getQuestionChainAnswerIfComplete).toBeCalled();
|
||||
});
|
||||
|
||||
test("should emit update:modelValue if returnedAnswer contains 'answer'", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testQuestion = {};
|
||||
const testReturnedAnswer = "1|answer|DB10840|No";
|
||||
|
||||
//Act
|
||||
wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer);
|
||||
|
||||
//Assert
|
||||
expect(wrapper.emitted()).toHaveProperty("update:modelValue");
|
||||
expect(wrapper.emitted()["update:modelValue"][0][0]).toMatchObject({
|
||||
answerResult: "DB10840",
|
||||
});
|
||||
});
|
||||
|
||||
test("should NOT emit update:modelValue if no returnedAnswer", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testQuestion = {};
|
||||
|
||||
//Act
|
||||
wrapper.vm.handleAnswer(testQuestion);
|
||||
|
||||
//Assert
|
||||
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
|
||||
});
|
||||
|
||||
test("should NOT emit update:modelValue if returnedAnswer contains 'nextQuestion'", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testQuestion = {};
|
||||
const testReturnedAnswer = "1|nextQuestion|DB10840|No";
|
||||
|
||||
//Act
|
||||
wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer);
|
||||
|
||||
//Assert
|
||||
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
|
||||
});
|
||||
});
|
||||
|
||||
describe("method getQuestionChainAnswerIfComplete...", () => {
|
||||
test("should return false if no returned answer", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
//Act
|
||||
const result = wrapper.vm.getQuestionChainAnswerIfComplete();
|
||||
|
||||
//Assert
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test("should set the answerSelected to match the answered one", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testReturnedAnswer = "1|answer|DB10840|No";
|
||||
await wrapper.setData({
|
||||
questionData: [
|
||||
{
|
||||
questionSequence: 1,
|
||||
questionText: "Question here?",
|
||||
answers: [
|
||||
{
|
||||
answerResult: "DB09410",
|
||||
answerText: "Yes",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
{
|
||||
answerResult: "DB10840",
|
||||
answerText: "No",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
//Act
|
||||
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
||||
|
||||
//Assert
|
||||
expect(wrapper.vm.questions[0].answerSelected).toBe("1|answer|DB10840|No");
|
||||
});
|
||||
|
||||
test("should remove answers of questions after the answered one", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testReturnedAnswer = "1|answer|DB10840|No";
|
||||
await wrapper.setData({
|
||||
questionData: [
|
||||
{
|
||||
questionSequence: 1,
|
||||
questionText: "Question here?",
|
||||
answers: [
|
||||
{
|
||||
answerResult: "DB09410",
|
||||
answerText: "Yes",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
{
|
||||
answerResult: "DB10840",
|
||||
answerText: "No",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
questionSequence: 2,
|
||||
questionText: "Question 2",
|
||||
answers: [
|
||||
{
|
||||
answerResult: "2-yes",
|
||||
answerText: "Yes",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
{
|
||||
answerResult: "2-no",
|
||||
answerText: "No",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
],
|
||||
answerSelected: "previously selected answer",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
//Act
|
||||
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
||||
|
||||
//Assert
|
||||
expect(wrapper.vm.questions[1].answerSelected).toBeUndefined;
|
||||
});
|
||||
|
||||
test("should return false if returnedAnswer is a nextQuestion (not a final answer)", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testReturnedAnswer = "1|nextQuestion|DB10840|No";
|
||||
|
||||
//Act
|
||||
const result = wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
||||
|
||||
//Assert
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test("should trigger scroll to .current-question if returnedAnswer is a nextQuestion (not a final answer)", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testReturnedAnswer = "1|nextQuestion|DB10840|No";
|
||||
|
||||
//Act
|
||||
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
||||
|
||||
await nextTick();
|
||||
|
||||
//Assert
|
||||
expect(Element.prototype.scrollIntoView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should return answer object if returnedAnswer is a final matching answer", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
const testReturnedAnswer = "1|answer|DB10840|No";
|
||||
await wrapper.setData({
|
||||
questions: [
|
||||
{
|
||||
questionSequence: 1,
|
||||
questionText: "Question here?",
|
||||
answers: [
|
||||
{
|
||||
answerResult: "DB09410",
|
||||
answerText: "Yes",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
{
|
||||
answerResult: "DB10840",
|
||||
answerText: "No",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
});
|
||||
|
||||
//Act
|
||||
const result = wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
|
||||
|
||||
//Assert
|
||||
expect(result).toMatchObject({
|
||||
answerResult: "DB10840",
|
||||
answeredQuestions: [
|
||||
{ questionNum: 1, questionText: "Question here?", selectedAnswerText: "No" },
|
||||
{
|
||||
questionNum: 1,
|
||||
questionText: "Does only your center sliding piece need to be replaced?",
|
||||
selectedAnswerText: "No",
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({
|
||||
questionDataProp = [
|
||||
{
|
||||
questionSequence: 1,
|
||||
questionText: "Does only your center sliding piece need to be replaced?",
|
||||
answers: [
|
||||
{
|
||||
answerResult: "DB09410",
|
||||
answerText: "Yes",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
{
|
||||
answerResult: "DB10840",
|
||||
answerText: "No",
|
||||
nextQuestionSequence: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}) {
|
||||
Element.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
const mountOptions = getMountOptions({});
|
||||
mountOptions.propsData = {
|
||||
questionData: questionDataProp,
|
||||
};
|
||||
mountOptions["attachTo"] = document.body;
|
||||
|
||||
const wrapper = shallowMount(questionChain, mountOptions);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<div v-for="(q, i) in questions" :key="i">
|
||||
<transition appear name="fade" mode="out-in">
|
||||
<buttonQuestion
|
||||
v-if="q.answerSelected || q.questionSequence === currentQuestionNum"
|
||||
class="radioQuestion"
|
||||
:class="q.questionSequence === currentQuestionNum && 'current-question'"
|
||||
:questionText="q.questionText"
|
||||
:answers="q.answers"
|
||||
:groupName="`question-${index}-${q.questionSequence}`"
|
||||
:modelValue="q.answerSelected"
|
||||
@update:modelValue="handleAnswer(q, $event)"
|
||||
isRequired
|
||||
:validationRules="validationRules" />
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||
import { useValidateForm } from "vee-validate";
|
||||
|
||||
export default {
|
||||
name: "questionChain",
|
||||
data() {
|
||||
return {
|
||||
currentQuestionNum: 0,
|
||||
questions: [],
|
||||
};
|
||||
},
|
||||
props: {
|
||||
questionData: Array,
|
||||
validationRules: String,
|
||||
modelValue: Object,
|
||||
index: Number,
|
||||
answerKey: String,
|
||||
},
|
||||
async created() {
|
||||
// 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
|
||||
|
||||
this.questionData.map((q, i) => {
|
||||
const question = {
|
||||
questionText: q.questionText,
|
||||
questionSequence: q.questionSequence,
|
||||
answers: q.answers.map((a) => {
|
||||
return {
|
||||
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,
|
||||
questionSequence: q.questionSequence,
|
||||
questionType: a.nextQuestionSequence ? "nextQuestion" : "answer",
|
||||
};
|
||||
}),
|
||||
answerSelected: q.answerSelected || "",
|
||||
};
|
||||
if (!q.suppressThisQuestion) {
|
||||
this.questions.push(question);
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.modelValue?.length > 0 && this.questions.length > 0) {
|
||||
// set this.currentQuestionNum to first valid question
|
||||
this.currentQuestionNum = this.questions[0].questionSequence;
|
||||
// scroll the next question into view
|
||||
this.$nextTick(() => {
|
||||
document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAnswer(question, returnedAnswer) {
|
||||
/*
|
||||
returnedAnswer example format:
|
||||
"1|answer|DD11132|Yes"
|
||||
*/
|
||||
|
||||
question.answerSelected = returnedAnswer;
|
||||
const isQuestionChainComplete = this.getQuestionChainAnswerIfComplete(returnedAnswer);
|
||||
|
||||
if (isQuestionChainComplete) {
|
||||
this.$emit("update:modelValue", isQuestionChainComplete);
|
||||
}
|
||||
},
|
||||
getQuestionChainAnswerIfComplete(returnedAnswer) {
|
||||
// this method will return either a final answer or Boolean false
|
||||
if (!returnedAnswer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Example returnedAnswers:
|
||||
// "1|nextQuestion|3|No"
|
||||
// "5|answer|DW02104|Yes"
|
||||
|
||||
const returnedAnswerArray = returnedAnswer.split("|");
|
||||
const questionNum = parseInt(returnedAnswerArray[0]);
|
||||
const questionType = returnedAnswerArray[1];
|
||||
const questionAnswer = returnedAnswerArray[2];
|
||||
const questionAnswerText = returnedAnswerArray[3];
|
||||
const answeredQuestions = [];
|
||||
|
||||
this.questions.forEach((q) => {
|
||||
// find this question and mark it as "answered" by populating answerSelected
|
||||
if (q.questionSequence === questionNum) {
|
||||
q.answerSelected = returnedAnswer;
|
||||
q.answerNumber = questionNum;
|
||||
q.selectedAnswerText = questionAnswerText;
|
||||
}
|
||||
// remove all answers AFTER this question...
|
||||
// (needed in case user is changing previously answered questions)
|
||||
if (q.questionSequence > questionNum) {
|
||||
delete q.answerSelected;
|
||||
}
|
||||
if (q.answerSelected) {
|
||||
answeredQuestions.push({
|
||||
questionText: q.questionText,
|
||||
selectedAnswerText: q.answerSelected.split("|")[3],
|
||||
questionNum: q.questionSequence,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// return false if there's a nextQuestion... or return an object with final answers (truthy)
|
||||
if (questionType === "nextQuestion") {
|
||||
// update to next question index
|
||||
this.currentQuestionNum = parseInt(questionAnswer); // update count to display next question
|
||||
// scroll the next question into view
|
||||
this.$nextTick(() => {
|
||||
document
|
||||
.querySelector(".current-question")
|
||||
.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
// reset current question index (removes .current-question class)
|
||||
this.currentQuestionNum = 0; // reset count
|
||||
|
||||
// return an object with the part answer, all the answered questions, and the part index
|
||||
return {
|
||||
answerResult: questionAnswer,
|
||||
answeredQuestions: answeredQuestions,
|
||||
index: this.index,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
buttonQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Loading…
Reference in a new issue