From 29b19f19353cbbce2a487636254ab7196467fe9a Mon Sep 17 00:00:00 2001 From: Eric Lim Date: Mon, 7 Oct 2024 09:01:55 +0200 Subject: [PATCH] feat(mon-pix): add password input component --- .../authentication/password-input/index.gjs | 102 +++++++++++++++ .../password-input/index-test.gjs | 120 ++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 mon-pix/app/components/authentication/password-input/index.gjs create mode 100644 mon-pix/tests/integration/components/authentication/password-input/index-test.gjs diff --git a/mon-pix/app/components/authentication/password-input/index.gjs b/mon-pix/app/components/authentication/password-input/index.gjs new file mode 100644 index 0000000000..677612756c --- /dev/null +++ b/mon-pix/app/components/authentication/password-input/index.gjs @@ -0,0 +1,102 @@ +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 PasswordInput 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-input.rules.min-length'), + }, + { + key: CONTAINS_UPPERCASE_KEY, + description: this.intl.t('components.authentication.password-input.rules.contains-uppercase'), + }, + { + key: CONTAINS_LOWERCASE_KEY, + description: this.intl.t('components.authentication.password-input.rules.contains-lowercase'), + }, + { + key: CONTAINS_DIGIT_KEY, + description: this.intl.t('components.authentication.password-input.rules.contains-digit'), + }, + ]; + } + + checkRules() { + 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 + handlePasswordChanged(event) { + this.value = event.target.value; + this.errors = this.checkRules(); + + if (this.value && this.errors.length === 0) { + this.validationStatus = 'success'; + } + + const { onInput } = this.args; + if (onInput) onInput(event); + } + + @action + handleValidationStatus() { + if (!this.value || this.errors.length > 0) { + this.validationStatus = 'error'; + } + } + + +} diff --git a/mon-pix/tests/integration/components/authentication/password-input/index-test.gjs b/mon-pix/tests/integration/components/authentication/password-input/index-test.gjs new file mode 100644 index 0000000000..1fbc32021c --- /dev/null +++ b/mon-pix/tests/integration/components/authentication/password-input/index-test.gjs @@ -0,0 +1,120 @@ +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 PasswordInput from 'mon-pix/components/authentication/password-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-input.rules.completed-message', + ERROR_MESSAGE: 'components.authentication.password-input.error-message', +}; + +module('Integration | Component | authentication | password-input', function (hooks) { + setupIntlRenderingTest(hooks); + + 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(); + }); +});