/**
 * 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 { asLocatorDescription, locatorCustomDescription } from '@isomorphic/locatorGenerators';
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '@isomorphic/locatorUtils';
import { escapeForTextSelector } from '@isomorphic/stringUtils';
import { isString } from '@isomorphic/rtti';
import { monotonicTime } from '@isomorphic/time';
import { ElementHandle } from './elementHandle';
import { DisposableStub } from './disposable';
export class Locator {
    _frame;
    _selector;
    _apiName = 'Locator';
    constructor(frame, selector, options) {
        this._frame = frame;
        this._selector = selector;
        if (options?.hasText)
            this._selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
        if (options?.hasNotText)
            this._selector += ` >> internal:has-not-text=${escapeForTextSelector(options.hasNotText, false)}`;
        if (options?.has) {
            const locator = options.has;
            if (locator._frame !== frame)
                throw new Error(`Inner "has" locator must belong to the same frame.`);
            this._selector += ` >> internal:has=` + JSON.stringify(locator._selector);
        }
        if (options?.hasNot) {
            const locator = options.hasNot;
            if (locator._frame !== frame)
                throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
            this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
        }
        if (options?.visible !== undefined)
            this._selector += ` >> visible=${options.visible ? 'true' : 'false'}`;
        if (this._frame._platform.inspectCustom)
            this[this._frame._platform.inspectCustom] = () => this._inspect();
    }
    async _withElement(task, options) {
        const timeout = this._frame._timeout({ timeout: options.timeout });
        const deadline = timeout ? monotonicTime() + timeout : 0;
        return await this._frame._wrapApiCall(async () => {
            const result = await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, state: 'attached', timeout });
            const handle = ElementHandle.fromNullable(result.element);
            if (!handle)
                throw new Error(`Could not resolve ${this._selector} to DOM Element`);
            try {
                return await task(handle, deadline ? deadline - monotonicTime() : 0);
            }
            finally {
                await handle.dispose();
            }
        }, { title: options.title, internal: options.internal });
    }
    _equals(locator) {
        return this._frame === locator._frame && this._selector === locator._selector;
    }
    page() {
        return this._frame.page();
    }
    async boundingBox(options) {
        return await this._withElement(h => h.boundingBox(), { title: 'Bounding box', timeout: options?.timeout });
    }
    async check(options = {}) {
        return await this._frame.check(this._selector, { strict: true, ...options });
    }
    async click(options = {}) {
        return await this._frame.click(this._selector, { strict: true, ...options });
    }
    async dblclick(options = {}) {
        await this._frame.dblclick(this._selector, { strict: true, ...options });
    }
    async dispatchEvent(type, eventInit = {}, options) {
        return await this._frame.dispatchEvent(this._selector, type, eventInit, { strict: true, ...options });
    }
    async dragTo(target, options = {}) {
        return await this._frame.dragAndDrop(this._selector, target._selector, {
            strict: true,
            ...options,
        });
    }
    async drop(payload, options = {}) {
        await this._frame._drop(this._selector, payload, { strict: true, ...options });
    }
    async evaluate(pageFunction, arg, options) {
        return await this._withElement(h => h.evaluate(pageFunction, arg), { title: 'Evaluate', timeout: options?.timeout });
    }
    async evaluateAll(pageFunction, arg) {
        return await this._frame.$$eval(this._selector, pageFunction, arg);
    }
    async evaluateHandle(pageFunction, arg, options) {
        return await this._withElement(h => h.evaluateHandle(pageFunction, arg), { title: 'Evaluate', timeout: options?.timeout });
    }
    async fill(value, options = {}) {
        return await this._frame.fill(this._selector, value, { strict: true, ...options });
    }
    async clear(options = {}) {
        await this._frame._wrapApiCall(() => this.fill('', options), { title: 'Clear' });
    }
    async _highlight() {
        // VS Code extension uses this one, keep it for now.
        return await this._frame._highlight(this._selector);
    }
    async highlight(options = {}) {
        const style = typeof options.style === 'object' ? cssObjectToString(options.style) : options.style;
        await this._frame._highlight(this._selector, style);
        return new DisposableStub(() => this.hideHighlight());
    }
    async hideHighlight() {
        await this._frame._hideHighlight(this._selector);
    }
    locator(selectorOrLocator, options) {
        if (isString(selectorOrLocator))
            return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
        if (selectorOrLocator._frame !== this._frame)
            throw new Error(`Locators must belong to the same frame.`);
        return new Locator(this._frame, this._selector + ' >> internal:chain=' + JSON.stringify(selectorOrLocator._selector), options);
    }
    getByTestId(testId) {
        return this.locator(getByTestIdSelector(testIdAttributeName(), testId));
    }
    getByAltText(text, options) {
        return this.locator(getByAltTextSelector(text, options));
    }
    getByLabel(text, options) {
        return this.locator(getByLabelSelector(text, options));
    }
    getByPlaceholder(text, options) {
        return this.locator(getByPlaceholderSelector(text, options));
    }
    getByText(text, options) {
        return this.locator(getByTextSelector(text, options));
    }
    getByTitle(text, options) {
        return this.locator(getByTitleSelector(text, options));
    }
    getByRole(role, options = {}) {
        return this.locator(getByRoleSelector(role, options));
    }
    frameLocator(selector) {
        return new FrameLocator(this._frame, this._selector + ' >> ' + selector);
    }
    filter(options) {
        return new Locator(this._frame, this._selector, options);
    }
    async elementHandle(options) {
        return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options });
    }
    async elementHandles() {
        return await this._frame.$$(this._selector);
    }
    contentFrame() {
        return new FrameLocator(this._frame, this._selector);
    }
    describe(description) {
        return new Locator(this._frame, this._selector + ' >> internal:describe=' + JSON.stringify(description));
    }
    description() {
        return locatorCustomDescription(this._selector) || null;
    }
    first() {
        return new Locator(this._frame, this._selector + ' >> nth=0');
    }
    last() {
        return new Locator(this._frame, this._selector + ` >> nth=-1`);
    }
    nth(index) {
        return new Locator(this._frame, this._selector + ` >> nth=${index}`);
    }
    and(locator) {
        if (locator._frame !== this._frame)
            throw new Error(`Locators must belong to the same frame.`);
        return new Locator(this._frame, this._selector + ` >> internal:and=` + JSON.stringify(locator._selector));
    }
    or(locator) {
        if (locator._frame !== this._frame)
            throw new Error(`Locators must belong to the same frame.`);
        return new Locator(this._frame, this._selector + ` >> internal:or=` + JSON.stringify(locator._selector));
    }
    async focus(options) {
        return await this._frame.focus(this._selector, { strict: true, ...options });
    }
    async blur(options) {
        await this._frame._channel.blur({ selector: this._selector, strict: true, ...options, timeout: this._frame._timeout(options) });
    }
    // options are only here for testing
    async count(_options) {
        return await this._frame._queryCount(this._selector, _options);
    }
    async normalize() {
        const { resolvedSelector } = await this._frame._channel.resolveSelector({ selector: this._selector });
        return new Locator(this._frame, resolvedSelector);
    }
    async getAttribute(name, options) {
        return await this._frame.getAttribute(this._selector, name, { strict: true, ...options });
    }
    async hover(options = {}) {
        return await this._frame.hover(this._selector, { strict: true, ...options });
    }
    async innerHTML(options) {
        return await this._frame.innerHTML(this._selector, { strict: true, ...options });
    }
    async innerText(options) {
        return await this._frame.innerText(this._selector, { strict: true, ...options });
    }
    async inputValue(options) {
        return await this._frame.inputValue(this._selector, { strict: true, ...options });
    }
    async isChecked(options) {
        return await this._frame.isChecked(this._selector, { strict: true, ...options });
    }
    async isDisabled(options) {
        return await this._frame.isDisabled(this._selector, { strict: true, ...options });
    }
    async isEditable(options) {
        return await this._frame.isEditable(this._selector, { strict: true, ...options });
    }
    async isEnabled(options) {
        return await this._frame.isEnabled(this._selector, { strict: true, ...options });
    }
    async isHidden(options) {
        return await this._frame.isHidden(this._selector, { strict: true, ...options });
    }
    async isVisible(options) {
        return await this._frame.isVisible(this._selector, { strict: true, ...options });
    }
    async press(key, options = {}) {
        return await this._frame.press(this._selector, key, { strict: true, ...options });
    }
    async screenshot(options = {}) {
        const mask = options.mask;
        return await this._withElement((h, timeout) => h.screenshot({ ...options, mask, timeout }), { title: 'Screenshot', timeout: options.timeout });
    }
    async ariaRef(options = {}) {
        const { ref } = await this._frame._channel.ariaRef({ selector: this._selector, timeout: this._frame._timeout(options) });
        return ref ?? null;
    }
    async ariaSnapshot(options = {}) {
        const result = await this._frame._channel.ariaSnapshot({ timeout: this._frame._timeout(options), mode: options.mode, selector: this._selector, depth: options.depth, boxes: options.boxes });
        return result.snapshot;
    }
    async scrollIntoViewIfNeeded(options = {}) {
        return await this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({ ...options, timeout }), { title: 'Scroll into view', timeout: options.timeout });
    }
    async selectOption(values, options = {}) {
        return await this._frame.selectOption(this._selector, values, { strict: true, ...options });
    }
    async selectText(options = {}) {
        return await this._withElement((h, timeout) => h.selectText({ ...options, timeout }), { title: 'Select text', timeout: options.timeout });
    }
    async setChecked(checked, options) {
        if (checked)
            await this.check(options);
        else
            await this.uncheck(options);
    }
    async setInputFiles(files, options = {}) {
        return await this._frame.setInputFiles(this._selector, files, { strict: true, ...options });
    }
    async tap(options = {}) {
        return await this._frame.tap(this._selector, { strict: true, ...options });
    }
    async textContent(options) {
        return await this._frame.textContent(this._selector, { strict: true, ...options });
    }
    async type(text, options = {}) {
        return await this._frame.type(this._selector, text, { strict: true, ...options });
    }
    async pressSequentially(text, options = {}) {
        return await this.type(text, options);
    }
    async uncheck(options = {}) {
        return await this._frame.uncheck(this._selector, { strict: true, ...options });
    }
    async all() {
        return new Array(await this.count()).fill(0).map((e, i) => this.nth(i));
    }
    async allInnerTexts() {
        return await this._frame.$$eval(this._selector, ee => ee.map(e => e.innerText));
    }
    async allTextContents() {
        return await this._frame.$$eval(this._selector, ee => ee.map(e => e.textContent || ''));
    }
    async waitFor(options) {
        await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options, timeout: this._frame._timeout(options) });
    }
    async _expect(expression, options) {
        return this._frame._expect(expression, {
            ...options,
            selector: this._selector,
        });
    }
    _inspect() {
        return this.toString();
    }
    toString() {
        return asLocatorDescription('javascript', this._selector);
    }
}
export class FrameLocator {
    _frame;
    _frameSelector;
    constructor(frame, selector) {
        this._frame = frame;
        this._frameSelector = selector;
    }
    locator(selectorOrLocator, options) {
        if (isString(selectorOrLocator))
            return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator, options);
        if (selectorOrLocator._frame !== this._frame)
            throw new Error(`Locators must belong to the same frame.`);
        return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator._selector, options);
    }
    getByTestId(testId) {
        return this.locator(getByTestIdSelector(testIdAttributeName(), testId));
    }
    getByAltText(text, options) {
        return this.locator(getByAltTextSelector(text, options));
    }
    getByLabel(text, options) {
        return this.locator(getByLabelSelector(text, options));
    }
    getByPlaceholder(text, options) {
        return this.locator(getByPlaceholderSelector(text, options));
    }
    getByText(text, options) {
        return this.locator(getByTextSelector(text, options));
    }
    getByTitle(text, options) {
        return this.locator(getByTitleSelector(text, options));
    }
    getByRole(role, options = {}) {
        return this.locator(getByRoleSelector(role, options));
    }
    owner() {
        return new Locator(this._frame, this._frameSelector);
    }
    frameLocator(selector) {
        return new FrameLocator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector);
    }
    first() {
        return new FrameLocator(this._frame, this._frameSelector + ' >> nth=0');
    }
    last() {
        return new FrameLocator(this._frame, this._frameSelector + ` >> nth=-1`);
    }
    nth(index) {
        return new FrameLocator(this._frame, this._frameSelector + ` >> nth=${index}`);
    }
}
let _testIdAttributeName = 'data-testid';
export function testIdAttributeName() {
    return _testIdAttributeName;
}
export function setTestIdAttribute(attributeName) {
    _testIdAttributeName = attributeName;
}
function cssObjectToString(style) {
    return Object.entries(style).map(([key, value]) => {
        const property = key.startsWith('--') ? key : key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
        return `${property}: ${value}`;
    }).join('; ');
}
//# sourceMappingURL=locator.js.map