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;
}