add password visibility toggles, confirm password field, terms policy check, and format validation for email and phone

This commit is contained in:
vickytechkey 2026-08-09 13:05:45 +05:30
parent 6906d30ed0
commit 9bb761f176
6 changed files with 285 additions and 16 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
# Logs
logs
html
*.log
npm-debug.log*
yarn-debug.log*

View file

@ -9,6 +9,8 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
cy.get('input#reg-email').type('test_seller@example.com');
cy.get('input#reg-phone').type('9876543210');
cy.get('input#reg-pass').type('super_secure_pass_123');
cy.get('input#reg-confirm-pass').type('super_secure_pass_123');
cy.get('input#reg-policy').check();
cy.get('button').contains('Register Business').click();
// 3. Step 1: Contact Verification
@ -98,4 +100,53 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
cy.contains('Print Label').should('be.visible');
cy.contains('Download SVG').should('be.visible');
});
it('enforces security constraints and validation rules (negative test cases)', () => {
// 1. Visit Home and verify dashboard components are not in DOM (route guarding)
cy.visit('/');
cy.get('.dashboard-container').should('not.exist');
cy.get('.sidebar-menu').should('not.exist');
// 2. Go to Registration
cy.contains('Get Started Today').click();
cy.get('input#reg-email').type('security_test@example.com');
cy.get('input#reg-phone').type('9000000000');
cy.get('input#reg-pass').type('password123');
cy.get('input#reg-confirm-pass').type('password123');
cy.get('input#reg-policy').check();
cy.get('button').contains('Register Business').click();
// 3. Step 1: Negative OTP validations
cy.contains('Step 1 of 3: Contact Verification').should('be.visible');
// Proceed button must be disabled initially
cy.get('button').contains('Next Step: Identity Verification').should('be.disabled');
// Submit invalid WhatsApp OTP
cy.contains('Send WhatsApp OTP').click();
cy.get('input[placeholder="Enter 123456"]').first().type('000000');
// Capture alert for incorrect OTP
const alertStub = cy.stub();
cy.on('window:alert', alertStub);
cy.contains('Verify Code').first().click().then(() => {
expect(alertStub).to.have.been.calledWith('Incorrect code. Enter 123456');
});
cy.contains('✓ Mobile & WhatsApp Verified').should('not.exist');
// Correct the code to enable proceed
cy.get('input[placeholder="Enter 123456"]').first().clear().type('123456');
cy.contains('Verify Code').first().click();
cy.contains('✓ Mobile & WhatsApp Verified').should('be.visible');
// Submit invalid Email OTP
cy.contains('Send Email OTP').click();
cy.get('input[placeholder="Enter 123456"]').last().type('999999');
cy.contains('Verify Code').last().click().then(() => {
expect(alertStub).to.have.been.calledWith('Incorrect code. Enter 123456');
});
cy.contains('✓ Email Verified').should('not.exist');
cy.get('button').contains('Next Step: Identity Verification').should('be.disabled');
});
});

79
package-lock.json generated
View file

@ -19,6 +19,7 @@
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.4",
"@vitest/ui": "^4.1.10",
"cypress": "^15.20.0",
"jsdom": "^30.0.1",
"oxlint": "^1.75.0",
@ -696,6 +697,13 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"dev": true,
"license": "MIT"
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz",
@ -1233,6 +1241,28 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/ui": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.10.tgz",
"integrity": "sha512-EOUqfXHTXtpSHsyLHH40ts3Ue+hRhSGwzwzMlK0dTEOLSDYyOXLyr5JDGmHQWhN2DYI30gw6dVx3cdgM9FZl+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.10",
"fflate": "^0.8.2",
"flatted": "^3.4.2",
"pathe": "^2.0.3",
"sirv": "^3.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "4.1.10"
}
},
"node_modules/@vitest/utils": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
@ -2172,6 +2202,20 @@
}
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true,
"license": "MIT"
},
"node_modules/flatted": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
"integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
"dev": true,
"license": "ISC"
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -3177,6 +3221,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -3819,6 +3873,21 @@
"dev": true,
"license": "ISC"
},
"node_modules/sirv": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/slice-ansi": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
@ -4123,6 +4192,16 @@
"node": ">=14.14"
}
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/tough-cookie": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",

View file

@ -24,6 +24,7 @@
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.4",
"@vitest/ui": "^4.1.10",
"cypress": "^15.20.0",
"jsdom": "^30.0.1",
"oxlint": "^1.75.0",

View file

@ -49,10 +49,14 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
const emailInput = screen.getByLabelText(/Business Email \*/i)
const phoneInput = screen.getByLabelText(/Mobile Number \*/i)
const passInput = screen.getByLabelText(/Create Password \*/i)
const confirmPassInput = screen.getByLabelText(/Confirm Password \*/i)
const policyCheckbox = screen.getByLabelText(/I accept the/i)
fireEvent.change(emailInput, { target: { value: 'seller@example.com' } })
fireEvent.change(phoneInput, { target: { value: '9876543210' } })
fireEvent.change(passInput, { target: { value: 'password123' } })
fireEvent.change(confirmPassInput, { target: { value: 'password123' } })
fireEvent.click(policyCheckbox)
const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {})

View file

@ -78,6 +78,13 @@ export default function App() {
})
const [mapCoordinates, setMapCoordinates] = useState({ lat: 12.9716, lng: 77.5946 })
// --- Password visibility, confirm password and policy states ---
const [showLoginPass, setShowLoginPass] = useState(false)
const [showSignupPass, setShowSignupPass] = useState(false)
const [showSignupConfirmPass, setShowSignupConfirmPass] = useState(false)
const [confirmPassword, setConfirmPassword] = useState('')
const [policyAccepted, setPolicyAccepted] = useState(false)
// --- Password Reset Page States ---
const [resetEmail, setResetEmail] = useState('')
const [resetOtpSent, setResetOtpSent] = useState(false)
@ -170,11 +177,50 @@ export default function App() {
const handleRegisterSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Email Validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
alert('Please enter a valid email address.');
return;
}
// Phone Validation
const phoneRegex = /^[6-9]\d{9}$/;
if (!phoneRegex.test(phone)) {
alert('Please enter a valid 10-digit mobile number starting with 6, 7, 8, or 9.');
return;
}
// Password Validation
if (password.length < 8) {
alert('Password must be at least 8 characters long.');
return;
}
if (password !== confirmPassword) {
alert('Passwords do not match.');
return;
}
// Policy Validation
if (!policyAccepted) {
alert('You must accept the Terms of Service and Privacy Policy.');
return;
}
navigateTo('profile-completion')
}
const handleLoginSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Email Validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
alert('Please enter a valid email address.');
return;
}
setIsProfileComplete(true)
navigateTo('dashboard', true)
}
@ -518,15 +564,35 @@ export default function App() {
</div>
<div className="form-group">
<label htmlFor="login-password">Password</label>
<input
id="login-password"
type="password"
className="form-control"
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<div style={{ position: 'relative' }}>
<input
id="login-password"
type={showLoginPass ? 'text' : 'password'}
className="form-control"
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
style={{ paddingRight: '45px' }}
/>
<button
type="button"
onClick={() => setShowLoginPass(!showLoginPass)}
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '1.1rem',
color: 'var(--text-muted)'
}}
>
{showLoginPass ? '👁️' : '🙈'}
</button>
</div>
</div>
<div style={{ textAlign: 'right', marginBottom: '1.5rem' }}>
<button type="button" className="nav-link" style={{ color: 'var(--text-muted)', fontSize: '0.85rem', textDecoration: 'none', background: 'none', border: 'none', cursor: 'pointer' }} onClick={(e) => { e.preventDefault(); navigateTo('forgot-password'); }}>Forgot Password?</button>
@ -567,17 +633,84 @@ export default function App() {
</div>
<div className="form-group">
<label htmlFor="reg-pass">Create Password *</label>
<div style={{ position: 'relative' }}>
<input
id="reg-pass"
type={showSignupPass ? 'text' : 'password'}
className="form-control"
placeholder="Minimum 8 characters"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
style={{ paddingRight: '45px' }}
/>
<button
type="button"
onClick={() => setShowSignupPass(!showSignupPass)}
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '1.1rem',
color: 'var(--text-muted)'
}}
>
{showSignupPass ? '👁️' : '🙈'}
</button>
</div>
</div>
<div className="form-group">
<label htmlFor="reg-confirm-pass">Confirm Password *</label>
<div style={{ position: 'relative' }}>
<input
id="reg-confirm-pass"
type={showSignupConfirmPass ? 'text' : 'password'}
className="form-control"
placeholder="Re-enter password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
style={{ paddingRight: '45px' }}
/>
<button
type="button"
onClick={() => setShowSignupConfirmPass(!showSignupConfirmPass)}
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '1.1rem',
color: 'var(--text-muted)'
}}
>
{showSignupConfirmPass ? '👁️' : '🙈'}
</button>
</div>
</div>
<div className="form-group" style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', margin: '1.25rem 0' }}>
<input
id="reg-pass"
type="password"
className="form-control"
placeholder="Minimum 8 characters"
value={password}
onChange={(e) => setPassword(e.target.value)}
id="reg-policy"
type="checkbox"
checked={policyAccepted}
onChange={(e) => setPolicyAccepted(e.target.checked)}
required
/>
<label htmlFor="reg-policy" style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: 0, cursor: 'pointer' }}>
I accept the <a href="#terms" onClick={(e) => e.preventDefault()}>Terms of Service</a> and <a href="#privacy" onClick={(e) => e.preventDefault()}>Privacy Policy</a> *
</label>
</div>
<button type="submit" className="btn btn-primary" style={{ width: '100%', padding: '0.75rem', marginTop: '1rem' }}>
<button type="submit" className="btn btn-primary" style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}>
Register Business
</button>
</form>