diff --git a/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js b/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js index 5cd3ec3e54f..7d7b83eede1 100644 --- a/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js +++ b/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js @@ -22,4 +22,24 @@ const buildOrganizationLearnerFeature = function ({ }); }; -export { buildOrganizationLearnerFeature }; +const buildOrganizationLearnerFeatureWithFeatureKey = function ({ + id = databaseBuffer.getNextId(), + organizationLearnerId, + featureKey, +} = {}) { + organizationLearnerId = organizationLearnerId === undefined ? buildOrganizationLearner().id : organizationLearnerId; + const featureId = buildFeature({ key: featureKey }).id; + + const values = { + id, + organizationLearnerId, + featureId, + }; + + return databaseBuffer.pushInsertable({ + tableName: 'organization-learner-features', + values, + }); +}; + +export { buildOrganizationLearnerFeature, buildOrganizationLearnerFeatureWithFeatureKey }; diff --git a/api/db/migrations/20241106164209_add-index-on-organization-learner-features.js b/api/db/migrations/20241106164209_add-index-on-organization-learner-features.js new file mode 100644 index 00000000000..3dc64d0966e --- /dev/null +++ b/api/db/migrations/20241106164209_add-index-on-organization-learner-features.js @@ -0,0 +1,25 @@ +// Make sure you properly test your migration, especially DDL (Data Definition Language) +// ! If the target table is large, and the migration take more than 20 minutes, the deployment will fail ! + +// You can design and test your migration to avoid this by following this guide +// https://1024pix.atlassian.net/wiki/spaces/EDTDT/pages/3849323922/Cr+er+une+migration + +// If your migrations target `answers` or `knowledge-elements` +// contact @team-captains, because automatic migrations are not active on `pix-datawarehouse-production` +// this may prevent data replication to succeed the day after your migration is deployed on `pix-api-production` +const TABLE_NAME = 'organization-learner-features'; +const COLUMN_NAME = 'organizationLearnerId'; + +const up = async function (knex) { + await knex.schema.table(TABLE_NAME, function (table) { + table.index(COLUMN_NAME); + }); +}; + +const down = async function (knex) { + await knex.schema.table(TABLE_NAME, function (table) { + table.dropIndex(COLUMN_NAME); + }); +}; + +export { down, up }; diff --git a/api/src/prescription/organization-learner/application/api/models/OrganizationLearner.js b/api/src/prescription/organization-learner/application/api/models/OrganizationLearner.js index 86d4232f731..fdaec58e645 100644 --- a/api/src/prescription/organization-learner/application/api/models/OrganizationLearner.js +++ b/api/src/prescription/organization-learner/application/api/models/OrganizationLearner.js @@ -1,9 +1,10 @@ export class OrganizationLearner { - constructor({ id, firstName, lastName, organizationId, ...attributes }) { + constructor({ id, firstName, lastName, features, organizationId, ...attributes }) { this.id = id; this.firstName = firstName; this.lastName = lastName; - this.division = attributes['Libellé classe']; + this.features = features; this.organizationId = organizationId; + this.division = attributes['Libellé classe']; } } diff --git a/api/src/prescription/organization-learner/domain/read-models/OrganizationLearner.js b/api/src/prescription/organization-learner/domain/read-models/OrganizationLearner.js index f8fa5754731..d4a2a497d1b 100644 --- a/api/src/prescription/organization-learner/domain/read-models/OrganizationLearner.js +++ b/api/src/prescription/organization-learner/domain/read-models/OrganizationLearner.js @@ -16,6 +16,7 @@ class OrganizationLearner { organizationId, certifiableAtFromLearner, userId, + features, } = {}) { this.id = id; this.firstName = firstName; @@ -27,6 +28,7 @@ class OrganizationLearner { this.authenticationMethods = authenticationMethods; this.organizationId = organizationId; this.userId = userId; + this.features = features; this._buildCertificability({ isCertifiableFromCampaign, diff --git a/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-repository.js b/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-repository.js index 7a841d15da2..6e3ceeaa0b9 100644 --- a/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-repository.js +++ b/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-repository.js @@ -47,6 +47,7 @@ async function get({ organizationLearnerId }) { 'subquery.isCertifiableFromCampaign', 'subquery.certifiableAtFromCampaign', knex.raw('array_remove(ARRAY_AGG("identityProvider"), NULL) AS "authenticationMethods"'), + knex.raw('array_remove(ARRAY_AGG(features.key), NULL) as features'), 'users.email', 'users.username', ) @@ -55,6 +56,12 @@ async function get({ organizationLearnerId }) { .leftJoin('subquery', 'subquery.organizationLearnerId', 'view-active-organization-learners.id') .leftJoin('authentication-methods', 'authentication-methods.userId', 'view-active-organization-learners.userId') .leftJoin('users', 'view-active-organization-learners.userId', 'users.id') + .leftJoin( + 'organization-learner-features', + 'view-active-organization-learners.id', + 'organization-learner-features.organizationLearnerId', + ) + .leftJoin('features', 'organization-learner-features.featureId', 'features.id') .groupBy( 'view-active-organization-learners.id', 'view-active-organization-learners.firstName', diff --git a/api/src/school/domain/models/OrganizationLearner.js b/api/src/school/domain/models/OrganizationLearner.js index 50192bd0009..87d9b536c3f 100644 --- a/api/src/school/domain/models/OrganizationLearner.js +++ b/api/src/school/domain/models/OrganizationLearner.js @@ -1,9 +1,19 @@ class OrganizationLearner { - constructor({ id, lastName, firstName, division, organizationId, completedMissionIds, startedMissionIds } = {}) { + constructor({ + id, + lastName, + firstName, + division, + features, + organizationId, + completedMissionIds, + startedMissionIds, + } = {}) { this.id = id; this.lastName = lastName; this.firstName = firstName; this.division = division; + this.features = features || []; this.organizationId = organizationId; this.completedMissionIds = completedMissionIds; this.startedMissionIds = startedMissionIds; diff --git a/api/src/school/domain/read-models/OrganizationLearnerDTO.js b/api/src/school/domain/read-models/OrganizationLearnerDTO.js index 9c49748574c..650a6d171c0 100644 --- a/api/src/school/domain/read-models/OrganizationLearnerDTO.js +++ b/api/src/school/domain/read-models/OrganizationLearnerDTO.js @@ -1,5 +1,14 @@ class OrganizationLearnerDTO { - constructor({ id, displayName, firstName, division, organizationId, startedMissionIds, completedMissionIds } = {}) { + constructor({ + id, + displayName, + firstName, + division, + organizationId, + startedMissionIds, + completedMissionIds, + features, + } = {}) { this.id = id; this.displayName = displayName; this.firstName = firstName; @@ -7,6 +16,7 @@ class OrganizationLearnerDTO { this.organizationId = organizationId; this.startedMissionIds = startedMissionIds; this.completedMissionIds = completedMissionIds; + this.features = features || []; } } diff --git a/api/src/school/infrastructure/serializers/organization-learner.js b/api/src/school/infrastructure/serializers/organization-learner.js index 81323cc93a6..f6696d11f94 100644 --- a/api/src/school/infrastructure/serializers/organization-learner.js +++ b/api/src/school/infrastructure/serializers/organization-learner.js @@ -2,7 +2,15 @@ import { Serializer } from 'jsonapi-serializer'; const serialize = function (organizationLearner) { return new Serializer('organizationLearner', { - attributes: ['firstName', 'displayName', 'division', 'organizationId', 'completedMissionIds', 'startedMissionIds'], + attributes: [ + 'firstName', + 'displayName', + 'division', + 'organizationId', + 'completedMissionIds', + 'startedMissionIds', + 'features', + ], transform: function (organizationLearner) { return { ...organizationLearner, diff --git a/api/tests/prescription/organization-learner/integration/infrastructure/repositories/organization-learner-repository_test.js b/api/tests/prescription/organization-learner/integration/infrastructure/repositories/organization-learner-repository_test.js index a3f5b31b72a..90c3c3de153 100644 --- a/api/tests/prescription/organization-learner/integration/infrastructure/repositories/organization-learner-repository_test.js +++ b/api/tests/prescription/organization-learner/integration/infrastructure/repositories/organization-learner-repository_test.js @@ -47,6 +47,23 @@ describe('Integration | Infrastructure | Repository | Organization Learner', fun expect(organizationLearner.email).to.equal('k.s@example.net'); expect(organizationLearner.username).to.equal('sassouk'); expect(organizationLearner.organizationId).to.equal(organizationId); + expect(organizationLearner.features).to.be.empty; + }); + + it("Should return organization learner's features", async function () { + const organizationLearnerId = databaseBuilder.factory.buildOrganizationLearner().id; + databaseBuilder.factory.prescription.organizationLearners.buildOrganizationLearnerFeatureWithFeatureKey({ + organizationLearnerId, + featureKey: 'ORALIZATION', + }); + databaseBuilder.factory.prescription.organizationLearners.buildOrganizationLearnerFeatureWithFeatureKey({ + organizationLearnerId, + featureKey: 'BLA', + }); + await databaseBuilder.commit(); + + const organizationLearner = await organizationLearnerRepository.get({ organizationLearnerId }); + expect(organizationLearner.features).to.deep.equal(['ORALIZATION', 'BLA']); }); it('Should return the organization learner with a given ID', async function () { diff --git a/api/tests/school/integration/domain/usecases/get-organization-learner-with-completed-mission-ids_test.js b/api/tests/school/integration/domain/usecases/get-organization-learner-with-completed-mission-ids_test.js index 53749359ac7..965f61ff922 100644 --- a/api/tests/school/integration/domain/usecases/get-organization-learner-with-completed-mission-ids_test.js +++ b/api/tests/school/integration/domain/usecases/get-organization-learner-with-completed-mission-ids_test.js @@ -42,6 +42,30 @@ describe('Integration | Usecase | get-organization-learner-with-completed-missio ); }); + it('should return organization learner with features', async function () { + const organizationLearner = + databaseBuilder.factory.prescription.organizationLearners.buildOndeOrganizationLearner(); + databaseBuilder.factory.prescription.organizationLearners.buildOrganizationLearnerFeatureWithFeatureKey({ + organizationLearnerId: organizationLearner.id, + featureKey: 'ORALIZATION', + }); + await databaseBuilder.commit(); + + const result = await usecases.getOrganizationLearnerWithMissionIdsByState({ + organizationLearnerId: organizationLearner.id, + }); + + expect(result).to.deep.equal( + new OrganizationLearner({ + ...organizationLearner, + division: organizationLearner.attributes['Libellé classe'], + completedMissionIds: [], + startedMissionIds: [], + features: ['ORALIZATION'], + }), + ); + }); + it('should return only the good organization learner', async function () { const organizationLearner = databaseBuilder.factory.prescription.organizationLearners.buildOndeOrganizationLearner(); diff --git a/api/tests/school/unit/application/organization-learner-controller_test.js b/api/tests/school/unit/application/organization-learner-controller_test.js index f9f2f4e0075..ae033d77eed 100644 --- a/api/tests/school/unit/application/organization-learner-controller_test.js +++ b/api/tests/school/unit/application/organization-learner-controller_test.js @@ -15,6 +15,7 @@ describe('Unit | Controller | organization-learner-controller', function () { organizationId: '345', completedMissionIds: ['rec12344', 'rec435'], startedMissionIds: undefined, + features: ['ORALIZATION'], }), ); const id = 4356; @@ -33,6 +34,7 @@ describe('Unit | Controller | organization-learner-controller', function () { 'display-name': undefined, division: 'CM2', 'organization-id': '345', + features: ['ORALIZATION'], }, id: '4356', type: 'organizationLearners', diff --git a/api/tests/school/unit/application/school-controller_test.js b/api/tests/school/unit/application/school-controller_test.js index a0c534c6905..214fa00e62a 100644 --- a/api/tests/school/unit/application/school-controller_test.js +++ b/api/tests/school/unit/application/school-controller_test.js @@ -53,6 +53,7 @@ describe('Unit | Controller | school-controller', function () { organizationId: 1, completedMissionIds: [], startedMissionIds: [], + features: [], }, ], }, diff --git a/api/tests/school/unit/infrastructure/serializers/organization-learner_test.js b/api/tests/school/unit/infrastructure/serializers/organization-learner_test.js index 3593c163257..28d4d0d99e8 100644 --- a/api/tests/school/unit/infrastructure/serializers/organization-learner_test.js +++ b/api/tests/school/unit/infrastructure/serializers/organization-learner_test.js @@ -25,6 +25,7 @@ describe('Unit | Serializer | JSONAPI | organization-learner', function () { 'organization-id': 'orga-1', 'completed-mission-ids': ['1'], 'started-mission-ids': ['2'], + features: [], }, }, }; diff --git a/junior/app/components/bubble.gjs b/junior/app/components/bubble.gjs index 73de8990b4d..69c72f805d7 100644 --- a/junior/app/components/bubble.gjs +++ b/junior/app/components/bubble.gjs @@ -1,5 +1,8 @@ import Component from '@glimmer/component'; import MarkdownToHtml from 'junior/components/markdown-to-html'; +import * as markdownConverter from 'junior/utils/markdown-converter'; + +import OralizationButton from './oralization-button'; export default class Bubble extends Component { get getClasses() { @@ -9,5 +12,19 @@ export default class Bubble extends Component { } return className; } - + + get textToRead() { + const parser = new DOMParser(); + const parsedText = parser.parseFromString(markdownConverter.toHTML(this.args.message), 'text/html').body.innerText; + return parsedText; + } + + } diff --git a/junior/app/components/challenge/item/integration-test.js b/junior/app/components/challenge/item/integration-test.js index 1e70acfcde0..c2b53527605 100644 --- a/junior/app/components/challenge/item/integration-test.js +++ b/junior/app/components/challenge/item/integration-test.js @@ -4,7 +4,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest, t } from '../../../helpers/tests'; -module('Integration | Component | challenge', function (hooks) { +module('Integration | Component | challenge item', function (hooks) { setupRenderingTest(hooks); test('displays embed', async function (assert) { this.set('challenge', { hasValidEmbedDocument: true, autoReply: true }); diff --git a/junior/app/components/challenge/template.hbs b/junior/app/components/challenge/template.hbs index 2f1a661e7e8..2cdee3e1201 100644 --- a/junior/app/components/challenge/template.hbs +++ b/junior/app/components/challenge/template.hbs @@ -2,16 +2,26 @@
{{#each @challenge.instruction as |instruction|}} - + {{/each}} {{#if (eq this.answer.result "ok")}} - + {{/if}} {{#if (eq this.answer.result "ko")}} - + {{/if}} {{#if this.displayValidationWarning}} - + {{/if}} { + this.isSpeaking = true; + }; + + utterance.onerror = () => { + this.isSpeaking = false; + }; + + utterance.onend = () => { + this.isSpeaking = false; + }; + + utterance.lang = 'fr-FR'; + utterance.pitch = 0.8; + utterance.rate = 0.8; + utterance.text = text; + + window.speechSynthesis.speak(utterance); + } + + +} diff --git a/junior/app/components/robot-dialog.gjs b/junior/app/components/robot-dialog.gjs index 732778c95e0..307bd2c32b6 100644 --- a/junior/app/components/robot-dialog.gjs +++ b/junior/app/components/robot-dialog.gjs @@ -1,21 +1,5 @@ -import { action } from '@ember/object'; import Component from '@glimmer/component'; -import * as markdownConverter from 'junior/utils/markdown-converter'; - export default class RobotDialog extends Component { - @action - readTheInstruction(text) { - if (!window.speechSynthesis.speaking) { - const utterance = new SpeechSynthesisUtterance(); - - const parser = new DOMParser(); - const parsedText = parser.parseFromString(markdownConverter.toHTML(text), 'text/html').body.innerText; - utterance.text = parsedText; - utterance.lang = 'fr-FR'; - - window.speechSynthesis.speak(utterance); - } - } get getRobotImageUrl() { return `/images/robot/dialog-robot-${this.args.class ? this.args.class : 'default'}.svg`; } @@ -29,12 +13,3 @@ export default class RobotDialog extends Component {
} - -// {{!-- {{! À activer quand le design + fonctionnalité sont actés }}--}} -// {{!-- {{! }}--}} -// {{!}} diff --git a/junior/app/models/organization-learner.js b/junior/app/models/organization-learner.js index 8016ed07801..298d60e4e21 100644 --- a/junior/app/models/organization-learner.js +++ b/junior/app/models/organization-learner.js @@ -7,4 +7,9 @@ export default class OrganizationLearner extends Model { @attr division; @attr completedMissionIds; @attr startedMissionIds; + @attr features; + + get hasOralizationFeature() { + return this.features?.includes('ORALIZATION'); + } } diff --git a/junior/app/routes/assessment/challenge.js b/junior/app/routes/assessment/challenge.js index b8be0590f0e..d546a840063 100644 --- a/junior/app/routes/assessment/challenge.js +++ b/junior/app/routes/assessment/challenge.js @@ -5,6 +5,7 @@ import { service } from '@ember/service'; export default class ChallengeRoute extends Route { @service router; @service store; + @service currentLearner; async model(params, transition) { const assessment = await this.modelFor('assessment'); @@ -20,7 +21,12 @@ export default class ChallengeRoute extends Route { }); } const activity = await this.store.queryRecord('activity', { assessmentId: assessment.id }); - return { assessment, challenge, activity }; + let oralization = false; + if (this.currentLearner.learner) { + const organizationLearner = await this.store.findRecord('organization-learner', this.currentLearner.learner.id); + oralization = organizationLearner.hasOralizationFeature; + } + return { assessment, challenge, activity, oralization }; } @action diff --git a/junior/app/routes/assessment/challenge_unit-test.js b/junior/app/routes/assessment/challenge_unit-test.js index 45ca42925b5..d2a1aceb278 100644 --- a/junior/app/routes/assessment/challenge_unit-test.js +++ b/junior/app/routes/assessment/challenge_unit-test.js @@ -28,16 +28,20 @@ module('Unit | Route | AssessmentChallengeRoute', function (hooks) { test('should call the assessment challenge route', async function (assert) { const store = this.owner.lookup('service:store'); const route = this.owner.lookup('route:assessment.challenge'); + const currentLearner = this.owner.lookup('service:currentLearner'); + sinon.stub(currentLearner, 'learner').value({ id: 156 }); const assessment = { id: 2, type: 'PIX1D_MISSION' }; const challenge = { id: 2 }; const activity = { id: 2 }; + const organizationLearner = store.createRecord('organization-learner', { features: ['ORALIZATION'] }); sinon.stub(route.router, 'replaceWith'); sinon.stub(route, 'modelFor').returns(assessment); sinon.stub(store, 'queryRecord').returns(challenge); + sinon.stub(store, 'findRecord').returns(organizationLearner); const result = await route.model(); - assert.deepEqual(result, { assessment, challenge, activity }); + assert.deepEqual(result, { assessment, challenge, activity, oralization: true }); }); }); diff --git a/junior/app/routes/identified/missions/mission/introduction.js b/junior/app/routes/identified/missions/mission/introduction.js index 93519f603fe..d2eaa7d4524 100644 --- a/junior/app/routes/identified/missions/mission/introduction.js +++ b/junior/app/routes/identified/missions/mission/introduction.js @@ -1,7 +1,14 @@ import Route from '@ember/routing/route'; +import { service } from '@ember/service'; export default class MissionIntroductionRoute extends Route { + @service currentLearner; + @service store; + async model() { - return this.modelFor('identified.missions.mission'); + const mission = this.modelFor('identified.missions.mission'); + const organizationLearner = await this.store.findRecord('organization-learner', this.currentLearner.learner.id); + const learnerHasOralizationFeature = organizationLearner.hasOralizationFeature; + return { mission, learnerHasOralizationFeature }; } } diff --git a/junior/app/styles/app.scss b/junior/app/styles/app.scss index 2a625c03e47..4a658431e90 100644 --- a/junior/app/styles/app.scss +++ b/junior/app/styles/app.scss @@ -20,6 +20,7 @@ @import 'components/bubble'; @import 'components/footer'; @import 'components/issue'; +@import 'components/oralization-button'; @import 'components/robot-dialog'; // Pages diff --git a/junior/app/styles/components/bubble.scss b/junior/app/styles/components/bubble.scss index bd088c2046f..c6fb81ad678 100644 --- a/junior/app/styles/components/bubble.scss +++ b/junior/app/styles/components/bubble.scss @@ -1,8 +1,13 @@ +.bubble-container { + display: flex; + align-items: center; +} + .bubble { position: relative; width: fit-content; height: fit-content; - margin: 8px 80px 8px 8px; + margin: 8px 16px 8px 8px; padding: 12px 24px; font-size: 1.5rem; text-align: left; diff --git a/junior/app/styles/components/oralization-button.scss b/junior/app/styles/components/oralization-button.scss new file mode 100644 index 00000000000..cb67ce835ec --- /dev/null +++ b/junior/app/styles/components/oralization-button.scss @@ -0,0 +1,23 @@ +.oralization-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 80px; + color: var(--pix-primary-500); + line-height: 0.7; + + button { + padding: 0; + + &:focus { + border: 2px solid var(--pix-primary-700); + } + } + + &--is-reading { + color: var(--pix-primary-700); + } +} + + diff --git a/junior/app/templates/assessment/challenge.hbs b/junior/app/templates/assessment/challenge.hbs index 507a51dc9fe..d88684865e8 100644 --- a/junior/app/templates/assessment/challenge.hbs +++ b/junior/app/templates/assessment/challenge.hbs @@ -1 +1,6 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/junior/app/templates/identified/missions/list.hbs b/junior/app/templates/identified/missions/list.hbs index a980a4ae48a..254d00c1e39 100644 --- a/junior/app/templates/identified/missions/list.hbs +++ b/junior/app/templates/identified/missions/list.hbs @@ -12,7 +12,6 @@
- {{#each this.orderedMissionList as |mission|}} {{#if (this.isMissionCompleted mission.id)}} diff --git a/junior/app/templates/identified/missions/mission/introduction.hbs b/junior/app/templates/identified/missions/mission/introduction.hbs index e78e6eef3bc..91d0a9afd97 100644 --- a/junior/app/templates/identified/missions/mission/introduction.hbs +++ b/junior/app/templates/identified/missions/mission/introduction.hbs @@ -2,21 +2,23 @@
- +

{{t "pages.missions.introduction-page.start-mission"}}

-
diff --git a/junior/public/images/icons/oralization-start.svg b/junior/public/images/icons/oralization-start.svg new file mode 100644 index 00000000000..848c75032cb --- /dev/null +++ b/junior/public/images/icons/oralization-start.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/junior/public/images/icons/oralization-stop.svg b/junior/public/images/icons/oralization-stop.svg new file mode 100644 index 00000000000..ce80f8ab63d --- /dev/null +++ b/junior/public/images/icons/oralization-stop.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/junior/tests/acceptance/display-challenge-test.js b/junior/tests/acceptance/display-challenge-test.js index 8c6698e7c06..0b24c47b013 100644 --- a/junior/tests/acceptance/display-challenge-test.js +++ b/junior/tests/acceptance/display-challenge-test.js @@ -2,6 +2,7 @@ import { visit } from '@1024pix/ember-testing-library'; import { module, test } from 'qunit'; import { setupApplicationTest, t } from '../helpers'; +import identifyLearner from '../helpers/identify-learner'; module('Acceptance | Challenge', function (hooks) { setupApplicationTest(hooks); @@ -28,6 +29,23 @@ module('Acceptance | Challenge', function (hooks) { assert.dom(screen.getByRole('button', { name: t('pages.challenge.actions.check') })).exists(); }); + test('Should display the oralization button if learner has feature enabled', async function (assert) { + const oragnizationLearner = this.server.create('organization-learner', { + features: ['ORALIZATION'], + }); + const assessment = this.server.create('assessment'); + this.server.create('challenge', { instruction: ['1ère instruction', '2ème instruction'] }); + this.server.create('activity', { assessmentId: assessment.id }); + + identifyLearner(this.owner, oragnizationLearner); + + // when + const screen = await visit(`/assessments/${assessment.id}/challenges`); + + // then + assert.strictEqual(screen.getAllByRole('button', { name: t('components.oralization-button.label') }).length, 2); + }); + test('do not display skip button when activity level is TUTORIAL', async function (assert) { const assessment = this.server.create('assessment'); const challenge = this.server.create('challenge', 'withInstruction'); diff --git a/junior/tests/acceptance/lesson-workflow-test.js b/junior/tests/acceptance/lesson-workflow-test.js index 1b0f704a215..6074d151406 100644 --- a/junior/tests/acceptance/lesson-workflow-test.js +++ b/junior/tests/acceptance/lesson-workflow-test.js @@ -1,5 +1,4 @@ import { visit } from '@1024pix/ember-testing-library'; -// import { click } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest, t } from '../helpers'; diff --git a/junior/tests/integration/bubble_test.gjs b/junior/tests/integration/bubble_test.gjs index 352756e5baf..ed38ffff170 100644 --- a/junior/tests/integration/bubble_test.gjs +++ b/junior/tests/integration/bubble_test.gjs @@ -15,4 +15,14 @@ module('Integration | Component | Bubble', function (hooks) { await render(); assert.dom('.bubble--success').exists(); }); + + test('displays bubble with oralization button', async function (assert) { + await render(); + assert.dom('.oralization-container').exists(); + }); + + test('displays bubble without oralization button', async function (assert) { + await render(); + assert.dom('.oralization-container').doesNotExist(); + }); }); diff --git a/junior/tests/integration/challenge_test.js b/junior/tests/integration/challenge_test.js new file mode 100644 index 00000000000..7e0f3bb5097 --- /dev/null +++ b/junior/tests/integration/challenge_test.js @@ -0,0 +1,32 @@ +import { render } from '@1024pix/ember-testing-library'; +import { hbs } from 'ember-cli-htmlbars'; +import { t } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'junior/helpers/tests'; +import { module, test } from 'qunit'; + +module('Integration | Component | Challenge', function (hooks) { + setupRenderingTest(hooks); + + module('if learner has oralization feature', function () { + test('should display oralization buttons on instruction bubbles', async function (assert) { + this.set('challenge', { instruction: ['1ère instruction', '2ème instruction'] }); + const screen = await render(hbs``); + + assert.strictEqual(screen.getAllByText(t('components.oralization-button.play')).length, 2); + }); + }); + module('if learner has not oralization feature', function () { + test('should not display oralization buttons', async function (assert) { + const store = this.owner.lookup('service:store'); + this.set('organizationLearner', store.createRecord('organization-learner', { features: [] })); + this.set('challenge', { instruction: ['1ère instruction', '2ème instruction'] }); + const screen = await render(hbs``); + + assert.dom(screen.queryByText(t('components.oralization-button.play'))).doesNotExist(); + }); + }); +}); diff --git a/junior/translations/fr.json b/junior/translations/fr.json index ad682feafb0..0e770f64a3f 100644 --- a/junior/translations/fr.json +++ b/junior/translations/fr.json @@ -8,6 +8,13 @@ "student-data-protection-policy-url": "https://pix.fr/politique-protection-donnees-personnelles-app-eleves" } }, + "components": { + "oralization-button": { + "play": "J'écoute", + "stop": "Stop", + "label": "Lire la consigne à haute voix" + } + }, "pages": { "pix-junior": "Pix Junior", "challenge": { diff --git a/orga/app/components/layout/sidebar.hbs b/orga/app/components/layout/sidebar.hbs index 42555f7d115..f10a5a9a5d7 100644 --- a/orga/app/components/layout/sidebar.hbs +++ b/orga/app/components/layout/sidebar.hbs @@ -31,14 +31,12 @@ {{t "navigation.main.missions"}} {{/if}} - {{#if this.shouldDisplayParticipantsEntry}} - - - - - {{t this.organizationLearnersList.label}} - - {{/if}} + + + + + {{t this.organizationLearnersList.label}} + diff --git a/orga/app/components/layout/sidebar.js b/orga/app/components/layout/sidebar.js index e9c9cea1ca7..95c55b9d3b7 100644 --- a/orga/app/components/layout/sidebar.js +++ b/orga/app/components/layout/sidebar.js @@ -33,10 +33,6 @@ export default class SidebarMenu extends Component { return this.currentUser.canAccessCampaignsPage; } - get shouldDisplayParticipantsEntry() { - return this.currentUser.canAccessParticipantsPage; - } - get organizationLearnersList() { if (this.currentUser.isSCOManagingStudents) { return { diff --git a/orga/app/services/current-user.js b/orga/app/services/current-user.js index 91af3b39402..61011aed9ff 100644 --- a/orga/app/services/current-user.js +++ b/orga/app/services/current-user.js @@ -84,10 +84,6 @@ export default class CurrentUserService extends Service { return !this.prescriber.missionsManagement; } - get canAccessParticipantsPage() { - return !this.prescriber.missionsManagement; - } - get hasLearnerImportFeature() { return this.prescriber.hasOrganizationLearnerImport; } diff --git a/orga/tests/integration/components/layout/sidebar-test.js b/orga/tests/integration/components/layout/sidebar-test.js index b6c9af9ed5f..29850fec3ef 100644 --- a/orga/tests/integration/components/layout/sidebar-test.js +++ b/orga/tests/integration/components/layout/sidebar-test.js @@ -56,7 +56,6 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { class CurrentUserStub extends Service { organization = Object.create({ id: '1' }); canAccessCampaignsPage = true; - canAccessParticipantsPage = true; } this.owner.register('service:current-user', CurrentUserStub); const intl = this.owner.lookup('service:intl'); @@ -125,7 +124,6 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { // given class CurrentUserStub extends Service { organization = Object.create({ id: '1', type: 'PRO' }); - canAccessParticipantsPage = true; } this.owner.register('service:current-user', CurrentUserStub); @@ -146,7 +144,6 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { class CurrentUserStub extends Service { organization = Object.create({ id: '1', type: 'SUP' }); isSUPManagingStudents = true; - canAccessParticipantsPage = true; } this.owner.register('service:current-user', CurrentUserStub); @@ -165,7 +162,6 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { class CurrentUserStub extends Service { organization = Object.create({ id: '1', type: 'SUP' }); isSUPManagingStudents = false; - canAccessParticipantsPage = true; } this.owner.register('service:current-user', CurrentUserStub); @@ -187,7 +183,6 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { organization = Object.create({ id: '1', type: 'SCO' }); isSCOManagingStudents = true; canAccessMissionsPage = false; - canAccessParticipantsPage = true; } this.owner.register('service:current-user', CurrentUserStub); @@ -206,7 +201,6 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { class CurrentUserStub extends Service { organization = Object.create({ id: '1', type: 'SCO' }); isSCOManagingStudents = false; - canAccessParticipantsPage = true; } this.owner.register('service:current-user', CurrentUserStub); @@ -306,7 +300,7 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { ); }); - test('should not display Campagne and Participants menus', async function (assert) { + test('should not display Campagne', async function (assert) { class CurrentUserStub extends Service { organization = Object.create({ id: 5 }); canAccessMissionsPage = true; @@ -318,9 +312,7 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { const screen = await render(hbs``); assert.dom(screen.queryByText('Campagnes')).doesNotExist(); - assert.dom(screen.queryByText('Participants')).doesNotExist(); assert.dom(screen.queryByText('Étudiants')).doesNotExist(); - assert.dom(screen.queryByText('Élèves')).doesNotExist(); }); }); diff --git a/orga/tests/unit/services/current-user-test.js b/orga/tests/unit/services/current-user-test.js index b49583a4b8b..17648073253 100644 --- a/orga/tests/unit/services/current-user-test.js +++ b/orga/tests/unit/services/current-user-test.js @@ -338,24 +338,6 @@ module('Unit | Service | current-user', function (hooks) { }); }); - module('#canAccessParticipantsPage', function () { - test('should return false if user has mission feature activated', function (assert) { - currentUserService.prescriber = { - missionsManagement: true, - }; - - assert.false(currentUserService.canAccessParticipantsPage); - }); - - test('should return true if user does not have missions feature activated', function (assert) { - currentUserService.prescriber = { - missionsManagement: false, - }; - - assert.true(currentUserService.canAccessParticipantsPage); - }); - }); - module('#canAccessImportPage', function (hooks) { hooks.beforeEach(function () { currentUserService.prescriber = { hasOrganizationLearnerImport: false };