add password visibility toggles, confirm password field, terms policy check, and format validation for email and phone
This commit is contained in:
parent
6906d30ed0
commit
9bb761f176
6 changed files with 285 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
# Logs
|
||||
logs
|
||||
html
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
|
|||
|
|
@ -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
79
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(() => {})
|
||||
|
||||
|
|
|
|||
165
src/App.tsx
165
src/App.tsx
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue