mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(snapshots): switch MutationObserver to only observer attributes (#5220)
Everything but attributes in the light dom is manually compared during DOM traversal, for example child nodes or scroll offset. This way we get a bullet-proof solution that works with input values, scroll offsets, shadow dom and anything else web comes up with. We also restore scroll only on the document scrolling element, for performance reasons. We should figure out the story around scrolling. Changes stationary snapshots from ~0.5ms to ~2.5ms due to DOM traversal.
This commit is contained in:
parent
bf8c30a88b
commit
a9de3d8fd2
@ -203,6 +203,13 @@ export class SnapshotServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
||||||
|
const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
function snapshotNodes(snapshot: trace.FrameSnapshot): NodeSnapshot[] {
|
function snapshotNodes(snapshot: trace.FrameSnapshot): NodeSnapshot[] {
|
||||||
if (!(snapshot as any)._nodes) {
|
if (!(snapshot as any)._nodes) {
|
||||||
@ -211,9 +218,9 @@ export class SnapshotServer {
|
|||||||
if (typeof n === 'string') {
|
if (typeof n === 'string') {
|
||||||
nodes.push(n);
|
nodes.push(n);
|
||||||
} else if (typeof n[0] === 'string') {
|
} else if (typeof n[0] === 'string') {
|
||||||
nodes.push(n);
|
|
||||||
for (let i = 2; i < n.length; i++)
|
for (let i = 2; i < n.length; i++)
|
||||||
visit(n[i]);
|
visit(n[i]);
|
||||||
|
nodes.push(n);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
visit(snapshot.html);
|
visit(snapshot.html);
|
||||||
@ -226,7 +233,7 @@ export class SnapshotServer {
|
|||||||
const visit = (n: trace.NodeSnapshot, snapshotIndex: number): string => {
|
const visit = (n: trace.NodeSnapshot, snapshotIndex: number): string => {
|
||||||
// Text node.
|
// Text node.
|
||||||
if (typeof n === 'string')
|
if (typeof n === 'string')
|
||||||
return n;
|
return escapeText(n);
|
||||||
|
|
||||||
if (!(n as any)._string) {
|
if (!(n as any)._string) {
|
||||||
if (Array.isArray(n[0])) {
|
if (Array.isArray(n[0])) {
|
||||||
@ -243,7 +250,7 @@ export class SnapshotServer {
|
|||||||
const builder: string[] = [];
|
const builder: string[] = [];
|
||||||
builder.push('<', n[0]);
|
builder.push('<', n[0]);
|
||||||
for (const [attr, value] of Object.entries(n[1] || {}))
|
for (const [attr, value] of Object.entries(n[1] || {}))
|
||||||
builder.push(' ', attr, '="', value, '"');
|
builder.push(' ', attr, '="', escapeAttribute(value), '"');
|
||||||
builder.push('>');
|
builder.push('>');
|
||||||
for (let i = 2; i < n.length; i++)
|
for (let i = 2; i < n.length; i++)
|
||||||
builder.push(visit(n[i], snapshotIndex));
|
builder.push(visit(n[i], snapshotIndex));
|
||||||
|
@ -62,6 +62,7 @@ export class Snapshotter {
|
|||||||
];
|
];
|
||||||
this._context.exposeBinding(kSnapshotBinding, false, (source, data: SnapshotData) => {
|
this._context.exposeBinding(kSnapshotBinding, false, (source, data: SnapshotData) => {
|
||||||
const snapshot: FrameSnapshot = {
|
const snapshot: FrameSnapshot = {
|
||||||
|
doctype: data.doctype,
|
||||||
html: data.html,
|
html: data.html,
|
||||||
viewport: data.viewport,
|
viewport: data.viewport,
|
||||||
resourceOverrides: [],
|
resourceOverrides: [],
|
||||||
|
@ -18,7 +18,7 @@ export type NodeSnapshot =
|
|||||||
// Text node.
|
// Text node.
|
||||||
string |
|
string |
|
||||||
// Subtree reference, "x snapshots ago, node #y". Could point to a text node.
|
// Subtree reference, "x snapshots ago, node #y". Could point to a text node.
|
||||||
// Only nodes that are not references are counted, starting from zero.
|
// Only nodes that are not references are counted, starting from zero, using post-order traversal.
|
||||||
[ [number, number] ] |
|
[ [number, number] ] |
|
||||||
// Just node name.
|
// Just node name.
|
||||||
[ string ] |
|
[ string ] |
|
||||||
@ -56,8 +56,9 @@ export function frameSnapshotStreamer() {
|
|||||||
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
|
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
|
||||||
const kCachedData = Symbol('__playwright_snapshot_cache_');
|
const kCachedData = Symbol('__playwright_snapshot_cache_');
|
||||||
type CachedData = {
|
type CachedData = {
|
||||||
|
cached?: any[], // Cached values to determine whether the snapshot will be the same.
|
||||||
ref?: [number, number], // Previous snapshotNumber and nodeIndex.
|
ref?: [number, number], // Previous snapshotNumber and nodeIndex.
|
||||||
value?: string, // Value for input/textarea elements.
|
attributesCached?: boolean, // Whether node attributes have not changed.
|
||||||
cssText?: string, // Text for stylesheets.
|
cssText?: string, // Text for stylesheets.
|
||||||
cssRef?: number, // Previous snapshotNumber for overridden stylesheets.
|
cssRef?: number, // Previous snapshotNumber for overridden stylesheets.
|
||||||
};
|
};
|
||||||
@ -67,14 +68,6 @@ export function frameSnapshotStreamer() {
|
|||||||
return obj[kCachedData];
|
return obj[kCachedData];
|
||||||
}
|
}
|
||||||
|
|
||||||
const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeHash(url: string) {
|
function removeHash(url: string) {
|
||||||
try {
|
try {
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
@ -89,10 +82,11 @@ export function frameSnapshotStreamer() {
|
|||||||
private _removeNoScript = true;
|
private _removeNoScript = true;
|
||||||
private _timer: NodeJS.Timeout | undefined;
|
private _timer: NodeJS.Timeout | undefined;
|
||||||
private _lastSnapshotNumber = 0;
|
private _lastSnapshotNumber = 0;
|
||||||
private _observer: MutationObserver;
|
|
||||||
private _staleStyleSheets = new Set<CSSStyleSheet>();
|
private _staleStyleSheets = new Set<CSSStyleSheet>();
|
||||||
private _allStyleSheetsWithUrlOverride = new Set<CSSStyleSheet>();
|
private _allStyleSheetsWithUrlOverride = new Set<CSSStyleSheet>();
|
||||||
private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
|
private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
|
||||||
|
private _fakeBase: HTMLBaseElement;
|
||||||
|
private _observer: MutationObserver;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
||||||
@ -102,13 +96,11 @@ export function frameSnapshotStreamer() {
|
|||||||
this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'rules', (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));
|
this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'cssRules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
||||||
|
|
||||||
|
this._fakeBase = document.createElement('base');
|
||||||
|
|
||||||
this._observer = new MutationObserver(list => this._handleMutations(list));
|
this._observer = new MutationObserver(list => this._handleMutations(list));
|
||||||
const observerConfig = { attributes: true, childList: true, subtree: true, characterData: true };
|
const observerConfig = { attributes: true, subtree: true };
|
||||||
this._observer.observe(document, observerConfig);
|
this._observer.observe(document, observerConfig);
|
||||||
this._interceptNativeMethod(window.Element.prototype, 'attachShadow', (node: Node, shadowRoot: ShadowRoot) => {
|
|
||||||
this._invalidateNode(node);
|
|
||||||
this._observer.observe(shadowRoot, observerConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._streamSnapshot();
|
this._streamSnapshot();
|
||||||
}
|
}
|
||||||
@ -136,14 +128,17 @@ export function frameSnapshotStreamer() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleMutations(list: MutationRecord[]) {
|
||||||
|
for (const mutation of list)
|
||||||
|
ensureCachedData(mutation.target).attributesCached = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private _invalidateStyleSheet(sheet: CSSStyleSheet) {
|
private _invalidateStyleSheet(sheet: CSSStyleSheet) {
|
||||||
if (this._readingStyleSheet)
|
if (this._readingStyleSheet)
|
||||||
return;
|
return;
|
||||||
this._staleStyleSheets.add(sheet);
|
this._staleStyleSheets.add(sheet);
|
||||||
if (sheet.href !== null)
|
if (sheet.href !== null)
|
||||||
this._allStyleSheetsWithUrlOverride.add(sheet);
|
this._allStyleSheetsWithUrlOverride.add(sheet);
|
||||||
if (sheet.ownerNode && sheet.ownerNode.nodeName === 'STYLE')
|
|
||||||
this._invalidateNode(sheet.ownerNode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet): string | undefined {
|
private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet): string | undefined {
|
||||||
@ -175,21 +170,6 @@ export function frameSnapshotStreamer() {
|
|||||||
return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
|
return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _invalidateNode(node: Node | null) {
|
|
||||||
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)
|
|
||||||
this._invalidateNode(mutation.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
markIframe(iframeElement: HTMLIFrameElement | HTMLFrameElement, frameId: string) {
|
markIframe(iframeElement: HTMLIFrameElement | HTMLFrameElement, frameId: string) {
|
||||||
(iframeElement as any)[kSnapshotFrameId] = frameId;
|
(iframeElement as any)[kSnapshotFrameId] = frameId;
|
||||||
}
|
}
|
||||||
@ -260,21 +240,13 @@ export function frameSnapshotStreamer() {
|
|||||||
|
|
||||||
private _captureSnapshot(snapshotId?: string): SnapshotData {
|
private _captureSnapshot(snapshotId?: string): SnapshotData {
|
||||||
const snapshotNumber = ++this._lastSnapshotNumber;
|
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 as HTMLInputElement | HTMLTextAreaElement).value;
|
|
||||||
const data = ensureCachedData(input);
|
|
||||||
if (data.value !== value)
|
|
||||||
this._invalidateNode(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
let nodeCounter = 0;
|
let nodeCounter = 0;
|
||||||
|
let shadowDomNesting = 0;
|
||||||
|
|
||||||
const visit = (node: Node | ShadowRoot): NodeSnapshot | undefined => {
|
// Ensure we are up to date.
|
||||||
|
this._handleMutations(this._observer.takeRecords());
|
||||||
|
|
||||||
|
const visitNode = (node: Node | ShadowRoot): { equals: boolean, n: NodeSnapshot } | undefined => {
|
||||||
const nodeType = node.nodeType;
|
const nodeType = node.nodeType;
|
||||||
const nodeName = nodeType === Node.DOCUMENT_FRAGMENT_NODE ? 'template' : node.nodeName;
|
const nodeName = nodeType === Node.DOCUMENT_FRAGMENT_NODE ? 'template' : node.nodeName;
|
||||||
|
|
||||||
@ -282,34 +254,61 @@ export function frameSnapshotStreamer() {
|
|||||||
nodeType !== Node.DOCUMENT_FRAGMENT_NODE &&
|
nodeType !== Node.DOCUMENT_FRAGMENT_NODE &&
|
||||||
nodeType !== Node.TEXT_NODE)
|
nodeType !== Node.TEXT_NODE)
|
||||||
return;
|
return;
|
||||||
if (nodeName === 'SCRIPT' || nodeName === 'BASE')
|
if (nodeName === 'SCRIPT')
|
||||||
return;
|
return;
|
||||||
if (this._removeNoScript && nodeName === 'NOSCRIPT')
|
if (this._removeNoScript && nodeName === 'NOSCRIPT')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const data = ensureCachedData(node);
|
const data = ensureCachedData(node);
|
||||||
if (data.ref)
|
const values: any[] = [];
|
||||||
return [[ snapshotNumber - data.ref[0], data.ref[1] ]];
|
let equals = !!data.cached;
|
||||||
nodeCounter++;
|
let extraNodes = 0;
|
||||||
data.ref = [snapshotNumber, nodeCounter - 1];
|
|
||||||
// ---------- No returns without the data after this point -----------
|
|
||||||
// ---------- Otherwise nodeCounter is wrong -----------
|
|
||||||
|
|
||||||
if (nodeType === Node.TEXT_NODE)
|
const expectValue = (value: any) => {
|
||||||
return escapeText(node.nodeValue || '');
|
equals = equals && data.cached![values.length] === value;
|
||||||
|
values.push(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkAndReturn = (n: NodeSnapshot): { equals: boolean, n: NodeSnapshot } => {
|
||||||
|
data.attributesCached = true;
|
||||||
|
if (equals)
|
||||||
|
return { equals: true, n: [[ snapshotNumber - data.ref![0], data.ref![1] ]] };
|
||||||
|
nodeCounter += extraNodes;
|
||||||
|
data.ref = [snapshotNumber, nodeCounter++];
|
||||||
|
data.cached = values;
|
||||||
|
return { equals: false, n };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nodeType === Node.TEXT_NODE) {
|
||||||
|
const value = node.nodeValue || '';
|
||||||
|
expectValue(value);
|
||||||
|
return checkAndReturn(value);
|
||||||
|
}
|
||||||
|
|
||||||
if (nodeName === 'STYLE') {
|
if (nodeName === 'STYLE') {
|
||||||
const sheet = (node as HTMLStyleElement).sheet;
|
const sheet = (node as HTMLStyleElement).sheet;
|
||||||
let cssText: string | undefined;
|
let cssText: string | undefined;
|
||||||
if (sheet)
|
if (sheet)
|
||||||
cssText = this._updateStyleElementStyleSheetTextIfNeeded(sheet);
|
cssText = this._updateStyleElementStyleSheetTextIfNeeded(sheet);
|
||||||
nodeCounter++; // Compensate for the extra text node in the list.
|
cssText = cssText || node.textContent || '';
|
||||||
return ['style', {}, escapeText(cssText || node.textContent || '')];
|
expectValue(cssText);
|
||||||
|
// Compensate for the extra 'cssText' text node.
|
||||||
|
extraNodes++;
|
||||||
|
return checkAndReturn(['style', {}, cssText]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const attrs: { [attr: string]: string } = {};
|
const attrs: { [attr: string]: string } = {};
|
||||||
const result: NodeSnapshot = [nodeName, attrs];
|
const result: NodeSnapshot = [nodeName, attrs];
|
||||||
|
|
||||||
|
const visitChild = (child: Node) => {
|
||||||
|
const snapshotted = visitNode(child);
|
||||||
|
if (snapshotted) {
|
||||||
|
result.push(snapshotted.n);
|
||||||
|
expectValue(child);
|
||||||
|
equals = equals && snapshotted.equals;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (nodeType === Node.DOCUMENT_FRAGMENT_NODE)
|
if (nodeType === Node.DOCUMENT_FRAGMENT_NODE)
|
||||||
attrs[kShadowAttribute] = 'open';
|
attrs[kShadowAttribute] = 'open';
|
||||||
|
|
||||||
@ -317,15 +316,66 @@ export function frameSnapshotStreamer() {
|
|||||||
const element = node as Element;
|
const element = node as Element;
|
||||||
// if (node === target)
|
// if (node === target)
|
||||||
// attrs[' __playwright_target__] = '';
|
// attrs[' __playwright_target__] = '';
|
||||||
|
if (nodeName === 'INPUT') {
|
||||||
|
const value = (element as HTMLInputElement).value;
|
||||||
|
expectValue('value');
|
||||||
|
expectValue(value);
|
||||||
|
attrs['value'] = value;
|
||||||
|
if ((element as HTMLInputElement).checked) {
|
||||||
|
expectValue('checked');
|
||||||
|
attrs['checked'] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (element === document.scrollingElement) {
|
||||||
|
// TODO: restoring scroll positions of all elements
|
||||||
|
// is somewhat expensive. Figure this out.
|
||||||
|
if (element.scrollTop) {
|
||||||
|
expectValue(kScrollTopAttribute);
|
||||||
|
expectValue(element.scrollTop);
|
||||||
|
attrs[kScrollTopAttribute] = '' + element.scrollTop;
|
||||||
|
}
|
||||||
|
if (element.scrollLeft) {
|
||||||
|
expectValue(kScrollLeftAttribute);
|
||||||
|
expectValue(element.scrollLeft);
|
||||||
|
attrs[kScrollLeftAttribute] = '' + element.scrollLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (element.shadowRoot) {
|
||||||
|
++shadowDomNesting;
|
||||||
|
visitChild(element.shadowRoot);
|
||||||
|
--shadowDomNesting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeName === 'TEXTAREA') {
|
||||||
|
const value = (node as HTMLTextAreaElement).value;
|
||||||
|
expectValue(value);
|
||||||
|
extraNodes++; // Compensate for the extra text node.
|
||||||
|
result.push(value);
|
||||||
|
} else {
|
||||||
|
if (nodeName === 'HEAD') {
|
||||||
|
// Insert fake <base> first, to ensure all <link> elements use the proper base uri.
|
||||||
|
this._fakeBase.setAttribute('href', document.baseURI);
|
||||||
|
visitChild(this._fakeBase);
|
||||||
|
}
|
||||||
|
for (let child = node.firstChild; child; child = child.nextSibling)
|
||||||
|
visitChild(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can skip attributes comparison because nothing else has changed,
|
||||||
|
// and mutation observer didn't tell us about the attributes.
|
||||||
|
if (equals && data.attributesCached && !shadowDomNesting)
|
||||||
|
return checkAndReturn(result);
|
||||||
|
|
||||||
|
if (nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element = node as Element;
|
||||||
for (let i = 0; i < element.attributes.length; i++) {
|
for (let i = 0; i < element.attributes.length; i++) {
|
||||||
const name = element.attributes[i].name;
|
const name = element.attributes[i].name;
|
||||||
let value = element.attributes[i].value;
|
|
||||||
if (name === 'value' && (nodeName === 'INPUT' || nodeName === 'TEXTAREA'))
|
if (name === 'value' && (nodeName === 'INPUT' || nodeName === 'TEXTAREA'))
|
||||||
continue;
|
continue;
|
||||||
if (name === 'checked' || name === 'disabled' || name === 'checked')
|
|
||||||
continue;
|
|
||||||
if (nodeName === 'LINK' && name === 'integrity')
|
if (nodeName === 'LINK' && name === 'integrity')
|
||||||
continue;
|
continue;
|
||||||
|
let value = element.attributes[i].value;
|
||||||
if (name === 'src' && (nodeName === 'IFRAME' || nodeName === 'FRAME')) {
|
if (name === 'src' && (nodeName === 'IFRAME' || nodeName === 'FRAME')) {
|
||||||
// TODO: handle srcdoc?
|
// TODO: handle srcdoc?
|
||||||
const frameId = (element as any)[kSnapshotFrameId];
|
const frameId = (element as any)[kSnapshotFrameId];
|
||||||
@ -341,69 +391,30 @@ export function frameSnapshotStreamer() {
|
|||||||
} else if (name.startsWith('on')) {
|
} else if (name.startsWith('on')) {
|
||||||
value = '';
|
value = '';
|
||||||
}
|
}
|
||||||
attrs[name] = escapeAttribute(value);
|
expectValue(name);
|
||||||
}
|
expectValue(value);
|
||||||
if (nodeName === 'INPUT') {
|
attrs[name] = value;
|
||||||
const value = (element as HTMLInputElement).value;
|
|
||||||
data.value = value;
|
|
||||||
attrs['value'] = escapeAttribute(value);
|
|
||||||
}
|
|
||||||
if ((element as any).checked)
|
|
||||||
attrs['checked'] = '';
|
|
||||||
if ((element as any).disabled)
|
|
||||||
attrs['disabled'] = '';
|
|
||||||
if ((element as any).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: NodeSnapshot = ['base', { 'href': document.baseURI }];
|
|
||||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
||||||
if (child.nodeName === 'BASE') {
|
|
||||||
base[1]['href'] = escapeAttribute((child as HTMLBaseElement).href);
|
|
||||||
base[1]['target'] = escapeAttribute((child as HTMLBaseElement).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 as HTMLTextAreaElement).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)
|
if (result.length === 2 && !Object.keys(attrs).length)
|
||||||
result.pop(); // Remove empty attrs when there are no children.
|
result.pop(); // Remove empty attrs when there are no children.
|
||||||
return result;
|
return checkAndReturn(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
const html = doc.documentElement ? visit(doc.documentElement)! : (['html', {}] as NodeSnapshot);
|
let html: NodeSnapshot;
|
||||||
|
if (document.documentElement)
|
||||||
|
html = visitNode(document.documentElement)!.n;
|
||||||
|
else
|
||||||
|
html = ['html'];
|
||||||
|
|
||||||
const result: SnapshotData = {
|
const result: SnapshotData = {
|
||||||
html,
|
html,
|
||||||
doctype: doc.doctype ? doc.doctype.name : undefined,
|
doctype: document.doctype ? document.doctype.name : undefined,
|
||||||
resourceOverrides: [],
|
resourceOverrides: [],
|
||||||
viewport: {
|
viewport: {
|
||||||
width: Math.max(doc.body ? doc.body.offsetWidth : 0, doc.documentElement ? doc.documentElement.offsetWidth : 0),
|
width: Math.max(document.body ? document.body.offsetWidth : 0, document.documentElement ? document.documentElement.offsetWidth : 0),
|
||||||
height: Math.max(doc.body ? doc.body.offsetHeight : 0, doc.documentElement ? doc.documentElement.offsetHeight : 0),
|
height: Math.max(document.body ? document.body.offsetHeight : 0, document.documentElement ? document.documentElement.offsetHeight : 0),
|
||||||
},
|
},
|
||||||
url: location.href,
|
url: location.href,
|
||||||
snapshotId,
|
snapshotId,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user