From 24d686ecfb47e515ae322a33b79bfba0ea74b952 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] Record when a doc is in `compatMode` and trigger this mode upon replay (#697) * Hygiene: clean up the xhtml namespace attribute; this is an artefact of the `serializeToString` method which we are using (I think) to be consistent with whitespace and to clean up invalid attributes. I'm removing as was confused as am adding tests related to doctypes * Record when a document is in `compatMode` and trigger this mode on the iframe upon replay https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode the included DOCTYPE was picked up from https://stackoverflow.com/questions/18976213/ - there may be better ways of triggering compatMode * Don't write an extra DOCTYPE if there's one already present in the snapshot. Rely instead on whatever doctype is there to trigger the BackCompat mode * Modify to write the correct doctype if we can sniff xhtml - don't have any evidence that this will make a difference * Dev convenience: Ignore files generated by editors * Typo fix * Was getting a 2000ms timeout on the 'before' hook I believe * Change certain tests to go directly to their localhost page instead of loading the html content programmatically in order to avoid triggering an incorrect BackCompat mode (incorrect in that the html content has a correct doctype) * Add test based on motivating site that had images lined up in a square which were all different sizes; very old style percentage width/height attributes were doing the right thing in quirksmode, which is what we are testing for here * Fixup rrweb test html to include a valid doctype and avoid BackCompat to ensure we're not accidentally testing against quirks modes. I didn't find an elegant way of avoiding the `BackCompat` when adding a minimal iframe, so some BackCompat has slipped in here, I don't think there's much harm --- packages/rrweb-snapshot/src/rebuild.ts | 14 + packages/rrweb-snapshot/src/snapshot.ts | 20 +- packages/rrweb-snapshot/src/types.ts | 1 + .../test/__snapshots__/integration.ts.snap | 58 +- .../rrweb-snapshot/test/html/compat-mode.html | 14 + .../rrweb-snapshot/test/html/picture.html | 2 +- .../test/images/compat-bottom.png | Bin 0 -> 2503 bytes .../test/images/compat-top-left.png | Bin 0 -> 2693 bytes .../test/images/compat-top-right.png | Bin 0 -> 3049 bytes packages/rrweb-snapshot/test/integration.ts | 78 ++- packages/rrweb-snapshot/typings/types.d.ts | 1 + .../__snapshots__/integration.test.ts.snap | 639 ++++++++++-------- .../test/__snapshots__/record.test.ts.snap | 233 ++++--- packages/rrweb/test/html/move-node.html | 1 + .../rrweb/test/html/mutation-observer.html | 1 + packages/rrweb/test/html/password.html | 1 + packages/rrweb/test/record.test.ts | 2 + 17 files changed, 645 insertions(+), 420 deletions(-) create mode 100644 packages/rrweb-snapshot/test/html/compat-mode.html create mode 100644 packages/rrweb-snapshot/test/images/compat-bottom.png create mode 100644 packages/rrweb-snapshot/test/images/compat-top-left.png create mode 100644 packages/rrweb-snapshot/test/images/compat-top-right.png diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 8459c379..2bcfa8b1 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -321,6 +321,20 @@ export function buildNodeWithSN( // close before open to make sure document was closed doc.close(); doc.open(); + if (n.compatMode === 'BackCompat' && + (n.childNodes && n.childNodes[0].type !== NodeType.DocumentType) // there isn't one already defined + ) { + // Trigger compatMode in the iframe + // this is needed as document.createElement('iframe') otherwise inherits a CSS1Compat mode from the parent replayer environment + if (n.childNodes[0].type === NodeType.Element && + 'xmlns' in n.childNodes[0].attributes && + n.childNodes[0].attributes.xmlns === 'http://www.w3.org/1999/xhtml') { + // might as well use an xhtml doctype if we've got an xhtml namespace + doc.write(''); + } else { + doc.write(''); + } + } node = doc; } diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 6f346e6e..ef312841 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -10,6 +10,7 @@ import { MaskTextFn, MaskInputFn, KeepIframeSrcFn, + documentNode, } from './types'; import { isElement, isShadowRoot, maskInputValue } from './utils'; @@ -379,11 +380,20 @@ function serializeNode( } switch (n.nodeType) { case n.DOCUMENT_NODE: - return { - type: NodeType.Document, - childNodes: [], - rootId, - }; + if ((n as HTMLDocument).compatMode !== 'CSS1Compat') { + return { + type: NodeType.Document, + childNodes: [], + compatMode: (n as HTMLDocument).compatMode, // probably "BackCompat" + rootId, + } + } else { + return { + type: NodeType.Document, + childNodes: [], + rootId, + } + } case n.DOCUMENT_TYPE_NODE: return { type: NodeType.DocumentType, diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index e482394b..8720783e 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -10,6 +10,7 @@ export enum NodeType { export type documentNode = { type: NodeType.Document; childNodes: serializedNodeWithId[]; + compatMode?: string; }; export type documentTypeNode = { diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.ts.snap index e087f79b..bb92551c 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`[html file]: about-mozilla.html 1`] = ` -" +" The Book of Mozilla, 11:9