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.
|
uses list semantics and live count updates.
|
||||||
depends_on:
|
depends_on:
|
||||||
- redesign-scanner-flow
|
- redesign-scanner-flow
|
||||||
status: open
|
status: closed
|
||||||
created: 2026-05-27
|
created: 2026-05-27
|
||||||
|
closed: 2026-05-29
|
||||||
|
pr: pending
|
||||||
---
|
---
|
||||||
|
|
||||||
# Convoy: scanner-redesign-a11y-fixes
|
# Convoy: scanner-redesign-a11y-fixes
|
||||||
|
|
||||||
Closes P1 follow-up from `audit-redesign-scanner-flow-44`.
|
Closes P1 follow-up from `audit-redesign-scanner-flow-44`.
|
||||||
|
|
||||||
## Scope
|
**Note:** Core a11y shipped in PR #45; this follow-up closes remaining polish
|
||||||
|
(bulk toolbar semantics, disambiguation button labels, condition select ids).
|
||||||
- `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
|
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
1. Ownership badge announced by screen readers.
|
- [x] Ownership badge announced by screen readers (PR #45 + tests)
|
||||||
2. All icon-only and placeholder `<select>` controls have accessible names.
|
- [x] Icon-only and placeholder controls labeled
|
||||||
3. Disambiguation + create-collection modals trap focus and restore on close.
|
- [x] Disambiguation + create-list modals trap focus (`use-focus-trap.js`)
|
||||||
4. Scanned cards queue uses list semantics; count updates are polite live region.
|
- [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">
|
<div className="space-y-2">
|
||||||
{disambiguation.candidates.map((candidate) => (
|
{disambiguation.candidates.map((candidate) => (
|
||||||
<button
|
<button
|
||||||
key={candidate.id}
|
|
||||||
type="button"
|
type="button"
|
||||||
|
key={candidate.id}
|
||||||
onClick={() => handleDisambiguationPick(candidate)}
|
onClick={() => handleDisambiguationPick(candidate)}
|
||||||
className="w-full flex items-center gap-3 p-3 rounded-lg border text-left hover:opacity-90"
|
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)' }}
|
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-tertiary)' }}
|
||||||
|
aria-label={`Select ${candidate.name}${candidate.set_name ? `, ${candidate.set_name}` : ''}`}
|
||||||
>
|
>
|
||||||
{candidate.image_url ? (
|
{candidate.image_url ? (
|
||||||
<img src={candidate.image_url} alt="" className="w-12 h-16 object-cover rounded" />
|
<img src={candidate.image_url} alt="" className="w-12 h-16 object-cover rounded" />
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,14 @@ export default function ScannedCardItem({
|
||||||
|
|
||||||
{!card.processed && (
|
{!card.processed && (
|
||||||
<div className="flex flex-wrap items-center gap-3 mb-3">
|
<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>
|
<span>Condition</span>
|
||||||
<select
|
<select
|
||||||
|
id={`scan-condition-${card.id}`}
|
||||||
value={card.condition || 'NM'}
|
value={card.condition || 'NM'}
|
||||||
onChange={(e) => onUpdateMetadata({ condition: e.target.value })}
|
onChange={(e) => onUpdateMetadata({ condition: e.target.value })}
|
||||||
className="px-2 py-1 rounded border text-sm"
|
className="px-2 py-1 rounded border text-sm"
|
||||||
|
|
@ -171,6 +176,7 @@ export default function ScannedCardItem({
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
color: 'var(--text-primary)',
|
color: 'var(--text-primary)',
|
||||||
}}
|
}}
|
||||||
|
aria-label={`Condition for ${card.name}`}
|
||||||
>
|
>
|
||||||
{CONDITION_OPTIONS.map((option) => (
|
{CONDITION_OPTIONS.map((option) => (
|
||||||
<option key={option} value={option}>
|
<option key={option} value={option}>
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,6 @@ export default function Login() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQuickLogin = (email, password) => {
|
|
||||||
setFormData({ email, password });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout>
|
<AuthLayout>
|
||||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<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>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="text-center">
|
||||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
|
|
|
||||||
|
|
@ -613,6 +613,7 @@ export default function Scanner() {
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
color: 'var(--text-secondary)'
|
color: 'var(--text-secondary)'
|
||||||
}}
|
}}
|
||||||
|
aria-label="Clear all scanned cards from queue"
|
||||||
>
|
>
|
||||||
Clear All
|
Clear All
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -659,15 +660,19 @@ export default function Scanner() {
|
||||||
{/* Floating Bulk Actions Toolbar */}
|
{/* Floating Bulk Actions Toolbar */}
|
||||||
{selectedCards.size > 0 && (
|
{selectedCards.size > 0 && (
|
||||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-50">
|
<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"
|
<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={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-secondary)',
|
backgroundColor: 'var(--bg-secondary)',
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
backdropFilter: 'blur(10px)'
|
backdropFilter: 'blur(10px)',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
{/* Selection Count */}
|
{/* 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"
|
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold text-white"
|
||||||
style={{ backgroundColor: 'var(--accent-ember)' }}>
|
style={{ backgroundColor: 'var(--accent-ember)' }}>
|
||||||
{selectedCards.size}
|
{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