Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.rec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
61 changes: 61 additions & 0 deletions source/ContentScript/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element, string>();

// Inputs the user has edited via keyboard or paste with no 'change' event seen yet
private editedInputs = new WeakSet<Element>();

async sendZestScriptToZAP(
zestStatement: ZestStatement,
params: {sendCache: boolean; notify: boolean}
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down
98 changes: 98 additions & 0 deletions test/ContentScript/integrationTests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
19 changes: 19 additions & 0 deletions test/ContentScript/webpages/scripted-mask.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Scripted Mask Test</title>
</head>
<body>
<input type="text" id="masked-input" />
<button id="submit">Submit</button>
<script>
// Mimics jQuery Inputmask: takes over typing, sets the value from script
// and never dispatches a native input/change event.
const input = document.getElementById('masked-input');
input.addEventListener('keypress', (e) => {
e.preventDefault();
input.value += e.key;
});
</script>
</body>
</html>
34 changes: 34 additions & 0 deletions test/ContentScript/webpages/scripted-update.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Scripted Update Test</title>
</head>
<body>
<input type="text" id="auto-input" />
<input type="text" id="masked-input" />
<button id="submit">Submit</button>
<script>
// Value is changed by script shortly after focus, without any user edit.
const auto = document.getElementById('auto-input');
auto.addEventListener('focus', () => {
setTimeout(() => {
auto.value = 'set by script';
}, 100);
});

// Mimics jQuery Inputmask: handles typing and deletion from script and
// never dispatches a native input/change event.
const masked = document.getElementById('masked-input');
masked.addEventListener('keypress', (e) => {
e.preventDefault();
masked.value += e.key;
});
masked.addEventListener('keydown', (e) => {
if (e.key === 'Backspace') {
e.preventDefault();
masked.value = masked.value.slice(0, -1);
}
});
</script>
</body>
</html>
Loading