Skip to content

Commit 1e7ca75

Browse files
ReidyTspaenleh
authored andcommitted
feat: changing question type create new question (#110)
1 parent 2a548f0 commit 1e7ca75

File tree

7 files changed

+172
-99
lines changed

7 files changed

+172
-99
lines changed

cypress/e2e/Admin/create/createView.cy.ts

Lines changed: 125 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import { Context, PermissionLevel } from '@graasp/sdk';
22

3-
import { DEFAULT_QUESTION_TYPE } from '../../../../src/config/constants';
3+
import {
4+
DEFAULT_QUESTION_TYPE,
5+
QuestionType,
6+
} from '../../../../src/config/constants';
47
import {
58
ADD_NEW_QUESTION_TITLE_CY,
69
CREATE_QUESTION_SELECT_TYPE_CY,
710
CREATE_QUESTION_TITLE_CY,
811
CREATE_VIEW_DELETE_BUTTON_CY,
12+
CREATE_VIEW_SAVE_BUTTON_CY,
13+
NUMBER_OF_ATTEMPTS_INPUT_CY,
914
QUESTION_BAR_ADD_NEW_BUTTON_CLASSNAME,
1015
QUESTION_BAR_CY,
1116
QUESTION_BAR_NEXT_CY,
1217
QUESTION_BAR_PREV_CY,
1318
QUESTION_STEP_CLASSNAME,
19+
TEXT_INPUT_FIELD_CY,
1420
buildQuestionStepCy,
21+
buildQuestionTypeOption,
1522
dataCyWrapper,
1623
} from '../../../../src/config/selectors';
17-
import { APP_SETTINGS, QUESTION_APP_SETTINGS } from '../../../fixtures/appSettings';
18-
import { fillMultipleChoiceQuestion } from './multipleChoices.cy';
24+
import {
25+
APP_SETTINGS,
26+
QUESTION_APP_SETTINGS,
27+
} from '../../../fixtures/appSettings';
1928
import { WAITING_DELAY_MS } from '../../../utils/time';
29+
import { fillMultipleChoiceQuestion } from './multipleChoices.cy';
2030

2131
const newMultipleChoiceData = {
2232
question: 'new question text',
@@ -34,57 +44,61 @@ const newMultipleChoiceData = {
3444
};
3545

3646
describe('Create View', () => {
37-
beforeEach(() => {
38-
cy.setUpApi({
39-
database: {
40-
appSettings: [],
41-
},
42-
appContext: {
43-
permission: PermissionLevel.Admin,
44-
context: Context.Builder,
45-
},
47+
describe('Empty Quizz', () => {
48+
beforeEach(() => {
49+
cy.setUpApi({
50+
database: {
51+
appSettings: [],
52+
},
53+
appContext: {
54+
permission: PermissionLevel.Admin,
55+
context: Context.Builder,
56+
},
57+
});
58+
cy.visit('/');
4659
});
47-
cy.visit('/');
48-
});
4960

50-
it('Empty data', () => {
51-
cy.get(dataCyWrapper(ADD_NEW_QUESTION_TITLE_CY)).should('be.visible');
52-
cy.get(dataCyWrapper(CREATE_QUESTION_TITLE_CY))
53-
.should('be.visible')
54-
.should('have.value', '');
55-
cy.get(`${dataCyWrapper(CREATE_QUESTION_SELECT_TYPE_CY)} input`).should(
56-
'have.value',
57-
DEFAULT_QUESTION_TYPE
58-
);
59-
cy.get(dataCyWrapper(QUESTION_BAR_CY)).should('be.visible');
60-
cy.get(dataCyWrapper(QUESTION_BAR_NEXT_CY)).should('be.disabled');
61-
cy.get(dataCyWrapper(QUESTION_BAR_PREV_CY)).should('be.disabled');
62-
});
61+
it('Empty data', () => {
62+
cy.get(dataCyWrapper(ADD_NEW_QUESTION_TITLE_CY)).should('be.visible');
63+
cy.get(dataCyWrapper(CREATE_QUESTION_TITLE_CY))
64+
.should('be.visible')
65+
.should('have.value', '');
66+
cy.get(`${dataCyWrapper(CREATE_QUESTION_SELECT_TYPE_CY)} input`).should(
67+
'have.value',
68+
DEFAULT_QUESTION_TYPE
69+
);
70+
cy.get(dataCyWrapper(QUESTION_BAR_CY)).should('be.visible');
71+
cy.get(dataCyWrapper(QUESTION_BAR_NEXT_CY)).should('be.disabled');
72+
cy.get(dataCyWrapper(QUESTION_BAR_PREV_CY)).should('be.disabled');
73+
});
6374

64-
it('Add questions from empty quiz', () => {
65-
// Add three questions and make sure they are added to the QuestionTopBar
66-
cy.get(dataCyWrapper(ADD_NEW_QUESTION_TITLE_CY)).should('be.visible');
67-
fillMultipleChoiceQuestion(newMultipleChoiceData);
68-
// eslint-disable-next-line cypress/no-unnecessary-waiting
69-
cy.wait(WAITING_DELAY_MS); // Wait for the new question to appear
70-
cy.get(`.${QUESTION_BAR_ADD_NEW_BUTTON_CLASSNAME}`).click();
71-
cy.get(dataCyWrapper(CREATE_QUESTION_TITLE_CY))
72-
.should('be.visible')
73-
.should('have.value', '');
74-
fillMultipleChoiceQuestion(newMultipleChoiceData);
75-
// eslint-disable-next-line cypress/no-unnecessary-waiting
76-
cy.wait(WAITING_DELAY_MS);
77-
cy.get(`.${QUESTION_BAR_ADD_NEW_BUTTON_CLASSNAME}`).click();
78-
cy.get(dataCyWrapper(CREATE_QUESTION_TITLE_CY))
79-
.should('be.visible')
80-
.should('have.value', '');
81-
fillMultipleChoiceQuestion(newMultipleChoiceData);
82-
// Verify the questions are added to the order list by checking the number of
83-
// question nodes in the QuestionTopBar, as we cannot check the app settings directly
84-
cy.get('html').find(`.${QUESTION_STEP_CLASSNAME}`).should('have.length', 3);
75+
it('Add questions from empty quiz', () => {
76+
// Add three questions and make sure they are added to the QuestionTopBar
77+
cy.get(dataCyWrapper(ADD_NEW_QUESTION_TITLE_CY)).should('be.visible');
78+
fillMultipleChoiceQuestion(newMultipleChoiceData);
79+
// eslint-disable-next-line cypress/no-unnecessary-waiting
80+
cy.wait(WAITING_DELAY_MS); // Wait for the new question to appear
81+
cy.get(`.${QUESTION_BAR_ADD_NEW_BUTTON_CLASSNAME}`).click();
82+
cy.get(dataCyWrapper(CREATE_QUESTION_TITLE_CY))
83+
.should('be.visible')
84+
.should('have.value', '');
85+
fillMultipleChoiceQuestion(newMultipleChoiceData);
86+
// eslint-disable-next-line cypress/no-unnecessary-waiting
87+
cy.wait(WAITING_DELAY_MS);
88+
cy.get(`.${QUESTION_BAR_ADD_NEW_BUTTON_CLASSNAME}`).click();
89+
cy.get(dataCyWrapper(CREATE_QUESTION_TITLE_CY))
90+
.should('be.visible')
91+
.should('have.value', '');
92+
fillMultipleChoiceQuestion(newMultipleChoiceData);
93+
// Verify the questions are added to the order list by checking the number of
94+
// question nodes in the QuestionTopBar, as we cannot check the app settings directly
95+
cy.get('html')
96+
.find(`.${QUESTION_STEP_CLASSNAME}`)
97+
.should('have.length', 3);
98+
});
8599
});
86100

87-
describe('Create View', () => {
101+
describe('Existing Quizz', () => {
88102
beforeEach(() => {
89103
cy.setUpApi({
90104
database: {
@@ -174,5 +188,67 @@ describe('Create View', () => {
174188
.find(`.${QUESTION_STEP_CLASSNAME}`)
175189
.should('have.length', 5);
176190
});
191+
192+
it('Update Question type should not create a new question', () => {
193+
const numberOfQuestions = 4;
194+
const numberOfAttempts = 3;
195+
196+
// update the number of attempts to ensure that changing question type
197+
// keep the good the number of attempts.
198+
cy.get(`${dataCyWrapper(NUMBER_OF_ATTEMPTS_INPUT_CY)} input`).clear();
199+
cy.get(`${dataCyWrapper(NUMBER_OF_ATTEMPTS_INPUT_CY)} input`).type(
200+
`${numberOfAttempts}`
201+
);
202+
cy.get(`${dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)}`).click();
203+
204+
// Check the current number of questions
205+
cy.get('html')
206+
.find(`.${QUESTION_STEP_CLASSNAME}`)
207+
.should('have.length', numberOfQuestions);
208+
209+
// update the question type and save
210+
const updatedQuestion = {
211+
question: 'My new text input question',
212+
questionType: QuestionType.TEXT_INPUT,
213+
answer: 'new answer',
214+
};
215+
cy.get(`${dataCyWrapper(CREATE_QUESTION_TITLE_CY)} input`).clear();
216+
cy.get(`${dataCyWrapper(CREATE_QUESTION_TITLE_CY)} input`).type(
217+
updatedQuestion.question
218+
);
219+
cy.get(`${dataCyWrapper(CREATE_QUESTION_SELECT_TYPE_CY)}`).click();
220+
cy.get(
221+
`${dataCyWrapper(
222+
buildQuestionTypeOption(updatedQuestion.questionType)
223+
)}`
224+
).click();
225+
cy.get(`${dataCyWrapper(TEXT_INPUT_FIELD_CY)}`).type(
226+
updatedQuestion.answer
227+
);
228+
cy.get(`${dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)}`).click();
229+
230+
// Check that the current number of questions is still unchanged
231+
cy.get('html')
232+
.find(`.${QUESTION_STEP_CLASSNAME}`)
233+
.should('have.length', numberOfQuestions);
234+
235+
// Check the question title, question type, the answer and attempts.
236+
cy.get(`${dataCyWrapper(CREATE_QUESTION_TITLE_CY)} input`).should(
237+
'have.value',
238+
updatedQuestion.question
239+
);
240+
cy.get(`${dataCyWrapper(CREATE_QUESTION_SELECT_TYPE_CY)} input`).should(
241+
'have.value',
242+
updatedQuestion.questionType
243+
);
244+
cy.get(`${dataCyWrapper(TEXT_INPUT_FIELD_CY)} input`).should(
245+
'have.value',
246+
updatedQuestion.answer
247+
);
248+
cy.get(`${dataCyWrapper(NUMBER_OF_ATTEMPTS_INPUT_CY)} input`).should(
249+
'have.value',
250+
numberOfAttempts
251+
);
252+
});
177253
});
178254
});

src/components/context/utilities.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,27 @@ export const validateQuestionData = (data: QuestionData) => {
259259
return true;
260260
}
261261
};
262+
263+
/**
264+
* Return the first valid value in three possible name of values or empty if all nullish
265+
* @param response The response to get the value.
266+
* @returns The first valid value of the data.
267+
*/
268+
export const getResponseValue = (response: Data | undefined) => {
269+
const text = (response as TextAppDataData)?.text;
270+
if (text) {
271+
return text;
272+
}
273+
274+
const value = (response as SliderAppDataData)?.value;
275+
if (value) {
276+
return value;
277+
}
278+
279+
const choices = (response as MultipleChoiceAppDataData)?.choices;
280+
if (choices) {
281+
return choices.join(', ');
282+
}
283+
284+
return '';
285+
};

src/components/create/CreateView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ const CreateView = () => {
106106
onChange={(changes: QuestionData) => {
107107
setNewData({
108108
...changes,
109+
questionId: newData.questionId,
109110
question: newData.question,
110111
explanation: newData.explanation,
112+
numberOfAttempts: newData.numberOfAttempts,
111113
});
112114
}}
113115
/>

src/components/play/PlaySlider.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const PlaySlider = ({
2727
}: Props) => {
2828
const min = values?.min;
2929
const max = values?.max;
30+
const defaultValue = Math.round((max - min) / 2 + min);
3031
const [marks, setMarks] = useState<{ value: number; label: number }[]>([]);
3132

3233
const sliderSx = {
@@ -40,6 +41,13 @@ const PlaySlider = ({
4041
};
4142

4243
useEffect(() => {
44+
// Notify with the default value if response's value is null.
45+
// Without it, if the user click on submit without changing the slider's value,
46+
// the response's value will be undefined instead of defaultValue displayed on the screen.
47+
if (!isReadonly && !response.value) {
48+
setResponse(defaultValue);
49+
}
50+
4351
let newMarks = [
4452
{ value: min, label: min },
4553
{
@@ -72,7 +80,7 @@ const PlaySlider = ({
7280
<Slider
7381
data-cy={PLAY_VIEW_SLIDER_CY}
7482
aria-label="Custom marks"
75-
value={response?.value ?? (max - min) / 2 + min}
83+
value={response?.value ?? defaultValue}
7684
valueLabelDisplay="on"
7785
onChange={(_e, val) => {
7886
if (!isReadonly) {

src/components/play/PlayView.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const PlayView = () => {
9494
object: Partial<T>,
9595
key: K,
9696
value: V,
97-
prevValue: V | undefined
97+
prevValue: V | undefined,
9898
) => {
9999
// reset correctness on value changed if not the same
100100
// this allow to show prev error and avoid to show success
@@ -173,7 +173,9 @@ const PlayView = () => {
173173
choices={currentQuestion.data.choices}
174174
response={newResponse as MultipleChoiceAppDataData}
175175
setResponse={(choices) => {
176-
setNewResponse(setInData(newResponse, 'choices', choices));
176+
setNewResponse(
177+
setInData(newResponse, 'choices', choices)
178+
);
177179
setShowCorrectness(false);
178180
}}
179181
showCorrection={showCorrection}
@@ -208,7 +210,9 @@ const PlayView = () => {
208210
values={currentQuestion.data}
209211
response={newResponse as FillTheBlanksAppDataData}
210212
setResponse={(text: string) => {
211-
setNewResponse(setInData(newResponse, 'text', text));
213+
setNewResponse(
214+
setInData(newResponse, 'text', text)
215+
);
212216
setShowCorrectness(false);
213217
}}
214218
showCorrection={showCorrection}

src/components/results/TableByQuestion.tsx

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { visuallyHidden } from '@mui/utils';
2121

2222
import { AppData, Member } from '@graasp/sdk';
2323

24-
import { QuestionType } from '../../config/constants';
2524
import {
2625
TABLE_BY_QUESTION_ANSWER_DATA_CY,
2726
TABLE_BY_QUESTION_CONTAINER_CY,
@@ -41,13 +40,10 @@ import {
4140
comparatorArrayByElemName,
4241
getComparator,
4342
} from '../../utils/tableUtils';
44-
import { computeCorrectness, sortAppDataByDate } from '../context/utilities';
43+
import { computeCorrectness, getResponseValue, sortAppDataByDate } from '../context/utilities';
4544
import {
46-
MultipleChoiceAppDataData,
4745
QuestionAppDataData,
4846
QuestionDataAppSetting,
49-
SliderAppDataData,
50-
TextAppDataData,
5147
} from '../types/types';
5248

5349
type Props = {
@@ -94,21 +90,8 @@ const TableByQuestion = ({
9490
* @returns {string} Response for given user.
9591
*/
9692
const getResponseDataForUserId = (userId: string) => {
97-
const response = getResponseForUserId(userId)?.data;
98-
99-
switch (question.data.type) {
100-
case QuestionType.TEXT_INPUT:
101-
case QuestionType.FILL_BLANKS:
102-
return (response as TextAppDataData)?.text ?? '';
103-
case QuestionType.SLIDER:
104-
return (response as SliderAppDataData)?.value ?? '';
105-
case QuestionType.MULTIPLE_CHOICES:
106-
return (
107-
(response as MultipleChoiceAppDataData)?.choices?.join(', ') ?? ''
108-
);
109-
default:
110-
return '';
111-
}
93+
// using this instead of switch allow to display answer after question type changed
94+
return getResponseValue(getResponseForUserId(userId)?.data);
11295
};
11396

11497
/**

0 commit comments

Comments
 (0)