(() => {
"use strict";
if (window.__HASGARD__) return;
const idMap = new Map();
let refCounter = 0;
const _logs = [];
let _logIdCounter = 0;
const MAX_LOGS = 500;
const _networkRequests = [];
let _netIdCounter = 0;
const MAX_REQUESTS = 200;
const ROLE_MAP = {
A: "link",
BUTTON: "button",
SELECT: "combobox",
TEXTAREA: "textbox",
IMG: "img",
H1: "heading",
H2: "heading",
H3: "heading",
H4: "heading",
H5: "heading",
H6: "heading",
P: "paragraph",
UL: "list",
OL: "list",
LI: "listitem",
TABLE: "table",
TR: "row",
TH: "columnheader",
TD: "cell",
NAV: "navigation",
MAIN: "main",
ASIDE: "complementary",
FORM: "form",
DIALOG: "dialog",
DETAILS: "group",
};
const INTERACTIVE_ROLES = new Set([
"button",
"link",
"checkbox",
"radio",
"switch",
"slider",
"textbox",
"combobox",
]);
function serializeArg(arg) {
if (arg === null) return null;
if (arg === undefined) return null;
if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') return arg;
try {
JSON.stringify(arg);
return arg;
} catch (_) {
return String(arg);
}
}
function extractSource() {
try {
const stack = new Error().stack;
if (!stack) return null;
const lines = stack.split('\n');
for (let i = 3; i < lines.length; i++) {
const line = lines[i];
if (line && !line.includes('__HASGARD__')) return line.trim();
}
return null;
} catch (_) { return null; }
}
const _originalConsole = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
info: console.info.bind(console),
};
['log', 'warn', 'error', 'info'].forEach(level => {
console[level] = function(...args) {
const entry = {
id: ++_logIdCounter,
timestamp: Date.now(),
level: level,
args: args.map(serializeArg),
source: extractSource(),
};
_logs.push(entry);
if (_logs.length > MAX_LOGS) _logs.shift();
_originalConsole[level].apply(console, args);
};
});
function consoleLogs(options) {
let result = _logs.slice();
if (options) {
if (options.level) {
result = result.filter(e => e.level === options.level);
}
if (options.sinceId) {
result = result.filter(e => e.id > options.sinceId);
} else if (options.since) {
result = result.filter(e => e.timestamp > options.since);
}
if (options.last) {
result = result.slice(-options.last);
}
}
return result;
}
function clearLogs() {
_logs.length = 0;
return { cleared: true };
}
const _dialogs = [];
let _dialogIdCounter = 0;
const _dialogPolicy = { action: "dismiss", promptText: null };
function recordDialog(type, message, defaultValue, accepted, returned) {
const entry = {
id: ++_dialogIdCounter,
timestamp: Date.now(),
type: type,
message: message == null ? "" : String(message),
accepted: accepted,
};
if (defaultValue !== undefined) entry.defaultValue = String(defaultValue);
if (returned !== undefined && returned !== null) entry.returned = String(returned);
_dialogs.push(entry);
if (_dialogs.length > MAX_LOGS) _dialogs.shift();
return entry;
}
window.alert = function (message) {
recordDialog("alert", message, undefined, true, undefined);
};
window.confirm = function (message) {
const accepted = _dialogPolicy.action === "accept";
recordDialog("confirm", message, undefined, accepted, undefined);
return accepted;
};
window.prompt = function (message, defaultValue) {
const accepted = _dialogPolicy.action === "accept";
const returned = accepted
? (_dialogPolicy.promptText != null ? _dialogPolicy.promptText : (defaultValue == null ? "" : defaultValue))
: null;
recordDialog("prompt", message, defaultValue, accepted, returned);
return returned;
};
function dialogs() {
return { dialogs: _dialogs.slice(), policy: { action: _dialogPolicy.action, promptText: _dialogPolicy.promptText } };
}
function clearDialogs() {
_dialogs.length = 0;
return { cleared: true };
}
function handleDialogs(params) {
const action = params && params.action;
if (action !== "accept" && action !== "dismiss") {
throw new Error('dialog action must be "accept" or "dismiss", got: ' + String(action).slice(0, 64));
}
_dialogPolicy.action = action;
_dialogPolicy.promptText =
action === "accept" && params.promptText != null ? String(params.promptText) : null;
return { action: _dialogPolicy.action, promptText: _dialogPolicy.promptText };
}
function bodySize(body) {
if (!body) return 0;
if (typeof body === "string") return body.length;
if (body instanceof URLSearchParams) return body.toString().length;
if (body instanceof Blob) return body.size;
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return body.byteLength;
return 0;
}
const _originalFetch = window.fetch.bind(window);
window.fetch = function(input, init) {
const method = (init && init.method) || (input && input.method) || "GET";
const url = (typeof input === "string") ? input : (input && input.url) || String(input);
const timestamp = Date.now();
const requestSize = bodySize(init && init.body);
return _originalFetch(input, init).then(function(response) {
const duration_ms = Date.now() - timestamp;
const status = response.status;
const responseSize = parseInt(response.headers.get("Content-Length") || "0", 10) || 0;
const entry = {
id: ++_netIdCounter,
timestamp: timestamp,
method: method,
url: url,
status: status,
duration_ms: duration_ms,
error: null,
request_size: requestSize,
response_size: responseSize,
};
_networkRequests.push(entry);
if (_networkRequests.length > MAX_REQUESTS) _networkRequests.shift();
return response;
}, function(err) {
const duration_ms = Date.now() - timestamp;
const entry = {
id: ++_netIdCounter,
timestamp: timestamp,
method: method,
url: url,
status: 0,
duration_ms: duration_ms,
error: err ? err.message : "Network error",
request_size: requestSize,
response_size: 0,
};
_networkRequests.push(entry);
if (_networkRequests.length > MAX_REQUESTS) _networkRequests.shift();
throw err;
});
};
const _origXhrOpen = XMLHttpRequest.prototype.open;
const _origXhrSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
const result = _origXhrOpen.apply(this, arguments);
this._hasgard = { method: String(method), url: String(url) };
return result;
};
XMLHttpRequest.prototype.send = function(body) {
if (this._hasgard) {
const hasgard = this._hasgard;
const timestamp = Date.now();
const requestSize = bodySize(body);
let recorded = false;
let onLoad, onError, onTimeout, onAbort;
const cleanup = () => {
this.removeEventListener("load", onLoad);
this.removeEventListener("error", onError);
this.removeEventListener("timeout", onTimeout);
this.removeEventListener("abort", onAbort);
};
const pushEntry = (status, error, responseSize) => {
if (recorded) return;
recorded = true;
cleanup();
const entry = {
id: ++_netIdCounter,
timestamp: timestamp,
method: hasgard.method,
url: hasgard.url,
status: status,
duration_ms: Date.now() - timestamp,
error: error,
request_size: requestSize,
response_size: responseSize,
};
_networkRequests.push(entry);
if (_networkRequests.length > MAX_REQUESTS) _networkRequests.shift();
};
onLoad = () => {
const cl = parseInt(this.getResponseHeader("Content-Length") || "0", 10) || 0;
const r = this.response;
const responseSize = (this.responseType === "" || this.responseType === "text")
? ((r && r.length) || cl)
: (r instanceof ArrayBuffer ? r.byteLength : (r instanceof Blob ? r.size : cl));
pushEntry(this.status, null, responseSize);
};
onError = () => { pushEntry(0, "Network error", 0); };
onTimeout = () => { pushEntry(0, "Timeout", 0); };
onAbort = () => { pushEntry(0, "Aborted", 0); };
this.addEventListener("load", onLoad);
this.addEventListener("error", onError);
this.addEventListener("timeout", onTimeout);
this.addEventListener("abort", onAbort);
try {
return _origXhrSend.apply(this, arguments);
} catch (err) {
cleanup();
throw err;
}
}
return _origXhrSend.apply(this, arguments);
};
function networkRequests(options) {
let result = _networkRequests.slice();
if (options) {
if (options.filter) {
result = result.filter(e => e.url.includes(options.filter));
}
if (options.failedOnly) {
result = result.filter(e => e.status >= 400 || e.status === 0 || e.error);
}
if (options.sinceId) {
result = result.filter(e => e.id > options.sinceId);
}
if (options.last) {
result = result.slice(-options.last);
}
}
return result;
}
function clearNetwork() {
_networkRequests.length = 0;
return { cleared: true };
}
function inputRole(el) {
const t = (el.getAttribute("type") || "text").toLowerCase();
switch (t) {
case "hidden":
return null;
case "checkbox":
return "checkbox";
case "radio":
return "radio";
case "range":
return "slider";
case "submit":
case "reset":
case "button":
return "button";
default:
return "textbox";
}
}
function getRole(el) {
const explicit = el.getAttribute("role");
if (explicit) return explicit;
if (el.tagName === "INPUT") return inputRole(el);
return ROLE_MAP[el.tagName] || null;
}
function getName(el) {
const label = el.getAttribute("aria-label");
if (label) return label.trim().slice(0, 50);
const labelledBy = el.getAttribute("aria-labelledby");
if (labelledBy) {
const parts = labelledBy
.split(/\s+/)
.map((id) => {
const ref = document.getElementById(id);
return ref ? ref.textContent : "";
})
.filter(Boolean);
if (parts.length > 0) return parts.join(" ").trim().slice(0, 50);
}
if (el.tagName === "IMG") {
const alt = el.getAttribute("alt");
if (alt) return alt.trim().slice(0, 50);
}
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT") {
if (el.labels && el.labels.length > 0) {
const labelText = Array.from(el.labels)
.map((label) => label.textContent || "")
.join(" ")
.replace(/\s+/g, " ")
.trim();
if (labelText) return labelText.slice(0, 50);
}
const placeholder = el.getAttribute("placeholder");
if (placeholder) return placeholder.trim().slice(0, 50);
}
const text = el.textContent || "";
const trimmed = text.replace(/\s+/g, " ").trim();
return trimmed.slice(0, 50) || null;
}
function isInteractiveElement(el) {
const tag = el.tagName;
if (tag === "INPUT") {
const t = (el.getAttribute("type") || "text").toLowerCase();
return t !== "hidden";
}
if (
tag === "BUTTON" ||
tag === "SELECT" ||
tag === "TEXTAREA" ||
tag === "A"
) {
return true;
}
if (el.hasAttribute("tabindex")) return true;
const role = el.getAttribute("role");
return role ? INTERACTIVE_ROLES.has(role) : false;
}
function describeElement(node, role, depth, existingRef) {
let ref = existingRef;
if (!ref) {
refCounter++;
ref = "e" + refCounter;
idMap.set(ref, node);
}
const entry = { ref: ref, role: role, depth: depth };
const name = getName(node);
if (name) entry.name = name;
if (node.value !== undefined && node.value !== "") entry.value = String(node.value);
if (node.tagName === "INPUT") {
var inputType = (node.getAttribute("type") || "text").toLowerCase();
if (inputType === "checkbox" || inputType === "radio") {
entry.checked = node.checked;
}
}
if (node.disabled) entry.disabled = true;
return entry;
}
function snapshot(options) {
const interactive = (options && options.interactive) || false;
const selector = (options && options.selector) || null;
const maxDepth = (options && options.depth != null) ? options.depth : 255;
refCounter = 0;
idMap.clear();
var root;
if (selector) {
try {
root = document.querySelector(selector);
} catch (e) {
throw new Error("Invalid selector: " + selector);
}
} else {
root = document.body;
}
if (!root) return { elements: [] };
const elements = [];
function walk(node, currentDepth) {
if (currentDepth > maxDepth) return;
if (node.nodeType !== Node.ELEMENT_NODE) return;
const role = getRole(node);
const isInteractive = isInteractiveElement(node);
if (interactive && !isInteractive) {
for (const child of node.children) {
walk(child, currentDepth + 1);
}
return;
}
if (role) {
elements.push(describeElement(node, role, currentDepth));
}
for (const child of node.children) {
walk(child, currentDepth + 1);
}
}
walk(root, 0);
return { elements: elements };
}
var QUERY_DIMENSIONS = ["text", "label", "placeholder", "testid", "alt", "title", "selector"];
function normalizeForMatch(value) {
return String(value == null ? "" : value).replace(/\s+/g, " ").trim();
}
function matchesQuery(actual, wanted, exact) {
var a = normalizeForMatch(actual);
var w = normalizeForMatch(wanted);
if (exact) return a === w;
return a.toLowerCase().indexOf(w.toLowerCase()) !== -1;
}
function elementDepth(node) {
var depth = 0;
var current = node.parentElement;
while (current && current !== document.body) {
depth++;
current = current.parentElement;
}
return depth;
}
function labelTextsOf(el) {
var texts = [];
var aria = el.getAttribute && el.getAttribute("aria-label");
if (aria) texts.push(aria);
var labelledBy = el.getAttribute && el.getAttribute("aria-labelledby");
if (labelledBy) {
labelledBy.split(/\s+/).forEach(function (id) {
var ref = document.getElementById(id);
if (ref) texts.push(ref.textContent || "");
});
}
if (el.labels) {
for (var i = 0; i < el.labels.length; i++) {
texts.push(el.labels[i].textContent || "");
}
}
return texts;
}
function queryCandidates(by, value) {
if (by === "selector") return document.querySelectorAll(value);
if (by === "placeholder") return document.querySelectorAll("[placeholder]");
if (by === "testid") return document.querySelectorAll("[data-testid]");
if (by === "alt") return document.querySelectorAll("[alt]");
if (by === "title") return document.querySelectorAll("[title]");
if (by === "label") return document.querySelectorAll("input, textarea, select, button, meter, output, progress");
return document.querySelectorAll("*");
}
function queryMatches(el, by, value, exact) {
if (by === "selector") return true;
if (by === "placeholder") return matchesQuery(el.getAttribute("placeholder"), value, exact);
if (by === "alt") return matchesQuery(el.getAttribute("alt"), value, exact);
if (by === "title") return matchesQuery(el.getAttribute("title"), value, exact);
if (by === "testid") return normalizeForMatch(el.getAttribute("data-testid")) === normalizeForMatch(value);
if (by === "label") {
return labelTextsOf(el).some(function (text) {
return matchesQuery(text, value, exact);
});
}
return matchesQuery(el.textContent, value, exact);
}
function query(params) {
var by = params && params.by;
if (QUERY_DIMENSIONS.indexOf(by) === -1) {
throw new Error("query: 'by' must be one of " + QUERY_DIMENSIONS.join(", ") + ", got: " + String(by).slice(0, 64));
}
if (params.value == null) throw new Error("query requires a 'value'");
var exact = !!params.exact;
var matched = [];
var candidates = queryCandidates(by, params.value);
for (var i = 0; i < candidates.length; i++) {
if (queryMatches(candidates[i], by, params.value, exact)) matched.push(candidates[i]);
}
if (by === "text" && matched.length > 1) {
matched = matched.filter(function (el) {
return !matched.some(function (other) {
return other !== el && el.contains(other);
});
});
}
return {
elements: matched.map(function (el) {
return describeElement(el, getRole(el) || "generic", elementDepth(el));
})
};
}
function filterElements(params) {
var refs = (params && params.refs) || [];
if (!Array.isArray(refs)) throw new Error("filter requires a 'refs' array");
var exact = !!(params && params.exact);
var hasText = params ? params.hasText : null;
var hasNotText = params ? params.hasNotText : null;
if (hasText == null && hasNotText == null) {
throw new Error("filter requires 'hasText' or 'hasNotText'");
}
var kept = [];
for (var i = 0; i < refs.length; i++) {
var el = requireEl(refs[i]);
var text = el.textContent;
if (hasText != null && !matchesQuery(text, hasText, exact)) continue;
if (hasNotText != null && matchesQuery(text, hasNotText, exact)) continue;
kept.push({ el: el, ref: refs[i] });
}
return {
elements: kept.map(function (entry) {
return describeElement(entry.el, getRole(entry.el) || "generic", elementDepth(entry.el), entry.ref);
})
};
}
function resolve(ref) {
return idMap.get(ref) || null;
}
function requireEl(ref) {
const el = idMap.get(ref);
if (!el) throw new Error("Unknown ref: " + ref);
return el;
}
function selectorAt(selector, index) {
var matches = document.querySelectorAll(selector);
var position = index < 0 ? matches.length + index : index;
var el = matches[position];
if (!el) {
throw new Error(
"No element at index " + index + " for selector: " + selector + " (" + matches.length + " matched)"
);
}
return el;
}
function resolveTarget(params) {
if (params.ref) return requireEl(params.ref);
if (params.selector) {
if (params.index != null) {
if (typeof params.index !== "number" || !Number.isInteger(params.index)) {
throw new Error("index must be an integer");
}
return selectorAt(params.selector, params.index);
}
var el = document.querySelector(params.selector);
if (!el) throw new Error("No element matches selector: " + params.selector);
return el;
}
if (params.x != null && params.y != null) {
var pointEl = document.elementFromPoint(params.x, params.y);
if (!pointEl) throw new Error("No element at (" + params.x + "," + params.y + ")");
return pointEl;
}
throw new Error("No ref, selector, or coordinates provided");
}
function dispatchPointerEvent(el, type, options) {
const init = Object.assign({
bubbles: true,
cancelable: true,
composed: true,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
button: 0,
buttons: type === "pointerdown" ? 1 : 0,
view: window,
clientX: 0,
clientY: 0,
}, options || {});
if (typeof PointerEvent === "function") {
return el.dispatchEvent(new PointerEvent(type, init));
}
const event = new MouseEvent(type, init);
try {
Object.defineProperty(event, "pointerId", { value: init.pointerId });
Object.defineProperty(event, "pointerType", { value: init.pointerType });
Object.defineProperty(event, "isPrimary", { value: init.isPrimary });
} catch (_) {}
return el.dispatchEvent(event);
}
function click(params) {
const el = resolveTarget(params);
el.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
const rect = el.getBoundingClientRect();
const x = params.x != null ? params.x : rect.left + rect.width / 2;
const y = params.y != null ? params.y : rect.top + rect.height / 2;
const downInit = {
clientX: x,
clientY: y,
button: 0,
buttons: 1,
detail: 1,
view: window,
};
const upInit = {
clientX: x,
clientY: y,
button: 0,
buttons: 0,
detail: 1,
view: window,
};
const mouseInit = function(options) {
return Object.assign({
bubbles: true,
cancelable: true,
composed: true,
}, options);
};
const pointerDownOk = dispatchPointerEvent(el, "pointerdown", downInit);
if (pointerDownOk) {
const mouseDownOk = el.dispatchEvent(new MouseEvent("mousedown", mouseInit(downInit)));
if (mouseDownOk && typeof el.focus === "function") {
el.focus();
}
}
dispatchPointerEvent(el, "pointerup", upInit);
if (pointerDownOk) {
el.dispatchEvent(new MouseEvent("mouseup", mouseInit(upInit)));
}
dispatchPointerEvent(el, "click", upInit);
return { ok: true };
}
function nativeValueSetter(el) {
const proto = Object.getPrototypeOf(el);
const desc = proto && Object.getOwnPropertyDescriptor(proto, "value");
return desc && typeof desc.set === "function" ? desc.set : null;
}
function fill(params) {
const el = resolveTarget(params);
el.focus();
const setter = nativeValueSetter(el);
if (setter) {
setter.call(el, params.value);
} else {
el.value = params.value;
}
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
return { ok: true };
}
function typeText(params) {
const el = resolveTarget(params);
el.focus();
const setter = nativeValueSetter(el);
for (const ch of params.text) {
el.dispatchEvent(new KeyboardEvent("keydown", { key: ch, bubbles: true }));
if (setter) {
setter.call(el, el.value + ch);
} else {
el.value += ch;
}
el.dispatchEvent(new InputEvent("input", { data: ch, inputType: "insertText", bubbles: true }));
el.dispatchEvent(new KeyboardEvent("keyup", { key: ch, bubbles: true }));
}
return { ok: true };
}
function select(params) {
const el = resolveTarget(params);
const tag = el && el.tagName ? String(el.tagName).toLowerCase() : "";
if (tag !== "select") {
const reported = (tag || String(el)).slice(0, 64);
throw new Error("select requires a <select> element, got: " + reported);
}
const wanted = String(params.value);
const options = Array.from(el.options || []);
const matched =
options.find((o) => o.value === wanted) ||
options.find((o) => (o.text || "").trim() === wanted.trim());
if (!matched) {
throw new Error("select: no option matches " + JSON.stringify(params.value));
}
const setter = nativeValueSetter(el);
if (setter) {
setter.call(el, matched.value);
} else {
el.value = matched.value;
}
el.dispatchEvent(new Event("change", { bubbles: true }));
return { ok: true };
}
function check(params) {
const el = resolveTarget(params);
if (params.checked != null) {
if (typeof params.checked !== "boolean") {
throw new Error("check: 'checked' must be a boolean");
}
if (el.checked === params.checked) return { ok: true };
el.checked = params.checked;
} else {
el.checked = !el.checked;
}
el.dispatchEvent(new Event("change", { bubbles: true }));
return { ok: true };
}
function disabled(params) {
const el = resolveTarget(params);
const aria = typeof el.getAttribute === "function" ? el.getAttribute("aria-disabled") : null;
return { disabled: !!el.disabled || aria === "true" };
}
function boundingBox(params) {
const rect = resolveTarget(params).getBoundingClientRect();
return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
}
function focus(params) {
const el = resolveTarget(params);
if (typeof el.focus !== "function") throw new Error("focus: element is not focusable");
el.focus();
return { ok: true };
}
function blur(params) {
const el = resolveTarget(params);
if (typeof el.blur !== "function") throw new Error("blur: element cannot be blurred");
el.blur();
return { ok: true };
}
function hover(params) {
const el = resolveTarget(params);
const rect = el.getBoundingClientRect();
const x = params.x != null ? params.x : rect.left + rect.width / 2;
const y = params.y != null ? params.y : rect.top + rect.height / 2;
const init = { clientX: x, clientY: y, button: 0, buttons: 0, view: window };
const mouseInit = Object.assign({ bubbles: true, cancelable: true, composed: true }, init);
dispatchPointerEvent(el, "pointerover", init);
dispatchPointerEvent(el, "pointerenter", Object.assign({}, init, { bubbles: false }));
el.dispatchEvent(new MouseEvent("mouseover", mouseInit));
el.dispatchEvent(new MouseEvent("mouseenter", Object.assign({}, mouseInit, { bubbles: false })));
dispatchPointerEvent(el, "pointermove", init);
el.dispatchEvent(new MouseEvent("mousemove", mouseInit));
return { ok: true };
}
function dblclick(params) {
const el = resolveTarget(params);
click(params);
click(params);
const rect = el.getBoundingClientRect();
const x = params.x != null ? params.x : rect.left + rect.width / 2;
const y = params.y != null ? params.y : rect.top + rect.height / 2;
el.dispatchEvent(
new MouseEvent("dblclick", {
bubbles: true,
cancelable: true,
composed: true,
clientX: x,
clientY: y,
button: 0,
buttons: 0,
detail: 2,
view: window
})
);
return { ok: true };
}
function scroll(options) {
const dir = (options && options.direction) || "down";
const amount = (options && options.amount) || 300;
const hasTarget =
options && (options.ref || options.selector || (options.x != null && options.y != null));
const target = hasTarget ? resolveTarget(options) : window;
if (dir === "top") {
if (target === window) {
target.scrollTo(window.scrollX, 0);
} else {
target.scrollTop = 0;
}
return { ok: true };
}
if (dir === "bottom") {
if (target === window) {
const docEl = document.documentElement;
const body = document.body;
const fullHeight = Math.max(
docEl ? docEl.scrollHeight : 0,
body ? body.scrollHeight : 0
);
const viewportHeight = docEl ? docEl.clientHeight : window.innerHeight;
const max = fullHeight - viewportHeight;
target.scrollTo(window.scrollX, Math.max(0, max));
} else {
target.scrollTop = Math.max(0, target.scrollHeight - target.clientHeight);
}
return { ok: true };
}
if (dir !== "up" && dir !== "down" && dir !== "left" && dir !== "right") {
const safeDir = String(dir).slice(0, 64);
throw new Error("Unknown scroll direction: " + safeDir + " (expected up|down|left|right|top|bottom)");
}
const dx = (dir === "left" ? -amount : dir === "right" ? amount : 0);
const dy = (dir === "up" ? -amount : dir === "down" ? amount : 0);
target.scrollBy(dx, dy);
return { ok: true };
}
function drag(params) {
var source = resolveTarget(params.source || params);
var sourceRect = source.getBoundingClientRect();
var startX = sourceRect.left + sourceRect.width / 2;
var startY = sourceRect.top + sourceRect.height / 2;
var endX, endY, dropTarget;
if (params.target) {
dropTarget = resolveTarget(params.target);
var targetRect = dropTarget.getBoundingClientRect();
endX = targetRect.left + targetRect.width / 2;
endY = targetRect.top + targetRect.height / 2;
} else if (params.offset) {
var docEl = document.documentElement;
var viewportWidth = docEl.clientWidth;
var viewportHeight = docEl.clientHeight;
if (startX < 0 || startY < 0 || startX >= viewportWidth || startY >= viewportHeight) {
source.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
sourceRect = source.getBoundingClientRect();
startX = sourceRect.left + sourceRect.width / 2;
startY = sourceRect.top + sourceRect.height / 2;
}
var offsetX = params.offset.x || 0;
var offsetY = params.offset.y || 0;
endX = startX + offsetX;
endY = startY + offsetY;
dropTarget = document.elementFromPoint(endX, endY);
if (!dropTarget) {
var pointLabel = "(" + Math.round(endX) + "," + Math.round(endY) + ")";
if (endX < 0 || endY < 0 || endX >= viewportWidth || endY >= viewportHeight) {
throw new Error("Drop point " + pointLabel + " is outside the viewport (" +
viewportWidth + "x" + viewportHeight + ") — reduce the offset");
}
throw new Error("No element at drop point " + pointLabel +
" for offset (" + offsetX + "," + offsetY + ")");
}
} else {
throw new Error("drag requires target or offset");
}
var dt = typeof DataTransfer === "function" ? new DataTransfer() : new ClipboardEvent("").clipboardData;
source.dispatchEvent(new MouseEvent("mousedown", { clientX: startX, clientY: startY, bubbles: true }));
source.dispatchEvent(new DragEvent("dragstart", { clientX: startX, clientY: startY, dataTransfer: dt, bubbles: true }));
source.dispatchEvent(new DragEvent("dragleave", { clientX: endX, clientY: endY, dataTransfer: dt, bubbles: true }));
dropTarget.dispatchEvent(new DragEvent("dragenter", { clientX: endX, clientY: endY, dataTransfer: dt, bubbles: true, cancelable: true }));
dropTarget.dispatchEvent(new DragEvent("dragover", { clientX: endX, clientY: endY, dataTransfer: dt, bubbles: true, cancelable: true }));
dropTarget.dispatchEvent(new DragEvent("drop", { clientX: endX, clientY: endY, dataTransfer: dt, bubbles: true, cancelable: true }));
source.dispatchEvent(new DragEvent("dragend", { clientX: endX, clientY: endY, dataTransfer: dt, bubbles: true }));
return { ok: true };
}
function drop(params) {
var el = resolveTarget(params);
var rect = el.getBoundingClientRect();
var x = rect.left + rect.width / 2;
var y = rect.top + rect.height / 2;
var dt = buildFileList(params.files);
el.dispatchEvent(new DragEvent("dragenter", { clientX: x, clientY: y, dataTransfer: dt, bubbles: true, cancelable: true }));
el.dispatchEvent(new DragEvent("dragover", { clientX: x, clientY: y, dataTransfer: dt, bubbles: true, cancelable: true }));
el.dispatchEvent(new DragEvent("drop", { clientX: x, clientY: y, dataTransfer: dt, bubbles: true, cancelable: true }));
return { ok: true };
}
function buildFileList(files) {
var dt = typeof DataTransfer === "function" ? new DataTransfer() : new ClipboardEvent("").clipboardData;
for (var i = 0; i < (files || []).length; i++) {
var f = files[i];
var binary = atob(f.data);
var bytes = new Uint8Array(binary.length);
for (var j = 0; j < binary.length; j++) bytes[j] = binary.charCodeAt(j);
dt.items.add(new File([bytes], f.name, { type: f.type || "application/octet-stream" }));
}
return dt;
}
function setInputFiles(params) {
var el = resolveTarget(params);
if (el.tagName !== "INPUT" || String(el.type).toLowerCase() !== "file") {
throw new Error(
"setInputFiles requires an <input type=\"file\">, got: " +
String(el.tagName).toLowerCase() +
(el.type ? '[type="' + String(el.type).slice(0, 32) + '"]' : "")
);
}
var files = (params && params.files) || [];
if (!el.multiple && files.length > 1) {
throw new Error("setInputFiles got " + files.length + " files but the input is not [multiple]");
}
el.files = buildFileList(files).files;
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
return { ok: true, count: el.files.length };
}
function wheel(params) {
var hasTarget =
params && (params.ref || params.selector || (params.x != null && params.y != null));
var el = hasTarget ? resolveTarget(params) : document.scrollingElement || document.documentElement;
var deltaX = (params && params.deltaX) || 0;
var deltaY = (params && params.deltaY) || 0;
var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
var notCancelled = el.dispatchEvent(
new WheelEvent("wheel", {
deltaX: deltaX,
deltaY: deltaY,
deltaMode: 0,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
bubbles: true,
cancelable: true,
composed: true,
view: window
})
);
if (notCancelled) el.scrollBy(deltaX, deltaY);
return { ok: true, defaultPrevented: !notCancelled };
}
function text(params) {
return resolveTarget(params).textContent || "";
}
function html(params) {
if (params && (params.ref || params.selector)) {
return resolveTarget(params).innerHTML;
}
return document.documentElement.innerHTML;
}
function value(params) {
return resolveTarget(params).value || "";
}
function attrs(params) {
const el = resolveTarget(params);
const result = {};
for (const attr of el.attributes) {
result[attr.name] = attr.value;
}
return result;
}
function visible(params) {
const el = resolveTarget(params);
const style = getComputedStyle(el);
const isVisible =
style.display !== "none" &&
style.visibility !== "hidden" &&
style.opacity !== "0" &&
(el.offsetWidth > 0 || el.offsetHeight > 0);
return { visible: isVisible };
}
function count(params) {
if (!params || !params.selector) {
throw new Error("count requires a selector parameter");
}
return { count: document.querySelectorAll(params.selector).length };
}
function checked(params) {
const el = resolveTarget(params);
return { checked: !!el.checked };
}
function navigate(options) {
const url = options && options.url;
if (url) window.location.href = url;
return { ok: true };
}
function url() {
return window.location.href;
}
function title() {
return document.title;
}
function state() {
return {
url: window.location.href,
title: document.title,
readyState: document.readyState,
viewport: { width: window.innerWidth, height: window.innerHeight },
scroll: { x: window.scrollX, y: window.scrollY },
};
}
function evalScript(options) {
var script = options && options.script;
if (!script) throw new Error("No script provided");
var expr;
try {
expr = new Function("return (\n" + script + "\n)");
} catch (e1) {
if (!(e1 instanceof SyntaxError)) throw e1;
if (hasTopLevelAwait(script)) {
try {
var asyncExpr = new Function(
"return (async () => (\n" + script + "\n))()"
);
return asyncExpr();
} catch (e2) {
if (!(e2 instanceof SyntaxError)) throw e2;
}
try {
var asyncStmt = new Function(
"return (async () => {\n" + script + "\n})()"
);
return asyncStmt();
} catch (e3) {
if (!(e3 instanceof SyntaxError)) throw e3;
throw new SyntaxError(
"top-level await detected but the script could not be auto-wrapped. " +
"Wrap explicitly: (async () => { /* ...; */ return value; })() — " +
"see docs/reference/cli.md"
);
}
}
var indirectEval = eval;
return indirectEval(script);
}
return expr();
}
function hasTopLevelAwait(src) {
if (src.length > 100000) return /\bawait\b/.test(src);
var stripped = src
.replace(/'(?:[^'\\]|\\.)*'/g, "''")
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\/\/[^\n]*/g, "")
.replace(/\.\s*await\b/g, ".__prop");
for (var k = 0; k < 6; k++) {
var prev = stripped;
stripped = stripped
.replace(/\bfunction\s*\*?\s*[\w$]*\s*\([^()]*\)\s*\{[^{}]*\}/g, "fn()")
.replace(/\([^()]*\)\s*=>\s*\{[^{}]*\}/g, "fn()")
.replace(/\b[\w$]+\s*=>\s*\{[^{}]*\}/g, "fn()")
.replace(/\([^()]*\)\s*=>\s*[^{};,)\n]+/g, "fn()")
.replace(/\b[\w$]+\s*=>\s*[^{};,)\n]+/g, "fn()");
if (stripped === prev) break;
}
return /\bawait\b/.test(stripped);
}
function waitForExpression(expression, timeout, poll) {
return new Promise(function (res, rej) {
var settled = false;
var poller = null;
function stop() {
settled = true;
clearTimeout(timer);
if (poller !== null) clearInterval(poller);
}
function finish(value) {
if (settled) return;
stop();
res({ found: true, value: serializeArg(value) });
}
function fail(err) {
if (settled) return;
stop();
rej(new Error("wait expression threw: " + (err && err.message ? err.message : String(err))));
}
var timer = setTimeout(function () {
if (settled) return;
stop();
rej(new Error("Timeout waiting for expression: " + String(expression).slice(0, 200)));
}, timeout);
function attempt() {
var value;
try {
value = (0, eval)(expression);
} catch (e) {
fail(e);
return;
}
if (value && typeof value.then === "function") {
value.then(function (resolved) {
if (resolved) finish(resolved);
}, fail);
return;
}
if (value) finish(value);
}
attempt();
if (!settled) poller = setInterval(attempt, poll);
});
}
function waitFor(options) {
var selector = options && options.selector;
var ref = options && options.ref;
var expression = options && options.expression;
var gone = (options && options.gone) || false;
var timeout = (options && options.timeout != null) ? options.timeout : 10000;
if (!selector && !ref && !expression) {
return Promise.reject(
new Error(
"waitFor requires 'selector', 'ref', or 'expression' (use --selector for CSS, @id for snapshot ref)"
)
);
}
if (expression) {
var poll = (options && options.poll != null) ? options.poll : 50;
return waitForExpression(expression, timeout, poll);
}
return new Promise(function (res, rej) {
function check() {
if (selector) return document.querySelector(selector);
if (ref) return idMap.get(ref) || null;
return null;
}
var el = check();
if (!gone && el) return res({ found: true });
if (gone && !el) return res({ found: true });
var timer = setTimeout(function () {
observer.disconnect();
rej(new Error("Timeout waiting for " + (selector || ref)));
}, timeout);
var observer = new MutationObserver(function () {
var found = check();
if (!gone && found) {
observer.disconnect();
clearTimeout(timer);
res({ found: true });
} else if (gone && !found) {
observer.disconnect();
clearTimeout(timer);
res({ found: true });
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
});
});
}
var MAX_WATCH_ENTRIES = 200;
function summarizeNode(node) {
var entry = { tag: node.tagName.toLowerCase() };
if (node.id) entry.id = node.id;
if (node.className && typeof node.className === 'string' && node.className.trim()) entry.class = node.className.trim();
var text = Array.from(node.childNodes)
.filter(function(n) { return n.nodeType === Node.TEXT_NODE; })
.map(function(n) { return n.textContent || ''; })
.join(' ')
.replace(/\s+/g, ' ')
.trim();
if (text) entry.text = text.substring(0, 80);
return entry;
}
function watch(options) {
var selector = options && options.selector;
var timeout = (options && options.timeout != null) ? options.timeout : 10000;
var stable = (options && options.stable != null) ? options.stable : 300;
var requireMutation = !!(options && options.requireMutation);
var root;
if (selector) {
root = document.querySelector(selector);
if (!root) throw new Error("watch: no element matches selector: " + selector);
} else {
root = document.body;
}
return new Promise(function (res, rej) {
var changes = { added: [], removed: [], modified: [], truncated: false };
var stableTimer = null;
var timeoutTimer = null;
var settled = false;
function finish() {
if (settled) return;
settled = true;
clearTimeout(timeoutTimer);
observer.disconnect();
res(changes);
}
function resetStableTimer() {
clearTimeout(stableTimer);
stableTimer = setTimeout(finish, stable);
}
timeoutTimer = setTimeout(function () {
if (settled) return;
settled = true;
clearTimeout(stableTimer);
observer.disconnect();
if (changes.added.length > 0 || changes.removed.length > 0 || changes.modified.length > 0) {
res(changes);
} else {
rej(new Error("watch timeout: no DOM changes within " + timeout + "ms"));
}
}, timeout);
if (!requireMutation) {
resetStableTimer();
}
function pushCapped(arr, entry) {
if (arr.length < MAX_WATCH_ENTRIES) {
arr.push(entry);
} else {
changes.truncated = true;
}
}
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
if (mutation.type === 'childList') {
for (var j = 0; j < mutation.addedNodes.length; j++) {
var node = mutation.addedNodes[j];
if (node.nodeType === Node.ELEMENT_NODE) {
pushCapped(changes.added, summarizeNode(node));
}
}
for (var k = 0; k < mutation.removedNodes.length; k++) {
var removedNode = mutation.removedNodes[k];
if (removedNode.nodeType === Node.ELEMENT_NODE) {
pushCapped(changes.removed, summarizeNode(removedNode));
}
}
} else if (mutation.type === 'attributes') {
var target = mutation.target;
var attrValue = target.getAttribute(mutation.attributeName);
var entry = {
tag: target.tagName.toLowerCase(),
attribute: mutation.attributeName,
};
if (attrValue === null) {
entry.removed = true;
} else {
entry.value = attrValue;
}
pushCapped(changes.modified, entry);
} else if (mutation.type === 'characterData') {
var parent = mutation.target.parentElement;
if (parent) {
pushCapped(changes.modified, {
tag: parent.tagName.toLowerCase(),
text: (mutation.target.textContent || '').replace(/\s+/g, ' ').trim().substring(0, 80),
});
}
}
}
resetStableTimer();
});
observer.observe(root, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
});
});
}
async function screenshot(options) {
var selector = options && options.selector;
var el = selector ? document.querySelector(selector) : document.documentElement;
if (!el) throw new Error("Element not found: " + selector);
if (typeof htmlToImage === "undefined" || !htmlToImage.toPng) {
throw new Error("html-to-image library not loaded. Bundle it into bridge.js for screenshot support.");
}
var renderOptions = { pixelRatio: 1 };
if (!selector) {
var body = document.body;
renderOptions.width = Math.max(el.scrollWidth || 0, body ? body.scrollWidth || 0 : 0);
renderOptions.height = Math.max(el.scrollHeight || 0, body ? body.scrollHeight || 0 : 0);
}
var dataUrl = await htmlToImage.toPng(el, renderOptions);
return dataUrl;
}
function storageGet(params) {
if (typeof params.key !== "string") {
throw new Error("storageGet requires a string key");
}
var storage = params.session ? sessionStorage : localStorage;
var val = storage.getItem(params.key);
if (val === null) {
return { found: false };
}
return { found: true, value: val };
}
function storageSet(params) {
if (typeof params.key !== "string" || typeof params.value !== "string") {
throw new Error("storageSet requires string key and value");
}
var storage = params.session ? sessionStorage : localStorage;
storage.setItem(params.key, params.value);
return { ok: true };
}
var MAX_STORAGE_ENTRIES = 500;
function storageList(params) {
var storage = params.session ? sessionStorage : localStorage;
var total = storage.length;
var len = Math.min(total, MAX_STORAGE_ENTRIES);
var entries = [];
for (var i = 0; i < len; i++) {
var key = storage.key(i);
entries.push({ key: key, value: storage.getItem(key) });
}
entries.sort(function (a, b) {
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
});
return { entries: entries, truncated: total > MAX_STORAGE_ENTRIES };
}
function storageClear(params) {
var storage = params.session ? sessionStorage : localStorage;
storage.clear();
return { cleared: true };
}
var MAX_FORMS = 100;
var MAX_FIELDS_PER_FORM = 500;
function formDump(params) {
var forms;
var totalForms;
if (params && params.selector) {
var found = document.querySelector(params.selector);
if (!found) {
throw new Error("Form not found: " + params.selector);
}
if (found.tagName.toLowerCase() !== "form") {
throw new Error("Selector matched a <" + found.tagName.toLowerCase() + ">, expected a <form>");
}
forms = [found];
totalForms = 1;
} else {
var all = document.querySelectorAll("form");
totalForms = all.length;
forms = [];
var formLimit = Math.min(totalForms, MAX_FORMS);
for (var fi = 0; fi < formLimit; fi++) {
forms.push(all[fi]);
}
}
var result = [];
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
var fields = [];
var elements = form.querySelectorAll("input, select, textarea");
var fieldLimit = Math.min(elements.length, MAX_FIELDS_PER_FORM);
for (var j = 0; j < fieldLimit; j++) {
var el = elements[j];
var tag = el.tagName.toLowerCase();
var elType = el.type || null;
var fieldVal;
if (tag === "select" && el.multiple) {
var selected = [];
for (var k = 0; k < el.options.length; k++) {
if (el.options[k].selected) {
selected.push(el.options[k].value);
}
}
fieldVal = selected;
} else {
fieldVal = el.value;
}
var field = {
tag: tag,
type: elType,
name: el.name || "",
value: fieldVal,
};
if (elType === "checkbox" || elType === "radio") {
field.checked = el.checked;
}
fields.push(field);
}
var formEntry = {
id: form.id || "",
name: form.getAttribute("name") || "",
action: form.action || "",
method: form.method || "get",
fields: fields,
};
if (elements.length > MAX_FIELDS_PER_FORM) {
formEntry.fieldsTruncated = true;
}
result.push(formEntry);
}
var truncated = totalForms > MAX_FORMS;
return { forms: result, truncated: truncated };
}
window.__HASGARD__ = {
snapshot: snapshot,
query: query,
filter: filterElements,
resolve: resolve,
click: click,
fill: fill,
type: typeText,
select: select,
check: check,
scroll: scroll,
text: text,
html: html,
value: value,
attrs: attrs,
navigate: navigate,
url: url,
title: title,
state: state,
eval: evalScript,
wait: waitFor,
screenshot: screenshot,
consoleLogs: consoleLogs,
clearLogs: clearLogs,
networkRequests: networkRequests,
clearNetwork: clearNetwork,
visible: visible,
count: count,
checked: checked,
disabled: disabled,
boundingBox: boundingBox,
focus: focus,
blur: blur,
hover: hover,
dblclick: dblclick,
watch: watch,
drag: drag,
drop: drop,
setInputFiles: setInputFiles,
wheel: wheel,
dialogs: dialogs,
clearDialogs: clearDialogs,
handleDialogs: handleDialogs,
storageGet: storageGet,
storageSet: storageSet,
storageList: storageList,
storageClear: storageClear,
formDump: formDump,
};
})();