ext(affordances): wirePasswordStrength via scheduleRate
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { wirePasswordReveal } from '../password-tools';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { wirePasswordReveal, wirePasswordStrength } from '../password-tools';
|
||||
|
||||
describe('wirePasswordReveal', () => {
|
||||
let form: HTMLElement;
|
||||
@@ -47,3 +47,48 @@ describe('wirePasswordReveal', () => {
|
||||
expect(btn.title).toBe('reveal');
|
||||
});
|
||||
});
|
||||
|
||||
describe('wirePasswordStrength', () => {
|
||||
let form: HTMLElement;
|
||||
let scheduleRate: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
form = document.createElement('div');
|
||||
form.innerHTML = `
|
||||
<input id="f-password" type="password" value="" />
|
||||
<div id="strength-bar-row" class="strength-bar-row" hidden>
|
||||
<div class="strength-bar"><span></span><span></span><span></span><span></span><span></span></div>
|
||||
<div class="strength-label"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(form);
|
||||
scheduleRate = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(form);
|
||||
});
|
||||
|
||||
it('shows bar with score class on input', () => {
|
||||
scheduleRate.mockImplementation((_pw, cb) => cb({ score: 3, guessesLog10: 11.4 }));
|
||||
wirePasswordStrength(form, { scheduleRate });
|
||||
const input = form.querySelector('#f-password') as HTMLInputElement;
|
||||
input.value = 'CorrectHorseBatteryStaple';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
const row = form.querySelector('#strength-bar-row') as HTMLElement;
|
||||
expect(row.hidden).toBe(false);
|
||||
expect(row.querySelector('.strength-bar')?.className).toContain('s-good');
|
||||
expect(row.querySelector('.strength-label')?.textContent).toContain('good');
|
||||
expect(row.querySelector('.strength-label')?.textContent).toContain('10^11');
|
||||
});
|
||||
|
||||
it('hides bar when input is empty', () => {
|
||||
scheduleRate.mockImplementation((_pw, cb) => cb({ score: -1, guessesLog10: -1 }));
|
||||
wirePasswordStrength(form, { scheduleRate });
|
||||
const input = form.querySelector('#f-password') as HTMLInputElement;
|
||||
input.value = '';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
const row = form.querySelector('#strength-bar-row') as HTMLElement;
|
||||
expect(row.hidden).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GLYPH_REVEAL, GLYPH_HIDE } from '../glyphs';
|
||||
import { STRENGTH_LABELS, entropyText, type Strength } from '../../setup/setup-helpers';
|
||||
|
||||
/// Returns a teardown fn the caller must invoke on unmount.
|
||||
export function wirePasswordReveal(form: HTMLElement): () => void {
|
||||
@@ -26,3 +27,41 @@ export function wirePasswordReveal(form: HTMLElement): () => void {
|
||||
btn.title = 'reveal';
|
||||
};
|
||||
}
|
||||
|
||||
export interface PasswordStrengthOpts {
|
||||
scheduleRate: (passphrase: string, cb: (s: Strength) => void) => void;
|
||||
}
|
||||
|
||||
export function wirePasswordStrength(form: HTMLElement, opts: PasswordStrengthOpts): void {
|
||||
const input = form.querySelector<HTMLInputElement>('#f-password');
|
||||
const row = form.querySelector<HTMLElement>('#strength-bar-row');
|
||||
if (!input || !row) return;
|
||||
const bar = row.querySelector<HTMLElement>('.strength-bar');
|
||||
const label = row.querySelector<HTMLElement>('.strength-label');
|
||||
if (!bar || !label) return;
|
||||
|
||||
const update = () => {
|
||||
const v = input.value;
|
||||
if (!v) {
|
||||
row.hidden = true;
|
||||
return;
|
||||
}
|
||||
opts.scheduleRate(v, (s) => {
|
||||
if (s.score < 0) { row.hidden = true; return; }
|
||||
row.hidden = false;
|
||||
// Reset score classes, then add the current one to the bar element.
|
||||
bar.className = 'strength-bar';
|
||||
const cls = STRENGTH_LABELS[s.score]?.cls ?? 's-very-weak';
|
||||
bar.classList.add(cls);
|
||||
// Light up segments 0..score (5-segment bar).
|
||||
Array.from(bar.children).forEach((seg, i) => {
|
||||
(seg as HTMLElement).classList.toggle('lit', i <= s.score);
|
||||
});
|
||||
const text = STRENGTH_LABELS[s.score]?.text ?? '?';
|
||||
label.textContent = `${text} · ${entropyText(s.guessesLog10)}`;
|
||||
});
|
||||
};
|
||||
|
||||
input.addEventListener('input', update);
|
||||
update();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user