From 0ebbff82bc423c506c382897bd6a102fe8a38813 Mon Sep 17 00:00:00 2001 From: CxPedroNascimento <174706762+cx-pedro-nascimento@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:34:07 +0000 Subject: [PATCH 1/2] feat: record popup window interactions with correct statement ordering Detect popup windows via window.opener in the content script and register them with the background service worker (ZAP_REGISTER_POPUP). The background assigns a new windowHandle, emits ZestActionSleep(10000) **before** ZestClientWindowHandle so ZAP waits for the popup to load before trying to locate it. All recorder statements in popup windows use the assigned handle. Adds ZestStatementWindowHandle and ZestStatementActionSleep types, bumps version to 0.1.9, adds the tabs permission to the recorder manifest, and includes unit tests verifying the sleep-before-window-handle ordering. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: CxPedroNascimento <174706762+cx-pedro-nascimento@users.noreply.github.com> --- __mocks__/webextension-polyfill.ts | 10 +++ source/Background/index.ts | 99 ++++++++++++++++++++++-- source/ContentScript/index.ts | 48 +++++++++--- source/ContentScript/recorder.ts | 24 ++++-- source/manifest.json | 2 +- source/manifest.rec.json | 5 +- source/types/zestScript/ZestStatement.ts | 48 ++++++++++++ source/utils/constants.ts | 4 + test/ContentScript/unitTests.test.ts | 47 +++++++++++ 9 files changed, 259 insertions(+), 28 deletions(-) diff --git a/__mocks__/webextension-polyfill.ts b/__mocks__/webextension-polyfill.ts index 6c067473..41862c39 100644 --- a/__mocks__/webextension-polyfill.ts +++ b/__mocks__/webextension-polyfill.ts @@ -56,6 +56,16 @@ const Browser = { }, tabs: { query: jest.fn(), + sendMessage: jest.fn(), + onCreated: { + addListener: jest.fn(), + }, + onUpdated: { + addListener: jest.fn(), + }, + onRemoved: { + addListener: jest.fn(), + }, }, }; diff --git a/source/Background/index.ts b/source/Background/index.ts index 9401275a..1082af19 100644 --- a/source/Background/index.ts +++ b/source/Background/index.ts @@ -21,8 +21,13 @@ import 'emoji-log'; import Browser, {Cookies, Runtime} from 'webextension-polyfill'; import {ReportedStorage} from '../types/ReportedModel'; import {ZestScript, ZestScriptMessage} from '../types/zestScript/ZestScript'; -import {ZestStatementWindowClose} from '../types/zestScript/ZestStatement'; import { + ZestStatementActionSleep, + ZestStatementWindowClose, + ZestStatementWindowHandle, +} from '../types/zestScript/ZestStatement'; +import { + DEFAULT_WINDOW_HANDLE, GET_ZEST_SCRIPT, IS_FULL_EXTENSION, LOCAL_STORAGE, @@ -31,6 +36,8 @@ import { RESET_ZEST_SCRIPT, SESSION_STORAGE, STOP_RECORDING, + ZAP_GET_WINDOW_HANDLE, + ZAP_REGISTER_POPUP, ZEST_SCRIPT, } from '../utils/constants'; @@ -41,6 +48,9 @@ console.log('ZAP Service Worker 👋'); */ const reportedStorage = new Set(); const zestScript = new ZestScript(); + +let windowHandleCounter = 1; +const popupTabHandles = new Map(); /* A callback URL will only be available if the browser has been launched from ZAP, otherwise call the individual endpoints */ @@ -233,6 +243,8 @@ async function handleMessage( case RESET_ZEST_SCRIPT: zestScript.reset(); + windowHandleCounter = 1; + popupTabHandles.clear(); break; case STOP_RECORDING: { @@ -246,6 +258,8 @@ async function handleMessage( sendZestScriptToZAP(data, zapkey, zapurl); } } + windowHandleCounter = 1; + popupTabHandles.clear(); break; } @@ -260,19 +274,78 @@ async function handleMessage( async function onMessageHandler( message: unknown, _sender: Runtime.MessageSender -): Promise { +): Promise { + const msg = message as MessageEvent; + if (msg.type === ZAP_GET_WINDOW_HANDLE) { + const tabId = _sender.tab?.id; + const handle = tabId + ? (popupTabHandles.get(tabId) ?? DEFAULT_WINDOW_HANDLE) + : DEFAULT_WINDOW_HANDLE; + return Promise.resolve(handle); + } + + if (msg.type === ZAP_REGISTER_POPUP) { + // Called by the content script when it detects window.opener !== null. + // Assign a new window handle for this popup tab if not already registered. + const tabId = _sender.tab?.id; + const popupUrl = (msg as unknown as {url?: string}).url ?? ''; + + if (tabId !== undefined) { + if (popupTabHandles.has(tabId)) { + return Promise.resolve(popupTabHandles.get(tabId)!); + } + windowHandleCounter += 1; + const handle = `windowHandle${windowHandleCounter}`; + popupTabHandles.set(tabId, handle); + + // Emit ZestClientWindowHandle so ZAP's runner can locate and register + // this popup window by URL before replaying any popup interactions. + // Use regex=true with the origin (scheme+host) so that URL parameters + // that change between sessions (nonce, state, etc.) don't break the match. + let urlPattern = '.*'; + try { + const parsed = new URL(popupUrl); + urlPattern = `${parsed.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*`; + } catch { + // popupUrl is empty or invalid; fall back to match-all + } + const sleepStmt = new ZestStatementActionSleep(10000); + const sleepStmtData = zestScript.addStatement(sleepStmt.toJSON()); + const stmt = new ZestStatementWindowHandle(handle, urlPattern, true); + const stmtData = zestScript.addStatement(stmt.toJSON()); + if (IS_FULL_EXTENSION) { + const items = await Browser.storage.sync.get({ + zapurl: 'http://zap/', + zapkey: 'not set', + }); + sendZestScriptToZAP( + sleepStmtData, + items.zapkey as string, + items.zapurl as string + ); + sendZestScriptToZAP( + stmtData, + items.zapkey as string, + items.zapurl as string + ); + } + + return Promise.resolve(handle); + } + return Promise.resolve(DEFAULT_WINDOW_HANDLE); + } let val: number | ZestScriptMessage = 2; const items = await Browser.storage.sync.get({ zapurl: 'http://zap/', zapkey: 'not set', }); - const msg = await handleMessage( - message as MessageEvent, + const result = await handleMessage( + msg, items.zapurl as string, items.zapkey as string ); - if (!(typeof msg === 'boolean')) { - val = msg; + if (!(typeof result === 'boolean')) { + val = result; } return Promise.resolve(val); } @@ -296,6 +369,20 @@ function cookieChangeHandler( Browser.runtime.onMessage.addListener(onMessageHandler); +Browser.tabs.onRemoved.addListener(async (tabId) => { + const handle = popupTabHandles.get(tabId); + if (!handle) return; + popupTabHandles.delete(tabId); + + const items = await Browser.storage.sync.get({ + zapurl: 'http://zap/', + zapkey: 'not set', + }); + const stmt = new ZestStatementWindowClose(0, handle); + const data = zestScript.addStatement(stmt.toJSON()); + sendZestScriptToZAP(data, items.zapkey as string, items.zapurl as string); +}); + if (IS_FULL_EXTENSION) { Browser.action.onClicked.addListener((_tab: Browser.Tabs.Tab) => { Browser.runtime.openOptionsPage(); diff --git a/source/ContentScript/index.ts b/source/ContentScript/index.ts index 77346404..32fd71ef 100644 --- a/source/ContentScript/index.ts +++ b/source/ContentScript/index.ts @@ -26,6 +26,7 @@ import { } from '../types/ReportedModel'; import Recorder from './recorder'; import { + DEFAULT_WINDOW_HANDLE, IS_FULL_EXTENSION, LOCAL_STORAGE, LOCAL_ZAP_ENABLE, @@ -35,6 +36,7 @@ import { SESSION_STORAGE, URL_ZAP_ENABLE, URL_ZAP_RECORD, + ZAP_REGISTER_POPUP, ZAP_START_RECORDING, ZAP_STOP_RECORDING, } from '../utils/constants'; @@ -302,18 +304,37 @@ function injectScript(): Promise { return new Promise((resolve) => { configureExtension(); withZapRecordingActive(() => { - Browser.storage.sync - .get({initScript: false, loginUrl: '', startTime: 0}) - .then((items) => { - console.log( - `ZAP injectScript items ${items.initScript} ${items.loginUrl}` - ); - recorder.recordUserInteractions( - items.initScript === true, - items.loginUrl as string, - items.startTime as number - ); - }); + // Detect popup windows using window.opener: if non-null, this page was + // opened via window.open() (e.g. OAuth login popup). This works in all + // modes (private, normal) and all browsers, unlike background-side detection. + const isWindowPopup = window.opener !== null; + const handlePromise = isWindowPopup + ? Browser.runtime + .sendMessage({type: ZAP_REGISTER_POPUP, url: window.location.href}) + .catch(() => DEFAULT_WINDOW_HANDLE) + : Promise.resolve(DEFAULT_WINDOW_HANDLE); + + Promise.all([ + Browser.storage.sync.get({ + initScript: false, + loginUrl: '', + startTime: 0, + }), + handlePromise, + ]).then(([items, handle]) => { + const windowHandle = (handle as string) || DEFAULT_WINDOW_HANDLE; + console.log( + `ZAP injectScript items ${items.initScript} ${items.loginUrl} handle=${windowHandle} popup=${isWindowPopup}` + ); + if (isWindowPopup) { + recorder.setWindowHandle(windowHandle); + } + recorder.recordUserInteractions( + isWindowPopup ? false : items.initScript === true, + items.loginUrl as string, + items.startTime as number + ); + }); }); withZapEnableSetting(() => { enableExtension(); @@ -334,6 +355,9 @@ Browser.runtime.onMessage.addListener( ) => { if (message.type === ZAP_START_RECORDING) { configureExtension(); + if (message.windowHandle) { + recorder.setWindowHandle(message.windowHandle); + } recorder.recordUserInteractions(); } else if (message.type === ZAP_STOP_RECORDING) { recorder.stopRecordingUserInteractions(); diff --git a/source/ContentScript/recorder.ts b/source/ContentScript/recorder.ts index c28deebb..5c1d90ec 100644 --- a/source/ContentScript/recorder.ts +++ b/source/ContentScript/recorder.ts @@ -34,6 +34,7 @@ import {ZestScriptMessage} from '../types/zestScript/ZestScript'; import {getPath} from './util'; import {downloadJson} from '../utils/util'; import { + DEFAULT_WINDOW_HANDLE, GET_ZEST_SCRIPT, STOP_RECORDING, ZAP_FLOATING_DIV, @@ -57,6 +58,8 @@ class Recorder { active = false; + windowHandle: string = DEFAULT_WINDOW_HANDLE; + haveListenersBeenAdded = false; floatingWindowInserted = false; @@ -104,7 +107,7 @@ class Recorder { sendScrollToToZap(elementLocator: ElementLocator, waitForMsec: number): void { this.sendZestScriptToZAP( - new ZestStatementElementScrollTo(elementLocator, waitForMsec), + new ZestStatementElementScrollTo(elementLocator, waitForMsec, this.windowHandle), {sendCache: false, notify: false} ); } @@ -131,7 +134,7 @@ class Recorder { } if (this.curLevel > level) { while (this.curLevel > level) { - this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(-1), { + this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(-1, '', this.windowHandle), { sendCache: true, notify: true, }); @@ -141,7 +144,7 @@ class Recorder { } else { this.curLevel += 1; this.curFrame = frameIndex; - this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(frameIndex), { + this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(frameIndex, '', this.windowHandle), { sendCache: true, notify: true, }); @@ -172,7 +175,7 @@ class Recorder { this.sendScrollToToZap(elementLocator, waited); this.sendZestScriptToZAP( - new ZestStatementElementClick(elementLocator, waited), + new ZestStatementElementClick(elementLocator, waited, this.windowHandle), {sendCache: true, notify: true} ); // click on target element @@ -225,7 +228,8 @@ class Recorder { new ZestStatementElementSendKeys( elementLocator, (event.target as HTMLInputElement).value, - waited + waited, + this.windowHandle ), {sendCache: false, notify: true} ); @@ -250,7 +254,8 @@ class Recorder { // Cache the statement as it often occurs before the change event occurs this.cachedSubmit = new ZestStatementElementSubmit( elementLocator, - this.getWaited() + this.getWaited(), + this.windowHandle ); this.cachedTimeStamp = event.timeStamp; // console.log('ZAP Caching submit', this.cachedSubmit); @@ -485,7 +490,8 @@ class Recorder { this.sendZestScriptToZAP( new ZestStatementLaunchBrowser( this.getBrowserName(), - loginUrl !== '' ? loginUrl : window.location.href + loginUrl !== '' ? loginUrl : window.location.href, + this.windowHandle ), {sendCache: true, notify: true} ); @@ -771,6 +777,10 @@ class Recorder { }, 100); }); } + + setWindowHandle(handle: string): void { + this.windowHandle = handle; + } } export default Recorder; diff --git a/source/manifest.json b/source/manifest.json index 2cc0c21a..9ab653ef 100644 --- a/source/manifest.json +++ b/source/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "ZAP by Checkmarx Browser Extension", - "version": "0.1.8", + "version": "0.1.9", "icons": { "16": "assets/icons/zap16x16.png", diff --git a/source/manifest.rec.json b/source/manifest.rec.json index 522b755e..aec05209 100644 --- a/source/manifest.rec.json +++ b/source/manifest.rec.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "ZAP by Checkmarx Recorder", - "version": "0.1.8", + "version": "0.1.9", "icons": { "16": "assets/icons/zap16x16.png", @@ -16,7 +16,8 @@ "incognito": "spanning", "permissions": [ - "storage" + "storage", + "tabs" ], "host_permissions": [ diff --git a/source/types/zestScript/ZestStatement.ts b/source/types/zestScript/ZestStatement.ts index cfbea65a..0d1d235d 100644 --- a/source/types/zestScript/ZestStatement.ts +++ b/source/types/zestScript/ZestStatement.ts @@ -27,7 +27,9 @@ import { ZEST_CLIENT_ELEMENT_SUBMIT, ZEST_CLIENT_LAUNCH, ZEST_CLIENT_SWITCH_TO_FRAME, + ZEST_ACTION_SLEEP, ZEST_CLIENT_WINDOW_CLOSE, + ZEST_CLIENT_WINDOW_HANDLE, ZEST_COMMENT, } from '../../utils/constants'; @@ -273,6 +275,50 @@ class ZestStatementWindowClose extends ZestStatement { } } +class ZestStatementWindowHandle extends ZestStatement { + windowHandle: string; + + url: string; + + regex: boolean; + + constructor(windowHandle: string, url: string, regex = false) { + super(ZEST_CLIENT_WINDOW_HANDLE); + this.windowHandle = windowHandle; + this.url = url; + this.regex = regex; + } + + toJSON(): string { + return JSON.stringify({ + windowHandle: this.windowHandle, + url: this.url, + regex: this.regex, + index: this.index, + enabled: true, + elementType: this.elementType, + }); + } +} + +class ZestStatementActionSleep extends ZestStatement { + milliseconds: number; + + constructor(milliseconds = 10000) { + super(ZEST_ACTION_SLEEP); + this.milliseconds = milliseconds; + } + + toJSON(): string { + return JSON.stringify({ + milliseconds: this.milliseconds, + index: this.index, + enabled: true, + elementType: this.elementType, + }); + } +} + class ZestStatementSwitchToFrame extends ZestStatement { frameIndex: number; @@ -329,4 +375,6 @@ export { ZestStatementElementSubmit, ZestStatementElementClear, ZestStatementWindowClose, + ZestStatementWindowHandle, + ZestStatementActionSleep, }; diff --git a/source/utils/constants.ts b/source/utils/constants.ts index bc5bbc82..28b25a9f 100644 --- a/source/utils/constants.ts +++ b/source/utils/constants.ts @@ -32,12 +32,16 @@ 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_WINDOW_HANDLE = 'ZestClientWindowHandle'; export const ZEST_CLIENT_ELEMENT_MOUSE_OVER = 'ZestClientElementMouseOver'; +export const ZEST_ACTION_SLEEP = 'ZestActionSleep'; export const ZEST_COMMENT = 'ZestComment'; export const DEFAULT_WINDOW_HANDLE = 'windowHandle1'; export const ZAP_STOP_RECORDING = 'zapStopRecording'; export const ZAP_START_RECORDING = 'zapStartRecording'; +export const ZAP_GET_WINDOW_HANDLE = 'zapGetWindowHandle'; +export const ZAP_REGISTER_POPUP = 'zapRegisterPopup'; export const ZEST_SCRIPT = 'zestScript'; export const STOP_RECORDING = 'stopRecording'; diff --git a/test/ContentScript/unitTests.test.ts b/test/ContentScript/unitTests.test.ts index f384ba12..19495e13 100644 --- a/test/ContentScript/unitTests.test.ts +++ b/test/ContentScript/unitTests.test.ts @@ -29,6 +29,8 @@ import { ZestStatementElementClick, ZestStatementElementSendKeys, ZestStatementSwitchToFrame, + ZestStatementWindowHandle, + ZestStatementActionSleep, } from '../../source/types/zestScript/ZestStatement'; jest.mock('webextension-polyfill'); @@ -486,3 +488,48 @@ test('should generate valid frame switch statement', () => { '{"windowHandle":"windowHandle1","frameIndex":0,"frameName":"testvalue","parent":false,"index":-1,"enabled":true,"elementType":"ZestClientSwitchToFrame"}' ); }); + +test('should generate valid window handle statement', () => { + const stmt = new ZestStatementWindowHandle( + 'windowHandle2', + 'https://example\\.com.*', + true + ); + + expect(stmt.toJSON()).toBe( + '{"windowHandle":"windowHandle2","url":"https://example\\\\.com.*","regex":true,"index":-1,"enabled":true,"elementType":"ZestClientWindowHandle"}' + ); +}); + +test('should generate valid action sleep statement', () => { + const stmt = new ZestStatementActionSleep(10000); + + expect(stmt.toJSON()).toBe( + '{"milliseconds":10000,"index":-1,"enabled":true,"elementType":"ZestActionSleep"}' + ); +}); + +test('popup flow emits sleep before window handle in script', () => { + // This verifies the fix: ZestActionSleep must come before ZestClientWindowHandle + // so that ZAP waits for the popup to load before trying to locate it. + const script = new ZestScript('recordedScript'); + const sleepStmt = new ZestStatementActionSleep(10000); + const windowHandleStmt = new ZestStatementWindowHandle( + 'windowHandle2', + 'https://login\\.microsoftonline\\.com.*', + true + ); + + script.addStatement(sleepStmt.toJSON()); + script.addStatement(windowHandleStmt.toJSON()); + + const parsed = JSON.parse(script.toJSON()); + const statements = parsed.statements; + + expect(statements).toHaveLength(2); + expect(statements[0].elementType).toBe('ZestActionSleep'); + expect(statements[0].milliseconds).toBe(10000); + expect(statements[1].elementType).toBe('ZestClientWindowHandle'); + expect(statements[1].windowHandle).toBe('windowHandle2'); + expect(statements[1].regex).toBe(true); +}); From c784a8adfe818a3e1db6824063676d1a8151fe86 Mon Sep 17 00:00:00 2001 From: CxPedroNascimento <174706762+cx-pedro-nascimento@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:51:26 +0000 Subject: [PATCH 2/2] fix: address PR review feedback from Simon - Remove dead ZAP_GET_WINDOW_HANDLE constant and its unreachable handler - Add IS_FULL_EXTENSION guard to tabs.onRemoved so recorder variant does not make ZAP API calls when a popup tab closes - Extract magic 10000 into POPUP_WINDOW_SLEEP_MS named constant - Replace type-unsafe double cast with typed RegisterPopupMessage interface - Remove non-null assertion; use explicit undefined check instead - Remove unreachable message.windowHandle branch in ZAP_START_RECORDING handler - Fix long lines in recorder.ts to satisfy prettier line-length rule - Fix prefer-destructuring lint error in content script unit test - Add unit tests for ZAP_REGISTER_POPUP handler covering: new tab assignment, same-tab caching, multi-tab handles, missing tab id, and invalid URL fallback --- source/Background/index.ts | 40 +++++++------- source/ContentScript/index.ts | 3 -- source/ContentScript/recorder.ts | 22 ++++---- source/utils/constants.ts | 1 - test/Background/unitTests.test.ts | 78 ++++++++++++++++++++++++++++ test/ContentScript/unitTests.test.ts | 2 +- 6 files changed, 113 insertions(+), 33 deletions(-) diff --git a/source/Background/index.ts b/source/Background/index.ts index 1082af19..078210cf 100644 --- a/source/Background/index.ts +++ b/source/Background/index.ts @@ -36,7 +36,6 @@ import { RESET_ZEST_SCRIPT, SESSION_STORAGE, STOP_RECORDING, - ZAP_GET_WINDOW_HANDLE, ZAP_REGISTER_POPUP, ZEST_SCRIPT, } from '../utils/constants'; @@ -51,6 +50,13 @@ const zestScript = new ZestScript(); let windowHandleCounter = 1; const popupTabHandles = new Map(); + +const POPUP_WINDOW_SLEEP_MS = 10000; + +interface RegisterPopupMessage { + type: string; + url?: string; +} /* A callback URL will only be available if the browser has been launched from ZAP, otherwise call the individual endpoints */ @@ -276,23 +282,17 @@ async function onMessageHandler( _sender: Runtime.MessageSender ): Promise { const msg = message as MessageEvent; - if (msg.type === ZAP_GET_WINDOW_HANDLE) { - const tabId = _sender.tab?.id; - const handle = tabId - ? (popupTabHandles.get(tabId) ?? DEFAULT_WINDOW_HANDLE) - : DEFAULT_WINDOW_HANDLE; - return Promise.resolve(handle); - } if (msg.type === ZAP_REGISTER_POPUP) { // Called by the content script when it detects window.opener !== null. // Assign a new window handle for this popup tab if not already registered. const tabId = _sender.tab?.id; - const popupUrl = (msg as unknown as {url?: string}).url ?? ''; + const popupUrl = (msg as unknown as RegisterPopupMessage).url ?? ''; if (tabId !== undefined) { - if (popupTabHandles.has(tabId)) { - return Promise.resolve(popupTabHandles.get(tabId)!); + const cached = popupTabHandles.get(tabId); + if (cached !== undefined) { + return Promise.resolve(cached); } windowHandleCounter += 1; const handle = `windowHandle${windowHandleCounter}`; @@ -309,7 +309,7 @@ async function onMessageHandler( } catch { // popupUrl is empty or invalid; fall back to match-all } - const sleepStmt = new ZestStatementActionSleep(10000); + const sleepStmt = new ZestStatementActionSleep(POPUP_WINDOW_SLEEP_MS); const sleepStmtData = zestScript.addStatement(sleepStmt.toJSON()); const stmt = new ZestStatementWindowHandle(handle, urlPattern, true); const stmtData = zestScript.addStatement(stmt.toJSON()); @@ -374,13 +374,15 @@ Browser.tabs.onRemoved.addListener(async (tabId) => { if (!handle) return; popupTabHandles.delete(tabId); - const items = await Browser.storage.sync.get({ - zapurl: 'http://zap/', - zapkey: 'not set', - }); - const stmt = new ZestStatementWindowClose(0, handle); - const data = zestScript.addStatement(stmt.toJSON()); - sendZestScriptToZAP(data, items.zapkey as string, items.zapurl as string); + if (IS_FULL_EXTENSION) { + const items = await Browser.storage.sync.get({ + zapurl: 'http://zap/', + zapkey: 'not set', + }); + const stmt = new ZestStatementWindowClose(0, handle); + const data = zestScript.addStatement(stmt.toJSON()); + sendZestScriptToZAP(data, items.zapkey as string, items.zapurl as string); + } }); if (IS_FULL_EXTENSION) { diff --git a/source/ContentScript/index.ts b/source/ContentScript/index.ts index 32fd71ef..61e36c99 100644 --- a/source/ContentScript/index.ts +++ b/source/ContentScript/index.ts @@ -355,9 +355,6 @@ Browser.runtime.onMessage.addListener( ) => { if (message.type === ZAP_START_RECORDING) { configureExtension(); - if (message.windowHandle) { - recorder.setWindowHandle(message.windowHandle); - } recorder.recordUserInteractions(); } else if (message.type === ZAP_STOP_RECORDING) { recorder.stopRecordingUserInteractions(); diff --git a/source/ContentScript/recorder.ts b/source/ContentScript/recorder.ts index 5c1d90ec..c3e45a73 100644 --- a/source/ContentScript/recorder.ts +++ b/source/ContentScript/recorder.ts @@ -107,7 +107,11 @@ class Recorder { sendScrollToToZap(elementLocator: ElementLocator, waitForMsec: number): void { this.sendZestScriptToZAP( - new ZestStatementElementScrollTo(elementLocator, waitForMsec, this.windowHandle), + new ZestStatementElementScrollTo( + elementLocator, + waitForMsec, + this.windowHandle + ), {sendCache: false, notify: false} ); } @@ -134,20 +138,20 @@ class Recorder { } if (this.curLevel > level) { while (this.curLevel > level) { - this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(-1, '', this.windowHandle), { - sendCache: true, - notify: true, - }); + this.sendZestScriptToZAP( + new ZestStatementSwitchToFrame(-1, '', this.windowHandle), + {sendCache: true, notify: true} + ); this.curLevel -= 1; } this.curFrame = frameIndex; } else { this.curLevel += 1; this.curFrame = frameIndex; - this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(frameIndex, '', this.windowHandle), { - sendCache: true, - notify: true, - }); + this.sendZestScriptToZAP( + new ZestStatementSwitchToFrame(frameIndex, '', this.windowHandle), + {sendCache: true, notify: true} + ); } if (this.curLevel !== level) { console.log('ZAP Error in switching frames'); diff --git a/source/utils/constants.ts b/source/utils/constants.ts index 28b25a9f..b258feb3 100644 --- a/source/utils/constants.ts +++ b/source/utils/constants.ts @@ -40,7 +40,6 @@ export const DEFAULT_WINDOW_HANDLE = 'windowHandle1'; export const ZAP_STOP_RECORDING = 'zapStopRecording'; export const ZAP_START_RECORDING = 'zapStartRecording'; -export const ZAP_GET_WINDOW_HANDLE = 'zapGetWindowHandle'; export const ZAP_REGISTER_POPUP = 'zapRegisterPopup'; export const ZEST_SCRIPT = 'zestScript'; diff --git a/test/Background/unitTests.test.ts b/test/Background/unitTests.test.ts index a2779bbb..d217618a 100644 --- a/test/Background/unitTests.test.ts +++ b/test/Background/unitTests.test.ts @@ -22,6 +22,11 @@ */ import {TextEncoder, TextDecoder} from 'util'; import * as src from '../../source/Background/index'; +import { + DEFAULT_WINDOW_HANDLE, + RESET_ZEST_SCRIPT, + ZAP_REGISTER_POPUP, +} from '../../source/utils/constants'; console.log(src); console.log(TextEncoder); @@ -36,6 +41,8 @@ global.TextDecoder = TextDecoder as typeof global.TextDecoder; // eslint-disable-next-line import/order,import/first import Browser from 'webextension-polyfill'; +global.fetch = jest.fn().mockResolvedValue({ok: true}); + test('Report storage', () => { // Given @@ -59,3 +66,74 @@ test('Report storage', () => { expect(success).toBe(true); }); }); + +describe('ZAP_REGISTER_POPUP handler', () => { + // eslint-disable-next-line @typescript-eslint/ban-types + let onMessageHandler: Function; + + beforeAll(() => { + const addListenerMock = Browser.runtime.onMessage + .addListener as jest.Mock; + onMessageHandler = addListenerMock.mock.calls[0][0]; + }); + + beforeEach(async () => { + (Browser.storage.sync.get as jest.Mock).mockResolvedValue({ + zapurl: 'http://zap/', + zapkey: 'testkey', + zapenable: false, + }); + // Reset zest script state between tests (resets counter and popup map) + await onMessageHandler({type: RESET_ZEST_SCRIPT}, {}); + }); + + test('assigns a new window handle for a new popup tab', async () => { + const result = await onMessageHandler( + {type: ZAP_REGISTER_POPUP, url: 'https://example.com/popup'}, + {tab: {id: 42}} + ); + expect(result).toBe('windowHandle2'); + }); + + test('returns cached handle for the same popup tab on repeated calls', async () => { + const first = await onMessageHandler( + {type: ZAP_REGISTER_POPUP, url: 'https://example.com/popup'}, + {tab: {id: 99}} + ); + const second = await onMessageHandler( + {type: ZAP_REGISTER_POPUP, url: 'https://example.com/popup'}, + {tab: {id: 99}} + ); + expect(first).toBe('windowHandle2'); + expect(second).toBe('windowHandle2'); + }); + + test('assigns different handles for different popup tabs', async () => { + const first = await onMessageHandler( + {type: ZAP_REGISTER_POPUP, url: 'https://example.com/popup1'}, + {tab: {id: 10}} + ); + const second = await onMessageHandler( + {type: ZAP_REGISTER_POPUP, url: 'https://example.com/popup2'}, + {tab: {id: 20}} + ); + expect(first).toBe('windowHandle2'); + expect(second).toBe('windowHandle3'); + }); + + test('returns default handle when sender has no tab id', async () => { + const result = await onMessageHandler( + {type: ZAP_REGISTER_POPUP, url: 'https://example.com/popup'}, + {} + ); + expect(result).toBe(DEFAULT_WINDOW_HANDLE); + }); + + test('handles invalid popup URL gracefully', async () => { + const result = await onMessageHandler( + {type: ZAP_REGISTER_POPUP, url: 'not-a-valid-url'}, + {tab: {id: 55}} + ); + expect(result).toBe('windowHandle2'); + }); +}); diff --git a/test/ContentScript/unitTests.test.ts b/test/ContentScript/unitTests.test.ts index 19495e13..dc6f8018 100644 --- a/test/ContentScript/unitTests.test.ts +++ b/test/ContentScript/unitTests.test.ts @@ -524,7 +524,7 @@ test('popup flow emits sleep before window handle in script', () => { script.addStatement(windowHandleStmt.toJSON()); const parsed = JSON.parse(script.toJSON()); - const statements = parsed.statements; + const {statements} = parsed; expect(statements).toHaveLength(2); expect(statements[0].elementType).toBe('ZestActionSleep');