From c3e55835aa6ed13db673d4ab5101c1c60306ce00 Mon Sep 17 00:00:00 2001 From: Simon Bennetts Date: Wed, 27 Nov 2024 17:13:12 +0000 Subject: [PATCH] Record enter key presses in input fields Signed-off-by: Simon Bennetts --- source/ContentScript/recorder.ts | 110 +++++++++++++++++++++-- source/types/zestScript/ZestStatement.ts | 47 ++++++++++ source/utils/constants.ts | 2 + 3 files changed, 153 insertions(+), 6 deletions(-) diff --git a/source/ContentScript/recorder.ts b/source/ContentScript/recorder.ts index f641070..ce905ac 100644 --- a/source/ContentScript/recorder.ts +++ b/source/ContentScript/recorder.ts @@ -23,6 +23,7 @@ import { ZestStatement, ZestStatementElementClick, ZestStatementElementSendKeys, + ZestStatementElementSubmit, ZestStatementLaunchBrowser, ZestStatementSwitchToFrame, } from '../types/zestScript/ZestStatement'; @@ -44,7 +45,16 @@ class Recorder { isNotificationRaised = false; - async sendZestScriptToZAP(zestStatement: ZestStatement): Promise { + // Enter keydown events often occur before the input change events, so we reorder them if needed + cachedSubmit?: ZestStatementElementSubmit; + + async sendZestScriptToZAP( + zestStatement: ZestStatement, + sendCache: boolean + ): Promise { + if (sendCache) { + this.handleCachedSubmit(); + } this.notify(zestStatement); return Browser.runtime.sendMessage({ type: ZEST_SCRIPT, @@ -52,20 +62,30 @@ class Recorder { }); } + handleCachedSubmit(): void { + if (this.cachedSubmit) { + this.sendZestScriptToZAP(this.cachedSubmit, false); + delete this.cachedSubmit; + } + } + handleFrameSwitches(level: number, frameIndex: number): void { if (this.curLevel === level && this.curFrame === frameIndex) { return; } if (this.curLevel > level) { while (this.curLevel > level) { - this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(-1)); + this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(-1), true); this.curLevel -= 1; } this.curFrame = frameIndex; } else { this.curLevel += 1; this.curFrame = frameIndex; - this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(frameIndex)); + this.sendZestScriptToZAP( + new ZestStatementSwitchToFrame(frameIndex), + true + ); } if (this.curLevel !== level) { console.log('Error in switching frames'); @@ -81,7 +101,10 @@ class Recorder { this.handleFrameSwitches(level, frame); console.log(event, 'clicked'); const elementLocator = getPath(event.target as HTMLElement, element); - this.sendZestScriptToZAP(new ZestStatementElementClick(elementLocator)); + this.sendZestScriptToZAP( + new ZestStatementElementClick(elementLocator), + true + ); // click on target element } @@ -118,12 +141,38 @@ class Recorder { this.handleFrameSwitches(level, frame); console.log(event, 'change', (event.target as HTMLInputElement).value); const elementLocator = getPath(event.target as HTMLElement, element); + // Send the keys before a cached submit statement on the same element + if ( + this.cachedSubmit && + this.cachedSubmit.elementLocator.element !== elementLocator.element + ) { + // The cached submit was not on the same element, so send it + this.handleCachedSubmit(); + } this.sendZestScriptToZAP( new ZestStatementElementSendKeys( elementLocator, (event.target as HTMLInputElement).value - ) + ), + false ); + // Now send the cached submit, if there still is one + this.handleCachedSubmit(); + } + + handleKeypress( + params: {level: number; frame: number; element: Document}, + event: KeyboardEvent + ): void { + if (!this.shouldRecord(event.target as HTMLElement)) return; + const {element} = params; + if (event.key === 'Enter') { + this.handleCachedSubmit(); + const elementLocator = getPath(event.target as HTMLElement, element); + console.log('Enter key pressed', elementLocator); + // Cache the statement as it often occurs before the change event occurs + this.cachedSubmit = new ZestStatementElementSubmit(elementLocator); + } } handleResize(): void { @@ -145,6 +194,9 @@ class Recorder { level: number, frame: number ): void { + // A list of all of the text elements that we have added event listeners to + const textElements = new Set(); + element.addEventListener( 'click', this.handleClick.bind(this, {level, frame, element}) @@ -173,6 +225,45 @@ class Recorder { i += 1; } }); + + // Add listeners to all of the text fields + element.querySelectorAll('input').forEach((input) => { + if (!textElements.has(input)) { + textElements.add(input); + input.addEventListener( + 'keydown', + this.handleKeypress.bind(this, {level, frame, element}) + ); + } + }); + // Observer callback function to handle DOM mutations to detect added text fields + const domMutated: MutationCallback = (mutationsList: MutationRecord[]) => { + mutationsList.forEach((mutation) => { + if (mutation.type === 'childList') { + // Look for added input elements + if (mutation.target instanceof Element) { + const inputs = mutation.target.getElementsByTagName('input'); + for (let j = 0; j < inputs.length; j += 1) { + const input = inputs[j]; + if (!textElements.has(input)) { + textElements.add(input); + input.addEventListener( + 'keydown', + this.handleKeypress.bind(this, {level, frame, element}) + ); + } + } + } + } + }); + }; + + const observer = new MutationObserver(domMutated); + observer.observe(document, { + attributes: false, + childList: true, + subtree: true, + }); } shouldRecord(element: HTMLElement): boolean { @@ -196,7 +287,10 @@ class Recorder { // send window resize event to ensure same size const browserType = this.getBrowserName(); const url = window.location.href; - this.sendZestScriptToZAP(new ZestStatementLaunchBrowser(browserType, url)); + this.sendZestScriptToZAP( + new ZestStatementLaunchBrowser(browserType, url), + true + ); this.handleResize(); } @@ -221,6 +315,7 @@ class Recorder { stopRecordingUserInteractions(): void { console.log('Stopping Recording User Interactions ...'); + this.handleCachedSubmit(); Browser.storage.sync.set({zaprecordingactive: false}); this.active = false; const floatingDiv = document.getElementById('ZapfloatingDiv'); @@ -367,6 +462,9 @@ class Recorder { } else if (stmt instanceof ZestStatementElementSendKeys) { notifyMessage.title = 'Send Keys'; notifyMessage.message = `${stmt.elementLocator.element}: ${stmt.keys}`; + } else if (stmt instanceof ZestStatementElementSubmit) { + notifyMessage.title = 'Submit'; + notifyMessage.message = `${stmt.elementLocator.element}`; } else if (stmt instanceof ZestStatementLaunchBrowser) { notifyMessage.title = 'Launch Browser'; notifyMessage.message = stmt.browserType; diff --git a/source/types/zestScript/ZestStatement.ts b/source/types/zestScript/ZestStatement.ts index f9b3355..7597563 100644 --- a/source/types/zestScript/ZestStatement.ts +++ b/source/types/zestScript/ZestStatement.ts @@ -23,9 +23,11 @@ import { ZEST_CLIENT_ELEMENT_CLICK, ZEST_CLIENT_ELEMENT_MOUSE_OVER, ZEST_CLIENT_ELEMENT_SEND_KEYS, + ZEST_CLIENT_ELEMENT_SUBMIT, ZEST_CLIENT_LAUNCH, ZEST_CLIENT_SWITCH_TO_FRAME, ZEST_CLIENT_WINDOW_CLOSE, + ZEST_COMMENT, } from '../../utils/constants'; class ElementLocator { @@ -91,6 +93,26 @@ class ZestStatementLaunchBrowser extends ZestStatement { } } +class ZestComment extends ZestStatement { + comment: string; + + windowHandle: string; + + constructor(comment: string) { + super(ZEST_COMMENT); + this.comment = comment; + } + + toJSON(): string { + return JSON.stringify({ + index: this.index, + enabled: true, + elementType: this.elementType, + comment: this.comment, + }); + } +} + abstract class ZestStatementElement extends ZestStatement { elementLocator: ElementLocator; @@ -147,6 +169,29 @@ class ZestStatementElementSendKeys extends ZestStatementElement { } } +class ZestStatementElementSubmit extends ZestStatementElement { + keys: string; + + constructor( + elementLocator: ElementLocator, + windowHandle = DEFAULT_WINDOW_HANDLE + ) { + super(ZEST_CLIENT_ELEMENT_SUBMIT, elementLocator); + this.windowHandle = windowHandle; + } + + toJSON(): string { + return JSON.stringify({ + value: this.keys, + windowHandle: this.windowHandle, + ...this.elementLocator.toJSON(), + index: this.index, + enabled: true, + elementType: this.elementType, + }); + } +} + class ZestStatementElementClear extends ZestStatementElement { constructor( elementLocator: ElementLocator, @@ -234,12 +279,14 @@ class ZestStatementElementMouseOver extends ZestStatementElement { export { ElementLocator, + ZestComment, ZestStatement, ZestStatementLaunchBrowser, ZestStatementElementMouseOver, ZestStatementElementClick, ZestStatementSwitchToFrame, ZestStatementElementSendKeys, + ZestStatementElementSubmit, ZestStatementElementClear, ZestStatementWindowClose, }; diff --git a/source/utils/constants.ts b/source/utils/constants.ts index 9955504..0135018 100644 --- a/source/utils/constants.ts +++ b/source/utils/constants.ts @@ -20,10 +20,12 @@ export const ZEST_CLIENT_SWITCH_TO_FRAME = 'ZestClientSwitchToFrame'; export const ZEST_CLIENT_ELEMENT_CLICK = 'ZestClientElementClick'; export const ZEST_CLIENT_ELEMENT_SEND_KEYS = 'ZestClientElementSendKeys'; +export const ZEST_CLIENT_ELEMENT_SUBMIT = 'ZestClientElementSubmit'; export const ZEST_CLIENT_LAUNCH = 'ZestClientLaunch'; export const ZEST_CLIENT_ELEMENT_CLEAR = 'ZestClientElementClear'; export const ZEST_CLIENT_WINDOW_CLOSE = 'ZestClientWindowClose'; export const ZEST_CLIENT_ELEMENT_MOUSE_OVER = 'ZestClientMouseOverElement'; +export const ZEST_COMMENT = 'ZestComment'; export const DEFAULT_WINDOW_HANDLE = 'windowHandle1'; export const ZAP_STOP_RECORDING = 'zapStopRecording';