"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeElement = exports.isElementReference = exports.isElement = exports.isElementInstance = exports.Element = void 0;
const selector_1 = require("./selector");
const utils = __importStar(require("@applitools/utils"));
const snippets = require('@applitools/snippets');
class Element {
    constructor(options) {
        var _a, _b, _c, _d, _e, _f;
        this._state = {};
        this._spec = options.spec;
        this._context = options.context;
        if (utils.types.has(options, 'element')) {
            this._target = options.element;
            if (this._spec.isElement(this._target)) {
                // Some frameworks contains information about the selector inside an element
                this._selector = (_a = options.selector) !== null && _a !== void 0 ? _a : (_c = (_b = this._spec).extractSelector) === null || _c === void 0 ? void 0 : _c.call(_b, options.element);
                this._index = options.index;
            }
        }
        else if ((0, selector_1.isSelector)(options.selector, this._spec)) {
            this._selector = options.selector;
        }
        else {
            throw new TypeError('Element constructor called with argument of unknown type!');
        }
        if ((0, selector_1.isSimpleCommonSelector)(this._selector) && !utils.types.isString(this._selector)) {
            this._commonSelector = this._selector;
        }
        else if (this._selector && this._spec.toSimpleCommonSelector) {
            this._commonSelector = this._spec.toSimpleCommonSelector((_f = (_e = (_d = this._spec).toSelector) === null || _e === void 0 ? void 0 : _e.call(_d, this._selector)) !== null && _f !== void 0 ? _f : this._selector);
        }
        else if (utils.types.isString(this._selector)) {
            this._commonSelector = { selector: this._selector };
        }
        else {
            this._commonSelector = null;
        }
    }
    get logger() {
        return this._context.logger;
    }
    get target() {
        return this._target;
    }
    get selector() {
        return this._selector;
    }
    get commonSelector() {
        return this._commonSelector;
    }
    get index() {
        return this._index;
    }
    get context() {
        return this._context;
    }
    get driver() {
        return this.context.driver;
    }
    get isRef() {
        var _a;
        return ((_a = this.context) === null || _a === void 0 ? void 0 : _a.isRef) || !this.target;
    }
    async equals(element) {
        var _a, _b, _c;
        if (this.isRef)
            return false;
        element = element instanceof Element ? element.target : element;
        const environment = await this.driver.getEnvironment();
        if (environment.isWeb) {
            return this._spec
                .executeScript(this.context.target, snippets.isEqualElements, [this.target, element])
                .catch(() => false);
        }
        else {
            return (_c = (_b = (_a = this._spec).isEqualElements) === null || _b === void 0 ? void 0 : _b.call(_a, this.context.target, this.target, element)) !== null && _c !== void 0 ? _c : false;
        }
    }
    async contains(innerElement) {
        const contains = await this.withRefresh(async () => {
            var _a, _b;
            var _c;
            innerElement = innerElement instanceof Element ? innerElement.target : innerElement;
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                this.logger.log('Checking if web element with selector', this.selector, 'contains element', innerElement);
                return false; // TODO implement a snipped for web
            }
            else {
                if ((_a = this._state.containedElements) === null || _a === void 0 ? void 0 : _a.has(innerElement))
                    return this._state.containedElements.get(innerElement);
                this.logger.log('Checking if native element with selector', this.selector, 'contains element', innerElement);
                // appium doesn't have a way to check if an element is contained in another element, so juristic applied
                if (await this.equals(innerElement))
                    return false;
                // if the inner element region is contained in this element region, then it then could be assumed that the inner element is contained in this element
                const contentRegion = await this.getContentRegion();
                const innerRegion = await this._spec.getElementRegion(this.driver.target, innerElement);
                const contains = utils.geometry.contains(contentRegion, innerRegion);
                (_b = (_c = this._state).containedElements) !== null && _b !== void 0 ? _b : (_c.containedElements = new Map());
                this._state.containedElements.set(innerElement, contains);
                return contains;
            }
        });
        this.logger.log('Element with selector', this.selector, contains ? 'contains' : `doesn't contain`, innerElement);
        return contains;
    }
    async init(context) {
        this._context = context;
        if (this._target)
            return this;
        if (this._selector) {
            const element = await this._context.element(this._selector);
            if (!element)
                throw new Error(`Cannot find element with selector ${JSON.stringify(this._selector)}`);
            this._target = element.target;
        }
        return this;
    }
    async getRegion() {
        const region = await this.withRefresh(async () => {
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                this.logger.log('Extracting region of web element with selector', this.selector);
                return this.context.execute(snippets.getElementRect, [this, false]);
            }
            else {
                this.logger.log('Extracting region of native element with selector', this.selector);
                const region = await this._spec.getElementRegion(this.driver.target, this.target);
                this.logger.log('Extracted native region', region);
                const normalizedRegion = await this.driver.normalizeRegion(region);
                // if element is a child of scrolling element, then region location should be adjusted
                const scrollingElement = await this.context.getScrollingElement();
                return (await (scrollingElement === null || scrollingElement === void 0 ? void 0 : scrollingElement.contains(this)))
                    ? utils.geometry.offset(normalizedRegion, await scrollingElement.getScrollOffset())
                    : normalizedRegion;
            }
        });
        this.logger.log('Extracted region', region);
        return region;
    }
    async getClientRegion() {
        const region = await this.withRefresh(async () => {
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                this.logger.log('Extracting region of web element with selector', this.selector);
                return this.context.execute(snippets.getElementRect, [this, true]);
            }
            else {
                return this.getRegion();
            }
        });
        this.logger.log('Extracted client region', region);
        return region;
    }
    async getContentRegion(options = {}) {
        var _a, _b;
        const environment = await this.driver.getEnvironment();
        if (!environment.isNative)
            return null;
        this.logger.log('Extracting content region of native element with selector', this.selector);
        const helper = await this.driver.getHelper();
        let contentRegion = await (helper === null || helper === void 0 ? void 0 : helper.getContentRegion(this, options));
        this.logger.log('Extracted content region using helper library', contentRegion);
        if (!contentRegion || !environment.isAndroid) {
            let attrContentRegion = null;
            try {
                const { left, top, width, height, scrollableOffset } = JSON.parse(await this.getAttribute('contentSize'));
                if ([left, top, width, height, scrollableOffset].some(value => !utils.types.isNumber(value)))
                    throw new Error('Invalid contentSize');
                attrContentRegion = {
                    x: left,
                    y: top,
                    width: width,
                    height: environment.isIOS ? Math.max(height, scrollableOffset) : height + scrollableOffset,
                };
            }
            catch (err) {
                this.logger.warn(`Unable to get the attribute 'contentSize' due to the following error: '${err.message}'`);
            }
            this.logger.log('Extracted content region using attribute', attrContentRegion);
            // ios workaround
            if (!attrContentRegion && environment.isIOS) {
                try {
                    const type = await this.getAttribute('type');
                    if (type === 'XCUIElementTypeScrollView') {
                        const [child] = await this.driver.elements({
                            type: 'xpath',
                            selector: '//XCUIElementTypeScrollView[1]/*', // We cannot be sure that our element is the first one
                        });
                        if (child) {
                            const region = await this._spec.getElementRegion(this.driver.target, this.target);
                            const childRegion = await this._spec.getElementRegion(this.driver.target, child.target);
                            attrContentRegion = {
                                ...region,
                                height: childRegion.y + childRegion.height - region.y,
                            };
                        }
                    }
                }
                catch (err) {
                    this.logger.warn(`Unable to calculate content region using iOS workaround due to the following error: '${err.message}'`);
                }
                this.logger.log('Extracted content region using iOS workaround', attrContentRegion);
            }
            if (attrContentRegion) {
                contentRegion = {
                    x: attrContentRegion.x,
                    y: attrContentRegion.y,
                    width: Math.max((_a = contentRegion === null || contentRegion === void 0 ? void 0 : contentRegion.width) !== null && _a !== void 0 ? _a : 0, attrContentRegion.width),
                    height: Math.max((_b = contentRegion === null || contentRegion === void 0 ? void 0 : contentRegion.height) !== null && _b !== void 0 ? _b : 0, attrContentRegion.height),
                };
            }
        }
        return contentRegion !== null && contentRegion !== void 0 ? contentRegion : (await this._spec.getElementRegion(this.driver.target, this.target));
    }
    async getContentSize(options = {}) {
        if (this._state.contentSize)
            return this._state.contentSize;
        const size = await this.withRefresh(async () => {
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                this.logger.log('Extracting content size of web element with selector', this.selector);
                return this.context.execute(snippets.getElementContentSize, [this]);
            }
            else {
                this.logger.log('Extracting content size of native element with selector', this.selector);
                try {
                    const contentRegion = await this.getContentRegion(options);
                    this._state.contentSize = utils.geometry.size(contentRegion);
                    const viewport = await this.driver.getViewport();
                    if (environment.isAndroid) {
                        this._state.contentSize = utils.geometry.scale(this._state.contentSize, 1 / viewport.pixelRatio);
                    }
                    if (contentRegion.y < viewport.statusBarSize) {
                        this._state.contentSize.height -= viewport.statusBarSize - contentRegion.y;
                    }
                    return this._state.contentSize;
                }
                catch (err) {
                    this.logger.warn('Failed to extract content size, extracting client size instead');
                    this.logger.error(err);
                    return utils.geometry.size(await this.getClientRegion());
                }
            }
        });
        this.logger.log('Extracted content size', size);
        return size;
    }
    async isPager() {
        this.logger.log('Check if element with selector', this.selector, 'is scrollable by pages');
        const isPager = await this.withRefresh(async () => {
            const environment = await this.driver.getEnvironment();
            if (environment.isAndroid) {
                const className = await this.getAttribute('className');
                return ['androidx.viewpager.widget.ViewPager'].includes(className);
            }
            else {
                return false;
            }
        });
        this.logger.log('Element scrollable by pages', isPager);
        return isPager;
    }
    async isScrollable() {
        this.logger.log('Check if element with selector', this.selector, 'is scrollable');
        const isScrollable = await this.withRefresh(async () => {
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                return this.context.execute(snippets.isElementScrollable, [this]);
            }
            else if (environment.isAndroid) {
                const data = JSON.parse(await this.getAttribute('scrollable'));
                return Boolean(data) || false;
            }
            else if (environment.isIOS) {
                const type = await this.getAttribute('type');
                return ['XCUIElementTypeScrollView', 'XCUIElementTypeTable', 'XCUIElementTypeCollectionView'].includes(type);
            }
        });
        this.logger.log('Element is scrollable', isScrollable);
        return isScrollable;
    }
    async isRoot() {
        // TODO replace with snippet
        return this.withRefresh(async () => {
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                const rootElement = await this.context.element({ type: 'css', selector: 'html' });
                return this.equals(rootElement);
            }
            else {
                return false;
            }
        });
    }
    async getShadowRoot() {
        const environment = await this.driver.getEnvironment();
        if (!environment.isWeb)
            return null;
        return this._spec.executeScript(this.context.target, snippets.getShadowRoot, [this.target]);
    }
    async getTouchPadding() {
        var _a;
        var _b;
        if (this._state.touchPadding == null) {
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb)
                this._state.touchPadding = 0;
            else if (environment.isIOS)
                this._state.touchPadding = 10;
            else if (environment.isAndroid) {
                const helper = await this.driver.getHelper();
                if ((helper === null || helper === void 0 ? void 0 : helper.name) === 'android') {
                    this._state.touchPadding = await helper.getTouchPadding();
                    this.logger.log('Touch padding extracted using helper library', this._state.touchPadding);
                }
                if (!this._state.touchPadding) {
                    this._state.touchPadding = await this.getAttribute('contentSize')
                        .then(data => JSON.parse(data).touchPadding)
                        .catch(err => {
                        this.logger.warn(`Unable to get the attribute 'contentSize' when looking up 'touchPadding' due to the following error: '${err.message}'`);
                    });
                    this.logger.log('Touch padding extracted using attribute', this._state.touchPadding);
                }
                (_a = (_b = this._state).touchPadding) !== null && _a !== void 0 ? _a : (_b.touchPadding = 20);
                this.logger.log('Touch padding set:', this._state.touchPadding);
            }
        }
        return this._state.touchPadding;
    }
    async getText() {
        const text = await this.withRefresh(async () => {
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                return '';
            }
            else {
                this.logger.log('Extracting text of native element with selector', this.selector);
                return this._spec.getElementText(this.context.target, this.target);
            }
        });
        this.logger.log('Extracted element text', text);
        return text;
    }
    async getAttribute(name) {
        // we assumes that attributes are not changed during the session
        if (utils.types.has(this._state.attributes, name)) {
            if (this._state.attributes[name] instanceof Error)
                throw this._state.attributes[name];
            return this._state.attributes[name];
        }
        const value = await this.withRefresh(async () => {
            var _a;
            var _b;
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                const properties = await this.context.execute(snippets.getElementProperties, [this, [name]]);
                return properties[name];
            }
            else {
                this.logger.log(`Extracting "${name}" attribute of native element with selector`, this.selector);
                (_a = (_b = this._state).attributes) !== null && _a !== void 0 ? _a : (_b.attributes = {});
                try {
                    this._state.attributes[name] = await this._spec.getElementAttribute(this.driver.target, this.target, name);
                    return this._state.attributes[name];
                }
                catch (err) {
                    this._state.attributes[name] = err;
                    throw err;
                }
                finally {
                    if (environment.isAndroid && name === 'contentSize') {
                        // android has a bug when after extracting 'contentSize' attribute the element is being scrolled by undetermined number of pixels
                        this.logger.log('Stabilizing android scroll offset');
                        const originalScrollOffset = await this.getScrollOffset();
                        await this.scrollTo({ x: 0, y: 0 }, { force: true });
                        await this.scrollTo(originalScrollOffset);
                    }
                }
            }
        });
        this.logger.log(`Extracted element "${name}" attribute:`, value);
        return value;
    }
    async setAttribute(name, value) {
        const environment = await this.driver.getEnvironment();
        if (environment.isWeb) {
            await this.context.execute(snippets.setElementAttributes, [this, { [name]: value }]);
        }
    }
    async scrollTo(offset, options) {
        return this.withRefresh(async () => {
            this.logger.log(`Scrolling to offset (${offset.x}, ${offset.y}) element with selector`, this.selector);
            offset = utils.geometry.round({ x: Math.max(offset.x, 0), y: Math.max(offset.y, 0) });
            const environment = await this.driver.getEnvironment();
            if (environment.isWeb) {
                let actualOffset = await this.context.execute(snippets.scrollTo, [this, offset]);
                // iOS has an issue when scroll offset is read immediately after it is been set it will always return the exact value that was set
                if (environment.isIOS)
                    actualOffset = await this.getScrollOffset();
                return actualOffset;
            }
            else {
                const currentScrollOffset = await this.getScrollOffset();
                if (!(options === null || options === void 0 ? void 0 : options.force) && utils.geometry.equals(offset, currentScrollOffset))
                    return currentScrollOffset;
                if (utils.geometry.equals(offset, { x: 0, y: 0 }) && environment.isAndroid) {
                    const helper = await this.driver.getHelper();
                    if ((helper === null || helper === void 0 ? void 0 : helper.name) === 'android') {
                        await helper.scrollToTop(this);
                        this._state.scrollOffset = offset;
                        return this._state.scrollOffset;
                    }
                }
                const contentSize = await this.getContentSize();
                const scrollableRegion = await this.getClientRegion();
                const maxOffset = {
                    x: Math.round(scrollableRegion.width * (contentSize.width / scrollableRegion.width - 1)),
                    y: Math.round(scrollableRegion.height * (contentSize.height / scrollableRegion.height - 1)),
                };
                const requiredOffset = { x: Math.min(offset.x, maxOffset.x), y: Math.min(offset.y, maxOffset.y) };
                let effectiveRegion = scrollableRegion;
                let remainingOffset = utils.geometry.equals(requiredOffset, { x: 0, y: 0 })
                    ? { x: -maxOffset.x, y: -maxOffset.y } // if it has to be scrolled to the very beginning, then scroll maximum amount of pixels
                    : utils.geometry.offsetNegative(requiredOffset, currentScrollOffset);
                if (environment.isAndroid) {
                    const viewport = await this.driver.getViewport();
                    remainingOffset = utils.geometry.round(utils.geometry.scale(remainingOffset, viewport.pixelRatio));
                    effectiveRegion = utils.geometry.round(utils.geometry.scale(effectiveRegion, viewport.pixelRatio));
                }
                const isPager = await this.isPager();
                const touchPadding = await this.getTouchPadding();
                const actions = [];
                // horizontal scrolling
                const xPadding = touchPadding + 3;
                const yTrack = Math.floor(effectiveRegion.y + effectiveRegion.height / 2); // center
                const xLeft = effectiveRegion.y + xPadding;
                const xDirection = remainingOffset.x > 0 ? 'right' : 'left';
                const xGap = xDirection === 'right' ? -touchPadding : touchPadding;
                const xCompensation = xDirection === 'right' ? -1 : 1;
                let xRemaining = Math.abs(remainingOffset.x);
                if (isPager) {
                    const xPages = Math.floor(xRemaining / effectiveRegion.width);
                    xRemaining = (effectiveRegion.width - xPadding * 2) * xPages;
                }
                while (xRemaining > 0) {
                    const xRight = effectiveRegion.x + Math.min(xRemaining + xPadding, effectiveRegion.width - xPadding);
                    const [xStart, xEnd] = xDirection === 'right' ? [xRight, xLeft] : [xLeft, xRight];
                    if (isPager) {
                        actions.push([
                            { action: 'press', y: yTrack, x: xStart },
                            // scroll through the page
                            { action: 'wait', ms: 170 },
                            { action: 'moveTo', y: yTrack, x: xEnd },
                            { action: 'release' },
                        ]);
                    }
                    else if (environment.isAndroid) {
                        actions.push([
                            // move through scrolling gap (actual scrolling will be triggered only after that)
                            { action: 'press', y: yTrack, x: xStart - xGap },
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', y: yTrack, x: xStart + xCompensation },
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', y: yTrack, x: xStart },
                            // perform actual scrolling
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', y: yTrack, x: xEnd },
                            { action: 'release' },
                        ]);
                    }
                    else if (environment.isIOS) {
                        actions.push([
                            // move through scrolling gap (actual scrolling will be triggered only after that)
                            { action: 'press', y: yTrack, x: xStart - xGap },
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', y: yTrack, x: xStart },
                            // perform actual scrolling
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', y: yTrack, x: xEnd },
                            // prevent inertial scrolling after release
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', y: yTrack + 1, x: xEnd },
                            { action: 'release' },
                        ]);
                    }
                    xRemaining -= xRight - xLeft;
                }
                // vertical scrolling
                const yPadding = Math.max(Math.floor(effectiveRegion.height * 0.08), touchPadding + 3);
                const xTrack = Math.floor(effectiveRegion.x + 5); // a little bit off left border
                const yBottom = effectiveRegion.y + effectiveRegion.height - yPadding;
                const yDirection = remainingOffset.y > 0 ? 'down' : 'up';
                const yGap = yDirection === 'down' ? -touchPadding : touchPadding;
                const yCompensation = yDirection === 'down' ? -1 : 1;
                let yRemaining = Math.abs(remainingOffset.y);
                if (isPager) {
                    const yPages = Math.floor(yRemaining / effectiveRegion.height);
                    yRemaining = (effectiveRegion.height - yPadding * 2) * yPages;
                }
                while (yRemaining > 0) {
                    const yTop = Math.max(yBottom - yRemaining, effectiveRegion.y + yPadding);
                    const [yStart, yEnd] = yDirection === 'down' ? [yBottom, yTop] : [yTop, yBottom];
                    if (isPager) {
                        actions.push([
                            { action: 'press', x: xTrack, y: yStart },
                            // scroll through the page
                            { action: 'wait', ms: 170 },
                            { action: 'moveTo', x: xTrack, y: yEnd },
                            { action: 'release' },
                        ]);
                    }
                    else if (environment.isAndroid) {
                        actions.push([
                            // move through scrolling gap (actual scrolling will be triggered only after that)
                            { action: 'press', x: xTrack, y: yStart - yGap },
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', x: xTrack, y: yStart + yCompensation },
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', x: xTrack, y: yStart },
                            // perform actual scrolling
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', x: xTrack, y: yEnd },
                            { action: 'release' },
                        ]);
                    }
                    else if (environment.isIOS) {
                        actions.push([
                            // move through scrolling gap (actual scrolling will be triggered only after that)
                            { action: 'press', x: xTrack, y: yStart - yGap },
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', x: xTrack, y: yStart },
                            // perform actual scrolling
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', x: xTrack, y: yEnd },
                            // prevent inertial scrolling after release
                            { action: 'wait', ms: 100 },
                            { action: 'moveTo', x: xTrack + 1, y: yEnd },
                            { action: 'release' },
                        ]);
                    }
                    yRemaining -= yBottom - yTop;
                }
                // ios actions should be executed one-by-one sequentially, otherwise the result isn't stable
                // pages should be scrolled one-by-one as well
                if (isPager || environment.isIOS) {
                    for (const action of actions) {
                        await this._spec.performAction(this.driver.target, action);
                    }
                }
                else if (actions.length > 0) {
                    await this._spec.performAction(this.driver.target, actions.flat());
                }
                const actualScrollableRegion = await this.getClientRegion();
                this._state.scrollOffset = utils.geometry.offsetNegative(requiredOffset, {
                    x: scrollableRegion.x - actualScrollableRegion.x,
                    y: scrollableRegion.y - actualScrollableRegion.y,
                });
                return this._state.scrollOffset;
            }
        });
    }
    async translateTo(offset) {
        offset = { x: Math.round(offset.x), y: Math.round(offset.y) };
        const environment = await this.driver.getEnvironment();
        if (environment.isWeb) {
            return this.withRefresh(async () => this.context.execute(snippets.translateTo, [this, offset]));
        }
        else {
            throw new Error('Cannot apply css translate scrolling on non-web element');
        }
    }
    async getScrollOffset() {
        var _a;
        const environment = await this.driver.getEnvironment();
        if (environment.isWeb) {
            return this.withRefresh(() => this.context.execute(snippets.getElementScrollOffset, [this]));
        }
        else {
            return (_a = this._state.scrollOffset) !== null && _a !== void 0 ? _a : { x: 0, y: 0 };
        }
    }
    async getTranslateOffset() {
        const environment = await this.driver.getEnvironment();
        if (environment.isWeb) {
            return this.withRefresh(() => this.context.execute(snippets.getElementTranslateOffset, [this]));
        }
        else {
            throw new Error('Cannot apply css translate scrolling on non-web element');
        }
    }
    async getInnerOffset() {
        const environment = await this.driver.getEnvironment();
        if (environment.isWeb) {
            return this.withRefresh(() => this.context.execute(snippets.getElementInnerOffset, [this]));
        }
        else {
            return this.getScrollOffset();
        }
    }
    async hover() {
        var _a, _b;
        this.logger.log(`Hovering on the element with selector`, this.selector);
        await ((_b = (_a = this._spec).hover) === null || _b === void 0 ? void 0 : _b.call(_a, this.context.target, this.target));
    }
    async click() {
        var _a, _b;
        this.logger.log(`Clicking on the element with selector`, this.selector);
        await ((_b = (_a = this._spec).click) === null || _b === void 0 ? void 0 : _b.call(_a, this.context.target, this.target));
    }
    async type(value) {
        var _a, _b;
        this.logger.log(`Typing text "${value}" in the element with selector`, this.selector);
        await ((_b = (_a = this._spec).setElementText) === null || _b === void 0 ? void 0 : _b.call(_a, this.context.target, this.target, value));
    }
    async preserveState() {
        const scrollOffset = await this.getScrollOffset();
        const environment = await this.driver.getEnvironment();
        const transforms = environment.isWeb
            ? await this.context.execute(snippets.getElementStyleProperties, [this, ['transform', '-webkit-transform']])
            : null;
        if (!utils.types.has(this._state, ['scrollOffset', 'transforms'])) {
            this._state.scrollOffset = scrollOffset;
            this._state.transforms = transforms;
        }
        return { scrollOffset, transforms };
    }
    async restoreState(state = this._state) {
        if (state.scrollOffset)
            await this.scrollTo(state.scrollOffset);
        if (state.transforms)
            await this.context.execute(snippets.setElementStyleProperties, [this, state.transforms]);
        if (state === this._state) {
            this._state.scrollOffset = undefined;
            this._state.transforms = undefined;
        }
    }
    async hideScrollbars() {
        const environment = await this.driver.getEnvironment();
        if (environment.isNative)
            return;
        if (this._originalOverflow)
            return;
        return this.withRefresh(async () => {
            const { overflow } = await this.context.execute(snippets.setElementStyleProperties, [this, { overflow: 'hidden' }]);
            this._originalOverflow = overflow;
        });
    }
    async restoreScrollbars() {
        const environment = await this.driver.getEnvironment();
        if (environment.isNative)
            return;
        if (!this._originalOverflow)
            return;
        return this.withRefresh(async () => {
            await this.context.execute(snippets.setElementStyleProperties, [this, { overflow: this._originalOverflow }]);
            this._originalOverflow = null;
        });
    }
    async refresh(freshTarget) {
        if (this._spec.isElement(freshTarget)) {
            this._target = freshTarget;
            return true;
        }
        if (!this._selector)
            return false;
        const element = this._index > 0
            ? await this.context.elements(this._selector).then(elements => elements[this._index])
            : await this.context.element(this._selector);
        if (element) {
            this._target = element.target;
        }
        return Boolean(element);
    }
    async withRefresh(operation) {
        if (!this._spec.isStaleElementError)
            return operation();
        try {
            const result = await operation();
            // Some frameworks could handle stale element reference error by itself or doesn't throw an error
            if (this._spec.isStaleElementError(result, this.selector)) {
                const refreshed = await this.refresh();
                if (!refreshed) {
                    throw result;
                }
                return operation();
            }
            return result;
        }
        catch (err) {
            if (this._spec.isStaleElementError(err)) {
                const refreshed = await this.refresh();
                if (refreshed)
                    return operation();
            }
            throw err;
        }
    }
    toJSON() {
        return this.target;
    }
}
exports.Element = Element;
function isElementInstance(element) {
    return element instanceof Element;
}
exports.isElementInstance = isElementInstance;
function isElement(element, spec) {
    var _a;
    return isElementInstance(element) || !!(spec === null || spec === void 0 ? void 0 : spec.isElement(element)) || !!((_a = spec === null || spec === void 0 ? void 0 : spec.isSecondaryElement) === null || _a === void 0 ? void 0 : _a.call(spec, element));
}
exports.isElement = isElement;
function isElementReference(reference, spec) {
    return isElement(reference, spec) || (0, selector_1.isSelector)(reference, spec);
}
exports.isElementReference = isElementReference;
async function makeElement(options) {
    var _a, _b, _c;
    let element;
    if (utils.types.has(options, 'element')) {
        if (options.element instanceof Element) {
            element = options.element;
        }
        else {
            options.element = (_c = (await ((_b = (_a = options.spec) === null || _a === void 0 ? void 0 : _a.toElement) === null || _b === void 0 ? void 0 : _b.call(_a, options.element)))) !== null && _c !== void 0 ? _c : options.element;
            element = new Element(options);
        }
    }
    else {
        element = new Element(options);
    }
    return element;
}
exports.makeElement = makeElement;
