diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 4a847141d..2155e6f23 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -377,11 +377,14 @@ export async function handleWriteCommand( const value = valueParts.join(' '); if (!selector || !value) throw new Error('Usage: browse fill '); const resolved = await session.resolveRef(selector); - if ('locator' in resolved) { - await resolved.locator.fill(value, { timeout: 5000 }); - } else { - await target.locator(resolved.selector).fill(value, { timeout: 5000 }); - } + const locator = 'locator' in resolved ? resolved.locator : target.locator(resolved.selector); + await locator.fill(value, { timeout: 5000 }); + // Playwright's fill() only dispatches an `input` event. Frameworks that + // validate on `change` (AngularJS ng-change, debounced strength/match + // checks — e.g. cPanel's Jupiter theme) never see the update, so a value + // that's correct in the DOM can still fail the framework's own + // validation. Dispatch `change` too so those listeners fire. + await locator.dispatchEvent('change'); // Wait for network to settle (form validation XHRs) await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {}); return `Filled ${selector}`; diff --git a/browse/test/fill-change-event.test.ts b/browse/test/fill-change-event.test.ts new file mode 100644 index 000000000..c11c88e10 --- /dev/null +++ b/browse/test/fill-change-event.test.ts @@ -0,0 +1,57 @@ +/** + * Regression test for `browse fill` on change-only validators. + * + * Playwright's Locator.fill() dispatches an `input` event but not `change`. + * Frameworks that validate on `change` (AngularJS ng-change, debounced + * strength/match checks — e.g. cPanel's Jupiter theme "Add FTP Account" + * password-match check) never see the update: the DOM value is correct but + * the framework's own validator still reports a mismatch. + */ + +import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; +import { startTestServer } from './test-server'; +import { BrowserManager } from '../src/browser-manager'; +import { handleWriteCommand as _handleWriteCommand } from '../src/write-commands'; + +const handleWriteCommand = (cmd: string, args: string[], b: BrowserManager) => + _handleWriteCommand(cmd, args, b.getActiveSession(), b); + +let testServer: ReturnType; +let bm: BrowserManager; +let baseUrl: string; + +beforeAll(async () => { + testServer = startTestServer(0); + baseUrl = testServer.url; + bm = new BrowserManager(); + await bm.launch(); +}); + +afterAll(async () => { + try { testServer.server.stop(); } catch {} + // Close only this file's own browser — never process.exit(): bun test runs + // all files in one process, so a delayed exit kills the whole suite + // (see test/no-suicide-exit.test.ts). close() can hang when the browser + // already died, so race it at 3s and abandon; the child is reaped at exit. + try { await Promise.race([bm?.close(), new Promise((resolve) => setTimeout(resolve, 3000))]); } catch {} +}); + +describe('fill dispatches change event', () => { + test('a change-only validator sees the filled value', async () => { + await handleWriteCommand('goto', [baseUrl + '/change-only-validator.html'], bm); + await handleWriteCommand('fill', ['#password', 'hello123'], bm); + await handleWriteCommand('fill', ['#password2', 'hello123'], bm); + + const status = await bm.getPage().locator('#match-status').textContent(); + expect(status).toBe('match'); + }); + + test('a change-only validator still catches a real mismatch', async () => { + await handleWriteCommand('goto', [baseUrl + '/change-only-validator.html'], bm); + await handleWriteCommand('fill', ['#password', 'hello123'], bm); + await handleWriteCommand('fill', ['#password2', 'different'], bm); + + const status = await bm.getPage().locator('#match-status').textContent(); + expect(status).toBe('no-match'); + }); +}); diff --git a/browse/test/fixtures/change-only-validator.html b/browse/test/fixtures/change-only-validator.html new file mode 100644 index 000000000..422fd6904 --- /dev/null +++ b/browse/test/fixtures/change-only-validator.html @@ -0,0 +1,31 @@ + + + + + Test Page - Change-Only Validator + + +

Change-Only Validator

+ + + + +
unknown
+ + + +