/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
import { asLocator } from '@isomorphic/locatorGenerators';
import { parseAttributeSelector, parseSelector, stringifySelector, visitAllSelectorParts } from '@isomorphic/selectorParser';
import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '@isomorphic/stringUtils';
import { generateAriaTree, getAllElementsMatchingExpectAriaTemplate, matchesExpectAriaTemplate, renderAriaTree, findNewElement } from './ariaSnapshot';
import { beginDOMCaches, enclosingShadowRootOrDocument, endDOMCaches, isElementVisible, isInsideScope, parentElementOrShadowHost, setGlobalOptions } from './domUtils';
import { Highlight } from './highlight';
import { kLayoutSelectorNames, layoutSelectorScore } from './layoutSelectorUtils';
import { createRoleEngine } from './roleSelectorEngine';
import { beginAriaCaches, endAriaCaches, getAriaDisabled, getAriaRole, getCheckedAllowMixed, getCheckedWithoutMixed, getElementAccessibleDescription, getElementAccessibleErrorMessage, getElementAccessibleName, getReadonly } from './roleUtils';
import { SelectorEvaluatorImpl, sortInDOMOrder } from './selectorEvaluator';
import { generateSelector } from './selectorGenerator';
import { elementMatchesText, elementText, getElementLabels } from './selectorUtils';
import { XPathEngine } from './xpathSelectorEngine';
import { ConsoleAPI } from './consoleApi';
import { UtilityScript } from './utilityScript';
export class InjectedScript {
    _engines;
    _evaluator;
    _stableRafCount;
    _browserName;
    _shouldPrependErrorPrefix;
    _isUtilityWorld;
    onGlobalListenersRemoved;
    _hitTargetInterceptor;
    _highlight;
    isUnderTest;
    _sdkLanguage;
    _testIdAttributeNameForStrictErrorAndConsoleCodegen = 'data-testid';
    _markedElements;
    window;
    document;
    consoleApi;
    _lastAriaSnapshotForTrack = new Map();
    _lastAriaSnapshotForQuery;
    // Recorder must use any external dependencies through InjectedScript.
    // Otherwise it will end up with a copy of all modules it uses, and any
    // module-level globals will be duplicated, which leads to subtle bugs.
    utils = {
        asLocator,
        cacheNormalizedWhitespaces,
        elementText,
        getAriaRole,
        getElementAccessibleDescription,
        getElementAccessibleName,
        isElementVisible,
        isInsideScope,
        normalizeWhiteSpace,
        parseAriaSnapshot,
        generateAriaTree,
        findNewElement,
        // Builtins protect injected code from clock emulation.
        builtins: null,
    };
    _autoClosingTags;
    _booleanAttributes;
    _eventTypes;
    _hoverHitTargetInterceptorEvents;
    _tapHitTargetInterceptorEvents;
    _mouseHitTargetInterceptorEvents;
    _allHitTargetInterceptorEvents;
    constructor(window, options) {
        this.window = window;
        this.document = window.document;
        this.isUnderTest = options.isUnderTest;
        // Make sure builtins are created from "window". This is important for InjectedScript instantiated
        // inside a trace viewer snapshot, where "window" differs from "globalThis".
        this.utils.builtins = new UtilityScript(window, options.isUnderTest).builtins;
        this._sdkLanguage = options.sdkLanguage;
        this._testIdAttributeNameForStrictErrorAndConsoleCodegen = options.testIdAttributeName;
        this._evaluator = new SelectorEvaluatorImpl();
        this.consoleApi = new ConsoleAPI(this);
        this.onGlobalListenersRemoved = new Set();
        this._autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
        this._booleanAttributes = new Set(['checked', 'selected', 'disabled', 'readonly', 'multiple']);
        this._eventTypes = new Map([
            ['auxclick', 'mouse'],
            ['click', 'mouse'],
            ['dblclick', 'mouse'],
            ['mousedown', 'mouse'],
            ['mouseeenter', 'mouse'],
            ['mouseleave', 'mouse'],
            ['mousemove', 'mouse'],
            ['mouseout', 'mouse'],
            ['mouseover', 'mouse'],
            ['mouseup', 'mouse'],
            ['mouseleave', 'mouse'],
            ['mousewheel', 'mouse'],
            ['keydown', 'keyboard'],
            ['keyup', 'keyboard'],
            ['keypress', 'keyboard'],
            ['textInput', 'keyboard'],
            ['touchstart', 'touch'],
            ['touchmove', 'touch'],
            ['touchend', 'touch'],
            ['touchcancel', 'touch'],
            ['pointerover', 'pointer'],
            ['pointerout', 'pointer'],
            ['pointerenter', 'pointer'],
            ['pointerleave', 'pointer'],
            ['pointerdown', 'pointer'],
            ['pointerup', 'pointer'],
            ['pointermove', 'pointer'],
            ['pointercancel', 'pointer'],
            ['gotpointercapture', 'pointer'],
            ['lostpointercapture', 'pointer'],
            ['focus', 'focus'],
            ['blur', 'focus'],
            ['drag', 'drag'],
            ['dragstart', 'drag'],
            ['dragend', 'drag'],
            ['dragover', 'drag'],
            ['dragenter', 'drag'],
            ['dragleave', 'drag'],
            ['dragexit', 'drag'],
            ['drop', 'drag'],
            ['wheel', 'wheel'],
            ['deviceorientation', 'deviceorientation'],
            ['deviceorientationabsolute', 'deviceorientation'],
            ['devicemotion', 'devicemotion'],
        ]);
        this._hoverHitTargetInterceptorEvents = new Set(['mousemove']);
        this._tapHitTargetInterceptorEvents = new Set(['pointerdown', 'pointerup', 'touchstart', 'touchend', 'touchcancel']);
        this._mouseHitTargetInterceptorEvents = new Set(['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click', 'auxclick', 'dblclick', 'contextmenu']);
        this._allHitTargetInterceptorEvents = new Set([...this._hoverHitTargetInterceptorEvents, ...this._tapHitTargetInterceptorEvents, ...this._mouseHitTargetInterceptorEvents]);
        this._engines = new Map();
        this._engines.set('xpath', XPathEngine);
        this._engines.set('xpath:light', XPathEngine);
        this._engines.set('role', createRoleEngine(false));
        this._engines.set('text', this._createTextEngine(true, false));
        this._engines.set('text:light', this._createTextEngine(false, false));
        this._engines.set('id', this._createAttributeEngine('id', true));
        this._engines.set('id:light', this._createAttributeEngine('id', false));
        this._engines.set('data-testid', this._createAttributeEngine('data-testid', true));
        this._engines.set('data-testid:light', this._createAttributeEngine('data-testid', false));
        this._engines.set('data-test-id', this._createAttributeEngine('data-test-id', true));
        this._engines.set('data-test-id:light', this._createAttributeEngine('data-test-id', false));
        this._engines.set('data-test', this._createAttributeEngine('data-test', true));
        this._engines.set('data-test:light', this._createAttributeEngine('data-test', false));
        this._engines.set('css', this._createCSSEngine());
        this._engines.set('nth', { queryAll: () => [] });
        this._engines.set('visible', this._createVisibleEngine());
        this._engines.set('internal:control', this._createControlEngine());
        this._engines.set('internal:has', this._createHasEngine());
        this._engines.set('internal:has-not', this._createHasNotEngine());
        this._engines.set('internal:and', { queryAll: () => [] });
        this._engines.set('internal:or', { queryAll: () => [] });
        this._engines.set('internal:chain', this._createInternalChainEngine());
        this._engines.set('internal:label', this._createInternalLabelEngine());
        this._engines.set('internal:text', this._createTextEngine(true, true));
        this._engines.set('internal:has-text', this._createInternalHasTextEngine());
        this._engines.set('internal:has-not-text', this._createInternalHasNotTextEngine());
        this._engines.set('internal:attr', this._createNamedAttributeEngine());
        this._engines.set('internal:testid', this._createNamedAttributeEngine());
        this._engines.set('internal:role', createRoleEngine(true));
        this._engines.set('internal:describe', this._createDescribeEngine());
        this._engines.set('aria-ref', this._createAriaRefEngine());
        for (const { name, source } of options.customEngines)
            this._engines.set(name, this.eval(source));
        this._stableRafCount = options.stableRafCount;
        this._browserName = options.browserName;
        this._shouldPrependErrorPrefix = !!options.shouldPrependErrorPrefix;
        this._isUtilityWorld = !!options.isUtilityWorld;
        setGlobalOptions({ browserNameForWorkarounds: options.browserName });
        this._setupGlobalListenersRemovalDetection();
        this._setupHitTargetInterceptors();
        if (this.isUnderTest)
            this.window.__injectedScript = this;
    }
    eval(expression) {
        return this.window.eval(expression);
    }
    testIdAttributeNameForStrictErrorAndConsoleCodegen() {
        return this._testIdAttributeNameForStrictErrorAndConsoleCodegen;
    }
    parseSelector(selector) {
        const result = parseSelector(selector);
        visitAllSelectorParts(result, part => {
            if (!this._engines.has(part.name))
                throw this.createStacklessError(`Unknown engine "${part.name}" while parsing selector ${selector}`);
        });
        return result;
    }
    generateSelector(targetElement, options) {
        return generateSelector(this, targetElement, options);
    }
    generateSelectorSimple(targetElement, options) {
        return generateSelector(this, targetElement, { ...options, testIdAttributeName: this._testIdAttributeNameForStrictErrorAndConsoleCodegen }).selector;
    }
    querySelector(selector, root, strict) {
        const result = this.querySelectorAll(selector, root);
        if (strict && result.length > 1)
            throw this.strictModeViolationError(selector, result);
        this.checkDeprecatedSelectorUsage(selector, result);
        return result[0];
    }
    _queryNth(elements, part) {
        const list = [...elements];
        let nth = +part.body;
        if (nth === -1)
            nth = list.length - 1;
        return new Set(list.slice(nth, nth + 1));
    }
    _queryLayoutSelector(elements, part, originalRoot) {
        const name = part.name;
        const body = part.body;
        const result = [];
        const inner = this.querySelectorAll(body.parsed, originalRoot);
        for (const element of elements) {
            const score = layoutSelectorScore(name, element, inner, body.distance);
            if (score !== undefined)
                result.push({ element, score });
        }
        result.sort((a, b) => a.score - b.score);
        return new Set(result.map(r => r.element));
    }
    ariaSnapshot(node, options) {
        return this.incrementalAriaSnapshot(node, options).full;
    }
    incrementalAriaSnapshot(node, options) {
        if (node.nodeType !== Node.ELEMENT_NODE)
            throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
        const ariaSnapshot = generateAriaTree(node, options);
        const rendered = renderAriaTree(ariaSnapshot, options);
        let incremental;
        if (options.track) {
            const previousSnapshot = this._lastAriaSnapshotForTrack.get(options.track);
            if (previousSnapshot)
                incremental = renderAriaTree(ariaSnapshot, options, previousSnapshot).text;
            this._lastAriaSnapshotForTrack.set(options.track, ariaSnapshot);
        }
        this._lastAriaSnapshotForQuery = ariaSnapshot;
        return { full: rendered.text, incremental, iframeRefs: ariaSnapshot.iframeRefs, iframeDepths: rendered.iframeDepths };
    }
    ariaSnapshotForRecorder() {
        const tree = generateAriaTree(this.document.body, { mode: 'ai' });
        const { text: ariaSnapshot } = renderAriaTree(tree, { mode: 'ai' });
        return { ariaSnapshot, refs: tree.refs };
    }
    getAllElementsMatchingExpectAriaTemplate(document, template) {
        return getAllElementsMatchingExpectAriaTemplate(document.documentElement, template);
    }
    querySelectorAll(selector, root) {
        if (selector.capture !== undefined) {
            if (selector.parts.some(part => part.name === 'nth'))
                throw this.createStacklessError(`Can't query n-th element in a request with the capture.`);
            const withHas = { parts: selector.parts.slice(0, selector.capture + 1) };
            if (selector.capture < selector.parts.length - 1) {
                const parsed = { parts: selector.parts.slice(selector.capture + 1) };
                const has = { name: 'internal:has', body: { parsed }, source: stringifySelector(parsed) };
                withHas.parts.push(has);
            }
            return this.querySelectorAll(withHas, root);
        }
        if (!root['querySelectorAll'])
            throw this.createStacklessError('Node is not queryable.');
        if (selector.capture !== undefined) {
            // We should have handled the capture above.
            throw this.createStacklessError('Internal error: there should not be a capture in the selector.');
        }
        // Workaround so that ":scope" matches the ShadowRoot.
        // This is, unfortunately, because an ElementHandle can point to any Node (including ShadowRoot/Document/etc),
        // and not just to an Element, and we support various APIs on ElementHandle like "textContent()".
        if (root.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ && selector.parts.length === 1 && selector.parts[0].name === 'css' && selector.parts[0].source === ':scope')
            return [root];
        this._evaluator.begin();
        try {
            let roots = new Set([root]);
            for (const part of selector.parts) {
                if (part.name === 'nth') {
                    roots = this._queryNth(roots, part);
                }
                else if (part.name === 'internal:and') {
                    const andElements = this.querySelectorAll(part.body.parsed, root);
                    roots = new Set(andElements.filter(e => roots.has(e)));
                }
                else if (part.name === 'internal:or') {
                    const orElements = this.querySelectorAll(part.body.parsed, root);
                    roots = new Set(sortInDOMOrder(new Set([...roots, ...orElements])));
                }
                else if (kLayoutSelectorNames.includes(part.name)) {
                    roots = this._queryLayoutSelector(roots, part, root);
                }
                else {
                    const next = new Set();
                    for (const root of roots) {
                        const all = this._queryEngineAll(part, root);
                        for (const one of all)
                            next.add(one);
                    }
                    roots = next;
                }
            }
            return [...roots];
        }
        finally {
            this._evaluator.end();
        }
    }
    _queryEngineAll(part, root) {
        const result = this._engines.get(part.name).queryAll(root, part.body);
        for (const element of result) {
            if (!('nodeName' in element))
                throw this.createStacklessError(`Expected a Node but got ${Object.prototype.toString.call(element)}`);
        }
        return result;
    }
    _createAttributeEngine(attribute, shadow) {
        const toCSS = (selector) => {
            const css = `[${attribute}=${JSON.stringify(selector)}]`;
            return [{ simples: [{ selector: { css, functions: [] }, combinator: '' }] }];
        };
        return {
            queryAll: (root, selector) => {
                return this._evaluator.query({ scope: root, pierceShadow: shadow }, toCSS(selector));
            }
        };
    }
    _createCSSEngine() {
        return {
            queryAll: (root, body) => {
                return this._evaluator.query({ scope: root, pierceShadow: true }, body);
            }
        };
    }
    _createTextEngine(shadow, internal) {
        const queryAll = (root, selector) => {
            const { matcher, kind } = createTextMatcher(selector, internal);
            const result = [];
            let lastDidNotMatchSelf = null;
            const appendElement = (element) => {
                // TODO: replace contains() with something shadow-dom-aware?
                if (kind === 'lax' && lastDidNotMatchSelf && lastDidNotMatchSelf.contains(element))
                    return false;
                const matches = elementMatchesText(this._evaluator._cacheText, element, matcher);
                if (matches === 'none')
                    lastDidNotMatchSelf = element;
                if (matches === 'self' || (matches === 'selfAndChildren' && kind === 'strict' && !internal))
                    result.push(element);
            };
            if (root.nodeType === Node.ELEMENT_NODE)
                appendElement(root);
            const elements = this._evaluator._queryCSS({ scope: root, pierceShadow: shadow }, '*');
            for (const element of elements)
                appendElement(element);
            return result;
        };
        return { queryAll };
    }
    _createInternalHasTextEngine() {
        return {
            queryAll: (root, selector) => {
                if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
                    return [];
                const element = root;
                const text = elementText(this._evaluator._cacheText, element);
                const { matcher } = createTextMatcher(selector, true);
                return matcher(text) ? [element] : [];
            }
        };
    }
    _createInternalHasNotTextEngine() {
        return {
            queryAll: (root, selector) => {
                if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
                    return [];
                const element = root;
                const text = elementText(this._evaluator._cacheText, element);
                const { matcher } = createTextMatcher(selector, true);
                return matcher(text) ? [] : [element];
            }
        };
    }
    _createInternalLabelEngine() {
        return {
            queryAll: (root, selector) => {
                const { matcher } = createTextMatcher(selector, true);
                const allElements = this._evaluator._queryCSS({ scope: root, pierceShadow: true }, '*');
                return allElements.filter(element => {
                    return getElementLabels(this._evaluator._cacheText, element).some(label => matcher(label));
                });
            }
        };
    }
    _createNamedAttributeEngine() {
        const queryAll = (root, selector) => {
            const parsed = parseAttributeSelector(selector, true);
            if (parsed.name || parsed.attributes.length !== 1)
                throw new Error('Malformed attribute selector: ' + selector);
            const { name, value, caseSensitive } = parsed.attributes[0];
            const lowerCaseValue = caseSensitive ? null : value.toLowerCase();
            let matcher;
            if (value instanceof RegExp)
                matcher = s => !!s.match(value);
            else if (caseSensitive)
                matcher = s => s === value;
            else
                matcher = s => s.toLowerCase().includes(lowerCaseValue);
            const elements = this._evaluator._queryCSS({ scope: root, pierceShadow: true }, `[${name}]`);
            return elements.filter(e => matcher(e.getAttribute(name)));
        };
        return { queryAll };
    }
    _createDescribeEngine() {
        const queryAll = (root) => {
            if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
                return [];
            return [root];
        };
        return { queryAll };
    }
    _createControlEngine() {
        return {
            queryAll(root, body) {
                if (body === 'enter-frame')
                    return [];
                if (body === 'return-empty')
                    return [];
                if (body === 'component') {
                    if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
                        return [];
                    // Usually, we return the mounted component that is a single child.
                    // However, when mounting fragments, return the root instead.
                    return [root.childElementCount === 1 ? root.firstElementChild : root];
                }
                throw new Error(`Internal error, unknown internal:control selector ${body}`);
            }
        };
    }
    _createHasEngine() {
        const queryAll = (root, body) => {
            if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
                return [];
            const has = !!this.querySelector(body.parsed, root, false);
            return has ? [root] : [];
        };
        return { queryAll };
    }
    _createHasNotEngine() {
        const queryAll = (root, body) => {
            if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
                return [];
            const has = !!this.querySelector(body.parsed, root, false);
            return has ? [] : [root];
        };
        return { queryAll };
    }
    _createVisibleEngine() {
        const queryAll = (root, body) => {
            if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
                return [];
            const visible = body === 'true';
            return isElementVisible(root) === visible ? [root] : [];
        };
        return { queryAll };
    }
    _createInternalChainEngine() {
        const queryAll = (root, body) => {
            return this.querySelectorAll(body.parsed, root);
        };
        return { queryAll };
    }
    extend(source, params) {
        const constrFunction = this.window.eval(`
    (() => {
      const module = {};
      ${source}
      return module.exports.default();
    })()`);
        return new constrFunction(this, params);
    }
    async viewportRatio(element) {
        return await new Promise(resolve => {
            const observer = new IntersectionObserver(entries => {
                resolve(entries[0].intersectionRatio);
                observer.disconnect();
            });
            observer.observe(element);
            // Firefox doesn't call IntersectionObserver callback unless
            // there are rafs.
            this.utils.builtins.requestAnimationFrame(() => { });
        });
    }
    getElementBorderWidth(node) {
        if (node.nodeType !== Node.ELEMENT_NODE || !node.ownerDocument || !node.ownerDocument.defaultView)
            return { left: 0, top: 0 };
        const style = node.ownerDocument.defaultView.getComputedStyle(node);
        return { left: parseInt(style.borderLeftWidth || '', 10), top: parseInt(style.borderTopWidth || '', 10) };
    }
    describeIFrameStyle(iframe) {
        if (!iframe.ownerDocument || !iframe.ownerDocument.defaultView)
            return 'error:notconnected';
        const defaultView = iframe.ownerDocument.defaultView;
        for (let e = iframe; e; e = parentElementOrShadowHost(e)) {
            if (defaultView.getComputedStyle(e).transform !== 'none')
                return 'transformed';
        }
        const iframeStyle = defaultView.getComputedStyle(iframe);
        return {
            left: parseInt(iframeStyle.borderLeftWidth || '', 10) + parseInt(iframeStyle.paddingLeft || '', 10),
            top: parseInt(iframeStyle.borderTopWidth || '', 10) + parseInt(iframeStyle.paddingTop || '', 10),
        };
    }
    retarget(node, behavior) {
        let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
        if (!element)
            return null;
        if (behavior === 'none')
            return element;
        if (!element.matches('input, textarea, select') && !element.isContentEditable) {
            if (behavior === 'button-link')
                element = element.closest('button, [role=button], a, [role=link]') || element;
            else
                element = element.closest('button, [role=button], [role=checkbox], [role=radio]') || element;
        }
        if (behavior === 'follow-label') {
            if (!element.matches('a, input, textarea, button, select, [role=link], [role=button], [role=checkbox], [role=radio]') &&
                !element.isContentEditable) {
                // Go up to the label that might be connected to the input/textarea.
                const enclosingLabel = element.closest('label');
                if (enclosingLabel && enclosingLabel.control)
                    element = enclosingLabel.control;
            }
        }
        return element;
    }
    async checkElementStates(node, states) {
        if (states.includes('stable')) {
            const stableResult = await this._checkElementIsStable(node);
            if (stableResult === false)
                return { missingState: 'stable' };
            if (stableResult === 'error:notconnected')
                return 'error:notconnected';
        }
        for (const state of states) {
            if (state !== 'stable') {
                const result = this.elementState(node, state);
                if (result.received === 'error:notconnected')
                    return 'error:notconnected';
                if (!result.matches)
                    return { missingState: state };
            }
        }
    }
    async _checkElementIsStable(node) {
        const continuePolling = Symbol('continuePolling');
        let lastRect;
        let stableRafCounter = 0;
        let lastTime = 0;
        const check = () => {
            const element = this.retarget(node, 'no-follow-label');
            if (!element)
                return 'error:notconnected';
            // Drop frames that are shorter than 16ms - WebKit Win bug.
            const time = this.utils.builtins.performance.now();
            if (this._stableRafCount > 1 && time - lastTime < 15)
                return continuePolling;
            lastTime = time;
            const clientRect = element.getBoundingClientRect();
            const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height };
            if (lastRect) {
                const samePosition = rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height;
                if (!samePosition)
                    return false;
                if (++stableRafCounter >= this._stableRafCount)
                    return true;
            }
            lastRect = rect;
            return continuePolling;
        };
        let fulfill;
        let reject;
        const result = new Promise((f, r) => { fulfill = f; reject = r; });
        const raf = () => {
            try {
                const success = check();
                if (success !== continuePolling)
                    fulfill(success);
                else
                    this.utils.builtins.requestAnimationFrame(raf);
            }
            catch (e) {
                reject(e);
            }
        };
        this.utils.builtins.requestAnimationFrame(raf);
        return result;
    }
    _createAriaRefEngine() {
        const queryAll = (root, selector) => {
            const result = this._lastAriaSnapshotForQuery?.elements?.get(selector);
            return result && result.isConnected ? [result] : [];
        };
        return { queryAll };
    }
    elementState(node, state) {
        const element = this.retarget(node, ['visible', 'hidden'].includes(state) ? 'none' : 'follow-label');
        if (!element || !element.isConnected) {
            if (state === 'hidden')
                return { matches: true, received: 'hidden' };
            return { matches: false, received: 'error:notconnected' };
        }
        if (state === 'visible' || state === 'hidden') {
            const visible = isElementVisible(element);
            return {
                matches: state === 'visible' ? visible : !visible,
                received: visible ? 'visible' : 'hidden'
            };
        }
        if (state === 'disabled' || state === 'enabled') {
            const disabled = getAriaDisabled(element);
            return {
                matches: state === 'disabled' ? disabled : !disabled,
                received: disabled ? 'disabled' : 'enabled'
            };
        }
        if (state === 'editable') {
            const disabled = getAriaDisabled(element);
            const readonly = getReadonly(element);
            if (readonly === 'error')
                throw this.createStacklessError('Element is not an <input>, <textarea>, <select> or [contenteditable] and does not have a role allowing [aria-readonly]');
            return {
                matches: !disabled && !readonly,
                received: disabled ? 'disabled' : readonly ? 'readOnly' : 'editable'
            };
        }
        if (state === 'checked' || state === 'unchecked') {
            const need = state === 'checked';
            const checked = getCheckedWithoutMixed(element);
            if (checked === 'error')
                throw this.createStacklessError('Not a checkbox or radio button');
            const isRadio = element.nodeName === 'INPUT' && element.type === 'radio';
            return {
                matches: need === checked,
                received: checked ? 'checked' : 'unchecked',
                isRadio,
            };
        }
        if (state === 'indeterminate') {
            const checked = getCheckedAllowMixed(element);
            if (checked === 'error')
                throw this.createStacklessError('Not a checkbox or radio button');
            return {
                matches: checked === 'mixed',
                received: checked === true ? 'checked' : checked === false ? 'unchecked' : 'mixed',
            };
        }
        throw this.createStacklessError(`Unexpected element state "${state}"`);
    }
    selectOptions(node, optionsToSelect) {
        const element = this.retarget(node, 'follow-label');
        if (!element)
            return 'error:notconnected';
        if (element.nodeName.toLowerCase() !== 'select')
            throw this.createStacklessError('Element is not a <select> element');
        const select = element;
        const options = [...select.options];
        const selectedOptions = [];
        let remainingOptionsToSelect = optionsToSelect.slice();
        for (let index = 0; index < options.length; index++) {
            const option = options[index];
            const normalizedOptionLabel = normalizeWhiteSpace(option.label);
            const filter = (optionToSelect) => {
                if (optionToSelect instanceof Node)
                    return option === optionToSelect;
                const matchesLabel = (label) => label === option.label || normalizeWhiteSpace(label) === normalizedOptionLabel;
                let matches = true;
                if (optionToSelect.valueOrLabel !== undefined)
                    matches = matches && (optionToSelect.valueOrLabel === option.value || matchesLabel(optionToSelect.valueOrLabel));
                if (optionToSelect.value !== undefined)
                    matches = matches && optionToSelect.value === option.value;
                if (optionToSelect.label !== undefined)
                    matches = matches && matchesLabel(optionToSelect.label);
                if (optionToSelect.index !== undefined)
                    matches = matches && optionToSelect.index === index;
                return matches;
            };
            if (!remainingOptionsToSelect.some(filter))
                continue;
            if (!this.elementState(option, 'enabled').matches)
                return 'error:optionnotenabled';
            selectedOptions.push(option);
            if (select.multiple) {
                remainingOptionsToSelect = remainingOptionsToSelect.filter(o => !filter(o));
            }
            else {
                remainingOptionsToSelect = [];
                break;
            }
        }
        if (remainingOptionsToSelect.length)
            return 'error:optionsnotfound';
        select.value = undefined;
        selectedOptions.forEach(option => option.selected = true);
        select.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
        select.dispatchEvent(new Event('change', { bubbles: true }));
        return selectedOptions.map(option => option.value);
    }
    fill(node, value) {
        const element = this.retarget(node, 'follow-label');
        if (!element)
            return 'error:notconnected';
        if (element.nodeName.toLowerCase() === 'input') {
            const input = element;
            const type = input.type.toLowerCase();
            const kInputTypesToSetValue = new Set(['color', 'date', 'time', 'datetime-local', 'month', 'range', 'week']);
            const kInputTypesToTypeInto = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']);
            if (!kInputTypesToTypeInto.has(type) && !kInputTypesToSetValue.has(type))
                throw this.createStacklessError(`Input of type "${type}" cannot be filled`);
            if (type === 'number') {
                value = value.trim();
                if (isNaN(Number(value)))
                    throw this.createStacklessError('Cannot type text into input[type=number]');
            }
            if (type === 'color')
                value = value.toLowerCase();
            if (kInputTypesToSetValue.has(type)) {
                value = value.trim();
                input.focus();
                input.value = value;
                if (input.value !== value)
                    throw this.createStacklessError('Malformed value');
                element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
                element.dispatchEvent(new Event('change', { bubbles: true }));
                return 'done'; // We have already changed the value, no need to input it.
            }
        }
        else if (element.nodeName.toLowerCase() === 'textarea') {
            // Nothing to check here.
        }
        else if (!element.isContentEditable) {
            throw this.createStacklessError('Element is not an <input>, <textarea> or [contenteditable] element');
        }
        this.selectText(element);
        return 'needsinput'; // Still need to input the value.
    }
    selectText(node) {
        const element = this.retarget(node, 'follow-label');
        if (!element)
            return 'error:notconnected';
        if (element.nodeName.toLowerCase() === 'input') {
            const input = element;
            input.select();
            input.focus();
            return 'done';
        }
        if (element.nodeName.toLowerCase() === 'textarea') {
            const textarea = element;
            textarea.selectionStart = 0;
            textarea.selectionEnd = textarea.value.length;
            textarea.focus();
            return 'done';
        }
        element.focus();
        const range = element.ownerDocument.createRange();
        range.selectNodeContents(element);
        const selection = element.ownerDocument.defaultView.getSelection();
        if (selection) {
            selection.removeAllRanges();
            selection.addRange(range);
        }
        return 'done';
    }
    _activelyFocused(node) {
        const activeElement = node.getRootNode().activeElement;
        const isFocused = activeElement === node && !!node.ownerDocument && node.ownerDocument.hasFocus();
        return { activeElement, isFocused };
    }
    focusNode(node, resetSelectionIfNotFocused) {
        if (!node.isConnected)
            return 'error:notconnected';
        if (node.nodeType !== Node.ELEMENT_NODE)
            throw this.createStacklessError('Node is not an element');
        const { activeElement, isFocused: wasFocused } = this._activelyFocused(node);
        if (node.isContentEditable && !wasFocused && activeElement && activeElement.blur) {
            // Workaround the Firefox bug where focusing the element does not switch current
            // contenteditable to the new element. However, blurring the previous one helps.
            activeElement.blur();
        }
        // On firefox, we have to call focus() twice to actually focus an element in certain
        // scenarios.
        node.focus();
        node.focus();
        if (resetSelectionIfNotFocused && !wasFocused && node.nodeName.toLowerCase() === 'input') {
            try {
                const input = node;
                input.setSelectionRange(0, 0);
            }
            catch (e) {
                // Some inputs do not allow selection.
            }
        }
        return 'done';
    }
    blurNode(node) {
        if (!node.isConnected)
            return 'error:notconnected';
        if (node.nodeType !== Node.ELEMENT_NODE)
            throw this.createStacklessError('Node is not an element');
        node.blur();
        return 'done';
    }
    setInputFiles(node, payloads) {
        if (node.nodeType !== Node.ELEMENT_NODE)
            return 'Node is not of type HTMLElement';
        const element = node;
        if (element.nodeName !== 'INPUT')
            return 'Not an <input> element';
        const input = element;
        const type = (input.getAttribute('type') || '').toLowerCase();
        if (type !== 'file')
            return 'Not an input[type=file] element';
        const files = payloads.map(file => {
            const bytes = Uint8Array.from(atob(file.buffer), c => c.charCodeAt(0));
            return new File([bytes], file.name, { type: file.mimeType, lastModified: file.lastModifiedMs });
        });
        const dt = new DataTransfer();
        for (const file of files)
            dt.items.add(file);
        input.files = dt.files;
        input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
        input.dispatchEvent(new Event('change', { bubbles: true }));
    }
    expectHitTarget(hitPoint, targetElement) {
        const roots = [];
        // Get all component roots leading to the target element.
        // Go from the bottom to the top to make it work with closed shadow roots.
        let parentElement = targetElement;
        while (parentElement) {
            const root = enclosingShadowRootOrDocument(parentElement);
            if (!root)
                break;
            roots.push(root);
            if (root.nodeType === 9 /* Node.DOCUMENT_NODE */)
                break;
            parentElement = root.host;
        }
        // Hit target in each component root should point to the next component root.
        // Hit target in the last component root should point to the target or its descendant.
        let hitElement;
        for (let index = roots.length - 1; index >= 0; index--) {
            const root = roots[index];
            // All browsers have different behavior around elementFromPoint and elementsFromPoint.
            // https://github.com/w3c/csswg-drafts/issues/556
            // http://crbug.com/1188919
            const elements = root.elementsFromPoint(hitPoint.x, hitPoint.y);
            const singleElement = root.elementFromPoint(hitPoint.x, hitPoint.y);
            if (singleElement && elements[0] && parentElementOrShadowHost(singleElement) === elements[0]) {
                const style = this.window.getComputedStyle(singleElement);
                if (style?.display === 'contents') {
                    // Workaround a case where elementsFromPoint misses the inner-most element with display:contents.
                    // https://bugs.chromium.org/p/chromium/issues/detail?id=1342092
                    elements.unshift(singleElement);
                }
            }
            if (elements[0] && elements[0].shadowRoot === root && elements[1] === singleElement) {
                // Workaround webkit but where first two elements are swapped:
                // <host>
                //   #shadow root
                //     <target>
                // elementsFromPoint produces [<host>, <target>], while it should be [<target>, <host>]
                // In this case, just ignore <host>.
                elements.shift();
            }
            const innerElement = elements[0];
            if (!innerElement)
                break;
            hitElement = innerElement;
            if (index && innerElement !== roots[index - 1].host)
                break;
        }
        // Check whether hit target is the target or its descendant.
        const hitParents = [];
        while (hitElement && hitElement !== targetElement) {
            hitParents.push(hitElement);
            // Prefer the composed tree over the light-dom tree, as browser performs hit testing on the composed tree.
            // Note that we will still eventually climb to the light-dom parent, as any element distributed to a slot is a direct child of the shadow host that contains the slot.
            hitElement = hitElement.assignedSlot ?? parentElementOrShadowHost(hitElement);
        }
        if (hitElement === targetElement)
            return 'done';
        const hitTargetDescription = this.previewNode(hitParents[0] || this.document.documentElement);
        // Root is the topmost element in the hitTarget's chain that is not in the
        // element's chain. For example, it might be a dialog element that overlays
        // the target.
        let rootHitTargetDescription;
        let element = targetElement;
        while (element) {
            const index = hitParents.indexOf(element);
            if (index !== -1) {
                if (index > 1)
                    rootHitTargetDescription = this.previewNode(hitParents[index - 1]);
                break;
            }
            element = parentElementOrShadowHost(element);
        }
        if (rootHitTargetDescription)
            return { hitTargetDescription: `${hitTargetDescription} from ${rootHitTargetDescription} subtree` };
        return { hitTargetDescription };
    }
    // Life of a pointer action, for example click.
    //
    // 0. Retry items 1 and 2 while action fails due to navigation or element being detached.
    //   1. Resolve selector to an element.
    //   2. Retry the following steps until the element is detached or frame navigates away.
    //     2a. Wait for the element to be stable (not moving), visible and enabled.
    //     2b. Scroll element into view. Scrolling alternates between:
    //         - Built-in protocol scrolling.
    //         - Anchoring to the top/left, bottom/right and center/center.
    //         This is to scroll elements from under sticky headers/footers.
    //     2c. Click point is calculated, either based on explicitly specified position,
    //         or some visible point of the element based on protocol content quads.
    //     2d. Click point relative to page viewport is converted relative to the target iframe
    //         for the next hit-point check.
    //     2e. (injected) Hit target at the click point must be a descendant of the target element.
    //         This prevents mis-clicking in edge cases like <iframe> overlaying the target.
    //     2f. (injected) Events specific for click (or some other action type) are intercepted on
    //         the Window with capture:true. See 2i for details.
    //         Note: this step is skipped for drag&drop (see inline comments for the reason).
    //     2g. Necessary keyboard modifiers are pressed.
    //     2h. Click event is issued (mousemove + mousedown + mouseup).
    //     2i. (injected) For each event, we check that hit target at the event point
    //         is a descendant of the target element.
    //         This guarantees no race between issuing the event and handling it in the page,
    //         for example due to layout shift.
    //         When hit target check fails, we block all future events in the page.
    //     2j. Keyboard modifiers are restored.
    //     2k. (injected) Event interceptor is removed.
    //     2l. All navigations triggered between 2g-2k are awaited to be either committed or canceled.
    //     2m. If failed, wait for increasing amount of time before the next retry.
    setupHitTargetInterceptor(node, action, hitPoint, blockAllEvents) {
        const element = this.retarget(node, 'button-link');
        if (!element || !element.isConnected)
            return 'error:notconnected';
        if (hitPoint) {
            // First do a preliminary check, to reduce the possibility of some iframe
            // intercepting the action.
            const preliminaryResult = this.expectHitTarget(hitPoint, element);
            if (preliminaryResult !== 'done')
                return preliminaryResult.hitTargetDescription;
        }
        // When dropping, the "element that is being dragged" often stays under the cursor,
        // so hit target check at the moment we receive mousedown does not work -
        // it finds the "element that is being dragged" instead of the
        // "element that we drop onto".
        if (action === 'drag')
            return { stop: () => 'done' };
        const events = {
            'hover': this._hoverHitTargetInterceptorEvents,
            'tap': this._tapHitTargetInterceptorEvents,
            'mouse': this._mouseHitTargetInterceptorEvents,
        }[action];
        let result;
        const listener = (event) => {
            // Ignore events that we do not expect to intercept.
            if (!events.has(event.type))
                return;
            // Playwright only issues trusted events, so allow any custom events originating from
            // the page or content scripts.
            if (!event.isTrusted)
                return;
            // Determine the event point. Note that Firefox does not always have window.TouchEvent.
            const point = (!!this.window.TouchEvent && (event instanceof this.window.TouchEvent)) ? event.touches[0] : event;
            // Check that we hit the right element at the first event, and assume all
            // subsequent events will be fine.
            if (result === undefined && point)
                result = this.expectHitTarget({ x: point.clientX, y: point.clientY }, element);
            if (blockAllEvents || (result !== 'done' && result !== undefined)) {
                event.preventDefault();
                event.stopPropagation();
                event.stopImmediatePropagation();
            }
        };
        const stop = () => {
            if (this._hitTargetInterceptor === listener)
                this._hitTargetInterceptor = undefined;
            // If we did not get any events, consider things working. Possible causes:
            // - JavaScript is disabled (webkit-only).
            // - Some <iframe> overlays the element from another frame.
            // - Hovering a disabled control prevents any events from firing.
            return result || 'done';
        };
        // Note: this removes previous listener, just in case there are two concurrent clicks
        // or something went wrong and we did not cleanup.
        this._hitTargetInterceptor = listener;
        return { stop };
    }
    dispatchEvent(node, type, eventInitObj) {
        let event;
        const eventInit = { bubbles: true, cancelable: true, composed: true, ...eventInitObj };
        switch (this._eventTypes.get(type)) {
            case 'mouse':
                event = new MouseEvent(type, eventInit);
                break;
            case 'keyboard':
                event = new KeyboardEvent(type, eventInit);
                break;
            case 'touch': {
                // WebKit does not support Touch constructor, but has deprecated createTouch and createTouchList methods.
                if (this._browserName === 'webkit') {
                    const createTouch = (t) => {
                        if (t instanceof Touch)
                            return t;
                        // createTouch does not accept clientX/clientY, so we have to use pageX/pageY.
                        let pageX = t.pageX;
                        if (pageX === undefined && t.clientX !== undefined)
                            pageX = t.clientX + (this.document.scrollingElement?.scrollLeft || 0);
                        let pageY = t.pageY;
                        if (pageY === undefined && t.clientY !== undefined)
                            pageY = t.clientY + (this.document.scrollingElement?.scrollTop || 0);
                        return this.document.createTouch(this.window, t.target ?? node, t.identifier, pageX, pageY, t.screenX, t.screenY, t.radiusX, t.radiusY, t.rotationAngle, t.force);
                    };
                    const createTouchList = (touches) => {
                        if (touches instanceof TouchList || !touches)
                            return touches;
                        return this.document.createTouchList(...touches.map(createTouch));
                    };
                    eventInit.target ??= node;
                    eventInit.touches = createTouchList(eventInit.touches);
                    eventInit.targetTouches = createTouchList(eventInit.targetTouches);
                    eventInit.changedTouches = createTouchList(eventInit.changedTouches);
                    event = new TouchEvent(type, eventInit);
                }
                else {
                    eventInit.target ??= node;
                    eventInit.touches = eventInit.touches?.map((t) => t instanceof Touch ? t : new Touch({ ...t, target: t.target ?? node }));
                    eventInit.targetTouches = eventInit.targetTouches?.map((t) => t instanceof Touch ? t : new Touch({ ...t, target: t.target ?? node }));
                    eventInit.changedTouches = eventInit.changedTouches?.map((t) => t instanceof Touch ? t : new Touch({ ...t, target: t.target ?? node }));
                    event = new TouchEvent(type, eventInit);
                }
                break;
            }
            case 'pointer':
                event = new PointerEvent(type, eventInit);
                break;
            case 'focus':
                event = new FocusEvent(type, eventInit);
                break;
            case 'drag':
                event = new DragEvent(type, eventInit);
                break;
            case 'wheel':
                event = new WheelEvent(type, eventInit);
                break;
            case 'deviceorientation':
                try {
                    event = new DeviceOrientationEvent(type, eventInit);
                }
                catch {
                    const { bubbles, cancelable, alpha, beta, gamma, absolute } = eventInit;
                    event = this.document.createEvent('DeviceOrientationEvent');
                    event.initDeviceOrientationEvent(type, bubbles, cancelable, alpha, beta, gamma, absolute);
                }
                break;
            case 'devicemotion':
                try {
                    event = new DeviceMotionEvent(type, eventInit);
                }
                catch {
                    const { bubbles, cancelable, acceleration, accelerationIncludingGravity, rotationRate, interval } = eventInit;
                    event = this.document.createEvent('DeviceMotionEvent');
                    event.initDeviceMotionEvent(type, bubbles, cancelable, acceleration, accelerationIncludingGravity, rotationRate, interval);
                }
                break;
            default:
                event = new Event(type, eventInit);
                break;
        }
        node.dispatchEvent(event);
    }
    previewNode(node) {
        if (node.nodeType === Node.TEXT_NODE)
            return oneLine(`#text=${node.nodeValue || ''}`);
        if (node.nodeType !== Node.ELEMENT_NODE)
            return oneLine(`<${node.nodeName.toLowerCase()} />`);
        const element = node;
        const attrs = [];
        for (let i = 0; i < element.attributes.length; i++) {
            const { name, value } = element.attributes[i];
            if (name === 'style')
                continue;
            if (!value && this._booleanAttributes.has(name))
                attrs.push(` ${name}`);
            else
                attrs.push(` ${name}="${value}"`);
        }
        attrs.sort((a, b) => a.length - b.length);
        const attrText = trimStringWithEllipsis(attrs.join(''), 500);
        if (this._autoClosingTags.has(element.nodeName))
            return oneLine(`<${element.nodeName.toLowerCase()}${attrText}/>`);
        const children = element.childNodes;
        let onlyText = false;
        if (children.length <= 5) {
            onlyText = true;
            for (let i = 0; i < children.length; i++)
                onlyText = onlyText && children[i].nodeType === Node.TEXT_NODE;
        }
        const text = onlyText ? (element.textContent || '') : (children.length ? '\u2026' : '');
        return oneLine(`<${element.nodeName.toLowerCase()}${attrText}>${trimStringWithEllipsis(text, 50)}</${element.nodeName.toLowerCase()}>`);
    }
    _generateSelectors(elements) {
        this._evaluator.begin();
        beginAriaCaches();
        beginDOMCaches();
        try {
            // Firefox is slow to access DOM bindings in the utility world, making it very expensive to generate a lot of selectors.
            const maxElements = this._isUtilityWorld && this._browserName === 'firefox' ? 2 : 10;
            const infos = elements.slice(0, maxElements).map(m => ({
                preview: this.previewNode(m),
                selector: this.generateSelectorSimple(m),
            }));
            return infos.map((info, i) => `${i + 1}) ${info.preview} aka ${asLocator(this._sdkLanguage, info.selector)}`);
        }
        finally {
            endDOMCaches();
            endAriaCaches();
            this._evaluator.end();
        }
    }
    strictModeViolationError(selector, matches) {
        const lines = this._generateSelectors(matches).map(line => `\n    ` + line);
        if (lines.length < matches.length)
            lines.push('\n    ...');
        return this.createStacklessError(`strict mode violation: ${asLocator(this._sdkLanguage, stringifySelector(selector))} resolved to ${matches.length} elements:${lines.join('')}\n`);
    }
    checkDeprecatedSelectorUsage(selector, matches) {
        const kDeprecatedSelectors = new Set(['_react', '_vue', 'xpath:light', 'text:light', 'id:light',
            'data-testid:light', 'data-test-id:light', 'data-test:light']);
        if (!matches.length)
            return;
        const deperecated = selector.parts.find(part => kDeprecatedSelectors.has(part.name));
        if (!deperecated)
            return;
        const lines = this._generateSelectors(matches).map(line => `\n    ` + line);
        if (lines.length < matches.length)
            lines.push('\n    ...');
        throw this.createStacklessError(`"${deperecated.name}" selector is not supported: ${asLocator(this._sdkLanguage, stringifySelector(selector))} resolved to ${matches.length} element${matches.length === 1 ? '' : 's'}:${lines.join('')}\n`);
    }
    createStacklessError(message) {
        const error = this._shouldPrependErrorPrefix ? new Error('Error: ' + message) : new Error(message);
        if (this._browserName === 'firefox') {
            // Firefox cannot delete the stack, so assign to an empty string.
            error.stack = '';
            return error;
        }
        // Chromium/WebKit should delete the stack instead.
        delete error.stack;
        return error;
    }
    createHighlight() {
        return new Highlight(this);
    }
    maskSelectors(selectors, color) {
        const highlight = this._createHighlight();
        const elements = [];
        for (const selector of selectors)
            elements.push(this.querySelectorAll(selector, this.document.documentElement));
        highlight.maskElements(elements.flat(), color);
    }
    _createHighlight() {
        if (this._highlight)
            this.hideHighlight();
        this._highlight = new Highlight(this);
        this._highlight.install();
        return this._highlight;
    }
    _ensureHighlight() {
        if (!this._highlight) {
            this._highlight = new Highlight(this);
            this._highlight.install();
        }
        return this._highlight;
    }
    addHighlight(selector, style) {
        const highlight = this._ensureHighlight();
        highlight.addElementHighlight(selector, style);
    }
    removeHighlight(selector) {
        const highlight = this._ensureHighlight();
        highlight.removeElementHighlight(selector);
    }
    setScreencastAnnotation(annotation) {
        const highlight = this._ensureHighlight();
        if (!annotation) {
            highlight.updateHighlight([]);
            highlight.hideActionPoint();
            highlight.hideActionTitle();
            return;
        }
        const fadeDuration = annotation.duration ?? 500;
        if (annotation.box) {
            highlight.updateHighlight([{
                    box: annotation.box,
                    color: 'rgba(0, 128, 255, 0.15)',
                    borderColor: 'rgba(0, 128, 255, 0.6)',
                    fadeDuration,
                }]);
        }
        if (annotation.point)
            highlight.showActionPoint(annotation.point.x, annotation.point.y, fadeDuration);
        if (annotation.actionTitle)
            highlight.showActionTitle(annotation.actionTitle, fadeDuration, annotation.position, annotation.fontSize);
    }
    addUserOverlay(id, html) {
        const highlight = this._ensureHighlight();
        highlight.addUserOverlay(id, html);
    }
    getUserOverlay(id) {
        const highlight = this._ensureHighlight();
        return highlight.getUserOverlay(id);
    }
    removeUserOverlay(id) {
        const highlight = this._ensureHighlight();
        highlight.removeUserOverlay(id);
    }
    setUserOverlaysVisible(visible) {
        const highlight = this._ensureHighlight();
        highlight.setUserOverlaysVisible(visible);
    }
    hideHighlight() {
        if (this._highlight) {
            this._highlight.uninstall();
            delete this._highlight;
        }
    }
    markTargetElements(markedElements, callId) {
        if (this._markedElements?.callId !== callId)
            this._markedElements = undefined;
        const previous = this._markedElements?.elements || new Set();
        const unmarkEvent = new CustomEvent('__playwright_unmark_target__', {
            bubbles: true,
            cancelable: true,
            detail: callId,
            composed: true,
        });
        for (const element of previous) {
            if (!markedElements.has(element))
                element.dispatchEvent(unmarkEvent);
        }
        const markEvent = new CustomEvent('__playwright_mark_target__', {
            bubbles: true,
            cancelable: true,
            detail: callId,
            composed: true,
        });
        for (const element of markedElements) {
            if (!previous.has(element))
                element.dispatchEvent(markEvent);
        }
        this._markedElements = { callId, elements: markedElements };
    }
    _setupGlobalListenersRemovalDetection() {
        const customEventName = '__playwright_global_listeners_check__';
        let seenEvent = false;
        const handleCustomEvent = () => seenEvent = true;
        this.window.addEventListener(customEventName, handleCustomEvent);
        new MutationObserver(entries => {
            const newDocumentElement = entries.some(entry => Array.from(entry.addedNodes).includes(this.document.documentElement));
            if (!newDocumentElement)
                return;
            // New documentElement - let's check whether listeners are still here.
            seenEvent = false;
            this.window.dispatchEvent(new CustomEvent(customEventName));
            if (seenEvent)
                return;
            // Listener did not fire. Reattach the listener and notify.
            this.window.addEventListener(customEventName, handleCustomEvent);
            for (const callback of this.onGlobalListenersRemoved)
                callback();
        }).observe(this.document, { childList: true });
    }
    _setupHitTargetInterceptors() {
        const listener = (event) => this._hitTargetInterceptor?.(event);
        const addHitTargetInterceptorListeners = () => {
            for (const event of this._allHitTargetInterceptorEvents)
                this.window.addEventListener(event, listener, { capture: true, passive: false });
        };
        addHitTargetInterceptorListeners();
        this.onGlobalListenersRemoved.add(addHitTargetInterceptorListeners);
    }
    async expect(element, options, elements) {
        const core = await this._expectCore(element, options, elements);
        const ariaSnapshot = this._ariaSnapshotForExpect(element, options);
        if (core.received === undefined && ariaSnapshot === undefined)
            return { matches: core.matches, missingReceived: core.missingReceived };
        return { matches: core.matches, received: { value: core.received, ariaSnapshot }, missingReceived: core.missingReceived };
    }
    _ariaSnapshotForExpect(element, options) {
        const expression = options.expression;
        if (expression === 'to.have.count' || expression.endsWith('.array'))
            return undefined;
        if (expression === 'to.match.aria')
            return undefined;
        if (element && isElementVisible(element)) {
            // Element-scoped snapshot. Containment matchers want the full subtree;
            // property matchers only need the element's own line.
            const isContainment = expression === 'to.have.text';
            return this._renderAriaSnapshot(element, { mode: 'default', depth: isContainment ? undefined : 1 });
        }
        // Element missing or hidden — fall back to a full-page snapshot for context.
        if (!this.document.body)
            return undefined;
        return this._renderAriaSnapshot(this.document.body, { mode: 'default' });
    }
    _renderAriaSnapshot(element, options) {
        // Bypass _lastAriaSnapshotForQuery — that cache is reserved for explicit
        // ariaSnapshot() calls used by the aria-ref selector engine.
        return renderAriaTree(generateAriaTree(element, options), options).text;
    }
    async _expectCore(element, options, elements) {
        const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
        if (isArray)
            return this.expectArray(elements, options);
        if (!element) {
            // expect(locator).toBeHidden() passes when there is no element.
            if (!options.isNot && options.expression === 'to.be.hidden')
                return { matches: true };
            // expect(locator).not.toBeVisible() passes when there is no element.
            if (options.isNot && options.expression === 'to.be.visible')
                return { matches: false };
            // expect(locator).toBeAttached({ attached: false }) passes when there is no element.
            if (!options.isNot && options.expression === 'to.be.detached')
                return { matches: true };
            // expect(locator).not.toBeAttached() passes when there is no element.
            if (options.isNot && options.expression === 'to.be.attached')
                return { matches: false };
            // expect(locator).not.toBeInViewport() passes when there is no element.
            if (options.isNot && options.expression === 'to.be.in.viewport')
                return { matches: false };
            if (options.expression === 'to.have.title' && options?.expectedText?.[0]) {
                const matcher = new ExpectedTextMatcher(options.expectedText[0]);
                const received = this.document.title;
                return { received, matches: matcher.matches(received) };
            }
            if (options.expression === 'to.have.url' && options?.expectedText?.[0]) {
                const matcher = new ExpectedTextMatcher(options.expectedText[0]);
                const received = this.document.location.href;
                return { received, matches: matcher.matches(received) };
            }
            if (options.expression === 'to.match.aria' && !options.selector) {
                if (!this.document.body)
                    return { matches: options.isNot, missingReceived: true };
                const result = matchesExpectAriaTemplate(this.document.body, options.expectedValue);
                return {
                    received: result.received,
                    matches: !!result.matches.length,
                };
            }
            // When none of the above applies, expect does not match.
            return { matches: options.isNot, missingReceived: true };
        }
        return await this.expectSingleElement(element, options);
    }
    async expectSingleElement(element, options) {
        const expression = options.expression;
        {
            // Element state / boolean values.
            let result;
            if (expression === 'to.have.attribute') {
                const hasAttribute = element.hasAttribute(options.expressionArg);
                result = {
                    matches: hasAttribute,
                    received: hasAttribute ? 'attribute present' : 'attribute not present',
                };
            }
            else if (expression === 'to.be.checked') {
                const { checked, indeterminate } = options.expectedValue;
                if (indeterminate) {
                    if (checked !== undefined)
                        throw this.createStacklessError('Can\'t assert indeterminate and checked at the same time');
                    result = this.elementState(element, 'indeterminate');
                }
                else {
                    result = this.elementState(element, checked === false ? 'unchecked' : 'checked');
                }
            }
            else if (expression === 'to.be.disabled') {
                result = this.elementState(element, 'disabled');
            }
            else if (expression === 'to.be.editable') {
                result = this.elementState(element, 'editable');
            }
            else if (expression === 'to.be.readonly') {
                result = this.elementState(element, 'editable');
                result.matches = !result.matches;
            }
            else if (expression === 'to.be.empty') {
                if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
                    const value = element.value;
                    result = { matches: !value, received: value ? 'notEmpty' : 'empty' };
                }
                else {
                    const text = element.textContent?.trim();
                    result = { matches: !text, received: text ? 'notEmpty' : 'empty' };
                }
            }
            else if (expression === 'to.be.enabled') {
                result = this.elementState(element, 'enabled');
            }
            else if (expression === 'to.be.focused') {
                const focused = this._activelyFocused(element).isFocused;
                result = {
                    matches: focused,
                    received: focused ? 'focused' : 'inactive',
                };
            }
            else if (expression === 'to.be.hidden') {
                result = this.elementState(element, 'hidden');
            }
            else if (expression === 'to.be.visible') {
                result = this.elementState(element, 'visible');
            }
            else if (expression === 'to.be.attached') {
                result = {
                    matches: true,
                    received: 'attached',
                };
            }
            else if (expression === 'to.be.detached') {
                result = {
                    matches: false,
                    received: 'attached',
                };
            }
            if (result) {
                if (result.received === 'error:notconnected')
                    throw this.createStacklessError('Element is not connected');
                return result;
            }
        }
        {
            // JS property
            if (expression === 'to.have.property') {
                let target = element;
                const properties = options.expressionArg.split('.');
                for (let i = 0; i < properties.length - 1; i++) {
                    if (typeof target !== 'object' || !(properties[i] in target))
                        return { received: undefined, matches: false };
                    target = target[properties[i]];
                }
                const received = target[properties[properties.length - 1]];
                const matches = deepEquals(received, options.expectedValue);
                return { received, matches };
            }
        }
        {
            // Viewport intersection
            if (expression === 'to.be.in.viewport') {
                const ratio = await this.viewportRatio(element);
                return { received: `viewport ratio ${ratio}`, matches: ratio > 0 && ratio > (options.expectedNumber ?? 0) - 1e-9 };
            }
        }
        // Multi-Select/Combobox
        {
            if (expression === 'to.have.values') {
                element = this.retarget(element, 'follow-label');
                if (element.nodeName !== 'SELECT' || !element.multiple)
                    throw this.createStacklessError('Not a select element with a multiple attribute');
                const received = [...element.selectedOptions].map(o => o.value);
                if (received.length !== options.expectedText.length)
                    return { received, matches: false };
                return { received, matches: received.map((r, i) => new ExpectedTextMatcher(options.expectedText[i]).matches(r)).every(Boolean) };
            }
        }
        {
            if (expression === 'to.match.aria') {
                const result = matchesExpectAriaTemplate(element, options.expectedValue);
                return {
                    received: result.received,
                    matches: !!result.matches.length,
                };
            }
        }
        {
            // Single text value.
            let received;
            if (expression === 'to.have.attribute.value') {
                const value = element.getAttribute(options.expressionArg);
                if (value === null)
                    return { received: null, matches: false };
                received = value;
            }
            else if (['to.have.class', 'to.contain.class'].includes(expression)) {
                if (!options.expectedText)
                    throw this.createStacklessError('Expected text is not provided for ' + expression);
                return {
                    received: element.classList.toString(),
                    matches: new ExpectedTextMatcher(options.expectedText[0]).matchesClassList(this, element.classList, /* partial */ expression === 'to.contain.class'),
                };
            }
            else if (expression === 'to.have.css') {
                received = this.window.getComputedStyle(element, options.pseudo ? `::${options.pseudo}` : undefined).getPropertyValue(options.expressionArg);
            }
            else if (expression === 'to.have.id') {
                received = element.id;
            }
            else if (expression === 'to.have.text') {
                received = options.useInnerText ? element.innerText : elementText(new Map(), element).full;
            }
            else if (expression === 'to.have.accessible.name') {
                received = getElementAccessibleName(element, false /* includeHidden */);
            }
            else if (expression === 'to.have.accessible.description') {
                received = getElementAccessibleDescription(element, false /* includeHidden */);
            }
            else if (expression === 'to.have.accessible.error.message') {
                received = getElementAccessibleErrorMessage(element);
            }
            else if (expression === 'to.have.role') {
                received = getAriaRole(element) || '';
            }
            else if (expression === 'to.have.value') {
                element = this.retarget(element, 'follow-label');
                if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')
                    throw this.createStacklessError('Not an input element');
                received = element.value;
            }
            if (received !== undefined && options.expectedText) {
                const matcher = new ExpectedTextMatcher(options.expectedText[0]);
                return { received, matches: matcher.matches(received) };
            }
        }
        throw this.createStacklessError('Unknown expect matcher: ' + expression);
    }
    expectArray(elements, options) {
        const expression = options.expression;
        if (expression === 'to.have.count') {
            const received = elements.length;
            const matches = received === options.expectedNumber;
            return { received, matches };
        }
        // Following matchers depend all on ExpectedTextValue.
        if (!options.expectedText)
            throw this.createStacklessError('Expected text is not provided for ' + expression);
        if (['to.have.class.array', 'to.contain.class.array'].includes(expression)) {
            const receivedClassLists = elements.map(e => e.classList);
            const received = receivedClassLists.map(String);
            if (receivedClassLists.length !== options.expectedText.length)
                return { received, matches: false };
            const matches = this._matchSequentially(options.expectedText, receivedClassLists, (matcher, r) => matcher.matchesClassList(this, r, /* partial */ expression === 'to.contain.class.array'));
            return {
                received: received,
                matches,
            };
        }
        if (!['to.contain.text.array', 'to.have.text.array'].includes(expression))
            throw this.createStacklessError('Unknown expect matcher: ' + expression);
        const received = elements.map(e => options.useInnerText ? e.innerText : elementText(new Map(), e).full);
        // "To match an array" is "to contain an array" + "equal length"
        const lengthShouldMatch = expression !== 'to.contain.text.array';
        const matchesLength = received.length === options.expectedText.length || !lengthShouldMatch;
        if (!matchesLength)
            return { received, matches: false };
        const matches = this._matchSequentially(options.expectedText, received, (matcher, r) => matcher.matches(r));
        return { received, matches };
    }
    _matchSequentially(expectedText, received, matchFn) {
        const matchers = expectedText.map(e => new ExpectedTextMatcher(e));
        let mIndex = 0;
        let rIndex = 0;
        while (mIndex < matchers.length && rIndex < received.length) {
            if (matchFn(matchers[mIndex], received[rIndex]))
                ++mIndex;
            ++rIndex;
        }
        return mIndex === matchers.length;
    }
}
function oneLine(s) {
    return s.replace(/\n/g, '↵').replace(/\t/g, '⇆');
}
function cssUnquote(s) {
    // Trim quotes.
    s = s.substring(1, s.length - 1);
    if (!s.includes('\\'))
        return s;
    const r = [];
    let i = 0;
    while (i < s.length) {
        if (s[i] === '\\' && i + 1 < s.length)
            i++;
        r.push(s[i++]);
    }
    return r.join('');
}
function createTextMatcher(selector, internal) {
    if (selector[0] === '/' && selector.lastIndexOf('/') > 0) {
        const lastSlash = selector.lastIndexOf('/');
        const re = new RegExp(selector.substring(1, lastSlash), selector.substring(lastSlash + 1));
        return { matcher: (elementText) => re.test(elementText.full), kind: 'regex' };
    }
    const unquote = internal ? JSON.parse.bind(JSON) : cssUnquote;
    let strict = false;
    if (selector.length > 1 && selector[0] === '"' && selector[selector.length - 1] === '"') {
        selector = unquote(selector);
        strict = true;
    }
    else if (internal && selector.length > 1 && selector[0] === '"' && selector[selector.length - 2] === '"' && selector[selector.length - 1] === 'i') {
        selector = unquote(selector.substring(0, selector.length - 1));
        strict = false;
    }
    else if (internal && selector.length > 1 && selector[0] === '"' && selector[selector.length - 2] === '"' && selector[selector.length - 1] === 's') {
        selector = unquote(selector.substring(0, selector.length - 1));
        strict = true;
    }
    else if (selector.length > 1 && selector[0] === "'" && selector[selector.length - 1] === "'") {
        selector = unquote(selector);
        strict = true;
    }
    selector = normalizeWhiteSpace(selector);
    if (strict) {
        if (internal)
            return { kind: 'strict', matcher: (elementText) => elementText.normalized === selector };
        const strictTextNodeMatcher = (elementText) => {
            if (!selector && !elementText.immediate.length)
                return true;
            return elementText.immediate.some(s => normalizeWhiteSpace(s) === selector);
        };
        return { matcher: strictTextNodeMatcher, kind: 'strict' };
    }
    selector = selector.toLowerCase();
    return { kind: 'lax', matcher: (elementText) => elementText.normalized.toLowerCase().includes(selector) };
}
class ExpectedTextMatcher {
    _string;
    _substring;
    _regex;
    _normalizeWhiteSpace;
    _ignoreCase;
    constructor(expected) {
        this._normalizeWhiteSpace = expected.normalizeWhiteSpace;
        this._ignoreCase = expected.ignoreCase;
        this._string = expected.matchSubstring ? undefined : this.normalize(expected.string);
        this._substring = expected.matchSubstring ? this.normalize(expected.string) : undefined;
        if (expected.regexSource) {
            const flags = new Set((expected.regexFlags || '').split(''));
            if (expected.ignoreCase === false)
                flags.delete('i');
            if (expected.ignoreCase === true)
                flags.add('i');
            this._regex = new RegExp(expected.regexSource, [...flags].join(''));
        }
    }
    matches(text) {
        if (!this._regex)
            text = this.normalize(text);
        if (this._string !== undefined)
            return text === this._string;
        if (this._substring !== undefined)
            return text.includes(this._substring);
        if (this._regex)
            return !!this._regex.test(text);
        return false;
    }
    matchesClassList(injectedScript, classList, partial) {
        if (partial) {
            if (this._regex)
                throw injectedScript.createStacklessError('Partial matching does not support regular expressions. Please provide a string value.');
            return this._string.split(/\s+/g).filter(Boolean).every(className => classList.contains(className));
        }
        return this.matches(classList.toString());
    }
    normalize(s) {
        if (!s)
            return s;
        if (this._normalizeWhiteSpace)
            s = normalizeWhiteSpace(s);
        if (this._ignoreCase)
            s = s.toLocaleLowerCase();
        return s;
    }
}
function deepEquals(a, b) {
    if (a === b)
        return true;
    if (a && b && typeof a === 'object' && typeof b === 'object') {
        if (a.constructor !== b.constructor)
            return false;
        if (Array.isArray(a)) {
            if (a.length !== b.length)
                return false;
            for (let i = 0; i < a.length; ++i) {
                if (!deepEquals(a[i], b[i]))
                    return false;
            }
            return true;
        }
        if (a instanceof RegExp)
            return a.source === b.source && a.flags === b.flags;
        // This covers Date.
        if (a.valueOf !== Object.prototype.valueOf)
            return a.valueOf() === b.valueOf();
        // This covers custom objects.
        if (a.toString !== Object.prototype.toString)
            return a.toString() === b.toString();
        const keys = Object.keys(a);
        if (keys.length !== Object.keys(b).length)
            return false;
        for (let i = 0; i < keys.length; ++i) {
            if (!b.hasOwnProperty(keys[i]))
                return false;
        }
        for (const key of keys) {
            if (!deepEquals(a[key], b[key]))
                return false;
        }
        return true;
    }
    if (typeof a === 'number' && typeof b === 'number')
        return isNaN(a) && isNaN(b);
    return false;
}
//# sourceMappingURL=injectedScript.js.map