diff --git a/mon-pix/app/components/authentication/password-checklist/password-checklist-input.gjs b/mon-pix/app/components/authentication/password-checklist/password-checklist-input.gjs new file mode 100644 index 00000000000..1d7b0d6b758 --- /dev/null +++ b/mon-pix/app/components/authentication/password-checklist/password-checklist-input.gjs @@ -0,0 +1,103 @@ +import PixInputPassword from '@1024pix/pix-ui/components/pix-input-password'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { t } from 'ember-intl'; + +import PasswordChecklist from './password-checklist'; + +const CONTAINS_UPPERCASE_KEY = 'containsUppercase'; +const CONTAINS_LOWERCASE_KEY = 'containsLowercase'; +const CONTAINS_DIGIT_KEY = 'containsDigit'; +const MIN_LENGTH_KEY = 'minLength'; + +export default class PasswordChecklistInput extends Component { + @service intl; + + @tracked errors = []; + @tracked validationStatus = 'default'; + @tracked value = ''; + + get hasValidationStatusError() { + return this.validationStatus === 'error'; + } + + get rules() { + return [ + { + key: MIN_LENGTH_KEY, + description: this.intl.t('components.authentication.password-checklist.rules.min-length'), + }, + { + key: CONTAINS_UPPERCASE_KEY, + description: this.intl.t('components.authentication.password-checklist.rules.contains-uppercase'), + }, + { + key: CONTAINS_LOWERCASE_KEY, + description: this.intl.t('components.authentication.password-checklist.rules.contains-lowercase'), + }, + { + key: CONTAINS_DIGIT_KEY, + description: this.intl.t('components.authentication.password-checklist.rules.contains-digit'), + }, + ]; + } + + handleValidation() { + const errors = []; + const hasDigit = (value) => /\d/.test(value); + const hasLowercase = (value) => /[a-z]/.test(value); + const hasUppercase = (value) => /[A-Z]/.test(value); + const hasMinLength = (value) => value.length && value.length >= 8; + const value = this.value; + + if (!hasDigit(value)) errors.push(CONTAINS_DIGIT_KEY); + if (!hasLowercase(value)) errors.push(CONTAINS_LOWERCASE_KEY); + if (!hasUppercase(value)) errors.push(CONTAINS_UPPERCASE_KEY); + if (!hasMinLength(value)) errors.push(MIN_LENGTH_KEY); + + return errors; + } + + @action + handleUpdatedPassword(event) { + this.value = event.target.value; + this.errors = this.handleValidation(); + + if (this.value && this.errors.length === 0) { + this.validationStatus = 'success'; + } + + if (this.args.onInput) { + this.args.onInput(event); + } + } + + @action + checkValidity() { + if (!this.value || this.errors.length > 0) { + this.validationStatus = 'error'; + } + } + + +} diff --git a/mon-pix/tests/integration/components/authentication/password-checklist/password-checklist-input-test.gjs b/mon-pix/tests/integration/components/authentication/password-checklist/password-checklist-input-test.gjs new file mode 100644 index 00000000000..df985dce45c --- /dev/null +++ b/mon-pix/tests/integration/components/authentication/password-checklist/password-checklist-input-test.gjs @@ -0,0 +1,122 @@ +import { fillByLabel, render } from '@1024pix/ember-testing-library'; +import { on } from '@ember/modifier'; +import { blur } from '@ember/test-helpers'; +import { t } from 'ember-intl/test-support'; +import PasswordChecklistInput from 'mon-pix/components/authentication/password-checklist/password-checklist-input'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +import setupIntlRenderingTest from '../../../../helpers/setup-intl-rendering'; + +const I18N = { + PASSWORD_INPUT_LABEL: 'pages.sign-in.fields.password.label', + RULES_STATUS_MESSAGE: 'components.authentication.password-checklist.rules.completed-message', + ERROR_MESSAGE: 'components.authentication.password-checklist.error-message', +}; + +module('Integration | Component | authentication | password-checklist-input', function (hooks) { + setupIntlRenderingTest(hooks); + + module('password rules', function () { + test('it respects all rules', async function (assert) { + // given + const password = 'Pix12345'; + const onInput = sinon.spy(); + + const screen = await render(); + + // when + await fillByLabel(t(I18N.PASSWORD_INPUT_LABEL), password); + const passwordInputElement = screen.getByLabelText(t(I18N.PASSWORD_INPUT_LABEL)); + await blur(passwordInputElement); + + // then + const onInputEvent = onInput.firstCall.args[0]; + assert.strictEqual(onInputEvent.target.value, password); + + assert.dom(passwordInputElement).doesNotHaveAttribute('aria-invalid'); + + const rulesStatusMessage = t(I18N.RULES_STATUS_MESSAGE, { rulesCompleted: 4, rulesNumber: 4 }); + assert.dom(screen.getByText(rulesStatusMessage)).exists(); + }); + + test('it does not respect any rules', async function (assert) { + // given + const password = ' '; + const onInput = sinon.stub(); + + const screen = await render(); + + // when + await fillByLabel(t(I18N.PASSWORD_INPUT_LABEL), password); + const passwordInputElement = screen.getByLabelText(t(I18N.PASSWORD_INPUT_LABEL)); + await blur(passwordInputElement); + + // then + assert.dom(passwordInputElement).hasAttribute('aria-invalid'); + + const rulesStatusMessage = t(I18N.RULES_STATUS_MESSAGE, { rulesCompleted: 0, rulesNumber: 4 }); + assert.dom(screen.getByText(rulesStatusMessage)).exists(); + }); + + test('it must have a minimum length of 8 chars', async function (assert) { + // given + const password = 'Pix1234'; + const onInput = sinon.stub(); + + const screen = await render(); + + // when + await fillByLabel(t(I18N.PASSWORD_INPUT_LABEL), password); + + // then + const rulesStatusMessage = t(I18N.RULES_STATUS_MESSAGE, { rulesCompleted: 3, rulesNumber: 4 }); + assert.dom(screen.getByText(rulesStatusMessage)).exists(); + }); + + test('it must contains at least one uppercase char', async function (assert) { + // given + const password = 'pix12345'; + const onInput = sinon.stub(); + + const screen = await render(); + + // when + await fillByLabel(t(I18N.PASSWORD_INPUT_LABEL), password); + + // then + const rulesStatusMessage = t(I18N.RULES_STATUS_MESSAGE, { rulesCompleted: 3, rulesNumber: 4 }); + assert.dom(screen.getByText(rulesStatusMessage)).exists(); + }); + + test('it must contains at least one lowercase char', async function (assert) { + // given + const password = 'PIX12345'; + const onInput = sinon.stub(); + + const screen = await render(); + + // when + await fillByLabel(t(I18N.PASSWORD_INPUT_LABEL), password); + + // then + const rulesStatusMessage = t(I18N.RULES_STATUS_MESSAGE, { rulesCompleted: 3, rulesNumber: 4 }); + assert.dom(screen.getByText(rulesStatusMessage)).exists(); + }); + + test('it must contains at least one digit', async function (assert) { + // given + const password = 'PIXpixPIX'; + const onInput = sinon.stub(); + + const screen = await render(); + + // when + await fillByLabel(t(I18N.PASSWORD_INPUT_LABEL), password); + + // then + const rulesStatusMessage = t(I18N.RULES_STATUS_MESSAGE, { rulesCompleted: 3, rulesNumber: 4 }); + assert.dom(screen.getByText(rulesStatusMessage)).exists(); + }); + }); +});