mirror of https://github.com/garrytan/gstack.git
fix(browse): dispatch a change event after fill for change-only validators
Playwright's Locator.fill() dispatches `input` but never `change`, so frameworks that validate on change (AngularJS ng-change, debounced strength/match checks) never saw the filled value — correct in the DOM, failing the framework's own validation. `browse fill` now dispatches `change` after the fill. Failing-first regression test with a change-only password-match fixture included. Contributed by @intelliot (PR #2475). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0572125a5b
commit
65b577f133
|
|
@ -377,11 +377,14 @@ export async function handleWriteCommand(
|
||||||
const value = valueParts.join(' ');
|
const value = valueParts.join(' ');
|
||||||
if (!selector || !value) throw new Error('Usage: browse fill <selector> <value>');
|
if (!selector || !value) throw new Error('Usage: browse fill <selector> <value>');
|
||||||
const resolved = await session.resolveRef(selector);
|
const resolved = await session.resolveRef(selector);
|
||||||
if ('locator' in resolved) {
|
const locator = 'locator' in resolved ? resolved.locator : target.locator(resolved.selector);
|
||||||
await resolved.locator.fill(value, { timeout: 5000 });
|
await locator.fill(value, { timeout: 5000 });
|
||||||
} else {
|
// Playwright's fill() only dispatches an `input` event. Frameworks that
|
||||||
await target.locator(resolved.selector).fill(value, { timeout: 5000 });
|
// 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)
|
// Wait for network to settle (form validation XHRs)
|
||||||
await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
|
await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
|
||||||
return `Filled ${selector}`;
|
return `Filled ${selector}`;
|
||||||
|
|
|
||||||
|
|
@ -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<typeof startTestServer>;
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Test Page - Change-Only Validator</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Change-Only Validator</h1>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Minimal repro of AngularJS ng-change / debounced cross-field validators
|
||||||
|
(e.g. cPanel's Jupiter theme "Add FTP Account" password-match check):
|
||||||
|
the listener only reacts to `change`, never `input`. A page like this
|
||||||
|
silently "loses" a Playwright-style value-set-without-a-change-event.
|
||||||
|
-->
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
<input type="password" id="password2" name="password2">
|
||||||
|
<div id="match-status">unknown</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function checkMatch() {
|
||||||
|
var a = document.getElementById('password').value;
|
||||||
|
var b = document.getElementById('password2').value;
|
||||||
|
document.getElementById('match-status').textContent =
|
||||||
|
a && a === b ? 'match' : 'no-match';
|
||||||
|
}
|
||||||
|
document.getElementById('password').addEventListener('change', checkMatch);
|
||||||
|
document.getElementById('password2').addEventListener('change', checkMatch);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue