From f0935517e698fbcf3f668f3221b6eb9dd484dd91 Mon Sep 17 00:00:00 2001 From: Michael Chadwick Date: Mon, 19 May 2025 09:48:04 -0700 Subject: [PATCH 1/3] added new PasswordValidator component --- .../app/components/password-validator.gjs | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 packages/frontend/app/components/password-validator.gjs diff --git a/packages/frontend/app/components/password-validator.gjs b/packages/frontend/app/components/password-validator.gjs new file mode 100644 index 0000000000..1eeff43c56 --- /dev/null +++ b/packages/frontend/app/components/password-validator.gjs @@ -0,0 +1,126 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import { isEmpty } from '@ember/utils'; +import { dropTask, timeout } from 'ember-concurrency'; +import { uniqueId, concat } from '@ember/helper'; +import { on } from '@ember/modifier'; +import pick from 'ilios-common/helpers/pick'; +import t from 'ember-intl/helpers/t'; +import eq from 'ember-truth-helpers/helpers/eq'; +import gt from 'ember-truth-helpers/helpers/gt'; +import YupValidations from 'ilios-common/classes/yup-validations'; +import YupValidationMessage from 'ilios-common/components/yup-validation-message'; +import { string } from 'yup'; + +export default class PasswordValidatorComponent extends Component { + @service intl; + + @tracked hasErrorForPassword = false; + @tracked password = null; + @tracked isSaving = false; + @tracked passwordStrengthScore = 0; + + validations = new YupValidations(this, { + password: string().ensure().trim().required(), + }); + + @action + async keyboard(event) { + const keyCode = event.keyCode; + + if (13 === keyCode) { + await this.save.perform(); + } + } + + @action + async setPassword(password) { + this.password = password; + await this.calculatePasswordStrengthScore(); + } + + async calculatePasswordStrengthScore() { + const { default: zxcvbn } = await import('zxcvbn'); + const password = isEmpty(this.password) ? '' : this.password; + const obj = zxcvbn(password); + this.passwordStrengthScore = obj.score; + } + + save = dropTask(async () => { + this.validations.addErrorDisplayForAllFields(); + const isValid = await this.validations.isValid(); + if (!isValid) { + this.hasErrorForPassword = true; + return false; + } + await timeout(250); // artificial "validation processing" + this.validations.clearErrorDisplay(); + this.hasErrorForPassword = false; + }); + +} From 0d9555728d838b1138c13a9ae47e34660361d048 Mon Sep 17 00:00:00 2001 From: Michael Chadwick Date: Mon, 19 May 2025 09:49:24 -0700 Subject: [PATCH 2/3] added PasswordValidator integration test and page-object --- .../components/password-validator-test.gjs | 84 +++++++++++++++++++ .../pages/components/password-validator.js | 34 ++++++++ 2 files changed, 118 insertions(+) create mode 100644 packages/frontend/tests/integration/components/password-validator-test.gjs create mode 100644 packages/frontend/tests/pages/components/password-validator.js diff --git a/packages/frontend/tests/integration/components/password-validator-test.gjs b/packages/frontend/tests/integration/components/password-validator-test.gjs new file mode 100644 index 0000000000..060986f60c --- /dev/null +++ b/packages/frontend/tests/integration/components/password-validator-test.gjs @@ -0,0 +1,84 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'frontend/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { component } from 'frontend/tests/pages/components/password-validator'; +import PasswordValidator from 'frontend/components/password-validator'; + +module('Integration | Component | password-validator', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.ok(component, 'component exists'); + assert.strictEqual(component.label, 'Password:', 'has Password label'); + }); + + test('it fails blank password', async function (assert) { + await render(); + + assert.false(component.hasError, 'no error with no input'); + await component.submit(); + assert.true(component.hasError, 'shows blank password error'); + }); + + test('it fails short password', async function (assert) { + await render(); + + await component.fillIn('abc'); + await component.submit(); + assert.true(component.hasError); + }); + + test('it passes valid password', async function (assert) { + await render(); + + await component.fillIn('abcde'); + assert.false(component.hasError); + }); + + test('password strength 0 display', async function (assert) { + await render(); + + await component.fillIn('12345'); + assert.strictEqual(component.meter.value, 0); + assert.strictEqual(component.strength.text, 'Try Harder'); + assert.ok(component.strength.hasZeroClass); + }); + + test('password strength 1 display', async function (assert) { + await render(); + + await component.fillIn('12345ab'); + assert.strictEqual(component.meter.value, 1); + assert.strictEqual(component.strength.text, 'Bad'); + assert.ok(component.strength.hasOneClass); + }); + + test('password strength 2 display', async function (assert) { + await render(); + + await component.fillIn('12345ab13&'); + assert.strictEqual(component.meter.value, 2); + assert.strictEqual(component.strength.text, 'Weak'); + assert.ok(component.strength.hasTwoClass); + }); + + test('password strength 3 display', async function (assert) { + await render(); + + await component.fillIn('12345ab13&!!'); + assert.strictEqual(component.meter.value, 3); + assert.strictEqual(component.strength.text, 'Good'); + assert.ok(component.strength.hasThreeClass); + }); + + test('password strength 4 display', async function (assert) { + await render(); + + await component.fillIn('12345ab13&HHtB'); + assert.strictEqual(component.meter.value, 4); + assert.strictEqual(component.strength.text, 'Strong'); + assert.ok(component.strength.hasFourClass); + }); +}); diff --git a/packages/frontend/tests/pages/components/password-validator.js b/packages/frontend/tests/pages/components/password-validator.js new file mode 100644 index 0000000000..75e284af8c --- /dev/null +++ b/packages/frontend/tests/pages/components/password-validator.js @@ -0,0 +1,34 @@ +import { + clickable, + create, + fillable, + hasClass, + isVisible, + text, + triggerable, + value, +} from 'ember-cli-page-object'; + +const definition = { + scope: '[data-test-password-validator]', + label: text('label'), + fillIn: fillable('input'), + value: value('input'), + validate: clickable('button'), + hasError: isVisible('.validation-error-message'), + submit: triggerable('keyup', 'input', { eventProperties: { key: 'Enter' } }), + strength: { + scope: '[data-test-password-strength-text]', + hasZeroClass: hasClass('strength-0'), + hasOneClass: hasClass('strength-1'), + hasTwoClass: hasClass('strength-2'), + hasThreeClass: hasClass('strength-3'), + hasFourClass: hasClass('strength-4'), + }, + meter: { + scope: '[data-test-password-strength-meter]', + }, +}; + +export default definition; +export const component = create(definition); From 187b2c468a6265a5ae1346b1bdbcde8cfc7c0e35 Mon Sep 17 00:00:00 2001 From: Michael Chadwick Date: Mon, 19 May 2025 11:30:56 -0700 Subject: [PATCH 3/3] NewUser component now using PasswordValidator component --- packages/frontend/app/components/new-user.gjs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/frontend/app/components/new-user.gjs b/packages/frontend/app/components/new-user.gjs index a733fe5f1d..f881d1b1a4 100644 --- a/packages/frontend/app/components/new-user.gjs +++ b/packages/frontend/app/components/new-user.gjs @@ -21,6 +21,7 @@ import YupValidations from 'ilios-common/classes/yup-validations'; import YupValidationMessage from 'ilios-common/components/yup-validation-message'; import { string } from 'yup'; import isEmail from 'validator/lib/isEmail'; +import PasswordValidator from 'frontend/components/password-validator'; export default class NewUserComponent extends Component { @service intl; @@ -409,22 +410,7 @@ export default class NewUserComponent extends Component { />
- - - +