2020-08-28 19:51:55 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-01-29 15:57:57 +01:00
|
|
|
export type NodeSnapshot =
|
|
|
|
|
// Text node.
|
|
|
|
|
string |
|
|
|
|
|
// Subtree reference, "x snapshots ago, node #y". Could point to a text node.
|
|
|
|
|
// Only nodes that are not references are counted, starting from zero.
|
|
|
|
|
[ [number, number] ] |
|
|
|
|
|
// Just node name.
|
|
|
|
|
[ string ] |
|
|
|
|
|
// Node name, attributes, child nodes.
|
|
|
|
|
// Unfortunately, we cannot make this type definition recursive, therefore "any".
|
|
|
|
|
[ string, { [attr: string]: string }, ...any ];
|
|
|
|
|
|
2020-08-28 19:51:55 +02:00
|
|
|
export type SnapshotData = {
|
2021-01-29 15:57:57 +01:00
|
|
|
doctype?: string,
|
|
|
|
|
html: NodeSnapshot,
|
2021-01-30 00:24:38 +01:00
|
|
|
resourceOverrides: {
|
|
|
|
|
url: string,
|
|
|
|
|
// String is the content. Number is "x snapshots ago", same url.
|
|
|
|
|
content: string | number,
|
|
|
|
|
}[],
|
2021-01-26 03:44:46 +01:00
|
|
|
viewport: { width: number, height: number },
|
|
|
|
|
url: string,
|
|
|
|
|
snapshotId?: string,
|
2020-08-28 19:51:55 +02:00
|
|
|
};
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
export const kSnapshotStreamer = '__playwright_snapshot_streamer_';
|
|
|
|
|
export const kSnapshotBinding = '__playwright_snapshot_binding_';
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
export function frameSnapshotStreamer() {
|
2021-01-29 15:57:57 +01:00
|
|
|
// Communication with Playwright.
|
2021-01-26 03:44:46 +01:00
|
|
|
const kSnapshotStreamer = '__playwright_snapshot_streamer_';
|
|
|
|
|
const kSnapshotBinding = '__playwright_snapshot_binding_';
|
2021-01-29 15:57:57 +01:00
|
|
|
|
|
|
|
|
// Attributes present in the snapshot.
|
2021-01-26 03:44:46 +01:00
|
|
|
const kShadowAttribute = '__playwright_shadow_root_';
|
2021-01-27 00:09:17 +01:00
|
|
|
const kScrollTopAttribute = '__playwright_scroll_top_';
|
|
|
|
|
const kScrollLeftAttribute = '__playwright_scroll_left_';
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2021-01-30 00:24:38 +01:00
|
|
|
// Symbols for our own info on Nodes/StyleSheets.
|
2021-01-29 15:57:57 +01:00
|
|
|
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
|
|
|
|
|
const kCachedData = Symbol('__playwright_snapshot_cache_');
|
|
|
|
|
type CachedData = {
|
|
|
|
|
ref?: [number, number], // Previous snapshotNumber and nodeIndex.
|
|
|
|
|
value?: string, // Value for input/textarea elements.
|
2021-01-30 00:24:38 +01:00
|
|
|
cssText?: string, // Text for stylesheets.
|
|
|
|
|
cssRef?: number, // Previous snapshotNumber for overridden stylesheets.
|
2021-01-29 15:57:57 +01:00
|
|
|
};
|
2021-01-30 00:24:38 +01:00
|
|
|
function ensureCachedData(obj: any): CachedData {
|
|
|
|
|
if (!obj[kCachedData])
|
|
|
|
|
obj[kCachedData] = {};
|
|
|
|
|
return obj[kCachedData];
|
2021-01-29 15:57:57 +01:00
|
|
|
}
|
|
|
|
|
|
2020-08-28 19:51:55 +02:00
|
|
|
const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
|
2021-01-29 15:57:57 +01:00
|
|
|
function escapeAttribute(s: string): string {
|
|
|
|
|
return s.replace(/[&<>"']/ug, char => (escaped as any)[char]);
|
|
|
|
|
}
|
|
|
|
|
function escapeText(s: string): string {
|
|
|
|
|
return s.replace(/[&<]/ug, char => (escaped as any)[char]);
|
|
|
|
|
}
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2021-01-30 00:24:38 +01:00
|
|
|
function removeHash(url: string) {
|
|
|
|
|
try {
|
|
|
|
|
const u = new URL(url);
|
|
|
|
|
u.hash = '';
|
|
|
|
|
return u.toString();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return url;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
class Streamer {
|
|
|
|
|
private _removeNoScript = true;
|
|
|
|
|
private _timer: NodeJS.Timeout | undefined;
|
2021-01-29 15:57:57 +01:00
|
|
|
private _lastSnapshotNumber = 0;
|
|
|
|
|
private _observer: MutationObserver;
|
2021-01-30 00:24:38 +01:00
|
|
|
private _staleStyleSheets = new Set<CSSStyleSheet>();
|
|
|
|
|
private _allStyleSheetsWithUrlOverride = new Set<CSSStyleSheet>();
|
|
|
|
|
private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
constructor() {
|
2021-01-30 00:24:38 +01:00
|
|
|
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
|
|
|
|
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'deleteRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
|
|
|
|
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'addRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
|
|
|
|
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'removeRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
|
|
|
|
this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'rules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
|
|
|
|
this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'cssRules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
2021-01-29 15:57:57 +01:00
|
|
|
|
|
|
|
|
this._observer = new MutationObserver(list => this._handleMutations(list));
|
|
|
|
|
const observerConfig = { attributes: true, childList: true, subtree: true, characterData: true };
|
|
|
|
|
this._observer.observe(document, observerConfig);
|
2021-01-30 00:24:38 +01:00
|
|
|
this._interceptNativeMethod(window.Element.prototype, 'attachShadow', (node: Node, shadowRoot: ShadowRoot) => {
|
|
|
|
|
this._invalidateNode(node);
|
2021-01-29 15:57:57 +01:00
|
|
|
this._observer.observe(shadowRoot, observerConfig);
|
|
|
|
|
});
|
|
|
|
|
|
2021-01-27 00:09:17 +01:00
|
|
|
this._streamSnapshot();
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-01-30 00:24:38 +01:00
|
|
|
private _interceptNativeMethod(obj: any, method: string, cb: (thisObj: any, result: any) => void) {
|
2021-01-26 03:44:46 +01:00
|
|
|
const native = obj[method] as Function;
|
|
|
|
|
if (!native)
|
|
|
|
|
return;
|
|
|
|
|
obj[method] = function(...args: any[]) {
|
2021-01-29 15:57:57 +01:00
|
|
|
const result = native.call(this, ...args);
|
|
|
|
|
cb(this, result);
|
|
|
|
|
return result;
|
2021-01-26 03:44:46 +01:00
|
|
|
};
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-01-30 00:24:38 +01:00
|
|
|
private _interceptNativeGetter(obj: any, prop: string, cb: (thisObj: any, result: any) => void) {
|
|
|
|
|
const descriptor = Object.getOwnPropertyDescriptor(obj, prop)!;
|
|
|
|
|
Object.defineProperty(obj, prop, {
|
|
|
|
|
...descriptor,
|
|
|
|
|
get: function() {
|
|
|
|
|
const result = descriptor.get!.call(this);
|
|
|
|
|
cb(this, result);
|
|
|
|
|
return result;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _invalidateStyleSheet(sheet: CSSStyleSheet) {
|
|
|
|
|
if (this._readingStyleSheet)
|
|
|
|
|
return;
|
|
|
|
|
this._staleStyleSheets.add(sheet);
|
|
|
|
|
if (sheet.href !== null)
|
|
|
|
|
this._allStyleSheetsWithUrlOverride.add(sheet);
|
|
|
|
|
if (sheet.ownerNode && sheet.ownerNode.nodeName === 'STYLE')
|
|
|
|
|
this._invalidateNode(sheet.ownerNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet): string | undefined {
|
|
|
|
|
const data = ensureCachedData(sheet);
|
|
|
|
|
if (this._staleStyleSheets.has(sheet)) {
|
|
|
|
|
this._staleStyleSheets.delete(sheet);
|
|
|
|
|
try {
|
|
|
|
|
data.cssText = this._getSheetText(sheet);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Sometimes we cannot access cross-origin stylesheets.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return data.cssText;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns either content, ref, or no override.
|
|
|
|
|
private _updateLinkStyleSheetTextIfNeeded(sheet: CSSStyleSheet, snapshotNumber: number): string | number | undefined {
|
|
|
|
|
const data = ensureCachedData(sheet);
|
|
|
|
|
if (this._staleStyleSheets.has(sheet)) {
|
|
|
|
|
this._staleStyleSheets.delete(sheet);
|
|
|
|
|
try {
|
|
|
|
|
data.cssText = this._getSheetText(sheet);
|
|
|
|
|
data.cssRef = snapshotNumber;
|
|
|
|
|
return data.cssText;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Sometimes we cannot access cross-origin stylesheets.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _invalidateNode(node: Node | null) {
|
2021-01-29 15:57:57 +01:00
|
|
|
while (node) {
|
|
|
|
|
ensureCachedData(node).ref = undefined;
|
|
|
|
|
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && (node as ShadowRoot).host)
|
|
|
|
|
node = (node as ShadowRoot).host;
|
|
|
|
|
else
|
|
|
|
|
node = node.parentNode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _handleMutations(list: MutationRecord[]) {
|
|
|
|
|
for (const mutation of list)
|
2021-01-30 00:24:38 +01:00
|
|
|
this._invalidateNode(mutation.target);
|
2021-01-29 15:57:57 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
markIframe(iframeElement: HTMLIFrameElement | HTMLFrameElement, frameId: string) {
|
2021-01-29 15:57:57 +01:00
|
|
|
(iframeElement as any)[kSnapshotFrameId] = frameId;
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
forceSnapshot(snapshotId: string) {
|
|
|
|
|
this._streamSnapshot(snapshotId);
|
|
|
|
|
}
|
2020-08-28 19:51:55 +02:00
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
private _streamSnapshot(snapshotId?: string) {
|
|
|
|
|
if (this._timer) {
|
|
|
|
|
clearTimeout(this._timer);
|
|
|
|
|
this._timer = undefined;
|
|
|
|
|
}
|
2021-01-29 15:57:57 +01:00
|
|
|
try {
|
|
|
|
|
const snapshot = this._captureSnapshot(snapshotId);
|
|
|
|
|
(window as any)[kSnapshotBinding](snapshot).catch((e: any) => {});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
}
|
2021-01-26 03:44:46 +01:00
|
|
|
this._timer = setTimeout(() => this._streamSnapshot(), 100);
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
private _sanitizeUrl(url: string): string {
|
|
|
|
|
if (url.startsWith('javascript:'))
|
|
|
|
|
return '';
|
|
|
|
|
return url;
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
private _sanitizeSrcSet(srcset: string): string {
|
|
|
|
|
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(',');
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
2021-01-26 03:44:46 +01:00
|
|
|
|
|
|
|
|
private _resolveUrl(base: string, url: string): string {
|
|
|
|
|
if (url === '')
|
|
|
|
|
return '';
|
|
|
|
|
try {
|
|
|
|
|
return new URL(url, base).href;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return url;
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
2020-09-11 00:33:39 +02:00
|
|
|
}
|
2021-01-26 03:44:46 +01:00
|
|
|
|
|
|
|
|
private _getSheetBase(sheet: CSSStyleSheet): string {
|
|
|
|
|
let rootSheet = sheet;
|
|
|
|
|
while (rootSheet.parentStyleSheet)
|
|
|
|
|
rootSheet = rootSheet.parentStyleSheet;
|
|
|
|
|
if (rootSheet.ownerNode)
|
|
|
|
|
return rootSheet.ownerNode.baseURI;
|
|
|
|
|
return document.baseURI;
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
2021-01-26 03:44:46 +01:00
|
|
|
|
|
|
|
|
private _getSheetText(sheet: CSSStyleSheet): string {
|
2021-01-30 00:24:38 +01:00
|
|
|
this._readingStyleSheet = true;
|
|
|
|
|
try {
|
|
|
|
|
const rules: string[] = [];
|
|
|
|
|
for (const rule of sheet.cssRules)
|
|
|
|
|
rules.push(rule.cssText);
|
|
|
|
|
return rules.join('\n');
|
|
|
|
|
} finally {
|
|
|
|
|
this._readingStyleSheet = false;
|
|
|
|
|
}
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
2021-01-26 03:44:46 +01:00
|
|
|
|
|
|
|
|
private _captureSnapshot(snapshotId?: string): SnapshotData {
|
2021-01-29 15:57:57 +01:00
|
|
|
const snapshotNumber = ++this._lastSnapshotNumber;
|
2021-01-26 03:44:46 +01:00
|
|
|
const win = window;
|
|
|
|
|
const doc = win.document;
|
|
|
|
|
|
2021-01-29 15:57:57 +01:00
|
|
|
// Ensure we are up-to-date.
|
|
|
|
|
this._handleMutations(this._observer.takeRecords());
|
|
|
|
|
for (const input of doc.querySelectorAll('input, textarea')) {
|
|
|
|
|
const value = (input as HTMLInputElement | HTMLTextAreaElement).value;
|
|
|
|
|
const data = ensureCachedData(input);
|
|
|
|
|
if (data.value !== value)
|
2021-01-30 00:24:38 +01:00
|
|
|
this._invalidateNode(input);
|
2021-01-29 15:57:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let nodeCounter = 0;
|
2021-01-26 03:44:46 +01:00
|
|
|
|
2021-01-29 15:57:57 +01:00
|
|
|
const visit = (node: Node | ShadowRoot): NodeSnapshot | undefined => {
|
|
|
|
|
const nodeType = node.nodeType;
|
|
|
|
|
const nodeName = nodeType === Node.DOCUMENT_FRAGMENT_NODE ? 'template' : node.nodeName;
|
2021-01-26 03:44:46 +01:00
|
|
|
|
|
|
|
|
if (nodeType !== Node.ELEMENT_NODE &&
|
2021-01-29 15:57:57 +01:00
|
|
|
nodeType !== Node.DOCUMENT_FRAGMENT_NODE &&
|
|
|
|
|
nodeType !== Node.TEXT_NODE)
|
2021-01-26 03:44:46 +01:00
|
|
|
return;
|
|
|
|
|
if (nodeName === 'SCRIPT' || nodeName === 'BASE')
|
|
|
|
|
return;
|
|
|
|
|
if (this._removeNoScript && nodeName === 'NOSCRIPT')
|
|
|
|
|
return;
|
|
|
|
|
|
2021-01-29 15:57:57 +01:00
|
|
|
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 || '');
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
if (nodeName === 'STYLE') {
|
2021-01-30 00:24:38 +01:00
|
|
|
const sheet = (node as HTMLStyleElement).sheet;
|
|
|
|
|
let cssText: string | undefined;
|
|
|
|
|
if (sheet)
|
|
|
|
|
cssText = this._updateStyleElementStyleSheetTextIfNeeded(sheet);
|
|
|
|
|
nodeCounter++; // Compensate for the extra text node in the list.
|
|
|
|
|
return ['style', {}, escapeText(cssText || node.textContent || '')];
|
2021-01-29 15:57:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const attrs: { [attr: string]: string } = {};
|
|
|
|
|
const result: NodeSnapshot = [nodeName, attrs];
|
|
|
|
|
|
2021-01-30 00:24:38 +01:00
|
|
|
if (nodeType === Node.DOCUMENT_FRAGMENT_NODE)
|
2021-01-29 15:57:57 +01:00
|
|
|
attrs[kShadowAttribute] = 'open';
|
2021-01-26 03:44:46 +01:00
|
|
|
|
|
|
|
|
if (nodeType === Node.ELEMENT_NODE) {
|
|
|
|
|
const element = node as Element;
|
|
|
|
|
// if (node === target)
|
2021-01-29 15:57:57 +01:00
|
|
|
// attrs[' __playwright_target__] = '';
|
2021-01-26 03:44:46 +01:00
|
|
|
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?
|
2021-01-29 15:57:57 +01:00
|
|
|
const frameId = (element as any)[kSnapshotFrameId];
|
|
|
|
|
value = frameId || 'data:text/html,<body>Snapshot is not available</body>';
|
2021-01-26 03:44:46 +01:00
|
|
|
} 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 = '';
|
|
|
|
|
}
|
2021-01-29 15:57:57 +01:00
|
|
|
attrs[name] = escapeAttribute(value);
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
|
|
|
|
if (nodeName === 'INPUT') {
|
2021-01-29 15:57:57 +01:00
|
|
|
const value = (element as HTMLInputElement).value;
|
|
|
|
|
data.value = value;
|
|
|
|
|
attrs['value'] = escapeAttribute(value);
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
|
|
|
|
if ((element as any).checked)
|
2021-01-29 15:57:57 +01:00
|
|
|
attrs['checked'] = '';
|
2021-01-26 03:44:46 +01:00
|
|
|
if ((element as any).disabled)
|
2021-01-29 15:57:57 +01:00
|
|
|
attrs['disabled'] = '';
|
2021-01-26 03:44:46 +01:00
|
|
|
if ((element as any).readOnly)
|
2021-01-29 15:57:57 +01:00
|
|
|
attrs['readonly'] = '';
|
|
|
|
|
if (element.scrollTop)
|
|
|
|
|
attrs[kScrollTopAttribute] = '' + element.scrollTop;
|
|
|
|
|
if (element.scrollLeft)
|
|
|
|
|
attrs[kScrollLeftAttribute] = '' + element.scrollLeft;
|
2021-01-27 00:09:17 +01:00
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
if (element.shadowRoot) {
|
2021-01-29 15:57:57 +01:00
|
|
|
const child = visit(element.shadowRoot);
|
|
|
|
|
if (child)
|
|
|
|
|
result.push(child);
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
2021-01-29 15:57:57 +01:00
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
if (nodeName === 'HEAD') {
|
2021-01-29 15:57:57 +01:00
|
|
|
const base: NodeSnapshot = ['base', { 'href': document.baseURI }];
|
2021-01-26 03:44:46 +01:00
|
|
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
|
|
|
if (child.nodeName === 'BASE') {
|
2021-01-29 15:57:57 +01:00
|
|
|
base[1]['href'] = escapeAttribute((child as HTMLBaseElement).href);
|
|
|
|
|
base[1]['target'] = escapeAttribute((child as HTMLBaseElement).target);
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
2021-01-29 15:57:57 +01:00
|
|
|
nodeCounter++; // Compensate for the extra 'base' node in the list.
|
|
|
|
|
result.push(base);
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
2021-01-29 15:57:57 +01:00
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
if (nodeName === 'TEXTAREA') {
|
2021-01-29 15:57:57 +01:00
|
|
|
nodeCounter++; // Compensate for the extra text node in the list.
|
|
|
|
|
const value = (node as HTMLTextAreaElement).value;
|
|
|
|
|
data.value = value;
|
|
|
|
|
result.push(escapeText(value));
|
2021-01-26 03:44:46 +01:00
|
|
|
} else {
|
2021-01-29 15:57:57 +01:00
|
|
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
|
|
|
const snapshotted = visit(child);
|
|
|
|
|
if (snapshotted)
|
|
|
|
|
result.push(snapshotted);
|
|
|
|
|
}
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-29 15:57:57 +01:00
|
|
|
if (result.length === 2 && !Object.keys(attrs).length)
|
|
|
|
|
result.pop(); // Remove empty attrs when there are no children.
|
|
|
|
|
return result;
|
|
|
|
|
};
|
2021-01-28 04:42:51 +01:00
|
|
|
|
2021-01-29 15:57:57 +01:00
|
|
|
const html = doc.documentElement ? visit(doc.documentElement)! : (['html', {}] as NodeSnapshot);
|
2021-01-30 00:24:38 +01:00
|
|
|
const result: SnapshotData = {
|
2021-01-29 15:57:57 +01:00
|
|
|
html,
|
|
|
|
|
doctype: doc.doctype ? doc.doctype.name : undefined,
|
2021-01-30 00:24:38 +01:00
|
|
|
resourceOverrides: [],
|
2021-01-26 03:44:46 +01:00
|
|
|
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,
|
|
|
|
|
};
|
2021-01-30 00:24:38 +01:00
|
|
|
|
|
|
|
|
for (const sheet of this._allStyleSheetsWithUrlOverride) {
|
|
|
|
|
const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
|
|
|
|
|
if (content === undefined) {
|
|
|
|
|
// Unable to capture stylsheet contents.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const base = this._getSheetBase(sheet);
|
|
|
|
|
const url = removeHash(this._resolveUrl(base, sheet.href!));
|
|
|
|
|
result.resourceOverrides.push({ url, content });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
2021-01-26 03:44:46 +01:00
|
|
|
}
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
(window as any)[kSnapshotStreamer] = new Streamer();
|
2020-08-28 19:51:55 +02:00
|
|
|
}
|
2021-01-29 15:57:57 +01:00
|
|
|
|
|
|
|
|
export function snapshotScript() {
|
|
|
|
|
function applyPlaywrightAttributes(shadowAttribute: string, scrollTopAttribute: string, scrollLeftAttribute: string) {
|
|
|
|
|
const scrollTops: Element[] = [];
|
|
|
|
|
const scrollLefts: Element[] = [];
|
|
|
|
|
|
|
|
|
|
const visit = (root: Document | ShadowRoot) => {
|
|
|
|
|
// 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 as HTMLTemplateElement;
|
|
|
|
|
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}')`;
|
|
|
|
|
}
|