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": {