From f844fc0d2472c77269376ea9b820317002d5caa4 Mon Sep 17 00:00:00 2001 From: Alexandre COIN Date: Tue, 13 Aug 2024 10:52:34 +0200 Subject: [PATCH] :recycle: certif: refacto login form to gjs --- certif/app/components/language-switcher.gjs | 43 ++++++ certif/app/components/language-switcher.hbs | 12 -- certif/app/components/language-switcher.js | 35 ----- certif/app/components/login-form.hbs | 67 --------- certif/app/components/login/form.gjs | 44 ++++++ certif/app/components/login/header.gjs | 21 +++ .../{login-form.js => login/index.gjs} | 26 +++- certif/app/services/locale.js | 4 + certif/app/templates/login.hbs | 2 +- .../auth/toggable-login-form-test.js | 1 + .../components/language-switcher-test.gjs | 139 ++++++++++++++++++ .../components/language-switcher-test.js | 60 -------- .../{login-form-test.js => login-test.gjs} | 48 ++++-- .../unit/components/language-switcher-test.js | 111 -------------- .../tests/unit/components/login-form-test.js | 41 ------ 15 files changed, 314 insertions(+), 340 deletions(-) create mode 100644 certif/app/components/language-switcher.gjs delete mode 100644 certif/app/components/language-switcher.hbs delete mode 100644 certif/app/components/language-switcher.js delete mode 100644 certif/app/components/login-form.hbs create mode 100644 certif/app/components/login/form.gjs create mode 100644 certif/app/components/login/header.gjs rename certif/app/components/{login-form.js => login/index.gjs} (79%) create mode 100644 certif/tests/integration/components/language-switcher-test.gjs delete mode 100644 certif/tests/integration/components/language-switcher-test.js rename certif/tests/integration/components/{login-form-test.js => login-test.gjs} (80%) delete mode 100644 certif/tests/unit/components/language-switcher-test.js delete mode 100644 certif/tests/unit/components/login-form-test.js diff --git a/certif/app/components/language-switcher.gjs b/certif/app/components/language-switcher.gjs new file mode 100644 index 00000000000..b198cdb229d --- /dev/null +++ b/certif/app/components/language-switcher.gjs @@ -0,0 +1,43 @@ +import PixSelect from '@1024pix/pix-ui/components/pix-select'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { t } from 'ember-intl'; +import { FRENCH_INTERNATIONAL_LOCALE } from 'pix-certif/services/locale'; + +export default class LanguageSwitcher extends Component { + @service locale; + + get alphabeticallySortedLanguages() { + const availableLanguages = this.locale.getAvailableLanguages(); + + const options = Object.entries(availableLanguages) + .filter(([_, config]) => config.languageSwitcherDisplayed) + .map(([key, config]) => ({ + label: config.value, + value: key, + })); + + const optionsWithoutFrSortedByLabel = options + .filter((option) => option.value !== FRENCH_INTERNATIONAL_LOCALE) + .sort((option) => option.label); + + const frenchLanguageOption = options.find((option) => option.value === FRENCH_INTERNATIONAL_LOCALE); + + return [frenchLanguageOption, ...optionsWithoutFrSortedByLabel]; + } + + +} diff --git a/certif/app/components/language-switcher.hbs b/certif/app/components/language-switcher.hbs deleted file mode 100644 index 43cd7007954..00000000000 --- a/certif/app/components/language-switcher.hbs +++ /dev/null @@ -1,12 +0,0 @@ - - <:label>{{t "common.forms.login.choose-language-aria-label"}} - \ No newline at end of file diff --git a/certif/app/components/language-switcher.js b/certif/app/components/language-switcher.js deleted file mode 100644 index 597877eb663..00000000000 --- a/certif/app/components/language-switcher.js +++ /dev/null @@ -1,35 +0,0 @@ -import { action } from '@ember/object'; -import Component from '@glimmer/component'; -import languages from 'pix-certif/languages'; - -const FRENCH_LANGUAGE = 'fr'; - -export default class LanguageSwitcher extends Component { - availableLanguages = this.mapToOptions(languages); - - get selectedLanguage() { - return this.args.selectedLanguage; - } - - @action - onChange(value) { - this.args.onLanguageChange(value); - } - - mapToOptions(languages) { - const options = Object.entries(languages) - .filter(([_, config]) => config.languageSwitcherDisplayed) - .map(([key, config]) => ({ - label: config.value, - value: key, - })); - - const optionsWithoutFrSortedByLabel = options - .filter((option) => option.value !== FRENCH_LANGUAGE) - .sort((option) => option.label); - - const frenchLanguageOption = options.find((option) => option.value === FRENCH_LANGUAGE); - - return [frenchLanguageOption, ...optionsWithoutFrSortedByLabel]; - } -} diff --git a/certif/app/components/login-form.hbs b/certif/app/components/login-form.hbs deleted file mode 100644 index c0b9d6bb11c..00000000000 --- a/certif/app/components/login-form.hbs +++ /dev/null @@ -1,67 +0,0 @@ -
- - - -
- - -
-
\ No newline at end of file diff --git a/certif/app/components/login/form.gjs b/certif/app/components/login/form.gjs new file mode 100644 index 00000000000..81aa15f4c96 --- /dev/null +++ b/certif/app/components/login/form.gjs @@ -0,0 +1,44 @@ +import PixButton from '@1024pix/pix-ui/components/pix-button'; +import PixInput from '@1024pix/pix-ui/components/pix-input'; +import PixInputPassword from '@1024pix/pix-ui/components/pix-input-password'; +import { on } from '@ember/modifier'; +import { t } from 'ember-intl'; + + diff --git a/certif/app/components/login/header.gjs b/certif/app/components/login/header.gjs new file mode 100644 index 00000000000..e249e0d1354 --- /dev/null +++ b/certif/app/components/login/header.gjs @@ -0,0 +1,21 @@ +import { t } from 'ember-intl'; + + diff --git a/certif/app/components/login-form.js b/certif/app/components/login/index.gjs similarity index 79% rename from certif/app/components/login-form.js rename to certif/app/components/login/index.gjs index 91b24477970..3e046764679 100644 --- a/certif/app/components/login-form.js +++ b/certif/app/components/login/index.gjs @@ -5,7 +5,10 @@ import { tracked } from '@glimmer/tracking'; import get from 'lodash/get'; import ENV from 'pix-certif/config/environment'; -export default class LoginForm extends Component { +import LoginForm from './form'; +import LoginHeader from './header'; + +export default class Login extends Component { @service url; @service intl; @service session; @@ -87,4 +90,25 @@ export default class LoginForm extends Component { return ENV.APP.API_ERROR_MESSAGES.INTERNAL_SERVER_ERROR.I18N_KEY; } } + + } diff --git a/certif/app/services/locale.js b/certif/app/services/locale.js index 8676d17bf5c..1f08ba8b732 100644 --- a/certif/app/services/locale.js +++ b/certif/app/services/locale.js @@ -16,6 +16,10 @@ export default class LocaleService extends Service { @service intl; @service dayjs; + getAvailableLanguages() { + return languages; + } + handleUnsupportedLanguage(language) { if (!language) return; return this.isLanguageSupported(language) ? language : DEFAULT_LOCALE; diff --git a/certif/app/templates/login.hbs b/certif/app/templates/login.hbs index 5af4c986c7b..65b040d4c61 100644 --- a/certif/app/templates/login.hbs +++ b/certif/app/templates/login.hbs @@ -1,7 +1,7 @@ {{page-title (t "pages.login.title")}}
- diff --git a/certif/tests/integration/components/auth/toggable-login-form-test.js b/certif/tests/integration/components/auth/toggable-login-form-test.js index 4d69303aee2..8f30634aba7 100644 --- a/certif/tests/integration/components/auth/toggable-login-form-test.js +++ b/certif/tests/integration/components/auth/toggable-login-form-test.js @@ -34,6 +34,7 @@ module('Integration | Component | Auth::ToggableLoginForm', function (hooks) { // then assert.dom(screen.getByRole('textbox', { name: emailInputLabel })).exists(); assert.dom(screen.getByLabelText(passwordInputLabel)).exists(); + assert.dom(screen.getByText(loginLabel)).exists(); }); test('[a11y] it should display a message that all inputs are required', async function (assert) { diff --git a/certif/tests/integration/components/language-switcher-test.gjs b/certif/tests/integration/components/language-switcher-test.gjs new file mode 100644 index 00000000000..0074ef57809 --- /dev/null +++ b/certif/tests/integration/components/language-switcher-test.gjs @@ -0,0 +1,139 @@ +import { render } from '@1024pix/ember-testing-library'; +import { click } from '@ember/test-helpers'; +import { t } from 'ember-intl/test-support'; +import LanguageSwitcher from 'pix-certif/components/language-switcher'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +import setupIntlRenderingTest from '../../helpers/setup-intl-rendering'; + +module('Integration | Component | Language Switcher', function (hooks) { + setupIntlRenderingTest(hooks); + + let localeService; + + let availableLanguages = { + en: { + value: 'English', + languageSwitcherDisplayed: true, + }, + fr: { + value: 'Français', + languageSwitcherDisplayed: true, + }, + el: { + value: 'Primitive Eldarìn', + languageSwitcherDisplayed: true, + }, + si: { + value: 'Sindarìn', + languageSwitcherDisplayed: true, + }, + }; + + hooks.beforeEach(function () { + localeService = this.owner.lookup('service:locale'); + }); + + module('when component renders', function () { + test('displays a button with default option selected', async function (assert) { + // given + sinon.stub(localeService, 'getAvailableLanguages').returns(availableLanguages); + + // when + const screen = await render(); + + // then + const selectALanguage = t('common.forms.login.choose-language-aria-label'); + + assert.dom(screen.getByRole('button', { name: selectALanguage })).includesText('English'); + }); + }); + + module('when component is clicked', function () { + test('displays a sorted list of available languages with french language first', async function (assert) { + // given + sinon.stub(localeService, 'getAvailableLanguages').returns(availableLanguages); + + // when + const screen = await render(); + + const selectALanguage = t('common.forms.login.choose-language-aria-label'); + + await click(screen.getByRole('button', { name: selectALanguage })); + await screen.findByRole('listbox'); + + // then + const options = await screen.findAllByRole('option'); + assert.dom(screen.getByRole('option', { name: 'English' })).exists(); + const optionsInnerText = options.map((option) => { + return option.innerText; + }); + + assert.deepEqual(optionsInnerText, ['Français', 'English', 'Primitive Eldarìn', 'Sindarìn']); + }); + + test(`displays all languages with "languageSwitcherDisplayed" attribute at true`, async function (assert) { + // given + availableLanguages = { + en: { + value: 'English', + languageSwitcherDisplayed: true, + }, + fr: { + value: 'Français', + languageSwitcherDisplayed: true, + }, + el: { + value: 'Primitive Eldarìn', + languageSwitcherDisplayed: false, + }, + si: { + value: 'Sindarìn', + languageSwitcherDisplayed: true, + }, + }; + + sinon.stub(localeService, 'getAvailableLanguages').returns(availableLanguages); + + const screen = await render(); + + // when + const selectALanguage = t('common.forms.login.choose-language-aria-label'); + + await click(screen.getByRole('button', { name: selectALanguage })); + await screen.findByRole('listbox'); + + // then + const options = await screen.findAllByRole('option'); + assert.dom(screen.getByRole('option', { name: 'English' })).exists(); + const optionsInnerText = options.map((option) => { + return option.innerText; + }); + + assert.deepEqual(optionsInnerText, ['Français', 'English', 'Sindarìn']); + }); + }); + + module('when a language is selected', function () { + test('should display correct language', async function (assert) { + // given + const onLanguageChangeStub = sinon.stub(); + sinon.stub(localeService, 'getAvailableLanguages').returns(availableLanguages); + + // when + const screen = await render( + , + ); + + const selectALanguage = t('common.forms.login.choose-language-aria-label'); + + await click(screen.getByRole('button', { name: selectALanguage })); + await screen.findByRole('listbox'); + await click(screen.getByRole('option', { name: 'Français' })); + + // then + assert.ok(onLanguageChangeStub.calledWithExactly('fr')); + }); + }); +}); diff --git a/certif/tests/integration/components/language-switcher-test.js b/certif/tests/integration/components/language-switcher-test.js deleted file mode 100644 index f0d77a3a287..00000000000 --- a/certif/tests/integration/components/language-switcher-test.js +++ /dev/null @@ -1,60 +0,0 @@ -import { render } from '@1024pix/ember-testing-library'; -import { click } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { module, test } from 'qunit'; -import sinon from 'sinon'; - -import setupIntlRenderingTest from '../../helpers/setup-intl-rendering'; - -module('Integration | Component | Language Switcher', function (hooks) { - setupIntlRenderingTest(hooks); - - module('when component renders', function () { - test('displays a button with default option selected', async function (assert) { - // when - const screen = await render(hbs``); - - // then - assert.dom(screen.getByRole('button', { name: 'Sélectionnez une langue' })).exists(); - }); - }); - - module('when component is clicked', function () { - test('displays a list of available languages with french language first', async function (assert) { - // given - const screen = await render(hbs``); - - // when - await click(screen.getByRole('button', { name: 'Sélectionnez une langue' })); - await screen.findByRole('listbox'); - - // then - const options = await screen.findAllByRole('option'); - assert.dom(screen.getByRole('option', { name: 'English' })).exists(); - const optionsInnerText = options.map((option) => { - return option.innerText; - }); - - assert.deepEqual(optionsInnerText, ['Français', 'English']); - }); - }); - - module('when a language is selected', function () { - test('calls onLanguageChange callback', async function (assert) { - // given - const onLanguageChangeStub = sinon.stub(); - this.set('onLanguageChange', onLanguageChangeStub); - const screen = await render( - hbs``, - ); - - // when - await click(screen.getByRole('button', { name: 'Sélectionnez une langue' })); - await screen.findByRole('listbox'); - await click(screen.getByRole('option', { name: 'Français' })); - - // then - assert.ok(onLanguageChangeStub.calledWithExactly('fr')); - }); - }); -}); diff --git a/certif/tests/integration/components/login-form-test.js b/certif/tests/integration/components/login-test.gjs similarity index 80% rename from certif/tests/integration/components/login-form-test.js rename to certif/tests/integration/components/login-test.gjs index fcaf65ed9b5..519f6f117dd 100644 --- a/certif/tests/integration/components/login-form-test.js +++ b/certif/tests/integration/components/login-test.gjs @@ -1,8 +1,8 @@ -import { render as renderScreen } from '@1024pix/ember-testing-library'; +import { render } from '@1024pix/ember-testing-library'; import Service from '@ember/service'; import { click, fillIn } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; import { t } from 'ember-intl/test-support'; +import Login from 'pix-certif/components/login'; import ENV from 'pix-certif/config/environment'; import { module, test } from 'qunit'; import { reject, resolve } from 'rsvp'; @@ -11,7 +11,7 @@ import sinon from 'sinon'; import setupIntlRenderingTest from '../../helpers/setup-intl-rendering'; const ApiErrorMessages = ENV.APP.API_ERROR_MESSAGES; -module('Integration | Component | login-form', function (hooks) { +module('Integration | Component | login', function (hooks) { setupIntlRenderingTest(hooks); let sessionStub; @@ -26,7 +26,7 @@ module('Integration | Component | login-form', function (hooks) { test('it should display login form', async function (assert) { // when - const screen = await renderScreen(hbs``); + const screen = await render(); // then assert.dom(screen.getByRole('img', { name: 'Pix Certif' })).exists(); @@ -46,7 +46,7 @@ module('Integration | Component | login-form', function (hooks) { return resolve(); }); const sessionServiceObserver = this.owner.lookup('service:session'); - const screen = await renderScreen(hbs``); + const screen = await render(); await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail' }), 'pix@example.net'); await fillIn(screen.getByLabelText('Mot de passe'), 'JeMeLoggue1024'); @@ -76,7 +76,7 @@ module('Integration | Component | login-form', function (hooks) { }; sessionStub.authenticate.callsFake(() => reject(invalidCredentialsErrorMessage)); - const screen = await renderScreen(hbs``); + const screen = await render(); await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail' }), 'pix@example.net'); await fillIn(screen.getByLabelText('Mot de passe'), 'Mauvais mot de passe'); @@ -87,6 +87,30 @@ module('Integration | Component | login-form', function (hooks) { assert.dom(screen.getByText(t(ApiErrorMessages.LOGIN_UNAUTHORIZED.I18N_KEY))).exists(); }); + test('should authenticate user with trimmed email', async function (assert) { + // given + sessionStub.authenticate.callsFake(function (authenticator, email, password, scope) { + this.authenticator = authenticator; + this.email = email; + this.password = password; + this.scope = scope; + return resolve(); + }); + const sessionServiceObserver = this.owner.lookup('service:session'); + const screen = await render(); + await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail' }), ' email@example.net '); + await fillIn(screen.getByLabelText('Mot de passe'), 'JeMeLoggue1024'); + + // when + await click(screen.getByRole('button', { name: 'Je me connecte' })); + + // then + assert.strictEqual(sessionServiceObserver.authenticator, 'authenticator:oauth2'); + assert.strictEqual(sessionServiceObserver.email, 'email@example.net'); + assert.strictEqual(sessionServiceObserver.password, 'JeMeLoggue1024'); + assert.strictEqual(sessionServiceObserver.scope, 'pix-certif'); + }); + test('it displays a should change password message', async function (assert) { // given const errorResponse = { @@ -98,7 +122,7 @@ module('Integration | Component | login-form', function (hooks) { service.currentDomain = { getExtension: sinon.stub().returns('fr') }; sessionStub.authenticate.callsFake(() => reject(errorResponse)); - const screen = await renderScreen(hbs``); + const screen = await render(); await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail' }), 'pix@example.net'); await fillIn(screen.getByLabelText('Mot de passe'), 'Mauvais mot de passe'); @@ -139,7 +163,7 @@ module('Integration | Component | login-form', function (hooks) { }; sessionStub.authenticate.callsFake(() => reject(notLinkedToOrganizationErrorMessage)); - const screen = await renderScreen(hbs``); + const screen = await render(); await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail' }), 'pix@example.net'); await fillIn(screen.getByLabelText('Mot de passe'), 'JeMeLoggue1024'); @@ -166,7 +190,7 @@ module('Integration | Component | login-form', function (hooks) { }; sessionStub.authenticate.callsFake(() => reject(gatewayTimeoutErrorMessage)); - const screen = await renderScreen(hbs``); + const screen = await render(); await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail' }), 'pix@example.net'); await fillIn(screen.getByLabelText('Mot de passe'), 'JeMeLoggue1024'); @@ -185,7 +209,7 @@ module('Integration | Component | login-form', function (hooks) { }; sessionStub.authenticate.callsFake(() => reject(msgErrorNotLinkedCertification)); - const screen = await renderScreen(hbs``); + const screen = await render(); await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail' }), 'pix@example.net'); await fillIn(screen.getByLabelText('Mot de passe'), 'JeMeLoggue1024'); @@ -199,7 +223,7 @@ module('Integration | Component | login-form', function (hooks) { module('when an invitation is cancelled', function () { test('it should display an error message', async function (assert) { // given & when - const screen = await renderScreen(hbs``); + const screen = await render(); // then assert @@ -217,7 +241,7 @@ module('Integration | Component | login-form', function (hooks) { module('when an invitation has already been accepted', function () { test('it should display an error message', async function (assert) { // given & when - const screen = await renderScreen(hbs``); + const screen = await render(); // then assert diff --git a/certif/tests/unit/components/language-switcher-test.js b/certif/tests/unit/components/language-switcher-test.js deleted file mode 100644 index 7525efa9620..00000000000 --- a/certif/tests/unit/components/language-switcher-test.js +++ /dev/null @@ -1,111 +0,0 @@ -import { setupTest } from 'ember-qunit'; -import { module, test } from 'qunit'; - -import createGlimmerComponent from '../../helpers/create-glimmer-component'; - -module('Unit | Component | language-switcher', function (hooks) { - setupTest(hooks); - - let component; - - hooks.beforeEach(function () { - component = createGlimmerComponent('component:language-switcher'); - }); - - module('#mapToOptions', function () { - test('sorts languages with french international language first', function (assert) { - // given - const languages = { - en: { - value: 'English', - languageSwitcherDisplayed: true, - }, - fr: { - value: 'Français', - languageSwitcherDisplayed: true, - }, - }; - - // when - const result = component.mapToOptions(languages); - - // then - const expectedResult = { - label: 'Français', - value: 'fr', - }; - - assert.deepEqual(result[0], expectedResult); - }); - - test('sorts other languages by "value"', function (assert) { - // given - const languages = { - en: { - value: 'English', - languageSwitcherDisplayed: true, - }, - es: { - value: 'Spanish', - languageSwitcherDisplayed: true, - }, - fr: { - value: 'Français', - languageSwitcherDisplayed: true, - }, - }; - - // when - const result = component.mapToOptions(languages); - - // then - const expectedResult = [ - { - label: 'Français', - value: 'fr', - }, - { - label: 'English', - value: 'en', - }, - { - label: 'Spanish', - value: 'es', - }, - ]; - assert.deepEqual(result, expectedResult); - }); - - module('when languages have attribute "languageSwitcherDisplayed" at "false"', function () { - test(`does not map these languages`, function (assert) { - // given - const languages = { - fr: { - value: 'Français', - languageSwitcherDisplayed: true, - }, - en: { - value: 'English', - languageSwitcherDisplayed: false, - }, - es: { - value: 'Spanish', - languageSwitcherDisplayed: false, - }, - }; - - // when - const result = component.mapToOptions(languages); - - // then - const expectedResult = [ - { - label: 'Français', - value: 'fr', - }, - ]; - assert.deepEqual(result, expectedResult); - }); - }); - }); -}); diff --git a/certif/tests/unit/components/login-form-test.js b/certif/tests/unit/components/login-form-test.js deleted file mode 100644 index ef7fc688ce5..00000000000 --- a/certif/tests/unit/components/login-form-test.js +++ /dev/null @@ -1,41 +0,0 @@ -import Service from '@ember/service'; -import { setupTest } from 'ember-qunit'; -import { module, test } from 'qunit'; -import sinon from 'sinon'; - -import createGlimmerComponent from '../../helpers/create-glimmer-component'; - -module('Unit | Component | login-form', (hooks) => { - setupTest(hooks); - - const authenticateStub = sinon.stub().resolves(); - const expectedAuthenticator = 'authenticator:oauth2'; - const eventStub = { preventDefault: sinon.stub().returns() }; - - let component; - - hooks.beforeEach(function () { - const sessionStub = Service.create({ - authenticate: authenticateStub, - }); - - component = createGlimmerComponent('component:login-form'); - component.session = sessionStub; - }); - - module('#authenticate', () => { - test('should authenticate user with trimmed email', async function (assert) { - // given - const emailWithSpaces = ' email@example.net '; - component.email = emailWithSpaces; - - const expectedEmail = emailWithSpaces.trim(); - - // when - await component.authenticate(eventStub); - - // then - assert.ok(authenticateStub.calledWith(expectedAuthenticator, expectedEmail, sinon.match.any, sinon.match.any)); - }); - }); -});