-
Notifications
You must be signed in to change notification settings - Fork 16
feat(reactjs-todo-davinci): add ValidatedPasswordCollector support #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ca65c80
31f9cbb
d371ed1
9f51e74
0b90b9c
1c2d63d
70eaca6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,35 +11,149 @@ import React, { useState, useContext } from 'react'; | |||||||||||||||||||||||||||||
| import { ThemeContext } from '../../context/theme.context.js'; | ||||||||||||||||||||||||||||||
| import EyeIcon from '../icons/eye-icon'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const Password = ({ collector, inputName, updater }) => { | ||||||||||||||||||||||||||||||
| const UPPERCASE_RE = /^[A-Z]+$/; | ||||||||||||||||||||||||||||||
| const LOWERCASE_RE = /^[a-z]+$/; | ||||||||||||||||||||||||||||||
| const DIGIT_RE = /^[0-9]+$/; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function buildRequirements(validation) { | ||||||||||||||||||||||||||||||
| const items = []; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (validation.length) { | ||||||||||||||||||||||||||||||
| const { min, max } = validation.length; | ||||||||||||||||||||||||||||||
| if (min != null && max != null) { | ||||||||||||||||||||||||||||||
| items.push(`${min}–${max} characters`); | ||||||||||||||||||||||||||||||
| } else if (min != null) { | ||||||||||||||||||||||||||||||
| items.push(`At least ${min} characters`); | ||||||||||||||||||||||||||||||
| } else if (max != null) { | ||||||||||||||||||||||||||||||
| items.push(`At most ${max} characters`); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (validation.minCharacters) { | ||||||||||||||||||||||||||||||
| for (const [charset, count] of Object.entries(validation.minCharacters)) { | ||||||||||||||||||||||||||||||
| if (UPPERCASE_RE.test(charset)) { | ||||||||||||||||||||||||||||||
| items.push(`At least ${count} uppercase letter(s)`); | ||||||||||||||||||||||||||||||
| } else if (LOWERCASE_RE.test(charset)) { | ||||||||||||||||||||||||||||||
| items.push(`At least ${count} lowercase letter(s)`); | ||||||||||||||||||||||||||||||
| } else if (DIGIT_RE.test(charset)) { | ||||||||||||||||||||||||||||||
| items.push(`At least ${count} number(s)`); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| items.push(`At least ${count} special character(s)`); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return items; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const Password = ({ collector, inputName, updater, validator, verify }) => { | ||||||||||||||||||||||||||||||
| const theme = useContext(ThemeContext); | ||||||||||||||||||||||||||||||
| const [isVisible, setVisibility] = useState(false); | ||||||||||||||||||||||||||||||
| const [isPrimaryVisible, setPrimaryVisible] = useState(false); | ||||||||||||||||||||||||||||||
| const [isConfirmVisible, setConfirmVisible] = useState(false); | ||||||||||||||||||||||||||||||
| const [validationErrors, setValidationErrors] = useState([]); | ||||||||||||||||||||||||||||||
| const [confirmValue, setConfirmValue] = useState(''); | ||||||||||||||||||||||||||||||
| const [confirmError, setConfirmError] = useState(''); | ||||||||||||||||||||||||||||||
| const [primaryValue, setPrimaryValue] = useState(''); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const passwordLabel = collector.output.label; | ||||||||||||||||||||||||||||||
| const isValidated = collector.type === 'ValidatedPasswordCollector'; | ||||||||||||||||||||||||||||||
| const requirements = | ||||||||||||||||||||||||||||||
| isValidated && collector.input.validation ? buildRequirements(collector.input.validation) : []; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * @function toggleVisibility - toggles the password from masked to plaintext | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| function toggleVisibility() { | ||||||||||||||||||||||||||||||
| setVisibility(!isVisible); | ||||||||||||||||||||||||||||||
| function handlePrimaryChange(e) { | ||||||||||||||||||||||||||||||
| const value = e.target.value; | ||||||||||||||||||||||||||||||
| setPrimaryValue(value); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (validator) { | ||||||||||||||||||||||||||||||
| const errors = validator(value); | ||||||||||||||||||||||||||||||
| setValidationErrors(errors); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const result = updater(value); | ||||||||||||||||||||||||||||||
| if (result && result.error) { | ||||||||||||||||||||||||||||||
| console.error('Error updating password collector:', result.error.message); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Keep confirm error in sync as the primary value changes | ||||||||||||||||||||||||||||||
| if (confirmValue && value !== confirmValue) { | ||||||||||||||||||||||||||||||
| setConfirmError('Passwords do not match'); | ||||||||||||||||||||||||||||||
| } else if (confirmValue) { | ||||||||||||||||||||||||||||||
| setConfirmError(''); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+77
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Clear the confirm error when both fields are emptied. If a user types in both fields (triggering a mismatch error), clears the Confirm field, and then clears the Primary field, the "Passwords do not match" error remains visible even when both fields are empty. This happens because when Change 🐛 Proposed fix // Keep confirm error in sync as the primary value changes
if (confirmValue && value !== confirmValue) {
setConfirmError('Passwords do not match');
- } else if (confirmValue) {
+ } else {
setConfirmError('');
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function handleConfirmChange(e) { | ||||||||||||||||||||||||||||||
| const value = e.target.value; | ||||||||||||||||||||||||||||||
| setConfirmValue(value); | ||||||||||||||||||||||||||||||
| if (primaryValue && value !== primaryValue) { | ||||||||||||||||||||||||||||||
| setConfirmError('Passwords do not match'); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| setConfirmError(''); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <div className="cstm_form-floating input-group form-floating mb-3"> | ||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||
| className={`cstm_input-password form-control border-end-0 bg-transparent ${theme.textClass} ${theme.borderClass}`} | ||||||||||||||||||||||||||||||
| id={inputName} | ||||||||||||||||||||||||||||||
| name={inputName} | ||||||||||||||||||||||||||||||
| type={isVisible ? 'text' : 'password'} | ||||||||||||||||||||||||||||||
| onChange={(e) => updater(e.target.value)} | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
| <label htmlFor={inputName}>{passwordLabel}</label> | ||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||
| className={`cstm_input-icon border-start-0 input-group-text bg-transparent ${theme.textClass} ${theme.borderClass}`} | ||||||||||||||||||||||||||||||
| onClick={toggleVisibility} | ||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <EyeIcon visible={isVisible} /> | ||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||
| {requirements.length > 0 && ( | ||||||||||||||||||||||||||||||
| <ul className="password-requirements"> | ||||||||||||||||||||||||||||||
| {requirements.map((req, i) => ( | ||||||||||||||||||||||||||||||
| <li key={i}>{req}</li> | ||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||
| </ul> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div className="cstm_form-floating input-group form-floating mb-3"> | ||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||
| className={`cstm_input-password form-control border-end-0 bg-transparent ${theme.textClass} ${theme.borderClass}`} | ||||||||||||||||||||||||||||||
| id={inputName} | ||||||||||||||||||||||||||||||
| name={inputName} | ||||||||||||||||||||||||||||||
| type={isPrimaryVisible ? 'text' : 'password'} | ||||||||||||||||||||||||||||||
| onChange={handlePrimaryChange} | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
| <label htmlFor={inputName}>{passwordLabel}</label> | ||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||
| className={`cstm_input-icon border-start-0 input-group-text bg-transparent ${theme.textClass} ${theme.borderClass}`} | ||||||||||||||||||||||||||||||
| onClick={() => setPrimaryVisible(!isPrimaryVisible)} | ||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <EyeIcon visible={isPrimaryVisible} /> | ||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {validationErrors.length > 0 && ( | ||||||||||||||||||||||||||||||
| <ul className={`${inputName}-error`}> | ||||||||||||||||||||||||||||||
| {validationErrors.map((msg, i) => ( | ||||||||||||||||||||||||||||||
| <li key={i}>{msg}</li> | ||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||
| </ul> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {verify && ( | ||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||
| <div className="cstm_form-floating input-group form-floating mb-3"> | ||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||
| className={`cstm_input-password form-control border-end-0 bg-transparent ${theme.textClass} ${theme.borderClass}`} | ||||||||||||||||||||||||||||||
| id={`${inputName}-confirm`} | ||||||||||||||||||||||||||||||
| name={`${inputName}-confirm`} | ||||||||||||||||||||||||||||||
| type={isConfirmVisible ? 'text' : 'password'} | ||||||||||||||||||||||||||||||
| onChange={handleConfirmChange} | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
| <label htmlFor={`${inputName}-confirm`}>Confirm Password</label> | ||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||
| className={`cstm_input-icon border-start-0 input-group-text bg-transparent ${theme.textClass} ${theme.borderClass}`} | ||||||||||||||||||||||||||||||
| onClick={() => setConfirmVisible(!isConfirmVisible)} | ||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <EyeIcon visible={isConfirmVisible} /> | ||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| {confirmError && ( | ||||||||||||||||||||||||||||||
| <p className={`${inputName}-confirm-error`} style={{ color: 'red' }}> | ||||||||||||||||||||||||||||||
| {confirmError} | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /* | ||
| * | ||
| * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. | ||
| * This software may be modified and distributed under the terms | ||
| * of the MIT license. See the LICENSE file for details. | ||
| * | ||
| */ | ||
| import { test, expect } from '@playwright/test'; | ||
|
|
||
| const BASE_URL = 'http://localhost:8443'; | ||
| const ACR_VALUE = '533a7ff229ca6395afd9dd6deb699944'; | ||
|
|
||
| async function navigateToRegistrationForm(page) { | ||
| await page.goto(`${BASE_URL}/login?acrValue=${ACR_VALUE}`); | ||
| await expect(page.getByRole('heading', { name: 'Select Test Form' })).toBeVisible(); | ||
| await page.getByRole('link', { name: 'USER_REGISTRATION' }).click(); | ||
| await expect(page.getByRole('heading', { name: 'Example - Registration' })).toBeVisible({ | ||
| timeout: 10000, | ||
| }); | ||
| } | ||
|
|
||
| test.describe('React - DaVinci ValidatedPasswordCollector', () => { | ||
| test('shows password requirements list', async ({ page }) => { | ||
| await navigateToRegistrationForm(page); | ||
| await expect(page.locator('ul.password-requirements')).toBeVisible(); | ||
| await expect(page.locator('ul.password-requirements li').first()).toBeVisible(); | ||
| }); | ||
|
|
||
| test('shows inline validation errors for a password that violates policy, then clears them', async ({ | ||
| page, | ||
| }) => { | ||
| await navigateToRegistrationForm(page); | ||
| const passwordInput = page.getByLabel('Password'); | ||
| await passwordInput.fill('a'); | ||
| await expect(page.locator('[class$="-error"] li').first()).toBeVisible(); | ||
|
|
||
| await passwordInput.fill('Demo_12345!'); | ||
| await expect(page.locator('[class$="-error"] li')).toHaveCount(0); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there anything blocking the user from submitting the form if there is an error? Or are these just UI hints?