"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Target = void 0;
const events_1 = require("events");
const exception_js_1 = require("../exception.js");
const index_js_1 = require("./inject/index.js");
const interceptor_js_1 = require("./interceptor.js");
const page_js_1 = require("./page.js");
class Target extends events_1.EventEmitter {
    constructor(browser, sessionId, targetInfo) {
        super();
        this.loaded = false;
        this.ready = false;
        this.loadingFailed = false;
        this.requestId = null;
        this.errorText = null;
        this.response = null;
        this.attachedPage = null;
        Object.defineProperties(this, {
            browser: {
                get: () => browser,
                enumerable: false,
            },
        });
        this.sessionId = sessionId;
        this.targetId = targetInfo.targetId;
        this.type = targetInfo.type;
        this.url = targetInfo.url;
        this.title = targetInfo.title;
        this.browserContextId = targetInfo.browserContextId || null;
        this.openerId = targetInfo.openerId || null;
        this.on('Inspector.targetCrashed', () => this.onTargetCrashed());
        this.on('Page.lifecycleEvent', ev => this.onLifecycleEvent(ev));
        this.on('Page.frameNavigated', ev => this.onFrameNavigated(ev));
        this.on('Page.domContentEventFired', () => this.onDomReady());
        this.on('Page.frameStoppedLoading', ev => this.onFrameStoppedLoading(ev));
        this.on('Network.requestWillBeSent', ev => this.onRequestWillBeSent(ev));
        this.on('Network.responseReceived', ev => this.onResponseReceived(ev));
        this.on('Network.loadingFailed', ev => this.onLoadingFailed(ev));
        this.on('Fetch.requestPaused', ev => this.onRequestPaused(ev));
        this.initPromise = this._init();
    }
    get logger() {
        return this.browser.logger;
    }
    async getPage() {
        await this.init();
        if (!this.attachedPage) {
            throw new exception_js_1.Exception({
                name: 'TargetDestroyed',
                message: 'Cannot obtain page because the target is destroyed'
            });
        }
        return this.attachedPage;
    }
    init() {
        return this.initPromise;
    }
    async _init() {
        try {
            if (this.openerId) {
                await this.send('Runtime.runIfWaitingForDebugger');
            }
            await this.send('Fetch.enable');
            await this.send('Page.enable');
            await this.send('Page.setLifecycleEventsEnabled', { enabled: true });
            this.injectScripts();
            const { frameTree } = await this.send('Page.getFrameTree');
            this.attachedPage = new page_js_1.Page(this, frameTree);
            await this.send('Network.enable', {
                maxTotalBufferSize: 100 * 1024 * 1204,
                maxResourceBufferSize: 50 * 1024 * 1204,
            });
            for (const fn of this.browser.targetInitFns) {
                await fn(this);
            }
            await this.send('Runtime.runIfWaitingForDebugger');
            this.browser.emit('pageCreated', this.attachedPage);
        }
        catch (error) {
            if (['CdpDisconnected', 'CdpTargetDetached'].includes(error.name)) {
                return;
            }
            this.browser.logger.error('Target init failed', {
                error,
                ...this.collectInfo(),
            });
        }
    }
    async activate() {
        const { targetId } = this;
        await this.browser.send('Target.activateTarget', { targetId });
    }
    close() {
        const { targetId } = this;
        this.browser.sendAndForget('Target.closeTarget', { targetId });
    }
    async send(method, params, timeout) {
        const { sessionId } = this;
        return await this.browser.connection.send({
            method,
            params,
            sessionId,
            timeout,
        });
    }
    sendAndForget(method, params) {
        const { sessionId } = this;
        this.browser.connection.sendAndForget({
            method,
            params,
            sessionId,
        });
    }
    updateInfo(targetInfo) {
        this.type = targetInfo.type;
        this.url = targetInfo.url;
        this.title = targetInfo.title;
    }
    onInfoChanged(targetInfo) {
        this.updateInfo(targetInfo);
    }
    async refreshInfo() {
        const { targetInfo } = await this.send('Target.getTargetInfo');
        this.updateInfo(targetInfo);
    }
    isPageTarget() {
        return ['iframe', 'page'].includes(this.type);
    }
    isValidTarget() {
        if (!this.isPageTarget()) {
            return false;
        }
        if (this.type === 'page') {
            return !!this.url && this.url !== 'about:blank';
        }
        return true;
    }
    collectInfo() {
        return {
            targetId: this.targetId,
            url: this.url,
            type: this.type,
        };
    }
    async navigate(url, options = {}) {
        return new Promise((resolve, reject) => {
            this.send('Page.navigate', { url }).catch(reject);
            this.ready = false;
            this.loaded = false;
            this.loadingFailed = false;
            this.waitForReady(options).then(resolve, reject);
        });
    }
    async waitForReady(options = {}) {
        await this.waitForEvents({
            events: ['ready', 'loaded'],
            ...options,
        });
    }
    async waitForLoad(options = {}) {
        await this.waitForEvents({
            events: ['loaded'],
            ...options,
        });
    }
    async waitForEvents(options) {
        const { events, rejectHttpErrors = true, rejectNetworkErrors = true, rejectTimeout = true, timeout = this.browser.config.navigationTimeout, } = options;
        const eventsHappened = events.some(ev => this[ev] === true);
        if (eventsHappened) {
            return;
        }
        await new Promise((resolve, reject) => {
            const cleanup = () => {
                clearTimeout(timer);
                for (const ev of events) {
                    this.removeListener(ev, onResolve);
                }
            };
            const onTimeout = () => {
                cleanup();
                if (rejectTimeout) {
                    const err = new exception_js_1.Exception({
                        name: 'NavigationTimeout',
                        message: `Page did not resolve to one of following states: ${events.join(', ')}`,
                        retry: false,
                        details: this.collectInfo(),
                    });
                    reject(err);
                }
                else {
                    resolve();
                }
            };
            const onResolve = () => {
                cleanup();
                const status = this.response?.status;
                if (rejectHttpErrors && status >= 400) {
                    const err = new exception_js_1.Exception({
                        name: 'NavigationFailed',
                        message: `Page navigation failed with HTTP status ${status}`,
                        retry: false,
                        details: this.collectInfo(),
                    });
                    return reject(err);
                }
                if (rejectNetworkErrors && this.loadingFailed) {
                    const err = new exception_js_1.Exception({
                        name: 'NavigationFailed',
                        message: `Page failed to load: ${this.errorText || '<reason unknown>'}`,
                        retry: false,
                        details: this.collectInfo(),
                    });
                    return reject(err);
                }
                resolve();
            };
            const timer = timeout ? setTimeout(onTimeout, timeout) : null;
            for (const ev of events) {
                this.addListener(ev, onResolve);
            }
        });
    }
    onRequestWillBeSent(event) {
        const { type, frameId, requestId, request } = event;
        if (type !== 'Document' || this.targetId !== frameId) {
            return;
        }
        this.requestId = requestId;
        this.loadingFailed = false;
        this.errorText = null;
        this.response = null;
        this.postData = request.postData;
    }
    onResponseReceived(event) {
        const { type, response, requestId } = event;
        if (type !== 'Document' || this.requestId !== requestId) {
            return;
        }
        this.response = response;
    }
    onLoadingFailed(event) {
        const { requestId, type, errorText } = event;
        if (type !== 'Document' || this.requestId !== requestId) {
            return;
        }
        this.loadingFailed = true;
        this.errorText = errorText;
    }
    onTargetCrashed() {
        const { sessionId } = this;
        const err = new exception_js_1.Exception({
            name: 'CdpTargetCrashed',
            message: 'CDP: Target crashed unexpectedly',
        });
        this.browser.connection.rejectAllForTarget(sessionId, () => err);
        this.emit('crash');
    }
    onLifecycleEvent(ev) {
        if (ev.frameId !== this.targetId) {
            return;
        }
        switch (ev.name) {
            case 'init':
                this.ready = false;
                this.loaded = false;
                break;
            case 'DOMContentLoaded':
                this.ready = true;
                this.emit('ready');
                break;
            case 'load':
                this.ready = true;
                this.emit('ready');
                this.loaded = true;
                this.emit('loaded');
                break;
        }
    }
    onFrameNavigated(ev) {
        if (ev.frame.id !== this.targetId) {
            return;
        }
        this.loaded = false;
        this.ready = false;
        this.loadingFailed = !!ev.frame.unreachableUrl;
        this.url = ev.frame.unreachableUrl || ev.frame.url;
        if (['iframe', 'page'].includes(this.type)) {
            this.logger.debug(`Navigate (${this.type}) ${this.url}`, {
                ...this.collectInfo(),
            });
        }
    }
    onFrameStoppedLoading(ev) {
        if (ev.frameId !== this.targetId) {
            return;
        }
        this.ready = true;
        this.loaded = true;
        this.emit('ready');
        this.emit('loaded');
    }
    onDomReady() {
        this.ready = true;
        this.emit('ready');
    }
    async onRequestPaused(ev) {
        const ireq = new interceptor_js_1.InterceptedRequest(this, ev);
        for (const interceptor of this.browser.interceptors) {
            try {
                const outcome = await interceptor.handler(ireq);
                if (outcome && outcome.method !== 'pass') {
                    await this.send(outcome.method, outcome.params);
                    return;
                }
            }
            catch (error) {
                this.logger.warn('Request interception failed', {
                    error,
                    ...this.collectInfo(),
                });
            }
        }
        const continuePayload = ireq.continue();
        this.sendAndForget(continuePayload.method, continuePayload.params);
    }
    injectScripts() {
        index_js_1.stubScripts.forEach(({ fn }) => {
            this.sendAndForget('Page.addScriptToEvaluateOnNewDocument', {
                source: `(${fn.toString()})()`
            });
        });
    }
}
exports.Target = Target;
//# sourceMappingURL=target.js.map