diff --git a/background.js b/background.js index 57a78b4..36c2480 100644 --- a/background.js +++ b/background.js @@ -2663,16 +2663,70 @@ chrome.tabs.onActivated.addListener(({ tabId }) => { queueMonitorSuggestionsForTab(tabId, "tab-activated", 350); }); -// Recording state: tabId -> { actions: [], jobId } +// Recording state: tabId -> { actions: [], jobId, pendingResolutions: [] } const recordingState = new Map(); async function injectRecorder(tabId) { await chrome.scripting.executeScript({ - target: { tabId }, + target: { tabId, allFrames: true }, files: ["recorder.js"], }); } +async function resolveIframeSelector(tabId, frameId) { + try { + const frames = await chrome.webNavigation.getAllFrames({ tabId }); + const frame = frames?.find((f) => f.frameId === frameId); + if (!frame) return null; + const parentFrameId = frame.parentFrameId; + if (parentFrameId == null || parentFrameId < 0) return null; + const frameUrl = frame.url; + + const results = await chrome.scripting.executeScript({ + target: { tabId, frameIds: [parentFrameId] }, + func: (url) => { + const iframes = [...document.querySelectorAll("iframe")]; + const match = iframes.find((f) => { + try { + return f.contentWindow?.location?.href === url || f.src === url || new URL(f.src, location.href).href === url; + } catch { + return false; + } + }); + if (!match) return null; + if (match.id) return `#${CSS.escape(match.id)}`; + + // Build a path-based unique selector walking up the DOM + function segmentFor(el) { + if (el.id) return `#${CSS.escape(el.id)}`; + const tag = el.tagName.toLowerCase(); + const siblings = el.parentElement + ? [...el.parentElement.children].filter((c) => c.tagName === el.tagName) + : []; + if (siblings.length > 1) return `${tag}:nth-of-type(${siblings.indexOf(el) + 1})`; + return tag; + } + + const segments = [segmentFor(match)]; + let node = match.parentElement; + while (node && node !== document.documentElement) { + segments.unshift(segmentFor(node)); + const path = segments.join(" > "); + if (document.querySelectorAll(path).length === 1) return path; + if (node.id) break; + node = node.parentElement; + } + return segments.join(" > "); + }, + args: [frameUrl], + }); + + return results?.[0]?.result ?? null; + } catch { + return null; + } +} + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === "complete" && recordingState.has(tabId)) { const rec = recordingState.get(tabId); @@ -3158,7 +3212,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse({ ok: false, error: "Valid tabId required to start recording." }); return; } - recordingState.set(tabId, { actions: [], jobId: message.payload?.jobId ?? null }); + recordingState.set(tabId, { actions: [], jobId: message.payload?.jobId ?? null, pendingResolutions: [] }); try { await injectRecorder(tabId); sendResponse({ ok: true }); @@ -3172,11 +3226,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message?.type === "stop-recording") { const tabId = Number(message.payload?.tabId); const rec = recordingState.get(tabId); - const actions = rec?.actions ?? []; recordingState.delete(tabId); + await Promise.allSettled(rec?.pendingResolutions ?? []); + const actions = rec?.actions ?? []; try { await chrome.scripting.executeScript({ - target: { tabId }, + target: { tabId, allFrames: true }, func: () => { document.getElementById("__vp-recorder-badge")?.remove(); window.__vpRecorderInstalled = false; @@ -3191,7 +3246,17 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const tabId = Number(sender.tab?.id); const rec = recordingState.get(tabId); if (rec && message.payload) { - rec.actions.push(message.payload); + const frameId = sender.frameId; + if (frameId && frameId !== 0) { + const action = { ...message.payload, iframeSelector: null }; + rec.actions.push(action); + const resolution = resolveIframeSelector(tabId, frameId).then((iframeSelector) => { + action.iframeSelector = iframeSelector ?? null; + }); + rec.pendingResolutions.push(resolution); + } else { + rec.actions.push(message.payload); + } } sendResponse({ ok: true }); return; diff --git a/manifest.json b/manifest.json index 5fb19c5..09a1d64 100644 --- a/manifest.json +++ b/manifest.json @@ -18,7 +18,8 @@ "tabs", "debugger", "scripting", - "sidePanel" + "sidePanel", + "webNavigation" ], "host_permissions": [ "http://*/*", diff --git a/recorder.js b/recorder.js index 9b82cb3..dda887f 100644 --- a/recorder.js +++ b/recorder.js @@ -255,29 +255,31 @@ if (!window.__vpRecorderInstalled) { true, ); - // Recording badge - const badge = document.createElement("div"); - badge.id = "__vp-recorder-badge"; - badge.innerHTML = 'REC'; - badge.style.cssText = [ - "position:fixed", - "top:12px", - "right:12px", - "z-index:2147483647", - "background:rgba(20,10,5,0.82)", - "color:#fff", - "padding:5px 11px", - "border-radius:999px", - "font:bold 11px/1.4 sans-serif", - "letter-spacing:0.06em", - "pointer-events:none", - "box-shadow:0 2px 10px rgba(0,0,0,0.4)", - "display:flex", - "align-items:center", - ].join(";"); - - const style = document.createElement("style"); - style.textContent = "@keyframes __vp-pulse{0%,100%{opacity:1}50%{opacity:0.3}}"; - document.head?.appendChild(style); - document.documentElement.appendChild(badge); + // Recording badge — only show in top frame + if (window.self === window.top) { + const badge = document.createElement("div"); + badge.id = "__vp-recorder-badge"; + badge.innerHTML = 'REC'; + badge.style.cssText = [ + "position:fixed", + "top:12px", + "right:12px", + "z-index:2147483647", + "background:rgba(20,10,5,0.82)", + "color:#fff", + "padding:5px 11px", + "border-radius:999px", + "font:bold 11px/1.4 sans-serif", + "letter-spacing:0.06em", + "pointer-events:none", + "box-shadow:0 2px 10px rgba(0,0,0,0.4)", + "display:flex", + "align-items:center", + ].join(";"); + + const style = document.createElement("style"); + style.textContent = "@keyframes __vp-pulse{0%,100%{opacity:1}50%{opacity:0.3}}"; + document.head?.appendChild(style); + document.documentElement.appendChild(badge); + } } diff --git a/script_generator.js b/script_generator.js index 48854e7..ed0e863 100644 --- a/script_generator.js +++ b/script_generator.js @@ -1750,9 +1750,9 @@ function renderRecordedActions() { list.scrollTop = list.scrollHeight; } -function resolveSelectorsSnippet(selectors) { +function resolveSelectorsSnippet(selectors, docVar = "document") { const list = JSON.stringify(selectors); - return `(function(){var ss=${list};for(var i=0;i !/^(xpath\/|aria\/|pierce\/|text\/|\(\/\/)/.test(s)) : [action.selector].filter(Boolean); if (action.type === "click") { const comment = action.label ? ` // ${action.label.replace(/[\r\n]+/g, " ")}` : ""; - lines.push(`${resolveSelectorsSnippet(selectors)}?.click();${comment}`); + lines.push(`${resolveSelectorsSnippet(selectors, "__doc")}?.click();${comment}`); continue; } if (action.type === "setValue") { const val = JSON.stringify(action.value); const comment = action.label ? ` // ${action.label.replace(/[\r\n]+/g, " ")}` : ""; - lines.push(`(function() { var el = ${resolveSelectorsSnippet(selectors)};${comment}`); + lines.push(`(function() { var el = ${resolveSelectorsSnippet(selectors, "__doc")};${comment}`); lines.push(`if (el) { el.value = ${val}; el.dispatchEvent(new Event('input', {bubbles:true})); el.dispatchEvent(new Event('change', {bubbles:true})); }`); lines.push(`})();`); continue; @@ -1793,7 +1808,7 @@ function convertRecordingToScript(actions) { if (action.type === "selectValue") { const val = JSON.stringify(action.value); const comment = action.label ? ` // ${action.label.replace(/[\r\n]+/g, " ")}` : ""; - lines.push(`(function() { var el = ${resolveSelectorsSnippet(selectors)};${comment}`); + lines.push(`(function() { var el = ${resolveSelectorsSnippet(selectors, "__doc")};${comment}`); lines.push(`if (el) { el.value = ${val}; el.dispatchEvent(new Event('change', {bubbles:true})); }`); lines.push(`})();`); continue; @@ -1801,7 +1816,7 @@ function convertRecordingToScript(actions) { if (action.type === "setChecked") { const comment = action.label ? ` // ${action.label.replace(/[\r\n]+/g, " ")}` : ""; - lines.push(`(function() { var el = ${resolveSelectorsSnippet(selectors)};${comment}`); + lines.push(`(function() { var el = ${resolveSelectorsSnippet(selectors, "__doc")};${comment}`); lines.push(`if (el) { el.checked = ${Boolean(action.checked)}; el.dispatchEvent(new Event('change', {bubbles:true})); }`); lines.push(`})();`); continue; @@ -1821,11 +1836,15 @@ function convertRecordingToPreactions(actions) { continue; } + const target = action.iframeSelector ?? undefined; + if (action.type === "click") { const cssSelectors = Array.isArray(action.selectors) ? action.selectors.filter((s) => !/^(xpath|aria|pierce|text)\//.test(s)) : null; - preactions.push({ click: cssSelectors && cssSelectors.length > 1 ? cssSelectors : primarySelector(action) }); + const preaction = { click: cssSelectors && cssSelectors.length > 1 ? cssSelectors : primarySelector(action) }; + if (target) preaction.target = target; + preactions.push(preaction); continue; } @@ -1834,7 +1853,9 @@ function convertRecordingToPreactions(actions) { ? action.selectors.filter((s) => !/^(xpath|aria|pierce|text)\//.test(s)) : null; const field = cssSelectors && cssSelectors.length > 1 ? cssSelectors : primarySelector(action); - preactions.push({ type: { field, value: action.value } }); + const preaction = { type: { field, value: action.value } }; + if (target) preaction.target = target; + preactions.push(preaction); continue; } @@ -1843,7 +1864,9 @@ function convertRecordingToPreactions(actions) { ? action.selectors.filter((s) => !/^(xpath|aria|pierce|text)\//.test(s)) : null; const field = cssSelectors && cssSelectors.length > 1 ? cssSelectors : primarySelector(action); - preactions.push({ select: { field, value: action.value } }); + const preaction = { select: { field, value: action.value } }; + if (target) preaction.target = target; + preactions.push(preaction); continue; }