diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 529a51eeff..d6d424d92e 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -363,6 +363,12 @@ exports[`integration tests [html file]: picture-in-frame.html 1`] = ` " `; +exports[`integration tests [html file]: picture-with-inline-onload.html 1`] = ` +" + \\"This + " +`; + exports[`integration tests [html file]: preload.html 1`] = ` " diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 39a6107635..48103d8748 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -31,6 +31,7 @@ import { isSerializedStylesheet, inDom, getShadowHost, + getInlineCSSProperties, } from '../utils'; type DoubleLinkedListNode = { @@ -574,7 +575,10 @@ export default class MutationBuffer { item.attributes.style = {}; } const styleObj = item.attributes.style as styleAttributeValue; - for (const pname of Array.from(target.style)) { + const targetStyle = target.getAttribute('style'); + const oldStyle = old.getAttribute('style'); + + for (const pname of getInlineCSSProperties(targetStyle)) { const newValue = target.style.getPropertyValue(pname); const newPriority = target.style.getPropertyPriority(pname); if ( @@ -588,7 +592,7 @@ export default class MutationBuffer { } } } - for (const pname of Array.from(old.style)) { + for (const pname of getInlineCSSProperties(oldStyle)) { if (target.style.getPropertyValue(pname) === '') { // "if not set, returns the empty string" styleObj[pname] = false; // delete diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 604c8810e2..57099bdbde 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -572,3 +572,13 @@ export function inDom(n: Node): boolean { if (!doc) return false; return doc.contains(n) || shadowHostInDom(n); } + +export function getInlineCSSProperties(value: string | null): string[] { + if (!value) { + return []; + } + return value + .split(';') + .map((declaration) => declaration.split(':')[0].trim()) + .filter((declaration) => !!declaration); +} diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index ad9b438600..ec80945294 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -1843,6 +1843,166 @@ exports[`record captures nested stylesheet rules 1`] = ` ]" `; +exports[`record captures shorthand style properties 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\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"size\\": \\"40\\" + }, + \\"childNodes\\": [], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + } + ], + \\"id\\": 5 + } + ], + \\"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\\": \\"style\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9 + } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\":root { --bg-orange: #FF3300; }\\", + \\"isStyle\\": true, + \\"id\\": 10 + } + }, + { + \\"parentId\\": 5, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 11 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 11, + \\"attributes\\": { + \\"style\\": { + \\"background\\": \\"var(--bg-orange)\\" + } + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 11, + \\"attributes\\": { + \\"style\\": { + \\"background-color\\": \\"rgb(0, 0, 0)\\", + \\"background\\": false + } + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + } +]" +`; + exports[`record captures style property changes 1`] = ` "[ { diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 1a0a87421f..32990d6fe6 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -10,6 +10,8 @@ import { IncrementalSource, styleSheetRuleData, selectionData, + styleAttributeValue, + mutationData, } from '@rrweb/types'; import { assertSnapshot, @@ -279,6 +281,54 @@ describe('record', function (this: ISuite) { assertSnapshot(ctx.events); }); + it('captures shorthand style properties', async () => { + await ctx.page.evaluate(() => { + const { record } = (window as unknown as IWindow).rrweb; + + record({ + emit: (window as unknown as IWindow).emit, + }); + + const styleElement = document.createElement('style'); + styleElement.innerText = ':root { --bg-orange: #FF3300; }'; + document.head.appendChild(styleElement); + + const div = document.createElement('div'); + document.body.appendChild(div); + + setTimeout(() => { + div.setAttribute('style', 'background: var(--bg-orange)'); + }, 5); + + setTimeout(() => { + div.setAttribute('style', 'background-color: #000000'); + }, 10); + }); + + await ctx.page.waitForTimeout(50); + + const attributeMutationEvents = ctx.events.filter( + (e) => + e.type === EventType.IncrementalSnapshot && + e.data.source === IncrementalSource.Mutation && + e.data.attributes.length, + ); + + const expectedShorthandBackground = ( + (attributeMutationEvents[0].data as mutationData)?.attributes[0] + ?.attributes.style as styleAttributeValue + )?.['background']; + const expectedLonghandBackground = ( + (attributeMutationEvents[1].data as mutationData)?.attributes[0] + ?.attributes.style as styleAttributeValue + )?.['background']; + + expect(attributeMutationEvents.length).toEqual(2); + expect(expectedShorthandBackground).toEqual('var(--bg-orange)'); + expect(expectedLonghandBackground).toEqual(false); + assertSnapshot(ctx.events); + }); + it('captures stylesheet rules', async () => { await ctx.page.evaluate(() => { const { record } = (window as unknown as IWindow).rrweb;