diff --git a/.changeset/eighty-drinks-mix.md b/.changeset/eighty-drinks-mix.md new file mode 100644 index 0000000000..ed03814285 --- /dev/null +++ b/.changeset/eighty-drinks-mix.md @@ -0,0 +1,5 @@ +--- +"rrweb-snapshot": patch +--- + +Fixes inserting DOCTYPE element that should happen to be inserted before HTML node element - not after diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 692da2d281..cec40effa2 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -558,6 +558,18 @@ export function buildNodeWithSN( } else { node.appendChild(childNode); } + } else if ( + n.type === NodeType.Document && + childN.type === NodeType.DocumentType + ) { + // DocumentType nodes must be inserted before the root element + // If there's already a root element, insert before it + if (node.firstChild && node.firstChild.nodeType === 1) { + // 1 = Element node type + node.insertBefore(childNode, node.firstChild); + } else { + node.appendChild(childNode); + } } else { node.appendChild(childNode); } diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index c8be11c070..46c480cf73 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -283,4 +283,98 @@ ul li.specified c.\\:hover img { expect.any(Error), ); }); + + describe('doctype insertion', function () { + it('should insert doctype before HTML element when both are children of document', function () { + // Create a document with both HTML and doctype as child nodes + // This simulates the scenario where HTML is processed first, then doctype + const doc = buildNodeWithSN( + { + id: 1, + type: NodeType.Document, + childNodes: [ + { + id: 2, + tagName: 'html', + type: NodeType.Element, + attributes: {}, + childNodes: [ + { + id: 3, + tagName: 'head', + type: NodeType.Element, + attributes: {}, + childNodes: [ + { + id: 4, + tagName: 'title', + type: NodeType.Element, + attributes: {}, + childNodes: [ + { + id: 5, + type: NodeType.Text, + textContent: 'Test Page', + }, + ], + }, + ], + }, + { + id: 6, + tagName: 'body', + type: NodeType.Element, + attributes: {}, + childNodes: [ + { + id: 7, + tagName: 'h1', + type: NodeType.Element, + attributes: {}, + childNodes: [ + { + id: 8, + type: NodeType.Text, + textContent: 'Welcome', + }, + ], + }, + ], + }, + ], + }, + { + id: 9, + type: NodeType.DocumentType, + name: 'html', + publicId: '-//W3C//DTD HTML 4.01//EN', + systemId: 'http://www.w3.org/TR/html4/strict.dtd', + }, + ], + }, + { + doc: document, + mirror, + hackCss: false, + cache, + }, + ) as Document; + + // Verify that the doctype was inserted before the HTML element + expect(doc.firstChild?.nodeType).toBe(Node.DOCUMENT_TYPE_NODE); + expect(doc.firstChild?.nodeName).toBe('html'); + expect((doc.firstChild as DocumentType).name).toBe('html'); + expect((doc.firstChild as DocumentType).publicId).toBe( + '-//W3C//DTD HTML 4.01//EN', + ); + expect((doc.firstChild as DocumentType).systemId).toBe( + 'http://www.w3.org/TR/html4/strict.dtd', + ); + + // Verify that HTML is the second child + expect(doc.childNodes[1]?.nodeName).toBe('HTML'); + expect(doc.childNodes[1]?.childNodes[0]?.nodeName).toBe('HEAD'); + expect(doc.childNodes[1]?.childNodes[1]?.nodeName).toBe('BODY'); + }); + }); });