* rrdom: add a diff function for properties * implement diffChildren function and unit tests * finish basic functions of diff algorithm * fix several bugs in the diff algorithm * replace the virtual parent optimization in applyMutation() * fix: moveAndHover after the diff algorithm is executed * replace virtual style map with rrdom cssom version has to be above 0.5.0 to pass virtual style tests * fix: failed virtual style tests in replayer.test.ts * fix: failed polyfill tests caused by nodejs compatibility of different versions * fix: svg viewBox attribute doesn't work Cause the attribute viewBox is case sensitive, set value for viewbox doesn't work * feat: replace treeIndex optimization with rrdom * fix bug of diffProps and disable smooth scrolling animation in fast-forward mode * feat: add iframe support * fix: @rollup/plugin-typescript build errors in rrweb-player Error: @rollup/plugin-typescript TS1371: This import is never used as a value and must use 'import type' because the 'importsNotUsedAsValues' is set to 'error' * fix: bug when fast-forward input events and add test for it * add test for fast-forward scroll events * fix: custom style rules don't get inserted into some iframe elements * code style tweak * fix: enable to diff iframe elements * fix the jest error "Unexpected token 'export'" * try to fix build error of rrweb-player * correct the attributes definition in rrdom * fix: custom style rules are not inserted in some iframes * add support for shadow dom * add support for MediaInteraction * add canvas support * fix unit test error in rrdom * add support for Text, Comment * try to refactor RRDom * refactor RRDom to reduce duplicate code * rename document-browser to virtual-dom * increase the test coverage for document.ts and add ownerDocument for it * Merge branch 'master' into virtual-dom * add more test for virtual-dom.ts * use cssstyle in document-nodejs * fix: bundle error * improve document-nodejs * enable to diff scroll positions of an element * rename rrdom to virtualDom for more readability and make the tree public * revert unknown change * improve the css style parser for comments * improve code style * update typings * add handling for the case where legacy_missingNodeRetryMap is not empty * only import types from rrweb into rrdom * Apply suggestions from code review Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com> * Apply suggestions from code review * fix building error in rrweb * add a method setDefaultSN to set a default value for a RRNode's __sn * fix rrweb test error and bump up other packages * add support for custom property of css styles * add a switch for virtual-dom optimization * Apply suggestions from code review 1. add an enum type for NodeType 2. rename nodeType from rrweb-snapshot to RRNodeType 3. rename notSerializedId to unserializedId 4. add comments for some confusing variables * adapt changes of #865 to virtual-dom and improve the test case for more coverage * apply review suggestions https://github.com/rrweb-io/rrweb/pull/853#pullrequestreview-922474953 * tweak the diff algorithm * add description of the flag useVirtualDom and remove outdated logConfig * Remove console.log * Contain changes to document * Upgrade rollup to 2.70.2 * Revert "Upgrade rollup to 2.70.2" This reverts commit b1be81a2a76565935c9dc391f31beb7f64d25956. * Fix type checking rrdom * Fix typing error while bundling * Fix tslib error on build Rollup would output the following error: `semantic error TS2343: This syntax requires an imported helper named '__spreadArray' which does not exist in 'tslib'. Consider upgrading your version of 'tslib'.` * Increase memory limit for rollup * Use esbuild for bundling Speeds up bundling significantly * Avoid circular dependencies and import un-bundled rrdom * Fix imports * Revert back to pre-esbuild This reverts the following commits: b7b3c8dbaa551a0129da1477136b1baaad28e6e1 72e23b8e27f9030d911358d3a17fe5ad1b3b5d4f 85d600a20c56cfa764cf1f858932ba14e67b1d23 61e1a5d323212ca8fbe0569e0b3062ddd53fc612 * Set node to lts (12 is no longer supported) * Speed up bundling and use less memory This fixes the out of memory errors happening while bundling * remove __sn from rrdom * fix typo * test: add a test case for StyleSheet mutation exceptions while fast-forwarding * rename Array.prototype.slice.call() to Array.from() * improve test cases * fix: PR #887 in 'virtual-dom' branch * apply justin's suggestion on 'Array.from' refactor related commit 0f6729d27a323260b36fbe79485a86715c0bc98a * improve import code structure Co-authored-by: Yun Feng <yun.feng@anu.edu.au>
This commit is contained in:
@@ -1,48 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RRDocument for nodejs environment buildFromDom should create an RRDocument from a html document 1`] = `
|
||||
"-1 RRDocument
|
||||
-2 RRDocumentType
|
||||
-3 HTML lang=\\"en\\"
|
||||
-4 HEAD
|
||||
-5 RRText text=\\"\\\\n \\"
|
||||
-6 META charset=\\"UTF-8\\"
|
||||
-7 RRText text=\\"\\\\n \\"
|
||||
-8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\"
|
||||
-9 RRText text=\\"\\\\n \\"
|
||||
-10 TITLE
|
||||
-11 RRText text=\\"Main\\"
|
||||
-12 RRText text=\\"\\\\n \\"
|
||||
-13 LINK rel=\\"stylesheet\\" href=\\"somelink\\"
|
||||
-14 RRText text=\\"\\\\n \\"
|
||||
-15 STYLE
|
||||
-16 RRText text=\\"\\\\n h1 {\\\\n color: 'black';\\\\n }\\\\n .blocks {\\\\n padding: 0;\\\\n }\\\\n .blocks1 {\\\\n margin: 0;\\\\n }\\\\n #block1 {\\\\n width: 100px;\\\\n height: 200px;\\\\n }\\\\n @import url(\\\\\\"main.css\\\\\\");\\\\n \\"
|
||||
-17 RRText text=\\"\\\\n \\"
|
||||
-18 RRText text=\\"\\\\n \\"
|
||||
-19 BODY
|
||||
-20 RRText text=\\"\\\\n \\"
|
||||
-21 H1
|
||||
-22 RRText text=\\"This is a h1 heading\\"
|
||||
-23 RRText text=\\"\\\\n \\"
|
||||
-24 H1 style=\\"font-size: 16px\\"
|
||||
-25 RRText text=\\"This is a h1 heading with styles\\"
|
||||
-26 RRText text=\\"\\\\n \\"
|
||||
-27 DIV id=\\"block1\\" class=\\"blocks blocks1\\"
|
||||
-28 RRText text=\\"\\\\n \\"
|
||||
-29 DIV id=\\"block2\\" class=\\"blocks blocks1 :hover\\"
|
||||
-30 RRText text=\\"\\\\n Text 1\\\\n \\"
|
||||
-31 DIV id=\\"block3\\"
|
||||
-32 RRText text=\\"\\\\n \\"
|
||||
-33 P
|
||||
-34 RRText text=\\"This is a paragraph\\"
|
||||
-35 RRText text=\\"\\\\n \\"
|
||||
-36 BUTTON
|
||||
-37 RRText text=\\"button1\\"
|
||||
-38 RRText text=\\"\\\\n \\"
|
||||
-39 RRText text=\\"\\\\n Text 2\\\\n \\"
|
||||
-40 RRText text=\\"\\\\n \\"
|
||||
-41 IMG src=\\"somelink\\" alt=\\"This is an image\\"
|
||||
-42 RRText text=\\"\\\\n \\"
|
||||
-43 RRText text=\\"\\\\n \\\\n\\\\n\\"
|
||||
"
|
||||
`;
|
||||
160
packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap
Normal file
160
packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap
Normal file
@@ -0,0 +1,160 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RRDocument for browser environment create a RRDocument from a html document can build from a common html 1`] = `
|
||||
"-1 RRDocument
|
||||
-2 RRDocumentType
|
||||
-3 HTML lang=\\"en\\"
|
||||
-4 HEAD
|
||||
-5 RRText text=\\"\\\\n \\"
|
||||
-6 META charset=\\"UTF-8\\"
|
||||
-7 RRText text=\\"\\\\n \\"
|
||||
-8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\"
|
||||
-9 RRText text=\\"\\\\n \\"
|
||||
-10 TITLE
|
||||
-11 RRText text=\\"Main\\"
|
||||
-12 RRText text=\\"\\\\n \\"
|
||||
-13 LINK rel=\\"stylesheet\\" href=\\"somelink\\"
|
||||
-14 RRText text=\\"\\\\n \\"
|
||||
-15 STYLE
|
||||
-16 RRText text=\\"\\\\n h1 {\\\\n color: 'black';\\\\n }\\\\n .blocks {\\\\n padding: 0;\\\\n }\\\\n .blocks1 {\\\\n margin: 0;\\\\n }\\\\n #block1 {\\\\n width: 100px;\\\\n height: 200px;\\\\n }\\\\n @import url('main.css');\\\\n \\"
|
||||
-17 RRText text=\\"\\\\n \\"
|
||||
-18 RRText text=\\"\\\\n \\"
|
||||
-19 BODY
|
||||
-20 RRText text=\\"\\\\n \\"
|
||||
-21 H1
|
||||
-22 RRText text=\\"This is a h1 heading\\"
|
||||
-23 RRText text=\\"\\\\n \\"
|
||||
-24 H1 style=\\"font-size: 16px\\"
|
||||
-25 RRText text=\\"This is a h1 heading with styles\\"
|
||||
-26 RRText text=\\"\\\\n \\"
|
||||
-27 DIV id=\\"block1\\" class=\\"blocks blocks1\\"
|
||||
-28 RRText text=\\"\\\\n \\"
|
||||
-29 DIV id=\\"block2\\" class=\\"blocks blocks1 :hover\\"
|
||||
-30 RRText text=\\"\\\\n Text 1\\\\n \\"
|
||||
-31 DIV id=\\"block3\\"
|
||||
-32 RRText text=\\"\\\\n \\"
|
||||
-33 P
|
||||
-34 RRText text=\\"This is a paragraph\\"
|
||||
-35 RRText text=\\"\\\\n \\"
|
||||
-36 BUTTON
|
||||
-37 RRText text=\\"button1\\"
|
||||
-38 RRText text=\\"\\\\n \\"
|
||||
-39 RRText text=\\"\\\\n Text 2\\\\n \\"
|
||||
-40 RRText text=\\"\\\\n \\"
|
||||
-41 IMG src=\\"somelink\\" alt=\\"This is an image\\"
|
||||
-42 RRText text=\\"\\\\n \\"
|
||||
-43 RRComment text=\\" This is a line of comment \\"
|
||||
-44 RRText text=\\"\\\\n \\"
|
||||
-45 FORM
|
||||
-46 RRText text=\\"\\\\n \\"
|
||||
-47 INPUT type=\\"text\\" id=\\"input1\\"
|
||||
-48 RRText text=\\"\\\\n \\"
|
||||
-49 RRText text=\\"\\\\n \\"
|
||||
-50 RRText text=\\"\\\\n \\\\n\\\\n\\"
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`RRDocument for browser environment create a RRDocument from a html document can build from a html containing nested shadow doms 1`] = `
|
||||
"-1 RRDocument
|
||||
-2 RRDocumentType
|
||||
-3 HTML lang=\\"en\\"
|
||||
-4 HEAD
|
||||
-5 RRText text=\\"\\\\n \\"
|
||||
-6 META charset=\\"UTF-8\\"
|
||||
-7 RRText text=\\"\\\\n \\"
|
||||
-8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\"
|
||||
-9 RRText text=\\"\\\\n \\"
|
||||
-10 TITLE
|
||||
-11 RRText text=\\"shadow dom\\"
|
||||
-12 RRText text=\\"\\\\n \\"
|
||||
-13 RRText text=\\"\\\\n \\"
|
||||
-14 BODY
|
||||
-15 RRText text=\\"\\\\n \\"
|
||||
-16 DIV
|
||||
-17 SHADOWROOT
|
||||
-18 RRText text=\\"\\\\n \\"
|
||||
-19 SPAN
|
||||
-20 RRText text=\\" shadow dom one \\"
|
||||
-21 RRText text=\\"\\\\n \\"
|
||||
-22 DIV
|
||||
-23 SHADOWROOT
|
||||
-24 RRText text=\\"\\\\n \\"
|
||||
-25 SPAN
|
||||
-26 RRText text=\\" shadow dom two \\"
|
||||
-27 RRText text=\\"\\\\n \\"
|
||||
-28 RRText text=\\"\\\\n \\\\n \\"
|
||||
-29 RRText text=\\"\\\\n \\"
|
||||
-30 RRText text=\\"\\\\n \\\\n \\"
|
||||
-31 RRText text=\\"\\\\n \\\\n\\\\n\\"
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`RRDocument for browser environment create a RRDocument from a html document can build from a xml page 1`] = `
|
||||
"-1 RRDocument
|
||||
-2 XML
|
||||
-3 RRCDATASection data=\\"Some <CDATA> data & then some\\"
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`RRDocument for browser environment create a RRDocument from a html document can build from an iframe html 1`] = `
|
||||
"-1 RRDocument
|
||||
-2 RRDocumentType
|
||||
-3 HTML lang=\\"en\\"
|
||||
-4 HEAD
|
||||
-5 RRText text=\\"\\\\n \\"
|
||||
-6 META charset=\\"UTF-8\\"
|
||||
-7 RRText text=\\"\\\\n \\"
|
||||
-8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\"
|
||||
-9 RRText text=\\"\\\\n \\"
|
||||
-10 TITLE
|
||||
-11 RRText text=\\"Iframe\\"
|
||||
-12 RRText text=\\"\\\\n \\"
|
||||
-13 RRText text=\\"\\\\n \\"
|
||||
-14 BODY
|
||||
-15 RRText text=\\"\\\\n \\"
|
||||
-16 IFRAME id=\\"iframe1\\" srcdoc=\\"
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='UTF-8' />
|
||||
<meta
|
||||
name='viewport'
|
||||
content='width=device-width, initial-scale=1.0'
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div>This is a block inside the iframe1.</div>
|
||||
<iframe id='iframe3' srcdoc='<div>This is a block inside the iframe3.</div>'>
|
||||
</body>
|
||||
</html>\\"
|
||||
-17 RRDocument
|
||||
-18 HTML
|
||||
-19 HEAD
|
||||
-20 RRText text=\\"\\\\n \\"
|
||||
-21 META charset=\\"UTF-8\\"
|
||||
-22 RRText text=\\"\\\\n \\"
|
||||
-23 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\"
|
||||
-24 RRText text=\\"\\\\n \\"
|
||||
-25 RRText text=\\"\\\\n \\"
|
||||
-26 BODY
|
||||
-27 RRText text=\\"\\\\n \\"
|
||||
-28 DIV
|
||||
-29 RRText text=\\"This is a block inside the iframe1.\\"
|
||||
-30 RRText text=\\"\\\\n \\"
|
||||
-31 IFRAME id=\\"iframe3\\" srcdoc=\\"<div>This is a block inside the iframe3.</div>\\"
|
||||
-32 RRDocument
|
||||
-33 HTML
|
||||
-34 HEAD
|
||||
-35 BODY
|
||||
-36 DIV
|
||||
-37 RRText text=\\"This is a block inside the iframe3.\\"
|
||||
-38 RRText text=\\"\\\\n \\"
|
||||
-39 IFRAME id=\\"iframe2\\" srcdoc=\\"<div>This is a block inside the iframe2.</div>\\"
|
||||
-40 RRDocument
|
||||
-41 HTML
|
||||
-42 HEAD
|
||||
-43 BODY
|
||||
-44 DIV
|
||||
-45 RRText text=\\"This is a block inside the iframe2.\\"
|
||||
-46 RRText text=\\"\\\\n \\\\n\\\\n\\"
|
||||
"
|
||||
`;
|
||||
1250
packages/rrdom/test/diff.test.ts
Normal file
1250
packages/rrdom/test/diff.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,67 +3,105 @@
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { RRDocument, RRElement, RRStyleElement } from '../src/document-nodejs';
|
||||
import { printRRDom } from './util';
|
||||
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 '../src/virtual-dom';
|
||||
|
||||
describe('RRDocument for nodejs environment', () => {
|
||||
describe('buildFromDom', () => {
|
||||
it('should create an RRDocument from a html document', () => {
|
||||
// setup document
|
||||
document.write(getHtml('main.html'));
|
||||
|
||||
// create RRDocument from document
|
||||
const rrdoc = new RRDocument();
|
||||
rrdoc.buildFromDom(document);
|
||||
expect(printRRDom(rrdoc)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RRDocument API', () => {
|
||||
let rrdom: RRDocument;
|
||||
beforeAll(() => {
|
||||
// initialize rrdom
|
||||
document.write(getHtml('main.html'));
|
||||
rrdom = new RRDocument();
|
||||
rrdom.buildFromDom(document);
|
||||
buildFromDom(document, undefined, rrdom);
|
||||
});
|
||||
|
||||
it('get className', () => {
|
||||
expect(rrdom.getElementsByTagName('DIV')[0].className).toEqual(
|
||||
'blocks blocks1',
|
||||
);
|
||||
expect(rrdom.getElementsByTagName('DIV')[1].className).toEqual(
|
||||
'blocks blocks1 :hover',
|
||||
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('get id', () => {
|
||||
expect(rrdom.getElementsByTagName('DIV')[0].id).toEqual('block1');
|
||||
expect(rrdom.getElementsByTagName('DIV')[1].id).toEqual('block2');
|
||||
expect(rrdom.getElementsByTagName('DIV')[2].id).toEqual('block3');
|
||||
it('can get head element', () => {
|
||||
expect(rrdom.head).toBeDefined();
|
||||
expect(rrdom.head!.tagName).toBe('HEAD');
|
||||
expect(rrdom.head!.parentElement).toBe(rrdom.documentElement);
|
||||
});
|
||||
|
||||
it('get attribute name', () => {
|
||||
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',
|
||||
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.getElementsByTagName('div')[0].getAttribute('iD')).toEqual(
|
||||
'block1',
|
||||
expect(() =>
|
||||
rrdom.insertBefore(rrdom.createElement('div'), null),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRElement on RRDocument allowed."`,
|
||||
);
|
||||
expect(
|
||||
rrdom.getElementsByTagName('p')[0].getAttribute('class'),
|
||||
).toBeNull();
|
||||
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');
|
||||
expect(rrdom.firstElementChild!.tagName).toEqual('HTML');
|
||||
|
||||
const div1 = rrdom.getElementById('block1');
|
||||
expect(div1).toBeDefined();
|
||||
@@ -73,31 +111,6 @@ describe('RRDocument for nodejs environment', () => {
|
||||
expect(div2!.firstElementChild!.id).toEqual('block3');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it('getElementsByTagName', () => {
|
||||
for (let tagname of [
|
||||
'HTML',
|
||||
@@ -114,6 +127,8 @@ describe('RRDocument for nodejs environment', () => {
|
||||
'BUTTON',
|
||||
'IMG',
|
||||
'CANVAS',
|
||||
'FORM',
|
||||
'INPUT',
|
||||
]) {
|
||||
const expectedResult = document.getElementsByTagName(tagname).length;
|
||||
expect(rrdom.getElementsByTagName(tagname).length).toEqual(
|
||||
@@ -126,6 +141,8 @@ describe('RRDocument for nodejs environment', () => {
|
||||
expect(node.tagName).toEqual(tagname);
|
||||
}
|
||||
}
|
||||
const node = new RRDocument();
|
||||
expect(node.getElementsByTagName('h2').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('getElementsByClassName', () => {
|
||||
@@ -148,6 +165,8 @@ describe('RRDocument for nodejs environment', () => {
|
||||
result: document.getElementsByClassName(className).length,
|
||||
});
|
||||
}
|
||||
const node = new RRDocument();
|
||||
expect(node.getElementsByClassName('block').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('getElementById', () => {
|
||||
@@ -157,6 +176,8 @@ describe('RRDocument for nodejs environment', () => {
|
||||
}
|
||||
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', () => {
|
||||
@@ -193,7 +214,7 @@ describe('RRDocument for nodejs environment', () => {
|
||||
}
|
||||
for (let element of rrdom.querySelectorAll('.\\:hover')) {
|
||||
expect(element).toBeInstanceOf(RRElement);
|
||||
expect((element as RRElement).classList).toContain(':hover');
|
||||
expect((element as RRElement).classList.classes).toContain(':hover');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -220,6 +241,243 @@ describe('RRDocument for nodejs environment', () => {
|
||||
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);
|
||||
@@ -250,6 +508,36 @@ describe('RRDocument for nodejs environment', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
922
packages/rrdom/test/document.test.ts
Normal file
922
packages/rrdom/test/document.test.ts
Normal file
@@ -0,0 +1,922 @@
|
||||
/**
|
||||
* @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).toBeUndefined();
|
||||
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(
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">',
|
||||
);
|
||||
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(
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "">',
|
||||
);
|
||||
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.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"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
31
packages/rrdom/test/html/iframe.html
Normal file
31
packages/rrdom/test/html/iframe.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Iframe</title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe
|
||||
id="iframe1"
|
||||
srcdoc="
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='UTF-8' />
|
||||
<meta
|
||||
name='viewport'
|
||||
content='width=device-width, initial-scale=1.0'
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div>This is a block inside the iframe1.</div>
|
||||
<iframe id='iframe3' srcdoc='<div>This is a block inside the iframe3.</div>'>
|
||||
</body>
|
||||
</html>"
|
||||
></iframe>
|
||||
<iframe
|
||||
id="iframe2"
|
||||
srcdoc="<div>This is a block inside the iframe2.</div>"
|
||||
></iframe>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Main</title>
|
||||
<link rel="stylesheet" href="somelink">
|
||||
<link rel="stylesheet" href="somelink" />
|
||||
<style>
|
||||
h1 {
|
||||
color: 'black';
|
||||
@@ -19,7 +19,7 @@
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
}
|
||||
@import url("main.css");
|
||||
@import url('main.css');
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -35,6 +35,10 @@
|
||||
Text 2
|
||||
</div>
|
||||
<img src="somelink" alt="This is an image" />
|
||||
<!-- This is a line of comment -->
|
||||
<form>
|
||||
<input type="text" id="input1" />
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
20
packages/rrdom/test/html/shadow-dom.html
Normal file
20
packages/rrdom/test/html/shadow-dom.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>shadow dom</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<template shadowroot="open">
|
||||
<span> shadow dom one </span>
|
||||
<div>
|
||||
<template shadowroot="open">
|
||||
<span> shadow dom two </span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,4 @@
|
||||
import { compare } from 'compare-versions';
|
||||
import { RRDocument, RRNode } from '../src/document-nodejs';
|
||||
import {
|
||||
polyfillPerformance,
|
||||
@@ -9,7 +10,8 @@ import {
|
||||
|
||||
describe('polyfill for nodejs', () => {
|
||||
it('should polyfill performance api', () => {
|
||||
expect(global.performance).toBeUndefined();
|
||||
if (compare(process.version, 'v16.0.0', '<'))
|
||||
expect(global.performance).toBeUndefined();
|
||||
polyfillPerformance();
|
||||
expect(global.performance).toBeDefined();
|
||||
expect(performance).toBeDefined();
|
||||
@@ -20,6 +22,18 @@ describe('polyfill for nodejs', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not polyfill performance if it already exists', () => {
|
||||
if (compare(process.version, 'v16.0.0', '>=')) {
|
||||
const originalPerformance = global.performance;
|
||||
polyfillPerformance();
|
||||
expect(global.performance).toBe(originalPerformance);
|
||||
}
|
||||
const fakePerformance = (jest.fn() as unknown) as Performance;
|
||||
global.performance = fakePerformance;
|
||||
polyfillPerformance();
|
||||
expect(global.performance).toEqual(fakePerformance);
|
||||
});
|
||||
|
||||
it('should polyfill requestAnimationFrame', () => {
|
||||
expect(global.requestAnimationFrame).toBeUndefined();
|
||||
expect(global.cancelAnimationFrame).toBeUndefined();
|
||||
@@ -59,12 +73,32 @@ describe('polyfill for nodejs', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not polyfill requestAnimationFrame if it already exists', () => {
|
||||
const fakeRequestAnimationFrame = (jest.fn() as unknown) as typeof global.requestAnimationFrame;
|
||||
global.requestAnimationFrame = fakeRequestAnimationFrame;
|
||||
const fakeCancelAnimationFrame = (jest.fn() as unknown) as typeof global.cancelAnimationFrame;
|
||||
global.cancelAnimationFrame = fakeCancelAnimationFrame;
|
||||
polyfillRAF();
|
||||
expect(global.requestAnimationFrame).toBe(fakeRequestAnimationFrame);
|
||||
expect(global.cancelAnimationFrame).toBe(fakeCancelAnimationFrame);
|
||||
});
|
||||
|
||||
it('should polyfill Event type', () => {
|
||||
// if the second version is greater
|
||||
if (compare(process.version, 'v15.0.0', '<'))
|
||||
expect(global.Event).toBeUndefined();
|
||||
polyfillEvent();
|
||||
expect(global.Event).toBeDefined();
|
||||
expect(Event).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not polyfill Event type if it already exists', () => {
|
||||
const fakeEvent = (jest.fn() as unknown) as typeof global.Event;
|
||||
global.Event = fakeEvent;
|
||||
polyfillEvent();
|
||||
expect(global.Event).toBe(fakeEvent);
|
||||
});
|
||||
|
||||
it('should polyfill Node type', () => {
|
||||
expect(global.Node).toBeUndefined();
|
||||
polyfillNode();
|
||||
@@ -73,6 +107,13 @@ describe('polyfill for nodejs', () => {
|
||||
expect(Node).toEqual(RRNode);
|
||||
});
|
||||
|
||||
it('should not polyfill Node type if it already exists', () => {
|
||||
const fakeNode = (jest.fn() as unknown) as typeof global.Node;
|
||||
global.Node = fakeNode;
|
||||
polyfillNode();
|
||||
expect(global.Node).toBe(fakeNode);
|
||||
});
|
||||
|
||||
it('should polyfill document object', () => {
|
||||
expect(global.document).toBeUndefined();
|
||||
polyfillDocument();
|
||||
@@ -80,4 +121,11 @@ describe('polyfill for nodejs', () => {
|
||||
expect(document).toBeDefined();
|
||||
expect(document).toBeInstanceOf(RRDocument);
|
||||
});
|
||||
|
||||
it('should not polyfill document object if it already exists', () => {
|
||||
const fakeDocument = (jest.fn() as unknown) as typeof global.document;
|
||||
global.document = fakeDocument;
|
||||
polyfillDocument();
|
||||
expect(global.document).toBe(fakeDocument);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { RRIframeElement, RRNode } from '../src/document-nodejs';
|
||||
|
||||
/**
|
||||
* Print the RRDom as a string.
|
||||
* @param rootNode the root node of the RRDom tree
|
||||
* @returns printed string
|
||||
*/
|
||||
export function printRRDom(rootNode: RRNode) {
|
||||
return walk(rootNode, '');
|
||||
}
|
||||
|
||||
function walk(node: RRNode, blankSpace: string) {
|
||||
let printText = `${blankSpace}${node.toString()}\n`;
|
||||
for (const child of node.childNodes)
|
||||
printText += walk(child, blankSpace + ' ');
|
||||
if (node instanceof RRIframeElement)
|
||||
printText += walk(node.contentDocument, blankSpace + ' ');
|
||||
return printText;
|
||||
}
|
||||
550
packages/rrdom/test/virtual-dom.test.ts
Normal file
550
packages/rrdom/test/virtual-dom.test.ts
Normal file
@@ -0,0 +1,550 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import * as rollup from 'rollup';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import * as typescript from 'rollup-plugin-typescript2';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import {
|
||||
cdataNode,
|
||||
commentNode,
|
||||
documentNode,
|
||||
documentTypeNode,
|
||||
elementNode,
|
||||
Mirror,
|
||||
NodeType,
|
||||
NodeType as RRNodeType,
|
||||
textNode,
|
||||
} from 'rrweb-snapshot';
|
||||
import {
|
||||
buildFromDom,
|
||||
buildFromNode,
|
||||
createMirror,
|
||||
getDefaultSN,
|
||||
RRCanvasElement,
|
||||
RRDocument,
|
||||
RRElement,
|
||||
RRNode,
|
||||
} from '../src/virtual-dom';
|
||||
|
||||
const _typescript = (typescript as unknown) as typeof typescript.default;
|
||||
const printRRDomCode = `
|
||||
/**
|
||||
* Print the RRDom as a string.
|
||||
* @param rootNode the root node of the RRDom tree
|
||||
* @returns printed string
|
||||
*/
|
||||
function printRRDom(rootNode, mirror) {
|
||||
return walk(rootNode, mirror, '');
|
||||
}
|
||||
function walk(node, mirror, blankSpace) {
|
||||
let printText = \`\${blankSpace}\${mirror.getId(node)} \${node.toString()}\n\`;
|
||||
if(node instanceof rrdom.RRElement && node.shadowRoot)
|
||||
printText += walk(node.shadowRoot, mirror, blankSpace + ' ');
|
||||
for (const child of node.childNodes)
|
||||
printText += walk(child, mirror, blankSpace + ' ');
|
||||
if (node instanceof rrdom.RRIFrameElement)
|
||||
printText += walk(node.contentDocument, mirror, blankSpace + ' ');
|
||||
return printText;
|
||||
}
|
||||
`;
|
||||
|
||||
describe('RRDocument for browser environment', () => {
|
||||
let mirror: Mirror;
|
||||
beforeEach(() => {
|
||||
mirror = new Mirror();
|
||||
});
|
||||
|
||||
describe('create a RRNode from a real Node', () => {
|
||||
it('should support quicksmode documents', () => {
|
||||
// seperate jsdom document as changes to the document would otherwise bleed into other tests
|
||||
const dom = new JSDOM();
|
||||
const document = dom.window.document;
|
||||
|
||||
expect(document.doctype).toBeNull(); // confirm compatMode is 'BackCompat' in JSDOM
|
||||
|
||||
const rrdom = new RRDocument();
|
||||
let rrNode = buildFromNode(document, rrdom, mirror)!;
|
||||
|
||||
expect((rrNode as RRDocument).compatMode).toBe('BackCompat');
|
||||
});
|
||||
|
||||
it('can patch serialized ID for an unserialized node', () => {
|
||||
// build from document
|
||||
expect(mirror.getMeta(document)).toBeNull();
|
||||
const rrdom = new RRDocument();
|
||||
let rrNode = buildFromNode(document, rrdom, mirror)!;
|
||||
expect(mirror.getMeta(document)).toBeDefined();
|
||||
expect(mirror.getId(document)).toEqual(-1);
|
||||
expect(rrNode).not.toBeNull();
|
||||
expect(rrdom.mirror.getMeta(rrNode)).toBeDefined();
|
||||
expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Document);
|
||||
expect(rrdom.mirror.getId(rrNode)).toEqual(-1);
|
||||
expect(rrNode).toBe(rrdom);
|
||||
|
||||
// build from document type
|
||||
expect(mirror.getMeta(document.doctype!)).toBeNull();
|
||||
rrNode = buildFromNode(document.doctype!, rrdom, mirror)!;
|
||||
expect(mirror.getMeta(document.doctype!)).toBeDefined();
|
||||
expect(mirror.getId(document.doctype)).toEqual(-2);
|
||||
expect(rrNode).not.toBeNull();
|
||||
expect(rrdom.mirror.getMeta(rrNode)).toBeDefined();
|
||||
expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(
|
||||
RRNodeType.DocumentType,
|
||||
);
|
||||
expect(rrdom.mirror.getId(rrNode)).toEqual(-2);
|
||||
|
||||
// build from element
|
||||
expect(mirror.getMeta(document.documentElement)).toBeNull();
|
||||
rrNode = buildFromNode(
|
||||
(document.documentElement as unknown) as Node,
|
||||
rrdom,
|
||||
mirror,
|
||||
)!;
|
||||
expect(mirror.getMeta(document.documentElement)).toBeDefined();
|
||||
expect(mirror.getId(document.documentElement)).toEqual(-3);
|
||||
expect(rrNode).not.toBeNull();
|
||||
expect(rrdom.mirror.getMeta(rrNode)).toBeDefined();
|
||||
expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Element);
|
||||
expect(rrdom.mirror.getId(rrNode)).toEqual(-3);
|
||||
|
||||
// build from text
|
||||
const text = document.createTextNode('text');
|
||||
expect(mirror.getMeta(text)).toBeNull();
|
||||
rrNode = buildFromNode(text, rrdom, mirror)!;
|
||||
expect(mirror.getMeta(text)).toBeDefined();
|
||||
expect(mirror.getId(text)).toEqual(-4);
|
||||
expect(rrNode).not.toBeNull();
|
||||
expect(rrdom.mirror.getMeta(rrNode)).toBeDefined();
|
||||
expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Text);
|
||||
expect(rrdom.mirror.getId(rrNode)).toEqual(-4);
|
||||
|
||||
// build from comment
|
||||
const comment = document.createComment('comment');
|
||||
expect(mirror.getMeta(comment)).toBeNull();
|
||||
rrNode = buildFromNode(comment, rrdom, mirror)!;
|
||||
expect(mirror.getMeta(comment)).toBeDefined();
|
||||
expect(mirror.getId(comment)).toEqual(-5);
|
||||
expect(rrNode).not.toBeNull();
|
||||
expect(rrdom.mirror.getMeta(rrNode)).toBeDefined();
|
||||
expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Comment);
|
||||
expect(rrdom.mirror.getId(rrNode)).toEqual(-5);
|
||||
|
||||
// build from CDATASection
|
||||
const xmlDoc = new DOMParser().parseFromString(
|
||||
'<xml></xml>',
|
||||
'application/xml',
|
||||
);
|
||||
const cdata = 'Some <CDATA> data & then some';
|
||||
var cdataSection = xmlDoc.createCDATASection(cdata);
|
||||
expect(mirror.getMeta(cdataSection)).toBeNull();
|
||||
expect(mirror.getMeta(cdataSection)).toBeNull();
|
||||
rrNode = buildFromNode(cdataSection, rrdom, mirror)!;
|
||||
expect(mirror.getMeta(cdataSection)).toBeDefined();
|
||||
expect(mirror.getId(cdataSection)).toEqual(-6);
|
||||
expect(rrNode).not.toBeNull();
|
||||
expect(rrdom.mirror.getMeta(rrNode)).toBeDefined();
|
||||
expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.CDATA);
|
||||
expect(rrdom.mirror.getId(rrNode)).toEqual(-6);
|
||||
expect(rrNode.textContent).toEqual(cdata);
|
||||
});
|
||||
|
||||
it('can record scroll position from HTMLElements', () => {
|
||||
expect(document.body.scrollLeft).toEqual(0);
|
||||
expect(document.body.scrollTop).toEqual(0);
|
||||
const rrdom = new RRDocument();
|
||||
let rrNode = buildFromNode(document.body, rrdom, mirror)!;
|
||||
expect((rrNode as RRElement).scrollLeft).toBeUndefined();
|
||||
expect((rrNode as RRElement).scrollTop).toBeUndefined();
|
||||
|
||||
document.body.scrollLeft = 100;
|
||||
document.body.scrollTop = 200;
|
||||
expect(document.body.scrollLeft).toEqual(100);
|
||||
expect(document.body.scrollTop).toEqual(200);
|
||||
rrNode = buildFromNode(document.body, rrdom, mirror)!;
|
||||
expect((rrNode as RRElement).scrollLeft).toEqual(100);
|
||||
expect((rrNode as RRElement).scrollTop).toEqual(200);
|
||||
});
|
||||
|
||||
it('can build contentDocument from an iframe element', () => {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
expect(iframe.contentDocument).not.toBeNull();
|
||||
const rrdom = new RRDocument();
|
||||
const RRIFrame = rrdom.createElement('iframe');
|
||||
const rrNode = buildFromNode(
|
||||
iframe.contentDocument!,
|
||||
rrdom,
|
||||
mirror,
|
||||
RRIFrame,
|
||||
)!;
|
||||
expect(rrNode).not.toBeNull();
|
||||
expect(rrdom.mirror.getMeta(rrNode)).toBeDefined();
|
||||
expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Document);
|
||||
expect(rrdom.mirror.getId(rrNode)).toEqual(-1);
|
||||
expect(mirror.getId(iframe.contentDocument)).toEqual(-1);
|
||||
expect(rrNode).toBe(RRIFrame.contentDocument);
|
||||
});
|
||||
|
||||
it('can build from a shadow dom', () => {
|
||||
const div = document.createElement('div');
|
||||
div.attachShadow({ mode: 'open' });
|
||||
expect(div.shadowRoot).toBeDefined();
|
||||
const rrdom = new RRDocument();
|
||||
const parentRRNode = rrdom.createElement('div');
|
||||
const rrNode = buildFromNode(
|
||||
div.shadowRoot!,
|
||||
rrdom,
|
||||
mirror,
|
||||
parentRRNode,
|
||||
)!;
|
||||
expect(rrNode).not.toBeNull();
|
||||
expect(rrdom.mirror.getMeta(rrNode)).toBeDefined();
|
||||
expect(rrdom.mirror.getId(rrNode)).toEqual(-1);
|
||||
expect(mirror.getId(div.shadowRoot)).toEqual(-1);
|
||||
expect(rrNode.RRNodeType).toEqual(RRNodeType.Element);
|
||||
expect((rrNode as RRElement).tagName).toEqual('SHADOWROOT');
|
||||
expect(rrNode).toBe(parentRRNode.shadowRoot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create a RRDocument from a html document', () => {
|
||||
let browser: puppeteer.Browser;
|
||||
let code: string;
|
||||
let page: puppeteer.Page;
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await puppeteer.launch();
|
||||
const bundle = await rollup.rollup({
|
||||
input: path.resolve(__dirname, '../src/virtual-dom.ts'),
|
||||
plugins: [
|
||||
resolve(),
|
||||
(_typescript({
|
||||
tsconfigOverride: { compilerOptions: { module: 'ESNext' } },
|
||||
}) as unknown) as rollup.Plugin,
|
||||
],
|
||||
});
|
||||
const {
|
||||
output: [{ code: _code }],
|
||||
} = await bundle.generate({
|
||||
name: 'rrdom',
|
||||
format: 'iife',
|
||||
});
|
||||
code = _code;
|
||||
});
|
||||
afterAll(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
page = await browser.newPage();
|
||||
await page.goto('about:blank');
|
||||
await page.evaluate(code + printRRDomCode);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await page.close();
|
||||
});
|
||||
it('can build from a common html', async () => {
|
||||
await page.setContent(getHtml('main.html'));
|
||||
const result = await page.evaluate(`
|
||||
const doc = new rrdom.RRDocument();
|
||||
rrdom.buildFromDom(document, undefined, doc);
|
||||
printRRDom(doc, doc.mirror);
|
||||
`);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('can build from an iframe html ', async () => {
|
||||
await page.setContent(getHtml('iframe.html'));
|
||||
const result = await page.evaluate(`
|
||||
const doc = new rrdom.RRDocument();
|
||||
rrdom.buildFromDom(document, undefined, doc);
|
||||
printRRDom(doc, doc.mirror);
|
||||
`);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('can build from a html containing nested shadow doms', async () => {
|
||||
await page.setContent(getHtml('shadow-dom.html'));
|
||||
const result = await page.evaluate(`
|
||||
const doc = new rrdom.RRDocument();
|
||||
rrdom.buildFromDom(document, undefined, doc);
|
||||
printRRDom(doc, doc.mirror);
|
||||
`);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('can build from a xml page', async () => {
|
||||
const result = await page.evaluate(`
|
||||
var docu = new DOMParser().parseFromString('<xml></xml>', 'application/xml');
|
||||
var cdata = docu.createCDATASection('Some <CDATA> data & then some');
|
||||
docu.getElementsByTagName('xml')[0].appendChild(cdata);
|
||||
// Displays: <xml><![CDATA[Some <CDATA> data & then some]]></xml>
|
||||
|
||||
const doc = new rrdom.RRDocument();
|
||||
rrdom.buildFromDom(docu, undefined, doc);
|
||||
printRRDom(doc, doc.mirror);
|
||||
`);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RRDocument build for virtual dom', () => {
|
||||
it('can access a unique, decremented unserializedId every time', () => {
|
||||
const node = new RRDocument();
|
||||
for (let i = 1; i <= 100; i++) expect(node.unserializedId).toBe(-i);
|
||||
});
|
||||
|
||||
it('can create a new RRDocument', () => {
|
||||
const dom = new RRDocument();
|
||||
const newDom = dom.createDocument('', '');
|
||||
expect(newDom).toBeInstanceOf(RRDocument);
|
||||
});
|
||||
|
||||
it('can create a new RRDocument receiving a mirror parameter', () => {
|
||||
const mirror = createMirror();
|
||||
const dom = new RRDocument(mirror);
|
||||
const newDom = dom.createDocument('', '');
|
||||
expect(newDom).toBeInstanceOf(RRDocument);
|
||||
expect(dom.mirror).toBe(mirror);
|
||||
});
|
||||
|
||||
it('can build a RRDocument from a real Dom', () => {
|
||||
const result = buildFromDom(document, mirror);
|
||||
expect(result.childNodes.length).toBe(2);
|
||||
expect(result.documentElement).toBeDefined();
|
||||
expect(result.head).toBeDefined();
|
||||
expect(result.head!.tagName).toBe('HEAD');
|
||||
expect(result.body).toBeDefined();
|
||||
expect(result.body!.tagName).toBe('BODY');
|
||||
});
|
||||
|
||||
it('can destroy a RRDocument tree', () => {
|
||||
const dom = new RRDocument();
|
||||
const node1 = dom.createDocumentType('', '', '');
|
||||
dom.appendChild(node1);
|
||||
dom.mirror.add(node1, {
|
||||
id: 0,
|
||||
type: NodeType.DocumentType,
|
||||
name: '',
|
||||
publicId: '',
|
||||
systemId: '',
|
||||
});
|
||||
const node2 = dom.createElement('html');
|
||||
dom.appendChild(node2);
|
||||
dom.mirror.add(node1, {
|
||||
id: 1,
|
||||
type: NodeType.Document,
|
||||
childNodes: [],
|
||||
});
|
||||
|
||||
expect(dom.childNodes.length).toEqual(2);
|
||||
expect(dom.mirror.has(0)).toBeTruthy();
|
||||
expect(dom.mirror.has(1)).toBeTruthy();
|
||||
|
||||
dom.destroyTree();
|
||||
expect(dom.childNodes.length).toEqual(0);
|
||||
expect(dom.mirror.has(0)).toBeFalsy();
|
||||
expect(dom.mirror.has(1)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can close and open a RRDocument', () => {
|
||||
const dom = new RRDocument();
|
||||
const documentType = dom.createDocumentType('html', '', '');
|
||||
dom.appendChild(documentType);
|
||||
expect(dom.childNodes[0]).toBe(documentType);
|
||||
expect(dom.unserializedId).toBe(-1);
|
||||
expect(dom.unserializedId).toBe(-2);
|
||||
expect(dom.close());
|
||||
expect(dom.open());
|
||||
expect(dom.childNodes.length).toEqual(0);
|
||||
expect(dom.unserializedId).toBe(-1);
|
||||
});
|
||||
|
||||
it('can execute a dummy getContext function in RRCanvasElement', () => {
|
||||
const canvas = new RRCanvasElement('CANVAS');
|
||||
expect(canvas.getContext).toBeDefined();
|
||||
expect(canvas.getContext()).toBeNull();
|
||||
});
|
||||
|
||||
describe('Mirror in the RRDocument', () => {
|
||||
it('should have a mirror to store id and node', () => {
|
||||
const dom = new RRDocument();
|
||||
expect(dom.mirror).toBeDefined();
|
||||
const node1 = dom.createElement('div');
|
||||
dom.mirror.add(node1, getDefaultSN(node1, 0));
|
||||
|
||||
const node2 = dom.createTextNode('text');
|
||||
dom.mirror.add(node2, getDefaultSN(node2, 1));
|
||||
|
||||
expect(dom.mirror.getNode(0)).toBe(node1);
|
||||
expect(dom.mirror.getNode(1)).toBe(node2);
|
||||
expect(dom.mirror.getNode(2)).toBeNull();
|
||||
expect(dom.mirror.getNode(-1)).toBeNull();
|
||||
});
|
||||
|
||||
it('can get node id', () => {
|
||||
const dom = new RRDocument();
|
||||
const node1 = dom.createElement('div');
|
||||
dom.mirror.add(node1, getDefaultSN(node1, 0));
|
||||
|
||||
expect(dom.mirror.getId(node1)).toEqual(0);
|
||||
const node2 = dom.createTextNode('text');
|
||||
expect(dom.mirror.getId(node2)).toEqual(-1);
|
||||
expect(dom.mirror.getId((null as unknown) as RRNode)).toEqual(-1);
|
||||
});
|
||||
|
||||
it('has() should return whether the mirror has an ID', () => {
|
||||
const dom = new RRDocument();
|
||||
const node1 = dom.createElement('div');
|
||||
dom.mirror.add(node1, getDefaultSN(node1, 0));
|
||||
const node2 = dom.createTextNode('text');
|
||||
dom.mirror.add(node2, getDefaultSN(node2, 1));
|
||||
expect(dom.mirror.has(0)).toBeTruthy();
|
||||
expect(dom.mirror.has(1)).toBeTruthy();
|
||||
expect(dom.mirror.has(2)).toBeFalsy();
|
||||
expect(dom.mirror.has(-1)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can remove node from the mirror', () => {
|
||||
const dom = new RRDocument();
|
||||
const node1 = dom.createElement('div');
|
||||
dom.mirror.add(node1, getDefaultSN(node1, 0));
|
||||
const node2 = dom.createTextNode('text');
|
||||
dom.mirror.add(node2, getDefaultSN(node2, 1));
|
||||
node1.appendChild(node2);
|
||||
expect(dom.mirror.has(0)).toBeTruthy();
|
||||
expect(dom.mirror.has(1)).toBeTruthy();
|
||||
dom.mirror.removeNodeFromMap(node2);
|
||||
expect(dom.mirror.has(0)).toBeTruthy();
|
||||
expect(dom.mirror.has(1)).toBeFalsy();
|
||||
|
||||
dom.mirror.add(node2, getDefaultSN(node2, 1));
|
||||
expect(dom.mirror.has(1)).toBeTruthy();
|
||||
// To remove node1 and its child node2 from the mirror.
|
||||
dom.mirror.removeNodeFromMap(node1);
|
||||
expect(dom.mirror.has(0)).toBeFalsy();
|
||||
expect(dom.mirror.has(1)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can reset the mirror', () => {
|
||||
const dom = new RRDocument();
|
||||
const node1 = dom.createElement('div');
|
||||
dom.mirror.add(node1, getDefaultSN(node1, 0));
|
||||
const node2 = dom.createTextNode('text');
|
||||
dom.mirror.add(node2, getDefaultSN(node2, 1));
|
||||
expect(dom.mirror.has(0)).toBeTruthy();
|
||||
expect(dom.mirror.has(1)).toBeTruthy();
|
||||
|
||||
dom.mirror.reset();
|
||||
expect(dom.mirror.has(0)).toBeFalsy();
|
||||
expect(dom.mirror.has(1)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('hasNode() should return whether the mirror has a node', () => {
|
||||
const dom = new RRDocument();
|
||||
const node1 = dom.createElement('div');
|
||||
const node2 = dom.createTextNode('text');
|
||||
expect(dom.mirror.hasNode(node1)).toBeFalsy();
|
||||
dom.mirror.add(node1, getDefaultSN(node1, 0));
|
||||
expect(dom.mirror.hasNode(node1)).toBeTruthy();
|
||||
expect(dom.mirror.hasNode(node2)).toBeFalsy();
|
||||
dom.mirror.add(node2, getDefaultSN(node2, 1));
|
||||
expect(dom.mirror.hasNode(node2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can get all IDs from the mirror', () => {
|
||||
const dom = new RRDocument();
|
||||
expect(dom.mirror.getIds().length).toBe(0);
|
||||
const node1 = dom.createElement('div');
|
||||
dom.mirror.add(node1, getDefaultSN(node1, 0));
|
||||
const node2 = dom.createTextNode('text');
|
||||
dom.mirror.add(node2, getDefaultSN(node2, 1));
|
||||
expect(dom.mirror.getIds().length).toBe(2);
|
||||
expect(dom.mirror.getIds()).toStrictEqual([0, 1]);
|
||||
});
|
||||
|
||||
it('can replace nodes', () => {
|
||||
const dom = new RRDocument();
|
||||
expect(dom.mirror.getIds().length).toBe(0);
|
||||
const node1 = dom.createElement('div');
|
||||
dom.mirror.add(node1, getDefaultSN(node1, 0));
|
||||
expect(dom.mirror.getNode(0)).toBe(node1);
|
||||
const node2 = dom.createTextNode('text');
|
||||
dom.mirror.replace(0, node2);
|
||||
expect(dom.mirror.getNode(0)).toBe(node2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('can get default SN value from a RRNode', () => {
|
||||
const rrdom = new RRDocument();
|
||||
it('can get from RRDocument', () => {
|
||||
const node = rrdom;
|
||||
const sn = getDefaultSN(node, 1);
|
||||
expect(sn).toBeDefined();
|
||||
expect(sn.type).toEqual(RRNodeType.Document);
|
||||
expect((sn as documentNode).childNodes).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
it('can get from RRDocumentType', () => {
|
||||
const name = 'name',
|
||||
publicId = 'publicId',
|
||||
systemId = 'systemId';
|
||||
const node = rrdom.createDocumentType(name, publicId, systemId);
|
||||
const sn = getDefaultSN(node, 1);
|
||||
|
||||
expect(sn).toBeDefined();
|
||||
expect(sn.type).toEqual(RRNodeType.DocumentType);
|
||||
expect((sn as documentTypeNode).name).toEqual(name);
|
||||
expect((sn as documentTypeNode).publicId).toEqual(publicId);
|
||||
expect((sn as documentTypeNode).systemId).toEqual(systemId);
|
||||
});
|
||||
|
||||
it('can get from RRElement', () => {
|
||||
const node = rrdom.createElement('div');
|
||||
const sn = getDefaultSN(node, 1);
|
||||
|
||||
expect(sn).toBeDefined();
|
||||
expect(sn.type).toEqual(RRNodeType.Element);
|
||||
expect((sn as elementNode).tagName).toEqual('div');
|
||||
expect((sn as elementNode).attributes).toBeDefined();
|
||||
expect((sn as elementNode).childNodes).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
it('can get from RRText', () => {
|
||||
const node = rrdom.createTextNode('text');
|
||||
const sn = getDefaultSN(node, 1);
|
||||
|
||||
expect(sn).toBeDefined();
|
||||
expect(sn.type).toEqual(RRNodeType.Text);
|
||||
expect((sn as textNode).textContent).toEqual('text');
|
||||
});
|
||||
|
||||
it('can get from RRComment', () => {
|
||||
const node = rrdom.createComment('comment');
|
||||
const sn = getDefaultSN(node, 1);
|
||||
|
||||
expect(sn).toBeDefined();
|
||||
expect(sn.type).toEqual(RRNodeType.Comment);
|
||||
expect((sn as commentNode).textContent).toEqual('comment');
|
||||
});
|
||||
|
||||
it('can get from RRCDATASection', () => {
|
||||
const node = rrdom.createCDATASection('data');
|
||||
const sn = getDefaultSN(node, 1);
|
||||
|
||||
expect(sn).toBeDefined();
|
||||
expect(sn.type).toEqual(RRNodeType.CDATA);
|
||||
expect((sn as cdataNode).textContent).toEqual('');
|
||||
});
|
||||
});
|
||||
});
|
||||
function getHtml(fileName: string) {
|
||||
const filePath = path.resolve(__dirname, `./html/${fileName}`);
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
Reference in New Issue
Block a user