diff --git a/.changeset/single-style-capture.md b/.changeset/single-style-capture.md
new file mode 100644
index 0000000000..96f81ed621
--- /dev/null
+++ b/.changeset/single-style-capture.md
@@ -0,0 +1,6 @@
+---
+"rrweb-snapshot": patch
+"rrweb": patch
+---
+
+Edge case: Provide support for mutations on a `).querySelector('style');
+ if (style) {
+ // as authored, e.g. no spaces
+ style.appendChild(JSDOM.fragment('.a{background-color:red;}'));
+ style.appendChild(JSDOM.fragment('.a{background-color:black;}'));
+
+ // how it is currently stringified (spaces present)
+ let browserSheet = '.a { background-color: red; }';
+ let expectedSplit = browserSheet.length;
+ browserSheet += '.a { background-color: black; }';
+
+ // can't do this as JSDOM doesn't have style.sheet
+ //expect(stringifyStylesheet(style.sheet!)).toEqual(browserSheet);
+
+ expect(findCssTextSplits(browserSheet, style)).toEqual([
+ expectedSplit,
+ browserSheet.length,
+ ]);
+ }
+ });
+
+ it('finds css textElement splits correctly when vendor prefixed rules have been removed', () => {
+ const style = JSDOM.fragment(``).querySelector('style');
+ if (style) {
+ // as authored, with newlines
+ style.appendChild(
+ JSDOM.fragment(`.x {
+ -webkit-transition: all 4s ease;
+ content: 'try to keep a newline';
+ transition: all 4s ease;
+}`),
+ );
+ // TODO: findCssTextSplits can't handle it yet if both start with .x
+ style.appendChild(
+ JSDOM.fragment(`.y {
+ -moz-transition: all 5s ease;
+ transition: all 5s ease;
+}`),
+ );
+ // browser .rules would usually omit the vendored versions and modifies the transition value
+ let browserSheet =
+ '.x { content: "try to keep a newline"; background: red; transition: 4s; }';
+ let expectedSplit = browserSheet.length;
+ browserSheet += '.y { transition: 5s; }';
+
+ // can't do this as JSDOM doesn't have style.sheet
+ //expect(stringifyStylesheet(style.sheet!)).toEqual(browserSheet);
+
+ expect(findCssTextSplits(browserSheet, style)).toEqual([
+ expectedSplit,
+ browserSheet.length,
+ ]);
+ }
+ });
+});
+
+describe('applyCssSplits css rejoiner', function () {
+ const mockLastUnusedArg = null as unknown as BuildCache;
+ const halfCssText = '.a { background-color: red; }';
+ const otherHalfCssText = halfCssText.replace('.a', '.x');
+ const fullCssText = halfCssText + otherHalfCssText;
+ let sn: serializedElementNodeWithId;
+
+ beforeEach(() => {
+ sn = {
+ type: NodeType.Element,
+ tagName: 'style',
+ childNodes: [
+ {
+ type: NodeType.Text,
+ textContent: '',
+ },
+ {
+ type: NodeType.Text,
+ textContent: '',
+ },
+ ],
+ } as serializedElementNodeWithId;
+ });
+
+ it('applies css splits correctly', () => {
+ // happy path
+ applyCssSplits(
+ sn,
+ fullCssText,
+ [halfCssText.length, fullCssText.length],
+ false,
+ mockLastUnusedArg,
+ );
+ expect((sn.childNodes[0] as textNode).textContent).toEqual(halfCssText);
+ expect((sn.childNodes[1] as textNode).textContent).toEqual(
+ otherHalfCssText,
+ );
+ });
+
+ it('applies css splits correctly even when there are too many child nodes', () => {
+ let sn3 = {
+ type: NodeType.Element,
+ tagName: 'style',
+ childNodes: [
+ {
+ type: NodeType.Text,
+ textContent: '',
+ },
+ {
+ type: NodeType.Text,
+ textContent: '',
+ },
+ {
+ type: NodeType.Text,
+ textContent: '',
+ },
+ ],
+ } as serializedElementNodeWithId;
+ applyCssSplits(
+ sn3,
+ fullCssText,
+ [halfCssText.length, fullCssText.length],
+ false,
+ mockLastUnusedArg,
+ );
+ expect((sn3.childNodes[0] as textNode).textContent).toEqual(halfCssText);
+ expect((sn3.childNodes[1] as textNode).textContent).toEqual(
+ otherHalfCssText,
+ );
+ expect((sn3.childNodes[2] as textNode).textContent).toEqual('');
+ });
+
+ it('maintains entire css text when there are too few child nodes', () => {
+ let sn1 = {
+ type: NodeType.Element,
+ tagName: 'style',
+ childNodes: [
+ {
+ type: NodeType.Text,
+ textContent: '',
+ },
+ ],
+ } as serializedElementNodeWithId;
+ applyCssSplits(
+ sn1,
+ fullCssText,
+ [halfCssText.length, fullCssText.length],
+ false,
+ mockLastUnusedArg,
+ );
+ expect((sn1.childNodes[0] as textNode).textContent).toEqual(fullCssText);
+ });
+
+ it('ignores css splits correctly when there is a mismatch in length check', () => {
+ applyCssSplits(sn, fullCssText, [2, 3], false, mockLastUnusedArg);
+ expect((sn.childNodes[0] as textNode).textContent).toEqual(fullCssText);
+ expect((sn.childNodes[1] as textNode).textContent).toEqual('');
+ });
+
+ it('ignores css splits correctly when we indicate a split is invalid with the zero marker', () => {
+ applyCssSplits(
+ sn,
+ fullCssText,
+ [0, fullCssText.length],
+ false,
+ mockLastUnusedArg,
+ );
+ expect((sn.childNodes[0] as textNode).textContent).toEqual(fullCssText);
+ expect((sn.childNodes[1] as textNode).textContent).toEqual('');
+ });
+
+ it('ignores css splits correctly with negative splits', () => {
+ applyCssSplits(sn, fullCssText, [-2, -4], false, mockLastUnusedArg);
+ expect((sn.childNodes[0] as textNode).textContent).toEqual(fullCssText);
+ expect((sn.childNodes[1] as textNode).textContent).toEqual('');
+ });
+
+ it('ignores css splits correctly with out of order splits', () => {
+ applyCssSplits(
+ sn,
+ fullCssText,
+ [fullCssText.length * 2, fullCssText.length],
+ false,
+ mockLastUnusedArg,
+ );
+ expect((sn.childNodes[0] as textNode).textContent).toEqual(fullCssText);
+ expect((sn.childNodes[1] as textNode).textContent).toEqual('');
+ });
+});
diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts
index aa4bb428ee..54d058a6d9 100644
--- a/packages/rrweb-snapshot/test/snapshot.test.ts
+++ b/packages/rrweb-snapshot/test/snapshot.test.ts
@@ -161,22 +161,27 @@ describe('style elements', () => {
it('should serialize all rules of stylesheet when the sheet has a single child node', () => {
const styleEl = render(``);
styleEl.sheet?.insertRule('section { color: blue; }');
- expect(serializeNode(styleEl.childNodes[0])).toMatchObject({
- isStyle: true,
+ expect(serializeNode(styleEl)).toMatchObject({
rootId: undefined,
- textContent: 'section {color: blue;}body {color: red;}',
- type: 3,
+ attributes: {
+ _cssText: 'section {color: blue;}body {color: red;}',
+ },
+ type: 2,
});
});
- it('should serialize individual text nodes on stylesheets with multiple child nodes', () => {
+ it('should serialize all rules on stylesheets with mix of insertion type', () => {
const styleEl = render(``);
+ styleEl.sheet?.insertRule('section.lost { color: unseeable; }'); // browser throws this away after append
styleEl.append(document.createTextNode('section { color: blue; }'));
- expect(serializeNode(styleEl.childNodes[1])).toMatchObject({
- isStyle: true,
+ styleEl.sheet?.insertRule('section.working { color: pink; }');
+ expect(serializeNode(styleEl)).toMatchObject({
rootId: undefined,
- textContent: 'section { color: blue; }',
- type: 3,
+ attributes: {
+ _cssText:
+ 'section.working {color: pink;}body {color: red;}section {color: blue;}',
+ },
+ type: 2,
});
});
});
diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts
index 484a8a7ecb..387199c01d 100644
--- a/packages/rrweb/src/record/index.ts
+++ b/packages/rrweb/src/record/index.ts
@@ -282,6 +282,7 @@ function record(
});
const stylesheetManager = new StylesheetManager({
+ mirror,
mutationCb: wrappedMutationEmit,
adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit,
});
@@ -385,7 +386,7 @@ function record(
if (isSerializedIframe(n, mirror)) {
iframeManager.addIframe(n as HTMLIFrameElement);
}
- if (isSerializedStylesheet(n, mirror)) {
+ if (inlineStylesheet && isSerializedStylesheet(n, mirror)) {
stylesheetManager.trackLinkElement(n as HTMLLinkElement);
}
if (hasShadowRoot(n)) {
@@ -396,9 +397,6 @@ function record(
iframeManager.attachIframe(iframe, childSn);
shadowDomManager.observeAttachShadow(iframe);
},
- onStylesheetLoad: (linkEl, childSn) => {
- stylesheetManager.attachLinkElement(linkEl, childSn);
- },
keepIframeSrcFn,
});
diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts
index f4267af340..2d3a634862 100644
--- a/packages/rrweb/src/record/mutation.ts
+++ b/packages/rrweb/src/record/mutation.ts
@@ -320,7 +320,10 @@ export default class MutationBuffer {
if (isSerializedIframe(currentN, this.mirror)) {
this.iframeManager.addIframe(currentN as HTMLIFrameElement);
}
- if (isSerializedStylesheet(currentN, this.mirror)) {
+ if (
+ this.inlineStylesheet &&
+ isSerializedStylesheet(currentN, this.mirror)
+ ) {
this.stylesheetManager.trackLinkElement(
currentN as HTMLLinkElement,
);
@@ -333,9 +336,6 @@ export default class MutationBuffer {
this.iframeManager.attachIframe(iframe, childSn);
this.shadowDomManager.observeAttachShadow(iframe);
},
- onStylesheetLoad: (link, childSn) => {
- this.stylesheetManager.attachLinkElement(link, childSn);
- },
});
if (sn) {
adds.push({
@@ -663,6 +663,20 @@ export default class MutationBuffer {
item.styleDiff[pname] = false; // delete
}
}
+ } else if (
+ this.inlineStylesheet &&
+ attributeName === 'rel' &&
+ value.toLowerCase() === 'stylesheet' &&
+ m.target.tagName === 'LINK'
+ ) {
+ if (m.target.sheet) {
+ console.warn(
+ 'have we missed the onload event due to delayed mutation?',
+ );
+ }
+ this.stylesheetManager.trackLinkElement(
+ m.target as HTMLLinkElement,
+ );
}
}
break;
diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts
index 6e0a8077b4..7a55ba342f 100644
--- a/packages/rrweb/src/record/stylesheet-manager.ts
+++ b/packages/rrweb/src/record/stylesheet-manager.ts
@@ -1,5 +1,10 @@
import type { elementNode, serializedNodeWithId } from 'rrweb-snapshot';
-import { stringifyRule } from 'rrweb-snapshot';
+import {
+ stringifyRule,
+ stringifyStylesheet,
+ absoluteToStylesheet,
+ getHref,
+} from 'rrweb-snapshot';
import type {
adoptedStyleSheetCallback,
adoptedStyleSheetParam,
@@ -13,39 +18,20 @@ export class StylesheetManager {
private mutationCb: mutationCallBack;
private adoptedStyleSheetCb: adoptedStyleSheetCallback;
public styleMirror = new StyleSheetMirror();
+ private mirror: Mirror;
constructor(options: {
+ mirror: Mirror;
mutationCb: mutationCallBack;
adoptedStyleSheetCb: adoptedStyleSheetCallback;
}) {
this.mutationCb = options.mutationCb;
this.adoptedStyleSheetCb = options.adoptedStyleSheetCb;
- }
-
- public attachLinkElement(
- linkEl: HTMLLinkElement,
- childSn: serializedNodeWithId,
- ) {
- 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);
+ this.mirror = options.mirror;
}
public trackLinkElement(linkEl: HTMLLinkElement) {
if (this.trackedLinkElements.has(linkEl)) return;
-
this.trackedLinkElements.add(linkEl);
this.trackStylesheetInLinkElement(linkEl);
}
@@ -80,10 +66,41 @@ export class StylesheetManager {
this.trackedLinkElements = new WeakSet();
}
- // TODO: take snapshot on stylesheet reload by applying event listener
private trackStylesheetInLinkElement(linkEl: HTMLLinkElement) {
- // linkEl.addEventListener('load', () => {
- // // re-loaded, maybe take another snapshot?
- // });
+ // if is already loaded, it will have .sheet available and that
+ // will get serialized in the snapshot. The following is for when that doesn't happen
+ linkEl.addEventListener('load', () => {
+ console.log('load');
+ if (!linkEl.sheet) {
+ return;
+ }
+ const id = this.mirror.getId(linkEl);
+ if (!id) {
+ // disappeared? should warn?
+ return;
+ }
+ let _cssText = stringifyStylesheet(linkEl.sheet);
+ if (_cssText) {
+ console.log('load emit');
+ _cssText = absoluteToStylesheet(
+ _cssText,
+ getHref(linkEl.ownerDocument),
+ );
+ // TODO: compare _cssText with previous emission
+ this.mutationCb({
+ adds: [],
+ removes: [],
+ texts: [],
+ attributes: [
+ {
+ id,
+ attributes: {
+ _cssText,
+ },
+ },
+ ],
+ });
+ }
+ });
}
}
diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts
index 89aa66d933..03696f385b 100644
--- a/packages/rrweb/src/replay/index.ts
+++ b/packages/rrweb/src/replay/index.ts
@@ -869,6 +869,9 @@ export class Replayer {
'html.rrweb-paused *, html.rrweb-paused *:before, html.rrweb-paused *:after { animation-play-state: paused !important; }',
);
}
+ if (!injectStylesRules.length) {
+ return;
+ }
if (this.usingVirtualDom) {
const styleEl = this.virtualDom.createElement('style');
this.virtualDom.mirror.add(
diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
index f349bd2669..411378d8eb 100644
--- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
@@ -1034,6 +1034,585 @@ exports[`record integration tests can mask character data mutations with regexp
]"
`;
+exports[`record integration tests can record and replay style mutations 1`] = `
+"[
+ {
+ \\"type\\": 0,
+ \\"data\\": {}
+ },
+ {
+ \\"type\\": 1,
+ \\"data\\": {}
+ },
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"style\\",
+ \\"id\\": 11
+ }
+ ],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 12
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"style\\",
+ \\"attributes\\": {
+ \\"id\\": \\"dual-textContent\\",
+ \\"_cssText\\": \\"body { background-color: black; }body { color: orange !important; }\\",
+ \\"_cssTextSplits\\": \\"33 67\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\",
+ \\"id\\": 14
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\",
+ \\"id\\": 15
+ }
+ ],
+ \\"id\\": 13
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 16
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 18
+ }
+ ],
+ \\"id\\": 17
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 19
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"style\\",
+ \\"attributes\\": {
+ \\"id\\": \\"single-textContent\\",
+ \\"_cssText\\": \\"a:hover { outline: red solid 1px; }\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\",
+ \\"id\\": 21
+ }
+ ],
+ \\"id\\": 20
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 22
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"style\\",
+ \\"attributes\\": {
+ \\"id\\": \\"empty\\",
+ \\"_cssText\\": \\"a:hover { outline: blue solid 1px; }\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 23
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 24
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 26
+ }
+ ],
+ \\"id\\": 25
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 27
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 28
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\",
+ \\"id\\": 30
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 32
+ }
+ ],
+ \\"id\\": 31
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
+ \\"id\\": 33
+ }
+ ],
+ \\"id\\": 29
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 13,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 3,
+ \\"textContent\\": \\".absolutify { background-image: url(\\\\\\"http://localhost:3030/rel\\\\\\"); }\\",
+ \\"id\\": 34
+ }
+ },
+ {
+ \\"parentId\\": 13,
+ \\"nextId\\": 34,
+ \\"node\\": {
+ \\"type\\": 3,
+ \\"textContent\\": \\"body { background-color: darkgreen; }\\",
+ \\"id\\": 35
+ }
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [
+ {
+ \\"id\\": 15,
+ \\"value\\": \\"body { color: yellow; }\\"
+ },
+ {
+ \\"id\\": 15,
+ \\"value\\": \\"body { color: yellow; }\\"
+ },
+ {
+ \\"id\\": 35,
+ \\"value\\": \\"body { background-color: purple; }\\"
+ }
+ ],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [
+ {
+ \\"id\\": 14,
+ \\"value\\": \\"\\\\n body { background-color: black !important; }\\\\n \\"
+ }
+ ],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": []
+ }
+ }
+]"
+`;
+
+exports[`record integration tests can record and replay textarea mutations correctly 1`] = `
+"[
+ {
+ \\"type\\": 0,
+ \\"data\\": {}
+ },
+ {
+ \\"type\\": 1,
+ \\"data\\": {}
+ },
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Empty\\",
+ \\"id\\": 11
+ }
+ ],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 12
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 13
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"div\\",
+ \\"attributes\\": {
+ \\"id\\": \\"one\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 16
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\",
+ \\"id\\": 17
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 19
+ }
+ ],
+ \\"id\\": 18
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
+ \\"id\\": 20
+ }
+ ],
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 14,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"textarea\\",
+ \\"attributes\\": {
+ \\"value\\": \\"pre value\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 21
+ }
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 21,
+ \\"attributes\\": {
+ \\"value\\": \\"ok\\"
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 21,
+ \\"attributes\\": {
+ \\"value\\": \\"ok3\\"
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 5,
+ \\"id\\": 21
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"1ok3\\",
+ \\"isChecked\\": false,
+ \\"id\\": 21
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 21,
+ \\"attributes\\": {
+ \\"value\\": \\"ignore\\"
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"12ok3\\",
+ \\"isChecked\\": false,
+ \\"id\\": 21
+ }
+ }
+]"
+`;
+
exports[`record integration tests can record attribute mutation 1`] = `
"[
{
@@ -3556,255 +4135,27 @@ exports[`record integration tests can record node mutations 1`] = `
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"\\",
- \\"isChecked\\": false,
- \\"id\\": 35
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 2,
- \\"type\\": 0,
- \\"id\\": 70
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 36,
- \\"attributes\\": {
- \\"style\\": {
- \\"color\\": [
- \\"black\\",
- \\"important\\"
- ]
- }
- }
- }
- ],
- \\"removes\\": [],
- \\"adds\\": []
- }
- }
-]"
-`;
-
-exports[`record integration tests can record style changes compactly and preserve css var() functions 1`] = `
-"[
- {
- \\"type\\": 0,
- \\"data\\": {}
- },
- {
- \\"type\\": 1,
- \\"data\\": {}
- },
- {
- \\"type\\": 4,
- \\"data\\": {
- \\"href\\": \\"about:blank\\",
- \\"width\\": 1920,
- \\"height\\": 1080
- }
- },
- {
- \\"type\\": 2,
- \\"data\\": {
- \\"node\\": {
- \\"type\\": 0,
- \\"childNodes\\": [
- {
- \\"type\\": 2,
- \\"tagName\\": \\"html\\",
- \\"attributes\\": {},
- \\"childNodes\\": [
- {
- \\"type\\": 2,
- \\"tagName\\": \\"head\\",
- \\"attributes\\": {},
- \\"childNodes\\": [],
- \\"id\\": 3
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"body\\",
- \\"attributes\\": {},
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 5
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"script\\",
- \\"attributes\\": {},
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
- \\"id\\": 7
- }
- ],
- \\"id\\": 6
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
- \\"id\\": 8
- }
- ],
- \\"id\\": 4
- }
- ],
- \\"id\\": 2
- }
- ],
- \\"compatMode\\": \\"BackCompat\\",
- \\"id\\": 1
- },
- \\"initialOffset\\": {
- \\"left\\": 0,
- \\"top\\": 0
- }
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 4,
- \\"attributes\\": {
- \\"style\\": \\"background: var(--mystery)\\"
- }
- }
- ],
- \\"removes\\": [],
- \\"adds\\": []
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 4,
- \\"attributes\\": {
- \\"style\\": \\"background: var(--mystery); background-color: black\\"
- }
- }
- ],
- \\"removes\\": [],
- \\"adds\\": []
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 4,
- \\"attributes\\": {
- \\"style\\": \\"\\"
- }
- }
- ],
- \\"removes\\": [],
- \\"adds\\": []
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 4,
- \\"attributes\\": {
- \\"style\\": \\"display:block\\"
- }
- }
- ],
- \\"removes\\": [],
- \\"adds\\": []
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 4,
- \\"attributes\\": {
- \\"style\\": {
- \\"color\\": \\"var(--mystery-color)\\"
- }
- }
- }
- ],
- \\"removes\\": [],
- \\"adds\\": []
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 4,
- \\"attributes\\": {
- \\"style\\": \\"color:var(--mystery-color);display:block;margin:10px\\"
- }
- }
- ],
- \\"removes\\": [],
- \\"adds\\": []
+ \\"source\\": 5,
+ \\"text\\": \\"\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 4,
- \\"attributes\\": {
- \\"style\\": {
- \\"margin-left\\": \\"Npx\\"
- }
- }
- }
- ],
- \\"removes\\": [],
- \\"adds\\": []
+ \\"source\\": 5,
+ \\"text\\": \\"\\",
+ \\"isChecked\\": false,
+ \\"id\\": 35
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 2,
+ \\"type\\": 0,
+ \\"id\\": 70
}
},
{
@@ -3814,11 +4165,13 @@ exports[`record integration tests can record style changes compactly and preserv
\\"texts\\": [],
\\"attributes\\": [
{
- \\"id\\": 4,
+ \\"id\\": 36,
\\"attributes\\": {
\\"style\\": {
- \\"margin-top\\": \\"Npx\\",
- \\"color\\": false
+ \\"color\\": [
+ \\"black\\",
+ \\"important\\"
+ ]
}
}
}
@@ -3830,7 +4183,7 @@ exports[`record integration tests can record style changes compactly and preserv
]"
`;
-exports[`record integration tests can record textarea mutations correctly 1`] = `
+exports[`record integration tests can record style changes compactly and preserve css var() functions 1`] = `
"[
{
\\"type\\": 0,
@@ -3854,84 +4207,17 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
- {
- \\"type\\": 1,
- \\"name\\": \\"html\\",
- \\"publicId\\": \\"\\",
- \\"systemId\\": \\"\\",
- \\"id\\": 2
- },
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
- \\"attributes\\": {
- \\"lang\\": \\"en\\"
- },
+ \\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 5
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"meta\\",
- \\"attributes\\": {
- \\"charset\\": \\"UTF-8\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 6
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 7
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"meta\\",
- \\"attributes\\": {
- \\"name\\": \\"viewport\\",
- \\"content\\": \\"width=device-width, initial-scale=1.0\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 8
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 9
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"title\\",
- \\"attributes\\": {},
- \\"childNodes\\": [
- {
- \\"type\\": 3,
- \\"textContent\\": \\"Empty\\",
- \\"id\\": 11
- }
- ],
- \\"id\\": 10
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 12
- }
- ],
- \\"id\\": 4
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\",
- \\"id\\": 13
+ \\"childNodes\\": [],
+ \\"id\\": 3
},
{
\\"type\\": 2,
@@ -3941,21 +4227,7 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
- \\"id\\": 15
- },
- {
- \\"type\\": 2,
- \\"tagName\\": \\"div\\",
- \\"attributes\\": {
- \\"id\\": \\"one\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 16
- },
- {
- \\"type\\": 3,
- \\"textContent\\": \\"\\\\n \\\\n \\",
- \\"id\\": 17
+ \\"id\\": 5
},
{
\\"type\\": 2,
@@ -3965,23 +4237,24 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
- \\"id\\": 19
+ \\"id\\": 7
}
],
- \\"id\\": 18
+ \\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
- \\"id\\": 20
+ \\"id\\": 8
}
],
- \\"id\\": 14
+ \\"id\\": 4
}
],
- \\"id\\": 3
+ \\"id\\": 2
}
],
+ \\"compatMode\\": \\"BackCompat\\",
\\"id\\": 1
},
\\"initialOffset\\": {
@@ -3995,23 +4268,33 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
- \\"attributes\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 4,
+ \\"attributes\\": {
+ \\"style\\": \\"background: var(--mystery)\\"
+ }
+ }
+ ],
\\"removes\\": [],
- \\"adds\\": [
+ \\"adds\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
{
- \\"parentId\\": 14,
- \\"nextId\\": null,
- \\"node\\": {
- \\"type\\": 2,
- \\"tagName\\": \\"textarea\\",
- \\"attributes\\": {
- \\"value\\": \\"pre value\\"
- },
- \\"childNodes\\": [],
- \\"id\\": 21
+ \\"id\\": 4,
+ \\"attributes\\": {
+ \\"style\\": \\"background: var(--mystery); background-color: black\\"
}
}
- ]
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
},
{
@@ -4021,9 +4304,9 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
\\"texts\\": [],
\\"attributes\\": [
{
- \\"id\\": 21,
+ \\"id\\": 4,
\\"attributes\\": {
- \\"value\\": \\"ok\\"
+ \\"style\\": \\"\\"
}
}
],
@@ -4038,9 +4321,9 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
\\"texts\\": [],
\\"attributes\\": [
{
- \\"id\\": 21,
+ \\"id\\": 4,
\\"attributes\\": {
- \\"value\\": \\"ok3\\"
+ \\"style\\": \\"display:block\\"
}
}
],
@@ -4051,18 +4334,37 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 2,
- \\"type\\": 5,
- \\"id\\": 21
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 4,
+ \\"attributes\\": {
+ \\"style\\": {
+ \\"color\\": \\"var(--mystery-color)\\"
+ }
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
},
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"1ok3\\",
- \\"isChecked\\": false,
- \\"id\\": 21
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 4,
+ \\"attributes\\": {
+ \\"style\\": \\"color:var(--mystery-color);display:block;margin:10px\\"
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
},
{
@@ -4072,9 +4374,11 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
\\"texts\\": [],
\\"attributes\\": [
{
- \\"id\\": 21,
+ \\"id\\": 4,
\\"attributes\\": {
- \\"value\\": \\"ignore\\"
+ \\"style\\": {
+ \\"margin-left\\": \\"Npx\\"
+ }
}
}
],
@@ -4085,10 +4389,21 @@ exports[`record integration tests can record textarea mutations correctly 1`] =
{
\\"type\\": 3,
\\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"12ok3\\",
- \\"isChecked\\": false,
- \\"id\\": 21
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 4,
+ \\"attributes\\": {
+ \\"style\\": {
+ \\"margin-top\\": \\"Npx\\",
+ \\"color\\": false
+ }
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
}
}
]"
@@ -5216,12 +5531,13 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
- \\"attributes\\": {},
+ \\"attributes\\": {
+ \\"_cssText\\": \\"#b-class, #b-class-2 { height: 33px; width: 200px; }\\"
+ },
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\"#b-class, #b-class-2 { height: 33px; width: 200px; }\\",
- \\"isStyle\\": true,
+ \\"textContent\\": \\"\\",
\\"id\\": 9
}
],
@@ -8112,12 +8428,13 @@ exports[`record integration tests should nest record iframe 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
- \\"attributes\\": {},
+ \\"attributes\\": {
+ \\"_cssText\\": \\"iframe { width: 500px; height: 500px; }\\"
+ },
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\"iframe { width: 500px; height: 500px; }\\",
- \\"isStyle\\": true,
+ \\"textContent\\": \\"\\",
\\"id\\": 14
}
],
@@ -12160,7 +12477,6 @@ exports[`record integration tests should record dynamic CSS changes 1`] = `
{
\\"type\\": 3,
\\"textContent\\": \\"\\",
- \\"isStyle\\": true,
\\"id\\": 18
}
],
@@ -15530,12 +15846,13 @@ exports[`record integration tests should record shadow DOM 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
- \\"attributes\\": {},
+ \\"attributes\\": {
+ \\"_cssText\\": \\".my-element { margin: 0px 0px 1rem; }iframe { border: 0px; width: 100%; padding: 0px; }body { max-width: 400px; margin: 1rem auto; padding: 0px 1rem; font-family: \\\\\\"comic sans ms\\\\\\"; }\\"
+ },
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\".my-element { margin: 0px 0px 1rem; }iframe { border: 0px; width: 100%; padding: 0px; }body { max-width: 400px; margin: 1rem auto; padding: 0px 1rem; font-family: \\\\\\"comic sans ms\\\\\\"; }\\",
- \\"isStyle\\": true,
+ \\"textContent\\": \\"\\",
\\"id\\": 14
}
],
@@ -15613,12 +15930,13 @@ exports[`record integration tests should record shadow DOM 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
- \\"attributes\\": {},
+ \\"attributes\\": {
+ \\"_cssText\\": \\"body { margin: 0px; }p { border: 1px solid rgb(204, 204, 204); padding: 1rem; color: red; font-family: sans-serif; }\\"
+ },
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\"body { margin: 0px; }p { border: 1px solid rgb(204, 204, 204); padding: 1rem; color: red; font-family: sans-serif; }\\",
- \\"isStyle\\": true,
+ \\"textContent\\": \\"\\",
\\"id\\": 28
}
],
diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap
index cb40f3328d..d7a7b9319b 100644
--- a/packages/rrweb/test/__snapshots__/record.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap
@@ -1386,18 +1386,19 @@ exports[`record captures inserted style text nodes correctly 1`] = `
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
- \\"attributes\\": {},
+ \\"attributes\\": {
+ \\"_cssText\\": \\"div { color: red; }section { color: blue; }\\",
+ \\"_cssTextSplits\\": \\"19 43\\"
+ },
\\"childNodes\\": [
{
\\"type\\": 3,
- \\"textContent\\": \\"div { color: red; }\\",
- \\"isStyle\\": true,
+ \\"textContent\\": \\"\\",
\\"id\\": 6
},
{
\\"type\\": 3,
- \\"textContent\\": \\"section { color: blue; }\\",
- \\"isStyle\\": true,
+ \\"textContent\\": \\"\\",
\\"id\\": 7
}
],
@@ -1460,7 +1461,6 @@ exports[`record captures inserted style text nodes correctly 1`] = `
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"h1 { color: pink; }\\",
- \\"isStyle\\": true,
\\"id\\": 12
}
},
@@ -1470,7 +1470,6 @@ exports[`record captures inserted style text nodes correctly 1`] = `
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"span { color: orange; }\\",
- \\"isStyle\\": true,
\\"id\\": 13
}
}
@@ -3255,6 +3254,204 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti
]"
`;
+exports[`record loading stylesheets captures stylesheets that are preloaded 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"http-equiv\\": \\"X-UA-Compatible\\",
+ \\"content\\": \\"IE=edge\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 11
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Hello World!\\",
+ \\"id\\": 13
+ }
+ ],
+ \\"id\\": 12
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
+ \\"id\\": 17
+ }
+ ],
+ \\"id\\": 16
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 4,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"link\\",
+ \\"attributes\\": {
+ \\"rel\\": \\"preload\\",
+ \\"as\\": \\"style\\",
+ \\"href\\": \\"http://localhost:3030/html/assets/style.css\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 18
+ }
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 18,
+ \\"attributes\\": {
+ \\"rel\\": \\"stylesheet\\"
+ }
+ }
+ ],
+ \\"removes\\": [],
+ \\"adds\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"adds\\": [],
+ \\"removes\\": [],
+ \\"texts\\": [],
+ \\"attributes\\": [
+ {
+ \\"id\\": 18,
+ \\"attributes\\": {
+ \\"_cssText\\": \\"body { color: pink; }\\"
+ }
+ }
+ ]
+ }
+ }
+]"
+`;
+
exports[`record loading stylesheets captures stylesheets that are still loading 1`] = `
"[
{
@@ -3435,6 +3632,169 @@ exports[`record loading stylesheets captures stylesheets that are still loading
]"
`;
+exports[`record loading stylesheets respects inlineStylesheet=false for late loading stylesheets 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"http-equiv\\": \\"X-UA-Compatible\\",
+ \\"content\\": \\"IE=edge\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 11
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"Hello World!\\",
+ \\"id\\": 13
+ }
+ ],
+ \\"id\\": 12
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
+ \\"id\\": 17
+ }
+ ],
+ \\"id\\": 16
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 4,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"link\\",
+ \\"attributes\\": {
+ \\"rel\\": \\"stylesheet\\",
+ \\"href\\": \\"http://localhost:3030/html/assets/style.css\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 18
+ }
+ }
+ ]
+ }
+ }
+]"
+`;
+
exports[`record no need for attribute mutations on adds 1`] = `
"[
{
diff --git a/packages/rrweb/test/html/style.html b/packages/rrweb/test/html/style.html
new file mode 100644
index 0000000000..b9c6975672
--- /dev/null
+++ b/packages/rrweb/test/html/style.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ style
+
+
+
+
+
+
+
+
+
diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts
index 5ce9b76469..bc2c26bbc3 100644
--- a/packages/rrweb/test/integration.test.ts
+++ b/packages/rrweb/test/integration.test.ts
@@ -105,7 +105,7 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});
- it('can record textarea mutations correctly', async () => {
+ it('can record and replay textarea mutations correctly', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'empty.html'));
@@ -170,6 +170,101 @@ describe('record integration tests', function (this: ISuite) {
]);
});
+ it('can record and replay style mutations', async () => {
+ // This test shows that the `isStyle` attribute on textContent is not needed in a mutation
+ // TODO: we could get a lot more elaborate here with mixed textContent and insertRule mutations
+ const page: puppeteer.Page = await browser.newPage();
+ await page.goto(`${serverURL}/html`);
+ await page.setContent(getHtml.call(this, 'style.html'));
+
+ await waitForRAF(page); // ensure mutations aren't included in fullsnapshot
+
+ await page.evaluate(() => {
+ let styleEl = document.querySelector('style');
+ if (styleEl) {
+ styleEl.append(
+ document.createTextNode('body { background-color: darkgreen; }'),
+ );
+ styleEl.append(
+ document.createTextNode(
+ '.absolutify { background-image: url("./rel"); }',
+ ),
+ );
+ }
+ });
+ await page.waitForTimeout(5);
+ await page.evaluate(() => {
+ let styleEl = document.querySelector('style');
+ if (styleEl) {
+ styleEl.childNodes.forEach((cn) => {
+ if (cn.textContent) {
+ cn.textContent = cn.textContent.replace('darkgreen', 'purple');
+ cn.textContent = cn.textContent.replace(
+ 'orange !important',
+ 'yellow',
+ );
+ }
+ });
+ }
+ });
+ await page.waitForTimeout(5);
+ await page.evaluate(() => {
+ let styleEl = document.querySelector('style');
+ if (styleEl) {
+ styleEl.childNodes.forEach((cn) => {
+ if (cn.textContent) {
+ cn.textContent = cn.textContent.replace(
+ 'black',
+ 'black !important',
+ );
+ }
+ });
+ }
+ });
+
+ const snapshots = (await page.evaluate(
+ 'window.snapshots',
+ )) as eventWithTime[];
+
+ // following ensures that the ./rel url has been absolutized (in a mutation)
+ assertSnapshot(snapshots);
+
+ // check after each mutation and text input
+ const replayStyleValues = await page.evaluate(`
+ const { Replayer } = rrweb;
+ const replayer = new Replayer(window.snapshots);
+ const vals = [];
+ window.snapshots.filter((e)=>e.data.attributes || e.data.source === 5).forEach((e)=>{
+ replayer.pause((e.timestamp - window.snapshots[0].timestamp)+1);
+ let bodyStyle = getComputedStyle(replayer.iframe.contentDocument.querySelector('body'))
+ vals.push({
+ 'background-color': bodyStyle['background-color'],
+ 'color': bodyStyle['color'],
+ });
+ });
+ vals.push(replayer.iframe.contentDocument.getElementById('single-textContent').innerText);
+ vals.push(replayer.iframe.contentDocument.getElementById('empty').innerText);
+ vals;
+`);
+
+ expect(replayStyleValues).toEqual([
+ {
+ 'background-color': 'rgb(0, 100, 0)', // darkgreen
+ color: 'rgb(255, 165, 0)', // orange (from style.html)
+ },
+ {
+ 'background-color': 'rgb(128, 0, 128)', // purple
+ color: 'rgb(255, 255, 0)', // yellow
+ },
+ {
+ 'background-color': 'rgb(0, 0, 0)', // black !important
+ color: 'rgb(255, 255, 0)', // yellow
+ },
+ 'a:hover, a.\\:hover { outline: red solid 1px; }', // has run adaptCssForReplay
+ 'a:hover, a.\\:hover { outline: blue solid 1px; }', // has run adaptCssForReplay
+ ]);
+ });
+
it('can record childList mutations', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts
index 51e7ad2342..1250b7451d 100644
--- a/packages/rrweb/test/record.test.ts
+++ b/packages/rrweb/test/record.test.ts
@@ -769,6 +769,31 @@ describe('record', function (this: ISuite) {
await server.close();
});
+ it('captures stylesheets that are preloaded', async () => {
+ ctx.page.evaluate((serverURL) => {
+ const { record } = (window as unknown as IWindow).rrweb;
+
+ record({
+ inlineStylesheet: true,
+ emit: (window as unknown as IWindow).emit,
+ });
+
+ const link1 = document.createElement('link');
+ link1.setAttribute('rel', 'preload');
+ link1.setAttribute('as', 'style');
+ link1.setAttribute('href', `${serverURL}/html/assets/style.css`);
+ link1.addEventListener('load', () => {
+ link1.setAttribute('rel', 'stylesheet');
+ });
+ document.head.appendChild(link1);
+ }, serverURL);
+
+ await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`);
+ await waitForRAF(ctx.page);
+
+ assertSnapshot(ctx.events);
+ });
+
it('captures stylesheets that are still loading', async () => {
ctx.page.evaluate((serverURL) => {
const { record } = (window as unknown as IWindow).rrweb;
@@ -790,6 +815,27 @@ describe('record', function (this: ISuite) {
assertSnapshot(ctx.events);
});
+ it('respects inlineStylesheet=false for late loading stylesheets', async () => {
+ ctx.page.evaluate((serverURL) => {
+ const { record } = (window as unknown as IWindow).rrweb;
+
+ record({
+ inlineStylesheet: false,
+ emit: (window as unknown as IWindow).emit,
+ });
+
+ const link1 = document.createElement('link');
+ link1.setAttribute('rel', 'stylesheet');
+ link1.setAttribute('href', `${serverURL}/html/assets/style.css`);
+ document.head.appendChild(link1);
+ }, serverURL);
+
+ await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`);
+ await waitForRAF(ctx.page);
+
+ assertSnapshot(ctx.events);
+ });
+
it('captures stylesheets in iframes that are still loading', async () => {
ctx.page.evaluate(() => {
const iframe = document.createElement('iframe');
diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts
index 30a36abfcb..88e1b25792 100644
--- a/packages/rrweb/test/utils.ts
+++ b/packages/rrweb/test/utils.ts
@@ -242,18 +242,18 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
function stripBlobURLsFromAttributes(node: {
attributes: {
- src?: string;
+ [key: string]: any;
};
}) {
- if (
- 'src' in node.attributes &&
- node.attributes.src &&
- typeof node.attributes.src === 'string' &&
- node.attributes.src.startsWith('blob:')
- ) {
- node.attributes.src = node.attributes.src
- .replace(/[\w-]+$/, '...')
- .replace(/:[0-9]+\//, ':xxxx/');
+ for (const attr in node.attributes) {
+ if (
+ typeof node.attributes[attr] === 'string' &&
+ node.attributes[attr].startsWith('blob:')
+ ) {
+ node.attributes[attr] = node.attributes[attr]
+ .replace(/[\w-]+$/, '...')
+ .replace(/:[0-9]+\//, ':xxxx/');
+ }
}
}