fix duplicated shadow dom (#1095)
* fix: duplicated elements in shadow doms clean observers before taking new full snapshots * add checker for replayer to make it stable when data has duplicated nodes * apply review suggestions * add change log * Apply formatting changes * update prettier to fit the master branch --------- Co-authored-by: Mark-Fenng <Mark-Fenng@users.noreply.github.com>
This commit is contained in:
@@ -53,7 +53,6 @@
|
||||
"@types/jest-image-snapshot": "^5.1.0",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@types/prettier": "^2.3.2",
|
||||
"@types/puppeteer": "^5.4.4",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"cross-env": "^5.2.0",
|
||||
@@ -65,7 +64,6 @@
|
||||
"jest": "^27.5.1",
|
||||
"jest-image-snapshot": "^5.2.0",
|
||||
"jest-snapshot": "^23.6.0",
|
||||
"prettier": "2.2.1",
|
||||
"puppeteer": "^11.0.0",
|
||||
"rollup": "^2.68.0",
|
||||
"rollup-plugin-esbuild": "^4.9.1",
|
||||
|
||||
@@ -348,8 +348,8 @@ function record<T = eventWithTime>(
|
||||
|
||||
// When we take a full snapshot, old tracked StyleSheets need to be removed.
|
||||
stylesheetManager.reset();
|
||||
// Old shadow doms cache need to be cleared.
|
||||
shadowDomManager.clearCache();
|
||||
|
||||
shadowDomManager.init();
|
||||
|
||||
mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting
|
||||
const node = snapshot(document, {
|
||||
|
||||
@@ -26,7 +26,7 @@ export class ShadowDomManager {
|
||||
private scrollCb: scrollCallback;
|
||||
private bypassOptions: BypassOptions;
|
||||
private mirror: Mirror;
|
||||
private restorePatches: (() => void)[] = [];
|
||||
private restoreHandlers: (() => void)[] = [];
|
||||
|
||||
constructor(options: {
|
||||
mutationCb: mutationCallBack;
|
||||
@@ -39,34 +39,20 @@ export class ShadowDomManager {
|
||||
this.bypassOptions = options.bypassOptions;
|
||||
this.mirror = options.mirror;
|
||||
|
||||
// Patch 'attachShadow' to observe newly added shadow doms.
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const manager = this;
|
||||
this.restorePatches.push(
|
||||
patch(
|
||||
Element.prototype,
|
||||
'attachShadow',
|
||||
function (original: (init: ShadowRootInit) => ShadowRoot) {
|
||||
return function (this: HTMLElement, option: ShadowRootInit) {
|
||||
const shadowRoot = original.call(this, option);
|
||||
this.init();
|
||||
}
|
||||
|
||||
// For the shadow dom elements in the document, monitor their dom mutations.
|
||||
// For shadow dom elements that aren't in the document yet,
|
||||
// we start monitoring them once their shadow dom host is appended to the document.
|
||||
if (this.shadowRoot && inDom(this))
|
||||
manager.addShadowRoot(this.shadowRoot, this.ownerDocument);
|
||||
return shadowRoot;
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
public init() {
|
||||
this.reset();
|
||||
// Patch 'attachShadow' to observe newly added shadow doms.
|
||||
this.patchAttachShadow(Element, document);
|
||||
}
|
||||
|
||||
public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) {
|
||||
if (!isNativeShadowDom(shadowRoot)) return;
|
||||
if (this.shadowDoms.has(shadowRoot)) return;
|
||||
this.shadowDoms.add(shadowRoot);
|
||||
initMutationObserver(
|
||||
const observer = initMutationObserver(
|
||||
{
|
||||
...this.bypassOptions,
|
||||
doc,
|
||||
@@ -76,14 +62,17 @@ export class ShadowDomManager {
|
||||
},
|
||||
shadowRoot,
|
||||
);
|
||||
initScrollObserver({
|
||||
...this.bypassOptions,
|
||||
scrollCb: this.scrollCb,
|
||||
// https://gist.github.com/praveenpuglia/0832da687ed5a5d7a0907046c9ef1813
|
||||
// scroll is not allowed to pass the boundary, so we need to listen the shadow document
|
||||
doc: shadowRoot as unknown as Document,
|
||||
mirror: this.mirror,
|
||||
});
|
||||
this.restoreHandlers.push(() => observer.disconnect());
|
||||
this.restoreHandlers.push(
|
||||
initScrollObserver({
|
||||
...this.bypassOptions,
|
||||
scrollCb: this.scrollCb,
|
||||
// https://gist.github.com/praveenpuglia/0832da687ed5a5d7a0907046c9ef1813
|
||||
// scroll is not allowed to pass the boundary, so we need to listen the shadow document
|
||||
doc: shadowRoot as unknown as Document,
|
||||
mirror: this.mirror,
|
||||
}),
|
||||
);
|
||||
// Defer this to avoid adoptedStyleSheet events being created before the full snapshot is created or attachShadow action is recorded.
|
||||
setTimeout(() => {
|
||||
if (
|
||||
@@ -94,12 +83,14 @@ export class ShadowDomManager {
|
||||
shadowRoot.adoptedStyleSheets,
|
||||
this.mirror.getId(shadowRoot.host),
|
||||
);
|
||||
initAdoptedStyleSheetObserver(
|
||||
{
|
||||
mirror: this.mirror,
|
||||
stylesheetManager: this.bypassOptions.stylesheetManager,
|
||||
},
|
||||
shadowRoot,
|
||||
this.restoreHandlers.push(
|
||||
initAdoptedStyleSheetObserver(
|
||||
{
|
||||
mirror: this.mirror,
|
||||
stylesheetManager: this.bypassOptions.stylesheetManager,
|
||||
},
|
||||
shadowRoot,
|
||||
),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
@@ -108,39 +99,57 @@ export class ShadowDomManager {
|
||||
* Monkey patch 'attachShadow' of an IFrameElement to observe newly added shadow doms.
|
||||
*/
|
||||
public observeAttachShadow(iframeElement: HTMLIFrameElement) {
|
||||
if (iframeElement.contentWindow) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const manager = this;
|
||||
this.restorePatches.push(
|
||||
patch(
|
||||
(
|
||||
iframeElement.contentWindow as Window & {
|
||||
HTMLElement: { prototype: HTMLElement };
|
||||
}
|
||||
).HTMLElement.prototype,
|
||||
'attachShadow',
|
||||
function (original: (init: ShadowRootInit) => ShadowRoot) {
|
||||
return function (this: HTMLElement, option: ShadowRootInit) {
|
||||
const shadowRoot = original.call(this, option);
|
||||
if (this.shadowRoot)
|
||||
manager.addShadowRoot(
|
||||
this.shadowRoot,
|
||||
iframeElement.contentDocument as Document,
|
||||
);
|
||||
return shadowRoot;
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!iframeElement.contentWindow || !iframeElement.contentDocument) return;
|
||||
|
||||
this.patchAttachShadow(
|
||||
(
|
||||
iframeElement.contentWindow as Window & {
|
||||
Element: { prototype: Element };
|
||||
}
|
||||
).Element,
|
||||
iframeElement.contentDocument,
|
||||
);
|
||||
}
|
||||
|
||||
public clearCache() {
|
||||
this.shadowDoms = new WeakSet();
|
||||
/**
|
||||
* Patch 'attachShadow' to observe newly added shadow doms.
|
||||
*/
|
||||
private patchAttachShadow(
|
||||
element: {
|
||||
prototype: Element;
|
||||
},
|
||||
doc: Document,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const manager = this;
|
||||
this.restoreHandlers.push(
|
||||
patch(
|
||||
element.prototype,
|
||||
'attachShadow',
|
||||
function (original: (init: ShadowRootInit) => ShadowRoot) {
|
||||
return function (this: Element, option: ShadowRootInit) {
|
||||
const shadowRoot = original.call(this, option);
|
||||
// For the shadow dom elements in the document, monitor their dom mutations.
|
||||
// For shadow dom elements that aren't in the document yet,
|
||||
// we start monitoring them once their shadow dom host is appended to the document.
|
||||
if (this.shadowRoot && inDom(this))
|
||||
manager.addShadowRoot(this.shadowRoot, doc);
|
||||
return shadowRoot;
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.restorePatches.forEach((restorePatch) => restorePatch());
|
||||
this.clearCache();
|
||||
this.restoreHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler();
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
});
|
||||
this.restoreHandlers = [];
|
||||
this.shadowDoms = new WeakSet();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user