"use strict";
/**
 * 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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.snapshotScript = exports.frameSnapshotStreamer = exports.kSnapshotBinding = exports.kSnapshotStreamer = void 0;
exports.kSnapshotStreamer = '__playwright_snapshot_streamer_';
exports.kSnapshotBinding = '__playwright_snapshot_binding_';
function frameSnapshotStreamer() {
    // Communication with Playwright.
    const kSnapshotStreamer = '__playwright_snapshot_streamer_';
    const kSnapshotBinding = '__playwright_snapshot_binding_';
    // Attributes present in the snapshot.
    const kShadowAttribute = '__playwright_shadow_root_';
    const kScrollTopAttribute = '__playwright_scroll_top_';
    const kScrollLeftAttribute = '__playwright_scroll_left_';
    // Symbols for our own info on Nodes.
    const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
    const kCachedData = Symbol('__playwright_snapshot_cache_');
    function ensureCachedData(node) {
        if (!node[kCachedData])
            node[kCachedData] = {};
        return node[kCachedData];
    }
    const escaped = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' };
    function escapeAttribute(s) {
        return s.replace(/[&<>"']/ug, char => escaped[char]);
    }
    function escapeText(s) {
        return s.replace(/[&<]/ug, char => escaped[char]);
    }
    class Streamer {
        constructor() {
            this._removeNoScript = true;
            this._needStyleOverrides = false;
            this._lastSnapshotNumber = 0;
            // TODO: should we also intercept setters like CSSRule.cssText and CSSStyleRule.selectorText?
            this._interceptNative(window.CSSStyleSheet.prototype, 'insertRule', () => this._needStyleOverrides = true);
            this._interceptNative(window.CSSStyleSheet.prototype, 'deleteRule', () => this._needStyleOverrides = true);
            this._interceptNative(window.CSSStyleSheet.prototype, 'addRule', () => this._needStyleOverrides = true);
            this._interceptNative(window.CSSStyleSheet.prototype, 'removeRule', () => this._needStyleOverrides = true);
            this._observer = new MutationObserver(list => this._handleMutations(list));
            const observerConfig = { attributes: true, childList: true, subtree: true, characterData: true };
            this._observer.observe(document, observerConfig);
            this._interceptNative(window.Element.prototype, 'attachShadow', (node, shadowRoot) => {
                this._invalidateCache(node);
                this._observer.observe(shadowRoot, observerConfig);
            });
            this._streamSnapshot();
        }
        _interceptNative(obj, method, cb) {
            const native = obj[method];
            if (!native)
                return;
            obj[method] = function (...args) {
                const result = native.call(this, ...args);
                cb(this, result);
                return result;
            };
        }
        _invalidateCache(node) {
            while (node) {
                ensureCachedData(node).ref = undefined;
                if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && node.host)
                    node = node.host;
                else
                    node = node.parentNode;
            }
        }
        _handleMutations(list) {
            for (const mutation of list)
                this._invalidateCache(mutation.target);
        }
        markIframe(iframeElement, frameId) {
            iframeElement[kSnapshotFrameId] = frameId;
        }
        forceSnapshot(snapshotId) {
            this._streamSnapshot(snapshotId);
        }
        _streamSnapshot(snapshotId) {
            if (this._timer) {
                clearTimeout(this._timer);
                this._timer = undefined;
            }
            try {
                const snapshot = this._captureSnapshot(snapshotId);
                window[kSnapshotBinding](snapshot).catch((e) => { });
            }
            catch (e) {
            }
            this._timer = setTimeout(() => this._streamSnapshot(), 100);
        }
        _sanitizeUrl(url) {
            if (url.startsWith('javascript:'))
                return '';
            return url;
        }
        _sanitizeSrcSet(srcset) {
            return srcset.split(',').map(src => {
                src = src.trim();
                const spaceIndex = src.lastIndexOf(' ');
                if (spaceIndex === -1)
                    return this._sanitizeUrl(src);
                return this._sanitizeUrl(src.substring(0, spaceIndex).trim()) + src.substring(spaceIndex);
            }).join(',');
        }
        _resolveUrl(base, url) {
            if (url === '')
                return '';
            try {
                return new URL(url, base).href;
            }
            catch (e) {
                return url;
            }
        }
        _getSheetBase(sheet) {
            let rootSheet = sheet;
            while (rootSheet.parentStyleSheet)
                rootSheet = rootSheet.parentStyleSheet;
            if (rootSheet.ownerNode)
                return rootSheet.ownerNode.baseURI;
            return document.baseURI;
        }
        _getSheetText(sheet) {
            const rules = [];
            for (const rule of sheet.cssRules)
                rules.push(rule.cssText);
            return rules.join('\n');
        }
        _captureSnapshot(snapshotId) {
            const snapshotNumber = ++this._lastSnapshotNumber;
            const win = window;
            const doc = win.document;
            // Ensure we are up-to-date.
            this._handleMutations(this._observer.takeRecords());
            for (const input of doc.querySelectorAll('input, textarea')) {
                const value = input.value;
                const data = ensureCachedData(input);
                if (data.value !== value)
                    this._invalidateCache(input);
            }
            const styleNodeToStyleSheetText = new Map();
            const styleSheetUrlToContentOverride = new Map();
            const visitStyleSheet = (sheet) => {
                // TODO: recalculate these upon changes, and only send them once.
                if (!this._needStyleOverrides)
                    return;
                try {
                    for (const rule of sheet.cssRules) {
                        if (rule.styleSheet)
                            visitStyleSheet(rule.styleSheet);
                    }
                    const cssText = this._getSheetText(sheet);
                    if (sheet.ownerNode && sheet.ownerNode.nodeName === 'STYLE') {
                        // Stylesheets with owner STYLE nodes will be rewritten.
                        styleNodeToStyleSheetText.set(sheet.ownerNode, cssText);
                    }
                    else if (sheet.href !== null) {
                        // Other stylesheets will have resource overrides.
                        const base = this._getSheetBase(sheet);
                        const url = this._resolveUrl(base, sheet.href);
                        styleSheetUrlToContentOverride.set(url, cssText);
                    }
                }
                catch (e) {
                    // Sometimes we cannot access cross-origin stylesheets.
                }
            };
            let nodeCounter = 0;
            const visit = (node) => {
                const nodeType = node.nodeType;
                const nodeName = nodeType === Node.DOCUMENT_FRAGMENT_NODE ? 'template' : node.nodeName;
                if (nodeType !== Node.ELEMENT_NODE &&
                    nodeType !== Node.DOCUMENT_FRAGMENT_NODE &&
                    nodeType !== Node.TEXT_NODE)
                    return;
                if (nodeName === 'SCRIPT' || nodeName === 'BASE')
                    return;
                if (this._removeNoScript && nodeName === 'NOSCRIPT')
                    return;
                const data = ensureCachedData(node);
                if (data.ref)
                    return [[snapshotNumber - data.ref[0], data.ref[1]]];
                nodeCounter++;
                data.ref = [snapshotNumber, nodeCounter - 1];
                // ---------- No returns without the data after this point -----------
                // ---------- Otherwise nodeCounter is wrong               -----------
                if (nodeType === Node.TEXT_NODE)
                    return escapeText(node.nodeValue || '');
                if (nodeName === 'STYLE') {
                    const cssText = styleNodeToStyleSheetText.get(node) || node.textContent || '';
                    return ['style', {}, escapeText(cssText)];
                }
                const attrs = {};
                const result = [nodeName, attrs];
                if (nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
                    for (const sheet of node.styleSheets)
                        visitStyleSheet(sheet);
                    attrs[kShadowAttribute] = 'open';
                }
                if (nodeType === Node.ELEMENT_NODE) {
                    const element = node;
                    // if (node === target)
                    //   attrs[' __playwright_target__] = '';
                    for (let i = 0; i < element.attributes.length; i++) {
                        const name = element.attributes[i].name;
                        let value = element.attributes[i].value;
                        if (name === 'value' && (nodeName === 'INPUT' || nodeName === 'TEXTAREA'))
                            continue;
                        if (name === 'checked' || name === 'disabled' || name === 'checked')
                            continue;
                        if (nodeName === 'LINK' && name === 'integrity')
                            continue;
                        if (name === 'src' && (nodeName === 'IFRAME' || nodeName === 'FRAME')) {
                            // TODO: handle srcdoc?
                            const frameId = element[kSnapshotFrameId];
                            value = frameId || 'data:text/html,<body>Snapshot is not available</body>';
                        }
                        else if (name === 'src' && (nodeName === 'IMG')) {
                            value = this._sanitizeUrl(value);
                        }
                        else if (name === 'srcset' && (nodeName === 'IMG')) {
                            value = this._sanitizeSrcSet(value);
                        }
                        else if (name === 'srcset' && (nodeName === 'SOURCE')) {
                            value = this._sanitizeSrcSet(value);
                        }
                        else if (name === 'href' && (nodeName === 'LINK')) {
                            value = this._sanitizeUrl(value);
                        }
                        else if (name.startsWith('on')) {
                            value = '';
                        }
                        attrs[name] = escapeAttribute(value);
                    }
                    if (nodeName === 'INPUT') {
                        const value = element.value;
                        data.value = value;
                        attrs['value'] = escapeAttribute(value);
                    }
                    if (element.checked)
                        attrs['checked'] = '';
                    if (element.disabled)
                        attrs['disabled'] = '';
                    if (element.readOnly)
                        attrs['readonly'] = '';
                    if (element.scrollTop)
                        attrs[kScrollTopAttribute] = '' + element.scrollTop;
                    if (element.scrollLeft)
                        attrs[kScrollLeftAttribute] = '' + element.scrollLeft;
                    if (element.shadowRoot) {
                        const child = visit(element.shadowRoot);
                        if (child)
                            result.push(child);
                    }
                }
                if (nodeName === 'HEAD') {
                    const base = ['base', { 'href': document.baseURI }];
                    for (let child = node.firstChild; child; child = child.nextSibling) {
                        if (child.nodeName === 'BASE') {
                            base[1]['href'] = escapeAttribute(child.href);
                            base[1]['target'] = escapeAttribute(child.target);
                        }
                    }
                    nodeCounter++; // Compensate for the extra 'base' node in the list.
                    result.push(base);
                }
                if (nodeName === 'TEXTAREA') {
                    nodeCounter++; // Compensate for the extra text node in the list.
                    const value = node.value;
                    data.value = value;
                    result.push(escapeText(value));
                }
                else {
                    for (let child = node.firstChild; child; child = child.nextSibling) {
                        const snapshotted = visit(child);
                        if (snapshotted)
                            result.push(snapshotted);
                    }
                }
                if (result.length === 2 && !Object.keys(attrs).length)
                    result.pop(); // Remove empty attrs when there are no children.
                return result;
            };
            for (const sheet of doc.styleSheets)
                visitStyleSheet(sheet);
            const html = doc.documentElement ? visit(doc.documentElement) : ['html', {}];
            return {
                html,
                doctype: doc.doctype ? doc.doctype.name : undefined,
                resourceOverrides: Array.from(styleSheetUrlToContentOverride).map(([url, content]) => ({ url, content })),
                viewport: {
                    width: Math.max(doc.body ? doc.body.offsetWidth : 0, doc.documentElement ? doc.documentElement.offsetWidth : 0),
                    height: Math.max(doc.body ? doc.body.offsetHeight : 0, doc.documentElement ? doc.documentElement.offsetHeight : 0),
                },
                url: location.href,
                snapshotId,
            };
        }
    }
    window[kSnapshotStreamer] = new Streamer();
}
exports.frameSnapshotStreamer = frameSnapshotStreamer;
function snapshotScript() {
    function applyPlaywrightAttributes(shadowAttribute, scrollTopAttribute, scrollLeftAttribute) {
        const scrollTops = [];
        const scrollLefts = [];
        const visit = (root) => {
            // Collect all scrolled elements for later use.
            for (const e of root.querySelectorAll(`[${scrollTopAttribute}]`))
                scrollTops.push(e);
            for (const e of root.querySelectorAll(`[${scrollLeftAttribute}]`))
                scrollLefts.push(e);
            for (const iframe of root.querySelectorAll('iframe')) {
                const src = iframe.getAttribute('src') || '';
                if (src.startsWith('data:text/html'))
                    continue;
                // Rewrite iframes to use snapshot url (relative to window.location)
                // instead of begin relative to the <base> tag.
                const index = location.pathname.lastIndexOf('/');
                if (index === -1)
                    continue;
                const pathname = location.pathname.substring(0, index + 1) + src;
                const href = location.href.substring(0, location.href.indexOf(location.pathname)) + pathname;
                iframe.setAttribute('src', href);
            }
            for (const element of root.querySelectorAll(`template[${shadowAttribute}]`)) {
                const template = element;
                const shadowRoot = template.parentElement.attachShadow({ mode: 'open' });
                shadowRoot.appendChild(template.content);
                template.remove();
                visit(shadowRoot);
            }
        };
        visit(document);
        const onLoad = () => {
            window.removeEventListener('load', onLoad);
            for (const element of scrollTops) {
                element.scrollTop = +element.getAttribute(scrollTopAttribute);
                element.removeAttribute(scrollTopAttribute);
            }
            for (const element of scrollLefts) {
                element.scrollLeft = +element.getAttribute(scrollLeftAttribute);
                element.removeAttribute(scrollLeftAttribute);
            }
        };
        window.addEventListener('load', onLoad);
    }
    const kShadowAttribute = '__playwright_shadow_root_';
    const kScrollTopAttribute = '__playwright_scroll_top_';
    const kScrollLeftAttribute = '__playwright_scroll_left_';
    return `\n(${applyPlaywrightAttributes.toString()})('${kShadowAttribute}', '${kScrollTopAttribute}', '${kScrollLeftAttribute}')`;
}
exports.snapshotScript = snapshotScript;
//# sourceMappingURL=snapshotterInjected.js.map