Remove Quick Login + scanner a11y polish (#56)
Drop alice/bob password prefill from the login page, add a regression test, and improve bulk-toolbar and disambiguation accessible names. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c197dc61ed
commit
e0218e4b05
7 changed files with 85 additions and 59 deletions
26
.convoys/purge-quick-login-from-loginpage.md
Normal file
26
.convoys/purge-quick-login-from-loginpage.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: purge-quick-login-from-loginpage
|
||||
classification: fix
|
||||
success_metric: |
|
||||
pages/login.js no longer ships alice123/bob123 in client HTML; smoke sign-in
|
||||
CTA unchanged.
|
||||
status: closed
|
||||
created: 2026-05-26
|
||||
closed: 2026-05-29
|
||||
pr: pending
|
||||
---
|
||||
|
||||
# Convoy: purge-quick-login-from-loginpage
|
||||
|
||||
Remove production Quick Login buttons that exposed test-user passwords in
|
||||
view-source HTML.
|
||||
|
||||
## Scope
|
||||
|
||||
- Delete Quick Login section + `handleQuickLogin` from `pages/login.js`
|
||||
- Regression test: no quick-login copy or password literals in rendered output
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Rotating alice/bob DB hashes (operators rotate manually if needed)
|
||||
- `TESTING_GUIDE.md` dev account table (local QA reference only)
|
||||
|
|
@ -7,24 +7,22 @@ success_metric: |
|
|||
uses list semantics and live count updates.
|
||||
depends_on:
|
||||
- redesign-scanner-flow
|
||||
status: open
|
||||
status: closed
|
||||
created: 2026-05-27
|
||||
closed: 2026-05-29
|
||||
pr: pending
|
||||
---
|
||||
|
||||
# Convoy: scanner-redesign-a11y-fixes
|
||||
|
||||
Closes P1 follow-up from `audit-redesign-scanner-flow-44`.
|
||||
|
||||
## Scope
|
||||
|
||||
- `components/ScannedCardItem.js` — ownership `role="status"`, labeled selects/buttons
|
||||
- `components/CameraScanner.js` — camera control labels, disambiguation focus trap, video label, heading fix
|
||||
- `pages/scanner.js` — create-collection modal a11y, bulk toolbar labels, queue list semantics, live count
|
||||
- `lib/use-focus-trap.js` (new) — shared modal focus trap
|
||||
**Note:** Core a11y shipped in PR #45; this follow-up closes remaining polish
|
||||
(bulk toolbar semantics, disambiguation button labels, condition select ids).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. Ownership badge announced by screen readers.
|
||||
2. All icon-only and placeholder `<select>` controls have accessible names.
|
||||
3. Disambiguation + create-collection modals trap focus and restore on close.
|
||||
4. Scanned cards queue uses list semantics; count updates are polite live region.
|
||||
- [x] Ownership badge announced by screen readers (PR #45 + tests)
|
||||
- [x] Icon-only and placeholder controls labeled
|
||||
- [x] Disambiguation + create-list modals trap focus (`use-focus-trap.js`)
|
||||
- [x] Scanned cards queue uses list semantics; count updates are polite live region
|
||||
|
|
|
|||
|
|
@ -1052,11 +1052,12 @@ export default function CameraScanner({ onCardScanned, onError }) {
|
|||
<div className="space-y-2">
|
||||
{disambiguation.candidates.map((candidate) => (
|
||||
<button
|
||||
key={candidate.id}
|
||||
type="button"
|
||||
key={candidate.id}
|
||||
onClick={() => handleDisambiguationPick(candidate)}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-lg border text-left hover:opacity-90"
|
||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-tertiary)' }}
|
||||
aria-label={`Select ${candidate.name}${candidate.set_name ? `, ${candidate.set_name}` : ''}`}
|
||||
>
|
||||
{candidate.image_url ? (
|
||||
<img src={candidate.image_url} alt="" className="w-12 h-16 object-cover rounded" />
|
||||
|
|
|
|||
|
|
@ -160,9 +160,14 @@ export default function ScannedCardItem({
|
|||
|
||||
{!card.processed && (
|
||||
<div className="flex flex-wrap items-center gap-3 mb-3">
|
||||
<label className="flex items-center gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
<label
|
||||
htmlFor={`scan-condition-${card.id}`}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
<span>Condition</span>
|
||||
<select
|
||||
id={`scan-condition-${card.id}`}
|
||||
value={card.condition || 'NM'}
|
||||
onChange={(e) => onUpdateMetadata({ condition: e.target.value })}
|
||||
className="px-2 py-1 rounded border text-sm"
|
||||
|
|
@ -171,6 +176,7 @@ export default function ScannedCardItem({
|
|||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-primary)',
|
||||
}}
|
||||
aria-label={`Condition for ${card.name}`}
|
||||
>
|
||||
{CONDITION_OPTIONS.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
|
|
|
|||
|
|
@ -60,10 +60,6 @@ export default function Login() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleQuickLogin = (email, password) => {
|
||||
setFormData({ email, password });
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
|
|
@ -161,41 +157,6 @@ export default function Login() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Login for Testing */}
|
||||
<div className="mt-6 pt-6 border-t border-opacity-20" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="text-center">
|
||||
<p className="text-sm mb-3" style={{ color: 'var(--text-secondary)' }}>
|
||||
Quick Login for Testing:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickLogin('alice@deckhearth.com', 'alice123')}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium border border-opacity-20 transition-all duration-200 hover:shadow-md backdrop-blur-sm hover:bg-opacity-80"
|
||||
style={{
|
||||
backgroundColor: 'rgba(var(--bg-primary-rgb), 0.6)',
|
||||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-primary)'
|
||||
}}
|
||||
>
|
||||
👤 Alice
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickLogin('bob@deckhearth.com', 'bob123')}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium border border-opacity-20 transition-all duration-200 hover:shadow-md backdrop-blur-sm hover:bg-opacity-80"
|
||||
style={{
|
||||
backgroundColor: 'rgba(var(--bg-primary-rgb), 0.6)',
|
||||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-primary)'
|
||||
}}
|
||||
>
|
||||
👤 Bob
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Don't have an account?{' '}
|
||||
|
|
|
|||
|
|
@ -613,6 +613,7 @@ export default function Scanner() {
|
|||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-secondary)'
|
||||
}}
|
||||
aria-label="Clear all scanned cards from queue"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
|
|
@ -659,15 +660,19 @@ export default function Scanner() {
|
|||
{/* Floating Bulk Actions Toolbar */}
|
||||
{selectedCards.size > 0 && (
|
||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-50">
|
||||
<div className="rounded-2xl shadow-2xl border px-6 py-4 flex items-center gap-4 max-w-4xl"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderColor: 'var(--border)',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<div
|
||||
className="rounded-2xl shadow-2xl border px-6 py-4 flex items-center gap-4 max-w-4xl"
|
||||
role="toolbar"
|
||||
aria-label="Bulk actions for selected scanned cards"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderColor: 'var(--border)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Selection Count */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2" aria-live="polite" aria-atomic="true">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold text-white"
|
||||
style={{ backgroundColor: 'var(--accent-ember)' }}>
|
||||
{selectedCards.size}
|
||||
|
|
|
|||
29
test/pages/login.test.js
Normal file
29
test/pages/login.test.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
|
||||
import Login from '../../pages/login.js';
|
||||
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/AuthLayout', () => ({
|
||||
default: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../components/AnimatedFireLogo', () => ({
|
||||
default: () => <div aria-hidden="true">logo</div>,
|
||||
}));
|
||||
|
||||
describe('Login page', () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('renders the sign-in CTA without dev quick-login credentials', () => {
|
||||
render(<Login />);
|
||||
|
||||
expect(screen.getByRole('button', { name: /sign in to deck hearth/i })).toBeTruthy();
|
||||
expect(screen.queryByText(/quick login/i)).toBeNull();
|
||||
expect(document.body.textContent).not.toMatch(/alice123|bob123/);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue