(function () { const GUARD = "__HireflowInteractionTracker"; if (window[GUARD]) { return; } const TYPE_DEBOUNCE_MS = 250; /** @type {{ recording: boolean, interactions: Array<{action: string, xpath_selector: string, text?: string}>, listenersAttached: boolean, typeTimer: ReturnType | null, pendingType: { xpath: string, text: string } | null, userEdited: WeakSet }} */ const state = { recording: false, interactions: [], listenersAttached: false, typeTimer: null, pendingType: null, userEdited: new WeakSet(), }; window[GUARD] = state; /** Input types that represent free-text (or numeric) typing — not clicks-only controls. */ const TEXT_LIKE_INPUT_TYPES = new Set([ "text", "search", "url", "tel", "email", "password", "number", "", ]); function normalizedInputType(el) { if (el.tagName !== "INPUT") { return ""; } const raw = el.getAttribute("type"); if (raw == null || raw === "") { return "text"; } return String(raw).toLowerCase(); } function isTextLikeField(el) { if (!el || el.nodeType !== Node.ELEMENT_NODE) { return false; } if (el.tagName === "TEXTAREA") { return true; } if (el.tagName === "INPUT") { return TEXT_LIKE_INPUT_TYPES.has(normalizedInputType(el)); } if (el.isContentEditable === true) { return true; } return false; } function markFieldUserEdited(el) { if (isTextLikeField(el)) { state.userEdited.add(el); } } function escapeXPathLiteral(str) { if (str.indexOf("'") === -1) { return "'" + str + "'"; } if (str.indexOf('"') === -1) { return '"' + str.replace(/"/g, '\\"') + '"'; } const parts = str.split("'"); const concatArgs = []; for (let i = 0; i < parts.length; i++) { concatArgs.push("'" + parts[i] + "'"); if (i < parts.length - 1) { concatArgs.push('"\'"'); } } return "concat(" + concatArgs.join(", ") + ")"; } function getXPath(element) { if (!element || element.nodeType !== Node.ELEMENT_NODE) { return ""; } if (element.id) { return "//*" + "[@id=" + escapeXPathLiteral(element.id) + "]"; } const segments = []; let node = element; while (node && node.nodeType === Node.ELEMENT_NODE) { if (node === document.documentElement) { segments.unshift("html"); break; } const parent = node.parentElement; if (!parent) { break; } const tag = node.nodeName.toLowerCase(); const sameTagSiblings = Array.from(parent.children).filter( (c) => c.nodeName === node.nodeName ); const index = sameTagSiblings.indexOf(node) + 1; if (sameTagSiblings.length === 1) { segments.unshift(tag); } else { segments.unshift(tag + "[" + index + "]"); } node = parent; } return "//" + segments.join("/"); } function flushPendingType() { state.typeTimer = null; if (!state.pendingType) { return; } const { xpath, text } = state.pendingType; state.pendingType = null; const last = state.interactions[state.interactions.length - 1]; if (last && last.action === "type" && last.xpath_selector === xpath) { last.text = text; } else { state.interactions.push({ action: "type", text, xpath_selector: xpath, }); } } function scheduleTypeCommit(xpath, text) { state.pendingType = { xpath, text }; if (state.typeTimer) { clearTimeout(state.typeTimer); } state.typeTimer = setTimeout(flushPendingType, TYPE_DEBOUNCE_MS); } function onClickCapture(ev) { if (!state.recording || !ev.isTrusted) { return; } const target = ev.target; if (!target || target.nodeType !== Node.ELEMENT_NODE) { return; } flushPendingType(); state.interactions.push({ action: "click", xpath_selector: getXPath(target), }); } function onBeforeInputCapture(ev) { if (!state.recording || !ev.isTrusted) { return; } if (typeof InputEvent !== "undefined" && ev instanceof InputEvent) { const t = ev.target; if (t && t.nodeType === Node.ELEMENT_NODE && isTextLikeField(t)) { state.userEdited.add(t); } } } function onKeyDownCapture(ev) { if (!state.recording || !ev.isTrusted) { return; } const el = document.activeElement; if (!el || !isTextLikeField(el)) { return; } if (ev.ctrlKey || ev.metaKey || ev.altKey) { if (ev.key === "v" || ev.key === "V" || ev.key === "x" || ev.key === "X") { markFieldUserEdited(el); } return; } if ( ev.key.length === 1 || ev.key === "Backspace" || ev.key === "Delete" || ev.key === "Enter" ) { markFieldUserEdited(el); } } function onPasteCutCapture(ev) { if (!state.recording || !ev.isTrusted) { return; } const t = ev.target; if (t && t.nodeType === Node.ELEMENT_NODE) { markFieldUserEdited(t); } } function onInputCapture(ev) { if (!state.recording || !ev.isTrusted) { return; } const target = ev.target; if (!target || target.nodeType !== Node.ELEMENT_NODE) { return; } if (!isTextLikeField(target)) { return; } if (!state.userEdited.has(target)) { return; } let text = ""; if (target.isContentEditable) { text = target.innerText || target.textContent || ""; } else { text = target.value != null ? String(target.value) : ""; } const xpath = getXPath(target); scheduleTypeCommit(xpath, text); } function onChangeCapture(ev) { if (!state.recording || !ev.isTrusted) { return; } const target = ev.target; if (!target || target.nodeType !== Node.ELEMENT_NODE) { return; } if (target.tagName !== "SELECT") { return; } flushPendingType(); const text = target.value != null ? String(target.value) : ""; state.interactions.push({ action: "select", text, xpath_selector: getXPath(target), }); } function attachListeners() { if (state.listenersAttached) { return; } document.addEventListener("click", onClickCapture, true); document.addEventListener("beforeinput", onBeforeInputCapture, true); document.addEventListener("keydown", onKeyDownCapture, true); document.addEventListener("paste", onPasteCutCapture, true); document.addEventListener("cut", onPasteCutCapture, true); document.addEventListener("input", onInputCapture, true); document.addEventListener("change", onChangeCapture, true); state.listenersAttached = true; } function detachListeners() { if (!state.listenersAttached) { return; } document.removeEventListener("click", onClickCapture, true); document.removeEventListener("beforeinput", onBeforeInputCapture, true); document.removeEventListener("keydown", onKeyDownCapture, true); document.removeEventListener("paste", onPasteCutCapture, true); document.removeEventListener("cut", onPasteCutCapture, true); document.removeEventListener("input", onInputCapture, true); document.removeEventListener("change", onChangeCapture, true); state.listenersAttached = false; } chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message.type === "START_RECORDING") { state.interactions = []; state.pendingType = null; state.userEdited = new WeakSet(); if (state.typeTimer) { clearTimeout(state.typeTimer); state.typeTimer = null; } state.recording = true; attachListeners(); sendResponse({ ok: true }); return false; } if (message.type === "STOP_AND_GET_INTERACTIONS") { state.recording = false; if (state.typeTimer) { clearTimeout(state.typeTimer); state.typeTimer = null; } flushPendingType(); detachListeners(); const interactions = state.interactions.slice(); state.interactions = []; sendResponse({ ok: true, interactions }); return false; } if (message.type === "GET_STATUS") { sendResponse({ ok: true, recording: state.recording, count: state.interactions.length, }); return false; } return false; }); })();