diff --git a/CHANGELOG.md b/CHANGELOG.md index 0639555..4589108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to the full browser extension will be documented in this fil The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased +### Fixed +- Record text fields updated via JS with no associated events. + ## 0.2.3 - 2026-08-26 ### Fixed - Record a clear statement before send keys when a text element has a preexisting value. diff --git a/CHANGELOG.rec.md b/CHANGELOG.rec.md index ea072da..a21263d 100644 --- a/CHANGELOG.rec.md +++ b/CHANGELOG.rec.md @@ -3,6 +3,10 @@ All notable changes to the recorder browser extension will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased +### Fixed +- Record text fields updated via JS with no associated events. + ## 0.2.3 - 2026-08-26 ### Fixed - Record a clear statement before send keys when a text element has a preexisting value. diff --git a/source/ContentScript/recorder.ts b/source/ContentScript/recorder.ts index 37298bc..ef5ae2b 100644 --- a/source/ContentScript/recorder.ts +++ b/source/ContentScript/recorder.ts @@ -84,6 +84,12 @@ class Recorder { private focusedInput: {element: Element; value: string} | null = null; + // Value of each input when it gained focus / was last recorded + private baselineValues = new WeakMap(); + + // Inputs the user has edited via keyboard or paste with no 'change' event seen yet + private editedInputs = new WeakSet(); + async sendZestScriptToZAP( zestStatement: ZestStatement, params: {sendCache: boolean; notify: boolean} @@ -305,6 +311,10 @@ class Recorder { this.focusedInput?.element === target || (this.isTrackedInputType(target) && !!target.defaultValue); this.focusedInput = null; + this.editedInputs.delete(target); + if (this.isTrackedInputType(target)) { + this.baselineValues.set(target, target.value); + } if (hadPreExistingValue) { this.sendZestScriptToZAP(new ZestStatementElementClear(elementLocator), { sendCache: false, @@ -319,6 +329,38 @@ class Recorder { this.handleCachedSubmit(); } + handleKeyEdit(event: KeyboardEvent): void { + const target = event.target as HTMLElement; + if (!this.isTrackedInputType(target)) return; + const isEditKey = + event.key.length === 1 || + event.key === 'Backspace' || + event.key === 'Delete'; + if (isEditKey) this.editedInputs.add(target); + } + + handleFocusIn(event: Event): void { + const target = event.target as HTMLElement; + if (this.isTrackedInputType(target)) { + this.baselineValues.set(target, target.value); + this.editedInputs.delete(target); + } + } + + // Some sites (e.g. jQuery Inputmask) set the value from script and never fire + // a native 'change' event, so fall back to comparing the value on blur. + handleFocusOut( + params: {level: number; frame: number; element: Document}, + event: Event + ): void { + const target = event.target as HTMLElement; + if (!this.isTrackedInputType(target)) return; + if (!this.editedInputs.has(target)) return; + this.editedInputs.delete(target); + if (this.baselineValues.get(target) === target.value) return; + this.handleChange(params, event); + } + handleKeypress( params: {level: number; frame: number; element: Document}, event: KeyboardEvent @@ -483,6 +525,25 @@ class Recorder { 'change', this.handleChange.bind(this, {level, frame, element}) ); + element.addEventListener('keydown', this.handleKeyEdit.bind(this), { + capture: true, + }); + element.addEventListener( + 'paste', + (event: Event) => { + const target = event.target as HTMLElement; + if (this.isTrackedInputType(target)) this.editedInputs.add(target); + }, + {capture: true} + ); + element.addEventListener('focusin', this.handleFocusIn.bind(this), { + capture: true, + }); + element.addEventListener( + 'focusout', + this.handleFocusOut.bind(this, {level, frame, element}), + {capture: true} + ); element.addEventListener( 'mousedown', (event: Event) => { diff --git a/test/ContentScript/integrationTests.test.ts b/test/ContentScript/integrationTests.test.ts index e185c73..cc40d06 100644 --- a/test/ContentScript/integrationTests.test.ts +++ b/test/ContentScript/integrationTests.test.ts @@ -692,6 +692,104 @@ function integrationTests( ]); }); + test('Should record input set by script without native change event', async () => { + // Given / When + await driver.toggleRecording(); + const wd = await driver.getWebDriver(); + await wd.get(`http://localhost:${_HTTPPORT}/webpages/scripted-mask.html`); + await pageLoaded(wd); + await wd.findElement(By.id('masked-input')).sendKeys('12345'); + await wd.findElement(By.id('submit')).click(); + await eventsProcessed(); + // Then + expect(actualData).toEqual([ + reportZestStatementComment(), + reportZestStatementLaunch( + `http://localhost:${_HTTPPORT}/webpages/scripted-mask.html` + ), + reportZestStatementScrollTo(3, 'masked-input'), + reportZestStatementSendKeys(4, 'masked-input', '12345'), + reportZestStatementScrollTo(5, 'submit'), + reportZestStatementClick(6, 'submit'), + ]); + }); + + test('Should not record value set by script without user edit', async () => { + // Given / When + await driver.toggleRecording(); + const wd = await driver.getWebDriver(); + await wd.get(`http://localhost:${_HTTPPORT}/webpages/scripted-update.html`); + await pageLoaded(wd); + const input = await wd.findElement(By.id('auto-input')); + await input.click(); + await wd.wait( + async () => (await input.getAttribute('value')) === 'set by script', + 5000 + ); + await input.sendKeys(Key.TAB); + await wd.findElement(By.id('submit')).click(); + await eventsProcessed(); + // Then + expect(actualData).toEqual([ + reportZestStatementComment(), + reportZestStatementLaunch( + `http://localhost:${_HTTPPORT}/webpages/scripted-update.html` + ), + reportZestStatementScrollTo(3, 'auto-input'), + reportZestStatementClick(4, 'auto-input'), + reportZestStatementScrollTo(5, 'submit'), + reportZestStatementClick(6, 'submit'), + ]); + }); + + test('Should record final value after script handled deletion', async () => { + // Given / When + await driver.toggleRecording(); + const wd = await driver.getWebDriver(); + await wd.get(`http://localhost:${_HTTPPORT}/webpages/scripted-update.html`); + await pageLoaded(wd); + await wd + .findElement(By.id('masked-input')) + .sendKeys('123', Key.BACK_SPACE, '4'); + await wd.findElement(By.id('submit')).click(); + await eventsProcessed(); + // Then + expect(actualData).toEqual([ + reportZestStatementComment(), + reportZestStatementLaunch( + `http://localhost:${_HTTPPORT}/webpages/scripted-update.html` + ), + reportZestStatementScrollTo(3, 'masked-input'), + reportZestStatementSendKeys(4, 'masked-input', '124'), + reportZestStatementScrollTo(5, 'submit'), + reportZestStatementClick(6, 'submit'), + ]); + }); + + test('Should not record scripted input when only focus moves', async () => { + // Given / When + await driver.toggleRecording(); + const wd = await driver.getWebDriver(); + await wd.get(`http://localhost:${_HTTPPORT}/webpages/scripted-update.html`); + await pageLoaded(wd); + const input = await wd.findElement(By.id('masked-input')); + await input.click(); + await input.sendKeys(Key.TAB); + await wd.findElement(By.id('submit')).click(); + await eventsProcessed(); + // Then + expect(actualData).toEqual([ + reportZestStatementComment(), + reportZestStatementLaunch( + `http://localhost:${_HTTPPORT}/webpages/scripted-update.html` + ), + reportZestStatementScrollTo(3, 'masked-input'), + reportZestStatementClick(4, 'masked-input'), + reportZestStatementScrollTo(5, 'submit'), + reportZestStatementClick(6, 'submit'), + ]); + }); + test('Should not record clear on automatically reset input', async () => { // Given / When await driver.toggleRecording(); diff --git a/test/ContentScript/webpages/scripted-mask.html b/test/ContentScript/webpages/scripted-mask.html new file mode 100644 index 0000000..c14844d --- /dev/null +++ b/test/ContentScript/webpages/scripted-mask.html @@ -0,0 +1,19 @@ + + + + Scripted Mask Test + + + + + + + diff --git a/test/ContentScript/webpages/scripted-update.html b/test/ContentScript/webpages/scripted-update.html new file mode 100644 index 0000000..4125430 --- /dev/null +++ b/test/ContentScript/webpages/scripted-update.html @@ -0,0 +1,34 @@ + + + + Scripted Update Test + + + + + + + +