/** * @jest-environment jsdom */ import { NodeType as RRNodeType } from 'rrweb-snapshot'; import { BaseRRDocumentImpl, BaseRRDocumentTypeImpl, BaseRRElementImpl, BaseRRMediaElementImpl, BaseRRNode, IRRDocumentType, } from '../src/document'; describe('Basic RRDocument implementation', () => { const RRNode = BaseRRNode; const RRDocument = BaseRRDocumentImpl(RRNode); const RRDocumentType = BaseRRDocumentTypeImpl(RRNode); const RRElement = BaseRRElementImpl(RRNode); class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} describe('Basic RRNode implementation', () => { it('should have basic properties', () => { const node = new RRNode(); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBeUndefined(); expect(node.textContent).toBeUndefined(); expect(node.RRNodeType).toBeUndefined(); expect(node.nodeType).toBeUndefined(); expect(node.nodeName).toBeUndefined(); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.toString()).toEqual('RRNode'); }); it('can get first child node', () => { const parentNode = new RRNode(); const childNode1 = new RRNode(); const childNode2 = new RRNode(); expect(parentNode.firstChild).toBeNull(); parentNode.childNodes = [childNode1]; expect(parentNode.firstChild).toBe(childNode1); parentNode.childNodes = [childNode1, childNode2]; expect(parentNode.firstChild).toBe(childNode1); parentNode.childNodes = [childNode2, childNode1]; expect(parentNode.firstChild).toBe(childNode2); }); it('can get last child node', () => { const parentNode = new RRNode(); const childNode1 = new RRNode(); const childNode2 = new RRNode(); expect(parentNode.lastChild).toBeNull(); parentNode.childNodes = [childNode1]; expect(parentNode.lastChild).toBe(childNode1); parentNode.childNodes = [childNode1, childNode2]; expect(parentNode.lastChild).toBe(childNode2); parentNode.childNodes = [childNode2, childNode1]; expect(parentNode.lastChild).toBe(childNode1); }); it('can get nextSibling', () => { const parentNode = new RRNode(); const childNode1 = new RRNode(); const childNode2 = new RRNode(); expect(parentNode.nextSibling).toBeNull(); expect(childNode1.nextSibling).toBeNull(); childNode1.parentNode = parentNode; parentNode.childNodes = [childNode1]; expect(childNode1.nextSibling).toBeNull(); childNode2.parentNode = parentNode; parentNode.childNodes = [childNode1, childNode2]; expect(childNode1.nextSibling).toBe(childNode2); expect(childNode2.nextSibling).toBeNull(); }); it('should return whether the node contains another node', () => { const parentNode = new RRNode(); const childNode1 = new RRNode(); const childNode2 = new RRNode(); parentNode.childNodes = [childNode1]; expect(parentNode.contains(childNode1)).toBeTruthy(); expect(parentNode.contains(childNode2)).toBeFalsy(); childNode1.childNodes = [childNode2]; expect(parentNode.contains(childNode2)).toBeTruthy(); }); it('should not implement appendChild', () => { const parentNode = new RRNode(); const childNode = new RRNode(); expect(() => parentNode.appendChild(childNode), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method."`, ); }); it('should not implement insertBefore', () => { const parentNode = new RRNode(); const childNode = new RRNode(); expect(() => parentNode.insertBefore(childNode, null), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method."`, ); }); it('should not implement removeChild', () => { const parentNode = new RRNode(); const childNode = new RRNode(); expect(() => parentNode.removeChild(childNode), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'removeChild' on 'RRNode': This RRNode type does not support this method."`, ); }); }); describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = new RRDocument(); expect(node.toString()).toEqual('RRDocument'); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(node); expect(node.textContent).toBeNull(); expect(node.RRNodeType).toBe(RRNodeType.Document); expect(node.nodeType).toBe(document.nodeType); expect(node.nodeName).toBe('#document'); expect(node.compatMode).toBe('CSS1Compat'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.documentElement).toBeNull(); expect(node.body).toBeNull(); expect(node.head).toBeNull(); expect(node.implementation).toBe(node); expect(node.firstElementChild).toBeNull(); expect(node.createDocument).toBeDefined(); expect(node.createDocumentType).toBeDefined(); expect(node.createElement).toBeDefined(); expect(node.createElementNS).toBeDefined(); expect(node.createTextNode).toBeDefined(); expect(node.createComment).toBeDefined(); expect(node.createCDATASection).toBeDefined(); expect(node.open).toBeDefined(); expect(node.close).toBeDefined(); expect(node.write).toBeDefined(); expect(node.toString()).toEqual('RRDocument'); }); it('can get documentElement', () => { const node = new RRDocument(); expect(node.documentElement).toBeNull(); const element = node.createElement('html'); node.appendChild(element); expect(node.documentElement).toBe(element); }); it('can get head', () => { const node = new RRDocument(); expect(node.head).toBeNull(); const element = node.createElement('html'); node.appendChild(element); expect(node.head).toBeNull(); const head = node.createElement('head'); element.appendChild(head); expect(node.head).toBe(head); }); it('can get body', () => { const node = new RRDocument(); expect(node.body).toBeNull(); const element = node.createElement('html'); node.appendChild(element); expect(node.body).toBeNull(); const body = node.createElement('body'); element.appendChild(body); expect(node.body).toBe(body); const head = node.createElement('head'); element.appendChild(head); expect(node.body).toBe(body); }); it('can get firstElementChild', () => { const node = new RRDocument(); expect(node.firstElementChild).toBeNull(); const element = node.createElement('html'); node.appendChild(element); expect(node.firstElementChild).toBe(element); }); it('can append child', () => { const node = new RRDocument(); expect(node.firstElementChild).toBeNull(); const documentType = node.createDocumentType('html', '', ''); expect(node.appendChild(documentType)).toBe(documentType); expect(node.childNodes[0]).toEqual(documentType); expect(documentType.parentElement).toBeNull(); expect(documentType.parentNode).toBe(node); expect(() => node.appendChild(documentType), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one RRDoctype on RRDocument allowed."`, ); const element = node.createElement('html'); expect(node.appendChild(element)).toBe(element); expect(node.childNodes[1]).toEqual(element); expect(element.parentElement).toBeNull(); expect(element.parentNode).toBe(node); const div = node.createElement('div'); expect(() => node.appendChild(div)).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one RRElement on RRDocument allowed."`, ); }); it('can insert new child before an existing child', () => { const node = new RRDocument(); const docType = node.createDocumentType('', '', ''); expect(() => node.insertBefore(node, docType), ).toThrowErrorMatchingInlineSnapshot( `"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode."`, ); expect(node.insertBefore(docType, null)).toBe(docType); expect(() => node.insertBefore(docType, null), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRDoctype on RRDocument allowed."`, ); node.removeChild(docType); const documentElement = node.createElement('html'); expect(() => node.insertBefore(documentElement, docType), ).toThrowErrorMatchingInlineSnapshot( `"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode."`, ); expect(node.insertBefore(documentElement, null)).toBe(documentElement); expect(() => node.insertBefore(documentElement, null), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRElement on RRDocument allowed."`, ); expect(node.insertBefore(docType, documentElement)).toBe(docType); expect(node.childNodes[0]).toBe(docType); expect(node.childNodes[1]).toBe(documentElement); expect(docType.parentElement).toBeNull(); expect(documentElement.parentElement).toBeNull(); expect(docType.parentNode).toBe(node); expect(documentElement.parentNode).toBe(node); }); it('can remove an existing child', () => { const node = new RRDocument(); const documentType = node.createDocumentType('html', '', ''); const documentElement = node.createElement('html'); node.appendChild(documentType); node.appendChild(documentElement); expect(documentType.parentNode).toBe(node); expect(documentElement.parentNode).toBe(node); expect(() => node.removeChild(node.createElement('div')), ).toThrowErrorMatchingInlineSnapshot( `"Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode."`, ); expect(node.removeChild(documentType)).toBe(documentType); expect(documentType.parentNode).toBeNull(); expect(node.removeChild(documentElement)).toBe(documentElement); expect(documentElement.parentNode).toBeNull(); }); it('should implement create node functions', () => { const node = new RRDocument(); expect(node.createDocument(null, '', null).RRNodeType).toEqual( RRNodeType.Document, ); expect(node.createDocumentType('', '', '').RRNodeType).toEqual( RRNodeType.DocumentType, ); expect(node.createElement('html').RRNodeType).toEqual(RRNodeType.Element); expect(node.createElementNS('', 'html').RRNodeType).toEqual( RRNodeType.Element, ); expect(node.createTextNode('text').RRNodeType).toEqual(RRNodeType.Text); expect(node.createComment('comment').RRNodeType).toEqual( RRNodeType.Comment, ); expect(node.createCDATASection('data').RRNodeType).toEqual( RRNodeType.CDATA, ); }); it('can close and open a RRDocument', () => { const node = new RRDocument(); const documentType = node.createDocumentType('html', '', ''); node.appendChild(documentType); expect(node.childNodes[0]).toBe(documentType); expect(node.close()); expect(node.open()); expect(node.childNodes.length).toEqual(0); }); it('can cover the usage of write() in rrweb-snapshot', () => { const node = new RRDocument(); node.write( '', ); expect(node.childNodes.length).toBe(1); let doctype = node.childNodes[0] as IRRDocumentType; expect(doctype.RRNodeType).toEqual(RRNodeType.DocumentType); expect(doctype.parentNode).toEqual(node); expect(doctype.name).toEqual('html'); expect(doctype.publicId).toEqual( '-//W3C//DTD XHTML 1.0 Transitional//EN', ); expect(doctype.systemId).toEqual(''); node.write( '', ); expect(node.childNodes.length).toBe(1); doctype = node.childNodes[0] as IRRDocumentType; expect(doctype.RRNodeType).toEqual(RRNodeType.DocumentType); expect(doctype.parentNode).toEqual(node); expect(doctype.name).toEqual('html'); expect(doctype.publicId).toEqual('-//W3C//DTD HTML 4.0 Transitional//EN'); expect(doctype.systemId).toEqual(''); }); }); describe('Basic RRDocumentType implementation', () => { it('should have basic properties', () => { const name = 'name', publicId = 'publicId', systemId = 'systemId'; const node = new RRDocumentType(name, publicId, systemId); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBeUndefined(); expect(node.textContent).toBeNull(); expect(node.RRNodeType).toBe(RRNodeType.DocumentType); expect(node.nodeType).toBe(document.DOCUMENT_TYPE_NODE); expect(node.nodeName).toBe(name); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.name).toBe(name); expect(node.publicId).toBe(publicId); expect(node.systemId).toBe(systemId); expect(node.toString()).toEqual('RRDocumentType'); }); }); describe('Basic RRElement implementation', () => { const document = new RRDocument(); it('should have basic properties', () => { const node = document.createElement('div'); node.scrollLeft = 100; node.scrollTop = 200; node.attributes.id = 'id'; node.attributes.class = 'className'; expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(document); expect(node.textContent).toEqual(''); expect(node.RRNodeType).toBe(RRNodeType.Element); expect(node.nodeType).toBe(document.ELEMENT_NODE); expect(node.nodeName).toBe('DIV'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.tagName).toEqual('DIV'); expect(node.attributes).toEqual({ id: 'id', class: 'className' }); expect(node.shadowRoot).toBeNull(); expect(node.scrollLeft).toEqual(100); expect(node.scrollTop).toEqual(200); expect(node.id).toEqual('id'); expect(node.className).toEqual('className'); expect(node.classList).toBeDefined(); expect(node.style).toBeDefined(); expect(node.getAttribute).toBeDefined(); expect(node.setAttribute).toBeDefined(); expect(node.setAttributeNS).toBeDefined(); expect(node.removeAttribute).toBeDefined(); expect(node.attachShadow).toBeDefined(); expect(node.dispatchEvent).toBeDefined(); expect(node.dispatchEvent(null as unknown as Event)).toBeTruthy(); expect(node.toString()).toEqual('DIV id="id" class="className" '); }); it('can get textContent', () => { const node = document.createElement('div'); node.appendChild(document.createTextNode('text1 ')); node.appendChild(document.createTextNode('text2')); expect(node.textContent).toEqual('text1 text2'); }); it('can set textContent', () => { const node = document.createElement('div'); node.appendChild(document.createTextNode('text1 ')); node.appendChild(document.createTextNode('text2')); expect(node.textContent).toEqual('text1 text2'); node.textContent = 'new text'; expect(node.textContent).toEqual('new text'); }); it('can get id', () => { const node = document.createElement('div'); expect(node.id).toEqual(''); node.attributes.id = 'idName'; expect(node.id).toEqual('idName'); }); it('can get className', () => { const node = document.createElement('div'); expect(node.className).toEqual(''); node.attributes.class = 'className'; expect(node.className).toEqual('className'); }); it('can get classList', () => { const node = document.createElement('div'); const classList = node.classList; expect(classList.add).toBeDefined(); expect(classList.remove).toBeDefined(); }); it('classList can add class name', () => { const node = document.createElement('div'); expect(node.className).toEqual(''); const classList = node.classList; classList.add('c1'); expect(node.className).toEqual('c1'); classList.add('c2'); expect(node.className).toEqual('c1 c2'); classList.add('c2'); expect(node.className).toEqual('c1 c2'); }); it('classList can remove class name', () => { const node = document.createElement('div'); expect(node.className).toEqual(''); const classList = node.classList; classList.add('c1', 'c2', 'c3'); expect(node.className).toEqual('c1 c2 c3'); classList.remove('c2'); expect(node.className).toEqual('c1 c3'); classList.remove('c3'); expect(node.className).toEqual('c1'); classList.remove('c1'); expect(node.className).toEqual(''); classList.remove('c1'); expect(node.className).toEqual(''); }); it('classList can remove duplicate class names', () => { const node = document.createElement('div'); expect(node.className).toEqual(''); node.setAttribute('class', 'c1 c1 c1'); expect(node.className).toEqual('c1 c1 c1'); const classList = node.classList; classList.remove('c1'); expect(node.className).toEqual(''); }); it('can get CSS style declaration', () => { const node = document.createElement('div'); const style = node.style; expect(style).toBeDefined(); expect(style.setProperty).toBeDefined(); expect(style.removeProperty).toBeDefined(); node.attributes.style = 'color: blue; background-color: red; width: 78%; height: 50vh !important;'; expect(node.style.color).toBe('blue'); expect(node.style.backgroundColor).toBe('red'); expect(node.style.width).toBe('78%'); expect(node.style.height).toBe('50vh !important'); }); it('can set CSS property', () => { const node = document.createElement('div'); const style = node.style; style.setProperty('color', 'red'); expect(node.attributes.style).toEqual('color: red;'); // camelCase style is unacceptable style.setProperty('backgroundColor', 'blue'); expect(node.attributes.style).toEqual('color: red;'); style.setProperty('height', '50vh', 'important'); expect(node.attributes.style).toEqual( 'color: red; height: 50vh !important;', ); // kebab-case style.setProperty('background-color', 'red'); expect(node.attributes.style).toEqual( 'color: red; height: 50vh !important; background-color: red;', ); // remove the property style.setProperty('background-color', null); expect(node.attributes.style).toEqual( 'color: red; height: 50vh !important;', ); }); it('can remove CSS property', () => { const node = document.createElement('div'); node.attributes.style = 'color: blue; background-color: red; width: 78%; height: 50vh !important;'; const style = node.style; expect(style.removeProperty('color')).toEqual('blue'); expect(node.attributes.style).toEqual( 'background-color: red; width: 78%; height: 50vh !important;', ); expect(style.removeProperty('height')).toEqual('50vh !important'); expect(node.attributes.style).toEqual( 'background-color: red; width: 78%;', ); // kebab-case expect(style.removeProperty('background-color')).toEqual('red'); expect(node.attributes.style).toEqual('width: 78%;'); style.setProperty('background-color', 'red'); expect(node.attributes.style).toEqual( 'width: 78%; background-color: red;', ); expect(style.removeProperty('backgroundColor')).toEqual(''); expect(node.attributes.style).toEqual( 'width: 78%; background-color: red;', ); // remove a non-exist property expect(style.removeProperty('margin')).toEqual(''); }); it('can parse more inline styles correctly', () => { const node = document.createElement('div'); // general node.attributes.style = 'display: inline-block; margin: 0 auto; border: 5px solid #BADA55; font-size: .75em; position:absolute;width: 33.3%; z-index:1337; font-family: "Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif;'; let style = node.style; expect(style.display).toEqual('inline-block'); expect(style.margin).toEqual('0 auto'); expect(style.border).toEqual('5px solid #BADA55'); expect(style.fontSize).toEqual('.75em'); expect(style.position).toEqual('absolute'); expect(style.width).toEqual('33.3%'); expect(style.zIndex).toEqual('1337'); expect(style.fontFamily).toEqual( '"Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif', ); // multiple of same property node.attributes.style = 'color: rgba(0,0,0,1);color:white'; style = node.style; expect(style.color).toEqual('white'); // url node.attributes.style = 'background-image: url("http://example.com/img.png")'; expect(node.style.backgroundImage).toEqual( 'url("http://example.com/img.png")', ); // vendor prefixes node.attributes.style = ` -moz-border-radius: 10px 5px; -webkit-border-top-left-radius: 10px; -webkit-border-bottom-left-radius: 5px; border-radius: 10px 5px; `; style = node.style; expect(style.MozBorderRadius).toEqual('10px 5px'); expect(style.WebkitBorderTopLeftRadius).toEqual('10px'); expect(style.WebkitBorderBottomLeftRadius).toEqual('5px'); expect(style.borderRadius).toEqual('10px 5px'); // comment node.attributes.style = 'top: 0; /* comment1 */ bottom: /* comment2 */42rem;'; expect(node.style.top).toEqual('0'); expect(node.style.bottom).toEqual('42rem'); // empty comment node.attributes.style = 'top: /**/0;'; expect(node.style.top).toEqual('0'); // custom property (variable) node.attributes.style = '--custom-property: value'; expect(node.style['--custom-property']).toEqual('value'); // incomplete node.attributes.style = 'overflow:'; expect(node.style.overflow).toBeUndefined(); }); it('can get attribute', () => { const node = document.createElement('div'); node.attributes.class = 'className'; expect(node.getAttribute('class')).toEqual('className'); expect(node.getAttribute('id')).toEqual(null); node.attributes.id = 'id'; expect(node.getAttribute('id')).toEqual('id'); }); it('can set attribute', () => { const node = document.createElement('div'); expect(node.getAttribute('class')).toEqual(null); node.setAttribute('class', 'className'); expect(node.getAttribute('class')).toEqual('className'); expect(node.getAttribute('id')).toEqual(null); node.setAttribute('id', 'id'); expect(node.getAttribute('id')).toEqual('id'); }); it('can setAttributeNS', () => { const node = document.createElement('div'); expect(node.getAttribute('class')).toEqual(null); node.setAttributeNS('namespace', 'class', 'className'); expect(node.getAttribute('class')).toEqual('className'); expect(node.getAttribute('id')).toEqual(null); node.setAttributeNS('namespace', 'id', 'id'); expect(node.getAttribute('id')).toEqual('id'); }); it('can remove attribute', () => { const node = document.createElement('div'); node.setAttribute('class', 'className'); expect(node.getAttribute('class')).toEqual('className'); node.removeAttribute('class'); expect(node.getAttribute('class')).toEqual(null); node.removeAttribute('id'); expect(node.getAttribute('id')).toEqual(null); }); it('can attach shadow dom', () => { const node = document.createElement('div'); expect(node.shadowRoot).toBeNull(); node.attachShadow({ mode: 'open' }); expect(node.shadowRoot).not.toBeNull(); expect(node.shadowRoot!.RRNodeType).toBe(RRNodeType.Element); expect(node.shadowRoot!.tagName).toBe('SHADOWROOT'); expect(node.parentNode).toBeNull(); }); it('can append child', () => { const node = document.createElement('div'); expect(node.childNodes.length).toBe(0); const child1 = document.createComment('span'); expect(node.appendChild(child1)).toBe(child1); expect(node.childNodes[0]).toEqual(child1); expect(child1.parentElement).toBe(node); expect(child1.parentNode).toBe(node); const child2 = document.createElement('p'); expect(node.appendChild(child2)).toBe(child2); expect(node.childNodes[1]).toEqual(child2); expect(child2.parentElement).toBe(node); expect(child2.parentNode).toBe(node); }); it('can insert new child before an existing child', () => { const node = document.createElement('div'); const child1 = document.createElement('h1'); const child2 = document.createElement('h2'); expect(() => node.insertBefore(node, child1), ).toThrowErrorMatchingInlineSnapshot( `"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode."`, ); expect(node.insertBefore(child1, null)).toBe(child1); expect(node.childNodes[0]).toBe(child1); expect(child1.parentNode).toBe(node); expect(child1.parentElement).toBe(node); expect(node.insertBefore(child2, child1)).toBe(child2); expect(node.childNodes.length).toBe(2); expect(node.childNodes[0]).toBe(child2); expect(node.childNodes[1]).toBe(child1); expect(child2.parentNode).toBe(node); expect(child2.parentElement).toBe(node); }); it('can remove an existing child', () => { const node = document.createElement('div'); const child1 = document.createElement('h1'); const child2 = document.createElement('h2'); node.appendChild(child1); node.appendChild(child2); expect(node.childNodes.length).toBe(2); expect(child1.parentNode).toBe(node); expect(child2.parentNode).toBe(node); expect(child1.parentElement).toBe(node); expect(child2.parentElement).toBe(node); expect(() => node.removeChild(document.createElement('div')), ).toThrowErrorMatchingInlineSnapshot( `"Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode."`, ); expect(node.removeChild(child1)).toBe(child1); expect(child1.parentNode).toBeNull(); expect(child1.parentElement).toBeNull(); expect(node.childNodes.length).toBe(1); expect(node.removeChild(child2)).toBe(child2); expect(node.childNodes.length).toBe(0); expect(child2.parentNode).toBeNull(); expect(child2.parentElement).toBeNull(); }); }); describe('Basic RRText implementation', () => { const dom = new RRDocument(); it('should have basic properties', () => { const node = dom.createTextNode('text'); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(dom); expect(node.textContent).toEqual('text'); expect(node.RRNodeType).toBe(RRNodeType.Text); expect(node.nodeType).toBe(document.TEXT_NODE); expect(node.nodeName).toBe('#text'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.toString()).toEqual('RRText text="text"'); }); it('can set textContent', () => { const node = dom.createTextNode('text'); expect(node.textContent).toEqual('text'); node.textContent = 'new text'; expect(node.textContent).toEqual('new text'); }); }); describe('Basic RRComment implementation', () => { const dom = new RRDocument(); it('should have basic properties', () => { const node = dom.createComment('comment'); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(dom); expect(node.textContent).toEqual('comment'); expect(node.RRNodeType).toBe(RRNodeType.Comment); expect(node.nodeType).toBe(document.COMMENT_NODE); expect(node.nodeName).toBe('#comment'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.toString()).toEqual('RRComment text="comment"'); }); it('can set textContent', () => { const node = dom.createComment('comment'); expect(node.textContent).toEqual('comment'); node.textContent = 'new comment'; expect(node.textContent).toEqual('new comment'); }); }); describe('Basic RRCDATASection implementation', () => { const dom = new RRDocument(); it('should have basic properties', () => { const node = dom.createCDATASection('data'); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(dom); expect(node.textContent).toEqual('data'); expect(node.RRNodeType).toBe(RRNodeType.CDATA); expect(node.nodeType).toBe(document.CDATA_SECTION_NODE); expect(node.nodeName).toBe('#cdata-section'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.toString()).toEqual('RRCDATASection data="data"'); }); it('can set textContent', () => { const node = dom.createCDATASection('data'); expect(node.textContent).toEqual('data'); node.textContent = 'new data'; expect(node.textContent).toEqual('new data'); }); }); describe('Basic RRMediaElement implementation', () => { it('should have basic properties', () => { const node = new RRMediaElement('video'); node.scrollLeft = 100; node.scrollTop = 200; expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBeUndefined(); expect(node.textContent).toEqual(''); expect(node.RRNodeType).toBe(RRNodeType.Element); expect(node.nodeType).toBe(document.ELEMENT_NODE); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.tagName).toEqual('VIDEO'); expect(node.attributes).toEqual({}); expect(node.shadowRoot).toBeNull(); expect(node.scrollLeft).toEqual(100); expect(node.scrollTop).toEqual(200); expect(node.id).toEqual(''); expect(node.className).toEqual(''); expect(node.classList).toBeDefined(); expect(node.style).toBeDefined(); expect(node.getAttribute).toBeDefined(); expect(node.setAttribute).toBeDefined(); expect(node.setAttributeNS).toBeDefined(); expect(node.removeAttribute).toBeDefined(); expect(node.attachShadow).toBeDefined(); expect(node.dispatchEvent).toBeDefined(); expect(node.currentTime).toBeUndefined(); expect(node.volume).toBeUndefined(); expect(node.paused).toBeUndefined(); expect(node.muted).toBeUndefined(); expect(node.playbackRate).toBeUndefined(); expect(node.play).toBeDefined(); expect(node.pause).toBeDefined(); expect(node.toString()).toEqual('VIDEO '); }); it('can play and pause the media', () => { const node = new RRMediaElement('video'); expect(node.paused).toBeUndefined(); node.play(); expect(node.paused).toBeFalsy(); node.pause(); expect(node.paused).toBeTruthy(); node.play(); expect(node.paused).toBeFalsy(); }); it('should not support attachShadow function', () => { const node = new RRMediaElement('video'); expect(() => node.attachShadow({ mode: 'open' }), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow"`, ); }); }); });