Skip to content

Commit

Permalink
feat(mon-pix): add password input component
Browse files Browse the repository at this point in the history
  • Loading branch information
er-lim authored and bpetetot committed Oct 10, 2024
1 parent 4bf83c6 commit 29b19f1
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 0 deletions.
102 changes: 102 additions & 0 deletions mon-pix/app/components/authentication/password-input/index.gjs
Original file line number Diff line number Diff line change
@@ -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';
}
}

<template>
<PixInputPassword
aria-describedby="password-checklist"
aria-invalid={{this.hasValidationStatusError}}
autocomplete="new-password"
name="password"
@errorMessage={{t "components.authentication.password-input.error-message"}}
@id="password"
@validationStatus={{this.validationStatus}}
{{on "input" this.handlePasswordChanged}}
{{on "blur" this.handleValidationStatus}}
...attributes
>
<:label>{{t "pages.sign-in.fields.password.label"}}</:label>
</PixInputPassword>

<PasswordChecklist id="password-checklist" @errors={{this.errors}} @rules={{this.rules}} @value={{this.value}} />
</template>
}
Original file line number Diff line number Diff line change
@@ -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(<template><PasswordInput {{on "input" onInput}} /></template>);

// 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(<template><PasswordInput {{on "input" onInput}} /></template>);

// 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(<template><PasswordInput {{on "input" onInput}} /></template>);

// 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(<template><PasswordInput {{on "input" onInput}} /></template>);

// 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(<template><PasswordInput {{on "input" onInput}} /></template>);

// 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(<template><PasswordInput {{on "input" onInput}} /></template>);

// 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();
});
});

0 comments on commit 29b19f1

Please sign in to comment.