Skip to content
This repository was archived by the owner on Aug 4, 2026. It is now read-only.
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
77 changes: 71 additions & 6 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Comment on lines +2681 to +2687
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(" > ");
},
Comment thread
xiadongdev marked this conversation as resolved.
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);
Expand Down Expand Up @@ -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 });
Expand All @@ -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;
Expand All @@ -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;
});
Comment on lines +3249 to +3255
rec.pendingResolutions.push(resolution);
} else {
rec.actions.push(message.payload);
}
}
sendResponse({ ok: true });
return;
Expand Down
3 changes: 2 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"tabs",
"debugger",
"scripting",
"sidePanel"
"sidePanel",
"webNavigation"
],
"host_permissions": [
"http://*/*",
Expand Down
52 changes: 27 additions & 25 deletions recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,29 +255,31 @@ if (!window.__vpRecorderInstalled) {
true,
);

// Recording badge
const badge = document.createElement("div");
badge.id = "__vp-recorder-badge";
badge.innerHTML = '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#ff4444;margin-right:5px;animation:__vp-pulse 1s infinite"></span>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 = '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#ff4444;margin-right:5px;animation:__vp-pulse 1s infinite"></span>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);
}
}
41 changes: 32 additions & 9 deletions script_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<ss.length;i++){try{var e=document.querySelector(ss[i]);if(e)return e;}catch(e){}}return null;})()`;
return `(function(){var ss=${list};for(var i=0;i<ss.length;i++){try{var e=${docVar}.querySelector(ss[i]);if(e)return e;}catch(e){}}return null;})()`;
}

function convertRecordingToScript(actions) {
Expand All @@ -1761,30 +1761,45 @@ function convertRecordingToScript(actions) {
const lines = [];
lines.push(`(async function() {`);
lines.push(`function wait(ms) { return new Promise(function(r) { setTimeout(r, ms); }); }`);
lines.push(`var __doc = document;`);

let scriptIframeSelector = null;

for (let i = 0; i < actions.length; i++) {
Comment thread
xiadongdev marked this conversation as resolved.
const action = actions[i];
if (i > 0) lines.push(`await wait(1000);`);

if (action.type === "navigate") {
lines.push(`window.location.href = ${JSON.stringify(action.url)};`);
scriptIframeSelector = null;
lines.push(`__doc = document;`);
continue;
}

const actionIframe = action.iframeSelector ?? null;
if (actionIframe !== scriptIframeSelector) {
if (actionIframe) {
lines.push(`__doc = document.querySelector(${JSON.stringify(actionIframe)})?.contentDocument ?? document;`);
} else {
lines.push(`__doc = document;`);
}
scriptIframeSelector = actionIframe;
}

const selectors = Array.isArray(action.selectors)
? action.selectors.filter((s) => !/^(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;
Expand All @@ -1793,15 +1808,15 @@ 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;
}

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;
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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);
Comment on lines +1867 to +1869
continue;
}

Expand Down