Fix inline link elements bug (#995)
* fix: bug when inlined link elements * test: update snapshot for test cases * apply Justin's review suggestions 1. make Mirror's replace function act the same with the original one when there's no existed node to get replaced. 2. when replacing with the link/style elements, keep their existing attributes to prevent potential bugs
This commit is contained in:
@@ -389,6 +389,11 @@ export class Mirror implements IMirror<RRNode> {
|
||||
}
|
||||
|
||||
replace(id: number, n: RRNode) {
|
||||
const oldNode = this.getNode(id);
|
||||
if (oldNode) {
|
||||
const meta = this.nodeMetaMap.get(oldNode);
|
||||
if (meta) this.nodeMetaMap.set(n, meta);
|
||||
}
|
||||
this.idNodeMap.set(id, n);
|
||||
}
|
||||
|
||||
|
||||
@@ -1165,7 +1165,6 @@ export function serializeNodeWithId(
|
||||
},
|
||||
stylesheetLoadTimeout,
|
||||
);
|
||||
if (isStylesheetLoaded(n as HTMLLinkElement) === false) return null; // add stylesheet in later mutation
|
||||
}
|
||||
|
||||
return serializedNode;
|
||||
|
||||
@@ -329,7 +329,7 @@ function record<T = eventWithTime>(
|
||||
shadowDomManager.observeAttachShadow(iframe);
|
||||
},
|
||||
onStylesheetLoad: (linkEl, childSn) => {
|
||||
stylesheetManager.attachLinkElement(linkEl, childSn, mirror);
|
||||
stylesheetManager.attachLinkElement(linkEl, childSn);
|
||||
},
|
||||
keepIframeSrcFn,
|
||||
});
|
||||
|
||||
@@ -324,7 +324,7 @@ export default class MutationBuffer {
|
||||
this.shadowDomManager.observeAttachShadow(iframe);
|
||||
},
|
||||
onStylesheetLoad: (link, childSn) => {
|
||||
this.stylesheetManager.attachLinkElement(link, childSn, this.mirror);
|
||||
this.stylesheetManager.attachLinkElement(link, childSn);
|
||||
},
|
||||
});
|
||||
if (sn) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Mirror, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import type { elementNode, serializedNodeWithId } from 'rrweb-snapshot';
|
||||
import { getCssRuleString } from 'rrweb-snapshot';
|
||||
import type {
|
||||
adoptedStyleSheetCallback,
|
||||
adoptedStyleSheetParam,
|
||||
attributeMutation,
|
||||
mutationCallBack,
|
||||
} from '../types';
|
||||
import { StyleSheetMirror } from '../utils';
|
||||
@@ -24,20 +25,20 @@ export class StylesheetManager {
|
||||
public attachLinkElement(
|
||||
linkEl: HTMLLinkElement,
|
||||
childSn: serializedNodeWithId,
|
||||
mirror: Mirror,
|
||||
) {
|
||||
this.mutationCb({
|
||||
adds: [
|
||||
{
|
||||
parentId: mirror.getId(linkEl),
|
||||
nextId: null,
|
||||
node: childSn,
|
||||
},
|
||||
],
|
||||
removes: [],
|
||||
texts: [],
|
||||
attributes: [],
|
||||
});
|
||||
if ('_cssText' in (childSn as elementNode).attributes)
|
||||
this.mutationCb({
|
||||
adds: [],
|
||||
removes: [],
|
||||
texts: [],
|
||||
attributes: [
|
||||
{
|
||||
id: childSn.id,
|
||||
attributes: (childSn as elementNode)
|
||||
.attributes as attributeMutation['attributes'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.trackLinkElement(linkEl);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
createCache,
|
||||
Mirror,
|
||||
createMirror,
|
||||
attributes,
|
||||
serializedElementNodeWithId,
|
||||
} from 'rrweb-snapshot';
|
||||
import {
|
||||
RRDocument,
|
||||
@@ -1643,6 +1645,41 @@ export class Replayer {
|
||||
(target as Element | RRElement).removeAttribute(attributeName);
|
||||
} else if (typeof value === 'string') {
|
||||
try {
|
||||
// When building snapshot, some link styles haven't loaded. Then they are loaded, they will be inlined as incremental mutation change of attribute. We need to replace the old elements whose styles aren't inlined.
|
||||
if (
|
||||
attributeName === '_cssText' &&
|
||||
(target.nodeName === 'LINK' || target.nodeName === 'STYLE')
|
||||
) {
|
||||
try {
|
||||
const newSn = mirror.getMeta(
|
||||
target as Node & RRNode,
|
||||
) as serializedElementNodeWithId;
|
||||
Object.assign(
|
||||
newSn.attributes,
|
||||
mutation.attributes as attributes,
|
||||
);
|
||||
const newNode = buildNodeWithSN(newSn, {
|
||||
doc: target.ownerDocument as Document, // can be Document or RRDocument
|
||||
mirror: mirror as Mirror,
|
||||
skipChild: true,
|
||||
hackCss: true,
|
||||
cache: this.cache,
|
||||
});
|
||||
const siblingNode = target.nextSibling;
|
||||
const parentNode = target.parentNode;
|
||||
if (newNode && parentNode) {
|
||||
parentNode.removeChild(target as Node & RRNode);
|
||||
parentNode.insertBefore(
|
||||
newNode as Node & RRNode,
|
||||
siblingNode as (Node & RRNode) | null,
|
||||
);
|
||||
mirror.replace(mutation.id, newNode as Node & RRNode);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// for safe
|
||||
}
|
||||
}
|
||||
(target as Element | RRElement).setAttribute(
|
||||
attributeName,
|
||||
value,
|
||||
|
||||
@@ -173,9 +173,12 @@ exports[`record captures CORS stylesheets that are still loading 1`] = `
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
@@ -188,10 +191,7 @@ exports[`record captures CORS stylesheets that are still loading 1`] = `
|
||||
\\"id\\": 9
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": []
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
@@ -2007,7 +2007,19 @@ exports[`record captures stylesheets in iframes that are still loading 1`] = `
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"link\\",
|
||||
\\"attributes\\": {
|
||||
\\"rel\\": \\"stylesheet\\",
|
||||
\\"href\\": \\"blob:null\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 10,
|
||||
\\"id\\": 13
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 10,
|
||||
\\"id\\": 12
|
||||
},
|
||||
@@ -2039,25 +2051,17 @@ exports[`record captures stylesheets in iframes that are still loading 1`] = `
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 13,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"link\\",
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 10,
|
||||
\\"id\\": 13
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"adds\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": []
|
||||
\\"attributes\\": [
|
||||
{
|
||||
\\"id\\": 13,
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
@@ -2286,24 +2290,42 @@ exports[`record captures stylesheets that are still loading 1`] = `
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"parentId\\": 4,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"link\\",
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||
\\"rel\\": \\"stylesheet\\",
|
||||
\\"href\\": \\"blob:null\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": []
|
||||
\\"attributes\\": [
|
||||
{
|
||||
\\"id\\": 9,
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { color: pink; }\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
|
||||
@@ -626,8 +626,11 @@ describe('record', function (this: ISuite) {
|
||||
|
||||
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
assertSnapshot(ctx.events);
|
||||
// 'blob' URL is different in every execution so we need to remove it from the snapshot.
|
||||
const filteredEvents = JSON.parse(
|
||||
JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'),
|
||||
);
|
||||
assertSnapshot(filteredEvents);
|
||||
});
|
||||
|
||||
it('captures stylesheets in iframes that are still loading', async () => {
|
||||
@@ -659,8 +662,10 @@ describe('record', function (this: ISuite) {
|
||||
|
||||
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
|
||||
await waitForRAF(ctx.page);
|
||||
|
||||
assertSnapshot(ctx.events);
|
||||
const filteredEvents = JSON.parse(
|
||||
JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'),
|
||||
);
|
||||
assertSnapshot(filteredEvents);
|
||||
});
|
||||
|
||||
it('captures CORS stylesheets that are still loading', async () => {
|
||||
|
||||
Reference in New Issue
Block a user