/** * @jest-environment jsdom */ import * as fs from 'fs'; import * as path from 'path'; import { NodeType as RRNodeType } from 'rrweb-snapshot'; import { RRCanvasElement, RRCDATASection, RRComment, RRDocument, RRElement, RRIFrameElement, RRImageElement, RRMediaElement, RRStyleElement, RRText, } from '../src/document-nodejs'; import { buildFromDom } from 'rrdom'; describe('RRDocument for nodejs environment', () => { describe('RRDocument API', () => { let rrdom: RRDocument; beforeAll(() => { // initialize rrdom document.write(getHtml('main.html')); rrdom = new RRDocument(); buildFromDom(document, undefined, rrdom); }); it('can create different type of RRNodes', () => { const document = rrdom.createDocument('', ''); expect(document).toBeInstanceOf(RRDocument); const audio = rrdom.createElement('audio'); expect(audio).toBeInstanceOf(RRMediaElement); const video = rrdom.createElement('video'); expect(video).toBeInstanceOf(RRMediaElement); const iframe = rrdom.createElement('iframe'); expect(iframe).toBeInstanceOf(RRIFrameElement); const image = rrdom.createElement('img'); expect(image).toBeInstanceOf(RRImageElement); const canvas = rrdom.createElement('canvas'); expect(canvas).toBeInstanceOf(RRCanvasElement); const style = rrdom.createElement('style'); expect(style).toBeInstanceOf(RRStyleElement); const elementNS = rrdom.createElementNS( 'http://www.w3.org/2000/svg', 'div', ); expect(elementNS).toBeInstanceOf(RRElement); expect(elementNS.tagName).toEqual('DIV'); const text = rrdom.createTextNode('text'); expect(text).toBeInstanceOf(RRText); expect(text.textContent).toEqual('text'); const comment = rrdom.createComment('comment'); expect(comment).toBeInstanceOf(RRComment); expect(comment.textContent).toEqual('comment'); const CDATA = rrdom.createCDATASection('data'); expect(CDATA).toBeInstanceOf(RRCDATASection); expect(CDATA.data).toEqual('data'); }); it('can get head element', () => { expect(rrdom.head).toBeDefined(); expect(rrdom.head!.tagName).toBe('HEAD'); expect(rrdom.head!.parentElement).toBe(rrdom.documentElement); }); it('can get body element', () => { expect(rrdom.body).toBeDefined(); expect(rrdom.body!.tagName).toBe('BODY'); expect(rrdom.body!.parentElement).toBe(rrdom.documentElement); }); it('can get implementation', () => { expect(rrdom.implementation).toBeDefined(); expect(rrdom.implementation).toBe(rrdom); }); it('can insert elements', () => { expect(() => rrdom.insertBefore(rrdom.createDocumentType('', '', ''), null), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRDoctype on RRDocument allowed."`, ); expect(() => rrdom.insertBefore(rrdom.createElement('div'), null), ).toThrowErrorMatchingInlineSnapshot( `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRElement on RRDocument allowed."`, ); const node = new RRDocument(); const doctype = rrdom.createDocumentType('', '', ''); const documentElement = node.createElement('html'); node.insertBefore(documentElement, null); node.insertBefore(doctype, documentElement); expect(node.childNodes.length).toEqual(2); expect(node.childNodes[0]).toBe(doctype); expect(node.childNodes[1]).toBe(documentElement); expect(node.documentElement).toBe(documentElement); }); it('get firstElementChild', () => { expect(rrdom.firstElementChild).toBeDefined(); expect(rrdom.firstElementChild!.tagName).toEqual('HTML'); const div1 = rrdom.getElementById('block1'); expect(div1).toBeDefined(); expect(div1!.firstElementChild).toBeDefined(); expect(div1!.firstElementChild!.id).toEqual('block2'); const div2 = div1!.firstElementChild; expect(div2!.firstElementChild!.id).toEqual('block3'); }); it('getElementsByTagName', () => { for (let tagname of [ 'HTML', 'BODY', 'HEAD', 'STYLE', 'META', 'TITLE', 'SCRIPT', 'LINK', 'DIV', 'H1', 'P', 'BUTTON', 'IMG', 'CANVAS', 'FORM', 'INPUT', ]) { const expectedResult = document.getElementsByTagName(tagname).length; expect(rrdom.getElementsByTagName(tagname).length).toEqual( expectedResult, ); expect( rrdom.getElementsByTagName(tagname.toLowerCase()).length, ).toEqual(expectedResult); for (let node of rrdom.getElementsByTagName(tagname)) { expect(node.tagName).toEqual(tagname); } } const node = new RRDocument(); expect(node.getElementsByTagName('h2').length).toEqual(0); }); it('getElementsByClassName', () => { for (let className of [ 'blocks', 'blocks1', ':hover', 'blocks1 blocks', 'blocks blocks1', ':hover blocks1', ':hover blocks1 blocks', ':hover blocks1 block', ]) { const msg = `queried class name: '${className}'`; expect({ message: msg, result: rrdom.getElementsByClassName(className).length, }).toEqual({ message: msg, result: document.getElementsByClassName(className).length, }); } const node = new RRDocument(); expect(node.getElementsByClassName('block').length).toEqual(0); }); it('getElementById', () => { for (let elementId of ['block1', 'block2', 'block3']) { expect(rrdom.getElementById(elementId)).not.toBeNull(); expect(rrdom.getElementById(elementId)!.id).toEqual(elementId); } for (let elementId of ['block', 'blocks', 'blocks1']) expect(rrdom.getElementById(elementId)).toBeNull(); const node = new RRDocument(); expect(node.getElementById('id')).toBeNull(); }); it('querySelectorAll querying tag name', () => { expect(rrdom.querySelectorAll('H1')).toHaveLength(2); expect(rrdom.querySelectorAll('H1')[0]).toBeInstanceOf(RRElement); expect((rrdom.querySelectorAll('H1')[0] as RRElement).tagName).toEqual( 'H1', ); expect(rrdom.querySelectorAll('H1')[1]).toBeInstanceOf(RRElement); expect((rrdom.querySelectorAll('H1')[1] as RRElement).tagName).toEqual( 'H1', ); }); it('querySelectorAll querying class name', () => { for (let className of [ '.blocks', '.blocks1', '.\\:hover', '.blocks1.blocks', '.blocks.blocks1', '.\\:hover.blocks1', '.\\:hover.blocks1.blocks', '.\\:hover.blocks1.block', ]) { const msg = `queried class name: '${className}'`; expect({ message: msg, result: rrdom.querySelectorAll(className).length, }).toEqual({ message: msg, result: document.querySelectorAll(className).length, }); } for (let element of rrdom.querySelectorAll('.\\:hover')) { expect(element).toBeInstanceOf(RRElement); expect((element as RRElement).classList.classes).toContain(':hover'); } }); it('querySelectorAll querying id', () => { for (let query of ['#block1', '#block2', '#block3']) { expect(rrdom.querySelectorAll(query).length).toEqual(1); const targetElement = rrdom.querySelectorAll(query)[0] as RRElement; expect(targetElement.id).toEqual(query.substring(1, query.length)); } for (let query of ['#block', '#blocks', '#block1#block2']) expect(rrdom.querySelectorAll(query).length).toEqual(0); }); it('querySelectorAll', () => { expect(rrdom.querySelectorAll('link[rel="stylesheet"]').length).toEqual( 1, ); const targetLink = rrdom.querySelectorAll( 'link[rel="stylesheet"]', )[0] as RRElement; expect(targetLink.tagName).toEqual('LINK'); expect(targetLink.getAttribute('rel')).toEqual('stylesheet'); expect(rrdom.querySelectorAll('.blocks#block1').length).toEqual(1); expect(rrdom.querySelectorAll('.blocks#block3').length).toEqual(0); }); }); describe('RRElement API', () => { let rrdom: RRDocument; beforeAll(() => { // initialize rrdom document.write(getHtml('main.html')); rrdom = new RRDocument(); buildFromDom(document, undefined, rrdom); }); it('can get attribute', () => { expect( rrdom.getElementsByTagName('DIV')[0].getAttribute('class'), ).toEqual('blocks blocks1'); expect( rrdom.getElementsByTagName('dIv')[0].getAttribute('cLaSs'), ).toEqual('blocks blocks1'); expect(rrdom.getElementsByTagName('DIV')[0].getAttribute('id')).toEqual( 'block1', ); expect(rrdom.getElementsByTagName('div')[0].getAttribute('iD')).toEqual( 'block1', ); expect( rrdom.getElementsByTagName('p')[0].getAttribute('class'), ).toBeNull(); }); it('can set attribute', () => { const node = rrdom.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 remove attribute', () => { const node = rrdom.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('get nextElementSibling', () => { expect(rrdom.documentElement!.firstElementChild).not.toBeNull(); expect(rrdom.documentElement!.firstElementChild!.tagName).toEqual('HEAD'); expect( rrdom.documentElement!.firstElementChild!.nextElementSibling, ).not.toBeNull(); expect( rrdom.documentElement!.firstElementChild!.nextElementSibling!.tagName, ).toEqual('BODY'); expect( rrdom.documentElement!.firstElementChild!.nextElementSibling! .nextElementSibling, ).toBeNull(); expect(rrdom.getElementsByTagName('h1').length).toEqual(2); const element1 = rrdom.getElementsByTagName('h1')[0]; const element2 = rrdom.getElementsByTagName('h1')[1]; expect(element1.tagName).toEqual('H1'); expect(element2.tagName).toEqual('H1'); expect(element1.nextElementSibling).toEqual(element2); expect(element2.nextElementSibling).not.toBeNull(); expect(element2.nextElementSibling!.id).toEqual('block1'); expect(element2.nextElementSibling!.nextElementSibling).toBeNull(); const node = rrdom.createElement('div'); expect(node.nextElementSibling).toBeNull(); }); it('can get CSS style declaration', () => { const node = rrdom.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'); }); it('can set CSS property', () => { const node = rrdom.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 = rrdom.createElement('div'); node.attributes.style = 'color: blue; background-color: red; width: 78%; height: 50vh;'; const style = node.style; expect(style.removeProperty('color')).toEqual('blue'); expect(node.attributes.style).toEqual( 'background-color: red; width: 78%; height: 50vh;', ); expect(style.removeProperty('height')).toEqual('50vh'); 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 = rrdom.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;'; const style = node.style; expect(style.display).toEqual('inline-block'); expect(style.margin).toEqual('0px 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'; 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)', ); // comment node.attributes.style = 'top: 0; /* comment1 */ bottom: /* comment2 */42rem;'; expect(node.style.top).toEqual('0px'); expect(node.style.bottom).toEqual('42rem'); // empty comment node.attributes.style = 'top: /**/0;'; expect(node.style.top).toEqual('0px'); // incomplete node.attributes.style = 'overflow:'; expect(node.style.overflow).toEqual(''); }); it('querySelectorAll', () => { const element = rrdom.getElementById('block2')!; expect(element).toBeDefined(); expect(element.id).toEqual('block2'); const result = element.querySelectorAll('div'); expect(result.length).toBe(1); expect((result[0]! as RRElement).tagName).toEqual('DIV'); expect(element.querySelectorAll('.blocks').length).toEqual(0); const element2 = rrdom.getElementById('block1')!; expect(element2).toBeDefined(); expect(element2.id).toEqual('block1'); expect(element2.querySelectorAll('div').length).toEqual(2); expect(element2.querySelectorAll('.blocks').length).toEqual(1); }); it('can attach shadow dom', () => { const node = rrdom.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 insert new child before an existing child', () => { const node = rrdom.createElement('div'); const child1 = rrdom.createElement('h1'); const child2 = rrdom.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('style element', () => { expect(rrdom.getElementsByTagName('style').length).not.toEqual(0); expect(rrdom.getElementsByTagName('style')[0].tagName).toEqual('STYLE'); const styleElement = rrdom.getElementsByTagName( 'style', )[0] as RRStyleElement; expect(styleElement.sheet).toBeDefined(); expect(styleElement.sheet!.cssRules).toBeDefined(); expect(styleElement.sheet!.cssRules.length).toEqual(5); const rules = styleElement.sheet!.cssRules; expect(rules[0].cssText).toEqual(`h1 {color: 'black';}`); expect(rules[1].cssText).toEqual(`.blocks {padding: 0;}`); expect(rules[2].cssText).toEqual(`.blocks1 {margin: 0;}`); expect(rules[3].cssText).toEqual( `#block1 {width: 100px; height: 200px;}`, ); expect(rules[4].cssText).toEqual(`@import url(main.css);`); expect((rules[4] as CSSImportRule).href).toEqual('main.css'); expect(styleElement.sheet!.insertRule).toBeDefined(); const newRule = "p {color: 'black';}"; styleElement.sheet!.insertRule(newRule, 5); expect(rules[5].cssText).toEqual(newRule); expect(styleElement.sheet!.deleteRule).toBeDefined(); styleElement.sheet!.deleteRule(5); expect(rules[5]).toBeUndefined(); expect(rules[4].cssText).toEqual(`@import url(main.css);`); }); it('can create an RRIframeElement', () => { const iframe = rrdom.createElement('iframe'); expect(iframe.tagName).toEqual('IFRAME'); expect(iframe.width).toEqual(''); expect(iframe.height).toEqual(''); expect(iframe.contentDocument).toBeDefined(); expect(iframe.contentDocument!.childNodes.length).toBe(1); expect(iframe.contentDocument!.documentElement).toBeDefined(); expect(iframe.contentDocument!.head).toBeDefined(); expect(iframe.contentDocument!.body).toBeDefined(); expect(iframe.contentWindow).toBeDefined(); expect(iframe.contentWindow!.scrollTop).toEqual(0); expect(iframe.contentWindow!.scrollLeft).toEqual(0); expect(iframe.contentWindow!.scrollTo).toBeDefined(); // empty parameter and did nothing iframe.contentWindow!.scrollTo(); expect(iframe.contentWindow!.scrollTop).toEqual(0); expect(iframe.contentWindow!.scrollLeft).toEqual(0); iframe.contentWindow!.scrollTo({ top: 10, left: 20 }); expect(iframe.contentWindow!.scrollTop).toEqual(10); expect(iframe.contentWindow!.scrollLeft).toEqual(20); }); it('should have a RRCanvasElement', () => { const canvas = rrdom.createElement('canvas'); expect(canvas.getContext()).toBeNull(); }); }); }); function getHtml(fileName: string) { const filePath = path.resolve(__dirname, `../../rrdom/test/html/${fileName}`); return fs.readFileSync(filePath, 'utf8'); }