diff --git a/api/src/school/domain/models/Activity.js b/api/src/school/domain/models/Activity.js index 44fea7a5fa3..8f9bf76068b 100644 --- a/api/src/school/domain/models/Activity.js +++ b/api/src/school/domain/models/Activity.js @@ -41,6 +41,10 @@ class Activity { return this.level === levels.TRAINING; } + get isTutorial() { + return this.level === levels.TUTORIAL; + } + get isSucceeded() { return this.status === status.SUCCEEDED; } diff --git a/api/src/school/domain/services/get-next-activity-info.js b/api/src/school/domain/services/get-next-activity-info.js index d48aaf2f7e2..3a7c700b22b 100644 --- a/api/src/school/domain/services/get-next-activity-info.js +++ b/api/src/school/domain/services/get-next-activity-info.js @@ -26,7 +26,7 @@ export function getNextActivityInfo({ activities, stepCount }) { if (_hasRunActivityLevel3Times(currentStepActivities, TRAINING) && lastActivity.isFailedOrSkipped) { return END_OF_MISSION; } - if (lastActivity.isSucceeded) { + if (lastActivity.isSucceeded || lastActivity.isTutorial) { return _getNextActivityInfoAfterSuccess(currentStepActivities, lastActivity, stepCount); } if (lastActivity.isFailedOrSkipped) { diff --git a/api/src/school/domain/services/update-current-activity.js b/api/src/school/domain/services/update-current-activity.js index 09ae009aec6..97ba1703a50 100644 --- a/api/src/school/domain/services/update-current-activity.js +++ b/api/src/school/domain/services/update-current-activity.js @@ -13,13 +13,7 @@ export async function updateCurrentActivity({ const answers = await activityAnswerRepository.findByActivity(lastActivity.id, domainTransaction); const lastAnswer = answers.at(-1); - if (lastAnswer.result.isKO()) { - return activityRepository.updateStatus( - { activityId: lastActivity.id, status: Activity.status.FAILED }, - domainTransaction, - ); - } - if (lastAnswer.result.isOK()) { + if (lastAnswer.result.isOK() || lastActivity.isTutorial) { const { missionId } = await missionAssessmentRepository.getByAssessmentId(assessmentId, domainTransaction); const mission = await missionRepository.get(missionId); if (_isActivityFinished(mission, lastActivity, answers)) { @@ -30,6 +24,12 @@ export async function updateCurrentActivity({ } return lastActivity; } + if (lastAnswer.result.isKO()) { + return activityRepository.updateStatus( + { activityId: lastActivity.id, status: Activity.status.FAILED }, + domainTransaction, + ); + } return activityRepository.updateStatus( { activityId: lastActivity.id, status: Activity.status.SKIPPED }, domainTransaction, diff --git a/api/tests/school/integration/domain/services/update-current-activity_test.js b/api/tests/school/integration/domain/services/update-current-activity_test.js index bc0aa2038de..80c9ea974f7 100644 --- a/api/tests/school/integration/domain/services/update-current-activity_test.js +++ b/api/tests/school/integration/domain/services/update-current-activity_test.js @@ -9,69 +9,142 @@ import { databaseBuilder, expect, knex, mockLearningContent } from '../../../../ import * as learningContentBuilder from '../../../../tooling/learning-content-builder/index.js'; describe('Integration | UseCase | update current activity', function () { - context('when last answer is ko', function () { - it('should update current activity with FAILED status', async function () { - const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment(); - const { id: activityId } = databaseBuilder.factory.buildActivity({ - assessmentId, - status: Activity.status.STARTED, - stepIndex: 0, - }); - databaseBuilder.factory.buildActivityAnswer({ - activityId, - challengeId: 'va_challenge_id', - result: AnswerStatus.statuses.KO, - }); + context('when activity level is tutorial', function () { + context('when activity is not finished', function () { + // eslint-disable-next-line mocha/no-setup-in-describe + [ + // eslint-disable-next-line mocha/no-setup-in-describe + { message: 'correctly answered', result: AnswerStatus.statuses.OK }, + // eslint-disable-next-line mocha/no-setup-in-describe + { message: 'wrongly answered', result: AnswerStatus.statuses.KO }, + // eslint-disable-next-line mocha/no-setup-in-describe + { message: 'skipped', result: AnswerStatus.statuses.SKIPPED }, + ].forEach(({ message, result }) => + it(`should not update current activity status when challenge is ${message}`, async function () { + const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment(); + const { id: activityId } = databaseBuilder.factory.buildActivity({ + assessmentId, + level: Activity.levels.TUTORIAL, + status: Activity.status.STARTED, + stepIndex: 0, + }); + databaseBuilder.factory.buildActivityAnswer({ + activityId, + challengeId: 'di_challenge_id', + result, + }); + + await databaseBuilder.commit(); - mockLearningContent({ - missions: [ - learningContentBuilder.buildMission({ - id: missionId, - content: { - steps: [ - { - validationChallenges: [['va_challenge_id'], ['va_next_challenge_id']], + mockLearningContent({ + missions: [ + learningContentBuilder.buildMission({ + id: missionId, + content: { + steps: [ + { + tutorialChallenges: [['di_challenge_id'], ['di_next_challenge_id']], + }, + ], }, - ], - }, - }), - ], - }); + }), + ], + }); - await databaseBuilder.commit(); + const currentActivity = await updateCurrentActivity({ + assessmentId, + activityRepository, + activityAnswerRepository, + missionAssessmentRepository, + missionRepository, + }); - const currentActivity = await updateCurrentActivity({ - assessmentId, - activityRepository, - activityAnswerRepository, - missionAssessmentRepository, - missionRepository, - }); + const activities = await knex('activities').where({ assessmentId }); + expect(activities.length).to.equal(1); + expect(activities[0].status).to.equal(Activity.status.STARTED); + expect(currentActivity.status).equals(Activity.status.STARTED); + }), + ); + }); + + context('when activity is finished', function () { + // eslint-disable-next-line mocha/no-setup-in-describe + [ + // eslint-disable-next-line mocha/no-setup-in-describe + { message: 'correctly answered', result: AnswerStatus.statuses.OK }, + // eslint-disable-next-line mocha/no-setup-in-describe + { message: 'wrongly answered', result: AnswerStatus.statuses.KO }, + // eslint-disable-next-line mocha/no-setup-in-describe + { message: 'skipped', result: AnswerStatus.statuses.SKIPPED }, + ].forEach(({ message, result }) => + it(`should update current activity with SUCCEEDED status when challenge is ${message}`, async function () { + const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment(); + const { id: activityId } = databaseBuilder.factory.buildActivity({ + assessmentId, + level: Activity.levels.TUTORIAL, + status: Activity.status.STARTED, + stepIndex: 0, + }); + databaseBuilder.factory.buildActivityAnswer({ + activityId, + challengeId: 'di_challenge_id', + result, + }); + databaseBuilder.factory.buildActivityAnswer({ + activityId, + challengeId: 'di_next_challenge_id', + result, + }); + + await databaseBuilder.commit(); + + mockLearningContent({ + missions: [ + learningContentBuilder.buildMission({ + id: missionId, + content: { + steps: [ + { + tutorialChallenges: [['di_challenge_id'], ['di_next_challenge_id']], + }, + ], + }, + }), + ], + }); + + const currentActivity = await updateCurrentActivity({ + assessmentId, + activityRepository, + activityAnswerRepository, + missionAssessmentRepository, + missionRepository, + }); - const activities = await knex('activities').where({ assessmentId }); - expect(activities.length).to.equal(1); - expect(activities[0].status).to.equal(Activity.status.FAILED); - expect(currentActivity.status).equals(Activity.status.FAILED); + const activities = await knex('activities').where({ assessmentId }); + expect(activities.length).to.equal(1); + expect(activities[0].status).to.equal(Activity.status.SUCCEEDED); + expect(currentActivity.status).equals(Activity.status.SUCCEEDED); + }), + ); }); }); - context('when last answer is ok', function () { - context('when activity is not finished', function () { - it('should not update current activity status', async function () { + context('when activity level is not tutorial', function () { + context('when last answer is ko', function () { + it('should update current activity with FAILED status', async function () { const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment(); const { id: activityId } = databaseBuilder.factory.buildActivity({ assessmentId, - level: Activity.levels.VALIDATION, status: Activity.status.STARTED, + level: Activity.levels.VALIDATION, stepIndex: 0, }); databaseBuilder.factory.buildActivityAnswer({ activityId, challengeId: 'va_challenge_id', - result: AnswerStatus.statuses.OK, + result: AnswerStatus.statuses.KO, }); - await databaseBuilder.commit(); - mockLearningContent({ missions: [ learningContentBuilder.buildMission({ @@ -87,6 +160,8 @@ describe('Integration | UseCase | update current activity', function () { ], }); + await databaseBuilder.commit(); + const currentActivity = await updateCurrentActivity({ assessmentId, activityRepository, @@ -97,13 +172,112 @@ describe('Integration | UseCase | update current activity', function () { const activities = await knex('activities').where({ assessmentId }); expect(activities.length).to.equal(1); - expect(activities[0].status).to.equal(Activity.status.STARTED); - expect(currentActivity.status).equals(Activity.status.STARTED); + expect(activities[0].status).to.equal(Activity.status.FAILED); + expect(currentActivity.status).equals(Activity.status.FAILED); }); }); + context('when last answer is ok', function () { + context('when activity is not finished', function () { + it('should not update current activity status', async function () { + const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment(); + const { id: activityId } = databaseBuilder.factory.buildActivity({ + assessmentId, + level: Activity.levels.VALIDATION, + status: Activity.status.STARTED, + stepIndex: 0, + }); + databaseBuilder.factory.buildActivityAnswer({ + activityId, + challengeId: 'va_challenge_id', + result: AnswerStatus.statuses.OK, + }); - context('when activity is finished', function () { - it('should update current activity with SUCCEEDED status', async function () { + await databaseBuilder.commit(); + + mockLearningContent({ + missions: [ + learningContentBuilder.buildMission({ + id: missionId, + content: { + steps: [ + { + validationChallenges: [['va_challenge_id'], ['va_next_challenge_id']], + }, + ], + }, + }), + ], + }); + + const currentActivity = await updateCurrentActivity({ + assessmentId, + activityRepository, + activityAnswerRepository, + missionAssessmentRepository, + missionRepository, + }); + + const activities = await knex('activities').where({ assessmentId }); + expect(activities.length).to.equal(1); + expect(activities[0].status).to.equal(Activity.status.STARTED); + expect(currentActivity.status).equals(Activity.status.STARTED); + }); + }); + + context('when activity is finished', function () { + it('should update current activity with SUCCEEDED status', async function () { + const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment(); + const { id: activityId } = databaseBuilder.factory.buildActivity({ + assessmentId, + level: Activity.levels.VALIDATION, + status: Activity.status.STARTED, + stepIndex: 0, + }); + databaseBuilder.factory.buildActivityAnswer({ + activityId, + challengeId: 'va_challenge_id', + result: AnswerStatus.statuses.OK, + }); + databaseBuilder.factory.buildActivityAnswer({ + activityId, + challengeId: 'va_next_challenge_id', + result: AnswerStatus.statuses.OK, + }); + + await databaseBuilder.commit(); + + mockLearningContent({ + missions: [ + learningContentBuilder.buildMission({ + id: missionId, + content: { + steps: [ + { + validationChallenges: [['va_challenge_id'], ['va_next_challenge_id']], + }, + ], + }, + }), + ], + }); + + const currentActivity = await updateCurrentActivity({ + assessmentId, + activityRepository, + activityAnswerRepository, + missionAssessmentRepository, + missionRepository, + }); + + const activities = await knex('activities').where({ assessmentId }); + expect(activities.length).to.equal(1); + expect(activities[0].status).to.equal(Activity.status.SUCCEEDED); + expect(currentActivity.status).equals(Activity.status.SUCCEEDED); + }); + }); + }); + context('when challenge has been skipped', function () { + it('should update current activity with SKIPPED status', async function () { const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment(); const { id: activityId } = databaseBuilder.factory.buildActivity({ assessmentId, @@ -114,14 +288,8 @@ describe('Integration | UseCase | update current activity', function () { databaseBuilder.factory.buildActivityAnswer({ activityId, challengeId: 'va_challenge_id', - result: AnswerStatus.statuses.OK, - }); - databaseBuilder.factory.buildActivityAnswer({ - activityId, - challengeId: 'va_next_challenge_id', - result: AnswerStatus.statuses.OK, + result: AnswerStatus.statuses.SKIPPED, }); - await databaseBuilder.commit(); mockLearningContent({ @@ -149,54 +317,9 @@ describe('Integration | UseCase | update current activity', function () { const activities = await knex('activities').where({ assessmentId }); expect(activities.length).to.equal(1); - expect(activities[0].status).to.equal(Activity.status.SUCCEEDED); - expect(currentActivity.status).equals(Activity.status.SUCCEEDED); - }); - }); - }); - context('when challenge has been skipped', function () { - it('should update current activity with SKIPPED status', async function () { - const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment(); - const { id: activityId } = databaseBuilder.factory.buildActivity({ - assessmentId, - level: Activity.levels.VALIDATION, - status: Activity.status.STARTED, - stepIndex: 0, + expect(activities[0].status).to.equal(Activity.status.SKIPPED); + expect(currentActivity.status).equals(Activity.status.SKIPPED); }); - databaseBuilder.factory.buildActivityAnswer({ - activityId, - challengeId: 'va_challenge_id', - result: AnswerStatus.statuses.SKIPPED, - }); - await databaseBuilder.commit(); - - mockLearningContent({ - missions: [ - learningContentBuilder.buildMission({ - id: missionId, - content: { - steps: [ - { - validationChallenges: [['va_challenge_id'], ['va_next_challenge_id']], - }, - ], - }, - }), - ], - }); - - const currentActivity = await updateCurrentActivity({ - assessmentId, - activityRepository, - activityAnswerRepository, - missionAssessmentRepository, - missionRepository, - }); - - const activities = await knex('activities').where({ assessmentId }); - expect(activities.length).to.equal(1); - expect(activities[0].status).to.equal(Activity.status.SKIPPED); - expect(currentActivity.status).equals(Activity.status.SKIPPED); }); }); }); diff --git a/api/tests/school/integration/domain/usecases/handle-activity-answer_test.js b/api/tests/school/integration/domain/usecases/handle-activity-answer_test.js index 18cd25e066d..5df852bf26b 100644 --- a/api/tests/school/integration/domain/usecases/handle-activity-answer_test.js +++ b/api/tests/school/integration/domain/usecases/handle-activity-answer_test.js @@ -21,17 +21,16 @@ import * as learningContentBuilder from '../../../../tooling/learning-content-bu describe('Integration | UseCase | handle activity answer', function () { const alwaysTrueExaminer = new Examiner({ validator: new ValidatorAlwaysOK() }); + const alwaysFalseExaminer = new Examiner({ + validator: { + assess: () => + new Validation({ + result: AnswerStatus.KO, + resultDetails: null, + }), + }, + }); context('when last answer is ko', function () { - const alwaysFalseExaminer = new Examiner({ - validator: { - assess: () => - new Validation({ - result: AnswerStatus.KO, - resultDetails: null, - }), - }, - }); - context('and mission is not finished', function () { it('last activity is started with accurate level in started assessment', async function () { const activityAnswer = domainBuilder.buildAnswer.uncorrected({ @@ -329,6 +328,55 @@ describe('Integration | UseCase | handle activity answer', function () { }); }); }); + context('when challenge belongs to unfinished tutorial and whatever answer it is', function () { + // eslint-disable-next-line mocha/no-setup-in-describe + [ + { name: 'correct answer', examiner: alwaysTrueExaminer }, + { + name: 'wrong answer', + examiner: alwaysFalseExaminer, + }, + ].forEach(({ name, examiner }) => + it(`last activity is still the started tutorial with ${name}`, async function () { + const activityAnswer = domainBuilder.buildAnswer.uncorrected({ + id: null, + challengeId: 'va_challenge_id', + }); + const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment({ + lastChallengeId: activityAnswer.challengeId, + }); + databaseBuilder.factory.buildActivity({ + assessmentId, + level: Activity.levels.TUTORIAL, + status: Activity.status.STARTED, + stepIndex: 0, + }); + + await databaseBuilder.commit(); + + mockLearningContentForMission(missionId); + + await handleActivityAnswer({ + activityAnswer, + assessmentId, + examiner, + challengeRepository, + assessmentRepository, + activityRepository, + activityAnswerRepository, + missionAssessmentRepository, + missionRepository, + }); + + await expectStatesAndLevel({ + assessmentId, + activityLevel: Activity.levels.TUTORIAL, + activityStatus: Activity.status.STARTED, + assessmentState: Assessment.states.STARTED, + }); + }), + ); + }); it('does not record activity answer when error occurs on update mission status', async function () { const initialActivityAnswerIds = await knex('activity-answers').select('id'); @@ -395,6 +443,14 @@ function mockLearningContentForMission(missionId) { }), ], challenges: [ + learningContentBuilder.buildChallenge({ + id: 'di_challenge_id', + skillId: 'skill_id', + }), + learningContentBuilder.buildChallenge({ + id: 'di_next_challenge_id', + skillId: 'skill_id', + }), learningContentBuilder.buildChallenge({ id: 'va_challenge_id', skillId: 'skill_id', @@ -414,6 +470,7 @@ function mockLearningContentForMission(missionId) { content: { steps: [ { + tutorialChallenges: [['di_challenge_id'], ['di_next_challenge_id']], validationChallenges: [['va_challenge_id'], ['va_next_challenge_id']], }, ], diff --git a/api/tests/school/unit/domain/models/Activity_test.js b/api/tests/school/unit/domain/models/Activity_test.js index 8b2974083ab..12da1662e46 100644 --- a/api/tests/school/unit/domain/models/Activity_test.js +++ b/api/tests/school/unit/domain/models/Activity_test.js @@ -9,26 +9,30 @@ describe('Unit | domain | Activity', function () { isDare: true, isValidation: false, isTraining: false, + isTutorial: false, }, { level: Activity.levels.VALIDATION, isDare: false, isValidation: true, isTraining: false, + isTutorial: false, }, { level: Activity.levels.TUTORIAL, isDare: false, isValidation: false, isTraining: false, + isTutorial: true, }, { level: Activity.levels.TRAINING, isDare: false, isValidation: false, isTraining: true, + isTutorial: false, }, - ].forEach(function ({ level, isDare, isValidation, isTraining }) { + ].forEach(function ({ level, isDare, isValidation, isTraining, isTutorial }) { it(`isDare should return ${isDare} when activity is ${level} level`, function () { const activity = new Activity({ level }); expect(activity.isDare).to.equal(isDare); @@ -43,6 +47,11 @@ describe('Unit | domain | Activity', function () { const activity = new Activity({ level }); expect(activity.isTraining).to.equal(isTraining); }); + + it(`isTutorial should return ${isTutorial} when activity is ${level} level`, function () { + const activity = new Activity({ level }); + expect(activity.isTutorial).to.equal(isTutorial); + }); }); /* eslint-enable mocha/no-setup-in-describe */ diff --git a/api/tests/school/unit/domain/services/get-next-activity-info_test.js b/api/tests/school/unit/domain/services/get-next-activity-info_test.js index 0aee7e20c53..90e1cea7921 100644 --- a/api/tests/school/unit/domain/services/get-next-activity-info_test.js +++ b/api/tests/school/unit/domain/services/get-next-activity-info_test.js @@ -74,6 +74,11 @@ describe('Unit | Domain | Pix Junior | get next activity info', function () { stepCount: 1, expectedActivityInfo: '0:TRAINING', }, + { + activities: ['0:VALIDATION:FAILED', '0:TRAINING:FAILED', '0:TUTORIAL:FAILED'], + stepCount: 1, + expectedActivityInfo: '0:TRAINING', + }, { activities: ['0:VALIDATION:FAILED', '0:TRAINING:SUCCEEDED', '0:VALIDATION:FAILED'], stepCount: 1, @@ -87,7 +92,7 @@ describe('Unit | Domain | Pix Junior | get next activity info', function () { { activities: ['0:VALIDATION:FAILED', '0:TRAINING:FAILED', '0:TUTORIAL:SKIPPED'], stepCount: 1, - expectedActivityInfo: '0:TUTORIAL', + expectedActivityInfo: '0:TRAINING', }, { activities: ['0:VALIDATION:SUCCEEDED', '1:VALIDATION:FAILED', '1:TRAINING:FAILED', '1:TUTORIAL:SUCCEEDED'],