rrdom (#613)
* create rrdom package * test(rrdom): add unit tests for polyfill.ts * fix(rrweb snapshot): type check errors Errors are caused by the declaration similarity of @types/mocha and @types/jest if we install both of them in the whole project. * Set tagNames to upper case by default This mirrors the `Element.tagName` implementation: ``` For DOM trees which represent HTML documents, the returned tag name is always in the canonical upper-case form. For example, tagName called on a <div> element returns "DIV". ``` https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName * Add workspace file * VSCode settings for rrdom tests * Add basic test for RRDocument * Only setup jest tests for rrdom * mock Node type and Event type for nodejs environment * test(rrdom): add snapshot for document.test.ts * fix issue of nwsapi import and add unit tests for rrdom * fix: querySelectorAll returns nothing when querying elements with ids and classNames * fix: error of unit test for Event polyfill Since Event class is built in nodejs after v15.0.0 * add a dummy implementation of canvas * add style element support * add unit test for style element Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
// 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\\"
|
||||
"
|
||||
`;
|
||||
259
packages/rrdom/test/document-nodejs.test.ts
Normal file
259
packages/rrdom/test/document-nodejs.test.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { RRDocument, RRElement, RRStyleElement } from '../src/document-nodejs';
|
||||
import { printRRDom } from './util';
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('get className', () => {
|
||||
expect(rrdom.getElementsByTagName('DIV')[0].className).toEqual(
|
||||
'blocks blocks1',
|
||||
);
|
||||
expect(rrdom.getElementsByTagName('DIV')[1].className).toEqual(
|
||||
'blocks blocks1 :hover',
|
||||
);
|
||||
});
|
||||
|
||||
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('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',
|
||||
);
|
||||
expect(rrdom.getElementsByTagName('div')[0].getAttribute('iD')).toEqual(
|
||||
'block1',
|
||||
);
|
||||
expect(
|
||||
rrdom.getElementsByTagName('p')[0].getAttribute('class'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('get firstElementChild', () => {
|
||||
expect(rrdom.firstElementChild).toBeDefined();
|
||||
expect(rrdom.firstElementChild.tagName).toEqual('HTML');
|
||||
|
||||
const div1 = rrdom.getElementById('block1');
|
||||
expect(div1).toBeDefined();
|
||||
expect(div1!.firstElementChild).toBeDefined();
|
||||
expect(div1!.firstElementChild!.id).toEqual('block2');
|
||||
const div2 = div1!.firstElementChild;
|
||||
expect(div2!.firstElementChild!.id).toEqual('block3');
|
||||
});
|
||||
|
||||
it('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',
|
||||
'BODY',
|
||||
'HEAD',
|
||||
'STYLE',
|
||||
'META',
|
||||
'TITLE',
|
||||
'SCRIPT',
|
||||
'LINK',
|
||||
'DIV',
|
||||
'H1',
|
||||
'P',
|
||||
'BUTTON',
|
||||
'IMG',
|
||||
'CANVAS',
|
||||
]) {
|
||||
const expectedResult = document.getElementsByTagName(tagname).length;
|
||||
expect(rrdom.getElementsByTagName(tagname).length).toEqual(
|
||||
expectedResult,
|
||||
);
|
||||
expect(
|
||||
rrdom.getElementsByTagName(tagname.toLowerCase()).length,
|
||||
).toEqual(expectedResult);
|
||||
for (let node of rrdom.getElementsByTagName(tagname)) {
|
||||
expect(node.tagName).toEqual(tagname);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('getElementsByClassName', () => {
|
||||
for (let className of [
|
||||
'blocks',
|
||||
'blocks1',
|
||||
':hover',
|
||||
'blocks1 blocks',
|
||||
'blocks blocks1',
|
||||
':hover blocks1',
|
||||
':hover blocks1 blocks',
|
||||
':hover blocks1 block',
|
||||
]) {
|
||||
const msg = `queried class name: '${className}'`;
|
||||
expect({
|
||||
message: msg,
|
||||
result: rrdom.getElementsByClassName(className).length,
|
||||
}).toEqual({
|
||||
message: msg,
|
||||
result: document.getElementsByClassName(className).length,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('getElementById', () => {
|
||||
for (let elementId of ['block1', 'block2', 'block3']) {
|
||||
expect(rrdom.getElementById(elementId)).not.toBeNull();
|
||||
expect(rrdom.getElementById(elementId)!.id).toEqual(elementId);
|
||||
}
|
||||
for (let elementId of ['block', 'blocks', 'blocks1'])
|
||||
expect(rrdom.getElementById(elementId)).toBeNull();
|
||||
});
|
||||
|
||||
it('querySelectorAll querying tag name', () => {
|
||||
expect(rrdom.querySelectorAll('H1')).toHaveLength(2);
|
||||
expect(rrdom.querySelectorAll('H1')[0]).toBeInstanceOf(RRElement);
|
||||
expect((rrdom.querySelectorAll('H1')[0] as RRElement).tagName).toEqual(
|
||||
'H1',
|
||||
);
|
||||
expect(rrdom.querySelectorAll('H1')[1]).toBeInstanceOf(RRElement);
|
||||
expect((rrdom.querySelectorAll('H1')[1] as RRElement).tagName).toEqual(
|
||||
'H1',
|
||||
);
|
||||
});
|
||||
|
||||
it('querySelectorAll querying class name', () => {
|
||||
for (let className of [
|
||||
'.blocks',
|
||||
'.blocks1',
|
||||
'.\\:hover',
|
||||
'.blocks1.blocks',
|
||||
'.blocks.blocks1',
|
||||
'.\\:hover.blocks1',
|
||||
'.\\:hover.blocks1.blocks',
|
||||
'.\\:hover.blocks1.block',
|
||||
]) {
|
||||
const msg = `queried class name: '${className}'`;
|
||||
expect({
|
||||
message: msg,
|
||||
result: rrdom.querySelectorAll(className).length,
|
||||
}).toEqual({
|
||||
message: msg,
|
||||
result: document.querySelectorAll(className).length,
|
||||
});
|
||||
}
|
||||
for (let element of rrdom.querySelectorAll('.\\:hover')) {
|
||||
expect(element).toBeInstanceOf(RRElement);
|
||||
expect((element as RRElement).classList).toContain(':hover');
|
||||
}
|
||||
});
|
||||
|
||||
it('querySelectorAll querying id', () => {
|
||||
for (let query of ['#block1', '#block2', '#block3']) {
|
||||
expect(rrdom.querySelectorAll(query).length).toEqual(1);
|
||||
const targetElement = rrdom.querySelectorAll(query)[0] as RRElement;
|
||||
expect(targetElement.id).toEqual(query.substring(1, query.length));
|
||||
}
|
||||
for (let query of ['#block', '#blocks', '#block1#block2'])
|
||||
expect(rrdom.querySelectorAll(query).length).toEqual(0);
|
||||
});
|
||||
|
||||
it('querySelectorAll', () => {
|
||||
expect(rrdom.querySelectorAll('link[rel="stylesheet"]').length).toEqual(
|
||||
1,
|
||||
);
|
||||
const targetLink = rrdom.querySelectorAll(
|
||||
'link[rel="stylesheet"]',
|
||||
)[0] as RRElement;
|
||||
expect(targetLink.tagName).toEqual('LINK');
|
||||
expect(targetLink.getAttribute('rel')).toEqual('stylesheet');
|
||||
|
||||
expect(rrdom.querySelectorAll('.blocks#block1').length).toEqual(1);
|
||||
expect(rrdom.querySelectorAll('.blocks#block3').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('style element', () => {
|
||||
expect(rrdom.getElementsByTagName('style').length).not.toEqual(0);
|
||||
expect(rrdom.getElementsByTagName('style')[0].tagName).toEqual('STYLE');
|
||||
const styleElement = rrdom.getElementsByTagName(
|
||||
'style',
|
||||
)[0] as RRStyleElement;
|
||||
expect(styleElement.sheet).toBeDefined();
|
||||
expect(styleElement.sheet!.cssRules).toBeDefined();
|
||||
expect(styleElement.sheet!.cssRules.length).toEqual(5);
|
||||
const rules = styleElement.sheet!.cssRules;
|
||||
expect(rules[0].cssText).toEqual(`h1 {color: 'black';}`);
|
||||
expect(rules[1].cssText).toEqual(`.blocks {padding: 0;}`);
|
||||
expect(rules[2].cssText).toEqual(`.blocks1 {margin: 0;}`);
|
||||
expect(rules[3].cssText).toEqual(
|
||||
`#block1 {width: 100px; height: 200px;}`,
|
||||
);
|
||||
expect(rules[4].cssText).toEqual(`@import url(main.css);`);
|
||||
expect((rules[4] as CSSImportRule).href).toEqual('main.css');
|
||||
|
||||
expect(styleElement.sheet!.insertRule).toBeDefined();
|
||||
const newRule = "p {color: 'black';}";
|
||||
styleElement.sheet!.insertRule(newRule, 5);
|
||||
expect(rules[5].cssText).toEqual(newRule);
|
||||
|
||||
expect(styleElement.sheet!.deleteRule).toBeDefined();
|
||||
styleElement.sheet!.deleteRule(5);
|
||||
expect(rules[5]).toBeUndefined();
|
||||
expect(rules[4].cssText).toEqual(`@import url(main.css);`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getHtml(fileName: string) {
|
||||
const filePath = path.resolve(__dirname, `./html/${fileName}`);
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
40
packages/rrdom/test/html/main.html
Normal file
40
packages/rrdom/test/html/main.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Main</title>
|
||||
<link rel="stylesheet" href="somelink">
|
||||
<style>
|
||||
h1 {
|
||||
color: 'black';
|
||||
}
|
||||
.blocks {
|
||||
padding: 0;
|
||||
}
|
||||
.blocks1 {
|
||||
margin: 0;
|
||||
}
|
||||
#block1 {
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
}
|
||||
@import url("main.css");
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>This is a h1 heading</h1>
|
||||
<h1 style="font-size: 16px">This is a h1 heading with styles</h1>
|
||||
<div id="block1" class="blocks blocks1">
|
||||
<div id="block2" class="blocks blocks1 :hover">
|
||||
Text 1
|
||||
<div id="block3">
|
||||
<p>This is a paragraph</p>
|
||||
<button>button1</button>
|
||||
</div>
|
||||
Text 2
|
||||
</div>
|
||||
<img src="somelink" alt="This is an image" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
83
packages/rrdom/test/polyfill.test.ts
Normal file
83
packages/rrdom/test/polyfill.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { RRDocument, RRNode } from '../src/document-nodejs';
|
||||
import {
|
||||
polyfillPerformance,
|
||||
polyfillRAF,
|
||||
polyfillEvent,
|
||||
polyfillNode,
|
||||
polyfillDocument,
|
||||
} from '../src/polyfill';
|
||||
|
||||
describe('polyfill for nodejs', () => {
|
||||
it('should polyfill performance api', () => {
|
||||
expect(global.performance).toBeUndefined();
|
||||
polyfillPerformance();
|
||||
expect(global.performance).toBeDefined();
|
||||
expect(performance).toBeDefined();
|
||||
expect(performance.now).toBeDefined();
|
||||
expect(performance.now()).toBeCloseTo(
|
||||
require('perf_hooks').performance.now(),
|
||||
1e-10,
|
||||
);
|
||||
});
|
||||
|
||||
it('should polyfill requestAnimationFrame', () => {
|
||||
expect(global.requestAnimationFrame).toBeUndefined();
|
||||
expect(global.cancelAnimationFrame).toBeUndefined();
|
||||
polyfillRAF();
|
||||
expect(global.requestAnimationFrame).toBeDefined();
|
||||
expect(global.cancelAnimationFrame).toBeDefined();
|
||||
expect(requestAnimationFrame).toBeDefined();
|
||||
expect(cancelAnimationFrame).toBeDefined();
|
||||
|
||||
jest.useFakeTimers();
|
||||
const AnimationTime = 1_000; // target animation time(unit: ms)
|
||||
const startTime = Date.now();
|
||||
let frameCount = 0;
|
||||
const rafCallback1 = () => {
|
||||
const currentTime = Date.now();
|
||||
frameCount++;
|
||||
if (currentTime - startTime < AnimationTime) {
|
||||
requestAnimationFrame(rafCallback1);
|
||||
} else {
|
||||
expect(frameCount).toBeGreaterThanOrEqual(55);
|
||||
expect(frameCount).toBeLessThanOrEqual(65);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(rafCallback1);
|
||||
// Fast-forward until all timers have been executed
|
||||
jest.runAllTimers();
|
||||
|
||||
let rafHandle;
|
||||
const rafCallback2 = () => {
|
||||
rafHandle = requestAnimationFrame(rafCallback2);
|
||||
};
|
||||
rafHandle = requestAnimationFrame(rafCallback2);
|
||||
|
||||
// If this function doesn't work, recursive function will never end.
|
||||
cancelAnimationFrame(rafHandle);
|
||||
jest.runAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should polyfill Event type', () => {
|
||||
polyfillEvent();
|
||||
expect(global.Event).toBeDefined();
|
||||
expect(Event).toBeDefined();
|
||||
});
|
||||
|
||||
it('should polyfill Node type', () => {
|
||||
expect(global.Node).toBeUndefined();
|
||||
polyfillNode();
|
||||
expect(global.Node).toBeDefined();
|
||||
expect(Node).toBeDefined();
|
||||
expect(Node).toEqual(RRNode);
|
||||
});
|
||||
|
||||
it('should polyfill document object', () => {
|
||||
expect(global.document).toBeUndefined();
|
||||
polyfillDocument();
|
||||
expect(global.document).toBeDefined();
|
||||
expect(document).toBeDefined();
|
||||
expect(document).toBeInstanceOf(RRDocument);
|
||||
});
|
||||
});
|
||||
19
packages/rrdom/test/util.ts
Normal file
19
packages/rrdom/test/util.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user