diff --git a/extension/src/popup/components/types/__tests__/login.test.ts b/extension/src/popup/components/types/__tests__/login.test.ts index 7077847..4dc46a6 100644 --- a/extension/src/popup/components/types/__tests__/login.test.ts +++ b/extension/src/popup/components/types/__tests__/login.test.ts @@ -26,7 +26,7 @@ vi.mock('../../../../setup/setup-helpers', () => ({ entropyText: vi.fn(() => ''), })); -import { renderForm } from '../login'; +import { renderForm, applyGeneratedPassword } from '../login'; import { sendMessage } from '../../../../shared/state'; describe('login form smart inputs', () => { @@ -154,3 +154,37 @@ describe('Login save shape', () => { 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); + }); +}); diff --git a/extension/src/popup/components/types/login.ts b/extension/src/popup/components/types/login.ts index f21bef7..d051e57 100644 --- a/extension/src/popup/components/types/login.ts +++ b/extension/src/popup/components/types/login.ts @@ -29,6 +29,15 @@ import { wireTotpPreview, wireTotpQr } from '../../../shared/form-affordances/to import { wireNotesMonoToggle } from '../../../shared/form-affordances/notes-tools'; 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 /// tickers / intervals / listeners the previous view may have attached. export function teardown(): void { @@ -433,7 +442,7 @@ export function renderForm( context: 'fill-field', onPicked: (value) => { const pw = document.getElementById('f-password') as HTMLInputElement | null; - if (pw) { pw.value = value; pw.type = 'text'; } + if (pw) applyGeneratedPassword(pw, value); }, }); });