Skip to content

Commit be5ba48

Browse files
authored
Merge pull request #5949 from Abhishek-Punhani/Issue5913
Update AnswersEditor layout and answer option structure
2 parents 2a280d7 + 7a6b8b1 commit be5ba48

6 files changed

Lines changed: 474 additions & 302 deletions

File tree

contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.spec.js

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vu
77

88
jest.mock('shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue');
99

10+
jest.mock('kolibri-design-system/lib/composables/useKResponsiveWindow', () => {
11+
return function useKResponsiveWindow() {
12+
const { ref } = require('vue');
13+
return { windowIsSmall: ref(false) };
14+
};
15+
});
16+
1017
const clickNewAnswerBtn = async wrapper => {
1118
await wrapper.findComponent('[data-test="newAnswerBtn"]').trigger('click');
1219
};
@@ -16,7 +23,7 @@ const rendersNewAnswerBtn = wrapper => {
1623
};
1724

1825
const clickAnswer = async (wrapper, answerIdx) => {
19-
await wrapper.findAllComponents('[data-test="answer"]').at(answerIdx).trigger('click');
26+
await wrapper.findAll('[data-test="answer"]').at(answerIdx).trigger('click');
2027
};
2128

2229
const clickMoveAnswerUp = async (wrapper, answerIdx) => {
@@ -59,6 +66,24 @@ describe('AnswersEditor', () => {
5966
expect(wrapper.html()).toContain('Question has no answer options');
6067
});
6168

69+
describe('answers label', () => {
70+
it.each([
71+
[AssessmentItemTypes.SINGLE_SELECTION, AnswersEditor.$trs.answersLabelSingleChoice],
72+
[AssessmentItemTypes.TRUE_FALSE, AnswersEditor.$trs.answersLabelSingleChoice],
73+
[AssessmentItemTypes.MULTIPLE_SELECTION, AnswersEditor.$trs.answersLabelMultipleChoice],
74+
[AssessmentItemTypes.INPUT_QUESTION, AnswersEditor.$trs.answersLabelNumeric],
75+
])('renders the correct label for %s questions', (questionKind, expectedLabel) => {
76+
wrapper = shallowMount(AnswersEditor, {
77+
propsData: {
78+
questionKind,
79+
answers: [],
80+
},
81+
});
82+
83+
expect(wrapper.text()).toContain(expectedLabel);
84+
});
85+
});
86+
6287
describe('for a single selection question', () => {
6388
beforeEach(() => {
6489
wrapper = mount(AnswersEditor, {
@@ -72,7 +97,7 @@ describe('AnswersEditor', () => {
7297
});
7398
});
7499

75-
it('renders answers as radio selects', () => {
100+
it('renders answers as radio controls', () => {
76101
const inputs = wrapper.findAll('input');
77102

78103
expect(inputs.length).toBe(2);
@@ -88,8 +113,38 @@ describe('AnswersEditor', () => {
88113
expect(inputs.at(1).element.checked).toBe(false);
89114
});
90115

116+
it('marks correct answer rows with the selected visual state', () => {
117+
const answerRows = wrapper.findAll('[data-test="answer"]');
118+
119+
// Correct row has both border-color and background-color applied
120+
expect(answerRows.at(0).attributes('style')).toContain('border-color');
121+
expect(answerRows.at(0).attributes('style')).toContain('background-color');
122+
// Incorrect row has border-color but no inline background-color (null omits it)
123+
expect(answerRows.at(1).attributes('style')).toContain('border-color');
124+
expect(answerRows.at(1).attributes('style')).not.toContain('background-color');
125+
});
126+
127+
it('renders all possible answers', () => {
128+
// First answer is open by default (openAnswerIdx=0) — edit mode TipTapEditor
129+
// Second answer is closed — view mode TipTapEditor
130+
const editors = wrapper.findAllComponents(TipTapEditor);
131+
132+
// Closed answer uses view mode to safely render rich text
133+
const viewEditor = editors.filter(e => e.props('mode') === 'view').at(0);
134+
expect(viewEditor.exists()).toBe(true);
135+
expect(viewEditor.props('value')).toBe('Peanut butter');
136+
137+
// Open answer uses edit mode
138+
const editEditor = editors.filter(e => e.props('mode') === 'edit').at(0);
139+
expect(editEditor.exists()).toBe(true);
140+
expect(editEditor.props('value')).toBe('Mayonnaise (I mean you can, but...)');
141+
});
142+
91143
it('renders new answer button', () => {
92144
expect(rendersNewAnswerBtn(wrapper)).toBe(true);
145+
expect(wrapper.findComponent('[data-test="newAnswerBtn"]').text()).toContain(
146+
AnswersEditor.$trs.addOptionBtnLabel,
147+
);
93148
});
94149

95150
describe('on new answer button click', () => {
@@ -140,8 +195,26 @@ describe('AnswersEditor', () => {
140195
expect(inputs.at(2).element.checked).toBe(true);
141196
});
142197

198+
it('renders all possible answers', () => {
199+
// First answer is open by default (openAnswerIdx=0) — edit mode TipTapEditor
200+
// Remaining answers are closed — each gets a view mode TipTapEditor
201+
const editors = wrapper.findAllComponents(TipTapEditor);
202+
203+
const viewEditors = editors.filter(e => e.props('mode') === 'view');
204+
expect(viewEditors.length).toBe(2);
205+
expect(viewEditors.at(0).props('value')).toBe('Peanut butter');
206+
expect(viewEditors.at(1).props('value')).toBe('Jelly');
207+
208+
const editEditor = editors.filter(e => e.props('mode') === 'edit').at(0);
209+
expect(editEditor.exists()).toBe(true);
210+
expect(editEditor.props('value')).toBe('Mayonnaise (I mean you can, but...)');
211+
});
212+
143213
it('renders new answer button', () => {
144214
expect(rendersNewAnswerBtn(wrapper)).toBe(true);
215+
expect(wrapper.findComponent('[data-test="newAnswerBtn"]').text()).toContain(
216+
AnswersEditor.$trs.addOptionBtnLabel,
217+
);
145218
});
146219

147220
describe('on new answer button click', () => {
@@ -175,7 +248,7 @@ describe('AnswersEditor', () => {
175248
});
176249
});
177250

178-
it('renders answers as radio selects', () => {
251+
it('renders answers as radio controls', () => {
179252
const inputs = wrapper.findAll('input');
180253

181254
expect(inputs.length).toBe(2);
@@ -202,31 +275,24 @@ describe('AnswersEditor', () => {
202275
propsData: {
203276
questionKind: AssessmentItemTypes.INPUT_QUESTION,
204277
answers: [
205-
{ answer: 'Mayonnaise (I mean you can, but...)', correct: true, order: 1 },
206-
{ answer: 'Peanut butter', correct: true, order: 2 },
278+
{ answer: '1.5', correct: true, order: 1 },
279+
{ answer: '2', correct: true, order: 2 },
207280
],
208281
},
209282
});
210283
});
211284

212-
it('renders all possible answers', () => {
213-
// The answer text won't render when `mount()` is used so we override
214-
// and use shallowMount here
215-
wrapper = shallowMount(AnswersEditor, {
216-
propsData: {
217-
questionKind: AssessmentItemTypes.INPUT_QUESTION,
218-
answers: [
219-
{ answer: 'Mayonnaise (I mean you can, but...)', correct: true, order: 1 },
220-
{ answer: 'Peanut butter', correct: true, order: 2 },
221-
],
222-
},
223-
});
224-
expect(wrapper.html()).toContain('Mayonnaise (I mean you can, but...)');
225-
expect(wrapper.html()).toContain('Peanut butter');
285+
it('renders open answer as a number input and closed answer as plain text', () => {
286+
expect(wrapper.find('input[type="number"]').element.value).toBe('1.5');
287+
288+
expect(wrapper.html()).toContain('2');
226289
});
227290

228291
it('renders new answer button', () => {
229292
expect(rendersNewAnswerBtn(wrapper)).toBe(true);
293+
expect(wrapper.findComponent('[data-test="newAnswerBtn"]').text()).toContain(
294+
AnswersEditor.$trs.newAnswerBtnLabel,
295+
);
230296
});
231297

232298
describe('on new answer button click', () => {
@@ -238,8 +304,8 @@ describe('AnswersEditor', () => {
238304
expect(wrapper.emitted().update).toBeTruthy();
239305
expect(wrapper.emitted().update.length).toBe(1);
240306
expect(wrapper.emitted().update[0][0]).toEqual([
241-
{ answer: 'Mayonnaise (I mean you can, but...)', correct: true, order: 1 },
242-
{ answer: 'Peanut butter', correct: true, order: 2 },
307+
{ answer: '1.5', correct: true, order: 1 },
308+
{ answer: '2', correct: true, order: 2 },
243309
{ answer: '', correct: true, order: 3 },
244310
]);
245311
});
@@ -260,14 +326,12 @@ describe('AnswersEditor', () => {
260326
});
261327
});
262328

263-
it('passes autofocus=true to the open answer editor', () => {
264-
const editors = wrapper.findAllComponents(TipTapEditor);
265-
expect(editors.at(1).props('autofocus')).toBe(true);
266-
});
267-
268-
it('passes autofocus=false to closed answer editors', () => {
329+
it('passes autofocus=true to the open (edit-mode) answer editor', () => {
330+
// A single TipTapEditor per answer switches mode reactively.
331+
// The editor for openAnswerIdx has mode='edit' and autofocus=true.
269332
const editors = wrapper.findAllComponents(TipTapEditor);
270-
expect(editors.at(0).props('autofocus')).toBe(false);
333+
const editModeEditor = editors.filter(e => e.props('mode') === 'edit').at(0);
334+
expect(editModeEditor.props('autofocus')).toBe(true);
271335
});
272336
});
273337

@@ -373,7 +437,7 @@ describe('AnswersEditor', () => {
373437
});
374438

375439
await wrapper.vm.$nextTick();
376-
await wrapper.findAll('input[type="radio"]').at(1).setChecked();
440+
await wrapper.findAll('.answer-selection input[type="radio"]').at(1).trigger('click');
377441
});
378442

379443
it('emits update event with a payload containing updated answers', () => {

0 commit comments

Comments
 (0)