/* eslint no-unused-vars: 0 */ /* * These are routines used by our testing code. */ /* Detect if we have any shadow DOM */ window.__haveShadowDom = function() { if (window.__haveShadowDomResult === undefined) window.__haveShadowDomResult = !!Array.from(document.querySelectorAll('*')).find(el => el.shadowRoot); return window.__haveShadowDomResult; }; // Like querySelectorAll(), but traverses shadow DOM window.querySelectorAllDeep = function(query, element) { const result = Array.from( element.shadowRoot ? element.shadowRoot.childNodes : element.nodeName === 'SLOT' ? element.assignedElements() : element.childNodes, ) .filter(element => element instanceof Element) .map(element => window.querySelectorAllDeep(query, element)) .flat(); if (element.matches?.(query)) result.push(element); return result; }; window.getQuerySelectorAll = function() { if (window.__querySelectorAllFunction === undefined) { if (window.__haveShadowDom()) { window.__querySelectorAllFunction = function (sel) { return window.querySelectorAllDeep(sel, document) }; } else { window.__querySelectorAllFunction = function (sel) { return Array.from(document.querySelectorAll(sel)) }; } } return window.__querySelectorAllFunction; }; function elem_contains_text(elems, text) { return elems.filter(elem => elem && elem.innerText.includes(text)); } window.ph_select = function(sel) { const querySelectorAll = window.getQuerySelectorAll(); if (sel.includes(":contains(")) { if (!window.Sizzle) { // Best effort support `:contains()` // Cockpit supports multiple forms: // - :contains(foo) // - :contains('foo') // - :contains("foo") const re = /:contains\(([\sa-zA-Z0-9]+)\)|:contains\('([^']+)'\)|:contains\("([^"]+)"\)/g; const matches = re.exec(sel); if (matches === null) throw new Error("Unsupported ':contains' when window.Sizzle is not available."); if (matches.length !== 4) throw new Error(`Match not found for builtin :contains ${sel}`); if (re.exec(sel) !== null) throw new Error(`Unsupported multiple ':contains' when window.Sizzle is not available`); const searchText = matches[1] || matches[2] || matches[3]; const base = sel.replace(re, ''); const elems = querySelectorAll(base); return elem_contains_text(elems, searchText); } else { return window.Sizzle(sel); } } else { return querySelectorAll(sel); } }; window.ph_only = function(els, sel) { if (els.length === 0) throw new Error(sel + " not found"); if (els.length > 1) throw new Error(sel + " is ambiguous"); return els[0]; }; window.ph_find = function(sel) { const els = window.ph_select(sel); return window.ph_only(els, sel); }; window.ph_find_scroll_into_view = function(sel) { const el = window.ph_find(sel); /* we cannot make this conditional, as there is no way to find out whether an element is currently visible -- the usual trick to compare getBoundingClientRect() against window.innerHeight does not work if the element is in e.g. a scrollable dialog area */ return new Promise(resolve => { el.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' }); // scrolling needs a little bit of time to stabilize, and it's not predictable // in particular, 'scrollend' is not reliably emitted window.setTimeout(() => resolve(el), 200); }); }; window.ph_count = function(sel) { const els = window.ph_select(sel); return els.length; }; window.ph_count_check = function(sel, expected_num) { return (window.ph_count(sel) === expected_num); }; window.ph_val = function(sel) { const el = window.ph_find(sel); if (el.value === undefined) throw new Error(sel + " does not have a value"); return el.value; }; window.ph_set_val = function(sel, val) { const el = window.ph_find(sel); if (el.value === undefined) throw new Error(sel + " does not have a value"); el.value = val; const ev = new Event("change", { bubbles: true, cancelable: false }); el.dispatchEvent(ev); }; window.ph_has_val = function(sel, val) { return window.ph_val(sel) === val; }; window.ph_collected_text_is = function(sel, val) { const els = window.ph_select(sel); const rest = els.map(el => { if (el.textContent === undefined) throw new Error(sel + " can not have text"); return el.textContent.replaceAll("\xa0", " "); }).join(""); return rest === val; }; window.ph_text = function(sel) { const el = window.ph_find(sel); if (el.textContent === undefined) throw new Error(sel + " can not have text"); // HACK: https://github.com/patternfly/patternfly-react/issues/11678 const el_copy = el.cloneNode(true); // find and clear .pf-v6-screen-reader subelements for HelperText el_copy.querySelectorAll('.pf-v6-c-helper-text__item-text > .pf-v6-screen-reader').forEach(el => { el.textContent = '' }); // 0xa0 is a non-breakable space, which is a rendering detail of Chromium // and awkward to handle in tests; turn it into normal spaces return el_copy.textContent.replaceAll("\xa0", " "); }; window.ph_attr = function(sel, attr) { return window.ph_find(sel).getAttribute(attr); }; window.ph_set_attr = function(sel, attr, val) { const el = window.ph_find(sel); if (val === null || val === undefined) el.removeAttribute(attr); else el.setAttribute(attr, val); const ev = new Event("change", { bubbles: true, cancelable: false }); el.dispatchEvent(ev); }; window.ph_has_attr = function(sel, attr, val) { return window.ph_attr(sel, attr) === val; }; window.ph_attr_contains = function(sel, attr, val) { const a = window.ph_attr(sel, attr); return a && a.indexOf(val) > -1; }; window.ph_mouse = function(sel, type, x, y, btn, ctrlKey, shiftKey, altKey, metaKey) { const el = window.ph_find(sel); /* The element has to be visible, and not collapsed */ if (el.offsetWidth <= 0 && el.offsetHeight <= 0 && el.tagName !== 'svg') throw new Error(sel + " is not visible"); /* The event has to actually work */ let processed = false; function handler() { processed = true; } el.addEventListener(type, handler, true); let elp = el; let left = elp.offsetLeft || 0; let top = elp.offsetTop || 0; while (elp.offsetParent) { elp = elp.offsetParent; left += elp.offsetLeft; top += elp.offsetTop; } let detail = 0; if (["click", "mousedown", "mouseup"].indexOf(type) > -1) detail = 1; else if (type === "dblclick") detail = 2; const ev = new MouseEvent(type, { bubbles: true, cancelable: true, view: window, detail, screenX: left + x, screenY: top + y, clientX: left + x, clientY: top + y, button: btn, ctrlKey: ctrlKey || false, shiftKey: shiftKey || false, altKey: altKey || false, metaKey: metaKey || false }); el.dispatchEvent(ev); el.removeEventListener(type, handler, true); /* It really had to work */ if (!processed) throw new Error(sel + " is disabled or somehow doesn't process events"); }; window.ph_get_checked = function(sel) { const el = window.ph_find(sel); if (el.checked === undefined) throw new Error(sel + " is not checkable"); return el.checked; }; window.ph_set_checked = function(sel, val) { const el = window.ph_find(sel); if (el.checked === undefined) throw new Error(sel + " is not checkable"); if (el.checked !== val) window.ph_mouse(sel, "click", 0, 0, 0); }; window.ph_is_visible = function(sel) { const el = window.ph_find(sel); return el.tagName === "svg" || ((el.offsetWidth > 0 || el.offsetHeight > 0) && !(getComputedStyle(el).visibility === "hidden" || getComputedStyle(el).display === "none")); }; window.ph_is_present = function(sel) { const els = window.ph_select(sel); return els.length > 0; }; window.ph_in_text = function(sel, text) { return window.ph_text(sel).indexOf(text) !== -1; }; window.ph_text_is = function(sel, text) { return window.ph_text(sel) === text; }; window.ph_text_matches = function(sel, pattern) { return window.ph_text(sel).match(pattern); }; window.ph_jump = function(path) { if (path[0] !== '/') throw new Error("ph_jump() needs to be called with a path"); if (window.name.indexOf("cockpit1") !== 0) throw new Error("ph_jump() called in non cockpit window"); const control = { command: "jump", location: path }; window.parent.postMessage("\n" + JSON.stringify(control), "*"); }; window.ph_focus = function(sel) { window.ph_find(sel).focus(); }; window.ph_scrollIntoViewIfNeeded = function(sel) { window.ph_find(sel).scrollIntoViewIfNeeded(); }; window.ph_blur = function(sel) { window.ph_find(sel).blur(); }; window.ph_blur_active = function() { const elt = window.document.activeElement; if (elt) elt.blur(); }; class PhWaitCondTimeout extends Error { constructor(description) { if (description && description.apply) description = description.apply(); if (description) super(description); else super("condition did not become true"); } } window.ph_wait_cond = function(cond, timeout, error_description) { return new Promise((resolve, reject) => { // poll every 100 ms for now; FIXME: poll less often and re-check on mutations using // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver let stepTimer = null; let last_err = null; const tm = window.setTimeout(() => { if (stepTimer) window.clearTimeout(stepTimer); reject(last_err || new PhWaitCondTimeout(error_description)); }, timeout); function step() { try { if (cond()) { window.clearTimeout(tm); resolve(); return; } } catch (err) { last_err = err; } stepTimer = window.setTimeout(step, 100); } step(); }); }; function currentFrameAbsolutePosition() { let currentWindow = window; let currentParentWindow; const positions = []; let rect; while (currentWindow !== window.top) { currentParentWindow = currentWindow.parent; for (let idx = 0; idx < currentParentWindow.frames.length; idx++) if (currentParentWindow.frames[idx] === currentWindow) { for (const frameElement of currentParentWindow.document.getElementsByTagName('iframe')) { if (frameElement.contentWindow === currentWindow) { rect = frameElement.getBoundingClientRect(); positions.push({ x: rect.x, y: rect.y }); } } currentWindow = currentParentWindow; break; } } return positions.reduce((accumulator, currentValue) => { return { x: accumulator.x + currentValue.x, y: accumulator.y + currentValue.y }; }, { x: 0, y: 0 }); } window.ph_selector_clips = function(sels) { const f = currentFrameAbsolutePosition(); const elts = sels.map(window.ph_select).flat(); return elts.map(e => { const r = e.getBoundingClientRect(); return { x: r.x + f.x, y: r.y + f.y, width: r.width, height: r.height, scale: 1 }; }); }; window.ph_element_clip = function(sel) { window.ph_find(sel); // just to make sure it is not ambiguous return window.ph_selector_clips([sel])[0]; }; window.ph_count_animations = function(sel) { let animations = window.ph_find(sel).getAnimations({ subtree: true }); // ignore animations that have already finished running animations = animations.filter(a => a.playState !== "finished"); return animations.length; }; window.ph_set_texts = function(new_texts) { for (const sel in new_texts) { const elts = window.ph_select(sel); if (elts.length === 0) throw new Error(sel + " not found"); for (let elt of elts) { // We have to be careful to not replace any actual nodes // in the DOM since that would cause React to fail later // when it tries to remove some of its nodes that are no // longer in the DOM. This means that setting the // "textContent" property is out, for example. // // Instead, we insist on finding an actual "Text" node // that we then modify. If the given selector results in // elements that have other elements in them, we refuse to // mock them. // // However, for convenience, this function digs into // elements that have exactly one other child element. while (elt.children.length === 1) elt = elt.children[0]; if (elt.children.length !== 0) throw new Error(sel + " can not be mocked since it contains more than text"); let subst = new_texts[sel]; for (const n of elt.childNodes) { if (n.nodeType === 3) { // 3 == TEXT n.data = subst; subst = ""; } } } } };