fix(ext/login): dispatch input event after regenerate sets password (B1)

Programmatic input.value = newPassword does not fire input events, so
the strength-meter listener at shared/form-affordances/password-tools.ts:65
never re-rates the new value — meter stays stuck on the prior reading.

Extract applyGeneratedPassword(input, value) helper that sets value, type,
then dispatches new InputEvent('input', { bubbles: true }). Vitest covers
the dispatch + a sanity check that bubbling listeners fire.
This commit is contained in:
adlee-was-taken
2026-05-02 16:46:06 -04:00
parent 575343dc19
commit 2df636e454
2 changed files with 45 additions and 2 deletions

View File

@@ -26,7 +26,7 @@ vi.mock('../../../../setup/setup-helpers', () => ({
entropyText: vi.fn(() => ''), entropyText: vi.fn(() => ''),
})); }));
import { renderForm } from '../login'; import { renderForm, applyGeneratedPassword } from '../login';
import { sendMessage } from '../../../../shared/state'; import { sendMessage } from '../../../../shared/state';
describe('login form smart inputs', () => { describe('login form smart inputs', () => {
@@ -154,3 +154,37 @@ describe('Login save shape', () => {
expect(addCall).toBeUndefined(); expect(addCall).toBeUndefined();
}); });
}); });
describe('regenerate handler dispatches input event', () => {
it('dispatches an InputEvent on the input after value is set', () => {
const input = document.createElement('input');
input.type = 'password';
document.body.appendChild(input);
const dispatchSpy = vi.spyOn(input, 'dispatchEvent');
applyGeneratedPassword(input, 'sCMtTJkF%GN^mF#-N6D%');
expect(input.value).toBe('sCMtTJkF%GN^mF#-N6D%');
expect(input.type).toBe('text');
expect(dispatchSpy).toHaveBeenCalled();
const evt = dispatchSpy.mock.calls.find(c => c[0] instanceof InputEvent)?.[0] as InputEvent;
expect(evt).toBeDefined();
expect(evt.type).toBe('input');
expect(evt.bubbles).toBe(true);
document.body.removeChild(input);
});
it('bubbling listener fires when applyGeneratedPassword is called', () => {
const input = document.createElement('input');
document.body.appendChild(input);
let listenerFired = false;
input.addEventListener('input', () => { listenerFired = true; });
applyGeneratedPassword(input, 'newpass');
expect(listenerFired).toBe(true);
document.body.removeChild(input);
});
});

View File

@@ -29,6 +29,15 @@ import { wireTotpPreview, wireTotpQr } from '../../../shared/form-affordances/to
import { wireNotesMonoToggle } from '../../../shared/form-affordances/notes-tools'; import { wireNotesMonoToggle } from '../../../shared/form-affordances/notes-tools';
import { scheduleRate } from '../../../setup/setup-helpers'; import { scheduleRate } from '../../../setup/setup-helpers';
/// Sets a generated password on an input, reveals it as plain text, then
/// dispatches a synthetic InputEvent so listeners (e.g. the strength meter)
/// re-evaluate the new value.
export function applyGeneratedPassword(input: HTMLInputElement, value: string): void {
input.value = value;
input.type = 'text';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
}
/// Called by the dispatcher before each render. Stops any in-flight /// Called by the dispatcher before each render. Stops any in-flight
/// tickers / intervals / listeners the previous view may have attached. /// tickers / intervals / listeners the previous view may have attached.
export function teardown(): void { export function teardown(): void {
@@ -433,7 +442,7 @@ export function renderForm(
context: 'fill-field', context: 'fill-field',
onPicked: (value) => { onPicked: (value) => {
const pw = document.getElementById('f-password') as HTMLInputElement | null; const pw = document.getElementById('f-password') as HTMLInputElement | null;
if (pw) { pw.value = value; pw.type = 'text'; } if (pw) applyGeneratedPassword(pw, value);
}, },
}); });
}); });